From e2c2f9f00eebef24118674b0daa986e8ca2f25ad Mon Sep 17 00:00:00 2001 From: ginan-release-bot Date: Mon, 15 Dec 2025 23:52:47 +0000 Subject: [PATCH] Release v4.0.0 --- .github/workflows/build.yaml | 483 + CHANGELOG.md | 95 + Docs/announcements.md | 45 +- Docs/codingStandard.md | 285 +- Docs/ginanFAQ.html | 13 +- Docs/home.md | 7 + Docs/images/GinanGUI-screenshot.png | Bin 0 -> 212847 bytes .../GinanWorkshop2025WindowsSetUp-TN2.png | Bin 0 -> 31994 bytes Docs/images/ICRF-75pc-20250121.png | Bin 0 -> 131649 bytes Docs/images/ICRF-75pc.png | Bin 190887 -> 0 bytes Docs/images/UseCasesv04.jpg | Bin 0 -> 193772 bytes Docs/overview.md | 7 - Docs/resources.md | 9 +- ...n-Workshop--Windows-Set-up--2025-06-23.pdf | Bin 0 -> 137199 bytes Docs/scripts.index | 3 +- Docs/theory.md | 2 +- README.md | 525 +- debugConfigs/pod_rt_example1.yaml | 19 +- debugConfigs/record_ssr_stream.yaml | 27 +- debugConfigs/sp3_ecef2eci.yaml | 51 + docker/Dockerfile | 75 +- docker/ginan-env.Dockerfile | 97 +- docker/tags | 5 - exampleConfigs/LEO_dynPOD.yaml | 340 + exampleConfigs/LEO_kinPOD.yaml | 328 + exampleConfigs/brdc2sp3.yml | 97 +- exampleConfigs/compare_orbits.yaml | 84 +- exampleConfigs/fit_sp3_pseudoobs.yaml | 346 +- exampleConfigs/pod_example.yaml | 980 +- exampleConfigs/ppp_example.yaml | 543 +- exampleConfigs/record_snr.yaml | 12 +- exampleConfigs/record_streams.yaml | 182 +- exampleConfigs/rt_ppp_example.yaml | 505 +- exampleConfigs/rt_rtk_example.yaml | 343 +- exampleConfigs/rtk_example.yaml | 342 +- .../slr_pod_with_pseudoobs_gal.yaml | 431 +- .../slr_pod_with_pseudoobs_lag.yaml | 373 +- ginan.code-workspace | 8 + inputData/products/products.list | 30 +- scripts/GinanEDA/backend/data/measurements.py | 36 +- scripts/GinanEDA/backend/dbconnector/mongo.py | 5 +- scripts/GinanEDA/eda/routes/dbConnection.py | 7 +- scripts/GinanEDA/eda/routes/measurements.py | 119 +- scripts/GinanEDA/eda/routes/states.py | 121 +- scripts/GinanEDA/eda/routes/trace.py | 12 +- scripts/GinanEDA/eda/utilities.py | 45 +- .../templates/measurements_diff.jinja | 211 + scripts/GinanEDA/templates/sidebar.jinja | 18 +- scripts/GinanEDA/templates/states_diff.jinja | 238 + scripts/GinanUI/README.md | 9 + scripts/GinanUI/__init__.py | 0 scripts/GinanUI/app/__init__.py | 0 scripts/GinanUI/app/controllers/__init__.py | 0 .../app/controllers/input_controller.py | 1516 ++ .../controllers/visualisation_controller.py | 320 + scripts/GinanUI/app/main_window.py | 497 + scripts/GinanUI/app/models/__init__.py | 0 scripts/GinanUI/app/models/archive_manager.py | 161 + scripts/GinanUI/app/models/dl_products.py | 489 + scripts/GinanUI/app/models/execution.py | 456 + scripts/GinanUI/app/models/rinex_extractor.py | 323 + .../app/resources/Yaml/default_config.yaml | 303 + scripts/GinanUI/app/resources/__init__.py | 3 + scripts/GinanUI/app/resources/ginan_logo.qrc | 5 + .../GinanUI/app/resources/ginan_logo_rc.py | 249 + scripts/GinanUI/app/utils/__init__.py | 0 .../GinanUI/app/utils/cddis_credentials.py | 119 + scripts/GinanUI/app/utils/cddis_email.py | 220 + scripts/GinanUI/app/utils/common_dirs.py | 17 + .../{ => GinanUI/app/utils}/gn_functions.py | 1 - scripts/GinanUI/app/utils/logger.py | 98 + scripts/GinanUI/app/utils/toast.py | 198 + scripts/GinanUI/app/utils/ui_compilation.py | 48 + scripts/GinanUI/app/utils/workers.py | 128 + scripts/GinanUI/app/utils/yaml.py | 144 + scripts/GinanUI/app/views/__init__.py | 0 scripts/GinanUI/app/views/main_window.ui | 1171 ++ scripts/GinanUI/docs/USER_GUIDE.md | 566 + .../docs/images/cddis_credentials_button.jpg | Bin 0 -> 168480 bytes .../docs/images/cddis_credentials_screen.jpg | Bin 0 -> 10471 bytes .../docs/images/ginan_ui_dashboard.jpg | Bin 0 -> 161791 bytes scripts/GinanUI/docs/images/mode_dropdown.jpg | Bin 0 -> 8017 bytes .../images/observations_output_buttons.jpg | Bin 0 -> 161039 bytes .../GinanUI/docs/images/pea_processing.jpg | Bin 0 -> 39717 bytes .../docs/images/plot_visualisation.jpg | Bin 0 -> 175341 bytes .../docs/images/plot_visualisation_web.jpg | Bin 0 -> 244808 bytes .../GinanUI/docs/images/process_button.jpg | Bin 0 -> 219748 bytes .../docs/images/product_downloading.jpg | Bin 0 -> 218439 bytes scripts/GinanUI/main.py | 23 + scripts/GinanUI/requirements.txt | 10 + scripts/README.md | 232 + scripts/__init__.py | 0 scripts/auto_download_PPP.py | 517 +- .../auto_generate_yaml.py | 0 .../{ => deprecated_scripts}/auto_run_PPP.py | 0 .../compareGinanJson.py | 0 .../createAppimage.sh | 4 +- .../download_slr_data.py | 0 .../qzss_ohi_merge.py | 0 scripts/download_archives.py | 261 - scripts/download_example_input_data.py | 322 - scripts/formatting/fix_doxygen.py | 82 + scripts/formatting/reorganise_include.py | 180 + scripts/gn/templates/auto_template.yaml | 14 +- scripts/installation/apple.md | 47 +- scripts/installation/generic.md | 76 +- scripts/installation/ubuntu20.sh | 32 +- scripts/installation/ubuntu22.sh | 2 +- scripts/installation/ubuntu24.sh | 95 + scripts/plot_pos.py | 920 +- scripts/plot_trace_res.py | 2811 ++++ scripts/plotting/obs_code_plot.py | 6 +- scripts/plotting/ztd_plot.py | 4 +- scripts/requirements.txt | 8 +- scripts/ssrMonitoring/README.md | 9 +- scripts/ssrMonitoring/analyse_orbit_clock.py | 1336 +- .../ssrMonitoring/auto_record_ssr_streams.py | 180 +- scripts/ssrMonitoring/download_rt_products.py | 33 +- scripts/ssrMonitoring/kill_pids.py | 68 +- scripts/ssrMonitoring/record_ssr_stream.py | 296 +- scripts/ssrMonitoring/upload_recordings.py | 165 +- src/Architecture/architectureDocs.hpp | 5 + src/CMakeLists.txt | 205 +- src/CMakePresets.json | 302 + src/cmake/GitVersion.cmake | 57 + src/cmake/toolchain/clang_linux_x64.cmake | 6 + .../toolchain/clang_mac_arm64.cmake} | 0 src/cmake/toolchain/clang_mac_x64.cmake | 7 + src/cmake/toolchain/gcc_mac_arm64.cmake | 7 + src/cmake/toolchain/mingw64.cmake | 57 + src/cpp/3rdparty/enum.h | 1184 -- src/cpp/3rdparty/enum_macros.h | 581 - src/cpp/3rdparty/iers2010/ch9/fcul_a.cpp | 2 +- src/cpp/3rdparty/iers2010/ch9/fcul_zd_hpa.cpp | 2 +- .../dehanttideinel/dehanttide_all.cpp | 4 +- src/cpp/3rdparty/iers2010/hardisp/admint.cpp | 2 +- src/cpp/3rdparty/iers2010/hardisp/eval.cpp | 2 +- .../iers2010/hardisp/hardisp_impl.cpp | 2 +- src/cpp/3rdparty/iers2010/hardisp/recurs.cpp | 2 +- src/cpp/3rdparty/iers2010/hardisp/shells.cpp | 2 +- src/cpp/3rdparty/iers2010/hardisp/spline.cpp | 2 +- src/cpp/3rdparty/iers2010/hardisp/tdfrph.cpp | 2 +- src/cpp/3rdparty/iers2010/iers2010.hpp | 4 +- src/cpp/3rdparty/magic_enum.hpp | 1508 ++ src/cpp/3rdparty/nrlmsise/nrlmsise-00.cpp | 1459 ++ src/cpp/3rdparty/nrlmsise/nrlmsise-00.h | 224 + .../3rdparty/nrlmsise/nrlmsise-00_data.cpp | 740 + src/cpp/CMakeLists.txt | 252 +- src/cpp/ambres/GNSSambres.cpp | 1072 +- src/cpp/ambres/GNSSambres.hpp | 72 +- src/cpp/common/acsConfig.cpp | 11905 +++++++++++----- src/cpp/common/acsConfig.hpp | 2407 ++-- src/cpp/common/acsConfigDocs.cpp | 61 +- src/cpp/common/acsQC.cpp | 1944 +-- src/cpp/common/acsQC.hpp | 71 +- src/cpp/common/algebra.cpp | 5669 +++++--- src/cpp/common/algebra.hpp | 1575 +- src/cpp/common/algebraTrace.cpp | 339 +- src/cpp/common/algebraTrace.hpp | 378 +- src/cpp/common/algebra_old.cpp | 816 +- src/cpp/common/antenna.cpp | 1441 +- src/cpp/common/antenna.hpp | 108 +- src/cpp/common/api.cpp | 13 +- src/cpp/common/api.hpp | 4 +- src/cpp/common/attitude.cpp | 2850 ++-- src/cpp/common/attitude.hpp | 71 +- src/cpp/common/azElMapData.hpp | 25 +- src/cpp/common/biasSINEXread.cpp | 668 +- src/cpp/common/biasSINEXwrite.cpp | 1220 +- src/cpp/common/biases.cpp | 1064 +- src/cpp/common/biases.hpp | 233 +- src/cpp/common/cache.hpp | 60 +- src/cpp/common/common.cpp | 177 +- src/cpp/common/common.hpp | 393 +- src/cpp/common/compare.cpp | 545 +- src/cpp/common/compare.hpp | 11 +- src/cpp/common/constants.cpp | 761 +- src/cpp/common/constants.hpp | 271 +- src/cpp/common/cost.cpp | 665 +- src/cpp/common/cost.hpp | 13 +- src/cpp/common/customDecoder.cpp | 315 +- src/cpp/common/customDecoder.hpp | 242 +- src/cpp/common/debug.cpp | 3500 ++--- src/cpp/common/debug.hpp | 9 +- src/cpp/common/eigenIncluder.hpp | 231 +- src/cpp/common/enumHelpers.hpp | 143 + src/cpp/common/enums.h | 2637 ++-- src/cpp/common/ephBroadcast.cpp | 1140 +- src/cpp/common/ephKalman.cpp | 241 +- src/cpp/common/ephPrecise.cpp | 985 +- src/cpp/common/ephPrecise.hpp | 72 +- src/cpp/common/ephSBAS.cpp | 196 +- src/cpp/common/ephSSR.cpp | 861 +- src/cpp/common/ephemeris.cpp | 833 +- src/cpp/common/ephemeris.hpp | 813 +- src/cpp/common/erp.cpp | 1203 +- src/cpp/common/erp.hpp | 146 +- src/cpp/common/fileLog.cpp | 119 +- src/cpp/common/fileLog.hpp | 27 +- src/cpp/common/gTime.cpp | 1129 +- src/cpp/common/gTime.hpp | 916 +- src/cpp/common/gpx.cpp | 376 +- src/cpp/common/gpx.hpp | 8 +- src/cpp/common/icdDecoder.hpp | 606 +- src/cpp/common/interactiveTerminal.cpp | 483 - src/cpp/common/interactiveTerminal.hpp | 131 - src/cpp/common/ionModels.cpp | 861 +- src/cpp/common/ionModels.hpp | 57 +- src/cpp/common/kalmanBlas.cpp | 479 + src/cpp/common/kalmanBlas.hpp | 139 + src/cpp/common/lapackWrapper.hpp | 473 + src/cpp/common/linearCombo.cpp | 509 +- src/cpp/common/linearCombo.hpp | 92 +- src/cpp/common/localAtmosRegion.cpp | 880 +- src/cpp/common/metaData.hpp | 33 +- src/cpp/common/mongo.cpp | 352 +- src/cpp/common/mongo.hpp | 341 +- src/cpp/common/mongoRead.cpp | 1981 +-- src/cpp/common/mongoRead.hpp | 130 +- src/cpp/common/mongoWrite.cpp | 2641 ++-- src/cpp/common/mongoWrite.hpp | 196 +- src/cpp/common/navigation.hpp | 178 +- src/cpp/common/ntripBroadcast.cpp | 1337 +- src/cpp/common/ntripBroadcast.hpp | 163 +- src/cpp/common/ntripTrace.cpp | 358 +- src/cpp/common/ntripTrace.hpp | 52 +- src/cpp/common/observations.hpp | 589 +- src/cpp/common/orbex.cpp | 676 +- src/cpp/common/orbexWrite.cpp | 938 +- src/cpp/common/orbexWrite.hpp | 43 +- src/cpp/common/orbits.cpp | 769 +- src/cpp/common/orbits.hpp | 44 +- src/cpp/common/packetStatistics.hpp | 194 +- src/cpp/common/platformCompat.hpp | 92 + src/cpp/common/pos.cpp | 326 +- src/cpp/common/pos.hpp | 8 +- src/cpp/common/receiver.cpp | 111 +- src/cpp/common/receiver.hpp | 254 +- src/cpp/common/rinex.cpp | 4379 +++--- src/cpp/common/rinex.hpp | 870 +- src/cpp/common/rinexClkWrite.cpp | 927 +- src/cpp/common/rinexClkWrite.hpp | 27 +- src/cpp/common/rinexNavWrite.cpp | 2011 +-- src/cpp/common/rinexNavWrite.hpp | 6 +- src/cpp/common/rinexObsWrite.cpp | 929 +- src/cpp/common/rinexObsWrite.hpp | 31 +- src/cpp/common/rtcmDecoder.cpp | 3397 ++--- src/cpp/common/rtcmDecoder.hpp | 358 +- src/cpp/common/rtcmEncoder.cpp | 2601 ++-- src/cpp/common/rtcmEncoder.hpp | 150 +- src/cpp/common/rtcmTrace.cpp | 1294 +- src/cpp/common/rtcmTrace.hpp | 178 +- src/cpp/common/rtsSmoothing.cpp | 1753 ++- src/cpp/common/rtsSmoothing.hpp | 246 +- src/cpp/common/satStat.hpp | 119 +- src/cpp/common/satSys.cpp | 75 +- src/cpp/common/satSys.hpp | 410 +- src/cpp/common/sinex.cpp | 6865 ++++----- src/cpp/common/sinex.hpp | 855 +- src/cpp/common/sinexParser.cpp | 269 +- src/cpp/common/sinexParser.hpp | 396 +- src/cpp/common/sp3.cpp | 667 +- src/cpp/common/sp3Write.cpp | 824 +- src/cpp/common/sp3Write.hpp | 19 +- src/cpp/common/ssr.hpp | 410 +- src/cpp/common/streamCustom.cpp | 167 +- src/cpp/common/streamCustom.hpp | 19 +- src/cpp/common/streamFile.hpp | 156 +- src/cpp/common/streamNtrip.cpp | 44 +- src/cpp/common/streamNtrip.hpp | 74 +- src/cpp/common/streamObs.hpp | 412 +- src/cpp/common/streamParser.cpp | 81 +- src/cpp/common/streamParser.hpp | 167 +- src/cpp/common/streamRinex.hpp | 83 +- src/cpp/common/streamRtcm.hpp | 513 +- src/cpp/common/streamSerial.cpp | 39 +- src/cpp/common/streamSerial.hpp | 232 +- src/cpp/common/streamSlr.hpp | 62 +- src/cpp/common/streamSp3.hpp | 71 +- src/cpp/common/streamUbx.cpp | 152 +- src/cpp/common/streamUbx.hpp | 21 +- src/cpp/common/summary.cpp | 140 +- src/cpp/common/summary.hpp | 15 +- src/cpp/common/tcpSocket.cpp | 1005 +- src/cpp/common/tcpSocket.hpp | 564 +- src/cpp/common/testUtils.cpp | 267 +- src/cpp/common/testUtils.hpp | 37 +- src/cpp/common/tides.cpp | 1605 ++- src/cpp/common/tides.hpp | 156 +- src/cpp/common/trace.cpp | 441 +- src/cpp/common/trace.hpp | 323 +- src/cpp/common/tropSinex.cpp | 1347 +- src/cpp/common/ubxDecoder.cpp | 652 +- src/cpp/common/ubxDecoder.hpp | 248 +- src/cpp/common/walkthrough.cpp | 435 - ...erTemplate.html => htmlFooterTemplate.hpp} | 33 +- ...erTemplate.html => htmlHeaderTemplate.hpp} | 28 +- src/cpp/inertial/posProp.cpp | 821 +- src/cpp/inertial/posProp.hpp | 247 +- src/cpp/iono/geomagField.cpp | 511 +- src/cpp/iono/geomagField.hpp | 52 +- src/cpp/iono/ionex.cpp | 946 +- src/cpp/iono/ionexWrite.cpp | 486 +- src/cpp/iono/ionoBSplines.cpp | 337 +- src/cpp/iono/ionoLocalSTEC.cpp | 1017 +- src/cpp/iono/ionoMeas.cpp | 676 +- src/cpp/iono/ionoModel.cpp | 767 +- src/cpp/iono/ionoModel.hpp | 111 +- src/cpp/iono/ionoSpherical.cpp | 740 +- src/cpp/iono/ionoSphericalCaps.cpp | 739 +- src/cpp/loading/boost_ma_type.h | 9 +- src/cpp/loading/input_otl.h | 38 +- src/cpp/loading/interpolate_loading.cpp | 477 +- src/cpp/loading/load_functions.cpp | 391 +- src/cpp/loading/load_functions.h | 16 +- src/cpp/loading/loadgrid.cpp | 161 +- src/cpp/loading/loadgrid.h | 78 +- src/cpp/loading/loading.cpp | 144 +- src/cpp/loading/loading.h | 36 +- src/cpp/loading/make_otl_blq.cpp | 608 +- src/cpp/loading/tide.cpp | 167 +- src/cpp/loading/tide.h | 89 +- src/cpp/loading/utils.cpp | 96 +- src/cpp/loading/utils.h | 27 +- src/cpp/orbprop/acceleration.cpp | 311 +- src/cpp/orbprop/acceleration.hpp | 58 +- src/cpp/orbprop/aod.cpp | 196 +- src/cpp/orbprop/aod.hpp | 25 +- src/cpp/orbprop/boxwing.cpp | 203 +- src/cpp/orbprop/boxwing.hpp | 36 +- src/cpp/orbprop/centerMassCorrections.cpp | 123 +- src/cpp/orbprop/centerMassCorrections.hpp | 26 +- src/cpp/orbprop/coordinates.cpp | 464 +- src/cpp/orbprop/coordinates.hpp | 255 +- src/cpp/orbprop/iers2010.cpp | 642 +- src/cpp/orbprop/iers2010.hpp | 167 +- src/cpp/orbprop/oceanPoleTide.cpp | 110 +- src/cpp/orbprop/oceanPoleTide.hpp | 34 +- src/cpp/orbprop/orbitProp.cpp | 2425 ++-- src/cpp/orbprop/orbitProp.hpp | 306 +- src/cpp/orbprop/planets.cpp | 188 +- src/cpp/orbprop/planets.hpp | 112 +- src/cpp/orbprop/spaceWeather.cpp | 111 + src/cpp/orbprop/spaceWeather.hpp | 58 + src/cpp/orbprop/staticField.cpp | 204 +- src/cpp/orbprop/staticField.hpp | 38 +- src/cpp/orbprop/tideCoeff.cpp | 252 +- src/cpp/orbprop/tideCoeff.hpp | 60 +- src/cpp/other_ssr/otherSSR.hpp | 226 +- src/cpp/other_ssr/prototypeCmpSSRDecode.cpp | 3213 +++-- src/cpp/other_ssr/prototypeCmpSSREncode.cpp | 2981 ++-- src/cpp/other_ssr/prototypeIgsSSRDecode.cpp | 1398 +- src/cpp/other_ssr/prototypeIgsSSREncode.cpp | 1308 +- src/cpp/pea/inputs.cpp | 1375 +- src/cpp/pea/inputsOutputs.hpp | 50 +- src/cpp/pea/main.cpp | 2311 +-- src/cpp/pea/minimumConstraints.cpp | 1960 +-- src/cpp/pea/minimumConstraints.hpp | 39 +- src/cpp/pea/outputs.cpp | 2029 +-- src/cpp/pea/peaCommitStrings.cpp | 204 +- src/cpp/pea/peaCommitStrings.hpp | 19 +- src/cpp/pea/peaCommitVersion.h.in | 5 - src/cpp/pea/peaLibVersion.h.in | 7 + src/cpp/pea/pea_snx.cpp | 421 +- src/cpp/pea/ppp.cpp | 2033 +-- src/cpp/pea/ppp.hpp | 411 +- src/cpp/pea/ppp_ambres.cpp | 583 +- src/cpp/pea/ppp_callbacks.cpp | 912 +- src/cpp/pea/ppp_obs.cpp | 4198 +++--- src/cpp/pea/ppp_pseudoobs.cpp | 1626 ++- src/cpp/pea/ppp_slr.cpp | 768 +- src/cpp/pea/ppppp.cpp | 3071 ++-- src/cpp/pea/preprocessor.cpp | 985 +- src/cpp/pea/preprocessor.hpp | 17 +- src/cpp/pea/spp.cpp | 2154 +-- src/cpp/rtklib/lambda.cpp | 1037 +- src/cpp/rtklib/lambda.h | 48 +- src/cpp/rtklib/rtkcmn.cpp | 310 +- src/cpp/sbas/decodeL1.cpp | 42 + src/cpp/sbas/decodeL5.cpp | 506 + src/cpp/sbas/sbas.cpp | 403 +- src/cpp/sbas/sbas.hpp | 157 +- src/cpp/sbas/sisnet.cpp | 442 +- src/cpp/sbas/sisnet.hpp | 62 +- src/cpp/slr/slr.hpp | 65 +- src/cpp/slr/slrCom.cpp | 276 +- src/cpp/slr/slrObs.cpp | 957 +- src/cpp/slr/slrRec.cpp | 127 +- src/cpp/slr/slrSat.cpp | 8 - src/cpp/trop/tropCSSR.cpp | 210 +- src/cpp/trop/tropGPT2.cpp | 666 +- src/cpp/trop/tropModels.cpp | 508 +- src/cpp/trop/tropModels.hpp | 184 +- src/cpp/trop/tropSAAS.cpp | 192 +- src/cpp/trop/tropSBAS.cpp | 176 +- src/cpp/trop/tropVMF3.cpp | 4982 +++++-- src/testing/enums_magic.h | 1248 ++ src/testing/test_magic_enum.cpp | 336 + vcpkg.json | 44 + 399 files changed, 120080 insertions(+), 81514 deletions(-) create mode 100644 .github/workflows/build.yaml create mode 100644 Docs/images/GinanGUI-screenshot.png create mode 100644 Docs/images/GinanWorkshop2025WindowsSetUp-TN2.png create mode 100644 Docs/images/ICRF-75pc-20250121.png delete mode 100644 Docs/images/ICRF-75pc.png create mode 100644 Docs/images/UseCasesv04.jpg create mode 100644 Docs/resources/Ginan-Workshop--Windows-Set-up--2025-06-23.pdf create mode 100644 debugConfigs/sp3_ecef2eci.yaml delete mode 100644 docker/tags create mode 100644 exampleConfigs/LEO_dynPOD.yaml create mode 100644 exampleConfigs/LEO_kinPOD.yaml create mode 100644 ginan.code-workspace create mode 100644 scripts/GinanEDA/templates/measurements_diff.jinja create mode 100644 scripts/GinanEDA/templates/states_diff.jinja create mode 100644 scripts/GinanUI/README.md create mode 100644 scripts/GinanUI/__init__.py create mode 100644 scripts/GinanUI/app/__init__.py create mode 100644 scripts/GinanUI/app/controllers/__init__.py create mode 100644 scripts/GinanUI/app/controllers/input_controller.py create mode 100644 scripts/GinanUI/app/controllers/visualisation_controller.py create mode 100644 scripts/GinanUI/app/main_window.py create mode 100644 scripts/GinanUI/app/models/__init__.py create mode 100644 scripts/GinanUI/app/models/archive_manager.py create mode 100644 scripts/GinanUI/app/models/dl_products.py create mode 100644 scripts/GinanUI/app/models/execution.py create mode 100644 scripts/GinanUI/app/models/rinex_extractor.py create mode 100644 scripts/GinanUI/app/resources/Yaml/default_config.yaml create mode 100644 scripts/GinanUI/app/resources/__init__.py create mode 100644 scripts/GinanUI/app/resources/ginan_logo.qrc create mode 100644 scripts/GinanUI/app/resources/ginan_logo_rc.py create mode 100644 scripts/GinanUI/app/utils/__init__.py create mode 100644 scripts/GinanUI/app/utils/cddis_credentials.py create mode 100644 scripts/GinanUI/app/utils/cddis_email.py create mode 100644 scripts/GinanUI/app/utils/common_dirs.py rename scripts/{ => GinanUI/app/utils}/gn_functions.py (99%) create mode 100644 scripts/GinanUI/app/utils/logger.py create mode 100644 scripts/GinanUI/app/utils/toast.py create mode 100644 scripts/GinanUI/app/utils/ui_compilation.py create mode 100644 scripts/GinanUI/app/utils/workers.py create mode 100644 scripts/GinanUI/app/utils/yaml.py create mode 100644 scripts/GinanUI/app/views/__init__.py create mode 100644 scripts/GinanUI/app/views/main_window.ui create mode 100644 scripts/GinanUI/docs/USER_GUIDE.md create mode 100644 scripts/GinanUI/docs/images/cddis_credentials_button.jpg create mode 100644 scripts/GinanUI/docs/images/cddis_credentials_screen.jpg create mode 100644 scripts/GinanUI/docs/images/ginan_ui_dashboard.jpg create mode 100644 scripts/GinanUI/docs/images/mode_dropdown.jpg create mode 100644 scripts/GinanUI/docs/images/observations_output_buttons.jpg create mode 100644 scripts/GinanUI/docs/images/pea_processing.jpg create mode 100644 scripts/GinanUI/docs/images/plot_visualisation.jpg create mode 100644 scripts/GinanUI/docs/images/plot_visualisation_web.jpg create mode 100644 scripts/GinanUI/docs/images/process_button.jpg create mode 100644 scripts/GinanUI/docs/images/product_downloading.jpg create mode 100644 scripts/GinanUI/main.py create mode 100644 scripts/GinanUI/requirements.txt create mode 100644 scripts/README.md create mode 100644 scripts/__init__.py rename scripts/{ => deprecated_scripts}/auto_generate_yaml.py (100%) rename scripts/{ => deprecated_scripts}/auto_run_PPP.py (100%) rename scripts/{ => deprecated_scripts}/compareGinanJson.py (100%) rename scripts/{ => deprecated_scripts}/createAppimage.sh (60%) rename scripts/{ => deprecated_scripts}/download_slr_data.py (100%) rename scripts/{ => deprecated_scripts}/qzss_ohi_merge.py (100%) delete mode 100644 scripts/download_archives.py delete mode 100755 scripts/download_example_input_data.py create mode 100644 scripts/formatting/fix_doxygen.py create mode 100644 scripts/formatting/reorganise_include.py create mode 100755 scripts/installation/ubuntu24.sh create mode 100644 scripts/plot_trace_res.py create mode 100644 src/CMakePresets.json create mode 100644 src/cmake/GitVersion.cmake create mode 100644 src/cmake/toolchain/clang_linux_x64.cmake rename src/{compile_mac_arm64.cmake => cmake/toolchain/clang_mac_arm64.cmake} (100%) create mode 100644 src/cmake/toolchain/clang_mac_x64.cmake create mode 100644 src/cmake/toolchain/gcc_mac_arm64.cmake create mode 100644 src/cmake/toolchain/mingw64.cmake delete mode 100755 src/cpp/3rdparty/enum.h delete mode 100644 src/cpp/3rdparty/enum_macros.h create mode 100644 src/cpp/3rdparty/magic_enum.hpp create mode 100644 src/cpp/3rdparty/nrlmsise/nrlmsise-00.cpp create mode 100644 src/cpp/3rdparty/nrlmsise/nrlmsise-00.h create mode 100644 src/cpp/3rdparty/nrlmsise/nrlmsise-00_data.cpp create mode 100644 src/cpp/common/enumHelpers.hpp delete mode 100644 src/cpp/common/interactiveTerminal.cpp delete mode 100644 src/cpp/common/interactiveTerminal.hpp create mode 100644 src/cpp/common/kalmanBlas.cpp create mode 100644 src/cpp/common/kalmanBlas.hpp create mode 100644 src/cpp/common/lapackWrapper.hpp create mode 100644 src/cpp/common/platformCompat.hpp delete mode 100644 src/cpp/common/walkthrough.cpp rename src/cpp/configurator/{htmlFooterTemplate.html => htmlFooterTemplate.hpp} (96%) rename src/cpp/configurator/{htmlHeaderTemplate.html => htmlHeaderTemplate.hpp} (94%) create mode 100644 src/cpp/orbprop/spaceWeather.cpp create mode 100644 src/cpp/orbprop/spaceWeather.hpp create mode 100644 src/cpp/pea/peaLibVersion.h.in create mode 100644 src/cpp/sbas/decodeL1.cpp create mode 100644 src/cpp/sbas/decodeL5.cpp delete mode 100644 src/cpp/slr/slrSat.cpp create mode 100644 src/testing/enums_magic.h create mode 100644 src/testing/test_magic_enum.cpp create mode 100644 vcpkg.json diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000..101d69530 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,483 @@ +name: Build and Package + +on: + push: + branches: [ main, develop, develop-weekly ] + pull_request: + branches: [ main, develop, develop-weekly ] + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - name: Linux x64 + os: ubuntu-latest + triplet: x64-linux + artifact-name: ginan-linux-x64 + build-dir: linux-Release + cache-key: vcpkg-linux-v4 + install-deps: | + sudo apt-get update + sudo apt-get install -y build-essential cmake ninja-build curl zip unzip tar pkg-config git + preset: release + + - name: macOS ARM64 + os: macos-14 + triplet: arm64-osx + artifact-name: ginan-macos-arm64 + build-dir: mac-arm64-Release + cache-key: vcpkg-macos-arm64-v4 + install-deps: | + brew install cmake ninja pkg-config libomp gcc + echo "/opt/homebrew/bin" >> $GITHUB_PATH + preset: macos-arm64-release + + - name: macOS x64 + os: macos-15-intel + triplet: x64-osx + artifact-name: ginan-macos-x64 + build-dir: mac-x64-Release + cache-key: vcpkg-macos-x64-v3 + install-deps: | + brew install cmake ninja pkg-config libomp gcc + echo "/usr/local/bin" >> $GITHUB_PATH + preset: macos-x64-release + + - name: Windows x64 (Cross) + os: ubuntu-latest + triplet: x64-mingw-static + artifact-name: ginan-windows-x64 + build-dir: windows-cross-Release + cache-key: vcpkg-windows-cross-v4 + install-deps: | + sudo apt-get update + sudo apt-get install -y build-essential cmake ninja-build curl zip unzip tar pkg-config git mingw-w64 g++-mingw-w64-x86-64 gcc-mingw-w64-x86-64 + preset: windows-cross-release + vcpkg-extra-flags: --allow-unsupported + + name: Build ${{ matrix.name }} + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install system dependencies + run: ${{ matrix.install-deps }} + + - name: Setup vcpkg binary cache + run: | + mkdir -p ${{ github.workspace }}/.vcpkg-cache + echo "VCPKG_BINARY_SOURCES=clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite" >> $GITHUB_ENV + echo "VCPKG_BUILD_TYPE=release" >> $GITHUB_ENV + + - name: Cache vcpkg + uses: actions/cache@v4 + with: + path: | + .vcpkg-cache + vcpkg + key: ${{ matrix.cache-key }}-${{ hashFiles('vcpkg.json') }} + restore-keys: | + ${{ matrix.cache-key }}- + + - name: Setup vcpkg + run: | + if [ ! -d "vcpkg" ]; then + git clone https://github.com/Microsoft/vcpkg.git + ./vcpkg/bootstrap-vcpkg.sh -disableMetrics + fi + echo "VCPKG_ROOT=${{ github.workspace }}/vcpkg" >> $GITHUB_ENV + + - name: Install vcpkg dependencies + run: | + export VCPKG_BUILD_TYPE=release + cd ${{ github.workspace }} + # Retry logic for transient network failures + for i in 1 2 3; do + ./vcpkg/vcpkg install --triplet ${{ matrix.triplet }} --x-install-root=./vcpkg_installed ${{ matrix.vcpkg-extra-flags }} && break || { + echo "vcpkg install attempt $i failed, retrying..."; + sleep 10; + } + done + + - name: Clean build directory + working-directory: src + run: rm -rf build/${{ matrix.build-dir }} + + - name: Configure CMake + working-directory: src + run: cmake --preset ${{ matrix.preset }} + + - name: Build + working-directory: src + run: | + if [ "$RUNNER_OS" = "Linux" ]; then + cmake --build build/${{ matrix.build-dir }} --parallel $(nproc) + else + cmake --build build/${{ matrix.build-dir }} --parallel $(sysctl -n hw.ncpu) + fi + + - name: Collect artifacts + run: | + mkdir -p artifacts + cp -r bin/* artifacts/ || true + find . -name "*.exe" -exec cp {} artifacts/ \; 2>/dev/null || true + + # Include required runtime libraries for macOS + if [ "$RUNNER_OS" = "macOS" ]; then + if [ -f "/opt/homebrew/lib/libomp.dylib" ]; then + cp /opt/homebrew/lib/libomp.dylib artifacts/ + elif [ -f "/usr/local/lib/libomp.dylib" ]; then + cp /usr/local/lib/libomp.dylib artifacts/ + fi + if [ -f "/opt/homebrew/lib/libgfortran.5.dylib" ]; then + cp /opt/homebrew/lib/libgfortran.5.dylib artifacts/ || true + elif [ -f "/usr/local/lib/libgfortran.5.dylib" ]; then + cp /usr/local/lib/libgfortran.5.dylib artifacts/ || true + fi + fi + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact-name }} + path: artifacts/ + retention-days: 30 + + build-gui: + needs: build + strategy: + fail-fast: false + matrix: + include: + - name: Linux x64 + os: ubuntu-latest + binary-artifact: ginan-linux-x64 + gui-artifact: ginan-gui-linux-x64 + ui-sed-cmd: sed -i '24s/.*/from scripts.GinanUI.app.resources import ginan_logo_rc/' app/views/main_window_ui.py + pyinstaller-args: --windowed + extra-steps: "" + + - name: macOS ARM64 + os: macos-15 + binary-artifact: ginan-macos-arm64 + gui-artifact: ginan-gui-macos-arm64 + ui-sed-cmd: sed -i '' '24s/.*/from scripts.GinanUI.app.resources import ginan_logo_rc/' app/views/main_window_ui.py + pyinstaller-args: --onedir --clean --target-arch arm64 --noconfirm + extra-steps: macos + + - name: macOS x64 + os: macos-15-intel + binary-artifact: ginan-macos-x64 + gui-artifact: ginan-gui-macos-x64 + ui-sed-cmd: sed -i '' '24s/.*/from scripts.GinanUI.app.resources import ginan_logo_rc/' app/views/main_window_ui.py + pyinstaller-args: --onedir --clean --noconfirm + extra-steps: macos + + - name: Windows x64 + os: windows-latest + binary-artifact: ginan-windows-x64 + gui-artifact: ginan-gui-windows-x64 + ui-sed-cmd: | + (Get-Content app/views/main_window_ui.py) | ForEach-Object { + if ($_.ReadCount -eq 24) { + "from scripts.GinanUI.app.resources import ginan_logo_rc" + } else { + $_ + } + } | Set-Content app/views/main_window_ui.py + pyinstaller-args: --windowed + pyinstaller-separator: ";" + extra-steps: "" + + name: Build GUI ${{ matrix.name }} + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Download binaries + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.binary-artifact }} + path: bin/ + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Python dependencies + run: | + pip install -r scripts/GinanUI/requirements.txt + pip install pyinstaller + + - name: Convert UI files to Python (Unix) + if: runner.os != 'Windows' + working-directory: scripts/GinanUI + run: | + pyside6-uic app/views/main_window.ui -o app/views/main_window_ui.py + ${{ matrix.ui-sed-cmd }} + + - name: Convert UI files to Python (Windows) + if: runner.os == 'Windows' + working-directory: scripts/GinanUI + shell: pwsh + run: | + pyside6-uic app/views/main_window.ui -o app/views/main_window_ui.py + ${{ matrix.ui-sed-cmd }} + + - name: Make binaries executable (Unix) + if: runner.os != 'Windows' + run: chmod +x bin/* + + - name: Install OpenMP (macOS) + if: matrix.extra-steps == 'macos' + run: | + echo "Installing OpenMP via Homebrew..." + brew install libomp + echo "OpenMP installed, checking location..." + ls -la /opt/homebrew/lib/libomp* 2>/dev/null || ls -la /usr/local/lib/libomp* 2>/dev/null || true + + - name: Copy required libraries (macOS) + if: matrix.extra-steps == 'macos' + run: | + echo "Looking for OpenMP library..." + + # libomp is keg-only, so check Cellar and opt paths + LIBOMP_PATH="" + if [ -f "/opt/homebrew/opt/libomp/lib/libomp.dylib" ]; then + LIBOMP_PATH="/opt/homebrew/opt/libomp/lib/libomp.dylib" + elif [ -f "/usr/local/opt/libomp/lib/libomp.dylib" ]; then + LIBOMP_PATH="/usr/local/opt/libomp/lib/libomp.dylib" + elif [ -f "/opt/homebrew/lib/libomp.dylib" ]; then + LIBOMP_PATH="/opt/homebrew/lib/libomp.dylib" + elif [ -f "/usr/local/lib/libomp.dylib" ]; then + LIBOMP_PATH="/usr/local/lib/libomp.dylib" + fi + + if [ -n "$LIBOMP_PATH" ]; then + echo "Found libomp at: $LIBOMP_PATH" + cp -v "$LIBOMP_PATH" bin/ + chmod 644 bin/libomp.dylib + else + echo "ERROR: libomp.dylib not found!" + echo "Searching in Cellar..." + find /usr/local/Cellar/libomp -name "libomp.dylib" 2>/dev/null || true + find /opt/homebrew/Cellar/libomp -name "libomp.dylib" 2>/dev/null || true + exit 1 + fi + + # Copy other potential dependencies + if [ -f "/opt/homebrew/lib/libgfortran.5.dylib" ]; then + cp /opt/homebrew/lib/libgfortran.5.dylib bin/ || true + elif [ -f "/usr/local/lib/libgfortran.5.dylib" ]; then + cp /usr/local/lib/libgfortran.5.dylib bin/ || true + fi + + echo "Contents of bin/ after library copy:" + ls -lh bin/*.dylib 2>/dev/null || echo "No .dylib files in bin/" + + - name: Build GUI with PyInstaller (Unix) + if: runner.os != 'Windows' + run: | + python -m PyInstaller --name GinanUI \ + ${{ matrix.pyinstaller-args }} \ + --add-data "scripts/GinanUI/app:app" \ + --add-data "scripts/plot_pos.py:scripts" \ + --add-binary "bin/*:bin" \ + --hidden-import PySide6 \ + --hidden-import PySide6.QtWebEngineWidgets \ + --hidden-import PySide6.QtWebEngineCore \ + --hidden-import plotly \ + --hidden-import scripts.plot_pos \ + --hidden-import scripts.GinanUI.app \ + --hidden-import scripts.GinanUI.app.models \ + --hidden-import scripts.GinanUI.app.models.execution \ + --hidden-import scripts.GinanUI.app.controllers \ + --hidden-import scripts.GinanUI.app.controllers.input_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.cddis_credentials \ + --hidden-import scripts.GinanUI.app.utils.cddis_email \ + --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 \ + --collect-all scripts.GinanUI \ + scripts/GinanUI/main.py + + - name: Build GUI with PyInstaller (Windows) + if: runner.os == 'Windows' + run: | + pyinstaller --name GinanUI ` + ${{ matrix.pyinstaller-args }} ` + --add-data "scripts/GinanUI/app;app" ` + --add-data "scripts/plot_pos.py;scripts" ` + --add-binary "bin/*;bin" ` + --hidden-import PySide6 ` + --hidden-import PySide6.QtWebEngineWidgets ` + --hidden-import PySide6.QtWebEngineCore ` + --hidden-import plotly ` + --hidden-import scripts.plot_pos ` + --hidden-import scripts.GinanUI.app ` + --hidden-import scripts.GinanUI.app.models ` + --hidden-import scripts.GinanUI.app.models.execution ` + --hidden-import scripts.GinanUI.app.controllers ` + --hidden-import scripts.GinanUI.app.controllers.input_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.cddis_credentials ` + --hidden-import scripts.GinanUI.app.utils.cddis_email ` + --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 ` + --collect-all scripts.GinanUI ` + scripts/GinanUI/main.py + + # Post-build cleanup + - name: Remove unnecessary files (Unix) + if: runner.os != 'Windows' + run: | + cd dist/GinanUI + # Only remove clearly unnecessary files + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete 2>/dev/null || true + if [ "$RUNNER_OS" = "macOS" ]; then + find . -name ".DS_Store" -delete 2>/dev/null || true + fi + + - name: Remove unnecessary files (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + cd dist/GinanUI + # Only remove clearly unnecessary files + Get-ChildItem -Recurse -Directory | Where-Object { $_.Name -eq "__pycache__" } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + Get-ChildItem -Recurse -File -Filter "*.pyc" | Remove-Item -Force -ErrorAction SilentlyContinue + + # macOS-specific post-build steps + - name: Copy Qt WebEngine resources (macOS) + if: matrix.extra-steps == 'macos' + run: | + set -e + PYSIDE_DIR=$(python -c 'import PySide6, os; print(os.path.dirname(PySide6.__file__))') + QT_DIR="$PYSIDE_DIR/Qt" + RES_SRC1="$QT_DIR/resources" + RES_SRC2="$QT_DIR/lib/QtWebEngineCore.framework/Resources" + RES_DEST="dist/GinanUI/_internal/PySide6/Qt/resources" + mkdir -p "$RES_DEST" + copied=false + if [ -d "$RES_SRC1" ]; then + cp -R "$RES_SRC1"/* "$RES_DEST/" 2>/dev/null || true + copied=true + fi + if [ -d "$RES_SRC2" ]; then + cp -R "$RES_SRC2"/* "$RES_DEST/" 2>/dev/null || true + copied=true + fi + if [ "$copied" != true ]; then + echo "Qt WebEngine resources not found; build may fail"; + fi + + - name: Create launcher script (macOS) + if: matrix.extra-steps == 'macos' + run: | + cat > dist/GinanUI/run.sh << 'EOF' + #!/usr/bin/env zsh + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + export QTWEBENGINEPROCESS_PATH="$SCRIPT_DIR/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess" + export QTWEBENGINE_RESOURCES_PATH="$SCRIPT_DIR/_internal/PySide6/Qt/resources" + export QTWEBENGINE_LOCALES_PATH="$SCRIPT_DIR/_internal/PySide6/Qt/resources/qtwebengine_locales" + export QTWEBENGINE_DISABLE_SANDBOX=1 + export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu-sandbox --disable-seccomp-filter-sandbox --disable-logging --log-level=3" + export DYLD_FRAMEWORK_PATH="$SCRIPT_DIR/_internal/PySide6/Qt/lib" + export DYLD_LIBRARY_PATH="$SCRIPT_DIR/_internal/PySide6/Qt/lib" + if command -v xattr >/dev/null 2>&1; then + xattr -dr com.apple.quarantine "$SCRIPT_DIR" 2>/dev/null || true + fi + exec "$SCRIPT_DIR/GinanUI" + EOF + chmod +x dist/GinanUI/run.sh + + - name: Copy QtWebEngineProcess (macOS) + if: matrix.extra-steps == 'macos' + run: | + PYSIDE_PATH=$(python -c "import PySide6; import os; print(os.path.dirname(PySide6.__file__))") + mkdir -p dist/GinanUI/Helpers + if [ -d "$PYSIDE_PATH/Qt/libexec/QtWebEngineProcess.app" ]; then + cp -R "$PYSIDE_PATH/Qt/libexec/QtWebEngineProcess.app" dist/GinanUI/Helpers/ + elif [ -d "$PYSIDE_PATH/Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app" ]; then + cp -R "$PYSIDE_PATH/Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app" dist/GinanUI/Helpers/ + fi + + - name: Copy runtime libraries to package (macOS) + if: matrix.extra-steps == 'macos' + run: | + echo "Checking for libraries to copy..." + ls -lh bin/*.dylib 2>/dev/null || echo "No .dylib files in bin/" + + # Copy OpenMP and other libraries into the package + mkdir -p dist/GinanUI/_internal/lib + + if [ -f "bin/libomp.dylib" ]; then + echo "Copying libomp.dylib to package..." + cp -v bin/libomp.dylib dist/GinanUI/_internal/lib/ + chmod 644 dist/GinanUI/_internal/lib/libomp.dylib + else + echo "ERROR: bin/libomp.dylib not found!" + exit 1 + fi + + if [ -f "bin/libgfortran.5.dylib" ]; then + echo "Copying libgfortran.5.dylib to package..." + cp -v bin/libgfortran.5.dylib dist/GinanUI/_internal/lib/ + fi + + echo "Libraries copied to package:" + ls -lh dist/GinanUI/_internal/lib/ + + # Update binaries to look in @executable_path/../lib for libraries + echo "Updating binary rpaths..." + for binary in dist/GinanUI/_internal/bin/*; do + if [ -f "$binary" ] && file "$binary" | grep -q "Mach-O"; then + echo "Processing $binary" + install_name_tool -add_rpath "@executable_path/../lib" "$binary" 2>/dev/null || true + # Update libomp reference + install_name_tool -change "/opt/homebrew/lib/libomp.dylib" "@rpath/libomp.dylib" "$binary" 2>/dev/null || true + install_name_tool -change "/usr/local/lib/libomp.dylib" "@rpath/libomp.dylib" "$binary" 2>/dev/null || true + fi + done + + echo "Verifying binary rpaths after update:" + otool -L dist/GinanUI/_internal/bin/pea | grep -i omp || echo "No OpenMP reference found" + + - name: Code sign binaries (macOS) + if: matrix.extra-steps == 'macos' + working-directory: dist/GinanUI + run: | + find . -type f -perm +111 -exec codesign --force --deep -s - {} \; + find . -name "*.dylib" -exec codesign --force -s - {} \; + find . -name "*.so" -exec codesign --force -s - {} \; + codesign --force -s - _internal/Python || true + codesign --force --deep -s - Helpers/QtWebEngineProcess.app || true + find _internal/PySide6/Qt/resources -type f -name "*.pak" -exec codesign --force --sign - {} \; || true + + - name: Upload GUI artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.gui-artifact }} + path: dist/GinanUI/ + retention-days: 30 diff --git a/CHANGELOG.md b/CHANGELOG.md index 08ce98fd8..cea31a8dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,101 @@ 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.0] 2025-12-16 + +## Added + +A Qt-based Graphical User Interface (GUI) for Ginan + +Ginan and GUI binaries for Linux, MacOS and Windows + +Major improvement in handling state errors in pre-fit and post-fit outlier screening + +New TRACE file plotting script (plot_trace_res.py) + +RNX2 --> RNX3 phase signal mapping in config + +Check on difference between BRDC and Kalman clocks before alignment + +Additional info to TRACE files for monitoring (snr, azimuth, signal data availability) + +Postfit omega test and address its numerical instability + +Incorrect error count increments (For phase rejects, receiver error counts and satellite error counts) + +Option to reset filter at specific times (periodic_reset) + +## Changed + +VCPKG as the primary package manager for Ginan + +Started refactor of code to use OpenBLAS / LAPACK instead of Eigen - including RTS and core Kalman filter
+(Improved efficiency with up to 30% faster on network runs) + +TRACE epoch from week / sec to generic datetime + +Use normal epoch instead of transmission time for satellite clock initialisation + +Improved SPP via Code bias correction + +Improved higher order ionospheric corrections in IF combination mode + +Improvements and restructure of Least squares estimation code + +EDA Updates: + +- Add ability to view differences for States and Measurements +- Check data exists before plotting +- Fix plotting of satellite / site availability +- Add cycle detection info to database + +LEO drag and SRP coefficient estimation (Properly estimate and resolve conflict with EMP estimation) + +Update variance before detecting cycle slips + +Trop SNX file written for all stations now + +Pass LLI flags through to savedSlip + +Move from better_enum to magic_enum + +Rename Ginan LLI flags to "retrack" + + +## Fixed + +Mutex compilation problem for OSX arm64 + +noTime with Sp3Write when filtered states not available + +Day-bound no date issue and finite vs mincon inconsistency + +Elevation calculation in preprocessor (PEA can run in preprocessing-only mode) + +NaN biases in SSR bias map + +Limit tropospheric model to reasonable heights + +Correctly write out Antenna delta in RNX file (H/E/N) + +IERS mean pole function to use TT time + +Ensure commit hash is generated for every compilation (help with ID version) + +Errors in writing POS and GPX files when RTS is on + +Issues with Chunking (rewrite) + +RNX file reading (Correct field width) + +Cmake file so loading software can compile + +## Deprecated + +## Removed + +## Security + # [3.1] 2024-09-02 ### Added diff --git a/Docs/announcements.md b/Docs/announcements.md index 4cb7bb385..5a53abb63 100644 --- a/Docs/announcements.md +++ b/Docs/announcements.md @@ -1,22 +1,29 @@ -> **2 Sept 2024** - the Ginan team is pleased to release v3.1.0 of the toolkit. +> **16 Dec 2025** - the Ginan team is pleased to release v4.0.0 of the toolkit. > -> The improvements delivered by this version include: +> **Main Highlights**: +> +> * Introduce Ginan binaries for Linux, MacOS and Windows +> * The binaries can be found on the GitHub website: +> * [Ginan Binaries](https://github.com/GeoscienceAustralia/ginan/releases/tag/v4.0.0) +> * Introduce a Qt-based Graphical User Interface (GUI) for Ginan: +> * Simply open RNX file and choose from drop-downs +> * Download the GUI from GitHub release page +> * Works across Linux, MacOS and Windows +> * Built using PySide 6 +> * A user manual for the GUI can be found here: +> * [Ginan GUI User Manual](https://github.com/GeoscienceAustralia/ginan/tree/main/scripts/GinanUI/docs/USER_GUIDE.md) +> +> ![GinanGUI Screenshot](images/GinanGUI-screenshot.png) +> *A screenshot of the Ginan GUI in action.* +> +> **Major Changes**: +> +> * Major refactor: use OpenBLAS / LAPACK instead of Eigen +> * Refactored RTS code +> * Refactored core Kalman filter code +> * Improved efficiency (up to 30% faster on network runs) +> * Major improvement in handling state errors in pre-fit and post-fit outlier screening +> * Move to vcpkg as the primary package manager for Ginan +> * Add RNX2 --> RNX3 phase signal mapping in config > -> * Boxwing model for the albedo -> * Sisnet (SouthPan) message support -> * SLR processing capability -> * PBO Position (.pos) format file output support -> * Apple silicon (M-chip) support -> * VMF3 file download python script (get_vmf3.py) -> * POS file visualisation python script (plot_pos.py) -> * EDA improvements -> * Improved documentation -> * Use case examples updated -> * Frequency dependent GLONASS receiver code bias estimation enabled -> * Improved missing/bad data handling -> * Bias rates from .BIA/BSX files parsed and used -> * Measurment and State error handling sigma_limit thresholds separated -> * Config file reorganisation (rec_reference_system: moved to receiver_options:) -> * Clock code handling modified -> * Many bug fixes. diff --git a/Docs/codingStandard.md b/Docs/codingStandard.md index 390593c04..05d480cbf 100644 --- a/Docs/codingStandard.md +++ b/Docs/codingStandard.md @@ -1,5 +1,18 @@ # Coding Standards for C++ +## Automatic Formatting + +This project uses clang-format (we use version 20) for automatic code formatting. The formatting rules are defined in the `.clang-format` file in the project root. + +To format your code: +```bash +# Format a single file +clang-format -i path/to/your/file.cpp + +# Format all C++ files in src/cpp except 3rdpart +find src/cpp -type f \( -name "*.cpp" -o -name "*.hpp" -o -name "*.c" -o -name "*.h" -o -name "*.cc" -o -name "*.cxx" \) -not -path "src/cpp/3rdparty/*" -exec clang-format -i {} + +``` + ## Code style Decades of experience has shown that codebases that are built with concise, clean code have fewer issues and are easier to maintain. If submitting a pull request for a patch to the software, please ensure your code meets the following standards. @@ -14,66 +27,68 @@ Overall we are aiming to ### Unconcise code - Not recommended - //check first letter of satellite type against something + //check first letter of satellite type against something + + if (obs.Sat.id().c_str()[0]) == 'G') + doSomething(); + else if (obs.Sat.id().c_str()[0]) == 'R') + doSomething(); + else if (obs.Sat.id().c_str()[0]) == 'E') + doSomething(); + else if (obs.Sat.id().c_str()[0]) == 'I') + doSomething(); - if (obs.Sat.id().c_str()[0]) == 'G') - doSomething(); - else if (obs.Sat.id().c_str()[0]) == 'R') - doSomething(); - else if (obs.Sat.id().c_str()[0]) == 'E') - doSomething(); - else if (obs.Sat.id().c_str()[0]) == 'I') - doSomething(); - ### Clear Code - Good - char& sysChar = obs.Sat.id().c_str()[0]; + char& sysChar = obs.Sat.id().c_str()[0]; - switch (sysChar) - { - case 'G': doSomething(); break; - case 'R': doSomething(); break; - case 'E': doSomething(); break; - case 'I': doSomething(); break; - } + switch (sysChar) + { + case 'G': doSomething(); break; + case 'R': doSomething(); break; + case 'E': doSomething(); break; + case 'I': doSomething(); break; + } ## Spacing, Indentation, and layout -* Use tabs, with tab spacing set to 4. -* Use space or tabs before and after any ` + - * / = < > == != % ` etc.. -* Use space, tab or new line after any `, ;` +* Use spaces (not tabs), with indentation width set to 4 spaces. +* Use space before and after any ` + - * / = < > == != % ` etc.. +* Use space or new line after any `, ;` * Use a new line after if statements. -* Use tabs to keep things tidy - If the same function is called multiple times with different parameters, the parameters should line up. +* Use spaces to keep things tidy - If the same function is called multiple times with different parameters, the parameters should line up. +* Line length should not exceed 100 characters. +* Use Allman brace style (braces on new lines). ### Scattered Parameters - Bad - trySetFromYaml(mongo_metadata,output_files,{"mongo_metadata" }); - trySetFromYaml(mongo_output_measurements,output_files,{"mongo_output_measurements" }); - trySetFromYaml(mongo_states,output_files,{"mongo_states" }); + trySetFromYaml(mongo_metadata,output_files,{"mongo_metadata" }); + trySetFromYaml(mongo_output_measurements,output_files,{"mongo_output_measurements" }); + trySetFromYaml(mongo_states,output_files,{"mongo_states" }); ### Aligned Parameters - Good - trySetFromYaml(mongo_metadata, output_files, {"mongo_metadata" }); - trySetFromYaml(mongo_output_measurements, output_files, {"mongo_output_measurements" }); - trySetFromYaml(mongo_states, output_files, {"mongo_states" }); + trySetFromYaml(mongo_metadata, output_files, {"mongo_metadata" }); + trySetFromYaml(mongo_output_measurements, output_files, {"mongo_output_measurements" }); + trySetFromYaml(mongo_states, output_files, {"mongo_states" }); ## Statements -One statement per line +One statement per line - `*`unless you have a very good reason ### Multiple Statements per Line - Bad - z[k]=ROUND(zb[k]); y=zb[k]-z[k]; step[k]=SGN(y); + z[k]=ROUND(zb[k]); y=zb[k]-z[k]; step[k]=SGN(y); ### Single Statement per Line - Good - z[k] = ROUND(zb[k]); - y = zb[k]-z[k]; - step[k] = SGN(y); + z[k] = ROUND(zb[k]); + y = zb[k]-z[k]; + step[k] = SGN(y); ### Example of a good reason: @@ -81,53 +96,61 @@ One statement per line #### Normal - switch (sysChar) - { - case ' ': - case 'G': - *sys = E_Sys::GPS; - *tsys = TSYS_GPS; - break; - case 'R': - *sys = E_Sys::GLO; - *tsys = TSYS_UTC; - break; - case 'E': - *sys = E_Sys::GAL; - *tsys = TSYS_GAL; - break; - //...continues - } + switch (sysChar) + { + case ' ': + case 'G': + *sys = E_Sys::GPS; + *tsys = TSYS_GPS; + break; + case 'R': + *sys = E_Sys::GLO; + *tsys = TSYS_UTC; + break; + case 'E': + *sys = E_Sys::GAL; + *tsys = TSYS_GAL; + break; + //...continues + } #### Ok - if (sys == SYS_GLO) fact = EFACT_GLO; - else if (sys == SYS_CMP) fact = EFACT_CMP; - else if (sys == SYS_GAL) fact = EFACT_GAL; - else if (sys == SYS_SBS) fact = EFACT_SBS; - else fact = EFACT_GPS; + if (sys == SYS_GLO) fact = EFACT_GLO; + else if (sys == SYS_CMP) fact = EFACT_CMP; + else if (sys == SYS_GAL) fact = EFACT_GAL; + else if (sys == SYS_SBS) fact = EFACT_SBS; + else fact = EFACT_GPS; #### Ok - switch (sysChar) - { - case ' ': - case 'G': *sys = E_Sys::GPS; *tsys = TSYS_GPS; break; - case 'R': *sys = E_Sys::GLO; *tsys = TSYS_UTC; break; - case 'E': *sys = E_Sys::GAL; *tsys = TSYS_GAL; break; - case 'S': *sys = E_Sys::SBS; *tsys = TSYS_GPS; break; - case 'J': *sys = E_Sys::QZS; *tsys = TSYS_QZS; break; - //...continues - } + switch (sysChar) + { + case ' ': + case 'G': *sys = E_Sys::GPS; *tsys = TSYS_GPS; break; + case 'R': *sys = E_Sys::GLO; *tsys = TSYS_UTC; break; + case 'E': *sys = E_Sys::GAL; *tsys = TSYS_GAL; break; + case 'S': *sys = E_Sys::SBS; *tsys = TSYS_GPS; break; + case 'J': *sys = E_Sys::QZS; *tsys = TSYS_QZS; break; + //...continues + } + +## Include Organization + +* Includes should be sorted automatically by clang-format. +* Include order priority: + 1. System headers (e.g., ``, ``) + 2. Project headers (e.g., `"common/common.hpp"`) + 3. Other headers ## Braces New line for braces. - if (pass) - { - doSomething(); - } + if (pass) + { + doSomething(); + } ## Comments @@ -155,17 +178,17 @@ if ( ( testA > 10) ### Bad - if (doSomeParsing(someObject)) - { - //code contingent on parsing success? failure? - } + if (doSomeParsing(someObject)) + { + //code contingent on parsing success? failure? + } ### Good - bool fail = doSomeParsing(someObject); - if (fail) - { - //This code is clearly a response to a failure - } + bool fail = doSomeParsing(someObject); + if (fail) + { + //This code is clearly a response to a failure + } ## Variable declaration @@ -192,20 +215,21 @@ for (int i = 0; i < 10; i++) if (found) { //... -} +} ``` ## Function parameters -* One per line. +* When parameters don't fit on one line, place each parameter on its own line. * Add doxygen compatible documentation after parameters in the cpp file. * Prefer references rather than pointers unless unavoidable. ``` void function( - bool runTests, ///< Run unit test while processing - MyStruct& myStruct, ///< Structure to modify - OtherStr* otherStr = nullptr) ///< Optional structure object to populate (cant use reference because its optional) + bool runTests, ///< Run unit test while processing + MyStruct& myStruct, ///< Structure to modify + OtherStr* otherStr = nullptr ///< Optional structure object to populate (cant use reference because its optional) +) { //... } @@ -237,7 +261,7 @@ struct MyStruct double offset_arr[10] = {}; OtherStruct* refStruct_ptr = nullptr; - map offsetMap; + map offsetMap; list> variationMapList; map subStructMap; }; @@ -254,7 +278,7 @@ if (acsConfig.some_parameter) ## Undesirable Code -* Do not use 'magic numbers', which require knowledge of other code fragments for comprehension. If a comment is required for explaining what a value means, the code should be rewritten with enums or defined constants. +* Do not use 'magic numbers', which require knowledge of other code fragments for comprehension. If a comment is required for explaining what a value means, the code should be rewritten with enums or defined constants. * Do not append `.0` to integer valued doubles unless they are required. * Never use `free()`, `malloc()`, or `new` unless it cannot be avoided. * Threads create synchronisation issues; they should not be used unless manual synchronisation is never required. @@ -277,11 +301,12 @@ struct MyStruct /** Function to demonstrate documentation */ void function( - bool runTests, ///< Run unit test while processing - MyStruct& myStruct, ///< Structure to modify - OtherStr* otherStr = nullptr) ///< Optional string to populate + bool runTests, ///< Run unit test while processing + MyStruct& myStruct, ///< Structure to modify + OtherStr* otherStr = nullptr ///< Optional string to populate +) { - //... + //... } ``` @@ -292,52 +317,52 @@ void function( ### Bad - double double_arr[10] = {}; + double double_arr[10] = {}; - //..(Populate array) + //..(Populate array) - for (int i = 0; i < 10; i++) //Magic number 10 - bad. - { + for (int i = 0; i < 10; i++) //Magic number 10 - bad. + { - } + } - map doubleMap; + map doubleMap; - //..(Populate Map) + //..(Populate Map) - for (auto iter = doubleMap.begin(); iter != doubleMap.end(); iter++) //long, undescriptive - bad - { - if (iter->first == someVar) //'first' is undescriptive - bad - { - //.. - } - } + for (auto iter = doubleMap.begin(); iter != doubleMap.end(); iter++) //long, undescriptive - bad + { + if (iter->first == someVar) //'first' is undescriptive - bad + { + //.. + } + } ### Good - Iterating Maps - map offsetMap; + map offsetMap; - //..(Populate Map) + //..(Populate Map) - for (auto& [siteName, offset] : doubleMap) //give readable names to map keys and values - { - if (siteName.empty() == false) - { - - } - } + for (auto& [siteName, offset] : doubleMap) //give readable names to map keys and values + { + if (siteName.empty() == false) + { + + } + } ### Good - Iterating Lists - list obsList; + list obsList; - //..(Populate list) + //..(Populate list) - for (auto& obs : obsList) //give readable names to list elements - { - doSomethingWithObs(obs); - } + for (auto& obs : obsList) //give readable names to list elements + { + doSomethingWithObs(obs); + } ### Special Case - Deleting from maps/lists @@ -346,7 +371,7 @@ Use iterators when you need to delete from STL containers: ``` for (auto it = someMap.begin(); it != someMap.end(); ) { - KFKey key = it->first; //give some alias to the key/value so they're readable + KFKey key = it->first; //give some alias to the key/value so they're readable if (measuredStates[key] == false) { @@ -363,15 +388,15 @@ for (auto it = someMap.begin(); it != someMap.end(); ) Commonly used std containers may be included with `using` - #include - #include - #include - #include + #include + #include + #include + #include - using std::string; - using std::map; - using std::list - using std::unordered_map; + using std::string; + using std::map; + using std::list + using std::unordered_map; ## Code sequencing @@ -379,11 +404,7 @@ Commonly used std containers may be included with `using` The software is to be kept largely sequential - using threads sparingly to limit the overhead of collision avoidance. Where possible tasks are completed in parallel using parallelisation libraries to take advantage of all CPU cores in multi-processor systems while still retaining a linear flow through the execution. -Sections of the software that create and modify global objects, such as while reading ephemeris data, will be executed on a single core only. +Sections of the software that create and modify global objects, such as while reading ephemeris data, will be executed on a single core only. This will ensure that collisions are avoided and the debugging of these functions is deterministic. For sections of the software that have clear delineation between objects, such as per-receiver calculations, these may be completed in parallel, provided they do not attempt to modify or create objects with more global scope. When globally accessible objects need to be created for individual receivers, they should be pre-initialised before the entry to parallel execution section. - - - - diff --git a/Docs/ginanFAQ.html b/Docs/ginanFAQ.html index 0310137e0..0c323ca90 100644 --- a/Docs/ginanFAQ.html +++ b/Docs/ginanFAQ.html @@ -14,7 +14,7 @@

Frequently asked questions about Ginan

-

Ginan is a precise point positioning software toolkit. It can calculate positions with centimetre-level accuracy using observations of global navigation satellite systems (GNSS) and correction data. It can also be used to create that correction data. More

+

Ginan is a precise point positioning software toolkit. It can calculate positions with centimetre-level accuracy using observations of global navigation satellite systems (GNSS) and correction data. It can also be used to create that correction data. More

@@ -57,7 +57,7 @@

Frequently asked questions about Ginan

The Ginan software is available from the Geoscience Australia GitHub site.

-

Geoscience Australia uses Ginan to produce precise positioning products and data correction streams. These are also free to use.

+

Geoscience Australia uses Ginan to produce precise positioning products and data correction streams. These are also free to use.

@@ -120,7 +120,7 @@

Frequently asked questions about Ginan

-

The science of calculating a position on Earth using observations of GNSS satellites is well known and has become a ubiquitous feature of our lives. Ginan uses the well-established State Space Representation (SSR) positioning model to enable users to achieve Precise Point Positioning (PPP). In the standard GNSS positioning process, small error sources limit the users calculated position accuracy at the meter level. The SSR methodology enables augmenting the users GNSS observations with corrections for those small errors, allowing Ginan to calculate PPP positions at the decimetre to millimetre level of accuracy, depending on the quality of PPP corrections used. More

+

The science of calculating a position on Earth using observations of GNSS satellites is well known and has become a ubiquitous feature of our lives. Ginan uses the well-established State Space Representation (SSR) positioning model to enable users to achieve Precise Point Positioning (PPP). In the standard GNSS positioning process, small error sources limit the users calculated position accuracy at the meter level. The SSR methodology enables augmenting the users GNSS observations with corrections for those small errors, allowing Ginan to calculate PPP positions at the decimetre to millimetre level of accuracy, depending on the quality of PPP corrections used. More

@@ -134,7 +134,7 @@

Frequently asked questions about Ginan

-

At the heart of Ginan is a Kalman filter processing engine. The Kalman filter is an algorithm that takes observations received from GNSS satellites, and other auxiliary metadata, and produces estimates of unknown variables or states, such as the user’s position. The Ginan Kalman filter uses GNSS observations in their raw un-differenced form and can estimate all GNSS observation model states using either un-combined or the ionosphere free combination of the GNSS observations. More

+

At the heart of Ginan is a Kalman filter processing engine. The Kalman filter is an algorithm that takes observations received from GNSS satellites, and other auxiliary metadata, and produces estimates of unknown variables or states, such as the user’s position. The Ginan Kalman filter uses GNSS observations in their raw un-differenced form and can estimate all GNSS observation model states using either un-combined or the ionosphere free combination of the GNSS observations. More

@@ -148,7 +148,8 @@

Frequently asked questions about Ginan

-

There are two ways to get Ginan running:

+

There are three ways to get Ginan running:

+

You can download the Ginan binaries from the GitHub release website, either with or without the graphical user interface (GUI). These binaries work on Linux, MacOS and Windows. This can be found here: Ginan Releases

You can download the Ginan source code from the GitHub site, and the other required software packages, onto a Linux machine (including Windows Subsystem for Linux) and compile them to create the Ginan executable application. The instructions are in the Ginan README.

You can use Docker Desktop. Docker Desktop is a software application you can download onto a Linux, Mac or Windows computer. It creates a container that provides all the resources that the Ginan Docker image needs to run. Using Docker makes the process of getting started with Ginan simpler. You must download and install Docker Desktop, and then get the Ginan Docker image.

@@ -165,7 +166,7 @@

Frequently asked questions about Ginan

Ginan is a sophisticated software application with hundreds of parameters that control what it does. All these parameters are contained in configuration files using the yaml syntax (yaml - which might stand for "yet another markup language").

-

Ginan is released with example files to help users achieve particular outcomes. To understand more about Ginan's yaml files and parameters read the Ginan yaml guide.

+

Ginan is released with example files to help users achieve particular outcomes. To understand more about Ginan's yaml files and parameters read the Ginan yaml guide.

diff --git a/Docs/home.md b/Docs/home.md index ca833bec6..a8c9560eb 100644 --- a/Docs/home.md +++ b/Docs/home.md @@ -7,3 +7,10 @@ Ginan is an open source toolkit for creating precise point positioning (PPP) ana The source code for the current version of Ginan is available for download from [this site](https://github.com/GeoscienceAustralia/ginan). New versions of Ginan with enhanced capabilities will be developed and released over time. Geoscience Australia is establishing operational instances of Ginan that produce PPP analysis [products and streams](page.html?c=on&p=products.md) on a continuous basis and which are available free of charge to the public. + +## How to cite + +If you use Ginan in a publication, please cite: +``` +McClusky, Simon; Hammond, Aaron; Maj, Ronald; Allgeyer, Sébastien; Harima, Ken; Yeo, Mark; Du, Eugene; Riddell, Anna, "Precise Point Positioning with Ginan: Geoscience Australia’s Open-Source GNSS Analysis Centre Software," Proceedings of the ION 2024 Pacific PNT Meeting, Honolulu, Hawaii, April 2024, pp. 248-280. https://doi.org/10.33012/2024.19598 +``` diff --git a/Docs/images/GinanGUI-screenshot.png b/Docs/images/GinanGUI-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..b0d18a0e60ce738c03a9c3deb6476147b84a9ffd GIT binary patch literal 212847 zcmaHSb9|lK@^@on$7W+Ejcuo~8r!yQHny6^HXEx^W81dz=G^yp&pG$p=I(#?v-fAM zdDhIDnKd)(JE5{NqHxey&>ud0fD;!JlK=1l%;3WZ5I#uow|`1?3`*X|>aA-`kPky4x3ndfIj@gb39HQy zoY+wR*vJWmxWvsTCa~mqyF5QDU?E+}m1TuM)bF*Pxw_piwE;P&4Xl z=b*g(_MRZfvp@5NwI$MN=V%C8x6bkvN<(|R8L54%4C5d;3u5}}`-7*4#wLf-nk^_Jka!-6zHE0YA zcMSY??xo}wdwa-H$jFS1=YJ;g4{~GHQDH0m*UtW1mUq_BcR~~lj}j3ah-uUEnxIBx zD)j*^bzCJz>WdgL%*kPJ6u(ssaz5YJW*?LQTz4L_!RO<4#<+t(<0H@*2LmiT0@WZo6hS^MnzQH z#HB}HuyMVbL64zTDM3%LLP_gVb&ghaOqOE|9kh#hf+E=;8A!Ws@8lzn7ty2-rUjP! z%6=V|xXzVix+pJx8jZVPV2L~lP>wvzbsE7coUFv;IblWOwQ36*cV?TQsG;^5y2i?O zlXEvXKYeYm<-bV&i@P!y#R@GrwKgqdAFT}_tt7w6*IsHc4V8wW@XUM*CP%8WxO!7? zLpo5>jaC=K1`Opnf)=gDj6ZpnB?tyn3~6kH6cn-IbJ^Cypx-u+w^JqRo%^yjykO}vXNMR@^$8pMSCSj6V1K&j zk0CO12Gj04q|EMK{-Dx&Q)wmC$PZcQl_o5>-q*B1Y7PW=ZdRQa zVA>n8X8w>y=pCFX89G|v%cR}~r=_;sf9l8G8W^qC)r=_tn3=XQ`&wFdU5osayL-Ps z`Ebc8tOCGQ)1FhKtxZ(?%W&Cl#Qv@yLESHnZ#-rk#<+GPZik1%K9BN$sd#HM-vn;y zvn{ulB3|%xcyo@9S$3_5;nZlg3On>n2}0bl0)5HVYVxYGhj~ST=hnXC+)XP-&DlE zk__>*YS7T}0`pDC)4X$4o;+q8K{~yWuB@u_9_|1& z6oC=iJo^1`Hyi#BGBjzSSq<7flVV^vVGbQry#0B;(~`@FBcC^0X7mAfBU(=rEBJ?f zBV9Y#Jdlu)fkP8;+o|5Z;+q_~jjmzAr?#c4S7cmVr|X6hz$~<2a?O(scdoKZ$@{?T zfA^jlz^U_9MQ|+3;j*vfV21f5JmUE;YS4Y(!A+PsTd46M6PI&OS9j-+`;Y(BS|4C* zS-$%#>QSzGzP579N&hNv+|S@8(Ebe88ekgtwv;oxA+v2sl!(C%xcmZbQ6_2yARO@s zeJ?bkfZS`iAHJUXIGtw<#;X?m3t46cu;aai z+-ik3nG24aoRGS>SaeN@Ys$%hIwN0~&Zhb^BHmUkiDr)S>L2gWsIup=$3yIH1|QRX zJ^b0H-D^!urem(k9r5T%<#ep!PbV4fx_t9fHj{`7(<<+;vnB>qtM8x{3{GCwX(ENs zL#1CFHzL7n1~nq#;cRv?_&YsFb?eH%J$Ck+)6a&5;m5#P+xV8TS?FO4|7GU5zf5fT zRaGi84ETOgy89}TuFB&dAct7H)AWID?2+y|yeUfN9ZYO}KBPc>XqF z$apEa=0wG477n(knxIA{Ux%X*wC z1c5aV5YuA<8fMJ|k-ugq61jXKy+k4{WCg+fhi@5PCXH_8M$X0J%h# zN+jdU`8>YDql-MDd?Xu`SLmdDwJcY7#}I67)84gf6tTLz#4vfX3VX#K(;mCgqTSNNUi0U?o~Qk zdZcH+l&|qj^vq}dbAVcikwXp17`2^#vp?!fWQr=wP<)fy&SW8DNhnI87 zRjT#_b&(5x{qDR?mAwu|q97iB+={B07W9EwHWB{>Y=X*N4TbvS5PWNZcMP;Rke3TZ zJvwcn=DlQ_c9(wXfi{BU;S5?u5JrrjL^75_y*vO6_MDN|sL;%^4b`7sIGc|YNCjp&T5-A*lk?mm zyV=WPmC-5XCP}$*e_ybM0i>E^zwzszr%S6QkZaCDp(X*iu`{j!U*9(+!C)gLRKf2z zL3)dNPmfZ>KG@DsEjRZB409m*X|tmK;Mcz@H#O2D#XGnW5Hx0^fXMKrwY9PIUGo{R z$LXXpWM4L8_Y{}`x2+X~QcP$W)AjLHDcckQ8@=IO+*!n>wRq}@nDXmK;Km7b9+?HR zFJYcHiUIl=>jxkGy9U+b$y|I3+863NQDkUs7^y<}OU5>i)W)`wc3sn7+R!fsY2<3B z#hzLX=~!gSq6(<`)`R_ZHvlFNGsbvHJ6I{y&!JXfUzKE6j=-HtdHm6BDarML*U-Ou zE?1XEca;6<;W})9_W9M-+F(!jj~hxilJ9N5$kQv=7?mov^Fa?uY{NK!8J2Juo)B*i zS6j#0BD5F$sca}Nm%yak=Ys=LY#h2gL7y+WcQX#M8{;T)Pa*g|E2(DaSCjKX#<^#8 zttH&{SY8fF*6z~czz;{4ufF=VlS~lDSFMCISe)>Modor#24=8(C4+($)i+@4iCbS~q9s#z7P2d?#0;aD9Ggf{C?u5mmHF zB%Yu{{BCHLXE1D-?xGi}7kHFi>+I{VMx>{YoO9I@5XUidh?zP%Z8r;l=d?g0mC6f_ z%R-qG^QC)*1E{!|Bh8-VPo*2M^$kaFvu_}hu82}I-lI|oSHZRK^HK_liVRrYFPm%Mz!ccru5_Y^D$Z!{t|df` zwYg2rv`mP3=JjTRQ+E%{C4cbvff&mB>_@U{Ay~MCx}6%?n;T2BP92*T-{}L!OR!&H zcxO566hL0Ku6)k)R9VZ*^SUouh(_C8k;fViA&}hBF6#)Jtbz;%9Ie}1YT1a60TXEH z6>rV~zxjoOaOZ-TV;0-ZC=PggKQ)>NMKw;7F(Up0Ry-Q_BAt ztM9G+nl!kNc~f$B>VVBPyhQt^7~j{3)fU;grC=s}0ZpMLfOSOmZY zD_`hGTx^IKJ*|P6F=h8EoTcml$D_BeA$~QpQICb>f222CnLdmkR9-a0wll{7NW{ex z9O$AWT9*vXF#c6d&PXmF|DDpkNCM0AT^FP(WBieKfP)+6>l^`BMH6 z8IuMKJFxSeC0h6|;{TazdW^q~@ER}3iu=pL|1F9C6M&t9577M5)K1lVGV?`He;a}D zaAkk-kEwso;Io3JCrC?M;6i(Esef=o@W`L#CG)O^3`hfk8`vwP_2<4Pd>)gx5xjeo zV}tMJ<4qK2S;%_%0WsX`u3+1Xo%ac5 z7t=EoUvtjQu}9-tSX$b=p4&IFK7d9=-GoSr2|Sx^Pl{mU0l*UOvf51g|I=0dUC%le zkzz%Om7}S4k!a-VDg*SnjGrq5xVVD3E=BQ?yKg<{OJR2ZZ&la1% zQry3<=?_xAtfXf%<^z4bz1Yl6^m1MuheMVg#MQcax%vS7tZe7ks?}Ftt{axQDi0PX znk+DtBrXj0Q`WZQCg1mbRCY%iic22Q{I_o(+w{G6Zjs*doj}Tr9|BSq=_^jD*x#HI zIN-a-_kZXtuWT01KVz59y9%AQdvdY}dgLPAX5DT4V)FjjL!zgXr0lk65AvQdyse$$ zOa!=^fx*_~Q*EZA&3Y|U+X1*0t2!WfJ^3S zSWmb+!#fD#snkG~(RRow{YnZdmL^)P_w@p8jBo9RpPT7O5e|IQTD*h<<%MfFVH@5x zQWO_rVC$_Mty?^9!EIV?Lb-TqMDZdUd%KRvKJSTyglHYTb@>10wAKGC1G9Y+do~95Y4;A@i1zc;6 z)8O9My;f78-dfX13FU1AjJ2%Hom%LA+UGkwzxT17bqgl>i;8E48z|J0>wYz8f zps}6tyKUuG)*d|Z&(~;pLBl)BG=wZPk1ga><3ARvPT;~7s|frInqH$k=y$T3=F4zh ztk!i@{i42y(|(0m(VA=NporqIg|EZQLnsxRF$ z*^IJ*N;N~5PLJq%G-2MF+2jFpoI2%K;!iUyYzbVHaW$dw&AG@kIh18$ER_a6vn?W( zTU6)z2gIZk3|w@NBLESP{@yzzDpXL)!b3(X()0ZQVt~PSb~HwbGJ$;23KB2mp`s`p zMTc2o;QTn-3WKweY*f3=|Ak2QVUG2KtI+b?&sW;O8W*-%%+s_z4m;Z19*~Z&LPh>l z!qDv!o?_-G8frl%+-;&ZxCoz+m^I-ofij*U01N%KHK+6WKbh zo#=yThSk@oV+a{yZ7pL(%5`{!syGf17LpB0l3`NgK43JWN^?r?Z7N5AjCy*uwZZ7S>h z+6KIyEf45%?%a8XQ0O8^N}P4(-4LGYP3a_1sRFw0(<@J>z^*J;VHe@E&{9fe8Xxa4 z<8u)lsg;ROilqia5?&IuFwrTrS;R~0tExK$@p-u}-9znqp^Nb`l*xZkjcT_ zw4DEs1EzuWY=HzuUexR5z!HTb2Ax(0{y<5^inl}5v!9TZ5FqGY%bzp@XS?19G9avc zSXGlm?`lGK#OnxP@?Ori%1X{L@Qp{MN}!B+NvtOcV=dBEqp+ef{FNHUt3F#1H1g)V zv`!fy;o$`Ht9z@S&)7>}y{P{%8V;@(>A$SxKO;KXwKAA3RX$~VQc3_{*{uG@@{nK) zasKHc!?4XWZF}3@oM?{4I*rh-yVcc2hTDc~iaLEmwV9X%MqLFaBZX+O-2-~@ilcdtmL2IOh|ubj0V@vFf()#6Ae6}W-oiX%xeP}4d2Zgl)A{7zjMValmXDh80-)H9Cn{UtH*Hg!pXE85rG~o1ae= zN6~1;W>_0`+x(Nz<<7O-f*kV9+)e7G65-Z?j0L*`YINo!A^k_3Sh=c=pCDkv7Dwwu zJF3<4G@tGZ35S}J^N$O!ZhLkt$!s16?e8GKiK!XUpooN*dM5B>T$kml1*AV&%wQ;8 zd@*7}*zZ>s;puAvd9>YlV&*!76A@~KRd&6|F=@gw8E+-QO0-2$5mOFNzAf8iv$=w8 zwdV5uf#IGmrO{A+LM~U1b--#K)!chTFlIAya4HACTfeG1-*F*Zh>f{l4BP)4*HX2+ z=ep!^6dbcBl&re#`z4mKP`QF#ouwR7<@);jy&Aw8HE`VV<#oIu87DL;QTJ>~5K?7` zz_~PX-M%k3)I7+XXP;CVJ}YRMEqatm7LMvRQ`>JvePCwY#gDAGS28{NJj^nnA+Ybd$h5>!8D7f$&s;a^VlWy z>t;S|`M%RmdP>+PIg*^<7uJks>gWe~LfS*RmiL0p@XehH_HY@mV3roKQ&liGu-T2- zLo@f)^ohZpXD0npSfh_&rOj8;7+!C&xYx##2i1GT6owV>4f^)7GmZ@Ez<+?)4>7J< z+hc&KN()prACAYKLBJ|14A(-}R}WiPa%;(2z|Yxqk1=wF+B(ohkzW51n!TFNwfp0#gq;3ZVIo z5Sb`2)0_BpV-f+{vd$-V@3n!=z2VJPN3cv$C!LWAu0jSg_QV?`-|bo4a7a8nO?oV* zh9&-hD;l32yGP2_D2x8$UXt_siRruQuOT*=i#(GS2`;u9)=y2e+pUIkUXKT0=}SMr zylIAlo3lRmNBATRv z-s&gKN}cDAQ#IhVJXFwN0t!4H#mRhJsI%1zN>AL5-zdBu96y@Po6lrLi56Bgu6V}L z=_nM0Y>jCw@N2I(Pe%jy@O1i;Fr7~(Qb9@#2Sj486zHv@!u%TxQ0YCHrMk5OxBAKr zbZr@jcrPD8cMn1VZJH$H(6_9}0O>}#0BW4>|FTM_1{b^dn|N`+({~tH@dKdk~ztI?U+it?w-6ao{TsBBHykhTEw+ zGO8$ENvjAd(??!{bmnji8}YcAI#55Y+S5H*eJT>V+w#)c$u|;Gq`F&$1p&+?;>0+n zj_C#UBPuP>(`x~s(!|An%5aI-JVOp}(W3=Ncm)e~1k-1Bk}Hyu8|Tg%b;V>wBk^F7 z>bn|gV1m$Rjv}j}2p%H&<{+rQc$iwCd#HF$XJd4aA*K3CE_n9&^`*FnKtqcaK#Zm> z^#fdSy|K{%5`1Jnv?ax_Dz%W|jJ+OZ6*kjF-FOJ;K0a; zN+q8}eaVZh(Td);k_(cX(TtEEw1)6~W;d3~xky7MUZOGg9hfsAbbyoc?r($G=kz4= zpPGDeh=NLt2U8AN$}Z0e79&UeWS?@nx9u#VOa)Xn_&_t|NdV|qU_oa!kA6W z>J`?LPOU5>LyQ(etM-}jZJ#MMyOh$>}8jv5Y)o5it&>pZlOPdM$+hv zhdkn|jb^P|g6p;8he!uV@v?Wk5$*fYMSNF-a&tK#Ns;-fQyy;aj|7{EruG|P?2S%3 zHt(ip7?ar(g#Eue%6|9Zmf8?pzg^>K9o6ho2u|FSos=I-kHC7D^%6nN!f{hQt@n`g$;3=~Yhp6AVXg<9-eHR1wm88vJk8>D_nQxHN=aT&?nKPZP@#(CH$)ioN- zdmY_-=op@xnH_}&7vN;`zG0Ud5?xO0qP4b_cl3V)EU?5#s0f)z%NZ$vWJTR$kT8NV z3-8Ae2@|qTC}G;bw#Wn*;~$XG*tJxfEVnUJHmmVr-XleqXY`)ecoi-eQQ*|H+|-Fs zw#gAq*#r7$9QwncuGg|@xoCI<-FSr9otGv0qdF6|6)Lwp9+nJ<4%=iSzniG$lWX*l z;NxDQG_;L%9Xf#)J6i+-&wDR^H4W0>|L<_Adrq<8p_e9sdldH~qxtQ4N3TVZbw;W+Rsy*v^f6_crU!olq)q-Y z=0#?il_x*iGI+GT%Ew}B@JHo?+kxdBk!p(el%FOS2rjfSc^rXX@tDqH#b=SST zlg)~R4>J-{FIBM0(&oAF0fVrkF_HZVj#M@r$6g-nIEPUkxvp8+rI8Ma%!zE?OILE) zQbFoX3z4um;f#=U%#1#U_v-5<=l(*TD_1M+VK4$Zt;AEoO!aLy*$zMAcKLe~HOEQn zluvz5$e9bVz1mYXas}=$cus=J>p-e0F_(&+`I3|=_Or4)!_?zAbtio3uXjaJ-O(HH zNOS!C5$a!>@PSMVW}vrup7GH9$lI7--fHQLfbswZM7IUN8?GtTg z3om0(o{;!W9ia^>P3a-yf|vnH$qGrj7|2K=CY*95Yf5NfpaDU5GucNZ{2Dm$EevKZ5CF?@G77Ozh+e-F0(9jvRh#&^jxTN_+fG z@^)qgvj_XRbu=d0RTEv!m(me#4g+9OMbFwFFxIkmi>a+I7Clo*&*YmH2hxtafNdz5 z#P>6ZnO^S6!Z7w}EzeZYJx6m>Jp|oXhyw9ZykO_9+Bvwkw7ovkTxTIEtK-)wS{2cU zN7rbUyLY5VM+LN6-We1IRo2w4(U~Hu>JnMpsh-a?3&ya065Io&vXvGLCW)29 zxtmwgL<`h$M5B6gB3gac3SOP}kbrpr&^#=m^zli2`&`TRH>_+Tpa%P>*x75RTw?z*u?tv)REC%AOfqDJf-+Ipt5}D~;?6QVCRg38C zS>vhpOKUWIXL4XM!@J<_mnP^8^G}tf@Eb5V+p-E~TbP~wYAlDiA}p;3mW0pfWDaj! zkdsY3mkCF8ADrP$x%)yhCX|0tuP2K#>1FzWo<{~v?6O5ION*Hokgte>(lYy3$vfN4 zDd7bSW+eog9_&lFQ8N1DhuCBIhl|SO&z2=D=Inh%^srUUZIpbgZNl!g&HBMz`pj7hN~q#+ABc>*hJMCYqq0^cH)8 zEpk@|-`w&W+|YHi-H5z{-G7Ko%9|~N&M}Ve;;iGVWix7**<_$ppjXC9XEW{{>RKM^ z7)y#uu|xe$nkL#?SIvO-VUO5TH`Jic?f2^E+@K0yMCgC?6PxmPkhMn258lC`zM503Bf zhk=;gyZ}o@jJY!A=6Z{49<2kr{hjA4R`&ZiNVgSllngwZPh|lQb@^Xf>d+1KxHdKw zv_mnG;cD<=HIf}Fz#y(u01cVE3jry-xS}DA%N!+*iW?cnb510*>?UMZfpp6!`kyT3 zp(5irku-%jj*JlQ3G+rSGp1uF-DLxLr(BYg zUBWyE9}3gX#cu_ODMmyvNDi=KV)bLuMIL#i*S=!0Dfe#74=N?Ie?wNUKaCcbbY8Rb z5viS;yHU%k+A15Oztd<5__?CR=4gZclFSb8tsyfSm4G#X8QGV+QW%;t=4_7#uO#b^lm;5bY#frr(Wr=0_M03riX^9|1eg1r zkU`AjMyKG&VLc<(tmA;g@^?J`)zzU&!Q|-@s%FPYuWO7t9#@Yyg~3V7aefpq1u{_b zSt8o5+3t`%EZ8fG-d^(+@`Y-*H;HmSO@9I!{0=YE~j7)0c2X9GJx1$ zArgakaT|H4l4yb%#oG+FozPcV7fd3<_i#DuvHO_5SixAlg`5{HY@`sO-*n1duFaM| zKtU0GVIqLcaZCr{He{g+ZPGwOr{D`JPwXCqs?T*Y*b4FncOqH^g3XJv`W*8*}0*1V> zcJJwA(xy@R3W)i2eprMLID!Fu6os#0q!H9EeS_xnr>s70D}f;6kRohs);FlfqDOE( zQ*(B@KT(EwzK}%+GtQkkDY)~#ukB&Epae@IBXXp91+(yDhxe zZDUHNI1Q6vXf$VznH^FiDLC!x1TxK+TE4|mPkM_!bq$%%KY|4PtxRIIgBl0K9QUKQ zgyVf}X1v2&hJUGT650{9uy7z%iT?^2uvz9XXtr|v{Ijv{O(l+Gt(WA7Uu!uuCMMi^ z0mAjRXUqDkx7{jxD~@blx6q6yl5np&E7Du7 z@+)$3iE<~r`$O3|b_VYPgOJ*>+xa%9RsnDen~#^)YqtruWYJ~6sZ|b;_~O95T|nIk zn9ucIh86%qdmm4buS{n_9ZM4Z%cOVu^C1F7)(*-blAon{%bp)=t;2|SY4w8(p%xqT z70N8#qukene`OsSx~*-NI4@FJ$E0*GQFF5ug@p?ld}j*-Cc?bCMoVGBicTXj>rX8( zC;?x;yKrnw+*ch8mJ7^65}{M@QzmMuwbNH*MG!SHfemA}2x%=cX5WXpIcW@0ZOcP` zOngFN>SqnKHzPP);drv87cxfVR$xU#qn`~~F3zn5sU18P{$9b)(-+kX+`AytwaVF` zJJ{21qCTUh+0EuC*UQuUD@$~vh5LmCge2<6uiur;;#8QK1EAaJ`qEjr4&Lp?yT@a7f(QvK;@P z(zfT?pFd0C&tWB8sF@Bc1qOWh{KW1AJi{v?2^h8dK(Qz-o8A3M>QEK& zpcrx^JaQNBhki?Rl$YH@bDh}zk_0gT!696B_G(%SB@BmdHRBx7<2NAV4o=FLx0@l= z*pcEU&;a40RWzGHf6=jLxh`(H2d8oPQi;X56(<&~4e-NtoLbAX!o|Jf<;u(cT=v)CmVPZ~032^B1qqy?PSe3^y)ANEyrd z=-S=PP`S5yL-@XATPRlk_sU9p{+x;FbGg}^GmTYH7~=$W9709~c|aD9M4V?`7$;#2*~7y>WLd zFyiVnxXhdMjj7q)0qC?QKe$-o1&d($o%A(FmW7mWE7?_>E@^ht{1}sw6axOa5Gz z`MgYMXBDtfle3NCUl>7x9lt>f8p%E&l%wZJ%8mY;V$}*ZkiVdQ&~Rb4`9;|jDN$E3 zVuiUg-r9V4QZnlSlOp*#y!+wS(cF~(5VIA2yUQ-&%rO^HrY1>!*atygK)O;Gg!?g@ z3sy^1x}c2-8_)5b+yebcIP9GEta}I1wyw^^`6u+-R@xxg`2iM|LiHAp2f?^_pt;*e zv!#fc6A|e8tsXt)T)OYxN-$1p86?~GI9F`Z5w_T#{VG8jJ)`>kYR0WYIKND;6dkDI z@p>j_vu4~Tp$j#qIk?ZzU4f|*-3g_rUZ@yDBE{i9*-#fJLkgJ)A8l3IRs{1X^yg$q za%5lJE3_g4N$M{eb9xIiz^WdHUTW>y91QS0wK%bRC9`4R7B>?rz!6mF+%)Q=u5yrO?Zkaw;;=C@~tXA2LKlOkuZDjOPo6!2is(c zjydGIUgs}Wu;Y}JU^8CWhtv%Kb;G|7mEjw(>SWoDRrrwbEY-p+Ki6j7fd`I$a#Lt* zWdF{)C&OiN^wbMkHfDB(`dQ;M%s|)#ENyuoFB|=(=_b~1w%JauBY9l4zI*8KElc7X z@`*R=C`Lj$KaVL&X4qhYMPt^^0t3}Q$n{18H7er|NRGjPf#ZW%uUX08)zDA6yzuKk>LPpmHzF2#Bj-lOtIZ^L^e2 z^UN^jMI*s0#|;Bo9QjqK#56^#jpB;iQ+67oEIaVwpV$3cF9q~xx7f`! z!}fR0O7V9X+-7C{tu=$!)d@MqXPX_qjVm=&SSr*3TpRHLk9!YXY1vX`2bHU}*n|_N zNb_}%yIyeNyKUo@jc|Hd>76@DuN%S(6!iiL3#f2(fx3`ri@(cA`@poA`*Q4c7-RF* ziwty^8c*|9@aS@@S+nkPe-1RMt7*xXt9Q3=PN(QV5p`TmYA;S5XN3kkGw*TAWayd_ z5tS3cmz2Mf_ui1cSSzXH!wAU=Ko5Wiistn0#!Y0I&^i*$sL-kjEw84j8r(>qjoj5Y zGMch_O#4clBW48$S6(~{T5s5Fyj=V3aji?@oZN!6PK5h^$DsmZbw2I%pBk10s6Lq7 z{J3jR@V_}87jHN>KlM^Qmu*hcoZ49i_mc9WxvZ0>#bojuaa%m@*FgxhMKK3AJY^HE$}~neuTa`;Ea*-eS*Wv ziNC-~#GK?YM#{X7lxWk!b;z7U&CKly;P^mm7OnN559IVoD}Xjv%h&yu0%M;Y^71nT znfH7wN?4lvD+=B6^3ZYd=FcKeIU>U&#sXyw_6@}H5uPZ%{Y4}S^25W>vL9h8eY6JO z=e+kQ+^(iyuCrlz{qCQm3@kF9R!Y%Gt#HE%L>d!a+R=I4L<#8E>1USTsB(vo@!?Hb zUvc<~GR2>;LOsjCcHpya)_S4m<1va>e32vC*t8|K)f?AHmxmY-4&lBcb$X@Mde}*> zUMZHrcc61c^j?Z>H;BDyiSkC5?);H*6W7`Etbb+sRaA7}Un;W6@4o>nzRqFqcj$6`l%JZXa!@n$>!{bIenKt}9G>NlK7)E0jq3-<*V%|Od zY14;^g=NDNO<&y21})Qz0zIjn*6=O+$MG@srxMgPaDOI&xWYYBn)#`ta48van9O^e z=xBJyPW^hVYBvZG*R{uZ$|`j%i^PJ-ORpXr*I8^s6`^^9{188d(}V-YF^Y!3WgeMJ z;TU=yC7%$|Vm#W(TXs{qlBkXyA05e#fC@NrM2oKe*`BDDW6)59@oV7Z8aUd>aNOF0 z4rRJ^ol*?mwX>LKWzmeV1+Sy?SYhO=0O`}kR_BNAc^fC32r z7^`Wbn+8#SAD%5VA6BR=x+IY0F?(koA?%yW-JdNE#VUSMm&OvVC zi53pKJLav&695MjB7YcmCTfGxx;e7og${sJ77!@NgF^EK)sgog zM5c_gTgez@9*KlwtZ(6A*;eOK1_!XsYsKqDSc;U5vfDe#fFBlL?za&6CEM~6Yk=&Q z+05tyNgKl%&yBh-kx_K5YL{TtWwnp^2Z2z}M}8wSDEA~ka=CWWREHzqC`agyuEN+o zD+@y4i#$%eF4o1&L+V#woOUakaPJ6AbFA#g&f_`Ty-&2uZlJ4cV(0FX@b*-kTedVl zx%oqRcGqqu%7o7N z#DL)>^xtLj|9|1)aRv;&LyC1`U}WY8wETro4pdY>Ug8?0=~K;@{LF82B`9y9qI~h9 zINgY_8`*Kvf~Xl^LxjtkO83FGuCZa!39-x--F0mIgz|yD%PBl;AC_11++Upz|2W$Q zgneS8DeVNODX(eU)J~0)v}}_UAl`xrqs}d=E4zqy^OT)fm!e*LQCg@Cxab%` zADkYegk+HQJ-B5@%f~+ueG{OM&X$3Rk!D6})dVd08oupc!FG0x3f1zdIT}m@99yCY zSa|U404~dpPh9E3EEcng*K(ycK#qfcrCT+&0 zZ>X2bx~hqm4D8>O;{H!ZTB(4W&|;)B&cb~NhBey(*iN{@78+bgJ^pRqNaR2*6woO! z%qj6!40Vmdnztp_vtB*J6G6z{0D&~>rjNz!d1p}g8wo~z`0u^K*I9clGKqRXEM{yP zD!jlWl=D$V?|e(wWI7uQr<5k{4TPgtEzhnRct=dj12l`z6;zi%;wR7**CJtxOk8jK zKM>(OP|2-jog5ri5i#C#zrNp>eU=H?TIXXSl9d~H!S9CI+$sh)j@1mZ`0kFNxcmfe(yCrl+QI(F zXv{>r{;LX8<$uAboYmL3p$3A;W|O6J;_(hhCS4%sIBAAE-+zM`z=aODI<{(IzEU1; z-njjREA)M(U7_;5B_g0oBC0G3zf&8Efr|=pt;l9q{Z~S#AsFZQZ!EyyoWx&`r^0+Q z{j3DfQsTc%lTi5k}ws#CD0U--U-#p#h52r9;?~Y~&^6~lPv{d1Ok;nW5 zE-x=HR49P|E|n7Ybbo2=>flvgv$ZY)-O2>q=TW|z!f#LT_tm+;0Mq$(on3whX>{3T zcspEIyC7W#5TrZiSHPzw(K`z>X<}!8tz_^2L_&^JoUgmOz7`Hg8v&r9C2A`C(I&M5 z0CIPC$Ki4jvzATFI&|I=vc*857a(XEs{?3OQT(0H7kgl@PTf6*W0e5W-lyH2KB%@w zV5gbHxli`V{QPSE5@^Gj+~J*b&abWZ0Vv~0G3|V@JS`7sUgg(=Z@~8zP2VG)+0&B& zcE-OKCt6*7UoE*hbdJ)DcK}zAV%nf$v)P2<{6K0&Qb2Fjs6E+}V zE2h4sZ)C1I*xCMrj4sf># zTFa;W#;MyH4$j}%TdDkD-q)_H?Rn~j-^YjVkMxt~Olkdy%i z9=BHjG1;izv#YmI;U^uG`?KlN91MASl2^HW0eL+fKj0_Z{R1V7rPIjE&QMd%WaWc^ z?z|GhXpO`r?;}eoyc)a~p#sJdkG;7n2Dd5lu&+@&vLE|8#%9sc+s1U~%iYf=xvcu^ zi5o-$-5k$NyQ>T3*bs0=3`4@Be8%GX+3eOJ6Sd(388>;@uU5V}d^vilCmMaK{jjR} zc_>Ei|6K-DAERV15vxuHy#^asc6nwfkFL#jBnZz+hj3)`p z$?pTkGal&VtujA$eO?jsV6k-G%}B#XU_iVM@a|KfY=Iw0>E_U52X`_j@InGs z27mt=Y%|4*FyC&fyhAVq*vDfcJW>;?k4x-9fJJ;_Ajae`i#aw`NG6NfG%ps4s3nP| zonx%ILN4fHjm@|t?M>nPHEy}Rn;CnRJ=j1{df{ED&msstdwzs-4X=nZ4}G1(qj-TZ zO?hE?n0hq=tYg9L+TLLds3T`rLnNiEcd(!H;wzmT1QWu9*UXSAcy8R=R1>>zS7&`@ zo}z|t_~DGsMkhUS zwc3bmjXOd_QFhmP!Vlf9bG}Gh&F*^7(=%5F7Zb)gm0};a8Acb=~JCU=?* z0mtEU!aBnF&tiKMwmP5(R@?4O@8@c`T2fL{%!CojnhKwn$aF+kTbOpv9L$Ijw)(Bb zEAKI9lroi|JkcLBdul6CMk@6=+h5H?0x-j7ts>SfDRY$hv6FkjyC09lpK7n6PkO%_ ze^W;-ji_*A7D$tqvElo4qRkmiva2}YG3$NvJa|;RIU6W_X7I}_oo*Rc`xAMPo)feX z1W*63=g0HOSYt?)lsu~n-@6x%*fM#V1pRRgRv9M4?LrOfIZZ`xA_f%IW zOh-7^F`rUGiKeB#i84@)4ZIGrtG3TXV}|T_QNNSy9`GFlN3i#uJ;wdwxa!@urX5Km0en;C}s3Q1JlNf|Qg8 znHjDfEQy!alB)qv4{zEoT*AvE*<3R#Rj4#7_Y7Cj+PJI*3RC$3P`8M?itSKd3ejI4 zKJnt=E=Aj#YYFVjqxnBN^kva9Ua31k(e*TLM=8;tE{?uoe^A4{R|CyszuVBMz-;nR zQSW>=)m8OA02(|VHY;nZ%F5`lE{fMn-wSd#jdki=IQ=5L%jEnYID}3Fy?aVn? z=!sDu=0+%Nq5Yg8b-!q{tKs!`oN)j^+9`1ppFAy{c+3&9p#W_pjTPSg;4 z(*e@TdL~f~7sZ7JT*lxV)oO2y6ZkufaI3B12GFKYn0P_JZ|)jX>exaT2@r+B{aRpk z6tKPHR4Kwn^y##Xp&q&ZA92pvC}eWS$RJiodvJmJ=S{Z+pkhQ!lpshohQ$hXu zW~XD@wr%Wm><&9d2Rlx8I<{>aJLuT9jgEbDX3oroaCXURldMDcr~5Tl9LLtDc=B3{d#n9#UiiYbXX9F+TEJ9e&Le&tOy0 zhL*D9-c5A$L)FO>neS6LZ5c~HY;WokE^uV$ECOl7APxzg&8tvD*G{*mE(*Q%eAfvR*Zp>mA zP7=)|PA3Pkgg*m9Ht^+(v~pbwT~~xw2#fkJi!*G%7ARpPKdnvDtiSW4B#XUC2oF03n)|ESQHx*7)s zcq|E|WpNLqrqVdGQqhgoPad#R>eSXq$s_Q)5qzS%bY{P;*X}a?OXo9%Da#8Opl=fA zbfHL8yzY`U6SF-S11mV^_I~X%9v545YT$thIZ3qyCl{Z+et)ih1F2CTx%4f?F-(>R zve_a&kN-YFsn`U_n>P3mf#DJ2k9N&v(rr!5m{=~IGxn&@QBPTtn!i>dVePy2C%!$J z>j~|+N?trZ9W{afeC0mhe0wygg@J!J)T#O=9r`2@k@Z5V4WxRHCFteT=(4YxNUA;C z`Jmm5KAhVgp^4nR{cKApkspwHt>@ht`!CJ4Rw7IeE(-F~*Vz{VqZzu@%_o9KaIk>7kp|gVe#{JYs_Pr}4P8r?AsdK43v_16u)7)%=Jw!UEjT}E}d$*VNU+Ide^VbbC_ z`se1;$cv+{PD#ot&CTnO+n+xW7?5a)b#m4-1LPqh5j?|A`B031$rzP4A0Btd-Yi1R zA{_?VA)+bAckin$ZQe$9C$;LmL)YUatne{0FM&)_;Vy?jib-aM+EGG{kc$4vh(xEu zDV`GpPDqX`N+Y-5-VDrUy((z}KJbM~B;DUDzW(UMi(AYQU58q5n!?!US28`H5-Cdg zu4AF+z&=O9i@?OBK7wS6nZVURyU=!;Ie}oO~e-lMMOx(RvxivF}Hl^V^4e-v@5Z!p-Dz z17CX8Afpmu&t52;ZGWs=E@k<07I%7r<^jS?7tx?&JsQJ8X?v!4nwz;5&lbtai~zpv z4txtPu6zIxf8~it)Qp}%j!XCAry#naVVlAY`xbE!`pajfZ(lz^mBP*RE=+Z+a7RML z1=sC_D5|EIZUi52WO#V>74xZp13md}>+1Oin((I0A7OudJ8&M&Hfff~3`E2`1<(6pvsU<<}wQ&K_l|zr+nlKp&;lKF~e?_3D`3F8!Nu&cbV^yVCKg11d|bo?LYcokx;*zqyWpvlIk5@)u^$&&xt@7UzOpxztPC{%@5# zQ(hm*vT+W-t(CgO<^=kytDxqG`4d6W-^-wh#4}3AtwA2PiyeGR5SBjAYouFW`Pd!b zD(CMnbANsX8aNpn8+$fCZ))RuLJskLp5gecdg6q@ z<`>;8tJ}+)iU)qba-=KCPruvwKLW zp1cf}}=iI)~ zSyb?-HAASgU4(BpC&pQiam?)+CM7kTw@W4W7|>i9dV(t8h2`Tkk{q2=K)8#sdUP5& zDDm-W5KBB95N9NWTd|($8nt4Ws8XR|w`N1P865!h-5G3XCskr|uq^O2zn2y;EZt?s z(2dCdW?hLrq4=uBK3_>;pzI_3RB`a76(LoR^IeI&H5idDQ&A;cYlUY zgY!)02m^`(x>=VAC;LTb z?mGbRg;`2wCY6osw#VV9-i!lrO0l%>eX`|)Q<#>F+rug?rXheMK^*Gv(P zn>)(~2XzpIJvo~*HAF`(D+3Qb# z?a*Au9BihXOwMKE36`LvOW#D}>-d)%W&5))vs<{nxm?ZDLULAO$^32dSj&s++pNKsN&qRel{ z#0&8lFR)be!Qou>_%<`s7FRq%b=iqY?E?I9$xRz7c)KacFYMtxJWH$CyJBJ&1BiIcq_oJ(44vwRgdZYG| zTk&J5)A688n(aKox!(nT{4(RVc_1P2EP=5&f?V~4OLTE#aEa2mx`Eg!hDv@?7qhr5 z!Df`;+65mznfVFTR*I2#23tIAbXaJCIZs`&c|>a?2ATV{`1~5tq)46~bCZn0guAgR zHWVfal~YrQO@5Ya-r`MJ3}w=NKAh3n8cQa7q%q zgRf0vhvsf>TvI}TX%b4H^R*J*HR`4!u_KxTAjTtHQwPlF?8Uu5F zE)OQT_77Z~s8p^o@_BS(Rj+G8K}>{pX65fpqZC)@>eI(J|&^mzN|{4QKx{OS&K4>uQ6sTJdsLVi(3(6t5rqK_;EOHfDH$( z-1(WznX^0Q$P9$~9|4W`zjek1kcdNyA6>rmyb?7}V{3CB1&tMax;#{{ zqg0DKXGI_WLT!NqgoxXcC=CZa{LI`MokhMNzM}KZO!d!jI2RXJ2Un!`gfP9M+~qOK zLDbAmN?jz#T=cO26TPaN1N7H;A84)_SLZ{jaLTl z3@jefUdjE7Z%?P^Bjo6&ChEHHj z1{U3tY>@0R3=WU1%cc9HCq}0Q48Zs(`~0$98RX&7*Trssoa>uy&m=Q^-gZci2iIfL zGk}kMW}>-#RR2ZFb`3u2rqBS0Jr+P=z8(&L)hVgb@(7a4Ao#1+d+(g0 z`Y9IM6~(U%Q%KS?!P89uKZ8S+^qfqC8rz!_3E}q{*M(RAteqvBt=?-c-2TOG%>M6l zrX6AhYsy=Pb#h8Tir_g(J&FSUaa>P&tw_zEQq zKeSva{SiN&^uzgP1&N6ni+SGb)t{{%E*XMW--D{$F44zPhdA8ZVmimH()Z^hTO!uz0c1h#kyZ6 zGeVa&fU~WjgIj%}C(i%uYnrR__B0wrOAm`?xK?zYW#37|KVK8Hk9w!eRH#O_P{P4v;-I3a@d9!*7Ow%g-o-P zZM#jNU3*1m2$J2SZr|^o#`}YBDi?UvYF%<>U!Y!e{bj$q97{KVr^A4R(lFCnT)zcg z;c0$yiH7bd#krx37c&6u=VVnzJ+~1!TQ#fs9+&->Cg6aL%Q+7~V!WRuQlvK`)rnab zB0m$wzR6Xr^O4ij{8s@5H#ZhXTWPrd%$7QxjrPSyouM-Ux6uE_sa_Gl#$tU;j_^U5 zNzq)A?ZLopoQ(~76tor8=?n)#P)p1c@Y|S)4qQ4%Sd%M`t9eFuU$U&_46v!u9W2DB z6Nd#K3Li-?-gBB94FsNi-ovZs-VKl4^XlZ{!=72lI;P5lRrSF@BGrY23hhAhP!jy9g(;Wi>r2u z*W#WYG{bOp@#0JJ%tV1DdwC!Zy5s@emia5M_qOoBRJ)AH+=s!~5BTg^CKUPqY}4dH)2^AGd|(;e)SUg_4Fctd480{5~GPDv=@Aa*z~_>YtE;sGuv z#A>0nrgB>o)uVs+@GubAi!@(>007xvqfh^SYQPpfbG6^6 z1??1^eX1I}d~IQ`bF1_29^3cFNdnFO3kmx3w(xrY)F1{%X|NlRK^1H{Sx33Qh4Gk@r zW{CBjD_GDSU0pdA@H;AyrY=4}y_2YVzP@2Id(tVX(S(F?&DX!E2?SV8Ow4yuY|yk+ zF6XDZJ%{9iSx(GfKKb-F1z{|lrOfs@mDA38!{zL~B0IcR_y?5&B4O0-&taqfmDT7BP9@4R8;ihsFVl| zXF!M?zGlk_9uo&lPfzb;-S5NM*_mf5XujSM`z%km8k_pSRls#p(qJP-?-J`^r3u?S z>g%=>L{KlqW3CT zFNCX>xdNIE%{D0SGe~}WV`*{BmdK>A^iV}++$X{i1%i#aB{yP0FkPF!eKIv8cyJ1t zo<9Dt+rN0sSUYR&!YTyiw{$gHQ`;7SfnLAw%MV85!P5tXYuj~ngwEId2cs~pzlK7E z$spl5#2QdOR~M3}z$Lw2b}n*HmH2N<}BNH^04Ez>2_!I^%Inksloc)-PDaz`(0zkp-*f4A<3b=#>^qs zdBm$^z>jTuoRZ_eS&MRej4#lLn;}{{R!K4&86otNo2nMrhhWRJzY-~g)Qc%JLz_Hl zd`Y?}NG?<&HC9?N9nu=z{8fjR^n`fZIS#Ln!;zex}cE*ph) z#8DB&s9>GzHT~OhIMN-Qm2bvD86zEk*1ysD*1Z|MNux4|SfPH`CadJD=OomeIxR$i z0S&FoQRt!FDhZ6KE8*d06A@#8==ar){V-5Tra@O8+}^2vz-e!3AR9zR`VWqO@!ua} zn;*;KlY!E*)k&hX($6CBWc0MK*sK*gEM>_%`6x`qS-R|+ z6G8hDJLSby^+a9aT=;>guCmWi=xpj`dN`}rKLbk1iA=zJ7knO(&pug5L7G+FQO6|I z(`6O3BH1diONRkLyAfb-zl&?YYYK>QFEpLmt7asOonES#^iON1F!{qpvxdOO=Ex01 z_4%iwAPE&t+b4?q=!%ZbkrhPin&!+-Iurd}fE-IV$C36GIXETKwog~Eid5kCNJf1i zDmP^qLy$Y)ODw6A_+8^tZh2D!)h?LkvlDWMQhI9k!8tmxovR~*4~v41?&9E8gK&u; zgj+ioFuHWGehcrr?kjhX_2R1YH~5PB!sT5d38oW_|C!?uF9@c=ZV#C!dTRBa+d-eQ z%P2@CuAAkgkT{ZDl{3KFC~JA-4a;S!ugt+Fi{L8G?2RD-8XqAN6U2=K)|Zp{@k7+m ztR1@I)LK*yWTLQT6|EDrewqo=sKEP9JELF}6Qq;#-akxMJ9~B60borBbE(~$l^2_&z|v<88cE zx~9xuQNfmU7PLJ%Y07;_C$(xGHCZ1=!>*xP<=RLxmg<>p)4F7ti@JzrFW99d!GWc1 z^dr+H6GYrDlo5vZ^ZUiPk{YwG*N5-KFh&Z-HWU)xPS)o?Zew=;BK@hA;h~bm8ZeP* zTmQ^t@ZbgeX#WsuQGgIjtz4f58=|Wr9udyUk8B~kI+2qA+r-v!)is$lb0w|6&`!;e>(a1D)JOYVNFX4r zw7q^<(ynn+sn()h2p6s+I7;A}tXHu=!@y{6E>u!XblOxKJu-?TP2RXJdsojhXsDI9%UYNoggQQY~*KfzRZOh0*N+?-9zLb*Ecg=2ru6)d51Pv$X@QZP*T<6>qHQ z@}vl$#S|Gwt^dy@^q1P`cdKA)xC350+}Kt!2}Ll~4^m65ubVJaS6$>2%Wa}FET)LA zf+Re7dCG1;T#j`qm8C5iexAx!|JbgFg)^$WwadUz3_)(r2qDArY{1L-ogW~unMO$I5pe;$?g8Ru!Q>R64Tp<1rFaXt3^eP{R+0Lp*+-)P6d3v z#g64-G^R?OK5g!yk#DfdD%!yh7pQo-U*&zt30W**{q4)IuwN+&bgotr4+vCQrB)wq zPZmSG(3xD$ct6NVz(?FkjrOfWe_ym7f7+dgJ1D#Cd%Uqhv(>{1GV~%TJyo4JEZq)c zo@=xS{rtDIV2|u~$0Zy5Pck5Rb|N?ioSi9;d?9$=nRZ1J@%PE!O@QbO1N$)ak* zm%{t1glMFsEf8$-y+>JnR(Q9WklofSYms>hk16L_3d*g7mBoOh$&-_xe31bLHoCL= zB|4k3*xP9*y^Yx?S8LX-{Dc@3=~4OY7lmBJzNz3=$9t7m%QVU4z=;c7{*e?8y4s;* zTJ!q^_nF-Pvxt|YcEimxI0^sH`aAckB!gf&?bQV#2A zKKqeH-Xq|(KnqZP=4ri-}()G=gs;TZ? zt*wJsn0|nw^t_MAudmGnfFn3jT{VOtiUjk{Z?FShOMj@`DTrwC677chJT6gL{Pp6`U$bnp58YJ$mze^A4Y zkBNgS(@A*ux|}oO1<=r0ETc54K7nqe6v6l zCmL-eAdrx%rErczT4C6Out=>SUkJG?dC;uclXUbwf$u0md@sC1z^3QLO0Zn|vvOEv zIvYvexNtaUf1ypr{aanxMut8z6uRS)bx}o>YFvhiwwKZ*3I|~Od)Ab+u~s{eVP%=Z zVpezVvAZLOAN!>8mC=gtB#~X&;sur1YDm3tX-hCtE6Gt{^^yXH7xHn;fT&?TV~QC= zLP9071~ucFH)L@HL7Vs-m{}|nqomxV@dUT`T00fLymK1al{lS)l;v~GX65mcaoHKI zbd%=?KH?7@Xo6FUw-Rc?R`lSV)S)LD zJ|rTmN}PUZw&+vGxh6Evwr|K|Cf<+uFi(kP_F~v*OoaHul+Ex*ECttY9pKMTM`jWp zvy;{2Oy$rbHLC9+`G#dnX*p?|;D76O}ibkiC`CSVj)4CdeC~nWoMzNf< zqv5_yYB5LQMW85Al2aKX73V_egUkRm<3E;q6?S%KK?B=yoT3?CIdxqMqPBtfLk9*P z4!1!52SDG*4(a6W8z>RyuW_!fE3^qNrOgL@VEwZK`kz`zOT|^7dqX*;G$@QHN=?tU<-<4~%9F^Bwc#8%oW%O0#O^O$ z=;O(6{AZRR4EQ+vOAYtPlgdOq4sQjZh|ZjmVRUt2cewgx)fny_Do)iYp*< z{=v=O?XP*OyxJbl=K1>?|GWsk+P9F!Va_~FMJLyOlr^>(+^Odi^mq93$*ME7k*B=cz2zsR@w5`Fq%ot>(E zEr)d$I*s-QJ*-MX*qfBhf;-vD)6%{LuI0>;1O9S(cLt&D&8dp>Jc0~utww;GMpg=; zL9sEx_&oQSok|0XM}n@dtEnJ$0|WJ2Da+Op*`@R-kDclcufgjuwDc3FM5`IfO#g5@I^;K^{`I9;n`G3o zU?b|xDBdpR<$~W9F=)!H&p{0aaOgff;quz*aT@W}@_tEzu$3s7vKVSey^QQjZ$^J? zcB$jTz0v({3YQ=W2T|$}Nq@DY^Nig(vYnk+_2Y8;#WZqm_Ps~6;V^$IVC>#HtmXAX zHqDp#;&m`2vtsU}N(JY*Xf~VqE#X!t4~sdJiv?$7_k6>Hsd}2BfizI4pBckJel)1p zk;7KE4S2X}dbxV$#a(^NlxE3>pGlt<1r0O4ZT*>7{#3jrY$q4D@%jsk9G1$nJw(HQ zX4WZZeTI{cm(>VR3i!!ztWx9IH}~hpkgXP0R<2rGn&9#ZuI$o2Q zV{X>=w;PNV^r)3^Rd;RpMlXQ2De}tgdgAchc7%8qVc7&~VTF#ykBqZ368S3kvFGg3 z_+?1r{zm@uUXKkrA7Vx=;(Cj%R3%xdqXd_Lc8F&#j`5Sfviunbgr0tj@#~DBrNJ*e z&Kg8e$m8bVSpB`*DhJw!@sntY2hAkz7LvyqCIm0g=2%~@_WiQa!VNRuZV+TN5{&U$ z9NC(ErK00Xgrv^bOP;M&1VF6r@jEUMjyU2K{Y+5wJBR{X1L`uL-sn z%TP`+?E7zQ>F@BVKX{+wd)7XHi_#YLuN&oGc*h@apsf227yUo@lb!r}(-SRwu;OGa z`cn1~U^_ux0rt*6^v*i~i|S#^HZl#66P8;P|EnkR2M|JC_KqtA|D^5S%`;D^a+t$G zZ_a_d3sm?6t5N;}C2vQV*>s6EugHJN!~Z{q1cZ0K-`wXHnb91appo*9|BZx~{ZXjW zRSTEbCncl@#h?0bdi{yI$OeCK9!v-~VPAu@O9B5IMEJKQ@nd?2+Wdr|$J?Ps@SawH zU;mr9|B^D8vhPF|WvTvOX)GLIfn$ukXuId{3b+c`loA=dgH`?)kZDC=T;#v zg|feW;Ug4y!%O7Cg483*-h!SyL=C!jlSZIe4N2e@2b5dj|F)*@uo7l|SG@)syg_>B zo6ige*kX@uA3Lc{)R|D3*f!QE%Y&t)$zwO`I4x{ zkE3km3$`HKI2|*iM0rPUbyl2Ef#MU4#dKyp1%Kv$Q}j=MlAuub8cYl>v3#1RfY(se zJHY!gIHdg0E6`ML1v#`^DiQvrmuqV;!F?m{TswQYKafej3-ZR0B>H2JP5139&xm0S zTxO-_8tGZYs;4IgtS15MKaGi6~bE) zHBvtdHY`tz^L4^lGy00eQ!7sYO9eQS&C9FtlVZ;}+h$Bf8GMXftNXs~ z)(`>0uZLFXZ9K0JEENSVAkbYb77jZCAIu`I$0n&Yln+PcNdx4z=c`=uxQ6H`V3Qq?gEADo2HExm?e%W^ahckv2*uBO{!(uH$_* zETccpsFv4mVfM!sF^m*`g02*P%5~s#nZ#Kl`5!w@nIGz;oWTVtLP6U12wKob6zuZ8 zrCotA@Rp%JJP!D zjNBywKSJ45eZ~P}4s@5dD9cqGYY<|RcfG<~rwj?va(^Qb)e?7;=f;LgN}zPUFeX2pn95h)APTNt)G~xe=Rf8qzDqkqC5g@+~`6METnapC@DuHah$i0X&NX%7o|qj-TD{DV7WYm!G2 z!KkS$^Bg3?6LDe2dKTzwx&EVhu3jaa0EFJ`HwKX20CaZlDp}g$5{gF5Xm1#u(?s7g z;Jk}T6iUj#v~04%&uhhS249MzZn9H~yVaU*E5SwB^?^r!(^e*Zip?%XJx#k7noa)E zWGX9eVVVMRK=H|Vx_g+LD6T?IB3o8L)WaJ;eygz#1o>@X$A6yE^o_*N_-$Jfvd6ug zZAsA~3i>CeHeN#b%r_75kENhRS&&dTj;k5WirGVoy=py_;5BF8gAmU4c2G=g`X#`q zP4M<5fY#*FF)$njT{P$D{$@_TB;d$zM)$gbCFG@6# zD^f_yE`VNyksRJ1iT_ZU%B)ad3aE@z-o3PtcP}l&P#By}3dciR);047q60oP$fEfA zI*=HRmSOjERrPmrGL~Hb@n}|R`5EbP>H`d_;ls|x@YV=uxJfiv#Vt|F$AK8HfAG^r zCwh@`KOT+)%uY+2n~|;PnNP6Q`ZUaEvcPgp8fj^Cl5gt1+N~#yZsVCT@x5vjYOUdk z>}bGWn$#8_1d07KB~lT%|<;8(D)w%3<B8_8`yq z?`oUGK|%S`^bFVU+8_G?V~E7D^{%#bdsO2BGB~g)nuV8deUtu{@bEbMueiIUiKwz7{d+5 z%Ip9lFO3hbyep!1@P?Zw(?yGO!1P^ZZY$Co)kFj+{U7#=at>66AgPTxoU<9LYRxB> z0;9euClb@q^s<$l6imv(l-?m#f+4xSzD!9I&7D+jrxX6M%(Xxn&C<)Y{rA?ot#U*y zU>K6+p?yiN++R2O8Se?ZKQkGvpnbKh?_Mi<&p2`O!t<^? z_iKO1=BE=egceVJU9^T=LlBLuqvxGzMAO>-PffLKkU!)X1UXRpQe@n^Lzl=ZBJ{h) z@fQqelJhg!DNE=6PDZtbn}(NjE`Rptu-bS;u-t5xT!XOnFq1WcC`_5|TlW%EFE`-z z)562ehs)e^OeErmvaW#_%>zDaK#fyVlP$j|!?a(LSEhpS(HvxL$WO8BQ{$DGDaMZc zfcjFL7I)ikLz7w;lg-~o^ea?MniA>7j>k?X8P0e`^Q^zE0egF;Im&3&B@h}v5aIw$ ziH17^HzAI4BP}&&whMqdGXW-H@ti#bK5d)s?BLc4gY+Wd%XooicR3(lp&ULeDsy6eK7l$_;xjJ8zMd!`9i+kyLXaoQ%)O=miOYn5&m)Nvqw&s!lEH&o-zeuc$CF z8C@mQ*&c0zxtgl>F>|YY#-E3WipZz1Xr_NSo)58%+;6j9j8!JU@9*FqJf zUBKB@@(`uBXkqlzER>I|Kf z>GzgO92~r7dayXR*3S{V8cZJpP%YWCfB~$$9U;r9xhfB@^B(CfTjhN56*fgyCR|93boEr$`cM{gi0B z6=zv(Yc)r81(ITGB{g4TEkoq;*gkCKy?b{Jl_*JromVUFU8^vKd|EIp-h9vmcfGzN zV(H%X?AsBV*ynVgcD@gSsjUWxh9hbi5k!x3@UeW6y6>#K`D9Ip@Q)aD+;-1vJ|wjF-19Wre|F=$%_y@58q;tBGG7 zN$vNLwlJKaYnrGyd{;Czp*I4XM0=^^#^sGv2ch7oduQr}H!97ydgKTgWT{%h^r0qa z+)VqLKAiMc6BTC;a;6hd*Sh`Ac>^y#0XDsuXp9@~ObLSd4K|ON+P2gAk_zESMqK(E z{XcQefHrV+4DN-GCcR1#mtL`)T~^U044zy2z;x7yL&mgmR0IT`1u~EMbB{aWHrU%= z#zq~)f?xfZ1!eX99UQ`#RtRm;n%JJ22$6S7Ob@z5v^DH-BrDp2El;0nM!WJVilvp{PNs5E=CItjQzY}`hB)SH`vjZ*c+vYP-uvOZi8*Q zTY8rc{Ukyo0~N5<)Uhk*8vLU!5%TzJ@T`Ma--k7#Kt0%0cdy+Ov*g?i)soBE6wK}8 zV%N{TB!}erN}-KU*eeyq>*vu=)0_aK9PNo2DmXoC`mQ``EP8%DJoQi(t#1o$wMucHj>1O1VU$9M|7xDOlj-h}SBtrfepT^W~I|XD;Lz zq^sG7QV}g}y=dB3VbkS8@0~3R1J>DMpbCqg$^;6Eb zE>3rT%2x-)Dq5GNHDi7HMShyMocHx%o z2$;EF%|iObuP&#Zo&tK(?vQ@QT(CzXuQ(a7y!>H<&=Kk2hbr`SJX4UuJ)Jx%c)Y^( zpq0+O`+%cX#X%wChZ;qd#kA4ZCx`s!fA-DD&=vj}$XCd?-E+*Kyr>&^#q!lgmF%|L z%(iU~pG2cB=vs0OToweWKhTz`W+KpzBiXqX>fk_YVo)it*4P@x!ZyBTe~XwO3Axht zgOrg}EFIg`*wY;2Hf)390@mxTBRnqmIIDk5d8J}AO3?}D;GWGUNA-@%VY!(xU#c_rZ4 z5ZDH{bJ5aLh}7AGU6&NRZ&qnsQdX3>=y<)@bkAkfT#peHd`}*fo;eh)&>gt^=oqvLf#6E@zp@Ix(9A?0>B1QgN47a(P^*IvGXl z8lA^Hd-q(!6}x5#Q{ADgSW(vp&Pd9}B!TbHvl|W)d*a{W(SjlMM_=L(5Sa%B%9)RFuw= z?C_&|t`^&KerBV^D6LyKk4h(x?6*p;4iOg`6;@2xe6jz<71zeyz0)KiR>WckmiUKZ zAyQTw$!yr1kZ+p-lO+k=Px1KkU4 zqvK6paC{1>fIYpO7&oP-bmvXP--ZO}Z0tq5(0=Q}mR4pJj2GcZy{jjmpHkRY5F$ep z-@_`wtY%|BCkD63#-J>AchF+C^l0^2e%Ibz?&q!H?w%GQe6VipnW3v(!S)+@#SAS} z@5`SJbz)>g2^`czi-wRoBi(TFzg;)PE!R4V9Acbo`nf8|g2K+q3_irl==cMVz(^*0 zDRq77T?yVmc{7L%HgH9f(wok>x z#*6)yb$$-q37uMKTAXR!FJA!WZPRA-{7kAvSO!-*82^OSgrAA}%4ZOYroWSM)KlNQ ze1`dl_aq$S55fzg@(m-Fg~Qb3bYzR@tuTW@yWl2iM65R=0G?Ks^p%O}HHD|0cUI~K zV{NIpoStaDq~cG;d^gsZ6a8i?kpe*g1voPwa~w4?1Vd2!_F2-rRpIX?Dl6+@LkNVMTwmIMosWjT?>ikE^ORX;nm3_l5vkegWhKQekDt zjx^Tz0<(W8cJ2PePGb4ZfS6K??iaEbrMq($Jn5MaD3hOxOQMJ^Rg@E}5sfT|^~EJH z%DveO9a%DMQw&1;8gyfYveRaPgcF~X9H9fE35)3r78+`=tAKm8%SR${Jdc`$o|yoPq-<-0(Ap%(V~lvrqDbLkM|F+=)Eh? zN302P!03w6g!2)<3o#!CL&ri){8{hC&&IEs#>&Aq%ZZ!y#X`Bu^M2d$NfN2pc!~AG z-^ti$2v5smQ8Vd#^a0tGl=-kj=6kavev-W3CH!Zn*axJL(JbrKKZm^t%UVN^j5|Z4&6Y| z#xEr+{<&VAs9{ZR;7(v2O4nE3OU06a1G|&=ax#3Z(<`XM8I?q4n@0E`XrOn&R|rKO ze9G#gFvGR3LpqbKI43Oo74NW`DC_K;4(@ml^*3KfblP}lU;cH?y|a+tmuMGCDrEHB z@Kk228{d5dn%PR;CpMi2?$;>U8*dhWjB;Foq47DA= zA8Eq9<4zCi(9EZzqEHx-oV{($@^sFu4A3nPSrJdL(X8mOd6$@2Krb}pqF3$->HudQ zoIx=P)B;w{j+9Tf4w}P}Yivqf3=DSyrAnmEnV=PUoY>hhTg?H(fg=LL`yB`e5OLcF zAr{A!c=o&`AggD|1m#-a%1zt>XA|1cp%=pePiX7=b|S~7-gxOf@G=-HNc!2!XMlBY zvuvQ9;3Rw@U?+Hi88`L`%J6KdP^{_CAXA2d|62losu7S|FFobk!gOjJ_8yAeHQ}%blvbl3=)P zuk5v()%%wOH{K%X>V0I>{2I7dt{gDyWpgvJ@5#CPP&nMuGfhsv;Z;)`M72Jl<$qty zc5dWVPHzuSCGYyVcO2wrix2p0M|TNR0Y?jTfncDh0Qh;VNu97cn(vE1avnzD5w&l8 z=Ma%0|0yumN$Gwmsum|nb`>k0r(P7Gc~*Tfu9e8L7>M{Ppasx?*!GgCw*EzPZ@zCe z%3Jn6l*ev<*jRbK2EdtbDVM*8tNse+W*~zNP*p#j!Ya2v?LiTIkH2@1{DRu0XQS{$ zXT!Pnt*0e6xRXkPMaQqy0vrEbQ8kYg(C~J{TvrYGiBSp{>E2q7pj-uY0w z5xpk(k0tf~3pY(}m!pm(89Q(9s5u))+{k&W3-)%H>(yFMgd;au(1?*U%N3XNX@*PM zx;x^hGkwqyf(~0!pY5jlX^s$&mbjOGI`rlIovFw-oF#6&2g%-x-LK!kF+I$|w$Dj| zy_CLZe;Gh7I&}WPmopm{DJzVJVnIzwRkUFe5OxsneZD-tVypRwX!HkXN`H@&bx?{` zAPrIJ^IdK zDKzmygc=39%&SXWcI5_Quc^Uyy<)mMh?hPT?dVzLh4lS0;LG zb9(Vmr?p8&Y&Rd4{DbUnZ1YJ1Eewt~V4Tyv_qzB8vrHZZY_kh&<{9Fsek_f^2Lzn- z?!?~p-dB|`c_I4UC~{*I(&OUNrzf+&gbGYPXO3yFO^f6~OemU;2!l>*VP~a{Pqm31 z+9pxfTf>|4XgQ{~#i|@uCCgw(2v!)3mA~K-cCQXRxH)Hjy81uVePuvgNw@8U-~eaO_8^G7(z*u{w6BQhiWkUKiv!SzLV2lM`jSsv;h3 zDjiYYtE^kTxjh#+OpjNxUbonpEE*nj%+LT{|D%_*P#!yIPLZ45LsN!a$h7W6mHX|D zD?EX@-%0)JhDgz5@f6%%m)obVi`96veh`nMe~1Yx8S?Li1Fp+zR`*P8k4@j-;8_GP z`M5Lsk2)nR`n~DSwV#+F;M5KGY*}V_kaZEpy#oNJee0KrG~7GADQ0w|(l;?3Icjc- z(Ba>^r|UD52dD}%DjQqKZ0s+h?$2v+fzn6*!A&KVU+Oz~)oz^P|Aex=!CrkjchEqn z&Ps1&*684WBtvCU!Q?Nn9H zq4bD}Ymk1?X7hO+Q4T#^1pyJuPA-X$Cx})%SwVQ6Lhs2bVCU)e^JbhYkDO{(@~8<# z0Q3U4%=&#U;b62?UsK09-jHY^qAweN;g@7-U~dIr9R2Zaf3U}WOKj8;sxko!T1Ye}3|>2WdyY zCGu&ZQxbors{OO8PH(ES|9Y)|^*}57mWF@Zz7GGD=l}0-`PQZG*Mz%&KJ(Y7_+Svd zW&GiMcMboCpZ+y$y3fOI|9bcTG=D2@`JKkWcl93z(!bvYGkS;n*IxfQqIC$Ex2g2a zbiw-Hr}D$VCEnkwkm(FZW-eC}`<{wOXjKx0Eukvtd3uJj!MmUY4OM1>sHqI+&@D;8 zi9LjztE4d}{0WAFtTl^0B23dSSRC=9ej&EB+Jn9JwT34mB;)?Ik$##7OTW!`ZiwJa;q3j)&V}?$ zS+HT>2r~y_?(q|R_FBAl?K&GQ8|?AH>QfHxr(${OJKYsLE%JNq;P zSSj~=t%cs4~c zU1YW;d#KsFylr0HH+_nD@%&{N{Hpux7ZytikfN73b2PgJsG^MM>Y>@xApT3?z9PJX zG;rbkORB`HI=|i_G1*58#G3~OFDGVI)&{m26SFB7B5+1s2E$*A1WW+-Kg)Ev&M*;V z@tK)_u79qA#KMp4DoCr)@0_Ef=adIK3XPyMl(l2BWAI_1f!1I>?v6czsWf5uZ5apD z3Fy!i$Szb{!voP9^k6~~kun+wK_m@EejbP6w;t66|=O(@npgBH{nR8GBURbG4KkGthgOgw| zw;q?KhF%{g%1Ejwrc<5!PnMV=-R^L0QH^U(bO^KY*v|Zi-=1!UAg;9?RcfPuIi{d< znB@2BLzd0sKOIS{BABTu7W9XRpr&Ln8iYB5%sjq(Y75;tNQx2$=qUOw2hdl8LMqt3 zY_ZNR6wjoU9M~aCM1f2TT=y$at$SKE-?4`8-%e*yYk)WX6HUVx{i~0SudeK^1Ra4_ z;xMTt&ROnQ%w}p5>yM)Eg#v5~QLXXk1trHv7T;U8FHhJ>{T4?6ty(fpH*%taR)7H- zLaB_+aXj<6A9z6JdD@oYl3mW6)$pTtvSxs!Zi!%XpsIgvMmm}je5PQvtQhee)}*&LM^xl(gM8i%6xdlp;`LZT@i=`A zk)pSC?pVVoG=RET>t{vIX5d>y_o!-($73LzpI1;Hr-9LI)wD4_F_&yB^cVc%8|er2 z?IoJ&gZgF|8>Nc#5A7Z(eEhDqQq_W)_VO-zRb(nk_vNcPVQ_>a$G&4}@h7V}u1^ti z{Ka%WRJhiDkUl{#KUocZX5RIzmHDmndiC8eXM2wbZflIJOs&1 zeb1Q>J-<+(`mE-L8>1+=Ytyf$Ivh-M=E(4q!)@{(@q#voPuz?>xWC|*r!}f?k6xAC zIQb_|cLXspQ`v!x$T*rb=E=K zF}!0$&RHQrg*qeB7P3;lIVz*C<7+P7B1#q(_i3?fl<)Y@+MLK?MdQnTGrb95^_sjx zLov`E(JJ2&fMt_Sm3*F}kDG9-_!94bojy3yVf^uP+k|H@fLEq`gr}wS=Jfl@Hs?{P z(DdQ%l&E^~>@D$mFY!m)^g!}}?=3PPmDdts*{7Eeho=i1C?pbaH{MQtW$4{(Sc$5E zPnjC)()>)BMpT0dupHz&X0yoVH|0e8ZX$5RqQq9SflmxfnPoNvsIp}RhS$P1+urP( zI)x`|^oYu%_-Iwb7z>OcWx>bK(mO%q)b7>L=IUr7*0RkHfT~gG8T!h zc3@cSnLp>-%ktMZC5Puz$s2bWXHq7du#eVcNiAb*sx~C!iib0^>YHJkvInV!i+|x- z3SDv-mRtJR`U4oD-$>8JO_Ja|9$I@@r%+%;K+LQ^+M3X^Sa5u{N|7uiF``%cP|C2k zv#jGos!melylCS)GE5p&Z7;$$iXErKZ~JLJLYt{#Co4P3>cLjS5zWMYEbQRZ6>o&y z_oX;SuM+Z$4wrbcCmB*ot6Z3OUp|BTdm|x%@qdMW!hu2k6nAb#B+u_Lx5Tk2TgI+z z$zx&>iV2~_uNJKF)&$xoD4=r|ceqj_q`71_C17!4UsSS)f`y*h2kxh4bE zBEHm7%HZ)zN^}94p|~W$7!WHcRauR5%K~HA@~Y%Gc{J{mkaad;UMjOmxZytUy4+7? zr!IKH`fG)CCPQ$(j!R(sb)>%`l|e=ZaBA8Ym%hTyZWISj|hljh_S z$NF3`K6V?sfl$hyZC0$nSjO=B)-IRmJM_i}0sOY=90x2U_Dd_V8k&Nl+6u6z(`=JP z7KY`xH-xf6X09T{>vC&Yl-&#pa3<#$wj>M;(tV-W>%5teNJYS0`hJl@((xf>uE;@_`2J+h1*5xP6t)J(5jhU4h%L}Inaf6IEJG0VjY1zu00@9!`Xb!{Yd89A@_+FM6e;68X>e^A9?XVVjH0SWU`|rP8GL z_q>u^?QmxtxXCQ}P%&rJhHm1pVz-V-H@m^9P~Wk~Z)QfW#$2nsUxM#->t!AT@+hc< z%F%6O52j@p`de76IxarwmwWXm8ei1|G|C4_K7OCJzGV=(FNR*$oQK<|3#@S!;bO!u z8_7Q}&c#*ZGr`+MtMt|U_`BBqxt!ieWPISEcaeH9wojn#P=Pvh0^rLJ1Wk7|F#dHP zqUeuqX`Ak=)THt=ln>t~2UFofdJ4g3o>Ihw$k112Bwj6d_8INzDq8T?Yv3uyi(~h@ z5k74jl-;(o0A7~OdOf+=57`C!W~{GmwX zupPyJ+iFV6PSO-|>iEQ_j_BrtZk~9^x#Fv994oK!4EtoWm4+EE=Kw-HhGGRQJ#jy2 zY*KPv=dm3So&5gUE`fr(WkRY=A!FmDY+&NLgmj_QVnD*8l@9QI*Qo6BR6Sj@gj=PA z8w%b`nEG)6n?etyT@z` zT~nP8yju=KsIQB%vB_P^{jTI_jGNs7@_&;FBK~@CTqDo&q-j)9lt2FG<3;KdQH5r5 z_EDTY=xDd0+m1O$oYQ)2yXWOvys13-qh3hi-1i$DC#B7pz}kE3+q2cNFgO_-_8O*E z`b+5VM(pQf>xuWnYC+`WadmL$^t^P8o)gXybuIGF?a#zKEqXB~iyfriukDAute}2$ z@}UXm6QW_i^o_8XFCS7|;&O&DE>{)W?su=o#JQM}9Q81^ytt)mLyNgvJ>LguyT>Xi z)a4cu<&(sFcX}1^JM=IY@6*=@0}wCN}9Tbzu$gCQvVx;p3{R|}3TsJZW{5$di!kNsqm3l(;V`Hl$2 z*FdB>3z?m;SNpw@-n!$V1Tj7dawTtbK2pRT9S*s@yNk8f98HBGTWy&sIG*?VUG+aU z)f%h`;dR^-;8&T&-wv%wM;!~LHo{sK++JgD-I@E|KoY4RtCw7Qc{!F|@2^C`?<##n z`fzhppG(|V{jyi*@8l8tOOA>**D=q`9DV|u_Pygn2#I>AdjKqNz3x%!LlNx7) zpyuFhKP8_mqwOjca(9qRfz18z6Q;ddu~od!YZ8A1Yvz0M>rtroPNh_PeIjeYDOKfb zqR;B=efmOiEt+XaVlur0v6rbCou@l#N^!7FPBqo_y)X}#S!b2>UhByaT12U37@Vi% z+G^q*Nyi{jJSnSINF_39J+c+haKtm;tvaoNVmID48RUesuYLpVWt_iKNvjg{Mav%C zidP`hHS@F7e~izY|6d33ebuMS#rV0LQDL>{1r_n}Fwb}onRtfLM&kE&=hjGc1ypn* z{pbaKn(RWy@!wr`qv1b%7_uu0>>=G*v^u5I|1L-x1(>l9CS~DB$Gk~-WdF|kI7Nx# zw~aoIvEtE@SunkCV}F~`y$ls9dwtlxOp3Ec^Dd%|HmCo#+i}u7KXsTh@#3VM`s(kH z1D5p-QlwP9e%dZHs%~>75Agn})ewdqisYMfP6|9| zAEj2THB0JpBb^2vA3U?-J$~u-?-BfB{CRwWDEXC*gKE-z7VfimR;N_3MiTLf zE^R>vnvxA!t3PV?Yjd$GENT@)1{zE)Qf)pbG>-TZn+#v9`0m)NfijG};K}6Q#kym7 zbV+MmL9T+I6OE$@b^*KT=sx;eN1Ijd>lR93pLz2~kY9g2uCFrrmLZddOb|lOf zt9}GLb_Wk9WT}cAxR$vzX}rfPI`(OyD2_ow8Emq{P;D||ECGY%NnF`iWImf>b}qKl z=a{|73oUr>SllR~Y0iW}t7Y_d(3H!qs3hVS8=$u@;>;xbo6PPjK2 zOXnUfzVR$&%dmF4D;$e~v`}ZLRD#DYacDcf##BIofrH=K4O}sn!*;hrS zBOoja9P*CBdhQ$R2)~FRTt4D2!^jplw7HS1*IN$t-W^VV>v9KLv^y%GQkyj4%rlKC zIX~YNU}|HH;c<+_=5bQbrsHa9k?WKbSQIyv@Xs&u<)tkGk0@#NC)74Jx#3c}m-LZ2 z|Hu3nPo?gwRjURo{g-+Zvgs_u@9Lb^Zxs^474S_C2%n5^5}uDS9|&}~rZYD7JPnk; zy1qylwq-l)9VBKLIX8*!dewPX$n@?xpI4?8xs)1&!|R`gz#W>8o-lcxG)q49_c&Jr z-shiz#44@0e=1JetlvSj*3cX0+Ty{9C;qy9_|+rsKIX?Fb0h3 zl_n>MlUh$^2&jzW$LTIxV@@ldZLeqf-M?}Nu`WGk)_hf(Uy{+i$WJ@I01dwg1TEAZ z6E??>HWpK_CWm;L8#aWxt~A-ff$r|U^vF`9r9a0=E!S6BOq-{d~zP~U*;`a|V;2sL zpE5=?@*Oq*Nlhrsu0Y?(m-O@U{nhq()q;zDOs# zn6Ik0kF`c{F76b#FTcPM78~EE4tGM8^+fZxb_tr9sUv6mPd7Gt!0X_%$mZz-mX?h< z^o?}I`(-%3t|&sBiczUJCsALA|4q<> zyI+}R>xZVEIWh)4q6EzLxe_4IYQ|sidYnIqQR7_XDlA)W z@cD5*xQT~mUz_2oR(x(?DohX2EA*Nxd>bO5mndx*hL|M9qOx+zQ z|A*b~w;u#*S4N~9g&ufrKQ$r^h6#&%w-N9ejN>8~-)l@Z%4uk;?)J3UfwGg+QYvn8 zm2*@*k7q;CM{%ZW=k3D@d5j7}Q3X|Ee9mrn4j-eYe~4vpf6e~VWZs?QN=$(vb~G>7!l%?4uA)}M>Fb_AJy5DKZ;z*V zIT!88ogz!zTmY5!&8;wS*W)K+bvqyn^j&CKP#DH1D5EpseF2@`>LH*-)KrO=LMrqsr7mh=@I$hjqtF$ z(h6#S)y@%2`D*ZxD~ep!{Dt`w8GNhKNHKZ9cHEOTxzpxk0shv=2)TJj{s(&Dt=C~# zJ-h?a(QZK&eTIvaOvp7FPb@Vn()_O9N+Cl_mmX zuTn8P--P_&e%A;}kAOIpO7R@6=amW{UC0 zTUurt-{zKVLqgTX<8O($KC?-D%d+<^_g|l_S459Fp1TAtkZm4&SkL6znV(9urn4ll zCR+5rOwhXjyfAm%iQiM$X}`X$Ew})5)Z9KuLMQ@)O1_~KpS$~vmb>BuJD5`)<3G_+ ze48uF4kTuWC}>rdbxXLFCZZEXqi(iY#gzUU9T}O28-JWwgB21Y-aFQX7jb}1sTf1J ziepu!yDT_H`w%+_vLO0w{4$*?v2r#J6ipb{23FJ61gRgVt`shpqS3Z;hjMg0buV)g z*y6irvzXPH)x&JT%jOFrygYEWIsKe!vOsZduow&~TRPu->A<|((v~(RA{+(e9eco5 zC)Z4xjG(7ktTy5=183S?Zh^AA=^n=KMc#^v)UDD@RPcHRnU9$l$R?FtFAQ zD}EBM8dy%}@^X3}XH>-wit6F*&g6vw}! zk7y6f^r(gQpzXG3Vt^4xf1M*+Y$Y&~$Q<5`{U5w48CV1D0OYBj%1qd`%SgoNQBF0C zzWC~fx(T>;okU4%X#rynrb8ULPD=1P&H`0FA zl^hBI>Y@9*K;En&^Gf;6 z14n2UI(EH0Ed;YV=^`>%oH}GUqww*&9;!Bx(+7Vc5`JzccU-39h|yGoaJ`$<40hcT zzN6Xbx2xjD4YikwM#I$|W8&=J+tAB_3lhZ`Kh=iXcG%j}PS4DL7{?BG*%3>3JAjdv zQ5{%HBEU)pSREjN5eIlfR}t?J^&$St$$p)L*(T*gZ};86y_w2kEAa0W#2`L4 z4cF|z4%K)Tj$@%La~1hyv9(Y<GfComZz~Bu10=9sz5JB7fUJK8@SDVZn$)us0E=1@UY_fj$4NbDhCdc(gt2V9Fb*ktnwZ+&Ujb!XQl!H z+9=Lzc4%J56jZ&_*sLWhlm}~iQ0)Rd_(y&_+Rh{J`|HpSrkA$L(*-UpMy6-Y(|`-V zAq0BMw3*EekhTQQl~Gtwngp}ig=FWEO7G1K4)?xga+<0X&>@S!b(%|6Dbg`=WMeJL zjl!eW&li6~(1G6N_tZ0wR3k?=PHNv}x^Qe~y7VABebV%l2>KuN^3T1xe3t{xpX?$(3lWDLt{*0F;{I|RYiHuRFKzZhZVj?0Wa|2PG>i0y6F;U zyAl-2(X(4=dZvAQd||u=WvKSDXr(EeqP;nH;Eur?J_~ib)$xp|tpt%mpxiM^St8E( zEsq!K_Myh@QE$z>$X2g7!P5{IZroAtXM=~$eC|ev2{Mfpm9A#nFx9<1yCKg)?^@bx zrN`Acm!Vr^$%PRzt<;Y6bLg~|m*H7g+kKgH#;v7VJU`MUg3G~EgYwsl1vkIN>XulQ_&Eqbzrr68XlmHCF&Y6SsO>`FbLi_gnBkLL`J2j zCb~s+OVzJk>ZGX_KuRf-1hK54rdx?&%b zJ4KB--$%-r%{fd~2A7s$DFP=&=Vjg=zH{Lnie4`dv2+a)ym}#WS4pKRbM(^Ytj;aj z)F98ITT0U0(G){lZkJSKO^^ZYUDmCg6v&#z`CkhD( zqc~GVv1p0)W!rT+kthZT(X$ViQ#iQf`0fGIS(E;K`I!!v`FTTx%LV*gR~@ONwKWv} zYDZn?Ar+T_N7{iMO_6>N;PcrwxhK(C?z4kG;wVYg>h`z_x<86CnN~SD7OcKq$XN^; zwL5!~Qhm%S#nKxMcksRdWGP6nd!Jc|8-_-dtAZU1+YfWJc4RIRHwUie_C4= zWpq~FW6q8tZQ}bbqK-O`f>qC8{xUTwEm=TWC58UGj*;rX$0c_sE`fVy+SoGkjkDl;xd7LQyFVpLd)W{2tF#cFM5pgM1qrNYRC2I>~vbz7= zZSDGln<-gTMuLEf%p2`WnGWEVDHPteII}<+h`0lD#U?;eMUl9_cop=ZSH(*yBMka@ z6mF9$H#@wC#B0GK4~u1WPyCF*K9h7o*2?935u<2s`hahM#aj!tkoz{k=z zlPA$)=5BW>83;g_#k?}B)9_4rH4NWv7;oRb8g^O)mKvFA9dI}!Y1N#RPQg;8b#>0p z(_!f^(7BKqZ<)V9}qn8*|F>)O$}!(81n~ilKVgMQhxwne^|~JVsIkNezJ0s|MO zg7)E`nd&%HFrkoWYNAIYF}^RJT%X>qh)^L9>TmL(YPr-6t=dI}LTrynE-0fE=m`xe z1-Z-jE(C4id@V%IcD(~>Wf(C$bO&a+o9?q_+oGFWF z(R9!6xa_nZMDA}NT%EZzjd;C@`iOoJWG;7bBEzm8C!@alg{0WvIbi$3)#)oZ$`_oy z3A3$dP}4pq&j%JGSN+CLkqQ~|W&nfyTgLsPMCafn9*zcpfFPTi-@FetFGNO^NYmq1 zF+(9%AtX#@WT)fJb<)Bg^r9$zs+u#S7lf$3zS|jL|L$v*m?c@K)8A>>wK7M@_FEk39kPMM#JAl&adky97M}R47OLkzk$72O znEklurI#3zg(X!xcCv8sSqi-~U6H&{!^*mvEZcx;qJrG&m$S57oSI+1 z_T*v&>E7K?6vrL^0<9Z%0#`snLFxb0~M}xl}4` z9%OmgK#Vk{N!)F=TJ|F(6aI5tcr;)H13a304jOYLsaO3=(-;wJKkIjg|B{79MzHzF zht)+eC%R>->U3_efPASVxehh%SOR^~@bK=gwaDUlccl~I?>nw6^mEl2@Y*!OV<*tc zZ_JOHklypJW9sF}5WMg^cy1|qQt;7uV`m_0BU4)~p7eaFjq^Yn$_ z;C0O(4CMQ#8jGl)^6zt69}mzbRK6WURWQhxrP&&AkH6Ri+~)L8E_%UQ;nT@gJbqpS zoJEwVkBdL+sP4y5xAMLsVNkp9$4{sikz4UhhbG`r94jTqeR2sr>v;Vh0YN+}E%Rl4 zUz`=(&4WBys$c#Bl)Ll@Nm`kXz_hyDG+PzoC2i$!CX<}qG(Yo?0Q4D1!N+Kn_m zsiLz&V&g^B?$achU)qm!Vw(9 zx9=@>QWrB)%@cDOU#{v?(^|a^tIV|8D5Gu<29xIUY$hp4f?hUigo4-#D4fhzj~Kow zLM=6_<;)*Vl0wwjcfoqH#ngPwO_ydJr^M}V#(i+r#T{BYm^^UE4ZzbsjKW!P=WduN zv#S_wd9{!vX5|L1t7qKU zMZBF0K;#ob{Ev&d^|{*1+HNG2fzUocvIiGJ4lw53maweDsFS-V@JTAG>mkNiRn-)u z&8yA7iq3Lwjf=jLln}>Fw*XcPGN524h3xF!~uIwU(aUr*L)Hx^NDR zk~JfU$fK)cUAI^0&8WSgftR1E-oBphIS_X*v2jxH-Eg1{C06H>217@EA*O~05H}`Q z?3v|b)*nObO>}yG(1(+V5<<$LW}54rr4Fkupqe17I`8NgeG8Gb>Zxl$@_O?zLgu0~ z^2$5Vh^k!iK97tgGzTTqDtXGuKlu{kAPk=C1OCkA;3@*V zouT(S{KhYZt%-BDUJ^Oz{_|k|IJ_=vF>^!m6lG+(14?%~6Gs7$^}K_&-N-FBwq9ur zB2Aup7{ka}y1qG7&1k=9eh&-ida^kS1J`J7pMR41MG_}=V4dAKxaKZ5{ROk4tu75t znA*~zh4HSb{@C-576AhZ%e;i3&_lXxI|Nj&E6Z=Y+TN2i*TK=^#U(Z7kC{Isi|)~o zAT-kZ$Hw>sa(YtJkpID(&^p_&VvsIzPeF5{!T(4| z4R0wyX}SH>Z*_;9!dx7%Hi)g`gop>2k5eQbJ6&)IF5JhdsQUr^(->L67yI1&?&XP| zEIXEwixjz zE{3($l0T$zJ;NjwFa<*ifD=~PDqJzeJOk)SG9ovB{+SXQcCG9anf?Lq4&QYZ2?yTz z;eM6*Nncx4NV6WG;uOPmkHx*H4=e)S?*u4P*q~fyx52vaxXK!J;T91t}=P_K4c~we;V9qXnTryi! z4431+jGX}BjC-Rn0%w@&ZgG`+rV z*04f29OAU>v&-e_5k*knRlA5Oz>+#;5FMlGQ0z74&GRhVn5z`r0?+-_bTl+8hxEpsIu^aROBA+JqT(tW0wFd)mBJ# z{ybYO>{Yc>AWfovtq@yfYOpj`YE)QNxskToT4jVuSQ)r@_u}5sLZ4Aj-a{*qO$|Vu zx!gh5N+(Srt-Q}s0N03Qh=^V%`v|KpS^f`VVw~!mKTTN3LF@l}`)7bemxm)JAxXj_ zBoR7<;g-`!T0n+8T}U8w6^6$A(d7aO$WP6|lo3yDp7v zn~JYCyV9;ycE;4V%|yy-#=1oxC zSWn?RkIMdK;`r#JA{XG3*(GPa?>DlvDJ-iNRYzm76J7C00=CH*`7swNGjS4%5{nq`NF2RK{EzRMH zax@Jn_Z9gj?1WU(juoRK#gCQy%K-!3~qk+7V{u5u)in`;uJoGNo>l`P45w%jL{&L!xfd1N+hvp ze?GXP#7B#oE5-?}tW@iJt(n4Ady9oNk37{CV9^!A?j%6O7TQjmIjF^vmqGcZp9vO8 z*j*0XMxF5DZew0@!zL%1B;P4SqwkEF^-lbfOB zwYxl4X-#3xElOjZec?<Ish;H_>T;GSkkP-8o*WYYv_hRfp*HpfUYWm?2 zAmUte%T7XNZ#YtEbo?}wK`9*1L%@>UcC=g6+QiOXm1m#I48J-`U@wm@C%1DYCEc?) zJVv0qJ4Xul?FY8VpQOi!)!@rIo4i^NF5^+(EP(W~l4j8%O*N8>zH1C&Hrk$ixu7sb zjQXmG-*Y(bZBl}$r4s_&SfzkG6z?1;HPTyKcgINQ@quF&6pffYkM_`d%tl=`*7SH*^NIGYF zf!u!Z>5%KzNqPV1Qt+m9yiUp=k3(9t$MHjP(wDemh{}f1PdSu*(O;O^FCUd(*6Pz2 z2dQ03(n_#nx=*=P3PEkSqnZpbCS_F(GaF1gErMxyZ^boD*ePb}0G#7Gl5SeaZ^a;Q z<+0?BAw!2Yy86K1luzq7ie7HWtky*K1drM+iX4tVbACLP0WVNn8KqO(DJ=U5t*=o~ zxtXCW@viadH0RoT%Usf`lQQaI-6XlRhYt;@Wl}Q4=jnPCHj?SI0Jfezo1S>1gEts* zb!mE}TP&<33hMM>jfA56wv$hK&TJfR_ITC={VGWY?yT4R_xg52@7JmqtCZ=6ShrfUM^EK(QMu@&%p7fF&_KyG z@v9Brz+t=>Q<1A+{W0fbHp+SVB#O^vPvStG?2Yp1e3gjOo5b9b<2hpbOw{4Inoygo zIye5KpEfM&axz-wepq}bmKh2uKj7TXmIo|@J{65}oZ%*bbs?8%WdUy4*#S7&z0%~e zidh@h;90lzoCu^7gwYIK5Zx?|NR=2j?(kJsW;Y0GWsdYz^TmV5Xt?Y)Q)ynr-Pq5vV4BJaV&j zjlEtmtUkDfzMV86ru1M>_^fjI6sqw?*QB{xje<(%t|wikrP&&A5P7pwW?Y9v{Tv>hdQ(b5tf9XbkSyGc$M)aU&VS=-~dcA zV&RF|!ufa^EI-!dj2)Et4BYc*zU&gQuu+P>IeaI)?{h;DiE%tU`%M%Bw`+ME$y_TE zPfH!y*%SYziiYWIBBehnRmnJAF}HCgm2=qm7x?0hQy29Hvywg0V*HM>rtj#*{4Hi) zu&(e{%3D7ga!6s(&g@UU~N^G8&V5H-k-P6V&LJ1~?h$lDJ!aPl)r?tQGF>5VPU*rrSs}1( zavPv6i|Bo*GCCqX2!K4{{VwC?_2qLu_;mOh$Tl9PMZw{ zTbR!emi*!a$!k#N)d7F0<{v8Pja2!jF3OhRe=7@dl6P7B`xqELO_>j4J9%p19#uQ6 zM0tIThF9mB^URUuaqW%iA|0I4p`n!y7(esMt0G(4z(Z79G3!;{h-KmPPH z$v)e(^6Yj6&FuNwwv2sNO{wWx)0dECuN}@i9|m1VsFsUOs(v zbA?9xiKtg2xTNP4{jA@gjsC+l~3%(thn(@n?-ZQ`~VS-?rgcp zN@Y?>;qz+u&u35Qxdm^B-n4@9K8=wO0vq*PUmwsTAMACzu1H8cThqdv>2(cs0u>#4 zl}mbre{}lXl-#eMecq)0S%Vg@;b6pHp{^+VZU9^8^JXdj*^9IHFuJ}Klc7MJaw~Ts z#OO*L=9Hc)y&YZ+4YtkDyb64z1Lx&UrtAHZsoXM($1akWP<$3Z-%8WsIu{aeeJfdG z0mYdANIU9I$0K~M{7~iVj-9&XZh`lZN&} ziwGl$KM%ff7fN4-JN{|7Iy-(he6PNCZ?$%R)^9x^s+s<&?&qIT=y;iGE{ekI3TWk8 z5?8Cpek7qwpny)<7M;m9ZsvcK^UCcxh;yp!=&tJ=cQn@zh%%|eFA*esn_`>O{rYUJ zOdGb@cttM{WX{=1`TP_&kkV#)PNNfA(;>5Q@*a|DPFxa~A;~I3;@kPLLhr8#&pU+Oun?2+O)tS}NeiiMeXXMyq)OuYkw-!5x0b_` z(Z1|g5c$%pdBfA8zgq4-Ez$3!(6MuN&QxnTn#|mV!>TlTXp4@dei!I|ZEG?1a!-gd zP84lf^+hX!c0dKC{tf(RZ%R$?(CW&x%eqTS#QN5jnuRikY-+zgi6P8Tzhwhi{^7kF zqM2>da)4p z5-udWP%t}ymd$TB;=Dt%``=4BncGY>OiIs7WizVUC~PcW*)0w5D$7CNXBGOZx-M_< zjT|rJeao6m$!1&W;b}e@w!oq=L*g5*-e6w?40H-p0pbzcXF~sny>tGqY}*!f#kTFF z;-q5Rw#|y|RBTl$w#^mWtk||~>*bui_q}`HdtQ70!25BwHrrft&Cy54*Lxp*g+r^8 z5-&a8q9k>=c)sa-39?-pYtH(S^{X*Dok~v4;}@Tf4Qrt)R+Gu2k(pMNn(zUZ2*AF% zEDB|bq~NUDW?14djjl^NVQp?Q?(b>5EoMu3^mfXiOLJlq}IF zqV%Z?*YZ=X`qDB=NIC8-XewzbQQdYr_BsBTRo(s9LJT0fl-kSn^Al5HoZ)TMD)r6}Qy z2cu&;%jX6a*qRCxp_7KWrfM7NRzp;GC1i*OT(SmP=hD)ztE(~raxd4#jNmi+ z9s1AYWLK})rp?tMTnwhKTm=P}5^CK}hvPBBr5Wv2`C8fPP1x+UXi{Ww^rWcCjR;Ug(zM*ol)R zGpwK;<=VHxA@p;MS2iFi4P>F$NC8luGA1nU28&-mXL&i#>h{@w%EUQHJ|FGI=`nSh z5bM-#Km0p!7RIAavPt`YwX;G>a!e#CRk(L-#L*lPOcc)HJQ_T(IDkEwGEE9wc|kWc zsFv!dvhdoJXv0a3qYB*Is0S2XQ+^|64cXKRt!hqWiv5|M3UJ+L$$L(>Fe%74xJHbV z0Syt^QQeS$IwERRomt%3vyy42$TSi-7_}k$-I|}JpwQ~h0*6l*%|7Ro-W-QNf}7p1 zo76{TiKcRP8fbZ{(oXOewHl9`=$ZbtV1D-j_{?$?@d z^P;>+*D{AIvs0r3XmlAcmo~eKjR{kWEe@*p0$UIyKmN^@ZRT#dnu*gJOq3E(KflPK zInzE1v>$|?JF3C4Ws=oplJJ09l%AFn8Y=loS!%TsaR<5HG-a*gZo3xhe<}$|eOHlTk_p`*l z+rs`CTc*M2-NhpnW9<7{JgS zApdZFR?l7I`C3Mv;!Ds$i0E$cA{x5Onmjw>d~g zr=I&*5KsfZE0IVUeRyD*pI_AUx4UcNT+sv-HBuxU1V*v9SInw!XyP(aPxInQo>5S3 z(u|}rRH}M@xj@JjXuM~3Y+w&5yPn}u+pG$k6dHswpUga<aoy^45R63*{^()ZzqFM0m{LFm+m%FB>Xqzo=e(^=ciX}6T__>02_UVmtCTuhR z(eycq>z)?=k;|9NR7}D2v4GYdp8RJcV%_b>x9Zm8i!kdJzq-DW(d3q2&Wz_y1dO2u z9h_3QmJWwdg|DcgQ}v$*CA#L8L~&e{`AzQNJY5$52GiQL;+HgkGb2OM@x0!L*twd- z3TsD?X}`)XU{IYm{Wrm3zg(Z3vT|bhm7X|y&bNOx-0kl*R&ctn&hj7@CG$PgdxvFR z0T{rz$El@CV>mBT%YqI6?)Y=7H_JqM)5`Qzr*~z1&F!Wg@OU^QAtj}M;-B@j0YH@o z45qYKz!UgflN%|;HMhA#<;_#R{KbnDW%3IOkhUAsW6OXU^YwO;E|T(aW`p!P926D0 zG;aq$U)4w{vDBr4f@$677lxjB3_uS)XDP2>{j(z%5g368D60FX?qOAXM~6woQEDdV zhsM|{)y&cLan`Q%mg;E#`M8}mN2XSFI^D_3&1n@-KMnELXdIty;ZxguTQkpvBEIY3 zkY{c9XLn@~UAZ%r&;(r!L$(YK5m~(xra(1TfBC%r{=8%iX)Zhoj-fx<&5keP>hd-B`bO z*Iz#7=8Zrf`4RUP)HZFtrxKTx=^oEpKbBA$*X;2Qdh@Y3^!v+Qh#`E8vyBSYor904 zGMw>tStp1(rMcp=hW)&$ z3^!5>ly=^A34K`-Xl2oXioa?BID&|u@&I|~MlnncQr5@rnz7h;1C?i5$u;-5Ibhe* z+ZttXJ6TquE6~RXTgJ{EA@|o~xof!0o$y|HJk=){z*UP$EbNIDW1IM6N2-l99nq!F zEr!jm3w&Pf&w&NRFJt#zt2ZLtLJ?byj(nxX?ahlM550a@+nT?Kg;mz7z9alj_-kx#+ zv?4LQcv#bI#46f#eR?p)EA!S~okulPyYWU-X#^47#|@oDHfMN%t0L`SP;l_QV-#kj8InbJpPn}c7!U{6d7{VP zm1a+_I81(fw%&Ma=l~X|_OWK2<1MB_j-nVnp^J%LhvBN{QAlCVkvjgRM?q1+_ilVEXci?$7u_e-@C}QpohqQLutDnr_9cibe zj$;HNJkz6yXi{sVbix{i1(YO74`7~16pZwO*fwy*ZF}ua$Jd6AP|Y-*kNSKzT!JJz z0o$hI{I#KT(Q*lN^;ktPyC6tnuDvG?7u2j&F>eDBHc{B(kT_* z3}3}ZYVjLMt;u>nUYCR~qH6DvZG6^ry%$5~MuViVEJi5dyzh@0F)kM4M$zBl^+~rr z!M^W6KOK#-CU7!%Yip&)Tx{0b0*AWL492~2GQGj!3Guyz)S|!bbn6hbE_+>xDBa7x zCYZPqN$PwCR&o^gg|eGOKc0**it?h|R4jiN>&jiltq5km9ytvJ`Jl4l#SZV+sD(xthqMhomXxVvJyQo$yBKKz#K5cK`RXINRtBQZqvI5Wm2cZ*BrP+4f=xqY7v^t^{h zuP=)un4d4Wvm=gK2z~Bu)P!y+!Q^|^a-GW+1tC?8XGbWX-mOY1QyaY1y8%$vBZaB( zd{#@BU$FoU-aEUTb;j80Pj1wW+?tlPixRK%m6an|xdq29>9Lg*kE{dCMdlqC*0qDr z4ttjpv&PF;Pp$I>7qD|9*wq0h0yK7EI_Uq35&l3Zb}>-935jQPRwdmpOF^fv#m(Va zpWVSffFsy0dVD_X_X(c)?#DrBO7mcPH(Wg8av?)$(fgv?2mER0`_}UYiWwV(QCP5L z>M0nf`tCoct_{$I=c*$j2@4}7tb3Y`@VUBv9|4ZY&CPVjkI(SDNa45`Z9o0txj$;1 zzQKSnlXTkkbi%f^w-aFLd61`+(6%VTTUwVH3|FL)*@(ktf43+J`^I0Tp5*{?L9z%S zI^LJ~-2;9&*9v6Oaxdz5CyIwSFP6tCXa7M&L0qkq!S=X4p<7zKZaU+#JvxWeh~tE6RiBXlbnEh3r@;!C@^6FDy{HY<-QsNhRGdP zXazJB5|GPpOtHBl7hln!BgQSyjUfcgztEvC7ie`myk_2w`8)4;6j!Y?kWvq!Y*f5L zWSx6Px-}RpA3$nUg4FaFmY0fgBdtsdXY*TlB&Pb|3Y=41^|qvemu_8J>J9wJ=zNLRHOcoR;d$O@jw=S0qv^# zd71GxOORwpYQ9h&?C?Vs{Y7L@*T12cZ*aIIN}s$mpx`8_+jh{w&3g$G{5Og_gNs%4 zlVjo5>cEAa*$f-|+Zw~Dl@UncMxx?wXcP_U0M$f1wX1a!OtXv=)l&L`JuC--t|{;D z@K6Aimmm*RUpQ|5t{9+G8^=9B@t!G!OG!aFQbl*v!Q%wbNuzjwDnD^x=$M(Ia(@#v zmSLe;BBY#b-Ejn5pHtm1m@Rpb#jFl?TD#||1V(H?=PRvXE~-4L z-|LN~7Sy&pexN3Tj_up@8iwp7PnU3{Ha>y+NKhEvT;ntB>}{^sw-z5wYty1*<%n3C zEizletFQ$Xuaq(hrXXgh{)nu4g2uNxS?txs4|>Vb&Zj20R~hS;3+uhP?LqoPFe3fL z!D?XjWr^Gw!tn6sq{9MTGwOS$lb`Nui2L^R zV6)uKS)|io@glI?v?oH4#hg`-g@~8O9M`8`_19&Z$8Cqe*=WaCm3~$1wWp3B(Hf!7 z+Du!3!+VQWR}o-g0C2UK*QmP-(qx)JZK`L`O6?C}CwI+@14|`J2EI@Q*ct+Pf^Y~D z@7%nUTWO-lY?^~bmTJ_BJ2uR?kO8tnMDBdx&)*wyW@2jBItwPMO{b`wvg(n&8$f-* zss*c5F<#%SeVj132ZE!x7){O;tUL_^1+u)u`LwMUF$6!SX+qcA<54$cxU=JbT_{n@ zjAY&jBf8HaeUV%|INdiDE^z=Lu3&6T zln|9!Ld2F)XEi`v*HfKCR}I(cXKP!Q1jgh^PGjOXS%b#uoZ`v?QvthY;-_hX+BDs5w0x^=CRc}cVMro zW$^`$uBCIfp8y{g?sl<|VM%;=Ry}u&6-UazacWUqjuKKTOJ|_Z_FRaML$sc&*^igK zoiZ{?XNtN&d`y3s#OhXqly*l$;?#1K&bXkEF#1lZdtX&j5jywAVW{S{(i^-{td}$~ ze5>)MlzZ@Wn+w4oiRcOV1j0!KA;z1%fCZ{}|F9aa6x+EM^6kLF5Us}PQI9JKtM!7gX;x-OH@_lx0o7-J7Y`$MC%+2NL4pA^ z?$Rt5)7_l+v0cPOqZ9s&nlg}nFd&W>aeRJ@LVZjT}%k}LL@c+_=zL0G)pt^a4X(2%(%7te96Y}+KsbvWTMI>T9 z*I_m4q*9jD$xb?fLLXyFdW3;F3{WrE#P~O3`8_ZDC^S|}nA^P?n6oUPgRFgosXnlB zyabUA&yBHDxkUc9v@-%pM&5=aH)O%VrzEUWYWmiGkD-UTQ-SVh$1B;T3ZSbIGzkbI z=HX~ov`_Nf&9IHtl#kV6keP>p;P0)~%r>lrjconnWq4D=lOUbw*e}k<{&+fd_6F)Ofm7gRdSvi(NzTVezogYN(d~?HF z2?0ZZ`-wcENHPS4C6=*xWVk7y?zl(&yLgjGRPkWN_2R;QULt1>p7wDlq9^T_4iN`L zvz}zK?YqCeQMHw3kZ89)^o$dn=s#7x*q1L_fY9#iWtZlgMgNY6 zT^DOGcoYG;y_NYqiiMZ=hznmVoqqGCegQEX*E5P^Ligm_OCf)KFs3tYt2Hd6+Qiy* zpEvGJNf7X`VcDd(f>wXi**95N1yk1cqKShz=Bc{Ep)B^NW&KOB{mbTFQ2DVD4tv;YIA>*!{crm7f1a(UzYB~~XkGHz=Aut}&|Q!D~@K?DCZ-ovm^ z=(flWm+46oA);VWpGxmGgoJ{IfMez7VpfnbD?>0ivUBA+ zFwTHB9tLA-)lGr>hL>Lz@AoK>2f9sxB~3jI zJpG3Ny7!^j%uFcC31o8cQ(a2joRPR{PISc4XiHI8$0%xb%}`~R341HI(@LtQH{nxG zAg;{eXlkm8w_jhb7n9fv4Z(gXMZC~Tjqum_@AQ;J7a;t~^tLQdUBH7!h&H&6=I=WI zDdl$jv8kamP)(|cedi@5t0C`~BuYVU# zlswmnK5hl4Xq{Yaq|}nGdd);}VL^^OC{lA!c3j{10g~=b+`rYVbEP3=JKCYk{$VK) zVZx^e+_ilai|K||d!QE*+YChm$`L9fF4kb;d5Q?=WSo-FyeMf?{cU)c`%q4|dWid9 z4%>f=m76D28Dpd@>lTk4LC zjEFQoTGNqz4TbzJ>kSsKn={4PDWea*`I^9vzWZhn2jR?RgpKcd1_BRXUfqb&p%7_O zDe1p4KcL^^vG&;BKy1IR_PSBk3LLo4ObvOnbjWVU(X7G=`2R)KgLi)TX`mLrb zT~>bpbw(m@_PjzYqu(Vh=Pfw@G*bteIf(4RXi^{Nc-J0M(%MB~E4Iee<4p++Nk}}Jlh})TJ zZpZY|DvoU1TQnu>;~!8S+#J#lE!E87oLTX|Q4t5iwW95ky6$((09!&{o;+1ww`A?~ z?_uh;w2dw;S3;2Ftm3}ACe3&qHfZ(`l80ciPX39mO4xo)TP`ouvll8eOT|N_zP_=E ziqE;?z+pNAy`R+uT)&6`qcO4F1?Huu`a+hYY-~o%5fXxsj6F778Pso{10a%imp+tT zAXmUppLI2lV7c+=V)P9Iqd;<$ z0Yx-SSPBK#^$FiuKsWg1+&~mhOiz^~OiSM=IAq1&v#67`Z}Mn)P&>u{-Rb{RhSptx z$u4?sgSq4QHvCfbHUvi!4(wjWX;+)hgYn{UzHg*cEZ`Ka!cK~R;NGte{l2gtxLac{ zxqwA^sBKd7NUl(&--~@=w7k~E^y+|EXtU`ZdHL4i04Wb0bV|Z~UY;7MXB1+%lCaWr z=8owY<+5EHWER_ak;HvW3eWxg1^#`|E(!*a@Vv7K2Eshf;}3!`LXBv7W*DS)9cf{> z-!_FwW{6}){T{$IQlMV<74hH$@*O%+HM`m3z<$p@6RD-nA@D0JCfoHi`=!jJ^Qv39 zRfeBTnaeTFfW!WyDa)ikWrl(Q;CJy@-J;#9_bu-F?<_=gvOy?Pj>~KJaI04EL)8lX zMz%}=>n6?<63R^|V9EKU@TY=yxTu+HUFeFb+)AR|DQVm}VdWk&6SYrE#=VVH*&jMJ zP(X)*b5mXuRiw!(3?kgaM5u%JM&;|Sg0r}9YlR+7{R6AoW*RZ7xi9;exa261Zoaw6 zkuPw}g4|pc`{lDoh#f7{n}dY%73~)@DjTq)!O`j$8M2^C1CBtg`f|9q(SxSD@?C*% zc2+SPiv!*+9U)goe5r;sW>v^z?kP4#>gnkXsoKIJfyEb;E_yuhbBo1Qhb&$&E^?Z+ zRvWSQzr|0&sZ97BB`w1CUf#NpKBv38;iu%HZ?o)sK_#JkcbM~q*=_|cz2V00dBPN3 zcN$Mmb%0Z^T~MBR=V<;XUBd>|<`rmlZcX!jLoXm2n2}2?yS=61@A0T1Jt7<1v z`J!fLKjBF$#JnPJ`R#?Wb+^kg7RJInGuab?&()!8Lgd)d?)YYay`Yl%l}jzb(UcQA zYq*u)kMzPinawz@&}8l;y@(y)#Qrk%12Y^H_q~n?{(vBnNMFWvR!6Dl$+jtVWP|;t zOd(mF1wRBqth=X#@fqznj+-R406A1pGLr56$~wFf>-RiP z(?nRd_W0tJbmW;aWM?fKqKOiMw>31DQhxWvI0|I3DomJ$O5MpUyii%u)~KFt5V7NkrS^^2XAmf&KQR?@$hSvQF2mSG z*J&(%nx_A|>o~Nt143+eJGKiz-9UQ;y5hTY*fr=+%ZHjTnJG1RS>lu0r* ziBEuF6>*G|sqVleL$<4m{F3U#Zoo{$3=QzY@cLHFk=Zm@joV{|&m+;7B6N=VYR&fg zDgr;B&yv(b4RR(SMqwIBqP+GyjyAA~&~?g8AW z-vH1qNV(yRHn4Z&T!1`r>D2>~**ti`6#xu}j_mfX&Lh2PujHD@A|__RP?AJNj>`x% zn#sb_=!6*L7y)ww=UsBs1wr*Z+ZCS###X`+!MpUbW?7l!_3v}*EgZn0xSm<&W|F2Y zC1!h6JY%j`p{4FW8(h)p?G?H$pHj?D#U?~Q#GmgTO8+OB#DHvTBYdHgcbo9u)S44e zgGlsL(T|7`gXZECV;_sw+OfT zHJwCd2K<4O0v1R0LiGOf9Fx-qv|OY))ax11QZ=jl+%BWns9r;LZkPj!6C_WYGh0oM z1hW=z!EsvglKBy7zOpTn2O(Kq?RsvHFE@@4T6lxqzr^1rd8HBO$5ADMh$f9qE8ulF z1UNRyKrl8oaB|iPGEWKGB`2oMuel60nWvm~U*-Z5BoXJ~%G-7H`2pJNy4Ft_{$+V{ zkGCbz7CzN5){?-=oI||WB(NnS! zDYy1NVJjV?t~f~(6FR~A+Oqe`#~0magC(PB&m%StnSc4;ZWxe#85`Q8QPtI#hR)g| zGf+}>^Gam@Dd58g=3VKauO$lqNsBVI;^WWF1>zOfy>c<6bp%8J2QJgiSE-^7u1qU( z4@=$gh9P#%CNbjxuY%;)82)AKDIAyX{7EM0KUr5=U`>`>*AbCxayCZjU#0bseKZ$8EVg31pJ!_(_fRYN5;}i=WgsTR2fj83si4R#2vmuujzBGSh z?)EO0yK^jq)yd)ggD-Yj66@#F4_qQ>2@s&h=X<+}D?%Q+r@R}+2@Azz?sE+*9DivD1pmzhYD7!i!W z>)~j|q@Ax{=L0DE!8BqyIGbJiAK~ja&A>UzLV0wo*2X4-i-?n6cH{YM`az@=n zq?6ho2l-HN^hQ=xA%_P3Bp2PRNyB+}f)U9T^kv;SF!aVDR;?v=r zfCMr14&%@eoASxyqxvB?f?=#h{4=~k%}~hczde6KVBt+3Vt)TqPv*s4sy~Ff%?EUv zsfG~Q{KJvnsAb8wmgBLSlfW@O=qEKZsHz`W@My9xKm-5rePQQP);Kl`*n76z$gh0# zH{n1ccV_r4WRrW6*Q*SHdrlv?7tCyfL|=y3k#ywV?-M+b%vh{auq@F9zH^c^3Vlj! zZ2jfq0V^hGgto-tjkIi2Ifi5xj>M=Y5@XD06D5`1rA``#7y!t z%DLPj6Vx<3SNhX!IE5+hM$gx2Zpn?%dWm60ep_Jo!ik4Gbp(aXwZ8Z@SBpE$iTE(0 z{{9eoqHEPQO(>-&Fhq#ZKXLX#SU%GG``u(^ce~qkJ-wK6$*^&*V&iT>wHh*d0Co+* zS$bd~ym*mvCu#hnE%9fr&?JpKf@gjxmr9;SmnjveKUW^NvZ$N#q-#fVjrnkc186lU zOaPA$F>mao$ALO1XzI5CT$w2>J^mA|+3n4kf;osiqma0(&Y(DNGb+CdyHGPA?t3F$ z=Jb1%d?_aSj2exS-Kn_wwi7LbY_}r&^}Iq|Oi#~HZs^%KK*iuQ2R>evBGQZedjsrg zF}!$6@s>5i31#4WW8WwMM?Xp>%XhJ|5Bk2Fr;bUJt@UoJun}HN6GZ5o8RY&H$~+F~ z?`+!AjXj0nh7`Gfd|ko9>_^<^b?J>iQ#j;Sh?^6-l(N#|p(9B*D4KxK`$7t~-k&0@ zb!%Y*#}tOhVc>e%3(mGGPAfvAGV~Mo2Z3eG+#uFMOLfO)fpauk1!|s3OyQ8-Mi?KB zsNr`rty3;U_zEceK-0rgd=#Z0tb*sd{SzEKOQ%*da7;eMR)Dz!XBdiQ@3xf+ba_Nm$F*d0M-sHmnYPps`>`$Ch~DAHRquv zTZuYcLUUXr9!NtFg|i?$TOs9^ufn+VGi=gp$WEHcWpEbk2PB4SZ+>$xBfS#|`uebc zri!yN9D8_-3tvb)g+QLb4!~sy;_~%?sHT*;_nPv~-lum%9Wfqm1RVt6uBi1$-|0`- zz}&e7!LitE1qY9+IrDyb>jKN33WGI)sKb}x|HK2rxGm0n5(1oK^AesZJE35aw>pz zkcBort|*_4b}}Z9{(!PHGQmI-SWGOT8X#llj5nAdijHxbWkL%-3!)>s1jSAC?$Ulo z9hWaY^o9J#Q09q0>*u+E-C?;R;fUi_();QbRWuKAl`w3CjvUtf7yaq?KfvPa8@`%k~$C+VX6BbDHHjij&9Rs4qm z`h&ay`T52a4KLW;UU}t9+WnZjtymP0}fYgbP9h(u!Fo#bXR za%ri95Y!L%ybC=bjfaJ3D6qScq0`BfVtfp!#YRD7A|RgWhhPKamo``hBfPEwXurlS zC6Npqu&&-6=UOMU=~in-eHlx)A|G(jQOMtUZns-pRHa;(jqN~h+pIH9;NU<8@v_Hj zFgG55+Vu51Ghuh@(xM!o8nZ04`D$Dnnn8$A-b^=;9i94qt-n?SmQJ8Mu;1l0Zw59k zXk*x31p2nJLKUPtmOCUN(zXg+fvs$yknYP(fzfjDppvY@v65KOwWCWKA)H8(5HJF` zg9>nVmLhLVQgkC$pEr->c(dFFEEpAkLx7w#zBFTRH>DeQ;49`XP}-pKKfSdsOLBgsnkp=ImcO{@rZX~PE!i_LM~sBOL+GNS57bN+ssYia#tOfw ze)%bMRv5qonNjOlM?Kw8YI?@Oz{aqPn6MUFSOYy{*1JiJbmQvr4!s(NGs9m{x^|`CVM_yy%`v<4&3iou}$aq^!qJ?wNE+W z6dkTjfw(nZQnzR30a_IJWo(aP^MwR1jwWn8@t@#B6DZ&&t0dPxGzub_>p@n`jo-17p#vco{+p*c2g5qQ{9-C%Kq@$Ylu>NC>Hc*k3f$e`zFA`#7Yi0&SU`UNEN`w2Pnfq+p~UyyVdB>Bdk;xtFNVEXPN87K!D^<7sR3e2PJ$->0LD8*v-G9<=T6`s2CW0Dj`w24`&0$wfDeb&CE zrWTd%`({|xoJ(&293Sg>$zH`%Jv|vf09}WM}_eC+*_FSED$OB`Pf_&ov2Lh`k{}z;RThC3uKss3QRUO zYr^-Ai4LqV+NFA{ofC!j^&bW)Zf=eyN}D zS1xXq&R7uOE8^jRqeS_$cbYXhB~)X#5O~V9?J8Tq%u6#Vf?>bAtCFm+K4H7>4w8K6 zHzFE+KSX^6;g;30L^l0oKoB6@UeyZ}pFCWDfR&5dhqCPKTFdK+I?#N&14$UeHTR%= z&MqVGJ>?Cjqt(o>!@44(&|d%M_^Z?fgHtF|a`4E&Ean-wvoHvst+v6Fvq`K0B(rVk zkNVp>;b9Advh)})b(IQWGQ|#{)CcRBLV*Xn1QdOfsU{3fwDy&f0ih$?cief?7>J1H zip%d>ICGC0KYZ=Ik9(tWcw^DDU+M)cWu*%R|L&dggGv(0UsGfu(`8%yS6s1qM${$J zVOmOJJfr^m3sK`R#>o#;7gmQEWti=H+v9%OU!tGsk&IA+Ni?aX`#=^Ho#t7HBm3^f z+{n7BTzM$c$gRjEz@ktll)qj~oLv@`J0Z+QJ^V z1Mx-%(yKJOHC}61S~Gr&O}RXltZMGEO$&G?R6$mH2v*-8pj;|XLRUvo)7Kl|eDcv7 zbLvH7p&r&=47gel8p_`xJ!mw)G5aBCG-+GLf0a{&Gcq0PVUgF(!D2-}mSZ zcu=vGPG$hl3bF3d>wc)-wH5MWI%Y~|MB|@7I)!)}KT3gf%?v2G^TcUzi zyylv1XY4E{+?g}D!OdPS)rC1wyqS#b`=TtR)7q89&6mNAPIuY8Nw6vJWiijF_yau& ziTmykPnOXQM&HNsuSMA~@I{_8{vOnC>X3ICji5%*-cLI%j#svrqP3I%w|j}`S6yva zLCcm=oiE;wHZs%V^Q0_nve7eE4T8D2$(76_CpeFhCHT7Mb@>eMZ|;pdFq@<=CWj*Z zYL8+A5o})nMyGbIasCBN$}`(bs4tg7D+V(nOu_T=sejjhwRx4Om2Qzm!B2#*iR4j4 zalB&$k-FYi&?$=*gU6e(kZQW*fK;0fhl}uZ|R$?S*ug(rG&n zSueRJWH2gjYq+{7HWtBu(*_qjK*+`$0hIdcjVx}0SDu@c&Eo&2j;@e){|09w{`fls zh7J{td$bW>>N$PXM zX0T!4w50BUl_X8^Z}p<}`F+AQU6E*70&s9c<@|mJ{v4&hZL3RJ#A0LnFBhOQk_Vtk zg}%^#NOYSw==$xAR@I%In{;K^?k#YFB>F`V2cK?__fpzHiP{*}jF^)!8yC6?%}{q7 z&H=#5=L_+=@dhlm{xLt#spdNuJlRmfPuSGk!Cl^Aw_}tjF|<&bSS0^;&$XYZc@{-& zg(56Cx!&6#r{ap)_U`mQ5)Mx0L?4#7*c+KnR2mfZ)64?L>Gm(VvBGgHRgI%%D|Hw< zglO5Zp$UH;5YimN&)c*^SKTUWb9tjry;2H9TSvQzhp*>kXhY6e579=GJZSu9@p_1r#AvRzMID>{ z!0e5s5#m*rq25Z>l84Q)aVXdjFFQq}$g;(o7MU76PK~fVg%r}2sOtJdkx-avdwv%_ zJn$F7_a=e-1S}`&gw$~$WI|0J-hpa%1a&E%Yq)iHe#igVdb)6*|5Xo8tw!?CeY6>c z!hmvMsC*`#h@(talzDMhQ7`w3@rz!hc&(D6<7nec^fSLjG5!1MRxk{sbQkj9pbPi{ z^wjrjE3e&JbiK92?qUVDa-8$P<7Yw0K;T``CxbCQGkeBN6vn0NtKf`kJ2xq2#w{W) z7Wk`YFHp3TW?-lX+Z}yfZx8lL-rwE0)Iz2?r~-+$Q8_cb1CPt#h<4AKvcxvp>=APq zy+3nN$)#>akiEqa`r<`{H<`}pZr;C3uRawY)_Cv|5T}`{KdZZ?|vbP%k;I`=_;-Mw0@eb+mMr$Rj^-DA$CBDdD;2%?4NZt&rn$R`KoV%Xwyb!rHI&QBk}UC=rtu zhi2@Ig23KLh-a=Pll9z~3l7~2dCNH!1(4Ib47f`^$Z6+A-w;x&SIkJ!T><`-mTE&2 z0$t7opqgyLa$+Qy;=Y$!X4U$Qe8lm+)B*u??SO1GT6LH`ap;z?tcDF=J<&|6@(|XF z(%y%CsQlVknlQgiM}kvqEre%opg;|x#rp|`Q@CNEja(#juXQjyn#ErpeqXKw2ido2 z1Ef|s?HEvnlCk{p(h!UW;CYR0~~_$p~O-q;sPFJ>6|dDeKOK1D)06?}{cKw_5LXtZ-ii-9EQHp#5aU01X#7dwpExt`%ZZ$g z17tKVchtvIk)Ud6dy%A#;9&IPt&-I4H^1lg*bf8eXlnTA1Ec<)-lk;TVbmY+1Ze-| ze1GOAzJ1#Nj$9xl4X&IL4fZyCvi8ZrRkc~#Z&H7V`2~_6wy4IFVR_*SHxJ1roJ`k3 zl76?o_o8a7(fYV{AA>>FX%8sp!Mgg2m>&!E{FVbS0?RU7*E2nz@an_CrqY0>4Nuu| zF}58l!PyUZN|ZxvIP92uAzt4DK7?44CEyVX&h7gwAA|K7>C4DdM(=gI{FFT`p!ItJzogefzaUfoK@ zXhDLxI5ygx9Dv-o!{~rDhB*QwcJcnM?_OhTpwDEKO{KWX)qv($X(l)>yd7Ihv8y-2WE*)=#3FE6C}RgV>f0fN^ZGkE zN~;xZrKj1&`pSubLSC|)tulm63OsvYtCP{#`cl3Uj|8P zw|BpcFpheU#$gD3hr=LBGh+R7~7 z66P@xq51)sAf|@dOmnH>5cY2Z*%kxp^fY@H-UMl2Za8FUs%yO^C9SXhiZWSd9p9eQ zc>02;3RiXW`>hO9?2(~tE3zbRU~99**530((akzJHHy=1x@rT z0CQaneQO&bkD)c8AQ2%2ZHB4l&%hK-+IKMVxzPfycRzZv$`Udx1JN62keHf8qYRVU zW))#@VYFXkZ-S=$P`~e;PJFUM3WS@>8|fS>-60|3AoBn~Q2_-z9 zfM)^eR=k`GWwG(lW%Efauk&bV(I=p3KNci~^rLyR;019ub{-GH=p?Zt*i}wMIl=@U zZ{!h=og9KeI-NR=hbhHtorJZxygyN_-zn~yHKlR0xWpJg@r%S*EC!0ujW^IlDOp_EwNT+TPL=vL45VC* zGEK-kkHAN-x5%fsjIv)|jyv|8ADZ~(S`EH8y(5db$>ZBZXyP!#d=0J|98tjYc_dB$7Y^2DMM@%?#Li zAvU60bFM86^90pcyl>HY!@E0H9Qxn;K?B%yg-<@}d)3ZOf)V0P57&6f0fYG^Z<8GJ z5HAlK{Q6$#pr-Jc4B0S=-X{L(A}{u-5!nU`r0wKUgE3LR><=f2%0mqaXf&PrZYKtF z!aoxvo<&KoIX7$M;_LqoyQp}8@FRlJFw+F|4PkltVvp3pVy&PHN3HA^-p@>;FC=ak zMu|pheWSj^f3`X#dNVzs9YMw%MD?Q& zNKY6rW%^z5TbRhM*m}4g(RIL$PWW-FO>TY3$=$zfSAV5}WHRgNkcM5|)O)`ES=d>_DdRe2;QR844f>f7dmu8xoktp@$MkO57KrW!e zDfB=1d&F^K{rZECgSyiFAL8CIJhP^08;xz-HYT=h+nLz5&51FwZQI5q6Wg}AbL)BT zcYp8k?VtPCI{I4cYIk+jS!Z``2enIRs|11OI4x9r?-=gqlz{g_#Zt4GrN+Y{X3zH8c_NjU-91DG#cA$ihVJdXiW`A z3DB(`Tcd^xD~slHsh$2dPn5X`SUd;{8*N88vIT ze?C8JrkR;&iTJk&%NCdrsaJs%ft}fQQZZT{C*Ss|EGOBAAJ;g1`wpZ7GY+C9YS2p8 z9)ledyZg}}1w0+s{n8aR{tX^lT1G!BIRM06!)Fx1{*wxAy&KN5*hZZzpB@PfFGwp5 z_qUb8J104;y*Z+tUb|2lZ?5`i*uZsXyWLGHESNVZ614}p;Y4_ z&KtlFDkCf9#j>G-h(O43L##^K*&d>_O6kPD)&)QD{ovq@!+qy~$&8jnnl;YG=t4FM zUU>Hq=h;qCh!|@+_M(|S{R&TKFXj!nMc?%R(D*a$U9>01(#oRKvc+ddDeu(IgSPx% zMl#n1QbOA6__;pQRDqv?^2(PB2yowduB6KyS28HI%N+^|p)g(&^oX(q;qUTmm$0lS97XhbskZc4u zl^fd7@9s5mDQp)M`?qO{hEUS-sMtE{=JmlQ{d`7YLHCB{J<-bML&LU`ncwn7a_8>s zuycP+F*8!V3Xz|81AA!K8QLE|tUV!yz5;1vGduV z`wtg@w@*%N`PbJ$$R!rumeVd|M=n*$CybsvH(pJ5|3-UEn^)63NOA!w3xlmX@0N+G z)@^sd&x|?567kcH=LrC>AmOPUxT-Goj=+V$>c z{kPk83TmREva(i$dV1Kft6w zhE;iLT#j+vtaPEWDy8vn8+hf@G_;tZTEt} zoRMBuP0OJur`QdBu=29gE>sv<>opF~P$lQRM42*J-}e>ft;s3Vb9Ame!T_Iz|128Z zjf{SsKC6pxLL`%zX&_jbAZk!2J>1zrdY$2xK`lZX7&z75m#L7Deb+h!5%{!U^_@46 z98E~hToHm9nWylGbXwX^PaRB4w%X&T?OCu7Vs4?747bTK6jEbp^s7@qWT^+klOcv3C1$G59GPqh`cdp^ z8PAcr4kV157sRqUsfoA>wPPy(6VWF4EiHCl#w@Y+h?*{{xbd@5^Z1J4;8IbEZNUX` zv%R{6t-YkZ8il@nlR4&yyk&s{T9PiSgEfw1c8V1dAptw&UF=&)_&C`;{M8{E#Z8d6a-iT$^cTMa`VKK(8cU!lCJoqvv-54Rh z>7Jyj&0W@%LjHI^WIbp4#5#Wb%C7YQceHK;L=J<4DjD^g95&M|34b4JboRB> zz=ud~4N-2@AX$bn^#|q)(HRGgs>&_wymMY<~(dqu)V@5d`oHrG~Oz55F$>v3Wmvy>R^ehBbQv{*fN8<^Y_ zILcJfeGiRXhC8)(pc)KumXb)PrKaL=rs+RIabtkhH$C~Y#{I;Z?LMvA9Av-0c<4*D%hfpQ zi0zpcZQV6lKM^On6P1QA)|>8NBlGSpU)yoj!TW-jFEl5a%JptQ+q9hp;rqNnIXcu` zo$UZlMAA6LqPl0R|HxPV7r^@k00YQTB_%x{k4#m0*h@;6yuD0tQgoeFN@~RUhCuQz zZwG&n>oBfWRdU&(#JcyCcIGp&#v1kR|Lg!145>gAU;#b%F3VFA`W?OO_RWsr19E$# z=tiH{ii@trN+iSmf&u@QK4xAgl5TTd5wea3s%g2e{)yKf3G5veruAO@?5P=T;o$>t z1IyeT`i%=V64J9~(TB8+{#J37GUm$pxB5eBfshh+suRW^XB+*YGv!BqZTp@BC7*W% z`bpbFK+Jl-G^?98DMPtsM2VXTsv3zwIJuI~i5Y5x6Ue8r~zdjn^pBznmf)1c2Grn<>j4 z!D1tV`q5>{8sg#;0RgtbO$q4w2YzHVXmt=^-dsm~+$x4vfYSw#qO_@z+m&TPM3W&i zLqOL8Ydh?8oC3o{a-xP2yfCm*Vfr)m0ZGf?X6@G?V0)NJsf4dpElaY!WoVAut~cB; zU6+ud(Z0l;8Pry53%#+(M&rlXF?b!hbLs`<)09!V4v<8(ypik}GMwoj%*yQQ1)D4t$|rZ?o99t`Qy+B&QkPLI~vR|CDX6 z5q`b_gdsMD?RW}=K$1X)EnlG;uFoCCf4v#Ki$d1Y4`*=Zs`kjZb;2h&IxhYDpHQO5#a zkQOkIA~Qn1+JM2j+&ud;tix(uf;gro&tlC(1rxF5 zjq3{E`GR2wx)*J0)>GH*ee8{bK!&MeDGg(d*6EmOT8?0=Gtg_@rDfJuTX32+t$UKR z7VUn&11&g>_auy*r68C%Za12s;ytW{Yi%@d*&8!*W_;f|fls(<-ceehw_WQxhKxn# z%SmxIr`Utw=|G0pY7iZ`2z0Ajz?FKC^DmI{+&Z`eW|gbZ5TpwFb*&nb?-hw+p58u^4kU{@^FmXmSG~$!z8D^2fCx1?Yf_aT0&C$vjy6_ zs7_6d0jmwz;Fh}=7<6l+NOy1NRqY3Dl%5k_SE8&cGUi|Y{yk*3G?`8a;(v*}f7~t| zx0ZcGKFrr(6i{GEd^Mf1gW%KR2XVO7fsque%8da1(1!DOZ zQn9MK*vL!yV%h>yKRT2Q1AQ1>ONekP4LVkH~ZM%{8 z0RO1j77q#k^yQ=7%&5sxc`CR-wMGL@O423e>>#4O4R<7{N2F?(sIc9 z`1b!M;rvV>?n!hURzsUd9k$S+!76Jda&j5*f5D5g55B_ZT|Dfpq zkHi+s*H;fL{zaz$XXpQqqDe-6;ry$&1b~Q?OHJV%H%^_R!IiYc=9#@Kqm$1G=o__0VvZT@4Pm_d+*o`yRu;Hi*BYeH8;fH*PuKC;xJ^E#l^w`EK+N&zMcHsj959!%Ydu>Lffps*>gI4tlzee51d(CE4GbQYu>>86 z6KzmH&VRyeTe0GRh!>vu2(!r_Y1<2IBJF|9_%Fb{)YI8!3H z3Z56^5bU5ci1qE!bH(u6INGsVE&)c~msk zFw77Zt9cGwF6l9r zHpFWuLSYJx0qr&p=PVdiYu%n%#F=!ni~51i3Us#tC#iBx?8*Gv_e!4kK&oDA#k=$& ztT^@nSj@or9{Z^A!>5;Llucl`7nrfQ6IL3<{EE{XW}Hr#K6_r zfB^dv(|cKIcr~0X^K{_OS-?$LwxklO5^+M(r6J_?DMO#C#}ktVokhT{1?b&&MgJp6Zg#5&ZQ^kr!JpcSB9*}Mdq~b|E-J;4 zs`{bs6v8GBOWDA#jeL=27$EZZ3xf@|N8PEa8w*BF7|am*{3gT6e(v1j8V$TCnh69S zL*gQBeWE0OAn@V`btd<3cZ=`Qd_vC0K=@7PX$ZS<{#SxT-)*D`7Cy5(Zvi4tj$WR8 zp*vmW&z9tA!eB)Pkx-;+NFK0^V$;=)?pVV2SzMxNvDyv$BUsRAC{+3REQG;qQtKbPuUyT zPPUb~Ddd*6v7-2h<5lb${WRLAj=k@{{FwW7yUp>8VjF?*N(3=4^zoj;4~{liUE}qP z*)fXVvto|%Am=bJx8Gb42^zpDtIjM9F1BdZH~5XGMK&Xd1sI9(MVotI1xy~gJK{QY zH-2UN6)h8u%WrQw=|rRTBTZ`v2Ss=!T&v{_xHz2L=0ei3dr)JZ_hNXCq8 zoOrLd>{Xw}8Y3hv5{^)?o-x_$6ZBx zZ8+@xiR}F`WxrS!P!9K?D44z7pxlWts@lMhb?Uv=Lzi#wWB7|n@O#E^#?8t)#joH zXbnQdh#(uX;5`m^oCqO~&g{`?G4=gNVgeKk z6&?N^fXKCNk$6vnfKYi^`ui8{ZlYuweMMF5Yz&aQY}|ufrL&~pQCE-|Ex#_XO{b}= z^BqAdJG66SkK2!@z`W?r8YmqoqiqHeWqtC{#&2Pn0hH4Yijxv!cqCK!X66@eq{A>F z3U48T>E^Wy8Z1hkm!l`m#n=6WufG^fiUiW^Bm~fqjL9km zWoVCxa1}0%v9u(3e<(@?+yr4UkU;kwrJf;`i%0-wZF*~yg(~^+N?3zi5_2m0qcot> z18TxzL}q8ot?iy;7EAQ ziKYA=ODDM1D)h7sNZB>oR_Pw=qrTQmI1y}E5X#qg5(tl&O0CDsjp2d`3D4yVvv!Z< zu01rG?lBNxU&Gl6oIw=L=rBbQRY{ujLM(dMU#{*z1R?yyEjage`-C9< zUAa&d9~2W}QZcxIACT(tw8awEs=+cF&$gNP97-J;-B?(kMl3b^6;Y(aSYVb8z7@7L zIJF>sZr!LGwx7;Lvpuzh={7k!sa_#XjspiypFfB<`}j%_VD?GnCePbr5Pn~Th0#R> z;K+Ixc)O9*O0DKCAp0&-q`elSG8o=Q<8(n${NpBm@U zbw(g!BgBLu-pfapX+qT6&K9D6!}HjRXqYPAT`2_cVyQ&6;@L%vLz0d}E<~!|LPmi9 zi5D^tHj)9oxOsF8pwTK>;a46{tvE|Z$;pDt+s6aH>)P+ra-xpK_bwGFNv9)oy>+G# zEHISnc~(d@zON z7b|al(N4+qT+8`q?onB(t%ET28)eZ?sxDvcP_g3cqq!p{Jo1L#%#GkBq0QUd!p+-0 z=_g&jz~>dc-R4|cyQGSgXc*_X=L1^Z1g7M6VGc@&u~I5Dm+UWL_a*aksDPviwNNqt zN&6u_;S=PnchJYsA4R175r!QgU0u*Aa= zVxYAfo`jHYK~#71Z7>zn#_ozMhQ4W-SgjeaXz~My+Tqdgq7#OH6~(2Ge?r#{yn4_J zqtR)w%U5LYRMG}eH1;gpant0EKF2}CXRwzFk+Fv;rWabRZ34;ORSD$!24Y4iaV%f@La8K*cmdis%b#`+^b%4uqzd$m1&Gn|!#Zd@??7vuG-1qIIcYx` zQf9oZ296^VFm!VSO~8%8nkIX4%(+;b7oV2z73QH@w&xs(GIZodEf9@xK%2{K75~(8 z-{}5ivL(CJ|ACm?FjxyGq`f=IahyQlpnTEo)*ZM3p=&@%Hnvm+9*#_bebOGKczZ48 zoL!Zlg}}zzXj$;qcN@<1{5MaG6>bbqNFoI zXA?o7=On`sU67_U+A3%RZD2Hr2+`sJ?RhuN z5UI5%Cx<~i{Me5L_qHZyOnslItPo?Y$fF%-&%L&t1abXwBQd{d{kKF|4DWE)R0QBD>UC0`9@h5zcp0 zk|n=c+L0ARFkXY|lRk@u=sDuRMuM*C%(b1e+|TGU$9tBNenHh_Yw_9C(1KtGYV;UN z7^*nmXAQ&(Zw-{aUL99nH#L)2T=r6w=aR@xlmc8}cX)Qwn6cE8+;td9)dSJN26$|jsq>|O^*}KEsfKln`@u_P zc+{Xd)HBL7n441GKWc2}y{@;hh^+M!42YXCy`6CYS+Df|PR!_vM~%y3g<`H7e|t{q zkV1An6+JR51jKs={Wd=(B3Tr`ltjb4+Rzxnf!5FGqpLE>l%Scg;SSqlvny5RE1W*%qOUeHhnAo zlt*OeVc%?nr__8hVXk4k25MwH8V==ypl`rtmENXs{oe znebeaGPRdfQbq`ImRjt0zjhCD#_&&yc`3k4(Ak^5y~)y>b$pPu8g(65sEr`7BV%g+f*5I*lIWz^Ms=mW086~DPUQw3ns`sNQ?Y>?7EP-@VUF^jN%lEOV> zBWlnm*z;3lxOC^1-Gs(eqdoP{0k)d+KbQ&&IU86a z;*|&%@e@F8WplCCQ+#z`qdj2ZANUL(rqjH<1S2I3P^fi zbYhKE3ba=w!gIb!YE{O;iPG3o?gRksl__~CgK4H)jEpCo5|accvJp10sCdzrOpz3q z_coc}mL`1#P@$;2(3aC&0X_JlrrVRj4-RBRb`tP{z$-@2&LnzZCEW+XJBGT{csgTu zhwjN+=3Oex6I)yNhoS8jRknqwsm3hTIe_7+FJj5G z0vVwHwRn3x5ucb3(Jqq^&{F@>C;qgdEPxC<+}|MBGGF>Ns1e0%#Z+&gN0_br37f)O z9Hfh!M&Nq)x4Kf#%-|kxB}S%31KYY{oE)@mNQZ=UhJ8{qe3A0$_lb$spvhjoh0FYy zYq6LgE~w7fdb3(7lYr`{(W&!LGEE*`^e@%r{Us@9rw|fWEYl?rY4)S??TEgs3s3r3 zzII;7L8&_S#@yhUf4M<*6VrLo$ zFj{TolI}s6d_19n<%m@uc%ofFQZ&tpB9oMIH$QZ@7onS4-ZYd_pST6;GDDOyGlHmx z@q3)DwVa&b`<>G?2scYGlPBy$dTuFHUreNuXp?@`ytx!okUpD-O)Uhs=D9R0icb~# zpE#6uenJ*5_7))WT1-*qwMC6*5M9;hZcFwXi z3Q3!#Sl_3~c8NK+dA{+sZ7O(@lMYp&b|B1yf%@G`al^F|-K4-JlNqJQ`AZO?wATyH zb}*nhf?k<8awVu$mCS2T+sk8es7`L0{$(FS)>=@I(<@stPOIAkxIEZ&H72;jO2P5NU%ou6{jbr zjAm?~pQ!6KQ3IKS)SlXJx}<|Z?dCl+^BF+z`RviAVMiaUM(8=h`@VQR5L%`Co3Hgh z|C^AFwABG>P13JdV5%^@|WQ<|54D%FY#qzHq|HoaaCA~AIfQpa0_3l?N;=D}6qWTz3l`tV z;;O&6T9fj0+4g!x*yW*d$HO)gOvj^mC)i)uTM~x$U|balr9^DMV@4vI#U$9=+kbVc zjVrBn%YRZ8{Li+_Li~k%u!^EXj#L=%;oyM#l0W^T;GLHbs_$d61}*_Q}*X6VuI>8p1r8|a3- z#4#bl=mqbq&4=UE_6Kg=nF_srAN4dnAxMqy`{tGsv~=ZM;F2UoLP|#()Ef-RNa!a9pc}$-&ImWVdCK?*dygkOw>9- z%poLPFj#SILpqICh+`7Vn9!^4f@q+o&nP+OrGjbf`+&Vz>gQMKDe1-1jbE6uRJ|wC=8yxR#cKMR z*Xk4)VKccxHzLec=K|AQ;5O{E8tzf>Ro~t}_EoRnEly+Dv3=Nh7?-86E490O+Nz-$ zT!aub@(y>e_uuz?^6UCd+=sNS-i86iGrxzbs^!l%UtnkQ_H+{at>@%okhTVmj_W1- zZUe9un;ek-y_~c#=VXLD!{Lp3LjSljTwi#;CfX)0mGTUvDh;i1oskf^J%5L@*4~I0 z^EwW!Y)4GksS{lxctU-1Z(t&Bou}L`2hF6qvJA10QW0!xckHewZBZPn1~wW0c?5Ef zBQK6Xo8;*=3Ma05vwa8V zl=Xd2D@dydd0j_T)MRgqhbaPgctV*mXd{gRA2TF)A(R|%MLTT&$pWH!-9B%C%Cjf| z^jQ>+L3omRqXTu3QmqcvH0V0%=IXY@keO-*XYw~XeC||@3?U4B$w;;NwBaKD%4pK_ zhA?5A6t7&&^ti}CyTK&dU#ut;An3B1d$y`Y4XnGrMeTc$QRt$Msa}MRc@T+8 zYQjmMoG4`{QoJ6__NJP?h|BE15hyiT+*rr0R_4@AL8vUU4blkw2*}5Fk$}Hf>~Ik5 zyLK?9Zi$PG+C^1W1F1O{ZfDYUCzj9sOhPHzo?3oA>MjIcIesR{%lm{Iq*${36H&wf zz^qj7UxVx=9-}Z+iqW^;92@=R&2< zsjlfXq*0G;Fmku~x}eTSqRJ0y%bOl08muJ;|6G7}RwP;LE<8_fi>w>A$b~UM^Tr{i zjSM${lw9CLj?VfmK-+laZO2|~PKsW^3q~-?iZO?f(wDkZF4?Gn`6~#92wrLMoFbEf z{3aG!CQFH5yK5+k%m!0B<#*xe?0%-09y?| zbX{r!FX~CY(r1hr-VKyoVVB6W*{^2&OkJG;EDjT^-=(BdY*NTa4t-B6HJ6*9)_vK# zx~R#qHx9g2Yh~qJ+-9@elE2!dLu*8Y3ucJ?S%1d@-JCxqqcp6~WI37;Ofp%YSLJPl z&*}CscR?0OABA~Lzi(032cbYcjZ9C8R7qbG(XIZX*XyQML>>g`V_t=G1YZcOrLp)O z2~@Ux;~YKf$2A{G2Mf(5eN5GZ&s%HqaHd&QQ~ zB4Lj*HM?*_AG(rDHyrJ?%?alsc+0Q1mZW)%-apNSQN;6-@ER@c#;ahb)&oh5i7q}g z5`R9ct+fC@*6VipAua_jYN4$wA|4(g1Tz8~_3ctcn_CD5bKlR6v*oIzgGm)jI|}_V zo47jx`DNDM4gsTYun|=_7U=57SVpmBBuC&Ad&8lCi3!cKdZnHiKIsMk2t)mOl)!!c;8io4alVYh# zE!gPH_B=~PfGTQ*pp;>v0^W2u*FD9bDG@!X+89SkZ>sS>B4U*nkQtG(pLa}dFP_S3 zSqJ+An;L;lo#;Xifn_0ebzZA7p^}`7(qD$ytOKsACiQ8XsdF7*NL>dm-)fHG3AI@_Xk1e#Bf??^d`eScxY{3^^_SJ zJYxRk36O+TWBaea&l#NGHx@CaUIJmtDKo8cXx66O1Ks*ni0hNVv4{@ikO%}3F zSGRvpZ`A-wY$AQdhrI*?=bCl1EQElO1i&=s{o1kL`ie$G<=0 zr+i{cIQ8qJk(6EGdp9X&e9<-4f$S5~Ay`R%+>VU56I=rK#?D9#^b9T^Nbzetyo3TA zkWF^)ulD0#cZp@4`{wgcwsm&xj&~#$(e{HIu)w(zR53@&yuwZaUZDLo=@29uu5voV zGVL)ekne~FZZ&4JxaHast<%<|bOlO%ms!JomZUy(EVSuHQv(lXW#j;{6%Tmnvxa2iix`ZYp-;Y1+1zLFH_Mx|(0sdPNmqFS-*6p6dIAN|`4q7?GE?Exkb_EzKShESYpGa>eJ3V!8IRGo1ZXUpz@fyh;u! zOUJ)~+`IZC41Aqz{d$#M>nNd-EjINwi>?(^ zN_-$gMGR|FHAw4d^>)`Sk4Osw?k6UAU6{yCvbxZkEwx7I{)-@_yk>^zlr0d@4Vdr1`Ccn?G##u>8kqnY6@U z=K+SHJoUOtB8@^Hm>ew+@ zM)TBZriQh;+tr;9eRVKjZD}l%$^WgiVEFqY0h$uN%2B~4qy6(D2MpwcoYuI}y7n!U z*RG@?kw);UuwUKlF}qq=+42JuPR0yQXOIc4xYuYH5l7kCE^UX)_vZ3b&ip+=f#Eu; zhc5=eGW$2mX!n+TklzwiElGX9&| zQbN*)T9%VY82$hJ^#A`%5qu;Kq!;*70Q8j5IitDwCr6OHV9$MR1U1I~ZqXQo@Fovz zWxzMlp#Ra#zZ<=!C+tUQ{3#Nwp`!Yf&dG4X4Z@x4p(3#4{rB+j#ruVHje1JSLEZUW zx-LQgFN5)AQ+j(;QM9$0r`UgAy6EFT7HvadcXH)Kv>V7ai%_;SG4SgJkX@LB%HTvgA{%8n> zV5?|`TK<22`PVG^pQU(le@&5hw-9NdzZTe^%X=51q<3HpLbRis$$%SUzAUx~%-6i& z_xCt*zor|%|4%l2Y9viRuKl>Zr`Q|mpcjibko2Y&d-FXc?^0Lb zatnEK_~1TsVpUAn6IicW;v9t|ljeF)L4}MA`)2|@6(c|85bkdNp!C6w;&K3^QwuRX z+olNnOcs8F7am&_Sk0PBU8|(jT>KRXR}E20DdBd_gaAIp9blUb;NY7;8s^No6F`G*8&Mz`qiD&bU}qtBgwem zXx-xtg+$R$5BM2Xce{UJ2oL$CQ6zQGzhlZNj+FI;%Y=?lqpalz8jNjt~+RWC46bV8kRD4!?)Zz5I-BzE{ zlc{|4fq9?(nv>OKW{BR0BgAmRo&|>-Fz)bF&lFtv(>33)9ia@i(BbrOtV=|&d{pQw zn|_2E9?pA)@kLt`@GzV{;z9ABkh{t!`E z`U3>yFy=;($Sl0~y_jy-Bhb{coq~A{pZmQdLx6MZoH^ftflufouj!~@(CIe&wtHYu z-pvP|OE%|Fvx{|mYpml(l3&rhv=h1K?J;AcS5g>voN!RO4e`m_Lu_yF81>b`jWgH6 zHKrYUO`^XqVV#1lY}-Fs3Xl4Jx1dnOlGYE_;ENTQJAJ)6KX+ADtdo)l->f`~oS*cx zCqOjgFe6&HXY$A5Q7!%~6bapZ3&v)DB0?sT9g+(qT<(Nk%~y^(Ia)np@Iq>8gzQ)2 z(;kKfD%BkZ{KQCh>xZ>2$-MnCCo&#rKPj!*i?Z>hqk<_(%8JPdg%07{gH7kx_FAz#As-#$VsRUJQ z7qN`Xn|kqQY{DlYhvNGbA1zOr(knS%lJWLOlBHXM`g`iNoGc{et@G^EAZWekd?;!o zpo9kr9PV9^r*KyT{ML#(VT@@uc~=go+y#JQ54tGqhRVfBMrfyH9N$ zUm`(1!8VRF-Q_eA^c^d|3oY9rGdAqd_4>B=(5)P!kVfka|3AAo6;Q-VW?7#~7rj}S zeK-D4ChN*pd;sJRFA~u8o?O^ddl~|zhu-S974S_&@~wv{RJV7sF3tTV@WI^Q=%<|@ zk-C{f(mAlmLcB~HCQ{^>!MxNnwg;F6K6Mf+;DuN8ZW6H8AuC*{Sy1#YV^$1eflxVm61q z`HP-OWp=&6&@0`T&zOUJg~euYI8nZ91o#E`AMq1M@6Iu4aWJc^}gF5)%C|7*$YxZ*FM;qAyqc>6PbLaaXEn;`jj4A z-{GKK&6#wn^fQfs>`r^#1IZ{{Z_#=zf}}}dRemfP?K$JgIHR)a@YVhYg7Mqw>8IYz zfI3Q4&!-j;x4O#-KzopSPBC!$Mr-&MQ(*$a3V%LKDa8>@Y@#=bJ96bApSl6>OoA@NleCUNJvZIu7U>@!IJe!t zANCG~w&nTjGux>NHSpMaNmL$tgEu*rR$=PXI;nlMR#4yMn?=xRK0RO>SU*F~XIUhG z@EhCWh>C3J4EGZ=MI;E9ng-z^mx(mTD}pg2hYdB$)zo*iKqPdL=-TlIa_51%N} zxG<$&g8L?UBsJ+L?;Pz+S6L^d5Cz(0(XDir@P%}7dukJ7KSv}ScI?KR$(|MQ$k

ze8ur8+4A4ipT~^eui8Hw?!4S&c?UCy2D%`C?&EwvNoV{;uBmDCe86mPKx=ug7pTU`maa!j&p~fB%a*u zP{8n__zCx;s!IX8HbK2Uv}*2}-wGvdYRg>mqp8-G5iHP7Y=K276kTudGH_Ip@YpIUTx$TRguY0Z5;eL+*94S z6~&y`q4#9{Lx1OyV`kXHP45$WCB!^4BJ}$Wjf!n(cQwZDITdq#|4BCX?nQ)g&+49f zzG;%;5Xe{PK*&?WxCBh7Y38r60y1P}rEXe?aX%IfoSh2{WAjERY_7T30R!+fQt5w1x$#M=@@vVt~O-^w7Lrs?(X zn!dYfnT`Q#cfLY=_l)lAtqr^~a)Z(EF8ocS%YnyzPKL220Ns^x$8wXnJVJ(ff zh&c2WQl($3C@$yWyD-#pq94MVM{>^m4HYpfkgL>pFz)n3cmft{0U)n8p%G4vE`AfV zF%jcRG!!y5RX{Ax^WcBD0B)3NfqJ;B`NO61nJH|9s*3nav|mE?=~L1V99K!|oBXt} zxOR+#l(Ii*$o;RuMjpHoTrRYJt+oDGK+-w;u5g|%XUd_CZ2BF+Ix7_^!G+4%24ukq zVTf0MIx}21IAR|2Tw?fJX) z&{qoc03*cY|0{nq7!iZ5_wKrL+oBTE$AHSlEwjJq`|+<2r5?4=xzuBJt|%XrAj8Qq z>=A4R#PaqTI`~`wPzAXfhesW5;shE4CACcK)SzqZtmL~w#qyb~Fbz)#^0__afp?kY z7@pAnet%x>JmZ#652RwGa@}WSjODFUd+)6UesFGkL_s>--n9(j*&IN!80TIFY@QMY zz4xaf)VCY42B+L8+cl)f;zxe;o8OcuP=phafy43jOZNf0BZ2&rP)4RQ0H}x&8{9p4 za~v2ND$35&=BSAy2iQsCf@>XH zK{roPUbXwz8OuKP*8^+G&fvB#)Ia#BY-Eks;87ZTH@io=Ll3?PBv&hZB=Na}c><`E z+Lj^>P8G+2C+C62QbuSEb?AU&P&DLGlBZnckIEE zH0E~r=QPKM6C%(Y?jU@;nY5^?#mvW(&|z=v0c{E~4*+o$9{|C>{q}G$&>%Jby6$eY zPlrB!)Fi8_T$~02qH>)W{0oJrF=C^poGKLaOU=Q@rPqV?tPD2(5MtqA0R{w*`REH&B%FoUH!t zZ<%$iBf3YT%8Y6SS~>K=ZcR7E`(LTkDAN3jqxW2*N^D_+B5_fCWF?G zaJcqd@j}d((HYfutEpDZZKSZ7MsMTSS4M z!V~3K{y+~g%2i{7bF=V+r5FC9<>{4jgJJ|CwFa>I3~9VtNA{k~O-S}x^rl65A;!vF zKES?T5Wp9^GTh0OHgcuh!U+;R8Y4}68~-D|7qa?7sf|(h5lo2ACw#QCn&B_7zDf{k zf#$2wd$k^U!q}ET`)p}xznkEx>V#9Su8A$hfuL0nbP4{<6Hkn>&RxV{d*o4Ez#g~= z{V=>V`<*ATuBe;+{Pm7~Qy5BPqV3Wv;{6!+TIO0L%b_;<;{gDVCxKcbv@+Ejw8%Nn zV0^5F;gS`S%pVid9gh$k?sjt)V8VeZS!mX9`sO%j>NUTL)C3Iy=JpP<45n6zHgEqQ8oVrM}wF4!5G?|JV*?gnEz5~)r7bUbw zuBGCQfcYToB^@RTt4LS#wpS$m4hbpeUlzz$#X)Y7YinmNkwEnsuaIx%$s)|M)cK++ zKa$cVAN8%al4H&r-1NR)t?s!(g6=zmOP#n|GeyIu7>zexsz$3St!O|~ z87Pz)h!Zhvh04ZG3>n(=M_&d6+X}}9Sa-k_;l%eZBUs{lsXu+fsAXiF7a2wF?EyCOIR{E^YnDTl!zA-Y;%K+X4x3voX~G_7W~iDY;Wtoso7-o zMn@qK`$~IAuI1gqV;58hd3ekJk(vF<{{MlQjcR&M4g|bvBZ~0ga`>`>Me;%Sk2QY1 z(ttw}+2&GUP6Y#Ij>_hQbd`%SSXpv6Bn>Lb2&>|eVKfcF#(9m|kG!}UTdWSXXzcBa z*yv94qz9Q{3(jUhXhh%Yw_4lPUzkIE8R6_V8&~> zI5B#!b4JWZ$l;6#?yx>?42 z<0uyM&e&zGgnDTTb zJs#^k6n2!PUq(9n1IdAiRox9Uz+09HgX&WXTe4RKmL*~{?pcJZg#DkmEahRvIT<`z z3tJr=d~(Oj7dc#5%U=Y0x{ewzXv3sC(SlkmMYvW#xBHHo2bRloNQnKq4X-zOwX{Js z-Pp3MKL*a!fH)4GV1PCNb!3i})~QHEpfCGOq;lQaLU;hvI!52nz6LfbK}Tn#2=4U3 z1bpxXCP9TLy(dFK2PPq83-KIO7yAm7h6Z!VmqIMxz=7ADaz+$t@gu|eLjuquHC<47 zRj6~kKgPVgsJ_y>Mmdn$kZ$q9T8S??JteV;b(ke@El{*}(?Yc|**h#Zz*sa|fx9gB zC9}9VM;|d)v{O6OrLOiTGnn4!dZfjbXk0XFkxwI|3^iKh&ZoQ(J+i&}?U91P{hb_3 z*zk_)gZ1%~4Y4##OxVo9wRq==5yG zp6j;jY?LL`dn)cJsN;L@awyPr-!*1zt^#r;2yIoPJ9G(@Lt9LTKUj=K67RwtMwEP z3w*UFriIYm${({cj0M^dr~^+;X=j{t5B?@eP0?DGe6aU$^P`Q(B0P9LWWV%%#NG9j zXlkNVFIpQd&NF)QoToT(5=%rUkN|k0C9i>q`T+F1(=V*9@qkaWtH2^?g<^-QORuFw z?%r4Y6t>^`v_(C!WZO0w8j^3`s%Es>azdi}V* zE-&1dHCMFQ(b7Xne^WAJ1eg7jEhf#Zin<#okX#60qE82B>5e!$X80>Nz? zS*ww2R~-Fr!bMybPOdCC#3q}u(i^DD>rm%{A~Nb!4%-aoXIE&qF>zMl>SE=S?Owh*Z6PBsV12&hKum00eLYvn|zZ z1C4*v_-f^um?+dIuoS=&+NFirPZoo z)*2r}n@H#=1ZvrPg-kPZ%EG!i+W3NPYZ(n4TAFzLHrq(sK;lTSkpzGTgox8Oih>Kz05gI@8i%_%{ z$3cYdJHW#=HP=pf&DcE0^8t6%X;EZhj?MMqjx-L27dYaaLMG_b_ok)FEnK}0lbty1 zcCt8Isqf)~qk|5GZd#DFDYjfiFT?q5K3X9BzpFy4Yl#ThE`ZDuxmWHbqt=jnf-bsg z({8Jbt=tBp2itl85YCm|u!J|-LPH2@tRAX@c0K{ zOW*C6B!_0#FDkHQb=8b{wc{s?Q&OB8U9U3kPQaN+BWdDI!y9@g*3770eP@748 zi;QKovv*lGXcJfDjZ;FnrNyl`OJ}zJl|yleX!?4O16L!~_UxIq;H|jG8Baj}3)QDv zKk`Q|YjF5<>G& ziEa{p*xA(%2DODtRjBcH7I__81<9@QCt9vA=g(hhXvx4H6IN$D-Y_!d06P+7%Ww+@ z?DjY@_wf?@<^2LEz_iff=Wl{BH@BI_(YcY;7gcqvJeat@^gc#&=D?tHTy0=*kD~fz z#&XNbFf2pXzD29)9JjNXreQ`a){@?S-(9&(q2GQqm5TTna>i(;;-qLnBCiNkIB;vi zw!wAG;K1y4K5)ei-*IxMM(a|6(TS$9L_X3?zm9k6?@#vBuA!qT+qlyO+nr5=v!DAN zT58MejNCoI^lR7M5&GtIVY|$%zcXU^{P7}IBwjboWeAaZL~ih4&mM=z0i=}pU}P;q zBk8?l(@VzF@Q1pR_R5hzvA=~!35x|U6bRPlaO6zJ7;U_sN~dxL?3!r0R*@jYy#hT!HZzjNtx=^; z8r&}yqFzYNlG8(PP>s{mxXc7?PB7c{jZYguxW><3!Gu@0HYte6&F%_+vFvBVivfcf zjqf-)AMb-=Noa_j|Lmf#cR~4N38`cH{KMbILaQLjyxI5XgdENBb(rc7(>41xDwvnD zuQb`76qcvO4?ws;rGMa{t=(KJZb%kbfHM!0opk*`&Q0}lvZ&vVYsoDC5xY*=Dhk=< zj=RS19n#V1BP>gguD-Dr)km~8U}6jIeO%1pZP6121bjJt5N~3fbdR*YNbo%x z5>o||jORLwS-8Y!Z+{RMRtCfQM#o3+#T1WE6*?Zo_0p9Bnzm$6VhV3Pwz~Br7-DlF z?kn^N@N)cLRZH$FAw*o}r5<0<)p6H^Dwb`1aMX$`v7-XSZp?HYi`fair zK4Gfuxsm}MlL$A>0WEi8qQ>Lv974i5gEiL{jOS>RI8Jn=F`f2jx@MG|RVBx=R49$T zAP9h^>C*uBb`vid_DR3v+VaVdZt+kQ);bi!;Q+%b?PD-KQ7OwkntU)(6IV;5ue3vV zVDgE-ez$$q~xW3^Z1ggHT+mv z&CL>W-T5ngT$e@H{tfu7hpul0R?s*+Cw4UT@e>}MfJ;5ZbgI9>hahms!PV4>7TfNy zvw!~b81uz7FvKwF0Evv=S4Q;=xCxgPnOVh5O;c_@T1MdbUZt#e!wP>oWrGvr7ZI(_ z5wtoZt?{s(EM17wyp#Rak_X2nJ-mE>bkMykYXeky?HGBhW>cD8$!2MLSh6dd5GR<` zF1EJE7JA~^ul-~}rvrX2L2d{Xq@i&N77<+JfP9YSUH2|C-=XHhwmeUDtotuVv>pcY zRqx;4`axQYrhJq2PM!5PM{Z0xS{Ff%kq*GqjTLY(GXoudPZGEzk*kbDS!xvcm?FVR zqULz1yJ3ecRuk75|L_K_yp0w>TfyWMDyP>|rfxOlLR~Mu)t=G0?Z(Y9jxud7HQqK9G^=GBoxx>NX)h>j7t(JZb=OI1$90I_gyg#~BTMaoI>dQR*AMaex&y zu!Ib?n4}_70)1`pdP8V3JedNUVy%Ub=q`s#6x;#EJXmUBN8!nlTio-VT0z`lrp5pX zD!n`-_@gNbkQLqa^zWwrVGh>J};5?iex#cAD|h=P$xxZ&=t1v$z*%l2R!O&r^0=~ zGAdXC-=UhV8QKa7gTNnFZY#Zq84}PiHlE>mJcy3>li@Bn8jo3I$rHfZMY&&j+QA|c zjSy7F71OG)zG2ptC8o&+@I{>G7cv@Ufm~MkYHw+qBjD`4WWkwH^uFo__0{S_$+o)) z_)_zqhkM3$#bHP+NmXqLnTWoRPvU6Tm%!tX$KSZ)lS~NKcYgMRJgR@M5~7w$2Gs`n zhj&eOw1d$xnHT+4mO?jTN{z!{9~nF1^&Gm^RWm7mtan`V6`bwp{I7?FS}1dch6Abr z;pxmUT_#ZKt?c1vZiT$T@_u4cja7o6zZrbPkEG8m4!1*BO~t&CUZYyA=Rw2B8GFMw z+kaE|bmd^zJ?Ig3VjX#U08#DmV^^1?zc~Ey-v}R4(igW#{2t^|w3Q3vhcO(z^6B1H zEPtc;_N6N`!!|PX-6A)Pkc^Haej%|p)Y+HH3h2TqMl9s)fARPi@RCyFMeiF}XaVX& zv#yS~K&`CmbgPcO5d{JEIxJAkp5O*I(V&`5C<38qiB1I|_Jn3MC67=;17}~0U}^%t z=FYd=cfO#=BC64;9qK5c*n-}V;^$m!XvJYEH` z?Z(!}D^02t#-XYx*^a1)OUjLnb|rKu)%flUMrsFNH#o&f{mtqVOzURC=~DSk54NTq zuEAamc3+bOf@?eLXSc*vp(ihJJFG#wkN8GYB3t%fKdAsFXb6LWnJC6Sk$vkfvVV|$ zs$3C9f9JSUj!9eq)SU%Tt`mQOhk^jBR!&Ul8wI(ImcA+XR4b+GN4ya=$!a_IO=)|K|7Q|K|67YmlEeANu}Ea5Br-bub~ljOGM><6yoR zI5DAqTdK3I=@*nHeW3JVY`TsaIJvFg>ugV}K&j^P8KN3^i=Dut-7P+0xC~eK)!PqJ z23y*BX}Fp=o;1?4zo(7M5uv96dZ@w=_;ay|sF46l8=@RooZQ))gZ#A>drhGuinCFO zC5NWSJ&w#@sZ^Xm_d z&vM{MBNOe~0bEx|C;2hye$$rL6bjd@oPZt=kaPI{1hzu!+#Pvk4x1#0`0Z?2%5g@y zSsRGo>(qa7sD>z9*}x-%YmRnAAg9nu0yUgI|DEIhZF&q*w@-SNfaU>SZYV1)TB0=I z#!~>iIyfPiCVL}YX%VJuBv~M;jhWlQcP@5igAY@DL^SyYwlEMIi={xgTTzj4&c^?1 z;|tNv2Bb*XnBl&hlw36Oy8;mD(|DWXEfBaXX3!EFnX{&C*T6v|*3Ga6By?mz?k%Gu zEB*40gUjtWf0dm&8ens5V&NW-h??@55{|03#+8LN0)C+J_N`@6WGb+}o7dx%Was38 zx-Fd&JqwP=0rx7zXVco?d7;(oh2#lADk4Vk%W=h?3)B)@YyT4A8{<$eTyp&S{^-1I zkljcV(8umq>q6qONT{6r$5&mzR(KMu+g#zDwmzv%2UKhp!#tEac7v z)hW`3mUr;i{mvcmY&4zmfL18PbGE)cY>dTzp6-76~xfK2FmR> z*DnR28uYaWx3w1x!BuxWWEhe<^<9xr!>it&yRxN~)NmkF31q=}AEaJ|^VoN$KL@lG zjc>=8Soz1%%{Yq_DE?cNLk?W+WIjmw(Z@5!#;@xLHJ{JyKoI?LfumRzp0?`%JXQOB~*C zO(X4kw?Tp1YdyceVji0Iv=wHxK`cnlF3Zo_ESBrR{rSltX$x9TVpM8e45r#mC?@@` zHt*EktB0nGOGKR}(^v3heuM7BgJ~(x7}2az-{nfYM_piL7P@8|aGc3!2xDz;@`ab9 z?-wrxpwjcvoYb`ucgvJbMZ}@KgYuul$35>=rF5o^M5v@_{XY8fUGj9QR-?EaInUxM zUVSrqN__`OrZaO2@AotR!zD;n_)9bhfIYAq6$R`O4eZg{VL?HB)1`TeuDYeU`h%{y zdGT^-w&N5}rEfk?AHy6RURod!| z(5HHTQ?bb<66-7^EQZa@kM|Zo4}PD%`u;4atAdg;z4j%zjiTWd9U{J(<}*WuFJC)j zuh!Ym^@*wpgR!>9n|2D90@>L9_h_qYaeLR0q9Xm>Ere}3o6rWLRigcOSeoo^T=8f+ zdoxee1=TI1oNy^@)&0kJUWU8Sn7uYL!nqHOkw_$K&R9PAO}b?H%eDUky=NiD#eC$nqTldH1(;L9cs_g+l z%}X_~-cE!iixJxDsNY~~_{^SJNB#pfTlBh74@}F!i}hQ(8%X7m?VzrX2DQ^g#+&vL z?-y`A3j{*G?d_Cb(H8u@6fj5*9;4d;E&;b&lb|r}8(ICiFt@s6TGF{o-642rS*ymB z!w1xvYA30nh-n*tAPvUc0AKL%NWo(2{YABxy_bf9of-f)J@GD`Yprf5>Cxp512=z< z1Q3+GAMkXe9}TsX%FfkBP7dCPm?XVdgA+Il{x?!+#{!!@iyFO)VVFp{o4Fw)d6m-u zxzZSMIeKz)CK160>bsk%OeqJQ=fl|=I&hhHxGR@3P!mh4IiVDTg*bi$eGTk4a+&@W z*+u=tWICUj>^^#7zL60~(vB(KK-l{E(pj(r>^T1~^#{`$7LeU~a0r`-I4XU~hsdgzQI1-FL1M=RQPA2l7R+7 z)ZJcZv=N-K8eSTD^BAHR{cE$Ued2SWSbSNmB#5{|$JW-mo>(YmKD{8+J$m-DBM=AU zBRT1WDWTQVqUBe3=86G_4~VMi1n!sjhd!~y%VGj6NiOhr^H_yMEj zeF3AmwuS=G<_0_qlcQQXW|GH>1cLL0Mbn-tvz?XqFsybGl=b9J&cVqYB=$>~ujCk|be-a%{kN#NsBiyxvNurlNA)4k8L zIzAo&9NbBVKtpy*+EnN?jj=eAs>5t?GYUK=MZKJXnsfRRp1lL;yC39%7R5o;Q zI#uM!<`c6`Q^o=^>gJS;(eH;UYAcN1X3rqQWJ4#Xz;yXwXIdRBHuxLx^zfP9_ved+ zV4JM6++s3<>~Z8!;qNN)M{b5wT~>9Tosi(y5#WWn*DEvKJzO-Z)-Vhl2QmOZ(pq|e zmSC@fysenE2;X3N)Bvn6W%u6@; z24DrF5CDSW;RSY7KI4GgZgC3i9-~mLoJVkcN9f@-f4#f@m5wYf`M0F`yE_H^6X@lo zS&gOMMet36ZYPB6^~b+0#F6jq4LAUl7r?yH&}9@+YKA=X*CTsQF05g#9PqV;&Upbk z8_{kpSpyco2+`Ujfmzod`>!tUB#Ux&Ahy#}MGm_Xnv8cxdMg<# z92!+LnQTKrjeg^w)t`%e`by0?Ou;Oh&Vn&7ZfmALX)pXu3qP!Ke|390yE-;AsdBpY z5-HppowJbZ)eaDk>7ykpX`Ww?*Km_oyjc4XJyCFir}?Xr1s1{d{R4tyK#SYBZ*sEctsHLe?4gH#*>OLjP6nUw!#`%gz&$yEZ8_lowh5-SFh9;(uKF z??2tWfMLe?_|}o;8@RfSQ4uH@8E_!~9zFm0xPRS~Q}+d=K@Q`!_k@ml>0Y?_J+i(| z?!UeEzn;xA4J;Nr;-15pv)E3q$NyDIO>QhHW%?9_lm4$eNHqy2uD*ri;X{c3#~}Rs zr<|-G&?>pvTkh{7St+seo8NVnvp8sUmvLIF?`B2@qkny44!b{h=n}u3(vcVCzgpSxHm-oZ{A^JnMq(cQXsgEa=Y<@-G$YJ1WN@h4|O{`tS07z;cp)tj4)XEH9sE z=0ENIA6Nf=ohJ~uaqf$f- zGs?eS^Z$26B+L9@*ywC2?5I7Z_Y?cey-XGyJr*8J>|I>~ei=|Hj4a|!jIsohZ&!gy zraJlmC`?GlXtN32T47p)j%k@HJF<)(oe2)th1N}Grh5rsr_G@~sKH>o!R^}DXn4no z<76GM+jbL9EIl!Pmm8&N6bi;VRJf$PrOH80tNv#Y<=FZs{Kj6Hw;7*R`q%eLCiy9* z2ZKl3RZZ4R~#I`dJju{NutFDMW zTP7mayvOv*SAk*|YY%2Ur~O=T=r<9zqpV_&N{AX#4jIhP1z>9B+xVii=nbrrR2VKB zr_2q~-L#pw>0be7-0q!wgLu?ZEOx4hYEv72CU-e$tL-$BAuE9nq`djUg!Ebv7s>uB zZfZ6?;_1xKu^$*t>+L%Ri$C4+K5Af!y-y1Qdim*0P<`n4X%Fj~n`uUjtZKRgMqf{k zt8ZYOmMM4`td%`k8mTB#(I%sk?f$HoanyZQ;NlaPWV%W*f{XdM(#;`U=q67=OnXiAFyvbJ0 zRESHJxzUXZDqUi_)m>kGqq#VXre&Tct{}n57`3Nip=`Zb)_XxjR}ny9?Cq(p%olEE zIM*el<7*nTEBw&+zHFPzkw11+wp4_;b;?AbQHY@Var#rYk30LUFg`OG(zqfX!2!8a z)pkIy70JPQlx`zJ8o>(xj;K*QvxJVL=K_uiFP~a;5l)?x0P*VG9%py>T7zL-mvH2D z?D&FM-Cpg_6p}3YoXVtkZT8Clns)w%h;=P>gKq~6Lq!{`mU2)dJ&T59ip+`EXFZ5w zSHcQV-lH*nGfl*R=TTY?bn1Y1C~W0&T_}t4VT415%le?2_(HEUT|k*3)Bv6q!9)Lo z%whX@)@gxnw3hmfnPHD${ed6y%3>+IM@nxPn1Lu{_PBAJ1&cdXOc@#Cu^c(y{ddEw zL$E7-U$0cC6&`2m;oidcwuoLAWZDQvFkCDk2sDw%JBe_v&5)f|Qw4zcEU*_|LKICp zQZX;@P6|QxHCE7*o>*GAX@qC;WIGyH8@NG#H2Jk4bxA+E0SZG)Swujl5EWa0o{%Y&XNYI1x=3ARzG**9nS402FVeZOAH=<-gOwJ0lNJl=xu*P9t8SaE5vcx6>jr~EFj|||Lh|9r<{&eLBNK<^PJTSr9ZDgwlxH!;k;q4ncQhzUhPP^CjY;r{F z)eLVp8`&Ni>2=dD_bmvo(!s>3U>hc{Z5EPJgEk%xHQZmbQMcDXw@zLym>+YS6(Di2 z+@mv`qYrf$tz@gve-#oNY{6NqHz>pNpVGPw6II{#8D!~6Ow18bdKhZxH8lubRCRHblLTmyJLw4|t1 z4nLm+rJzl8n6!tAm;k(nqC413k&nRowhUO?hN5HvRG4 zwgtVW+Ih?Pc!s|dqqD;A0Z$v5S%2@^_!c!R86Gv$n8xMCFH0hsXFBC_f$)1YdAnX^ zu){~~m9q)W+8Byr-qvW`E56%9>mYt&bLJ|03q$cIo~j|kM3wnDuQM_nAJKYq6KmC> z1V@hnFByKs#80l|)Y?ebjLI~Gd^G%4GC-XZ8k%<_tk&hPDXe%2d?#{H#oY=xTxCIq9r_I6`&6LdR9gPg zD<}woDbv52pRc0AsTPawxQJes>E1klImD^l*FKTWYH$2GWPW)4P!kZQ^s%Se(~Ltq zQ+6fLb!LxH$ag24)_}A1>P|4SogVttY+nE32P3(*rPkt$YVQ$gYScC zPK0-X(r$U=u-$j3G0(Y;?J_vnt|jOrNf#$)~0pUPwB)cTVEe|yIK zFX_l0u(p1gR;l}58L{QG4a=)3Vm8`+o%r}eu+2Dhi+X)>S8%{3^aBOl-`x^B-S^Mj zyb-$efw&+_`brF;Ujgr5U^06!q>9vA$-p|%r)Q}(@&pPeOc2ltpv%T}W-5Ex2i~&+ z;eka3WDS6*bpWgft}Hy7ZIK9iEQ+`I)mz-@*Zl5SjJYra)>R{EJc1gWr!J>KVrt8I z%V@K$V;N0I{fahYbh}U`KEKEtDp~995&UcAxOR#ot?VcHQS{iVK-WRu*YZkD7OBd< zzkpAfML3xVydOs>71{#wRe*caZ>eR&jcl)Gq2_zb)catJDYRBW2(5gF`x^I1nwkl8|SVAVbADrwW7+NaJr&wHEsAAyceZwKv!K8_rBkIY^8x46>jyqoh+V@_vx-8*%lU?^5;PcS*D{ z-k)A#!qKkmng%O5t{`uWmBt(nKjDZiR|U~hD5mH*E0)Ur#{7th!mhH`b0+(t3p;@$ zd$atz3tq!E=71x-dPn&%#Ck@z&5N`J|CvAn4quf|e+$NECIqwvYvB3XybnYLtpC|` zT(5whCRu=OSAY;EQ^cjHCcotSw7a$;qx)T`nI6yABFVy`wc+P}uFe+WlFe3mDS?pY zl+?S)Xcp?7x*Jn8U?_7$*N^$0=BN9Fj;AnaGF-qh5Cjbc8@?FM>PtYb@)-%=enhYAqW6VV6dTf8jxeVR5itn}a>NBGKIpCDBu z^s7(;6iGJ+t2vbVDdJt0^KhTZa7GI9qJZswp}W1EwI~p?oj{i0Hi}%K!_pd4%oj19 z>hbyo`;Qy)VUc`Vlr=`HX9``Oyzqobm^ia-5OU)(Hhb#2(9QfiepBUTeI|%iA)9k? zbFV~7j6XGpClBm^U<&mBcuaV%0J?9iE<$_Chy#T9c)^Vvp%JC$xSbv9DJfK{!B4ru zS>)qc)9HZg)&sWQH- zdr&Vc7hOnUWDph2bXv5WFwh~Lvt?`&pMn&0?YPH`m4)6c`PhqL zZu+i$%BNL6R|)xLT(tTOZc1bMHM-FDR%N zDoG2F!roLoN3V_Of41NLNL2QurxX?KY$2(g^hC&kH;U1_-@ZkTtU+&M@gAJ@qbP^T zO$0?2R*&6DdLksDuDaViyNW;c`VPXoo$p#+g_Ia=jr`!OXNI;i-jL7m!s;X=MGaSt zi4*dXB=_WScN^HNT|4VQkz-UVGHg^X>{}qRrL>)E2idYFJk^{~`PS+f;$rSsPCLw{ zkU+5gaJa{-wb7d!2Anu2l49IjL$}g?-SMFM?j@Eojlt4}jR}Enr(g)Q>#-iDm8Elz z;6yKYl`o1Fv>l0Vf`TIg$-HLl$&?O$nw3@$39!*?!ivD7H<)tEM)Y#VFR_80go}uD zkZe`Jj!{X(3Q+O29b{8V-4jjOj(qDd(r@5z(Om@dZZ4GHf5>4?OIw69>UGCwW0y4; z$=K(h4JcW{`+Z}Zdt|(Kh*!@(!Jc4=D`-V)m1Bl zQ%xisoPAUp(U?wIeWFP2K@v70ao)hh#)8GSoMZr6W@7hsR_4hRj-P{Hl;-CuBHf;# z+pdSh^JJR(hLbbOF}^+bG=)g71!i4K%0TUEq^4K4fdP;O$y08mp;bI*j+yooo)*{y z`}Ht{j%8s-He`UA#-K1JX#0Y}lo`F7H0HOdI=|}VOBosPgt=r2qxf$B0pTR-v95p* z3;ZZDVMRwO8r%zgYBt}vI6C2BRUHO88(Q6UNB(JR*xng8a3@s+0_lLis9diwT(CV)F+$ih1*w*+f-Z;*~`TRJt5rqBD<7LSnM7BUdmdp8=9gA z;8Zq}({6idq~D(OvY1U0)>B|==pMqNnI{>R2sH2YItmKMa-vXF+=@kw?>jgx1BVm^ z{b&?!1#-ykD+H~Dz;SSkt%uL6#APHoT3&yXx7g4{ZrQ<%$mtj|8e5?pJharI!aFcm zfkjq{fP!auV5ER`z4Qd@s`>!7W7q8&rgk_g677AqRlA;QX}damB67DSN8^66&)OJ6 zByDzL)Z)jAhd^rgGTx{tkO|UcDC6*;MYLw9g<5%_vOsqRJt;<%4{Wp>Mtk0*m}`z; zF~zf5yOSLzpxyG9JfyBJ34}_v)_>2ZxQW|N@brn0clH7{%+B7WixFGb8E(W+H(<7F2nQF<=;`?MYHEm3w1}UD z!v%GCXxcX(<7pF#6F%wHd((X7-oTi5v=QSOo)8ycy#fZ&;$~9C1H2&#vu02~d;b zVQ14Uu4k65j|2Amk7xKUGTmA^ewf~{yR;-Z231=s({ z(3TjDAKc5g+2k&aEbEW+em+R0H0O99@S+V&j77Hud){|;1?}p~R(7K^kaF~n@)_6h z)N0?T0SG`Ttfr&4E(~;R`a{XBNxXGaYs>a|T#b zQ2kBj)d)(=xTSM8Bs_mKwmIO9&-SYq{S5orH_qy7@YGk6_TuEGUfB?r?g;HvVl(id zey3AbD7zm?$_B7LFo!{;~h-;c@qi$0Y9XAaHvS6PIyA{b)KSn;S~pF zJ6R)nFwWs>-$|Tv;TPjduLJBmWQrKoVEf=PgW(WD6r7G9XFvMQBEBo&wb`w1UV$({ zlDO_-npGVw1DB|?&I~_b3p*g&X5pJmSB#$TT7)mXp0N8UR#{;i7{3gqv$qpMh$mH3 zyT1kEJHRF19vRcK`i$Da`gb=G*cns7WuNm61em1@M$na4Ho?3rN=!8M$8dF(a+RLv z*X=GoaBf-=T8&@bllC+}>FlXure&(QpO{qH+9L?xIWfzRrchy#=w1UK&$)K?>x~$v z#6XQiN1QSsQEXY_U(HW+cb1v3yH=WDrd~bgt;L*kTo)d%kF#j&@RuOYI>REtD~z?d z>VS~phrg~Dfc4z+E{jrrDRc-A^+@MxN&gz3)?A3ylXXwHm1qebsU(W)v&4bi@dZYd zl)g!(JHSEVS>ez*ORE)Tf4y~(+i1kFfP=i4tL&6X^AF_|0rY~RXN_$ zKG)^9(MbD_Q{F#R4uNPrY2|Y~&_#V(<%-1$+(V9TG{^bIRSWOhQnB>)$1|z^s~oxX z7MV_(ZO*WbF&r#|lOkzVb{nSnq%w{BoRjbEzG9xw$1H%tfOKmGQ>!TN&8Rs(d7zFS zy#M>6_GH%+2OrOBA^ppSq(+3V7tH$--)&BSv}Oq3UsjGa1Yw;O)fCn$vE0?VORfnA957^i$t&iFUe@FcxDisz)kR}j!c}` z&6+$LjK%_}S?Zl>{VP17;^3*^ zx@8`RBvVOw`c+A+Z=YvgP{hW-D%SaSBpqrwqJ7s;sOcj^;NPH=C}g_2V~JUPjRMg9 z*+T>)#sSwVeo=**kc{+UXkmCcvm1n2E%-d*=^q=DFOB>P%<6}G+Ii0k*nxemI+}*T zZaaL&#D&mCie7gD?BzyBt*5+tz1tM{AG!9obAIFNiP_kZwC+Swa!S_ZO4jemIPXhY zc-gc+TNE=rf#|T$oF0wgqKOfCTB4PY76qs^1^>+f28F-?GlW*xKHeJn4p2_~X;1p6 z;(vPJkKp|DXrYkw`B@7JdFB3>5AwepTYr}mVElTWuR9DnZu&1O`WPAE>Slhf1BubJG0XIx}B4IGga;e%0UqACDoY zL1?fb!wOs8S2xzsppp@U_4M#SY5KZN-w*6)#og9)4xK*N02=$hsC&z>IFM~!IJgCO z*FbQB2bUm0gS)$1a2oeULy+L^?(P;KxVr>*?S@7!GiT2}Ge_?I`+fhq`zd&;)>>6d z-gi}f6VLktLOns&{FuaY^*i@5hJuo&+f;;`le2vI)Cu7)9OAD@z9)o!wIafyt(utL zN=X-Z6Y?@P;y=;hY7s9nxgKJte}b^D0`S;_1AnQqc*YRdDfIsXhWuCcU zfNYQl4GBTdM*&XyPyUPS{yjY86=rPwBDWg;zlym#?~ng=5NaFnzYY<}fhCAz#=++K zr@sF_xr+nn5h;glBb_h3<5S3oNI!?Y7HOLeYEU$J$iu1gbZdT>eM|x9*yRG?wBNoE z_|?1nD{&g&hQJw244!{jPr0`tuWv{arPZD+PN;EOo&XQ7(3P zvzn8mNhvGzjVyzG?+m{-#Y+}6759FynSOm&(Av7wm|x;!R0ccpxrh0LW}XtI;5)*c zCI@5CdXT2q1DW!5V=+VqJ~_1GVgZ6`%kp;ESn9D36264j$R|7S;i~D((UVhl)YCGLAaF#+e;(4_pr8E zGw796d$e^<0cB>$X9GpR!8%8?yEkvO2*$F%ku}gB68Y+UMP9g}woCmQ{K+c&bBjOv zAZ*^mBKn1ynV%1*702B>g5cjcXCAu}=%)GRJ)nWt40Dep_gd1lp~LDM^asH6XI+(1 zBdh)J)LSAW$CrhvUI5nvE7GTSNqFxpd(A6R(*AVbm?e}m3V|8;2A#&9gZMrrD3^=p z91rQDl(9j)aXhNwW;?;8wMpVwT2vl6S9?cmGkCK8Asz8JdX?_+B~*FshHf2`vb?*% z7Vw3%8~H=qu-?%Y(5S&ExpL~xAQ#`2q_OG|Vdeag92Rw3EFnkT!uWoc+_8S;OZl-) zbfPd*Z&WwuqPK0gw{I&&#TeP?14v||d?uxSiw~>emt`CYp~}_1elGf31gU_14UvR+ z|6%?nM-CdOg0y-`d8r_v)dVZ5g~C5Jn!qk}ELmKGedN}Y887x6A$8kWcSQ6D_#ZL% zNQvdbSz?kSR+@Qpqn@!=Gt|cr^@+N%3d(;X3RqASM0H^-x45HrZxJd}&9uf18%Zn}_!KSRIAy!I}YbalbIUKtM zy*_iyd)d}{Vh7(EVs|QK=O8~in6sYNUd#5GR_+lJp$NpO`mu4m7^>vk$QRrb5D!^` z(gr-^ED|QK+aX^*o1i0ZTg>{Wk=Z7;dH}$Kj6YBSX~5ayhGYsi+JS7ye4pE+AbarT z7$m{=NYt5 z%KwvxGEvw~4R31pe$I@B4|aU0?0DI*&l{j*b?vh{T?nI{z10T1e99(rtWoWr)(&ce z_zpBrENptYeL}%oH>q)ZIo{?zlS{vMee>MJg;tOn+dKQSDkmO@MHnA;0ePV{I^~LY zD=Q9I@J)+pB5vu`Hlo)DIT^2eL>YL9n(qAET-;M(O20v7;L{YbvI>aWtVp?fURcDW zg?5OvCaCeCU$4vw7F!)BtMz1GPisZM43X3MuHi*YJg?d_(V4YV%q?aU(%41PU!9}zdy96IDBiE+}qOJsy1rEKKAOXaZ%rWG_7f7(-hTSnVKe`KhB6EIX0 zdtrQSZaF=Sf1YU>Rg&fApK()o#62HnADdL6ovW4R)I4j>u^nDo!#Wgj0Ay!HCO0EqX-a< zd~+2jyj?b~pyB}P9%{*W`0-m?c5WI; z>7vKM#-W2&4Oiif>tJWWN;SX2jVb|7N6w4Ji<1a91P#!)dDk2F0A#UDa)Tk} zhUCQzBeRpbC-mjn_{|CmV7rXV-1638)VRalEjEQzNsY5f=(O&!_cfaA(4%u+&7aGI z3TZKYiQP8s*FuX+PnlF6F^jy@m8aoO1al)aZNbPn zArMOKUiX4L^Bav31vD`>FnjApz?%)l#FUofSLMOFt_(yMSV`59xV7XcMcRNq*w_I-#y zfo^lPw#v!r687YIkIQFz5GI5{{(azN5}q*=mE)muA#^M1QHW7y#A3@+Z;&%wQlP$L z4e8{yrG_UPSsC)B8~Rs21$l|8fK(%EQ#){gghdR_YaJbLK3yB-P3h@+c;^D!*}b9(L-#Uc9N!-Y`I;DnBp(`BXC72968*)XD% zvfc z_=Vg-U>yH2X_QCRIoM?W3DRZ%m$Vvnwm6mW))hT4m56^GY6|?8s3) zpXz2-|3ugcDWIv>kyTW#U-^4l$#yh;8%T19Z=Ck*Lb@+vW4-^PU}<9*7ZXLR5}zrViM~s=fk{{b5=|*7H8(xc-K2i) zajF5_ryvEBDOXOStz-jn&uoBAgbCxk)-eyLuDYEXcrgQhwxrzbDO@!jh$ zRgZ)AO7a3@Wr5IliC#wgtNNbYGvBR(6h8ZoRq`7`OXnao zomhWIZCHg^4-Z7>(8`h6fFd|eitJ89YIRYA-+y4;Ip+SMc|ZCA_GkXe%Yd&lil(&R zC?j+IF6lHoN1T0O?BgmK#OrdhGCXdDH+e(6gADR!5ehC-F<=|HfA8ogW44M0`4~tT z=Fn5KG_GUO<<@R?zQD;EE7llv`DqO#30K;=itAdi0jp{i-pV3Fuo~AX8gXs_DsVU> zS{1&cmIU1K1P(})BaCX1taQQP?;h6}R#O{`KS~_J9CBlP9P!@&j`M|5p-mromLYz( ztSl^UH*9W0ekk*MyKv70$y!jf%}Wo&#kcR6tK#Ni1Y+R2N^18J-p=9dHoHP30W-Ku zh$z~M?wne2X-Orl+QNZmwMkx#(GyF7=?SeXgWW^5XwiJ(!F3Tb;PXh~TlDjqLoZKq z)y^+`gyZ#MkW1+4U|r=!wGW||m%@a$BSXi&Ra4<tee?PD)j{l{hFrRc|$3_JPmW<+CZ$E9woctu7HvRccJl$@I?763N^9U9X#i zgyj(E!}Q8$kW~C|Tmy$IvG1 z+dFTJi0^HxVz=x|JN1>*C_4{2;SWCeaX&oa)7;g%!(;jSR6wADv1K%FxJ4YcZ$)4` zSx(SxsMHOd+_TdMN#B>v+fc+-{Um!hJ2gIsBxQB1z@YO@R+bye|0C!rz^XB}E=lP}`roWq~5nIgI3kiq*@Hhr*|8O(XtLF;{ z!-(ta=wr0BFO!|nSnGN2fLcQTfg^ms9dk1HdF>RAebnn+_vLDfn63v4$WV+$ z6zA7svxsZWBw_1*D1wYHAv)$nGpj3sgnn*AV}N}&dS*;fS&6&D)m@JlY&y9oPo8zB zL`rHwR3n=qqs=InLBcxo<#C8Q?f7_FpbE#{_9Xqs>3+L_ZFo77dwnl4=c%MI|6NVY zXFPbaz-}0#ZxZ-0I;s`5?}05}iF^q=CJQHLG^mfp^?uV_FqF233MT;& z$$_w|9d4Kls;*nv(I|{cdDq@4i?vif*50}Gh?+xs+QEv0RlN;w=?Q$uEGo@%m5ddtdBuPFdMATz4)vQ7%k@(KHy`~Q&Ni3$sJxd7p9{*wh&9sHWDNU@e$j2kqmeXb{eBNM(4G-GvlH+J^BWVfE3MMgDN?|{!z z8$8$2I1f16bu%7M)$z0s(Fn2YqZ719t)x$L#c(p<;lXgp2 ze3uxB-B+R)29=8I#+HcBO(nY59N(LlH!1^`5~SPy0Pt>bG1p2RKYY4QWFqEk>}W91 zi`np|OPn`FAB|KeaVBq$;&It5Jvyn+ebex(GQy770pL@=MR#urlXjz3L}%i5weR54 z34#lK*-o89Xm0E@I=SC_dy`)vq;)OC#b@;SdV+w3(2j7yjg!9JaDHCa!32x=qMRS$ zzsk0$oSv6YrJAcR5G{a9J0)$=nzkx6>hksUiJ+9BkoEonOUx9!07Sa@3KncpR4H$i zc--X7CX#;Yx3O)nwW0xLioha4zU;BHt_H#%N5gpnhOl zi$@k>rg}VC>N`EpIhw6S5X_{XpT*6=+-oPXHIkGIB+^#9<)=pl_+D81X?$>+0yfVb~mhU?wvk^VnZ?NG9r-m;8bPGiU zYFWss3fex}`F3BW{xU+ky2rU-`V&a6JOM43;jP8QdWg0#1*5tBt}1+GWs>9RYx&fi zyw~39`3N!iv#^7Wz=NKcSJ1xSKa*iBs*6deB`o%5I<~ijx?Ycwx8yvb>|BU81ARSi z$HJ~hNKu;+?v(C>qgANAqn>=~TO%7TY>u)~ZWUvlwCgX~=Ic2nxz(G;OJKo41PA`yp6F|bJBR>q^*T02l=fQC+z&d`CC z2(Rz`p&J9@SX|U#L&eVvsD5q>o#B3MvwX07tho~4^O+^WZiO{m+;g%(eF5zJIt z>cd_6N_s`qx<}ZKyss{A1PcC(3ozatE{NQIH$d=MRT$EkxiuV-m2=uCaZtxH%Ui9# zO*;Xlw0VzHZNG}JR%+W%RiaShVYv}-Dm7X6;087`H~1Rg<|^i3KzGe0gUiKs;3w{6 z`^XLk8Wu<}C-7;0ragRk~urwYphs)n6^>3_$VLw$?KWsLR|K7~C`&8zmZ z9s6vaJMZJ`K*(M6qz$XR65SJa`gGandnu@Sl1{c8wkG>nZ82vIj(NgVfu9eUTeC~y zeb71SNQj*nA`=!y!6a9Us&&@m)o3uo244@jd^C)Ot2 zTK|r`ELCeoVkGGgVi|DPvJ>GG|1QWNx4#lEcw8o)D2?fKOTEwoQG>-d5aObC7mkBkf0BV?U2?V3af!Lqwyy z95vp@L)?+jCPo_JiA`ss|Ewc0?~d#ufKvA>vShPab>kI&%S`u2 zEWA6|jmDiB2T~Lu!jh##SB-~J^1mj!iDw;CpS&)zmn2~MiDL%>yR?m0j^@CSEb3+! znHJ0ZJLvgv?M>I}vxU1aM`pc+1xM_T5{1QIGngi*=JKEH@>mEMgCt5yFD^^p@W&f@ z?W(@X{n}$(C(eFbC?RnJ_T_3vEAJcOBCgqQFQ#P4JS+H&-j_p&U0wa`%(=vi4f)!m z`9n=>7R%kES;u1gY&W4fX`eoRTg=%0a77G0Ou`C=zb!rElT{$ZX&I=t>`@P1FD5T< zsHwv~zScigDRZf{M}6St(y06cXZ%m-FB1tW?=jKBSUnx~KYre9!HdiD+3&Q>&=bK; zt>lI7H8IM)~r28836*Vjm)eq&7+HB5T9 zI`Vy5a8(x8nl+<05$6zq8J8&Q{`r787Y%&c*1DOu3a3Z=65%=HH&C^K9;%UvIv}L{ zGbmkeF5;C${lbiLbWoQ2Y(pfl65XHdXFDj&C?pQ4UGld1M7oNyU%7Rc@kvh$_VUfJ zsIbXW`XP8X8RMzr zc>G<2G2wi>hSWy|4`%p=zXUy;=`_AK;wWj;+o+SqxbApu8&^f7l)OwbpU&r=L3)08 zyo7Q12NAEX>22kQOG($;$5E}#&F8#b)vfsCsA&p{T%B975gHioCUR_$!xvQ#O0ABR zDb|~TuU+|UyuzewzzMC#!!Mph5be>np3>mj9)UduEd+ST9WPwEP{eCF0lZo+hSl1|UM zmT8lEdE`+8KuOlCI&losPADzpXTiSlX@D$ZjGTBws+!@WED#>o2VpxAOr6`I6CNcS zB4ZD;@2vKv$_)Q4(KAD864q%VQBcb@qv7F|9q%4~a5d!T@E1m-oz1sAu_MLm9V|YK z7ESgm2ZCdndx1;9rtM||6WvylJA1Xwxprpj@NmaW3YRmbhEIt?!X^?8(VDHc9BBH8 zVy5518Qat)pYU6^ZsVV(R@ds=w9WX2AzA5cC*S|+hNz%x%gY0S$(~+b0`Wc72}5)g zO^rc%(R6J2E07^ZJcyVz4Q2d-4tb zQo>Ub$w-R&)b%8>3QYU6*3g~z+Kg!pv-;n-Cz zGgJ9KF>A|9_%D``3sd%&3%+9)AMi)le;g#)u!zN~N6=C|U8~46+wlL}>fgXcO1(;G z+sTyie;39;O4Qxk3tgm`6`GNumot=_l7e_x zpkCHsQd?gioR^o^SRtjAfA{U%H{`Y8SqopFOo5oQv$L-QaRtvp(r*(Ro3gbc?FQ@W z#zyXCgR6FLP^DW3XRMUqnItKW(?G5S@M1UFU@zw9>Y3}&Y)`xA_2%)hjeQ+e*O0$$ z{Lf~XzsEO!?IaIwSY>79dQtf&3EBkQkHbH*25U@)s%;in>?l|)TKryw<}3ApF8kv~ z7n?okxaD%Pvf?5lZ`e6F8eZlrbebkbwGa@(R=abQOM-72aCj5(=)a^TCKlMqI>)Ui z%;VP6(9mqY4vZ5hcvAobqU8HKef`QA7#R4aP>xWwR29kjzHhBOlVc-rF#g&Xh{t@j zR?oXqTU#5LoUG9a`0OB!H!Z9E`1m+xZ*wGX)vlb)Ll?*NB)sqOW4SZiRi#jlUX@MV zdVeI5F@@WX2&NV;=L+yYYBQi0>64_SH3uImKE379-ky1LtF(b^z&a_vn`Kbj?r1X4 zGHAo^We(n^&Hdc|MqiXM|4s4t79xE7q9={ipt?F2(Kf4%1#}9&j~_+7cA0P))yHNe zMSCQzcEZCE?by_j`R2>DX5P!HB2UW%bllv0ovqa4SA7ka~_gdTc0f&#nzq@j%5)u zC4BlFRR#bBv)O18^H(ZPXiOyX?l-8l3l@7g0 zV>2H88)h?jeA=L<<_At&DFzh^X5~L9d#Y)a=_V9r_#18Rifuwe)z)*Rq4Ft@+I)2R zw%YXX88d{Hts+*M9MV?rj5KJAIpWc{W{FU#5_YmNUnC`@EYoBoTM!!FpI$WMB{6yE z*4ifj!=L?Y{oh5P=>6ZrWjW)~Gjh@eskJf-@bYRBze%viO}0NwhBgZMiNlp%I;%KN zHHY!5gqoouVL1t}q>JDEEZgh$sAXjjL|y{xe=~yApSrAo%qB~ix@SEi6L64DR9m<_ ztVI_szdWytszuMQjx3^Hsk@}BvV5XYqDX(ONo3%}q^zf1(s|sMB>Fz)!+}MI43x2z zuac6|G8U?GXuayQDDv1IQ^?O(Mlwj5*}%$P*$SGTQGFzwdEBs(GgI7ZOM2ckq86XX zl-4Xv8brDt5HJ;v8_8^4Vkr$zEni(OTy! zuGSG)`@7z(Sy5@_8}x&+p*A0$vWNk4ri`65`oOoqsgF<|eZFtIl43IIyEy3%d1ayW zt>Q=CO79nPI?!)2!uj38bcCMb>>cxvTJKn!lMn&ep3VLi|%WdYz0M8 zb6ePNRawCgFDD%+x~dBuf}K8ZP%&+XIVC<@0%$V6<@T#{9?pJ>!XZzVt%%GKPQ1fX zzKovDR(b@|{?Puk14AsPJc}pJnWSkj5dN9(Gpr7^m$)U+OIDoi^n0+B6@2dV0%+{* z7?9AXpI{AgO}N{Bx8!{~^wHw9Vc|I+!kD#w(p>V$2#W)a3>D%pCPJBw@dZ(r3_6z^ z7i&zxXKrD^ra#%kcNw=Lx4%7|Lgq``yTGR74to951*Fe(`FW4JpQR`dbc5s)+;4*# zvZ!fn+yj}8c8_(nW@W;~dt?x$DUm~de?`sB$90BS7n_8Xe^pIgKBwbtHr`1^Td-Sb zJ{e-=Vri2P=%?%Nd|z&c$-!^6OsWG%*;J4YxjinzY*n6*5j`r2+fe3_5aqXBtY-P_ zzTo4vtUse!a@v{bw1l){Hh8}*RMHfK?4Y<{@KZ=)SRcvDAkN0y_fUP+?SJV8-$b?rJk z!=F~3Z3_(Pr=Nc#hCB*j_T_Nx7opud^IpYjM`6crhMPe~lYhMia*r!1zv z{JE4d!u$7dyvKfq`^5-j)mOr3a7g6UR6^r{g z{%QLE{v8#HQd~lfA^*g^^O`i805`&wo(NG*z(|t8*&G62l)h8(`z_f7De4HGG8k5B1rL!3JC62&;G(S5(Z+ zMvM7I#1@rN-N?*_=ZdAXn;~y+Z*#(xV03`iy{pV&C`w26GrbkA^#NNU_;KI9K`W)Z z(eXa4M{X49(v2*h1)nkXn#C6J??-BsF49Y{2V-=;j<59K1GryN_E$X$O;F&Wifsd@ zXFI_^FshZ$#3q*oT~0Hbl-1Xi#Jxvn5@)4`Jp7)H4QzrN7n#cqy7Z|VlbN(~Pa$(v zz6DP&T5uA>zUsrJ9=Lx$kp8_Z|8+ivA%>He#bsv(V%SvXw9%9tyT+{KlHn?94N)M= zP#U2iBiG*yi^-!4T=+Lx^2H!sS{Zzm3+FT#(HfGDB@-;g?A;b;n3)@ZxvOkDoAxny zhni~}9T_=2o3Z_Y!8Es6w+&hB(}=43rE`b6sjY_9efynkhf@W=vZPzrhV19$;$%JC zvB2{=WUS=xE}uVtmio@c#kIU4nWqHVxJ-FVp?jMBy49kKBNhK{RDT_>f7zI8RQE2H zS(Zdn@CmN+9qTJLb=LLrY`-BktV}u5Z!hZO09eA|m1O!|vF4{%*gM*&w?_Y|0z4WS zsk!~#q;^6BL#+m5&3NcSE4aH`B%JPi4pEHFprRR;PK!o9Fs|55mvn|6wkboPiP5Y| zQiz2IO>4}iFBJ9la&--i(Z~THK7{Y|TPz5CGjoj(&~x3-=+tPt#tnm9t~Yb>zDX+d z+fe}pEr2q~40?iubz&0}&Lz8xUPK380ks~+XM(m9LZ&n@*Ny!P&vV?|T?_5N3RW_)&lPa%RbM}=$Ld^Eq-f^*2}8&83D*N2Dc8L^-r9hk zargNwls$$=cZ=4TYeyA=OymR|R{%&JPV0c?UGx>Uy$~YaDwjnV1G2|AwKHU)TX`Co zT@Ui@exF5J3barEIP2{VVaLLdSDiu6kW2#-uiFP({pVGzY`6bnZ-0H2f*sE~_rm0b zz-eraR^Y>rKah@-=!HPKet#e(bHD36cRB%VVfx%MPKc(FpJ$$a=^=b z!0HtGpa$quHLupUNqOE|IYyQ3+imnt|L|J1zU;2O17me^abrrv`k{mLA;xYKwaa;b z_m%&#EB}(xYY^cmkJ;?ZcO%;uFVKMS430hDq1&;S0?{|0WqJO>I21(W$n--}eEUe9 z`DnzcG2Mp0k$J(3c6U#1yHSJK$EjY85O_`)r9brI590Gl@ZOH`hTq=6rA_Y?Gh>{) zSP=Sx%jdN%y*l$HrodCG8uZ&LW!(!E=y%Wk{9)4nrR7{!@4F594dg5+CBJOQCCx3b zx*c9hk!0v0y2|jpDCly=$sowj-DL;lDwhRzttWjVod&)HFRWMeoTESDp~xONEu>7P z5KP0dC1<#yBekt^@=DHKkAlz1V~3h1I-@m?^rSv{ZJ!}=Tt4EEj-cpwFN1Vry`>YD z3;yqN5vOzv3ARNKj)EYOJ-Z0u30)(+{c$huQ$Z~$yqerdityryv%5QVpJ4U$u3PrACh_>z2mB+Wultb4o zH^cJ{t($qYM6u4eceaBrJl>JpW)X_Mi02dC@>hu4_2>3_8o1+Bi_M69$3l#QxjLRR zX-!~IRcE9Ix)LtYQrbF|FQOmKL~GRz+~e=F^=7-@K95I}X&$4dy=her;v<6dH#cWI z@W7_d$fnUB$qSJ+#;X|z7&&){J$_HyYQ|SYBa!wP7S z3?VQ3GRH`QzU~dQPzF2CoKcDm)Pe8sK$DB3 ziJviu}Xak3I?rhJHx48r4VZ7eVwrj2L*z|$cTVZ z8(i5^npE?4;R6QA(L+zvT|oM-o4!@o5^!jom!KD8@9!NndU@k#$}FH z%kFD{OdeU?G=15DIRzfCR+8p_uq*d{GBHmxo@WzvUq)kw90QyQ$7xdf4OxA7w!!<5)inPTJ(|8b!6Ri=(;a) zH8@ec=>5^gW_O5w0otwc=r-(jCs!D#nwUgvXd)8^+rZum&RVNBBr+7TpprEK>< zH|91k`fog)7#Uw-7-R8dUl9?U+`UEX(F?%;qVvMatJRSCfVd^~-96XI?e`584#LeA!dw z{|xc}Cxtp;T!;|53cGZi{Rr%DLo)%TrMGR|(VaYyvK>mHcYB_GOk2H7m|1)pX#?*y zbuE>^ySAz+21jQ_h6@V>wF`57TYtpa*I$bf2axp^vw5=&VWf9Y^7 ze3{kRl;Z#bOI+3c0$-3fQC#($Q^oLx|g731Bmeh7*jCL!;EHp!-cvG!jXUR+^V$e)FP1M(@~kf z-t4*X8+lox-e)z+U&moCoB@A1+S^OaeMkdCz>83SJ=v(OnaBS0=|iMoDHChxk2&b z1pu!5O&fWa$AC!cBGF@zaL-cHYqh?xo-FTi%uT=jAV+}YOwvSW;`g?M?q&m7iPVR_ z-Krw<$YcHGO3#|XomP#;3eef-mXaY(UAN`6bJr!S15E#|1V4ZBt@mUru6WFPBHO-F z4O0J?fYD!U(_DP>3Ta7)ta}#BpL6^ptciBf=r}sNvx&+n9Ss5`UaR>^GcoQLc`fM8 zCnw(Z=C#v1!0*f;D0o+je|RKDTA*C|JaKPF^wM)nm(b5ufN=DQlkRk|#RdNU(tXr#^>pD4l;GXeP+&!crFmx{G>o)YBmZQPFCyAW z2(aR1D&`fy(jfjxxiNCAo519Bi{oljR)nHwk_&ym@zPTy__dwfY5_jXM%oQt#cZW_ zr;)~H17@heG{(nti8ZN#s)F0;2uSS-|N1scSCficGb;^ zO*&MwX#Wq(B!SW(t5{h=^|fRm6G{fYJlr|HCAWV85S!E~Dl_w9y2;K~QVIB-0UoJ+ z2+mOAX~21kS?k4&Po(SQ{OH1j(+iTb%9P$VO*h4QYmzG)qgD7q_h18X)34au?GO|) z&NTeX%K!t4{XQ%3?n4#@pX%`E?(;ioInUBJD9{Go39Oyx@i;d=E`Fl*__4H}5>z72FjIa1|)sd^bdTP{9~&o=e3-Y_0GCQioO8SCoipPFzy&ohf9 z&7ms9R` zClJjeXhD#~5G6+mrpx;|lkfj&8vDV=m6nvuZ$|L!!}2 z-bQVFVzLgyDR_oeBZtZhWxdz~+t1@k?RSjTz@e8$m+IilA>rfR6usu{KBU@I&JZnPKb=6Ikq{{!WY4mc_ZoxlZ z&>)OQpP{;||7hUE^10 zn6G!N$F{?fcexXtFHs&Yp*(k#JfBkW;|qaN)*r)!CDFQ8dWxFp&K?wCJ0rZ>pVqtV zXj)~7S~J`t)xB@ciCU|L;+#9mIn#$Ozg48Pw07Tvevd8!&ID2wNcvyOpK<3i!LebY zZ;BZ5x&Kk?`){YUJv6MB&%>c-mdnU_JI4vF|MuXoTzlKdiLu+)O6_!lfk(0w4Jl_h1As<{;QL)VDU^F7dF- zW0h#cW}ElQ*qtHWk)+Z-&lBJj(DnDwvwTuPhoRWyUju=x8k^a}L{ptcuA~H0!WGGL-}hLm}18q#4jg zmCFhpzt?T+iIaNlWl6?Aswn>!nWGhAy6|}f$T?=`O3;**nfd7Cm?Yr(R;uRxJ{2DA zdTi9vhWKU?Gx+$WGX_q=!P2xm7+68RbN)*g=d_P@vA_3*$b(#G{uw^W3f<&rOv*9L zfd6Pho!b3W^ih)9gwSg>(7V@a;R^^}|M~!sJrIK)`-~@i$^{RkjS9Hj(s8WW&Ei#W zMCtNak{NVM{6h@B1|gk(=sw_iZ;u6PLT@vxYID`6vcFvKf0lLsq3Vkmf8}AXq5V+a zWzvuni&Ukqw)l=SOrKNtw2Pe$9^F9n^Ep|ys(=~~vn8G{sTpCYFAMLey>IP{jsGKr z(m;ZGEkwzPlv@5#-P@gKa}zr(P#Y$^zNN{M*07|j5RS4%-XfULm)sI zBDF8H@HYLWs}J0Bg71`lf6Xq_5&j@PvGZIMn|pRG)4Q(?eYE$g&Tf3%fQh!3P9aB| zPfptGX8=pwMyuF5?cd{Zr>0T5>S%T1RE$yAEq+MI)>W<}dee)Jus`Lp@~57XLksBI z^Q+A1XqPL~Qv6<g&jRrkyd zUt97vl64a}N^6|8lPb>i5{RvNKAe9rR=uOwtYP~P&}fepy$o2;kg* z8a4r43cXHk;5mkSE{4NQC!Xnc$?bGC_h{Uv3YMxxJ=3-3&f@x=B zuK>rAAFf?@{^x1#X*O;#sh<(+<{MlC<5NMD^)IPd`S4Sa_mmkGsX@40KG!o<6 zFXe5HBhN+4B>&muMdi18EK>t*xo)?>hqL&EBnqyV?|pSYy))ApbebMmT5{u`V*4Nn zH6&bp;RS7_dYKXPowQtLLNkLF<&VDw-@Skn1zqf;@V2?|Bjaw7C!(D$HvE`}1{Cci zt9c}oR9XJqT-~U#{x>Vrer1zf(7y_)eye|tjXU5=nz)e85>4TwLyF~pDx{#SKlWBX zqaV`p6YM134XZPwZA!kZtZE;Wf>#t7Kl1c&cJ3;3*=e*UDLxAOFcqLd*I9-utM*Y2 zKxK5HY_4&vo^i?9`>EF6a*P5tE{cn=E>71{^6Qnr$(l~MyMZ@6tHpqrhFb9N0XJts zlDg~6o1r;5I9Ey$Lx){joJ;SmR^k`wU z6dbvDE2BgiCRY5>tMyC-WnWV=6QdUlsOP=2Bz>clJ#Biy+xeYxf0*&l>%c-Nqsm69R{<%@&XQGolW;Oyy?cD z`f420>Sd45GwiP8Ce zdCqOqB67GSY-Q&~O|N^P$5wX2aD;d^=y&TlD&v8%E^j2?W0NoB7U2FGyRNde)3<>X zC{hEezawTXk@&ByvZvbpAHLo)EUs;7+YRp4xLe~60UCFA4H`VSI|O%kg1cLAC%C&i z!QI_+So__3y=$NIo$qJ&HD}GybBrERbw5=B4<2x5tF zFp)!Sy0qrLi&tt7fYq4ohn>Bd2?2U!wPR}rY2_}{s1%a=O@Gga_AZyl>g-;=*F4~2 zOnhi<`7F6EjgLvR8K{;Pah4ayw(1;hiK4H+v~punull!$EpBri&;;m3dRZv_WD@7n zRib73er@~x_I|Ya?i}s1jRAuXqN*+?lWrul^?w2{YZ|bja(>iCL6J%CFZ&3Usti^_ z9BRDIhce2v3paz#lAvEgg(Gz7(y>E7W^!$8tB{8p5{=I58K`d#26rN8n9UBt!7VTw zIS;RPBT_@TTpZE!r&lM z0J=yzE(NdxUOvUZ=BhWgKyye6FHoPzqKsDTzDQYhC6rgU@52N!65ow}uD`9Zfu6Cr z3ZbT2OkG1IPHdt-MoM*ua3ek%MuNYo%H2Y9Ne;{Yc`pkCqd*`o;BuFsNRtD%Ol}@F zG%d(4=Q)6Oo+`XV-25gWh}aqeNhLOk-+vHCv@p31{l&5Z0 zEyS{ig)!VYhB$VBRX}z!JtVQ)rIsDl01diAgE%Y^>%FqD9xGkyRgLO^G~2nD%#}aO zEZz4ABnD+c&JS7)f4|DaNmDrO8^tJN*wE`nz=0QtQt*D z)>RN=lziRS?_HTQswDk}fCH}{QF}fu-HDohDj!q`Yum`oz*cKV&xfC?)VH21ugPTA z2Ug~BYtP2QSWlGhC}U6Q9p3QjCcK_SW@U@(7j>d(qvff8d4a$;`m`K6ZFZhdE;P~A zSm!=V6IjR}l`*VUJ}YcjLKK!dYCwWpUWcY?ieqYz|B}fr725cq6iLO+2=(-C$->eE zOTiKm+vTy%ciHPUi?smdy>x#R0X7YN;4-He1wNo=Rqi8H;d6APqVZ=bIvH0^G{<3&hB#P;K`{pazi>4=;lm`8sZ_6U@=M3ShxF-|5 zRgDA4FO`*O*tXWKC(ldRCXDW@W}s-PAnwzp zsKIk^C8H*tScBY{2DUy#3g>IMtgvkHAqFr04g};h=-+zRiFGiR9&z z2i7tT!wNS;g6d?~QmZTp`3uCFsU#Qjp-XNj4O&md} z>Fs8i7Ix;&^{sm1g;8R1Q9L~|MMk+)b47F>k~bYW#BJ}RP?7U@4y~o%8joM6H1))i zz64tFBb+(AyUoZ}geJ6}0_q=p$qv;I^n>0~%lKfLav0ulcSY+ecd_&xgVsWXM&c$X zFpfep{T=lzx7c>!jNp>y{HCrtfTYqR{dahtka<-m0^_Ye%ef{@O~_y52m!OE<_|TA zm3K2R0G|nPk30sfFs;swaPIrx9_}0#){A^@e0nDlqbxvnR1>iVocPp$;yjRu@8lIP z5A-_$P%bk}Heliz@n?8{?p=Q61zvk*Nk0`4LGX`7NV3&jq6`|kF7&*$TPn(}Ugm+L zx3T*w{cO@HDuUTrm5K{3qM5|ykV((``43-WB_Y`9D%SNf?cKEJl!4AM{r$tj19UYJ z)g@m^b5Rz&Js~o2#enayg3V34~qKv_B8e^z?L+tyX>Vway5cx(m_DGjQk(nA_Sp2OAQN|nY zm~U`FSzb{cjL;|?BvcBME@!zv?IF(h*K=W)(+f`b@_#+4F3ZJhC&~TWjq{jCreC@g zj`(YcGbF(-%}8)63{X*V9gF`*C?#|42eB}4&LwIf8ptFq=bcEY=&q^m0{>FJVh|g{ zLQ9O20D@zU6qg+`-z~K6r^5-ZVE^t{d|Z-3yyY$q=ph=~uqlp{hP&aayK!Rh&It^1 zd)Kfm1NqlN$A7YaZKljMKCFX+xz8YL`8@XQe{x}}CY!Mo0#2L9DphfM5OPch{NjN0 zUFv`x^T{%9+q@l1UrFTmO*BD3atlN?NCH`>^m(mQF zfLbeDl)6aYvoR(p$8s&QpHI(-4)?z2=3k~*kO2GbxtnIcKjMqMmX5$F#Zj>r?3{YO z$ArV~jYwS>r1fbn67|rS_&Z(}Q!=VK5J+iCHvPLS$6WerbjQ^j>XdqfKNJu;yr)LH zT;Y!SYRAgW0~B`d0H`Wv`OE~9DjXUAxRI<4u7U}-94hWTWZV7DUsvt=!0cU+s?@Fz zcVZ9xIvLqs|M`l%?g%n$rn|S=UvwfBKK`2(0vw@n1;izArl<9h8k2yZIaAoKn`{?> zOR{jS4~hfvB^D>&V=_K1igPosVQ^3EvVBqUap_)C28RO_{Sy^f@p$<8;q(a{8$Pu<;ewsGxYhCi>wb<5+72xi53K%U%Nw z?#MnD8h3wAwf5bkb0+2bJ?2+IP*6RVrV%Wl(~j9ct*$)IAd}_djn~66FJ3FoGbbVgQ7nB?`i)P18k!YCGMJ+J7RGUw*13hKLUbMVfUEj`yFhhEpwg1Ol9$kmgR*6cfJAfQMv8;vDVwJyX4Q57;B}7 z3a_6=?H}p{>e;PQ>r}1Pxgsk^<|7SRQ1D_ncr#X*Yz(4?Ca9YG2fVXU^DPbI-WYc} z>UcMKyn3qAg7>nr!M5T$bChCPK11I~nUHEb)4`{o8WI)T7!Lp(5bAWpb}P{;*N|PX z^#Q7sm3y$2w$*Nq&Eg|8+qn_dN{x{h*<-9DiAi z+Uy#;l_zozDk!<>1jOf49#aA(=Gq&uiu|&91IAS5~CG?j~@Shl~(@aR%zD}fSPy$bsflt)HNPawja56QexY zYoVeSDb3NUS20@rliE^KBl=c+WtMNuBx^Mp&q9;ldCkeTB+#~t09Qb5G8UVIIiccs zcW)#An|?}0V8w9c%bGbd^6gGnNNLj?Qjoi4Dv<^8urQWrj>xA_dn6Yz=a&# zc*j~IPs$s#kZ$QZjRVy&v{6S`R{JifC-*olupi+aUiYH(0h)cv+KxDi?5 zu6j2V<|XB^x20Hw$*EK-^_3pDlfG9SXj3G`%sb4@9Shm^otA@B(R!Y0N-E5V_1hH6 z-2Sxq@0ldb`*hHs3#L6=Dwx!ZcMim$NS|XdK#}l(bdJMN7jIhWLT$)j6R{oz4A+>_ zt01<6ctixT`7{aIkx`oKV5ZJHi98&1x<}js;m)Iwp60YU=G45&`OEr=>RuJOS<&`J znd%;lBd?lrTlDJd=h7h){OKTWS}Jpc&BG8*bBKAV_JorBC3z=gV>fgLMD7t$UeTMv z?3x9(0}!F|2zMblX>L+YjG`PO$%y1CU(gt#k&27o$ZR~i7YpNXQM~C&9#zc1r<&Ev zsdKcrKED9j4ZCRjOSr6?uYa2-rdoVZLrYtGhT@rJ}MWcG2Uh;VIl@CMNNn43)B~RQeP{q|B^>je1Y&dZ4 zWZR!jY?~oH_z$b{ILH(WMvy?EKy9GU7%|rXR%CuHCDu+AQ zSEkK+m#9+!6sJ@X^bat##X#j$8?tV`jjEwNHlUx6C)GBt>lo}`ujt$6@5qCKj<=^n zh__{wrI$NjyP8>+or3k_Zg#J=^Xu={`u-L>pp&`*?oZ~F-6NWOjUM;}<6tLKS_GRp z3_Y6X0hUU_g|%3ckrx6~Zw=(7G?VixCA&P~x9eItWS?t9Omrswt~`*}D4pt&2$N2X zUY7-83xc)N3bH9Q<$qqiedn}{|Ha_^Q4A%nW`W1`T_&$R1){D$SE^AHDTAdel@UxH z3bRBrsNTLPNX4!#*&Q*?G+~UN3{Bz0w}SxnGj_Zg|Bxy5Pup*AaCTNNV_#x@=+X!X zfE`Vyi-;Oc4A%581AB84w^v3lBto)xc1<_CTprE$x^gQo5_*vCyxA{LyvJ_dHD`N{ zQ^xoDjh(tss(P~mKfD(mx!h)soG)1N(!Gn7hc<521i}7xcl(_{fdM>#JjShhiXq{1 zMc}vfzc7cJ`?&J2l(lFaTI>Xie#HF#6t80RsPqO97@&P3^A_Z%E%8p1HbG<#RoN`+ z8Z*CyDf#6lwz(;BT}NN?VZB@8==-p^7^<-YZpnQ126!65RZ~952}*Av=^#p zg8eUmJ;fkJ)XYzwIE;nY6tDUt5>n3AicwvvveXBsfeI#QXZtcpzhv zy~BA|fmM4#-vLT7!_R`BMi%&{U`sG+qI)e(s9Xf!*)Xv-YwE>mQF&l6#xZXv#`uE3 zN>kl91~@+Tb7!R2VlkPEl2oEB{oHkA7R%~Xrle3blymxCrZ0U_!T1gYm$ zryi}VV;R%C^CzG<$F2bxs!bYn;_HP~tQI39-6IUhc9`TS0}aa49=9h()kh_>Cp+F= z?OBCdR8HULf1&Q+m12&*DjsCZZU~0O9m-J_P@s2}tX1OE5nLrp@Ur`|LX{ox6#~Xy z_r$rHH^vIPCCRzSd20HoMNanBgn9P;^spACUL23aOHCrL;!QB2o{4dTgQaO@MIx-2 z4knSp$@v#M{K?%yCzo&Ed(Ts54^}6K#v;lNmwxJ?&@eZ$&F2KfKfY}6h6N~2IFE5I zFvX^yhb52N>K%897J^R1F|{tr-bPtxToGltifB-G>XOOz7x^c&Sjo~a{NA`wts9K& zn>R+YSy5}^f0di&g93aZFc&yzAZgB+GO{u&cGm8Hm_{?W{Zfanp)_NQKPB~TltM5v z1MBu1DpDO7zqU%gu5TibqTf4(g8^Ui^t{g?HIFb{-MqD;09)&O>&81e;Zcxhc{yww zySCca@4QWozR+0!HR~PAH22;_ShJBP37kP{S*Q{gCm|1O8u8p$-LDaQOdO3jd_k+_ z{Ni4|_AiF~uTbG8w|G|!Fd&H}HpZzOCEayT{S#c>ShEAKOYD0L+Sgj~23>HqB&M@G zz)ns!2YaW0nz4(XgnRdf2D6aLu(}G8s!lT|6DFpCcBEG>wCCE7SaHCB2vSDt9);$; zh_i`A$qHPbtIAQ~x9lwW>vEFx_ac}lFpPs`&v_b)RQ1VzosC;!aTE6HRr21jUVGs4JGjGql3s^L#;t!mWg90kGHb#q7>RPYUCY*C6OAb0*EXb$H(ui zzj3=$!&NdS4mTB!@S1uX?+q4(+4FmUQQBx9jvi_DPdgg}KImyee7yqNk+aW+5;NY^ z9V@5Ai(t8NDRC3TlMRY|Z4OshylC98Jcea8iOn#x;s!7$DH%RRW^mpbTUezRgs;9_ z;$#B-MOH}p9xRcNp7w)t9^&07ZYL5H0`7OWReTTnOCbU0b&_^jAuHee3isw za#hllA);4>%6IE`lj>KNYP{q2+vo!X9gpPB2wFqxM#Xe( zEkrZizFuSC%VWuTQ5W3JTh2zq#R_F|#ETHIoNyOr#RLwL(X|jrW%TgRnd5y{D5iBV z5PGQoF=QfW@{=QnY%%^b6WdI0D^Zg9O^xO%%`Q^XH4QFkz!BRau1XqNBPu3V2#((e zTXEDAF-$C`5X4cXqo?U_e0~mR|B?oWl~QPFt!vu)uCd5+4wss+FF57d5V& z9~)6*?p~NQSkj+a2VG&``dS|M6L<~DsojyB3XK)N0?--d_I-%UiA!#DB=I8plMIFx zwRAfTqlMGS`=?@3b)smiBVak004AvZJ@jCRyrlewf^edn+t*25Cf)1P7+z;INCws9 z-7bCs#^R3oM*~^#q{yOa+pXFPiIT+bwQMy%c}0M#H;(*;`Z#|-yV>0&MQ98IU)3ON zr)+r+U`NXs=Ox!Es{3rC>W9{$Gp{R4_IT;2l9?xm_T*NZrW;S=H2A0cRIYjf06v6Q zd_$3|7egh1*@s)b5mJP3Nb-s^8MVvR->+UB;0eC;TguX%AMT8$!qS=G(?u|&t7-nxhcZdGLFTV>*`^;68l+j0_M!CZ*7>EGt6l$td zv#WY5CWO{S6Tz3uODvyk?^-8ix7~G6ZxH3`zTc7B9JNWgP>=ej7v!jM{H+JM6cZGj zEM3WC1d0Ed1^}F2IPG(Ra+=d>u|Z$rQD=+MsLI=Yc#)zKeyBe)Sd`Z|j<8~TPJs)I z?aO;$D|P*794Y`lVc_oWJ|#s|9WK?(#P(x{Unz+XX#z&2S}lOjBzR=6Kt=VytkQV1{u zN`0q$C=6zzrOUuCB6R%=GiYfez8)RoQ$m(z%39_n`Gp~XTFNQM@p4IwHC%;16@#G^ zTex|qR#rTrsns6T@9QlzOlBXXh_PFf;8Ki8S(=GaU?+~o*5>5Z8W}Kf4T+7#m&Wo~ zl}CW(Zrw`k3^InHhk8UBd-yz04-Y1mSOC3TSch)b`7SC81oQ>#t#g^VOfDgf`(JW_ zt3yzkr{U_}n+q8|901sD3a$ctV8+gR-yeCbWgPwVYn!SZDw9fT%TsvGN%Mq`sv~^7 zUh``zN%?CJ2Ih-?LxJv~AV`F#*hQ?DDP(0_wE>iQWVQG?IVAws+7ASL%+=8V1c|WD zt5`GA{$+bN~vaIwY3o+p+;+p^T zVfCFgkm!op45YrdB(SPoG_EU!#cd%Nvj3=OA`A^>cSTwbAMc!{j+~U{FLS+xU^)m( z$ns!EKB!AzjDy|HnlBcZEPfxAjy-Gol#`0kq}#!9<|f0(hiHP=fl1{tbpAo6NN{%$ z5P`U$%yk&n&9tZ2MoTwPhJE2zgYJ!%Pqx`w$e!5TjGE3Qn5MVzhSLKG;zO_)4Id%B zx*k>&(^+v;b*4j^af}0)M8&0*AItBwRzW)3nM0l_5$jPqk)ix9q*+8lE z4SQ+!V}~uNpUFARKlt3P|9Uzb0Ak)P_vygfI~@L%f4&JVJF;(auGQIXim;kHk*vsWdj#s%B-t_G48&71dUq?F#BWJ>!w%f|S0?*-i`k)Ko`n^;^frKqpTt=tpg%I0*u2?YOzK5*CF~wxr3Hou|Nt39(s6 zYt)-BUnkbPu1hqq{g!hjC${6ObG)f;)X;CvswV8B4*Z(=)g#-m-%S-uk12Q%QnNdt=}-O z{a^juKL&L+^X;wt*Rg}Z$_@7UM6#ceWtPa1kam~Gv`8-xH<`zgK;;c3_z$MVm7&q~ z;%u1#dx8gq!^RjGAzj-c^OE?Bq^X3c_h|!XpH4w}u@8K-@iIS#M-wGtleCC%mFttT zpn%g!Bpz}_&6_d(1{@oV$c(jfmsPwU;2G)WZQ(tOs_bU`y8uG?)XS+rYf={eE${=Y z5<*Znk@*`1))R&FfAasq)`CX&q|_2WGM;O%atP;|M%U8FBiHRZg6~DgmQd=uL*2gk z!--Komf4MXVHklXtP1i4!@b3O!n|S^;Z}!{gp|(rxBTSnktR@u8|(0>J%94^ih$LV z{XJlI7w_3|D+ChXAfIqYA_=wXU592>Q%s`sGa%_b1Nw_F`&a3BOIgNMFL8Yi?3|~Z zYCHkG4=1K!(PruVma4;f9~h%D$185JNk;|t%}#e~Gq%71*IAVjDZE|^2(9n7O0Hmb~K6-&iXthcM&YDD8qd7xNE_N zovImB^Rz%8C^qWFom9cEc+5K{_O(`>lgu+=8V{RJ0rd;1!oe}Moa$rN?l!){@-XW{ z=4Xg6MaALPh3dnH48t${>@C^%q`W*?7W(qIObIC4m(v<4{9yAs-%nZck%NfzA2J5C z2#N~Ko7HMfc!HdL`JH*7f9SIf{|XboDl~Zm=5i!%c;YuI?aAd`D+^bXg4LnKyA?In zpkhI2D5w`KkUP&}6$CT&oHCL-D(HPm8pl7d%O!}nR1adn?%DBo(5#gt2=Knf0A3os z#Nkphy7ZT>0qsIm_>3E-Udn3iD)i+xXtyZJb#OGWM@FFUEuC@? z6b@{E8cq(&__mfE_y-4BYt4qrutbXxeS$y0o%L zE3_4-oX}@%IBCsJ0tirMCkYP%oq5vKKhl|3?vd0cE#)KQjt>w~WkGlNjAyp`Vv zgtibT`hiBpL~MeaaRAs!9f`(T`ZQHq<6m>6EuS+WP1`WE+j|p=^g*M>+w}nJO<>w8 zondoI%yg*#{3y#j{zSC7V_(McB?069qVs0`p9;;Aoo%rMO04P_Bx7C4Zt=ShEer7e;S9_d&J^q%2~LB;3xz)72S>`NHj3y!2bur&%-If#^Q|R z%6h8Q7^5oG%C_Op z>#3_ln2}{EmFKg0t*Me)Yk&MI{=O0F^ZnD(LopVqrzSM~5|cKFnS+_q@1FBq)IcI| zI|xt9KYLMh8=KGzd==r$bl*YW{*)1!vi7Z@U_#XJ42yA1wNGqJH1mX}dNOByTCfeiVKhZU~P#C{58&}mV zf4B2f`?=_V)=IE%uBVIL8&udc0IPE@) zzQaF|st1LpQ*9Of1Kx(#a6X=L`Vw9eg;fU&hud@ruBd%Rs zsY^ILAB92@P98I6p{1U_<4(9fe{W>?LeVg5ZF3z(lDp2viE-p)3*NORo$fvo<= z7q|%P_wf6nz!Ti#P8nUVN(pUEv8i-5DQHPfRnvGsXKK3qK6s>XWXw6n*Bs(}bWu|v z`R7Yt8Kn0WYKho+q#7taBP5FDD{LFn*MX|9;rNFET;qW$))j%S`H~si4jHPY{@;aW|2B+4>dBWJE}iiIZQ?(S{-qnY6CwMMl;hWG zs1^O6aO?k)miwKc`E4v3<*BCq*QFY0W}@~h-;KW@_3SV)4n|IWfX_ocHw}WZ23Yr1 zL|~anW$-)SUmx{Juxvc>Xts2BOiP#iYeu!=T0GGP9%{0`e2hGbB?4}QpKKHQFXw5Q zDJd@SDorLg8(+8SSAFCDMHNAMEf!yeu2CUiCS(hJ#Oy=Z@Re!~K096YGEem?Ww)g? z#kKgk-S?;B96LD73Xz|4s_)>JDj2yc3moJhNM8-rlc`D+Z{D#7PrSagZ{68}v(T-; z?^Ic2Sc5qQkjKSm`S*2upNEyNhe@&BZ%sx^yuNJtTxW?t>o1!RY`s=$xK`siN77NN z#sqS)#Sx8bsVn&^jmkcLNtdb5t)cc{8Ah^gO6OF~*PoS7%qR)bjObO6Ik}w!0>w2> z{6An~m=&@({Z*H!dJfp1~VAleq`{*#+NLfi5;;ez4_2@ zxBZuosk%RQ)P~Z+h5!CrbJBuKlX~VSSB(7ld$iivL8eKK?5fAcw)CN*vV%{*7c05+ zh<_cvJCNV96Cv?0i{2@K*m5b_#yIYg=-T{%J4Yc5C3pAbu7n0$$a5Kr6=Q9CmTfXT zxQZV`4yV3f!qVz8R&6*!I+JD#n7eYhvl7#HwexvP4tqjhB=QGD*+(t4ThVcTnq(&h_P?$dWADrN9!?xq+|6^xSnG~HdFAkMMz5@z%6jg z(arS1q2Q6YTgQlNSTlIVYlvhEmxBslB6V`6La1b7-FFR7TlGLTCA|?dDFDX6w=?9> z>+~9hFp6yOx#q!qw+}8_yZS8S%}`*CXeGM0O}={@BO4b6d4Y_&&wx2+4^)eN5x5-d z-=TbdO_vh%0S_}Xy@awW@XzeNRim`)FxnX6UmoZ$*oW}`G9ouJkU4=losk& ztEdFF1QlD?*a=!`f|cciaEg+4Fb?Gt^|g~k^p5#zj!XXTVGM8%NDR{QGXH;kvxo*> zsmfE;-xn6M{VVLFo;|~ZtbB0b4nAp=n~4e>EgzaKs>xYCPFt9=*5c+*30VxNn4)83 z)_xs(0#kBI64C;^kn&UP&)Ybd9tP8KXzd@qwycJy1y49K>=!zKiWXU!Ya*DuNb|-JaKt#C?lwtI7H1foUk*Zzi z0dxQtW_o&Ph>OCzGDqy|AuyLgRz-Z|d!(TUWnmmM_f|ko(TpujMGvEK(^S*8gio8^C%EA0OyM4au*0*d~Tac$NXhg$0e-lqBqMzoKNiVn`m z${r~ay|#g9o@i#nIFGi;1u`p*Ao^4G6LWr=n!O-m(xgiv>3m4=oR}LI6CCt;`G!fK z4j|FA@SW*{H3dy3Vk)-K-YX4zIT$gr2f zW6!V7jjURq*`vHLjW~-5uu-EtwGHb#9S*2i3c0)}uv7hz3g9lj)~n?&?S@zv!p7HH?}gbaQmVX#Q7#3iUV>vW~KxP>K5q-RqPu*TLyeeSLus$ zukuTYO!({$<(-t`V9Tk`NM;+%ESDQ6jtr*e78<7%W7w?U^qD~gA!Qr5n#*onqnnzf zr*`Y*}6@T1E z`e847xB#-!7k+*Ox!2c!r;iR@Cpps3&#uB6N}vX0mg6-=T8ef~&RvC5fX~6LX_q zOcvj8WwJ&bd&1YRr`bBbx9T1#v)gTdMBG@)40ttT_+tEJnX4dX*SakBqNb|zGq3WlpRDU>TCA}*4(hqa#9L0W$CYel+w+XD@Iueg zdJ6(8bNB-x7Xt!rJ$JPc8dg6ybMvfpUTGe|JSih&dVwEssa396a}bbokqnc~gD{m7rqUGVoaq-a4ve zO{@*j)NYO8u-s{T4ZQuX|Mk7k!op2KTwdkav1 zQu)t#?!t!k8>zK;ji4P&*#*Xn00;6C6wPZr23o&~U?&9D^JzYEy@|*eV&!(u!g<3+ z;Oq<1gY*aq_z*?64b1=*txAk@1Nzq(>+G!V7!{S}7AFksCEfd)Fj<_llSQp*%$LzH zm6B8Ru+Qc&fD&=pNLoX__CCQ*|HF+A87-)6zwlD{FfUu zV8vVBD3a3)qeiTeB|%AE#0$jdO|UFyp-iKdsm|10Rlwwzs}+)>;!?Sh^o?Jn6Yry7 zWBK1Cx>a~yvzgE+UyiH~M7P$%9Plp|vpJ{`7ONE;u078p?5{km7so5gXc#$Za725qXwpd5B^13e&><4gCET`NW2%sS&j63>f%(|tXU zJJdKZd`1*Dj0Y1DSyAdJ{!}`;AUx=ofEx{T(`K;d;K%`e4qG5x07Fq)Y?SKI;X~n> zX^DX_#6{rHHxKoEvG1(=uk1kN;Nh`qcmFtn`kg@fxui&y!TK#qHQyG7N4D{XQMBNw zkYT60(^O)#JPpPjNp3!lC=D@3Mo{;-&>AtQBB{rR>PmvM`@ZB)Cgp7FfJ4vBbY*`H zs|6*aqKzXJ$FdwHij1AXl3Y=XS`_C+9)wGf!{y6gmoh=KqC|VhrLvw9tPP7yx#U*) z1xk5iiDPnS$oWf7-sbenno!j^47u77m#@rN-j|-HOFJ}d>hl41%06OQvgv734tx3s zYIs+|4cALjVwpVdzd7Pl(^_M)+Mv?!yPYPIZOUNBfi*kF<(`VU8=(( zisg3wMbny9vB*|9B1LuElIBi8=_dW?uTqaK!J8#;Zs8+Fp0Kf{7c9U{jRiUT`~Ed8 zC7KaqM${iYr5EQB0rQ4DYRkPUJlKhj9We*i@ms(-wxj+C@?M=RtiID>ey9O1AY;m`Y6wv;-8UOs@MDlx7EHopE+xj8yt+hY6g_ z%QY0+<;2#3VSP6I&}i8N`f$cCyN7>iUo)B+EZ!`QUN^kz1_ML3A}nD}h;0D;Om^JU zked8mr$nRr&6TkoizK2SiDQaP*!1joj|4eIu!VMwkHO28RlsHz^nc~b_>cpaVf{nk zY?nzQI3r0ut!04K6K9-1m14^G=(>VVAoyqKFPK5RbLNaOF!@1Nvl2EE2tamxc4rRz zB9}?~UW@iEF|@d%#Pq1MG!7A1=U)O?H>0x9A#(+$fv1FUq~c9FRzL=!mT#!+Je9;w zd*kqH&Y4z0GX69}y-wQANBzRFm(?PqT0%78+$uye(1)^<_(HyfB99VNBpuWDgWRBE z2V74m7)UUJx9?|MI@oy1K#k9f5>#$&JRVkw)9}cOl^FJRe8_f; ziF=e_o5(O^bHALEBH55 zl)&#fohID|j}NE&=4MI0jW&Dl+YX<>?W@(t@sb)@chYIe<7^At~?JW{*?t!%^!W6^s$LXYOPgg z;#a$H9qWG#rp;jA>saRi(SPNP`}caic_S=no5MsOR%{fc6Iim1ac{d4_V#sH_PM)= z(jMN1Ts}VW2oXqB7M-7`9;!P@8rcX1(M+ulhV6qVub^oOEBbjkF2VDDO9P>}X|<>T zW=eami{@>W=ao!MjHbM=O~&<3DFDRYo&k*PlVqCZV?_AlCLeA6GbwgKQXQbk!tz#M zuWGRhl(8CN*Ia|m@D|{mi+rm`n}Rj)$hX<~ZL?N%r(Emt3C@3eIwy=!%!J=!_hX-QH5?^{bA8i}js-aQp>2^eA`jtP`#Gw+GUr;g&q&wG z^LD*9q``npY9NJWDG=38RWos$?wVvb&1|_+_VfVele)n)7$)GQh_a}0;+OjVYd8Xd z`+8ngCA^)rOvtDLXYOq4_M}YGN@o@dcNAy*_NbB^uE>?bQflaoD$adC1#IZ{Vhu@W zGYSj4u@-}%9A$=Mhv1BmjQb2A+D}VLLI&`7#`!VTtKyGpG7(W@-qwc9a95JEXzO6Z z7d*BlY$9vxdR&u*ka{Ff(9OM2NP0!iyXviD(IOT(&>X9hvEj4Q3Yu%#87lm9ZHJ5q z>j#JL;(X1?mspJF`xaEFPHzI%B8;AP7=+);ix{~jY+0YCVS2;Io=v^w@4yY55$I__s=h$nrtCDai~P7Y{{xT@Yge_Z(7pHm+ zW>eT!;O$G=HEF{h1%KeUB8YB{uUYSs3D%-TPR>_8iP5sB_hE>SDlAzzCcVm$_FU!i zAQ&YxC&*xB{K&a#LFu3~q`{?)(aoas%HaBgoe68dvP%>pHbuzBE0j*&Li=y;3_O@A zWTLS8JnY{j{fFZcAJV^cWGr>jh&>2O+p6yJN zg{BtvxWh~+Bip{AK;Cq%z*7@kYW=Sa8I1hQlwb-LS+xPonX`8XHd_8QZBDAlD8kam zvcLtKEkjMQsY;i@J|jTYjJ&QaJO>AczRAjWUZHp?2}HJ|2QF1Q9)My7-d9v1cG{C} zLB@oy@+*d~S} zgHth)N<$jzzoSObrh2*uZMq}spqa(7KIt8-z5##$qZxaK$(3pbgKjNI&xZVrpg9L4 z?Edi9Jtb4ycaZW7m??>}eF!(CK>V%27M6lTGfHFRnNwNPZJ{%8%w4iZt*uqJoOwEz z3BRR6PQ$M>xw%S7#u`jmFsT^pAt|M~cNdy#och-H2d32OzmA3yqKLwCY?aV4n>fI(a@m$OeRH^;tWdF}}%`Le3_P|b< zs(m;wBlq1NVy|^cDPj$`+>V1QrXVYA#Zwn!*fc&=9D`{!th$2k7V=GYriiS8TtBx% zTznkTs-8i$Fjnb*4Su-f8_+;L@BWk2Nx}e~J` z0>sut2zOLe`z`DK2YhdSt!U6me(=jb_Rt@ZhJ&z=1TNlLhkE&*53e(o&1dh#c@nmZ zPmYV!V86)T+Hlgx;r)Y3-bc!jZXLGcvEa>*LOqo%uB-%G!XrF}~t z`8@=M{$mKkRj$W@W~_2ZbQ(}MfT07f)GI6p-z}Ob=9FTv|EHaezOII5^V4JtBV*je0qXcCQY+|=R;L38@Xr= z32a0he9_SwO~|qXzs=*6Pw4vKvc7Q4V3b{uDS{p7`XN>wmtnh851pNKN5z z%zl)jf~l?zaCeQRuJd5)G*#Q2y=mMq=K}w{`7cY!uy)m4GOBpXETpnvRDYTLFZ=)e zaLx~gWqk3DGv|%~`ev0$z7PNZe{G6j18nt2t^?VOuz$DopO*e{pWOkZMr?;;unzeD zyJdg>#(&+JqXh|^AL&nZg0#|d>3c)wYK~QdWneS-`&AtK2{Jayx?vlbu3f$_)-kv0 za+8UN80xm}mGM9Y8Z2sj8Knx!hk}nJtVODIkErfiZKT(8=xc*jblZ-w_21O>sHeu9 z)zgeTtrrtTe`ekR!W@Kud;S0=wd*YZG+Qgt3NyNU8{V?V|F1qg%5I&iqX>muKA=yQ2ZIF6i zgXz{O^i$OqE5fbu&l+#jQ?W<#E~8(@XT7DB!X)!NkPsLncTW3x#O8bKeWVJ%!vlJ( zh;Gg1mcB%sY78GplF6`rQ|4mWX;U7%DnsdW#&M=FCdph`&MrkD4~XdgvVNdIy$=gD zsFM1!H;n$ZkhbT!TXx=>&cx`SsJZ=&KDK122D!<8IRXo&aSqgAM-J&AiHH|q72+8) z^w)2)fR$TE5;rR!8;pq43pl>XsKe7WSj9+WF4gz`VWm!@<5d(+?V_Zd{D&s}zAg%; ztW_^tnqe0*8j#><(#;zJMLPwvFMR`GC?ZpfPVzq+Q= z2PErarD4y(|Kvc|F(l{Kk^Y-_6DxYJhBI3t_IT>qVJMI`hhD-q?&g_-JvW|5oUezt zZ3v%GaBCv2tW?)DFbAlcDp4xuX!Sh~L03+b(Sn~Oype&NBP}!eMggIw#BhreDgTK0 z*hAG>2pGMDs~^3^OT)tml;FWbY|+@1X5O6W$K-JU;kH7>PfYKPOCfcAmg(=v zCuKbuu!Zwm4UDCvBng_v#IS?+I`9g()9LXHnI8{kd@xosy8Kem(;=zNw0sBvk5p2G z9Z%a!-fDxS@x03fQ#8T4-hn4f0d*3x<8?X!GUCJui(g(6G;Hg)gI%!xnOl2_C19AV zCU+C{OuEX6?{#tMkgKTE0s*&Ie9iSMvM9wO>yR5i&E+oZi8x|bYYfss;9>sl(23|e zTDhSz~>hw&3}7@TA~ zZP~RVFr+O1K4wjF@M?*YR;vxf1iG&3 zqVav;S0;kNDBn{Pm0i~xf@9CD6<(GiGzf`b?n|v2TRsABI7`(*A;{7z#~un#*Ijoo zlacKgom;Mm;>^A+`Crx1YEJhQZ8$d=Z=l(BmK(?qF3y8rp1ug*dBdKldWYs0+Em>% zG08EVCkA){17-epb?$R6wm5OV^~Fapc~;hmI6UXSUmuH`lWc zp;Q3Nz*X4A2@-tjhJ5~`j28fl%+KR^<{ujc!AN1k88t>@*N<*Y5)=?cX`gM3v6Q3P zoopMW~Q74)iaq#1P` zDWC`kkHh1a<&qO}+U~~DQVR=S?x+sJP}Of}}9n-fz@fu&%K8V!sdfmQsuS8cDv&Q(pG@DK0Wc zZmj}UxaxZVPsHOifqb=fn|QOWeIYy^yqCakQUxPm}DNKLUwFX?7E9x#%rn zN%6HAPPf0~pG*g}l)#NTHx^`Q_T3Siw5uY0~tybDCi@5JJ(t_Fu? z2}^Zqk?%JBgq==ej;~hiEuGCMjlvD4^Zf|jqu+Wpt`OU~;(F|aOlQROnRvx+g=MIklm9mGFH{+s>dR~g;zyzsb3r|L8fsrh z=(2)qU!F>VR#Pi}%EB!m1z6=eO7v)a!0|`3Pf2-^9iqRv2MkIg`-@Xv5#rI4#0ID$ z-@WcpTcY;KAs=wZR3#@TN6LD;7plR2wcXgp3>epa$YPdY)#1X3?=e~u82yL@)wS0T znx46m^&5>#uXi6L4=qE55NM>$h+{qit-+A(uR!F^#0<=4X(ZZU0DZp!&cdt%q7wug zB;Df?f|o=V5~Y8G6sV)skpN!jK>-~sc=t3gLd^+#)*s|>f6!oVCIXR&Eoa>2N1|i6 zzX1@ZcN~BWTTzD4b5T^04Wp3=r6cAOPD_e|Q6REg;ametq~l1B9)-^!-Qja#=W5?c z)lJ42GBV?kf@)VY9p$YL2p|EAn0KF%+UzU|8OcB=LS;S9fB>Msq zBCsd1DQn=VhB_btVGytevMzCZu}L|$7>BRug8?ViKP7D6d@Lx5n09HJPEo)Y<4PaV zJz%y~7tv-<{Ns!eXyP{wHWIg3Qv^Qfciw0SSm$13`_}?Rua;m!Rt|%>bV|^G;wtT4 zyHRd21XL|?Og2+(9t`*2xKp7ClumgkvL-W3*g~qk-Ys4JxWk$=M?gV_u?bNyj!q`nC_cUJHHwIt^g?o#*!DCQ)Lcd=hShr!AYWmbVCDFMn{~PUTz>K6 zd;Nk0?qs$jxR2NL2E0I>O3>2z3YtES9D3nK9jU)Dc8$WVubFj7QsoJQ=}RE1g2xMS zHokf{*HKc19{-NXT^PF4;9}_F%o7|P+2@?!k+Gta8No$Pb}7gK3)Gz&hd?&Ek@yl- zIN2d1adhj`en=ol4(Bx#T_g3x_jmFq^Rq#&br4!8y4r1hexpp&xAK+4aMLgw@%&{Z z9$iFLcKd|O;wn?fe`=Hn{3SxR)b{Q4A-VHbgcS5nVD%2ec9{Qwr9?6#8s`qIOP z_VUD!tkA|_XglAvP?bk~IHHsnGYsgX5yK!XmYw2MhSx)vrKsXHX#B=RufLnt^nxJR zCWmJ;_dqZl62SM9~D>&!Xxx0h`(L#BnIucT2PPkNW_BAiXm4Dgs?)h(VHYn&f2iMXDy znJQc+z8D+UjZ3uD7|cdw~`rb>AS;ErfM*sV#$ zFur%5fX;&SII4ktW#oBJ5RjpE@!z={K;kNDVOVLQP8VQ?8AxR_oVB|VBoLZ`Jw`lo z_TQ{~y^*!R1QfD|gde)`W1N(;J-DIjxj>i{A$9o$C%$wmixHQ~CTq}zR14bsK2&`> zWaS22$AK(l!HwjhlEz!u*h?h)Cz9SqU9SXV8M__s`)k#bjA$j}9|owUGi1T~p)3-T z4aV9^39c8#VdguZ?P8wQ!_5xAaivwLlJ4~wsuf#&PUlA-8P4RG7zWoPvIk4~oH3z+ zmx(-6HEziLZQus6EX`I@*VOT-@>vjxFa~9|5$W|L;$@m5727vJq$P2)4vQRu5z6g| z%QK@W>-iLv1K;-ML#21|$+jB5=N<8Hr{ZxLOC+)*8EGTdxwH96uc*BxQELdwSR0!d zE*9a1YP(;B>;v+9Ox0gtQ0}*op(NLXz^0iU4G+$r4&`AZrNv@%X>^E9E=&bBXPZI5 zVmHuZVfg%lN&3&Unv)ZIZG?g&>nzue%$m&V>GpPp%QN`TcR#0etZ*#U-iE+di08N$ z2j*%v3koiEw4PUzLF*&T$9NF;VfWd%Qg@ki4eY{KT>HhD>vze6>h!fu)DuUS6r7}f8vDr%?6WRn8zROfQeUAN4N4Ue4 znP8k2t+~!b5mXel17e0w*^~rdu2}=$u432KGK7M=#5VFR59O&eHD&jfko{&Fu418r z#P-6wLd)spXxz?eh#|NW|JaGZCAn>5lg_O};cYLsn!txX#?wn~-|{PH;h5T2+U9K@ zr!xQ&p!ev_@*Ml&R}-WdxysbH+TMg4=20E{u5Da~QBUA*|3pKC`r7tJK6IJexk)+Z z)!WIs5o#)^Wu)FJ22$#QFkueIwQf9LLmk!Q$ByigYCajnV&t? zfTxq(h(^@%BV?-Ox0W#YzTxv!;gwPY$uH>jpnN5{oZqLyMVcO6t?IC%e9fpPqvajw zv1Tguqnn(ZfSH{yKW0m zLDd9x8__+eIK_H9Y9)~PT1ql2pi2f#+yD4B@&_%LZh}gJsVXTX_N(dZ4c|%jBoVqM zhFk7Rq6SFT%Cs3B%ANPUixQJ_HAoA~jLrtKTM%bEB@lxx*QqbQv#Q?S5hG2;f-*nA zEy)-d`=s+Tb6p97GDY>(@YLRFWIPGJ;H^*P&|+|Hi8I!r zfU0UE2T&o$j7%go(ToRB9SYYG+rxG;lD5z`BF1M8rS(y8$H^IEhsH*sk~HuZ?tMsT zZ^{Xt5Zd1xkFHk*r@MMN`AH22$qP1rxA_+8(k-M2J3^=*28xwYB$+e+>uz;OJx%5e zaYZ*oYA*2|yjc@6r$~X>r90KA=$Xta8I_X=yqH_Xj}kM-LGrjy)IX2Z4nB%Cn3_5k zbdSIrnsCsLnq8zUTOP=;iX=}<{#Y8P_YIxj;VQxp|9rB4DMvlz7EPERVudtZ(DX1s zgu)f#UT89-dP&Zq)`JbSGZA&4*cAE=DrweJQswlsC(B0BK=?F_R;0NcZP!7iU>gn1_8$|mF(!Lpi2P#9>pe`;h2d~YpBBCXQ#(EboN#Yzt88u-`t{ zh!$bhMLijw|4M{mk$af!?|>kk{UD*91ECoY;t_L_y0&xsrLYG=4~@+)n&mHU^)m>! z|72o9o$Hr&QJFytIv@T^8^>8IM+{4l;QMI(p@_z*y@US8yC5j1>aXW#++X5l!EVCKZ;UM}B!pB?O4=HO$EWgg4TwOI* z>h0Oe2~Y5eaBMjz?|ASf7XAtry~llCb3l|*q$=zVqaE&jcO8D`kzNrsD9gG*e+%cE zJm4?&h*<~r5`Er2P2J#ptd#F{BDH))Z_d063?@Y?JPc9Etv3)Wqw=Asb3xC2j4nmv ziFZm8pDJMMhov7A*&h$fs64h3E8%>M}1^Cd+2FRpN zFr@sUVBunYiz!D9ERQH>kgYRLY7m#XV`UKUYVIbaY5@gmL8vEHHejRP^Ku<;e~5xjt@ zj{rRvuoMcNTR{h9sja*QWx{q4cXnWUpOAfK3XI(mclEhnlv3y@J%uVovPyzchu0K~ zJ%RIJAxS|<9|nWDgT|Ax-_WYxS`&>VM+h?&BbPl_VC>~|FexIakUeMzi;yauOk zZ~jG{+59zSW*lcTsj$N7d_)C3`4$v(tUEczfIjT4GR>?KWnvME=UxT3z*fZ1D6CDr{^-_{ayYTvA@91BJL<h5)#hhz)p*E?d&U@%~{}Xq22Zo4w*6L@?9`#w)E6Bl}J1f|2q|ak96| zMd5c+WqYV6i-sn!UmXbr8nnp+kF)G-DF!{%yKmE?aYq}ku70erdXNA6=pkzuiSX$J zJ}%iA|NWXV3ZEyc*Dltzbrfn0GC9(ptW!|fYAE*kL;;uQmoEW_w{PKxPJ}NNlQFz_ z@dfa!{Viq`!Pk&-81Nj^osY|Xj03ps!Ndj`zrha%bSSatou%xH>f8E-khDi^;yAiy zx<1nr;@*T?DR3N3RFWM7@|8LpDd22qs#b4bp-yjGvO(*#4u%{nPg+IkFywkOn1fTP z#D0Nlmh%Lztie3f)38Vp+P5<;HrR(Ev)YodwQd*R=5Gw;YCq;1b4qTWwek^5k~D2` zS0PyxO7~>ij~%L-=|7KI&Zz;4NT1DZNB=U$<9N+cY*Fl!VVYj=K{f~JuI1%8``+;t z9jEbv<((v!DVljNfA$z=MZCDj342%NC(p0m0aIk1cHR*!Up!L>8g)flt0s|cR4#F+ z&~*KGojRmNXZyKCRzgjrNIm2V=7=iMU>DAzJ3Gx(bPpS|xzj%2c zrR^;-+PM4a0wppIDzKv^K_>p(3CKzb+UzQ@;;Xup*a;cROZmfq66$zEgovNBQO7hW zQ+|osY1XMf4#&P3hgP-ChbDc(^Fo;*49~XBg|$9Vvq4_O_Lo{`jtI}tUtY!k$(})9KL;`xB-uP zGaI!H4KuMFzYD8!xKGlLq|+I#HTR3qD@l<^zIRfXpm25S|4t@1Ijc*N2vaWz$Tcn8*BSQ9c+JiY2 z2VJnshpPnlAolQ(9_EbclKuEil3(>jkU*UD;zD|-={^12i#|d@*pb{lj&Vhxi8u1O zjTs}CQnZ$gNri)e(zVXksvfyNAOnxm-@usl9%j2qq6P(%2xx0{*i80}-NRPU`kD@D z^h%aA@P)GV*E1?c_#-V=doT^8y582;t5y2h$7(^tl1-6JiUWz;-?}we`g;z%I3C1{ zdf?Uboj1i9JZ7`J_HZ7M2pyE^QkMb!kg7jDD-_it$)>4pr^KVq3-lh@{*Rj`1MoNu znNJWM*T1*Ow9}HECEz7r)>RcJ<<~m$q4VKQLZ-Q^f3vdv9=VeVyI^yyY^($Ko-gxl zCC1Q?mw{}%0&ZnC2w^#i{$wrnSn0Pxk|PqhOKAUH_k-TW8fuF+kR|NM!h2d^`hA8i z0aA;>ink2>`}jn#!MJT(`{3NUg_dl>czqD;=aSa+4-Ix^lR650)^fq%8f;Ziil{;q zEnIQOeAnj0xV~nzb2N+{;fctS^=d&N_6ggB(5#6jonoF9MQzWBKl9{N3>52~Yjxs4 z1sRth2Ha>rE;8_bK>kde8PUWx7xMIXN@m4edppw?c3ftYnR5*cI zE5@vf0<*$61=AU(A?I@v>G=fs> zKVXuDN9#EB<^x>JQd$s6RD-Oq328Cb@_%w8q=kuaDL@ut<}M4SDrXahZEA3=r=+} z^%o4<5qvb@#Syd9E7m3P^w`7?(@~ff3L;*)rQ2)6#VT7#Q$U;NMclKPrOSuBWftKMpC+;2f zO2=;LkUt=SZ5I9EQKn~-i1e0xh8%G9jkUr($$MO+f4`oJfJ%Jv75wHHljkbD7<7vjf-JRzYAP=n%v%t zq9w<{*daY&Q9gBYi)K-8iqPFuz(Gq?pO$HQg$V*hP{K*MvT6O$1Z7ds(bzD}YHo+!hh^@T*T zA`J0rMxB3Uc>uovFxRRwB-vIQZc66noUns{RWa)`Qgo?V!rOAKf$7dDL&G!7dW) zA?Q;)Qy}4sS0iJwR*TX&jqDC4J7aG_MK^hW3Df0QT^Fi0+tbx<{1036mzYH+j^7El zhIWV@P^|D(K;;XOXd1oTrq5gZ)Ju26;ZzSgaXqqXrP0=7H^JrBFUOT0!OXl*y})jk z4(6sC!PZP#m zo}cHOyfQR^y|1Miz1c%2d%k7Um7$G@EYDN0hEn!Aj3lIp)+52}D{0A6NCd|&qNd3F zKXU9pv_pT1m5S&AZ!eZ3HTwslwyfQbAdY$;-wdD53Lmd6BpJM4q2LlLRkA;_3_NRu zjTIuwtsj1`fOHPLI09*{c~FjQbY_3e;f|sOPnInoBf6Po~fgT zM=BkLzIQv7h>M5BU@2_8qM+7R^n)2;ZkJMFg7&T=5Wz!0JH*f3UueA#PX>@ZbcF>n zzF^DbjPI$M9JXhd@T@*l5Dn$40+t6;pzh<^f5~6eAOM{{8~S)~TPpwa3^Zhj%=74J z?1y9}2cA}!^C~By2)gowe%nd*s;4#-eNDDH0kRpRBJ+j2{jQQaTx()q*%043nScPF z@g`%051C`>E$NAJMCbBXkm-E|O;wjuY9jk@BY(O| z)4$o8zXSPi4O^BS3S#2p3s#@LmqtaiArt#e9OPnUicN*GoD^%k;_u z+8|3~w^D8nk0837h2y=7Wd~)%zVEYn?Ql0%r3)H$=2jZWoj-sZ#XhS`<6vZ^!GG6~ zzb}uC1gPx{zxvkCjP@Sz;ZULr>=>e(}a+=tgO=oC>Wy=ZG6 zzUVp7#290*@^D+xO7oxwhJeu|K(-JNMPbQ}DxVmUK>?VdGJu9#T&g0DyUXR2G{U*k zn`(Zjmy}<*z`h^Jmwtm)`wt!A)AwW|h(ajqyG|%ku5rp8tNpzcU07B)U0* zh7|RxaUO>Du1kO;#l|XoH^0GB*!QTHN_Toz4+*c3Ti6Ib7#&u<1%vY^Q$PH|BnVNr zN+aFXPrAHu`aE;H(`<^Xec2B+2#i63_xU$*`tRm5a}WJSm_X zo!~O5{vEe}2iCt|S&aoqVAsQ@1X>FHD8r%W1FVs(u!SThopfMe@~-FT#HNHO00}XU zFzVsNP!-*L^N|VX^=eT@I+ZZ63M(n5LI=(8dzP_FsR;)hI>X|ec5j;euwYYgWhecG ztpRaN0$81essrS~jyV5aM!=naM%MST?(?P@X8URi113d0DIKY=m-lUeL!IlH38|>_ zSZG|j>j2&p(0iWIYIZ&#z-4zBA{RHRF-blDN5{NwT=?T5s;e~YY-rmsjeo3$MuG#u z*!_AiR&22v&vfqo=bu_=)MCm-lC}O50sV)wrx>9iWyhE*Pw*0G2NupT-(#soF%S%6 zZ!;S6IFkx6)M{%*)Na&cD-^re%`qSD$gyw(o2v2+%3@3=>6yNTPHomRnfG~DBB*cj z1*})q^O#QsdbsW-*aw$2KAeSC3%1tOLKDTG?BFfc#|x+|sC5(Z)C&gFif7$DtOU_X zIC{-MIbNoPC2D)ZXq7(BBLS;w^@4)ZI1$vtwnV)X*fRmt( zBK=~8x?TZS@xSJjbb`~fQ~8eu`XALb>k1h$Kn>l{!p0K?MLeXv6YF{C?2@=yM@nfW zGl!W;g%kb59}^7B-EQFl%y4Vr&KnL`6wPqAbT4WN;hv)#&w6W=PK4LG zp2Jd1syVE6+b{?6S@O=b99w6!=u?t1# zG}>v|?!4G=@$MF(4}^N>mO4Zt#9GbFYU$hCnf%z#Qh8+}Rg7vZr{j7h<_;WjI4>tG zPEI)x&rIkCmq_ggxl3SiYXpb>or_Z}Ze z46zEacl6U1AlmI+qwKIWv-i7TMCg-G>E4Y>K;!J1aGa$d#D|391!S`*A9@~Wx0WLd zn>*Ph)HWFo!Z#{&>h}EuqvpN=eBOtv)QTeE7(A;%_h*!!ow|yrk^hB)+F+b>@$_iU zFeiCv@RCK>Xn#3Gy4sf4S+J!z#9ELhvq&)%KoOs$Kb6$zlxvZ9i#gmKkls!n@!2Fm zRSFi2NfEPJfY+I8=d&8;=N)&d_be?*K6Z4z536<#Y;TtsV%7>ZJo9o<{C}433h_mJ zu$`pGX==8(`kB6iaeezoCufGF*4~3oFv*aV7@$_kVp8h31scF=F3rS>jU-zba(P{4 zK=K)3HpfL)8n;c2GAVz$wcBm;#s7AMHFhBV*_4A!q9pb+Ncy~J^kT*qKkZJNR^*PD zKd4bJA2cAja*K2TJt^2Z@C1Ys#W3&(PyLL+%&z@lS%Pz5T0iJ<#utzD6Fe@}p?|FT zuRP#4B(!~0wM)UIxivXxWRJ|&T!Z+NEc^+epiKA8I?e+IWu@pyqi_wil{Kgnp8-by zsX9+PUvilI-yYlq0^I&<2J`Ir>!UHex-A6flj7%M;*7iy%{254_FdCoN zutkRha3{6p$lt-=K_X@LaeS9&q$$_EZVgK3(7vu3 z6LaAmY93qC5kgo_KnVxr6L$KtM_l4W4R_FEGA@Wj6H|bN0V6yry}xEOi*Iy*cdttH zCAbrsj_|wk09&P>V$(JZY+SKS^cO1;^R6YZgQ%1F6&Up!La4JeHJe>|lo(Tt zXA5cw`d#&Gg@TA`cRtK(s|7QRJ28Z5fwpec+b`N$L7U0fGpgS=yc5U zP2(t<^R4scA?c%t#%iP4fmqy1q>3y_COlCCby^;M)?7S66109rkwUmCK|jIbZ!JLX zZ})0G)YvWy{LzUSv`@};4IXGq0KunW4W6rG-x=yMy;a-ZbNvjtiuGW@Dn`#H9T6M4 z$y+<%Jlbr;U>N#$r`&_Wj7hFtq}2BfXIm&zAxm{%;;B0F9Fkxf1yaiuIAq$BK3>`>Uj<35xpmhNb|nHEg5@ z?NdHe`5CZ`6oiVU;iX!O^VKBfU_1ZUfz-hM>Of2IIB7wT>Gg=i(-u{v!>&pUB5I5V zuju-*s@&{BR{Wl^AXd-HkUVZ*_7UiXmtrW>FOD^kRTt1V06SZmD3P0o(&EEJX$Tk* zsm$OYD?mvV8+AkB2nz^+Tqt(Ts~S|1g7X9Jij4``@Efeg<0f?aj$-7hU0jP2+NR%k zLRnOk%@-Z@-4z+^u4TdKA25p9>b1}M8*>(1c75<4Sb3d|b@>q6h3`#)X@R6Y9fsj~ zzxyy5i!fnG9=9IWlPP)*0Off^8xOzXG+YWNKAW4Wc{QPj%RrFD-!zKtDkydmp~os_ zi5C$}&Ag~G1r~v+4T5s4v5AaXATo5${Jstxmqp(pUlk@kf(4^VBAxZxmpbqTApKlK zsy9khLCm^~EP=t7I!rFXh*}?X68p%=2?2}bfL9O*48s{CHfWm=%1&_lv(j7QIVcGF zIy)rOk!`gd?BSrN9j)m2hgZ~o_JRSLlNPh9)3Xen7Ot6 z_EmT$tbTGMPUoc(3`gg0Jm0qlX&2A#>3jI@dd=kV`eO4GEhaFweuu|ZdRWN{HBrK` z@m3G`3zyw%F25x|0d(({*F(3t#Aib<)Pf)n&U zZ{#n2B_|s8XF&ZnJo_HkLqzn~iCOaK9-%s$4hF$RF*VGx(&85T6i|Zkd7BloIVMXZ zrZ&?ZdLmKF;HX!`+KQ&dI<3#ex1VZn2bEDmiD1jm1WE3d3YNcd%RS%%#RV;1KdFVk zAVNDzm4%yJjS31*l$fNN<~;ay41lVuyaK#3)#50<`kLHsOU{XDLXPy`94V5?UmJA7A}|C zAI!5k-C@SlR1@MSS%9Fc1+HEyb`yD8QlLmOAX5bpB;qVcTq?C;rl=ceHN)=itKg3=#0wJlotea*Sxt}b6f zUZz{P8ZO9d;5Liqgs;-)W!afWK+`Vt$9S2=z@@KVx#X}#k-*d}a8eIg8T{eKigYv% z*LZw;QRiUHDgypOeSh#`V)NYMpJ_GRar^{{-Mrbh!#;LSvDn#Y1Ik-B66&JJQMNz6 z6XVEv-Fm*o9z$+`kGL%S=jNCh(z`g?*0u4CJap?l_0`f&;(Al!O3z>_IS~3_>VDJj zah;p=*0ft3x%GTmwO%-S4?eSK2eao312Hi=**80whTFEE2kCcFJf9EA!=6u98bxT~ zjbZ-wwvE0J^JOrioQQE^;t$j`?E(F%PPK&j`C%QM6yL#Tu8O0%0kgzTOturu)vP%&8$HtPpHZa;(aX&v`qykyAtx)TG*IfaRfI!JV z1svm}hV~lz>O!#h`=h%Ul5Dz)!Aax(L}n7i@;v37fU?!z&EJi;4B)#NfYw11crmuj zGP2Xx!l(9*ICBT|=2Fd8m$dqcPn*^12ewsJb?<)L56$_FpbE7<`%!^U6WagGw;K(owrWb zcsO_6IE%|Ns{l+UmGn$`lIYT2p>&61YubNG{)LeRrG)vXOBZ<%BlV zO2d4Pm&fL*x}m+sjE)@cY>-BefK=2FGVX!VG~ zt8q#4aWXdvE`uUdC>2m^cHw<@eIujg86Ta3y;^!{1MXjqR#WrSh(OW(T;rRycr?(% zSvvNvHO+o?UYhO_XxXhO}NU!;CG)q}fI7A%@h4#XrsIhhZ?^+^+!q?%EE6*<$&} zLjhgUb#X=}5${`hmlsk(mYW>>A9w?@YBX4+*5D|VM7A$PFqZ#bffLbO5rp=zqo0ZD z({p;X@6(nSB*lpcvH7vnjGni-yd-2#gEwfHTV;C%pH4N^Q}GbIgm;CUepK5tAPeoZ zFLAnBhY7=hD-;!LN+Edi%ZAu;0rU1)4*>^<&VHhg&~pU%HAeXf1#ebiVIQymxjC@u z9wWTEz|ygjZL#{LRPD6Gfb+}l=Xn#s@G{Mn)-}u$;tY%%kBXt^X(uIWO#-HZF#7~1 z)4p4u4`w(?3rhOG7?Ui0>bJH17oamQPTe8`_ik%@Oc(O~m^x zyxLS9iJ3Cd3L`po-Jc!peNFCP7`TY<-x$We=UXXYYe4I65xC-WB^$u=X;HtjBCM5l z)sXSZuiO+FEf|!u_)c3S>}jb2Urdlvf;J=_KR#GrJ z9AdRy@43b7PyBm}9GesfqKzAIcWR>rL;qpj+YWTrIgVcwq z9$RqWeby%lM)l1gFLdH1iE#42Y!r5 zY9TpiY!Nho@!@g#kp%nnJyT8c`-EvV%XNe^cyk8mqCw+wHz5gVo1@2<-3JvB{Ur4} zybwzMFofrRe20i{S;@8kEg^J>(ir%oNjjCfYX&}ow@mmAkwy0lSeqvj#m`;}@G;3%c zm(dmcZmj(l6LZp_lG+>iFVlXOQt{1EION+?>q?mXo6;Pu_{VciLmTa*ll^f3n}P`E z-5}k1Xa1ET9r^^sXXR}w{Y(G;AP5_q+}9j7$H^_mMa|2A98SdE-n41riMNWSqylEB zx41xNU;)Jv3bCQVLlFJWisa?>tB>VlHB`2?hpDE0ijL|0;le)z~=GMD*%+l zUzo+UvlG61B8{o#A>Q@+$;ECvC7S6)gd)Tezl&y8ci?yJ^A=Szt{fj9y6&$73aNzg z?>xoegPm{qniX{emrPz;XY$jeMW6EI!N1_fE9iFg2mIC~tFq%7tL}Q|gh=cQ=IKg3 zY#N1o4w>>H5+}@WX~$YbleTlsUN%NR3!xr1kBU&Ze}GKeJ4W6>4;eM-n>S48aHC_j zna8u~)QUO`1%(5^x%Z&4bEz!HJO5p0wDABA#9gz``g%YJPO|+ z1T@31w*4-yl*W+T=3BplB2?s+@&(mB!iRI@=fvn>XfFXOqrOEPWIEE)=#Z@GYf38G zL|jhx4+QeW(l_B)r*{`r&R)*lL<{5$5AOM-b>A#g@atQcxddxA8F#FVD;C4OCks;t z`nPvyW`MHKPpOb0yKA`IFqGwCb|tOc_LXI4Y^p_`VDmDb|H~BYkjY_{1(i0rLmF)X z>~~?jS>YI(iFpMyy5`l~^kGnQ$N(Fcr9&;@Xpfbhm*kPanb;3D?H%E>Ad9-+95}{09kYy^gkZ@RR~7 z|HT^uo=Icme>hNgclAuPt+b9{T+{{89Om9<62gs;%5Yv{;WDOD{bkcjc%JrVA$M z>gOMv(2{`H)7fJo``PwbX@L`dm#fTURO9${@J1l@JA#z9g*klgN0DqQ;f_)Gn8Q4u znzEpl`kaLm+Yg{?w}$KzV36n;!@lQFLa(dnb--MXPRob%_YX~S2KwWC1aoj?GDH(- zYIKq#boz9H#&_rf7S?t8#Vi^OyJ`8x_%+u~W?(WsDHelS5JxQCU@*L$vL?$l6B;)< z;8yC6{d6+Dqmv~0T2D0up3+d+Nr8QM_TZMPHb; zHHo;jF43_l?hR7?w@{q$jO6|*9TRQQVGMiN9eU_yFHX> zDrW1x%`Uzaf9nP#Bkk^P^(qBbDojbmm9E@ggv;p4gK7x*h{Y#&wWzX8fTpA4tj2LC zXnvE;9&82QW%&VajO$W}ndBoZ2 z=#wyPjFlEo(4V_}GTSD8W&5}}IwzH@bEAqgI?DhkIharVBezu4!sC&v=+fNiwHb`q z=i(eQwp3$#&|OD#*N+opt#6%JC=d+21V36b=X&nIgROPC6D)=wi5S1=LPJ-Zjqn6o z>E~7(hC=9Qzj7_KIU)HcedyDpr|B%l=vvq#)}Cv6dMx_}-=n`b{{YB|yv=R&Rcabu zWte;s1-}=04Sta?o1Rh8`!^2h`swQgRlQeU>u?4Dsvs{nPAWUPyt1GnB;<^Y!ezox z1sk+@jW&4vX5*GW=g>ZOfr9WxJTQ23@eAh_>Hm@SR$*~2P1kVH;O;KL-JRg>?hxE1 z1a~Jm!QFzpySqDsyTjlX^qcJc?DydR59egAzPq}rtJYdwrMTDB)~;*cj%jp6334xp zm%(M>zV^6>!4(pFHx`Kw>xY7(Wa~X+P|8!H2p+hsLpua_@7t&>k|A6+lWcs8{rg1M z__@`=N|ccl`rO8Z{*2*%g}5pbl#;Acf%eZ9*gV`J7WR2|yd0+(eVRO0Fw8cKILPp_ zspV;7OX-K|pQ2ez+lXDI9=x3ZcJyS;ESr{;KDY7F{`+XIA<*B&zc z8!N7&{LRn=sh?3{M7Pic6({jOO=h&{z_9NSaikRGot*^XkGD?;YE3F#N?P=vT)hS} zsyNY;O|+%@VvUo=^aj*l^_;Xr2Vp>x|3HV69LFksJ|JPMNeFB3K@ln)XnI~1*I?y6 z{zzYD$j+YVKRIwY)fE5=RU}ZJ=qkw@Sn<>Vga5TSE(QbZ=$?WD7`UimRbTB}>Ldu$~ZG*i^*| z{QI&%bV_26_G;X9UfBKRjy|8=L|2aY3)UGY*X2cF9wPal&P}1Pw2Vne>m;H3ODXws z^_-!;BSU5Zod+G~6K90msw;MTU+mzpuJR}E6I^)-g2U*%Zn(NK#*5w#tQOq0nW^M~DB`mSw%& zlEogp`o^lGeuUSv$-A+ps(P%^XK7Z;9txBwNI(X=Z*e(*Ga=T9f?$}e?bY}$Q8>6y ze0&UvV>gR9Q&UNA?&dXrZz6H`mtUk zB9oYB53`5zs}=iSDkXQ+qy+^B|0VX8#7MALRJeY)LB}L>j?ZFl5TM7|$xrS#v0H=x zPlACl8>>xGS$DFVCtO`@Dg7Y=tKwJfnZO{*J=kDiS_jUiVJ4O?mHN z>R-IC#||ZTBs@GaDc8f9VQyYLm2I4xKA^^W&w zmwwwcqje3T2cnJ{&u#BF9k_dE6egr;@%SKV=pCYBb-<0s-{pP%ySxt^XXJlV zIDgm$e}4pWsxcNSZ-|G?(ZMC*&of=1rbS_Q!eMxcecEX~Kok_?F8iNN-d;M|9m{K_ z<8vJKf)%Yn(pq)T9E~0hnK%GvvDBw*Jkc=c5(ruEKcHoRmXn9pB9U_m(n8FHWi*d@ z%@sv!V7KLlb4Fo^pzj|<%(N@AyoO=jigtzaZm-)@Dj`GCZ(T2m4(f@&q0UgllQub* zbi=h${&QEOTlPSzjkui$K$Rb8DJ{Wcvd-1Tf5ukpN15|~nUe2bgA68aSd@NTC%0`< zkILR`V3DbX)NQiP(}>*O%`YVG#p7;!V2u~^p2}aBrJK_r+(6-a zl*ZsZV=uxb&*fiK*jfanbW5MH&h=0S)ENP)oFVK4xHA4mrXI-CXdz|N8#skHK8=t# zCE^C^%9dk-{65S$$!N(fPa6cy1q@hz2EZ|=ILnXz2Vilp&!g3qPZZ)h(SnhE3tp_C z&U=4zbOK_L0sv~6S@s2q`g%nN782WL3p--x#hYcRDw%|Sf%{@>r908_M}>EPQUKE` z>52Qj@vK3?bDPKVo+)i)oq15-=YIeI#PFWMZn3y{h=-IQPBV~=yko;SQi*i#MLVDu z)a_|O7QB}OeT^ryFIQRnf4afDcEqh{PRFdjI}%VNx4nfS9(3&MYK(9wmeVuaAkx9h zJCr-w6z73Fyyh|&De^_4s`r##NauH;ts@H7)7Y2|EpNcKW_~>I0(_%6_5VW=#}RaX z4_1*1nfgNDfUp(%L?&#T@J($5=dN&al$(Pd9H~KjEIRP}(aC-(E&oFc^zGNGKge4e zm5oo`D0^qGRwkIL!Vi5OJ@vYSnXIg0i&!SIvaRV1eMx@emd*|bA`XL%H@N>ErWYF!-|w9CFj<^_M@U*Eo6gDw3ee1S>lVhC}~dcy>A1lwg#yLoWCJ_l>!<|P$dbeuW&GX!>>+ZAb2U78joHqfMcG%QmOmVe zRYC5Mz6i=*R{JPY1Q~?!6wDjSh6$j+QBgOavN^0o{iz!}vr{_ADSfyGVoV z-1gf>+zkG&Sh54NF+8L1pL!Axl6qYWLbmF7OFe)GeMv}pKWL<6N9iI;!WyuHb7J2y z(JnzqCnYTQBuE~w_W$R#(~O-TT$pG&{RwJ#s8CRo1HI8sc%Kl5O$|Y91kduP;}XEd z13tfQz&m~Kgk#t+VG*Fm|Kr$S@{_2I-!Ai1T&D^ZX-bbJfIo{{CtL83RN$r}sOpPM zu8v=hgKxw$$xMSqa5spG@{1a;EjZcvI--G1+spj+kO9NQ&m>}yec%ZMf+3y^h+_r& ztmKUqBKlR zKTRGFO@L-v!)(EB`AF`VS-K3UDq7XL$HR=LL^^)F>Co##^FQGDgw8|sjcY%539PCI z*H*91BN_IKl05&I1T;g`?WLI5-tl;|%_3@kXBfujut6XO;TVvQ)=zr6dY#sr zQI%`#$Jf6P;-CU?bQf{3m0*JHkyk{mSTQqotZJ>CQR{A_2wU*G;*6d@)L%dCcAG?e zBVRHmKVXv|*C=PU4c2jclBqh;_TA8eQDeUpG%$@+AHTmf1~7v7k-T6a5Vry!p7uhL z0ky<(YEk7Hu`U&#ONdbe*7l3e_eyKW4~S>2Y7m{d{ho{ER~$J562?qF&A48&~OHlEoJy5zZH8+oCcO_=8~5Nmx-cXoBZNs?dbDx_kkQ6^%3T%c}W+ zD#FTdBA7HT6v7`XS=&1o!37k7_VALQ>lk&gu4pach`VGVlO>mp`>{DkiVpIuJ7cUC z&`6=r7Es`+tY(GYTDIJ(bdgvirZY@d*^UD`txHtQgzK-%F zk}2uK&D;p%hiD!apfVy9(mn?S< ziv~0QSo2dbAtS~;w`(=0X;|T*8-4D7)B_13_K(u&-Et=`6_ic%Od;|h3Y8cwJJr& zQLKllUX=5>*8|r_vlBKmjX1A@7?Zz*5cA?jviT>XXP&+pXT)gM!q9i@}X`!BkrJz`^*Bh zCjL7ZfeK^&uM^}KP20Ct+MSlga;z$GNyL{>hJ+L!W5YzvMl&A-jJ3@h-!NI4_7zK` za&cfP46J$*rJ*ad529Ae`VIY94)sZ@YCZg&;g9-tWWqF6GNLG+z5Ms7{N1(z>l%3AaCZdXC~Ly~ywp(FAw%4f@Ns zp@56z2BXht(`)23+u8tJpRyCLY9s!L2*JRBk&=KqTLeIS#(|uaWx?r)lr|_k^rZFx zs_{R(yAl{rEN!Ep|4NW+Vc^B9cCE-fb9wi?nejV%TWnCV14LXRJ~<(ZPrYq(s^GJ- z62Pj&5L{oto*@-zI?x#qCIy_53U(J+Uz_-Z&|9KoN!2b=yl$?KhQ7yYF+U)ZzrX7u zdogZ^y-f#l4VynUX}}uxRZX2zl#8t*r{0OnoKVHBwW+R^Bn>yO3Nj1^`O72g>U?5l zd>52W6SbK+W1j}$2n$CJX#J4;4?Gq2plOF2Qz}2n>_Q^Eb66JSJ&E$sXp}Vv-vq}m zIe44YvyB;@cKNyZQ{4?2dKCL2a84L40J=u(FT?m>nx|Xmnvi5y-51tVqe5;wyMIS_ z{$93eeBNZCAgZG<;~Z1~ZB?*83JUn37)i_CCvWuRs9>K_?huj%Ypu_XulvI?ETnPv za8gFQYm{;aO*08p{P>P$We@y&-ukU|gz;|`Ee9I9$qZ#Yi__EBaVYpu+9`eww_Z$kE6N3RlhReHW}cF%$1{1>qCmgG3ejY z6rh6tYjekI$T`)x8XIDyxdce=u;6G63*3T6k6~z-uBWBK5EY5-eON+D*7(_G_`3;- ztZw38V{|7sM)>wf%{!xcSPSKbEo8|zYMESX;i|=Tp7EKpOHGe+HMN9_$df%Aj|Yrp zw7pC9)X3E%pq@uhj~$1d6yh*20q2lv6_(SfQ!6&3sH92{Fs38Wk#$@rth=o8LXa!0 z0&lGD|Lt)$B$CO{%=J^52w|{>V`MDu%%?`NxcgxBLqj^Sn+8I7suv9hg&yIS&G%o1)eUJoEtt47a|qyh>FT$zD$f~ z0IR|M06}wlt;{f=hCjG*3z*;(aiI9hn@D#QzE&}zC_njDpAU(oEQj^KO;aKP_jpOx z$t^k{`)`#Zx%;TxQNeLuEVq@#EcW|G0P;4vFX9@b8I$=Q|JK8snh|7=&5w-sSnAx`jF}_)4qEE z)NZ;&fZVca$JJFw{!a<@9o~gvkq!z$js!Z4dMe;#BL*t3bFKgWh5_>pbJOIqe$TOD#O` zBnNM{+u@T$^c^G7Vx`4i)0^6B%R7HMN3#{t_fzHWfX?=pS63$m@rqlVFN zjySU)b<<93MIQQVpDTlKEeR z_#laUtIqdZrqKB3hWID!r34G11k#-k^^8cebcmoNI>%^$r|(QUKo^Wt)JpR**n`Xb z@DUgaE{wd7>OLLul}ALUoo|fzI4(cmDh?a7*4Hx|C4+#zn--8xna8SMiTM*bH_Jca z_ORBm`k~|pZ{Rl=?tFwnp{cChdwGHD?{f#ohHW5-Dc)B=XJ#y5+oa&0x& zUW|aREGVK@ubz{djE~-Cd1R=i_Vl^pGM+jjxZK2_ycK~YGrzQ~Kq5<`)8MIpf1 zQ|3^d|60UL6M~df2yJk_Aq$~&mw)Qn1%hx`RRaR}v|Nt&+bC^7l4NiOUc!-ZSg9uy z3#x<>joQ&w21;-ogI{0Z;4Ll91cp4aq%+1wO{LyFqW;h<#>q<3DyMF^z9M$V-9r!G zj|^zmDYS}%t1!zq=j46F*Q0+bLc6%ZlZ^9S#q7C1#&6M&s=7!Bf6&B4e|SIbfBwe% z*WY*_=Ia0Rx36E~{CRB7jMmRBX4|YaE{EZFD~NR%C^3-&@)RMr*FvCwD|;)|Ds4o1 zo?vWw-@EF@5+qU@Vq;ys7ASk5n?RftzN|H|tF=~K4esV}73Qhe_t-{NU@&BaL6cns z{gAJ*T^ZY9(d)^3?vVRg81!K=guHdeG?9lU3qi9xphOLn>|N+Q|DzeEX$aTj(0%IU zp}xH-1)uFn2|-J`+LnXJ?MvY9A?E+U+nm27OZ(z#FaM~a3G0#NP`jIK&$&=$B^j;b z>M4Mg&3sX*=qFIrazw=+z_v0?ymp_-gpS0*dB&h`ckJv(bP;DN;%=?<$@TI(Z2sxn z>xKwP-pJHM{u3nz~A*MJ`jK_7`{wtrr{ScDXx$rk$ixhfDG)hC%;| zMEit^Ras8t8-$b>1Wno8kIIs3HQ4rC4@PA{5rjkS zJdfroE+B_I%5}S<A>>i6chT+kEEykbNy~X z@G5G*y!(MTy4o*};-1r~i+x!80^j)(frxx+(#sb!XyA@PqJVW~%I7sFP~!Q{#Y`Sa zqJ`0b{U1vUTh`ChX=qV{4h=cBQp^U|Gd$Yb8DM<|l=xhXf7cd!23W)=VI z^y5yZc9qAwQzMwoQei#|FpG<;zOxMjrc{a2oO8(=rjxsXASQq6Au0Tn|FTl?(L}Py zIYW?mgR(A*+w-^J`4y3Vj~^5M*u5gDGpqPoyOty-=DZ7MY&kQuy(c{xjZRhZRVly9 zt^N~dPD8peBh(^%h>5qe1d&d@J07~MvJ0oU1zb!0io%5Fb{b-gjhFoHOFi76#%i&x zszGM}|JFOMq*h2F!7Z&IxacT7oMrq-YP0o3#a4D-)lHSasO#CyYGn-NLHGYsLs(dh z$>vH~Zjuw7f6D_<@NdBZ7lx5H3@Of56uJ#fa_YUT5g`=2vx|Jjo^Q}QOM&#&wh`Lj z080nxDv19KKmC?J|D#fev!P|P$m3{Zb+QarRvZ_flPV)~*zavo6EXKufU|?ERp=(- za%jW4q;k6}0BSrPB1Y5#CeVS$QSJ7qTC zmZlfFOS-?HTMVk7Jz$4gK#{A;t+Kv1Hr?r%WGj=NmhtAk(oUzZ1Bp=I;W*6@=lV}W z1zAg(Ly;v8j-{kb(ec>@*d>~e4+48^vD;fgziN=2`$te_=X|pYyINnK1+GXhd!k5w zQTlgz%gq95`$qQ)>@(Xm(P>R01J3B|67OIGapmOQ{W~*$sg#)&464r|^{{Ytq!=c9 zG;I=YpRu?%6x9ZaUqOfPQN{uT>$SE|t3_2B27 z*xA;-%khs9{Lj>JT%fz)FgjrY?Y|6H0>-pwy@sOA-I^F|Gl44!mAdeI>h)d3%a1a! zZo3;sGTPYbE@Z9iFU{5E!FoJNvNS`q8LN#Z%LC=-%k2PCnNkz$$h)3l1|9E-BEUZV zrvvt)K%z&=2F!-aS3S-3H(Ib|IbkVGOgc3WXCmqE19(%h8(pVGs=~aka(3lhxf+f< zz#{(}l+Lm$!e-Y-5syScUhYfX2cP?tlv`PBd}jL!*x^*ua&IWMsuu9&LFP|fDoT|~ zy)hL0)w^Y$l+GETqS<1)WvS#;wzE1g@Vq7V+h>Cqr|5nGuxz|?TE$zRk_a?QhweoL z%p;yJ168oiQ#X@(aBt28eP^joxH|lX>RqNH4AUyn-(WdElHd+%%SC*tQMGC(fkROi z_9B9@EWEc}@jKgLwXsstOj6HDTZJp`X7KS=jNng`f zLN$hGD_=GL^>aoRu&hDj`#%hgu0WlXWw{fB|MP!GG;YW3TumJ4Ah&OmHV3w!kxllJ zNg7~H4iofbZjvE0^T(B?ZxRLWh?AgcuwlI0dxR7l{R#Tf#T>_8M|U={>Xms(#cyb0+%UphNhU5MXV?*Nj0R+ASJz= z2iU#boFAXi>R$Q!vc`6z0l3hRd+!OKFOH+w(R*C0z~P&{C-QU4|9Nd2?U5Z@rVfl^ z$W+(sdiUg0$Xc3xS&i}O;Muh!WYDwh4@>x=)i&S)j8n(pFgaG2A>|nE`zlLru9{dm zj)x(nUa=5+EWHY36I9$@dacDc@lu|Ma<(1T{8?*Et)@`S?Qx3(m+Vbc!l+JE?43RK zzJ#2wH0n8pHqDuos_Q`_KI7Qr+&%8qI8Fah06E>(!5I(Ew07c)eKupm5o6cj-n*z^ zKV5Yy8o(V2oJsxd9#J%XB>^1*P`6ctt%5K6WWqfqC8#M*fgZWPoIgU%nFw$^*V*Y7 z?I*aWs?E9}<(J^gG^z4t6EN|plrc_D?>QrQPcZopaC%J@QiiR& zW>OUKUQ}?WEF(|%`f?{DDkEdK{I7@acMEw z!mapdX~C}=nFYFCA+ z9B};9%zQpTJ@o2A$*Ve=>Z{YA`Wq7{wcDBF_pw;OH8^#M;RfIS+MeO0miC*LR2Uf( zS)HP7Y((kJ-7v@F#1M8?CRhDhGegfV3=KJh4e4}pM`NMVV%*tQCy8GcwuTmVB>@8B z>cs)-V@Ed#cH&vkvXHBc=<~xFj!1umg(OCdd&O=)qrT~H%@#r?e#vS5sT^9C%y}o1 zDDJ(Tr1Gg!t>KT?N{bQ*$)|`{n7y$`*LOFo)b}*uTdgWx#YT%k4gjyOdy(>!jT)7b zEZU)g`IIUB>aRA{-)J=&|#8*-gtv6hbT(!@Q20J&WBP&zD2Ii ze5Ggh!!Ye!*(z?vwP9nM_sV9PuDtN0;@#NdpZ4^t5_d4=I$@h^#3z+S_ZC+jI1-o3_H=*p*#6 z77mlpxJ*XH?VX?jAW7Xu{Cm6IFUJ6XOLkV`ccGbI_0gv6QvKAmW`6FYUIP>|^2i(8 z?g6&k)A?QnDH^4mksiRzRj^}i&QGwDzZmS1X(ZMxrK{>j%r0f57T9^$^i(G_m`f<8 zxZW-k=zea)T=>)hQwM5Q2Jc}>HiE@ELuRP}xkBboEC1h)w=6(LD8#u$pR3IYo=eO0 zT0xSwSR*c!qyPRMo;X4lXuD)}HoF+bAVPo zvO)^>WSyM7)>;p*`nX}yKY)B5*;I)=NtvWuEKL=rV!O(vn&!!hArp42J*7~6f1U_|Kxhx#x6 z#pGE_3;dZ~!gBWxEWw!ha!$D>aL&AQT|Sm)4f9P=HkF877=W_xZ4)MB+=Jt#Uj=hpTuT1Ap?|um1|! zZc-(Jz=|z|CKLozVNSY%;O@KQ@2XQb&p~et;e#qnY**pp14|fi%Z+&p8JRdNGYG@x_=EXX^vEd-ky-mj!=JXnq{~%7$0*x;C3r+tU zVU>iy^W^QG+&y^>2#EYu*O7>X>@@5`Gz>h-*e%7TWhi0WQbq#_27||1k!Fsb9rVgJC@3cN-HIV!5 z))4TVEtEG?R8tYk#u<>94v7JnIoRi!+7gtC^UgLMM1E}AO8(MeHW@37;lE`P)on^^ zB2=T)fu&j{@>NZG$FdZ-s-wMXt%o|G*%sVlCterz)-IV`h>6%`H@r!^WV!slm?`5- znwM_)o9S^#(;19^b}&DpNkLJs3c0H<^rCaF)~+2Fz%hP-nVlo}y9$Mx4Kku1v1_6y zF#rzigOa2<_07w7fcO9KS|Jz=-heePty;5Tk5{iNezbRi*jPd7VM2jQoa`JeolXiILs8RSoQTGomncEKJ@22N==m4b>(yOYKqP3VF(3PZ!Cdxy zF^dx5Kc`7Z_C9Nl=C!kV_xm7?9JaPMu3*C|^`fHn?lrqBQy)Axqt>@~VQB**(HZz` z1U?N~;k;=;bJ|NUim<+RKJp(j$qcp=n{T6UTDl+g|9HJJ!80Bq=i zMCq4N_1L2GE8HBO4)0f5D=`+${f{+S9{KmZVjtL07!XuQxBT6oay;V z{QBd*)AiGoQ$tpn)()Rz?KZ(G@b;{yJN+E&%3B&`-J{Lnt1q4NlmST_%h^LQv4rRC z3_#66I@L!cD^0(;wcdwC(b}Aaapi9lyf?RVDK7^hZ{B1t#7>wi2u>51LV|9|hXi}P zM33~H=JAStM0QgwnV*q}UA^Llf7@qkwscY!XD)@;C#M0YsNAAwX}1sMn&h8nh+Lw4 zDl+!dr!!OAiE~mjh~5?)MMXv%+nou0U9jd3(AhR*u!*lSM$he#)_%}&C2>rLHXVb_U6h#^cbx^Mh9mfN8+OtkVLS8JnM0)KXox0_a{pw$FoOXjBZ#{V0ti^Z!Xi&4<*OyY- zzUznaUHpFf^9yzuXKzvS8dNH4f82}%nc1THM3ptv%4{X)ZIZ66=68pAJ6YA&mnk*p z=UtQjLNt#3(VUQw%Z={!7SpV@0lNAJ(UnBm2Ny4Ty1<~c)U#e{z}ZxD=JZ_XQRAio zU9ZOrCm}6!)s2=5E-QTSO)UDQr=9=EaV(PEE{_-w$u)2#SoAnY4Bz6nD_f1o+Kp zY6it268ZyTA5?z-?!iO{6Pue_{zbv^_@5TQ^_2@_S;onG)Dte`TF4k7!HF3jYanZDgQ%~2B^3`kM}5`cfVaREBPwO- zSxkGSy7hn;G+xv}aI4ruJ$p(&b5<&IJG`zaUUGv3xRD*v_@Y3qo9XFR+5ZC|n+5yB z)R~#)o2CAj%73S-!DpkK7^HOgHaj{W_l2`? ztrVLFjqZq?U6Dm6QzD|sIYQIt8n4sjH?h+3^yks zK6DP8Vc`QH5fV0Gx!Ml8hLE^>)NB7dy_93Nx-HH2FjnQ}K&?H2Mle zqM0#O$44;eg3soMOx1Rk{16XO#h6s)?oZ1QzAywVs?Hl zE&7VD^_LLsCJxH%2`#RlZ#f7D%kJJj?BWsznIn?av6b0!UVVhqNpZd(1iAQ7`^y(; zlg{4pj}UxJcSBSSfKEGO==S*t9ps2_a=P2N$RUl`0xElbD{lEF@}>sAZogh+E(VU! z^HiZ0gHQZ{s~-QOciLh!ZhI2}f!ADctbnDQc8jK{yJvOgQgLWUNF_eAWJS<^nqkt) zP#tj%C42A_loJ((fDUb;8KoJ)e}|SaLWnwauyc1-1dekmGe#MnZN^3mQpZs3<-3zK zd9HC#*>FEZolezme+*%c{*1CgOpXGFkXigzy>9U!ntFC!Ym%S#v_R|P=S#fC5XBMADs#Vtz%FE#@8jA$g#C0;YnZ z>a;7rVYzDHYU8sho05v{0{dorChvy+wsde{4^let4tcwti@GGYPXL^2UnSGU6VN4|~`H zbYNY)-CHqe(mI5^@>hmbV${_&MeK5&y}dWi*qU%pP~O|>yO{+ahtzf)lgZ}^0Jc1G zYZ~=-H(Bzei=%pr5a6Wqxcj za3=Q8!~_Xv8cVN2r*szTg*pGYwn>!b zQ?7|*x+&Xd7{6W(KHy$%_zW_4u##t&G%9#2s**uWwg_-jj4-f#lngQ@Uas4M@Kk}K1GWTRdPRZE+1?%^bmUKR_N zy|-IlcPqaW&1BC)bpT%`FTq%wiwT-%kkLiPLu^O|CNg)Xcwhw50Dl~AzGOswXK;+q zxe@JnVYH9dR@QDUHcttsJ8=hd3F7jcUTZJur6x-#R46x^k0(aEoaU}h9<$EK!C-ab z?u*^96J4!3CdEPNQ5|%k9pfnp;tf1c6D^p7)PSg|1l)pA`wQma|+Ly3D^mF!kQ(6XDh{wFNJfKGmx1)Zx>=AboZWsLNv7Ku`n$}m(Zp4l3lHo4o|rf#D8 zX&h%$>%r2YC+V{DA5N{rYYgCR^YlO!myi=0=xlIX8RmE01sphOziiIn)|&%6#SW#K z|HhgCR>>Z{pQRPI+A;Qq8PjxdrO(%p6YhSzf?I%35c!gMimI(YHKk0)P1&Y!@}^pH z-iY%)#pm|_5|^-#1z6=FNksp!EuMm%0XQS-h_I-edz*`OhtX?IFx)%s$?LcWA%C4A z{jH#&i9K~K@Nvp)_Dgwfw;I6151Qf4F18Mmx-yw=_XrBOUMyG<4bmb6MI3niiWp4N zY+FZ9db9@ea&|m*;Nk;R=}X}lI6+fr)}be>EoIo%zY!;r_^^ooY<+yle<6^CfOBl8 z={amH8PJ}NuCct9j#EY4l9<2{ju;v1NFSIGP7ByP$CSil_r*z?AqjK)KZiS6@Orviu@y@{ zM1nbsu=hngZB<@n!cB;6*%HZen`Z>?bEU`f*Rv{Bu8dC0TS9Qo>^NhU^TAmhxgBMpAc7 z)OCb+Qkwm-kLQLyp@644!2@UwzS|Xt zendrcabaOd8nZW+q2MK>ZZ9rA-GQrDpO^+z)($P zA78Bt=(b8K!)wiqvdzLlXX%M6pg~nm@&a%`(8QP-1289&u-pQB@-&4|yRq>aZ22W4 zWanUycB(?yzG1#-JaVyF2tea5HC6Dl1=y_Yg&{Q?4I64hiBss5rKXnIY7OI*B%Fui zLU=!A!>WE|W$vmZ1R?Cqw;lSoLw54W0u+e`avnBOu<%C~kLPdhb*QI125M-9Dg6sY`bz7FX1FmZvM- z6&zE*$k#slvSuh0z?A7q6~DnL*ummLEXrhnEgHb$ zT_Z}b7HDEgB8pw3FSr;{*I8AUHLVKEoB1X`&YyDjb-(&t_V?@SCF=#kEqdWVyNyu{{GxUnu*l1)56D4!pJ zsmtmYBp)^6xZR?_e<`7J2cob7{&mzPkBHmA_E=TW)&& z75hOrSM^~?{U`T};|qY4^YMc~QZEU(^GKL+Bl0_Laj-$M4Ar!_lN?^hkFOUY*@8^O zF1lfzEM@PM2)pk-W(uZP3aql=ah3?HE%+<2NZPla^O68@J4vb5jE^M-{Sy!KC*NL< zT(gob%U*yQnZH}g`yfm5>*h3D>D%4TF1vBB`dOK@Blx37ynz@di{s?d+SRvM)f{2w z4gJ7lFmF3Hx?y`G0_`OFab?|U^Q2DN_++@txSaeB+%qL^%Z25l!S{2Y9EwOP2jA6S zZSRUggJv4p8)Z@WHl`!sk*+4WHIZoE8-rSl?+1dMVX14%>-l(?+|iraY0wl3(7e$H zOWz~(#?U#K7fJ0)Q!7RDsiSJ&=LFJ{JQVDN4e^Fumxr>oh>mKX@>cpWZ4X+l>S@X@ z92b6?>7HgwB?MFuPd*CzF{>GtvuEN(3%mF{83Vkdy-u)Jq_|ISS|#PBn(4B2Z8YnYf^(O~czLR(E!ACq z*T+Cx31uqqN`reLvFuf?t0%vr;Mlf|Jy5p!ZvWubv}C=Nkd4ULQAjq7OY_ z7fuf<2%#O35(bQtt7*O)ylT7WbRz;dSkQthiDS6b{IV+iY!Q4jy^?Q5S&{x zzPTa9X8zm{<;aEFES-lf$g%yYDqGBor4QHanbUs)N65n3#(-3th5N$FB+!NIm5wy@ z*e~LA+$}d)xh*l4$AfT4OOxu`U;j==>E3dk0mCs+ivFo9A)_& zV4XC{o%}0-xWG-unjzYoMyMS%w@+HF+`PXtUc-~pF!OWs<0gS5am#enA|0Vk8{Qtz zM;im?kjN|bS5LLD_Dgn~+6aO1$MX4xFOC=D796{!!}`*l{MGRQUxjU2BO^Yz1ZQe) z6(IwW9XB0I4Y0Lo$!);lhBuq2xVL=(4fRn*;vkQIp!XDJv5#3R69*UN-k{GV)A(zI z$+<@PWjAn_RF{B_^#9~RAX6X3W&Xe!<)M6m73nPEfcqNN+a;dbQckhyRgNAw??&!= ztXFZ&jib+gpG5ISps@JOn8oECyuW{W1gBZ?O6%4if!^2SOV{cm?5v_-#!0Ui4x2%Y zq!@-Dg$zv+`R^71GA%!93!TvM4`(OOV(iuQIuq64y1rJ8J{JHBkK|L|f1;NAP6-d*&&3e5 zaf1U4HDh4c^z15Lv|&>{qJ1vO8w7TY)Pf62Y7I*lS2M*qPn|Nxk4^W5&BN0TQ|c#rchFee`Hs$0YVQB`R_(a z%OwAY)EOV+Iq609b4&?E0}o}X7}g$(3C8*gYWHn`sZ!IX-wG;2QME; zIih<9@jVWfgN4N})l`bwB9;U#k%rYK`GCm86~s~9r%#(Eb6?aGji?Im(3tUltP*rTvpzY(WlEe&)qGn~ zAzXDc>KI{052QPDJR) zHl^*eBSQGR{(0G!cN4~lf3y6@lY1E=YUdrd8{?zI&B1z zQU7r|?A|b8FzAavJevNL`)Hs<>^mt*pe>{0e&YXFm4G@?5{&*`gB?dz6k`0?2#0&g z6ggpZX`Aa^b~rU`hyoEd1g`?^CagIPPlB7fnaWkH>KNOfo`^x;G>95v6(zm3*q>*b z_8B5k_?Vzj$zKX4p<-8oPex{l2;l|1N!mBgB)gU2kMgh=h0Y-&Qc z_^YIKKIjb{Wwd2UY<)wOMUpt*eAaSwS@zZ{m*cG^!or~Uf*z=tUH1_t1W&OvMJ*)O zJ`)fB=9ENX`;LW0^k^*llaw~}AKq1lHeh4fy#pzfXHbhHDo%+KHEnzymvFW)F3ik+ z`O{%xdwdixf_4pZMhrmcjMqMk8Os(t)~fe2cp!@0jyeTCU&}8R)8+=TJA8pUnE%J# zSB2G?Y;6X2cXx;2?jAH7cXtmE+#$FI2@;&(?(Q1g-3jjQFgx8zpVR%HGxN;V+|1qP zWACr3R;~3euj;kHnF8us>>#_~;G%v(^a-?ylkiLZwiw2*(uAyAT zl29`$M5PbrvkmDW<0VB;I~)>;Jw)@!o!1>4KUZr+4g%s#dFvRt(ldu+TkaL9)>|sz zmBI@lZ?^3s?!+sb9rxI!_G4Ftl&{t~7l=&&4ruhJW7~r0;5_fq;V;AJ9SmxI59mjl zMA1k_YbKE2+4lTB47)hXNn1z-j}ZKr@=m(#!t*vu&-J#-S)G&Y!a6r$S^+B0oyL@4 zLmM{gFZZr3?9Xb(GaC`mcq~|PIR{l(7+;5?UY(ypxKATGB?T!lgpg~A?I&DYbVId> z9ADdFwqTj+u>DSTdHlU^<$8q~^=utm#@F7fk8XU+ryMpGR%B7a{WQm?fs6%CMxp9JBI0(4kk9@!i}_ZgjQD3pIG-ub>b} zKVJo#ksrL7la0+9_fb4p&d@)1jX-vRUYy@ue-jRuQ zxbv$@z0vfiW3G8Nh;sh+FxBL7N=qh4Hg3#jJCwks8&^@JzXz2E9MG2-UlXBYSsztwfY8Z9=>RX@E>v0sk4GF(T z$S@>;>;E*AV=fMb2#n*dl%BijF!-h5`(W92DAI&k^UFbJi& zs<*-kCX&;s%HsQ>bS%^3K0289-Riln&FDDoiibK=xKDH%#kU~&ufeae33u=<=tiA8 zYghAM3%Z>~j=3B@?PAMsc7T9In@|*e0$zqbl$i-fuV*-pTB*_ux;W~af^M062j%xM z{Mi4^DG_EU2GFxb$MExqtGfCE-*|yaB+C;n&*zD6)PN z%Do~{58X~tLjLH-6N2)(PXZjUN-gQ114J^9P_bDBz|LhAclIfYW4};5jV+y- zB&v8z3Ivldcw%ff7Um2&Y*F%v-N;D})2(hnmA@>s_spLg$t8`P6J+<-)>nOyO2KR* zt7o|;BNK3)JCD13G;w}9uTLh&lJ(_9r1LggSLvl{Y-UyvF+aSB=SZXwPT1H z+a>=@oxevguwQQ$_Y6sV=iF`|*j<5`U95U|CKQihf#VMBa#S~q6O`&tYb}{?5|7?7 zK)k7+85RKJb=f=LR|boE%@;K!UHazBtDlL%6M$j5irx?xint=9`QoE!cB&lphoSUI zs14fn^)R8F5KA$0oOydZ==+XjH$`PNcQQH&pkT2Tj^L7x#JYp!b)QE zX7`IQ(JWDUq&qsNo(3XLKZnumSH_K2b}4jsr-seW2GOu-&7Va|% zWh8yfNj<^ta7Iu6k^E^QD}uLH{BO$JADDQinMlZviB0*v8CQ34rkvhy7AHK%biO|G zv>~NG-rNN$BSLiS5+$C6Z{A!lO*Gj!URl@R`cO95Oibzd5fWH!3_N6_`g%nSg@&Scw;H2i4RzMMl(8FwW9yzgJo}3ZXY$P$DF=>c$L?{#r9eIFg#!ToeNGkDz5`L3o#0z92gv zQ{pQ6s5g*oHmk@pdpeKDO~#yKrbKXk-iXC!jD4t&wXW%_wdL0ZVQV|y!S}0%a}Vay zf94>hQT}xdXN!x+Hcp+qng=yaa{c;*NanU9h5{I%M(;fpM~e%=TIH z)$W}i3OJsxVaFH~IQ8;8DJ{-$qCHQmX0tP(Q)ZrBuWpE5oBun=zs0 z3Zjl7{nHQ14Hn9Rsp<=Jvft{!nkb*VRj*mNjLrlRW-sptV-6U<>pwk%1FJNnc@WaJ zx3Ozs`38oHS>E;eXwBlS@?3IJrox=r`4fRzrS-z-Fm@G3J#Uu#r{z^FhS}bYmFw4Y zKCr}BZUm#<#D1?^4pCL9`4KVv-?adBq&(OA5ovLKMk5N|8Wm|hK{%J6CoADZBHgDO zo&~fYr^H&Cg=@QGKZhxgWhJE7c6g zie}^BC4k09Dzs}@d3w2HlYG=4X{Wbed{<<~mA1XqCWneJ+&!NYK8IF*RBxO1%?*b; zHj8@CCRpsfvvQKNAP*M8E{w)oFkK29*Rvj zDD$CA&e#vYSX70+mI$ro?BJtwUBRoy+3o7(-S(U^undSdKb%bvc;S>m2C5kmfpw?a z<&^#03g)syvCJL^+B2}{ipvF2y_2i-(EDv0`=(85u_|unCKsLXvPO|g^_CA9L&Kv0 z+@J^L@!4z$J{l;HRiXV*Wjdxz*nGeyI6Hh9XEy}sUXya4gIB&{ZsfMbRR$&FT~F_Q zoH^+WZvQCE!*4Y8{-}4Bs4wUA@tkY0eM6JBVl?M`D(~~MreW`XfRFx2MDY1uiBGGm z2JXZ`qxk$8SFez}Dka5}Kg9c+?tHDnDAOUxLdk2~POiFBWNkgG60U|@rlJ+~G{Pp^ zz{>aK(1;>K`DUBVk}U1&dqY`a+fYeP?%F7*5kH=TvEwu8F>Ecg+Ht;~9)zWr(&{fn zrw(WK?K!pLeBF8K1fVXoS6E3laK1AEYXx9$u76I1+6v{7chQ8Km34!AU&?<9dz=Z^ zjwqTUE zrK*H9HHs1!sX@7~^lBrys8`w)6V^6tzi{*|$8Uc1T?DmbVP4|DH02ulj`rjx_H6&c zN0v@7NJQUgp0q^c&}64_yRy|g$JJ-7rNf(Se`S7idNVo;26|NBMKS(r0C(b3u@0Jr ziaC@x2GP-rM5P7CxojTjUQlI`L+JCIH5aSFEegiPm~<0L)>n!uqL*uC`TT&$t5V>b zO@9C>(k`H3M8#%n*uM-R8LGX&U_OgIc zzU<0i`BFn@QL4F4xgr>He+=I)qi8XvNYCYvTA%z?Mcl@H+IHITv}#^ne}ii3OpCZ% zSsAXI4oPpslDfvGv`~Go$^^3Dnu0=$z3=LnIe*N(mI*&ptXK3kqTpYl}1u zoJ8a^Jy5WQz^9qSnfJaWMbU$!0)dtEGu;~TyE|piyIFj2&#eUw-v*06Y$HL|*cusu z=5IcW+@FxcK_U$t&`icO((`R4yKkxIT26%dqjcY9J{{s!e2F1 ze}ulUB&-niwN(jI`}GCDhpZ_ujp#)hlGljP+% zd$I5USq}jNP??H2`wfgsGoM0JS>+BCd#rzqv|I1!X#&n|d$HZVO&HsKx($BCwkX=M z{W*$g%>B_{zc;EK)qYSNs=lsdZ^th+O4I|j(xRWx`r#Iwdcl;YDHGDQNDuq!Fme5P zr1|W8c_;O)|?;SBca4_OaTswPbyfUAz%! zE8Gx0&i$;r#(wH_-ldSQi^;XY7fm*vD%>9%1dvzuoStUr$(o!va%>|V%(Xd@iQ3Hk zQ4NrYCJlBG`Q2Ad`yTYP1-m0#psEZUaZ_~TIL~hl=lr!9|J02y!kOr>Rq={QW8D?` zm^rIf@QxK0XsTZ0z>8$yyMBNNQs9et(pvL1Y1v<^ zv!Qv3!)SZ|V9)K;rXl~ug8tb8>5jk0io4o|zfHhcEVy~U*V9C|$AVS!_QVgkgqXBA zU$oRPR-e+I20fM!-L3UGk}9uHv8PUF4uHR!jF3SoB09W2t11`93g#rZ<{IiC;#iBA zKMO580Zpj-gux8dd$284kDIy1{&y;b{RnsqjST5uhsvtAfFTuluh~cWZcw}p`wfAx z2+lf$PZzV#z_v9^nVZvvLAsmph5ftxIW}bmQ@fQeTr=TCg)B;4*WT>j9vebaQL-TT z519E$-Vr1O?g~OS17i9o%wfQ$!+p8pIl_$hE^Z0M*tKJPCS63HcE$iJL7CH~dCrK2 zJO$#i_O}2uw=~^`qVgDAXNI;i*koehSr3`cLaZUoNQ1dg3?jvm#>9Dcx(7wE_|Nr&Jw z!;j&8$?D&8;!16FA6BdD?b)c`3d)R|zZ=3VdD_K}9o5W))0eG!U^r91<+-~wl7wGy zkZUs93BlxIIu_-7HX01%9@m@2-_kve#N^%CXhW*sN*YVpcT`st{yE=uEb!q3U=pVA z^}rMrEPb?NY?Es~R%B3GZE4oQ%L-suPC+4<==(NOiFFWA6io-nTQ;zt=ja+fx1 zc3KweI8P{jj`XG>c+aoIXYl6ccxGcF7>vz;sop_}iZ@SQCIMXW)3+A21oKi6Bzm1x zj}J|QDo5|Q_mj+am?er%u|0M3Hv-jI&VIzSgkEr$a+f0v36lVz!Hq{SKzm3nMMfe+ zL-%s;7=Lyll?8`~lN`1i-C8E^KWyARtOztbjdQx%KfSP!^tnTP&JVz}es;sVS#|&$ zeQAO4dAvJGs`CL$_uM9ScRKY=XDI8RBA_E4==cF?QFzCAkIhl88*bv!z)rm`&36OP zQeJCBe_!qtS~*C*6nc#P>V&-`FikL~`^u<0|B<8q3CayOQ2BSOB@mZcKC738I^VfsjAku3kwYKI8D{ z+WPb!(OJBNNk>^e*D$lUW?mLO%9KwJyF@!m17Afm)VkxjtTLl=MTiS_r3;j``VU!f zCV|#ujpI_?{&-b_9Gd-e?YDFmqoXAbdJW^A7q3PwxAl29o049I#ZM5M>ma9TID27J z*|`MKd?|jqE}_UtiT6pI{UMx^IQ9qV<8)nNFsIx2mufJVGHtBpQWNnm4b=q|kW&K8 zOs9}>B92~bz@vzQp^Sfccnz89=$gtNpi_cqI0i~pe^^3ATIkx=Krh|y6;%aa$R<8D zvk4O!c7@!oK&nuGtSE*l3(i@K2$TfL3Wd@Ib3V$29~<>{P^C0r*P zNIyW;uBf!~#Tn7v5m1N|4KMa@uqv`XiCo~7OQzIX?Sc_-(+nZ;v#&7?rim{!-mTV? zaVs-OC&#dgC+vcywXK%GWz|HO&V4+nmVWv8W>Fmzx56L|q*xW3ZR6T-NbM31TbqrD zpdH^P8!B&t?vdAHf$1JCv@#VbRZ->FphA^DYv$CM^cLk=*f9KjGAvtuNrlBz2yVB=W7)DgXuVqUkR%>YdPUg?h9jepBV=~ z1w3BvoTsR1CAdCqJ=BoOS=Oc1t<8|V*&XxPTry5je0eOY6u>I{1$VKBU0Yx4V_Db~Es$VwGj( z$(&|#BtNnmojF{Q@ zvahgbZW8K?LqJX*LEI{*!InG0XzGN?cZZB6b16O1mEPpOFQE&qR;{x6!oJ&nNyTjZ>|&exWt$?X1iI=) zFxoKd*WUZl1B+8e*8Gu$Iec2AHEr%Y)hA6$^lx4&b>7>llg*Y)Z_QVBMXDUyewOyd zK>sEhAmP01#3{;rRY~esX)pND@o8J;y25>eTkh&A?a2bNX-&03w^B0p3hE? zs}e^GKXbR)9`qW66oL;0afZ_i4{Pq+TMfq--g;oq)^!(zb`j0 z)Sk9rdS_LHNx$XTK|mA8dUIU7_tE)dTOeVt(iEVrrF_xhUv1Q9SfFjMgOB04hQa?X z41Y+5FF3HpX%q@Tmn5tOr>@N9Pc-|Dq^9s#1kP*>)FuUL^T+qpt?DKhtb5<87JNu!L)ICMaZ^Q4%fV6W!Gwiv9&E(TvIH^l+o)Jwcr zQ*DHARIjVBvTbl^t<>w~^@4-ut!vehJQelBgqF=elBZ%O<@O9NQuG*`ZQg(Bw1y?m z=EmC-QJwZ*Gw>D_G8}jqQ4S&bQxbuKDgk@~Jd2?EOHa%QQF*MYmf>){!yiPcB3WL zrZP+s1;Prq>yD8^-kBgjx9>kgB^Ua}V_2n1nh+r>v8Wp~Nx{232eC zLCrq9Qnj^aSIss)CNI^+z@0j~2~j&^e`e>bST`ERIn2_e)}b8b#^KA5mfr`e0WD-y z;H!rwt%uZqZ^55DB7{T9WRxe$=jE;bhR~G>tkMor|2W!xo{vpwdqT6_k6`Vs+>jbe zsj0yUf*Y3o5R*4f(_b`*krD#e(187GvKC(@D?mo+AR z`p*WE-`T2zfgGr($zwko%cN@ZU{uN|=_7IlPhs#F8h@k$w@VI9+@a{!?);===DU=E z9}&Wn_`!U#7C4@_%z7!zeu_=?4mbE>v;dpo@F8V9T-8=Sf(5jxtlBR%^M}+?(*n=h zz{Jq7_;uIVJ$UYv%(qrG8K0iB=rqGG)f^AD~%xSU=@tb!}; z^}zT#jYhy&D(<#F-&v|rebjCwz&OPToUoDQ;yQI|si8>rFZtSVxP)6s0^}|n3Z^rA zxM|U_zeqw19u@#>g6I|2c=)}qfLWN-g(#THq0j3dq^HcmT~5N)z!1qw4YY#iaRE@J zqKHO^0+}-1vifFwARbRHve>l*`TN(J#s8sjM7>>Ztt{#M;?T879vP9aJix$?yGK32 zQ&M`LLZ5A~23!W1(6mR5aZ*dI$z>E5noTVU*rX)3Dr*Rp@iB?~*;8cGXVUU}$KFEt z!HzH;MVc0XTl&N+2(J6fK#Qv-Hgp;!^tDC@TOl;8^)yyhZstl55%nQ5mgWwM<|q(_^?b& zPOXqhbymADsSlMM`lpJ}aUfgt*`*Zxvd!3Q?ydQy>Gzy|?w2s|Y!r~;W@j>u8JV?U zxqhq6MvvvWt;d)T_~TDRVE)L%07wgT8>F(-usPM)4pzG>amldi4sP69#ciPxt@V3FRJHb2N7BTk*in zT$ocsEfuuMsIN@x-;W3zO%Tc>{>6g7m;qR_P_14fpcFD&rZe|aFTy}{10Ue4EaKW zgpz2n>`0ZMgZ{;ILJO2JZAh#VBR$ed$R8-7b<**D1rX%kQcB^SEq+qr!qyjgIESBZ z0p6_Qf;h_OgJ@+WY3SI%14BEJ!Y(y@ zZs!;blqJS9kL3X#cw1decSr^kL3ksFJtc%WuAhDMOy^)08FPZov~pGn*nxh>yE+Pk zQm-*#jWPxE!*U$j1d}Kh=M3=JIr;wZ%UFXQGY!qOvY!yj^=b(UJV0Vq*|UNf%v9h# zqIu}vXZ~?Meh?+2t;)|n;WKNC;{}}|h)a`ypw zSTq^`iweNnQ4R3e#L=GEyl3WOI|O12lx*rGC%59MK%!`iec=Y1`D$}CLy^TV`wbqe zw%-(D*d3F;)0r8{yAw>Kk(3zAJy2BM;H%1NMM+1u`cKHoz0>6(e9yplr0$Y@^9|zD zCJt=zHO=G;@1;}3Vyu72C=T=q_Zy`Y#Zj(H-XOsjUQ|Lj2?uvDKEhiz+r(NIDbwm7 zPMS$TGAzdPy)m?}M0wW%H8q;`ez|&2ktBeVL9c(how|v`2{%UApcYDEZOYfjOo?gt z{#?DX9O@u#Ay{eUUSsZahEwxS_HFzzSFe?PuVI6}joH1$93RNdTn35k2OlN}$0}PwYrqxHSshNCn2i z*S()}T~ENt{W9qg;0Z|Q9aHj@D~bTItC{IhCodI0T7-1Xvt!Xs*|k(khXHx2g?=w5 zBb0&$(OS%V6Df4jrmvm2??td&gUDn;bU6K?gle`FLxS;8XXqTAzY1*}b!(G$>amuE zheNJ2?AGYKa}j;oCxR1-+OUMr5gU(6wkvjPSKoFXW{htZwHCY;82}#U`3z%9vQB(< z+9j3LaFe=ZrbEETR>5+kZ0-w^a@!7*B^SVy;A1uspJAq17_nnRyS0|9#O!GbDj#^= zA0L!C=cEQdHEKp|uq7~kFb|X9igs{yzQ$}Y?VKvmJvMN1yO}&%Ha0vAx&5=Tf)5&9 zJ!S^#OTWpnhp1WnQfpPwZ8!OkT$v*s#Kcs$$p3*{Fb?0nIzXtZ zzooZcCr#9yxx49GFR>x)ek>-Z9u;LjLLFlxX=Y1P?&Db&9lrfBnQXiU3RnRf++T5P z;QxrSirqjw-$Z0Lk8$cXj{$|%r#QV5Y=@mL1cR-U9wCuf0N%p!krJ~W#}`TxF{n|V z0352I0;inJ*@Su(81|tzu`PAO`|!D3(A~5-aPiF4AD<2n{NNTp;?~_g;`)v@NJ(J> z+JW(UAAz^j0~RwHHyQ>{l(`8(6e5>ZZ0OOkvT~ypdoHeCd}?WP<}-srIf+Z|w^NCu zttk{D%t!$hM?0-iSidUy%nqs6K~76LLu(B^eirXP*^HV6^dKnzL5uFEMuPHtxLTCD zJFM91ZCiO)u=wVBW&MfQj!#!x>Xh)yYqbm28MDqYQHM{p@mfg8r+1%-Z%X6C(z2D0 ztqO}YrHl8L+*=uPi;Bz(OAqvF<25yTYfr607;fmRzLSO@9S%Q@4U@A-QGO@3L~Vb( zH)nO>zD+%Cyd5289pj%HUIpPW#2@R?65vXU56zKLTd!6TfNpM51MkU}lJ+A7Pn{TG z$DMobIc_oxyMYTT9f%!4(CjhlKcXOo)6)7B*-F3?t0-2tr5b_K`3in*ZN;mM0x~aV z55IbjHpC`Kp41W4$;^QJs`W_-hR}z{1qZ!S^sT!J#F5TM@{edM(i|05-J=C_ zL{+X&h3s4kneRZPRKJL*{J=HL3YY1`vpQNr+-8F4?X>5HpcVpw1*~&}Pj_SWq%W&y zei|RI0KJ@MHLfyyNmH?NOO*o2K}ANoUgUcpLH70A=%_+879HWiwx$16wP1IYEnG!M zH`L|MF-Cn%`1Ynu&Mx3Q6NNF2F_F{?c$w^b>l9U{$Jv6#?}II#Am_df`M?_n1Mzq4ONdZ=IB>s)M zq>k1FbSEXK^|0Ybws7Fj=3-WR@mKzZUqbgur|1Qw^^ zL*LmEaJ+LvDh^-K9%1<8v`~mmL|-xH>CP4(MSWVr2VjVI)ZYt@quDfb?JXMH$4S$N zuiS11i(+Q@$*q`@y&lyTLD=cNR@7}6Mq?LQ4!P^Z`WSjiz9L!lbw_5113VvbbgVbD z{Xo$(-vFuUw~H1cP}3v?0zZIBya5jj@cs1M7{W_nZa^;azb@gwaO%hJ0)UhwB2fBT3X_y(*>VgliR|HA*{OMI89Ejy(FnQYMS zO2@w$K}FbV;YHFebx6mC&hJq#w>W{dzj}cPgwiH);Q7X(rB&F^ve3H$o|LX%u1&)^ zrhRfSg6azc=9UM$S7rUL;>EzB_&X*7Br6XNj9QX3WdK>WT2C$j$C@%rIJ#ueM9#9l z*#l9gnFDeOhNh5AEMSU0Es6KrPa6Jhc|fX(!!QwR#UmyvtcUu-;NJQWwYQl>@=}fJ zZ>(~C)0)o=i(^e`T<9X#F?muo9d*6k+DKaLKqr|i)1TS_d?KeaFwujF#8VE);R9nV z!7ycKw(yj!@pOe{-C26QB2X{dsIBLW_?@@ zJD@XBEGKTPy&4ql-R=jT_GYtuOmnBbDUBut2|J|lT7~O;N!rt%6nFpqjSra5>!l$b z8G{}Sdr86>w>@m|$@U4i{-qt)t)a`vB+dU=E_$Y~0&vAv$}63N*v-DQG9coW3#rlt zgKql6k_6-BS!lFj!&3$Ng4Qc_L{_#G`|vROptqREOqCv@H3gPnm&3;3tIeS!ywO{y zzRCqZX3n>}ZpZH*AU{60KQ4eM(;Of@VdJ^ScW++{Zgh8_FJ6#yVBg)N%d+xE=sJ!P zvjMLSaK2`)vVkBE^}Q&GYwA>g-c;n}gx_BRdB$tbEMkvACx~J-lQ-&pX@(w|wk4~y z3Bm|pBr`vEcbz-=#H>4`u%a3izsH2n`{4~-bPy@Xj%(|!knG`)C|nk9@~yU>NPAlH zw3QHNI-<;_AB<q~nYTH_3oA>f#u8siX1RR!;ltI~q?% zE=-y)qpr=TU+;bxp^3>YeOfs`FfW8ZNeebuVWI-Z=MO8dmmuw&CS>4s?zwtx>Na_? zq;De%tFE7#D0g<6{<6~V?BhN3gpam-lb3+EFh>N| z26+*ZAC#Tlt+JdImC-|sNWEk%8Qg--=DE;#b z2s%Bi%5#&E05jr&k=;WlL!fYHUqn1ztW|R$GV$NgY*NQts z(&d)bu0glBimF7sBXXWvC5o-dk+UQH66ZG38)i}!2X!sWfS8M-6LZUiP?^N%s({Lc zC9}Y-ff@CI;^v&nK-ly8r}c1YbA=^T)OQ=J-#ChTA=dXQJsSKi&Gp8#S=&ulEU7cF zeXE7V*GBbZ9q5TGh0w;NWB6JEj5eA~3X#3GDLus?t*d#WNZUEh6iM##1Vw5qm5zsp z`=QYSjx@EBL!0<}$!}H)RK?dfbW%eGl!$GYU1R3qi#=~uUq{-{ z7d2Yc*62A%aNOsBMV2=%$kYpJa|)7Y;VbD^4iXBzJ*8ZK=O{6>Z$KlfOEosC;oB34 zGRh}8SwBTfL@h^{_-L>XZWx5oCe9Z z>xjzs<$P60-g2NM8@hsE1wUyN-VcZaYEl#C%L@Wd0*2VEOuiB8etTL}PYy@^F#1hI zBo_OW!)MIG)O;z$Mdg#?{Myf}Cyt9;XR@_MUSSucR$Enhwo{W3)RdvU33`X{J8P@6zAv1NnT1 z6PykM+MH_WgEbuEM})V3SScW|@sJ>p#xkOel~EFO#B^riu&5MeUF;x0fRe$6HNcWN=nb!4U2ie}f*-ZNoq`F?s*7pahTpOQuL% zlR2$S&-PMuj9__vNFHw6y97|ZSL}C1(>-{IO(+^P%a)BD>1uS{42_h3i6gag2Y9+@ z{^ocFoWpDBBW%v$!9s@}lxdo+uZ5_NL7v(9)Nm8<9Kj=>6W`R< z(eZRk>p5PE*zpQ3_7?_J5$0NGJ6ZYmBxNBr+9*eSHRpCu`z+W2RGyzS6Fn>DIPo{W zODCx>b*_}+cSWD{>yb!<6Y+*bmP+bh^@C+DZy=mX!p6JUg&;l7O6=68sXJ<&Hb7I1 z*Gez*I^$CDTy;vKtqJd}37pBrXE_|3Vy{&4v8XVT!)t?Ho1*+B0v&hYsAaajRN=O7 zCEy%7Kr3lw9}vv)M+_pF*)Q;m(c( zaannJi1oMNpT^1|t;O!s(egJK+A)%9=?G0{g+zi_CC0EL>r=IvD-tOlY+C#_rf~UO zGVcmC8h+t#CiqYF4kCuBa08(%rO-g2hs3{FBc9=~!<0xJgM16kSw_$^uY$ z%ovSILRGikO3vtc^##kczB(WQIt6m2J(6gLpcAwGYT0`3ia52kyhke%W8RA!xsZFC zCdXdU^9|vvy!a>lTS)*jih;_L*YKk$#akzv06zv?ed{SseF`NKer^yD5SI!{9jyx5 zYY^|$ddRHSC8Lh@PHeg$m3NVm9rxWno9)eZWF<+@dp2c$tO=EKOkQo^agg<$ccGC6 zrSJSRPECLpgTUrR*_rFpXh;6#8MGXLZp>F*fiCBJ1}8_##eA@ba6fU^MDs~)+kR6X z`lS&stfP4M=^WeUAlfYe%P@Ejv?Rr#5aye=0~&)%#04DqkZ{LtS_1`>Cm8S zoh1u2dkllP#*Cm<_3~ThCD@rmQhecA_F&2jdt|fbz7p@3=*!;!X`M5H0`Vg<6HOgr z3kyI2v00C?smH7`($Ipzu*TRB%W?xM5!n=DCrs)8mZ%$RQ}1&e^z_NYIUf2Gd}+^d z`4pgVoVQro!YEO>Q|*0gg&Cw!wnNQo8B)U-HLgfQR zIy^|CRlE6LQ;2y~AG4)ba2F%A8?u|#U|(+UMV9yH(RCS8vR!Uo)_=SSp6AW>79vYC zJ3KfE^7#ev5gNWA8b&7svp~*324!)bwiks^p(hk;?YPp`{7Vh^sfh?hkSzJoav(T) zkIwEuesqFkaXj{g81l%JZkvqWYvXY0-MgO}*D{_skO$ceEQB7lq>`yg;==PV4A}5G z2X!9)>UZ6mw{fdGrgiFxu_f~-(K=+1*Nq%>L3v!HKw$T!Q1<%(`a#pBg=7XF@NC@WT%=J(^irz1pNC$VYnhnrSDG{?z z;k+;SSd&-_^dlqGg%WVX*CQxhchiEfl~zG9W9fVP#kw9dO4XY#DGk$|3v|hB>53&l zM8T&|Yg2#uo$ncmSpfZPz4PL%Ok(g1>wq~8c=niV7}J!ZVEXw6WSxtMV2Uc!VENv$ zR{W37Dqy-!Ih9+Mu#iq{XC4{Xhe_PML>VLLe$TT9P|n<$x9E&=OtY=x2+JDR2m(Xc z(GW_I>x(Ucd(A_N)w0h3#_NFY3viPekd1GphMo%qOjE`qJo;Ve$D%E$WvfE!E1Z{z zm1kG)mi_-ki?c~^at)0zLs9&C|UD2`IPIxI8;qT|hX0 zx&A+33xa6|BErYPjS?)ryIUlY6ek9<{=-Xd`3NG)Vw~Xyui;T|HrhbWkBHvzT~bNYyF2g{Ar%$VCoCa7BmMZdkljJXa`%T6FPm=$KB#AAd4=kc* z5BV*BEk&R#I6h`xDv#;Md4H3Y2vMOd0Hzj-w%9=iL9z zS5$pL77RlhK>u<<{_XdFeDsS&(HE*S#@B%T?_V(liX+4SX)29by`LyQ8IGPWaA7%M zDtuQ6vhBP6&3Uf!Ad+;FHAN4QZI*&)D%Qn(W){n-{}f%_JYRA;nJn5ePy-l)y(bSS zK6wxA~Q^OT~kLIFSzfg|~6JOc80BMsreA50StP z0uPCU`Spg{j_9vQEZ9S$ztac5_ecgAK-E{!9*4uiOl%c`I`>Q6#DjudGeP{N>VH#U zeH;328IHxgAASJ0r|0)7`xrZzjUwBl)kXid!GXB+^cbJuFE!PL6^{mg=SR*gVF&a^Osi+m znRG!+)FD|!&;gXinTk@xm0v_oY+unE9E$#3`t({2v0dCl7F!0jM#Q^%9Zpa%Y_}3g&8#c&o4N zTCP{a3gf7;s$xd3b)po2B-qbXoJP}80As1`iYh0eaCsdjw=N;$5%s_sgFa-hw|=Ho z(0`2KN&!R)FN)^0$hQcfu|=j81#kUW=+X9HV5!T<$r|futPerM)@6-@!rVyNg$(He zEWnfo7>H*_gqkuyX$uF6Fb-3A4vc4LRRDLPM@vkfy^3{p<>+1F!nD3C1HvvQ!Frr2 z6_ci!14F%ToM|ztPV&NS;sK%WDwb!!^Ja`=-oYl1>Dyay)u>DUUbJ+YFHFkqfuFz^ z7v|?;hWS@6ssr56W)KiAwD{1GI1l|UaUV}F9Z#Wn55tq#QP(S>?tnCVWDZ3S+?9bsHC8kX1jvwCj>d)g%akDf(X!LE05;5YKX=` zZFFpi<-9+^Nh0ZwA_|J_!@U&UwtgfYp7RW*!D7iMjW{}>QA9gudSI@aF3-u{AhHUU zFLw!Q&bb?>5y-GS;FJ%u^M?L3=dm(g{sp6~boizCY53s+K9d9zRh#40R;n68>; z1Z%A!lJ5f|2SH-w_u;bssoSHp@vj5;g8?vZ^}N&py#OusInc@aKC+FJQH~wcisvrW z`9iZ7HmjuMHc*0YO2yv*?B}B;wBtWIVvXVr?62d8>Dy5j33Oe%oJW}j7u9OmXg<8H zslNFm%`(%)Z%pX$5nXR%;`d_oCP9#GglcB%X(hmZMM1)3qCoPA+s zw;5d7O&qT02XR}YpjD`2Bun&xAe^JDH(L@`*>W7kQSzpLIws?+HZC`aQAK-xeD(Ph(sv ztixzlUvJ1;yt?%-fN7Y-wH^o*0DHL(6a(uPiqZ_4&0t1%o;t=ijG`>J-hM6@R>*Bl zgudlETKfBav!ih3&N@R1wxT3h=E0;Sc&A~#F=I$01RnQ4kjt-%251K_9I4e6!9y2J zitSjY49Fsk3Yo^%2`j>VZcNF3*4ng&9D-D&MiW;(+)^e_Md?%zy=zWR9snH>T4aD= zK_n-uts{^vr?LvDq-05|MIPFgrnZ(PhI=R6;?Db-+3F#P`v>#>yT($X13?QWm(L_4 zYF>0n>a!9N3Z`UrQVHE+pF_pGF2#?M{!*gonL^*;-q8=`6qUmIkwj-+R(qaA#WSB_ z2`+1?75Tz$Zf%N=pu&)j;&)9BZ2)(wVe8IRPtN*u0iK~Aaua)*`I{qzHn4Q&>uJ8Z zKd>|a$tZ4)Ji=N9Bm5M?7z&>CgW;}#b$enK4g19v%+pGxz=Lx6n}uAa=j-pK8aH}l zib3G=X=~jJHwP(F`i8Wk(go1)jEjNYFg%5gELuJ3+TrEVd_ktjV4)bR`-|u|oX&=` zAU-{XYLTsx+plVfBkfkfE{7T25n3;2c$3g0#qaD%-i1-h@n^Pp!}2ckGh! zTx$ zx;vdi=go6cCpAjxhU)2U7zM;Moi%~u)Lu}LRDB*JuR6gYE7fQ%_ zuA!+!HR>u7#0ig3E5z1#W51c9o;TobFN%scYv7Hm!M#o9VNyoNr+wRy#$F^-7 z9oy-c9s6cxzL{^n-<`Sl{BfT1>|J}+s#@#RuJ>K5s%%QAkUUhX27mdsBMi$M}J&@7=?7B9MQnmW!Yu?Id%|~ic*fWB{ z8%Rry7M$m%wmgkCGz`{lgNV*9e1l|7q3oWh!1-$q%@)ev=T8ahzd;eJ#hM)>ExwsK zaKMuVlkRP!(m`M2xSXeEQb-Pj|4b~N32>d^6+tCMP2z(uMHxx0&c_l%;c!M%ui03& zP0h7mq;FA3cB^5>(aSq1jTMlccc^{HA;E4X$o+l2s5|_6&iWz#s3@w@)CebA%p`BI z{t@hFf_;X7oAG;mTDyl$2I)0fYy>MqJDFY zj|^}HQcMw62(zK+`2?y!4<(Tl0@d$FRN8F|#s2nYa;^mDyDH+jTUq9TD~VD_UX-KV zIS{aA=P())ugl7N=)*sE21?&wTad2qc6 zk8|K^ZTr{|F^S9aRPsivTO5!Ng!fpex9Fsc;P~1a8H@F<5m$v{SY7a`NJPN5s%U|h zoC1qcBtCNY(8fF2H-(fD+?WjWwS5}Qa1!i1hPtC(eS=w9TGjx%-vt(D$6 z)#wuoCZ>3-7~xkwM3*8nCd&R8u#u;ICa#60D8z-}KRL6{#4%78S{~IMZv^9+te@tc zwR(?qua;%31F>$?^lw&gbJ#loV*}9_t?>#cbSbtr^l$B1uw}G-m5r z%z*TGJ4cNevf=WPtqIj3UWu*gVPsA5W=QXu z1C-ccV)+Tes?4C;2$4zf0jYJdA6}~8GR~imt{P4V`86~Gs7A{@8-q(3RHs1yiyDek zp=IGiTd8D38R@=BXxG_K1}h^lY7?-yn~AE)(`sZ}g~?lttQpMR4IE#%z6#x>ME{uXXVs>hho|-jmVUvG6@(tYJAIiC&k;5*HMJmU ztXT5X@pNQTfmtAVoaB$thzpbPxF5s^5z6KN`o&tCBk+oP(hRR6J5x~%h#lQ9)Or4; z`t+-`Tk2&C@~8MlxXa6OUVHr+aRmrR{53Eg#ae%`&xn}FzU$QQgGm%psEfn6Fi*Dd zzYiQAxpluK_+5N#BckMxdGYYeV)PHT^#jahBSF4va+8Vmlw3VC!A$Pq{DCoULH9CK z+WwqRJ7%I>9*b2E{PCyv^U>NmPE#8=XSzV~4%5o`|6otDyX!u{_j9@Z+P~7B+1(1i z6-m(5%=qK4pj-Zdt36vYYwqAq{%fY+-t7rqeqlc?Va-YKkBtyfRQ?0!F}nEbR_1ND za;vUX5?FqJ>-wQn$2dXE4o!I1JnJ6)IaHw{nyQagbbs+;`XG&E0tkwvsyy~B#n1=(ul zME?io<^g1V?&WeVg%=r0;5xiU%`YlbyHUPNxSbmOw*(PeL_wk2Ah-4T|qi*G&sdIcIkRL$_%b-RWfv znAe(TSogHca&rBDJIF|ZfSYhXkYzlZ(NJe;Lyh6j?jR-0{yPu$rzni3kC)EiI&V4d z9|gO0fub-i7yv4N&LRKP_>bt$$AD>ev&Q(Rz&|DY$-!KfencRiOK;g%|BSqS@Od%a z3nyBd>KhPNuy_Ud40h%r?^0R*bzKwvfddzMDGno^U*bQ|wO5&lUP#9tSVPmdFcz1= z?3!I$*ZIZfMrSf*m#gk#dfp-Yj&wrm9IfgUWHg4V$SQR2AUE3b%QcZ?LL)KD9~Lt{ z)h4P=DT%fU;)U##rMEcY5)NEF3JyQS?RYtQryt6bHPJmC%cA@1 zM#BLHS~fcYrhS-Now7*p_CL&;krJQ(FRLIu7bczdh6k#+^i{n+Lu#6oMg$({?($p+ z@Q!(8ZtRv^cQ=J%`5n8Vdt~noS1j~hCDITg!Sy$MJ%f8@nzT-=6C#)-+8SG-1GfIe4d~x3*h9h6%2(MAnn`dpCFL7#Q-38n>QU% zw{4T#LWagrjWsE`{mC_z0UKXsMld7ZPm%G`Mq}_&R}1wUy;}$Nofupi$?s$~qZHI+ znvTLXH2h1?Jhph@kT3hvd2Jc^LGxxmZxgnG3)=#V3ke+i%IQe;7fte~+~2BO&T$wM z2y{vXdxh!Xz~`**oQNV-VZ*(Fo@~^j6S4plp~2!%c5N>j=@;I-kVS~=2r8Cfc|E7$ zaH-Sy3q^&uLc16(&$LJY0VGv(u)N-n@EedTev>d>h~)4tp>D#C?z1of>h9Uzq|_;) zzXU)o^mq3`mT+RI%n1Sv&I(PXNsb({B{>Fjax{jeOK$^z>~oYGI!OPOOj`4GbA))S(SXz&c9;iP z(X83$4C^km^8tNo^1$>1jRQ5+$RPU_BXfjg0uIZr{cBsU9d82C>w6N(2 zuz6nSjs!SdrMhfbgqF8M3`V%3yq<96;`Fl|1hILq*dm+k)*?LYtZ2;Hr5<=qW9J~- ztxGft-#aWKT~oAW2wP>ayezDELT zqPa(|^pn<0yCLuSpi#FbsHsS#J2r1eR){u{FZSyiQpzb=N{a!X1&q+(YL3n7=u$Ha zY^%;=KH%8k?n7fGw2+DS(;*w6nvk)$zJ*GhwX!sVQ#)&6*pD5?)z>!$j`>f;aJAB=qo0OvB@05hfpYKF&!Q0VTM_BScnR{7Cz* zP+Y2Vc^pfl3=DSw+wwzz4p{nqQx?mYf1w%(v9CvvtkC{!W zegGB_lFjjt$9BIx@&|jb=@rpAAAA%^X1om-d6;^+pFX^#$Ee|LT=I@rZ8ezbXRVH& z#5QCtQv|U}N3K4IzD>Dh_}HNfB1HPtefL^S3+ZkEK3%*?sAJHYp~VAww*T>pK?!(B zotDvSj&EE-JL>*UL~v&fBYwlaw5e$5v3^_zyAyv8~X$nuyc_)(Azes-Zdj%HS^;FSwXW?cp?ezKF}-+$glBZfp==n1!Dz=2WSB8D&hgz==BF=Sz_adl zKMib_2bOo(&Z@@VP{@~y@R8-kMH0*B>zvF{R2~UqU$bou^iH97meR}HmTw;u2|m5w zrVR5vOWlM4IijKz$80lW0m8$0qB^vQMNPc-SIYON8#|M3fMgcO-h7rpNQk8?eKbOA$2~hcwXKM z<{zMz0PKqZaBtrCXehc`y!8mMh6^K976s%%|k^-Cl%>o3_++oE5e&PO2Jd#Ua zJet!~br}yZQHO|R3sbguHn2%}%vZx&=AhJ2=jw6J@y+ByR=$(N`!$Cf7@e-S$t~9w zxw#(O-|1rht8feGn2!Z?>|Mke%z?q+Z7!z*d~%`Hm00vwQ_9cu6HM_)j%N%!T225x z{jB{&Y~9{$*28l(q@N@ukevtNP8!krCbDUReB6e;QBZ}dcNRD+2&R6Gm8>yjyuE>W z5DX^4__>m$q2*hh7;+MZLE_I_dn-J{m+PTy=-@dQ<5!37MgVZDXvCr7hg3d8gGk(I z0LU>{HyJTJk4dzF)SAUZNh3n*iz^_sb>ID6d1sV&DCckv>fOQze>VyhS4fF+1`+Uz zp|^tp86MF)!H&)R_{8E^QiE_{gJx7{a`4&X25%OE*7~#?*^K^WFRo2mJqFUWd}f)9 z4oU-lz3(|PW6PMfr=(s8XSE$}$Wb};jwe71S9=Kbzoogp&)ix+7*U_W$zy zGB@*y4ZM0gYUrMSA)9($Z4AdRs0s@{y&C!xK3sV<1*m78nSpXU?l-zqR5u$~!P*ME zeM8ee*(Voo!wHFk!T%-q32PrNHaWvNx-`43ZmT$j(_LgHAIb6TnQ@;Oy(?Vc1Wj-T z*3-`cizD6&yaNdl8HZQbP9&i@!iBJ&xE1TvxnUJJl+pe*CaZKbzoI6t1>+i3D=r$@ zUM|m7L~KR03JDj(zj%>o7u%R*a)Mo~Ek~TepIyq0Q59221$H$r&}jSM6dBB;Q}#XE z9|$qg=)7WRfj0@9j2aJzOdFfgu_IMJ+DUKb^MHq~OP`0fgmV#?ZaWLrdtjreD+N6k zOdQr8!+)ls&fD~5V|-~SwXKmQ+W*+@>YV3W;>I+C*#iFa;Hv%rxS z(#jDbnYSH=H2%p=TdO}f$Ea+J7?-415v6*Isz4%QC=+>U`2NHg(LIvrgOWHiryCm$Jf77B{dK9cXkLV-^RH(?1`Oqq z#Lj0C%v-No?QDyeLbqF97&U@^SK({AnN{od_hg4Uv}isd*FRI9h(g)RwH=@<_B;H5 z8YXw!xQqvynqqdP$TrZ?We$B8q{(7V+UeoBwE+_7PC~=+_iRw3^PV8HK@9?2D%2DP z12ygUkk$+MVJ7f_l@fhV(BO1i>$($ zd4*eN8b10a1I`$$Gs2$Q;)C2-@hR5_AkFuuQ8hNrJp(i#{7rN_&+lA8*QD-4u^R#L zA6ZR)$fzHf74D}c&27>HJ0lp+z5fjzwRs^wkeZV*O+bw>qdg{=w!d#GF^zhYUt%zE zeyNb*MyKyLhx?oul^~^HHr;LOu=QH^n)co1zHl-NX zUr6b#eaWhxDoT(nmQ&7%t0gQZp9(Aglil@Ryf1SAhCzW%l$<57P6Q>w#(@0uoORfE zvdM~5PbA6D>AXCMIJ%0y*zhD%wrnOO_(VHWL6>AL|~@N>P2wTngC;(_`mk#*F7> z{5ZD>$`Y$RPzlAHSaeO9>#9jKKPB8!dsbB_mLXNlwlIwZs zyBE)=UuxLZw5!tQix{&InnSsf7B!-Vc1r&f*Md-GbYZNFenLQ7eGP!;@KZ7yXrDk? zRwd@e^WP>iaUNvDw&fmxhFfs~G|jXMc+U>$LeC26c(&9~y`WG!gxM=gTG4r0vPc|NEh;y|FY@KmENB$gVOR<*{gfAL3g-{>kRT)4EHh6*Sf z(7mrjK2SxULCN=MFh#{_DSBPyl+8d z#j3!W2x9^EI8%od;TFMoW|~<6fVJHMuPtpGR!`%Hn`V zCMJ953*FqvL);$d+8sv}w@(l;iP&l0>L;4FY-&%N_fW5aNEIN#O7y_ncv!d%H|$U@ z7roy1emB$IplxCggVrh`h3+Rfdh>|$zce+?H zNNHy7xq%X_7OjCMz7sMRr*YLR%JwWH6vj~ryp8UBv}aGK`}VQkCzplML1fM;ap(LT zk&#AxG9mP&Nzf0ivX~kjX!#}B$nRP;40@+MiL0qU3}2&fP%*GSg(02O8D}gi$n=~p zM!6JL<{AU%YPMxn04Y%7UV*W;5ayI5R_0vb2hpL2j|$$^a(VgxP(9x!#YkJ;n z0osgkKk-m;53=12adgF}ectK>O_BagUd$MlTx$a*3Rz^}sq4&g@tdCh8lD)XlZXm~ zf|SRf{A)3bJ?&t$c20vA4+`Y*()%kw*hNkJa7-MMpLcED-_ayD(6#k-@b=CpessCx z+*w0@bq|B(qBGJL3;t@$;etlE6JD@3f;4{#2?5fuoMg^b;(%SAHH&%6vU^a1=zPg` zvw{Iz(sL5@u^QMurhmiVhme0G;gaH8FYf$YNw=J^P}T(ZkE#Y*Ys+`suFbI7HF-?0 zZN#Lx(r_IOqxf7gL-QteM@^RP{bNa13Ml-flbh4-e5;6^}wZ1*MiUP>vgz@WwN?AafPcp+`)$F0?lf5!oTm$ ze-E=PXSYoEf(#Cr*XZ9J-t^eo&;=d@G}TPnqojf&ncQzZ5AHH}2`QwjIf}&tY3z~2 z_I1{BJ;7T8MIU~D=-G_HI!K@po@e#ClG5V!@XGRfTSQC-;{SE?cbAi{ptbY{Oz@xcGCmr7erZ8aN zs*(uNg4K0bK9}<$&9`e2UuZTc;K#ia#7o0Ltg}`st-eg>(jac0g($1C%hc7P2SM_5G7sWn{?JKrWop z4`X+Qn^JG_2;4MWN49+WfhpM`=a+zW@0CNHxt?`ZZJW=XmItBubXtS)B=f*dl*U}Q zPsmIO2wb7}Y)?E;Pb1v@&83-QOU%y%cicZRx#WhNcc0DUtMf$^%5ONrwGL1&eJZfJ zU%R0m!VR>~7e1}0=hoO56*u$jk4-T5%Yj~Sg)%17(pwMSo8SvL%nqzOs5;kTE;N)v zM8C$aY%@UPgd^KQ@FNW=0mb*4s47}SGET_Xd>E;sWDXfnuH>2?6KCQYAMwqQByB0r z9|Iffv6d7jb4dIF{|yEOWc-Sq49e^?^_=Mvau%!`LMB?5em*3#M0tgr_S!3AS;snQ zn?F@Qw>0=#MUHo8-jmha3&Ls4ViK)$Y+EY@9 zg>>1zX`&q*zD=|LNm^5~&`ak&i8_rZHtQ2B(lvA%T5T>Bq{X7nHy8h>HSzxG40PKT z0~Kc5SbYo2=?kDZL>rnfh}qmK1~LY-kqZww$FwwS-Wt!lVmE;Dt|f3>bI!atp>h>b zz;B#61CJkjELa(Qdz3d98^w2&2qk)tq>(>T5Yg1F1^v)T2?Eu+psH0M3>1K4t54WJ zqXIrwFd?Cmd-g+goO%!zOjct)LK#94l^yJ4g>aB}s*0WIZnVZ@i2UI9Bx;M^BQA=; zDr;#%n4!AWo9{<4p4{Ol+gX?$_dt(z-DjE8>87eSA@QG1Z_=~dR%Qxsr)iB4J)68C$4@E z(N*mI^a=M#Oo(5xl?qhzM6Pdt;)CVKrP`DRv0^`kpO3yWn|ssv=C>6RmFFQ>zApx)PFHe9Od$ zJb@jDK$W`bACPKmL2759r7x>lg)5NaZGWw=)k=*U#gK#*<@l zs6na7nK4nPh+2ne%)J+g88*7HOI&R&^}W~u%K?>O^r;xu1ZnqLt>56qhB|3QbUbHT zkj91rh3YtFh#`_dgg>G>6uH4?1&T=TZnL&dhM7L5BQJ~YFp5$oUJQN zP&(@Rukv}c`*BGjSWf{>efPb|R<8`(&F<9!{t$to8X}e&>v+f|u%8p1*~8xHIQ=C8 zLD>tGRziL4MCPU)W00HP)ctDiu?+a>w~`-&?OKW@=gSexAFMgv7+8eehNw(8Tey`c z%0tW?oVT0z+fgws4jK(Er4&-smU6#l3_j@2v6q)ugYUkD4m-e#(Uj?r=L4DI*P`Q} zj=TX0E5qrG$|>JKD>F6bOJT!r)UBf=Dk8QdlnVOzZcKDaF*T5JX^2qIK>?I?H((5?@< z??;Fc9&JczR&c#7G!TwfKO=$9>T^s7r#DSo)un}D&y(*15*fps`aPAR0$V% zpRDZQUxeWv`ccijB5E_!MUmr9-kD>^kOuP3BLvBUs_@$doR1W1jWhCrwb2zp zE3BR5QD)mtNXHR6P`$kXTnR9_kh8sPGWenPEou_D*pF%>FLvJM5N>yGTE>2)9YXI0 zElJrPYXbO=r04MPxqetOGksX)X?7ner)&IhVSE*i3aaz>eLC99*P@Bob}w9SRNgdG zaw})AJ1*r;A`-A#+1t#aV=8jtEz3zb4E^S~Pj)y$tKJdwR#-Wc-i(8_;-@0zDwkno z#$k$!>95#tZ#Vf|i=5g#Wm6m=`6 znE3$)V2#QlS596`%fuzn7tt`s``3jBq{Mba*4G@ALg(sfD2Vdv>h9+?%3_k}Hro0< zvJ2TeGL+V|e~SUSssXq1t2yD$0j-$84VE^sQ4l=WVvW6iq|ZEo=Eo${7pY~3wS)~> zBoXsFWW_&xB|yUOqcX7}wtlQXWe((Du=SkK#%+_ef%EhPi6)cb z+XHYd`8@mBnp@`936dtbuo3R%f$bzwc&q>L+s;CzXaLsE;~A3CMvDn0h*wks6lsJM ziW=@n_#tC^TSd~m_hDSFW18yctCc`Ti_h1yFp!`o>(f*}J1|CBCR#j@GT%#7KB*&n z=s|$5A^G`vex?$J?u7w07@#{l*4K5wPEmIptlZ?}T^9GUI-c+O$U}M}Rf3Aa5m~_tUnF{qUyfRI|SUK`(- zZ`MQ4kw`4AWV4#7$Pa7|zBo1190g<^o;?>0k5VsT%8wzzz&+!;X zUrYTwgRRin4#qS=1eigI-@fIio-3gkD1T&Pq&7jBMuXbux!UZXok6GVF2_DbAwjX5 z-tlwVsmcyUYIY}wo8-M4s$PuR>7N#oY6DP0K~9tCgwXu#hPY(I44%;Xo->`((VXBF zC3g0O!Oh;sG2{ipz`KL$vwfXQDP&*OCj9_fEMa-3;?YWQky-8V+>q&=`O5oGa4Y*~ z1{5>Qr|5umDL7udFnVekCuomYJim|K2@KwyT^ubIf&0ijBc|`~4Nl|2A!*LrQRu+9 z`x&>q0O{>W)v<7`{k|Drf7$oSKDxHCB#zmwkKg0qXd9sNfM%9 z0u*jh%Bissb9cNNz<+(yNl!ueHJOruJEJ5`Np3j004!{hO)#*WhWi4rL-&>2A)Gp5 z?Bm@}3>bzQEiJqM(xZBN_mOL0Mqa0mN})*rQ!*4qWYEo!qI3qbxPBJn?98jrX4Sn5 zv)y9zFv>vk(h4+UM%sjrSQvx?gw#om}crDv-7;f)`-Mw9g5W`@qad$3sdH18r z6-N=!2-4b@dxxlq=K_7-#XW_99Owd>ESo(Cnf_+QqpflB?Mes1Z{mqG#YZOW&Y)1& z@CQAH6W@WeAvbN@aDPR*`+k`TW?Y`rY2?gKayt<=6eb<+1n{adf$>O<=G@aDKirIJACpcyLnHXZ z^%k_fw;g`cX<>-$1Up8^k$S(P@6Ft^^an7F&B9uSXrlz3<)vEse@_?7UcH{@tyHQ~ zfV)FltU0%2?|&B zTs0J*+@5nh7aFVWQx9AH$4Q-~v0A!N_y}negYv0R}%`kSYY#m<#+Cun-B^)IZDBIYw~ zYwewPfHWRU6^A80P!EMIY-n#S*YZjMGF3PP!TuN1ALZ&wbFpMwqC)3^)7hPgys*#ZKhphS`u;Tj!A?1i1z z{7k9YnZfZG9zWCXdK7om>)sU^25*a_jdy}OqW0fZT`A=*4x|Hc=G7i^H=kOR_>?sX zZs5SQZ_u{{qRO7#iM*C+XEgJ$J3~ng%n5aOZBm?+BeI?iQy&D8Ld$^E4&}zcl0>YH zqHXp3!87S%f-(ozPidxdrwrDC9o~|9)(iX(Wqc+O$w!M3HD%1h2l13+zM1-r`!&;b zDbE5URE6Eg3v4Wex%<#Js{nRD1P~3Cup_C(>=?P?)zl{eQn<6W)Uq!z*`f@^9bjXu zj!~`;hkY2LD=J^5S`gYDY7Hk&w+lm&52R&m%=vI(JHKMeqN#i=z{MS)i7&3lBNe0BRLbP1lT}0ABWwE$|+_ zv+ma%E?(Cz9z7EnE!yFw)A9v(jVad6J1>RUl6)j57de^YDXpWTI+&zG@hw?&HYbvM zA+X1ukFzvwG)%yQx~(W>8*|NraOUX_k}0MT6DgMC$dNfp*TD+X3P57T4hYc>gpgC&-Nj zkxFDB!t%V!vI1C^$M5`=l4-3d<&-O+#F3kOJXdN?)q4OfqSe=UxBH!$kyTDk>*?r~ z=e|rW(pHJZYN29PmiQ}A0eWuy&AB8KQ*E}M@#%aooXLE)g2-)&n)c6E5XIfheXO_Y z5xMUMu`McfDtL_ZiES2|*HoowSlO4CkSy*GdJSw~+P`!p2hMjXYr#a)h=eD@tAc^c z)~XIP6vtWWCUU+#^od7TR#>y7GgV4_v{e9eg8piyU(;NgK=&OLcjD zz&vz}42Cc()H?(Zj+>^lRyRx10RE`BN@5e(LX=B8vO6p6v`vfi;jiN~6AU_F!9vH= znHh7`4E#(BIQDXuRPRXW^$V>ob!d37N?Dy!Qa`P9z(BtpzOkOoH>FYc`KvQWbIsho zt_`RoZOHeXUv&~S9}R9;TkbG$tVQp0c4Zv2jL3KnS{oM^Qoa3P#GR-vcHiAU-fV6? z{+g^3$mb)hQc~g@4)QOL7C9*xREtVBAelS4?>{7aN8(gHX4o73uE zy6y?CSJ#GTOBoiKcZJx=Bt`kJ3&pmuYkK+lv4e~jZ{Y)OgSoD$4s+RQ8N!_D<2OT# z24>+4v~u298$OzMKlHOlT^a6z{Bk~SSDOeTDVAM^E|{htIy{ZSge(_wP404GPDL50&eIQsf_8SCk3BPjnO{0T#ee-tPO1f~%Ms2*TGoE7d_NY& zodi{y%gduYwdXsoLo%iAQuz_K8f_qY0*bWOkx~kHMYXmsk2;=}C4|CmIFyf>wLDpH zII2L?S}hfvCvcGHa>H#nD<^E2Y1lc@q6=_Bcb98^wY++tugC2?epJNf#zHCH^3|QU zSmDpZmMBZMYH`hAq2%yhD~J1Oe%vzYGE(bS*Ew=<9(d~AynD!AmN>?GqFS+#wp5pS z{QbTLIwz;K&0Fx+3M1`=^GV|pc(QEM+WW{yMwoj^Ng;SaX*BF-n#Ym`ORbL2K0TR0 z?Bq-JesZ3Tina16cLj5!8L1LH%fXD`+0Bfg_LKGP-RX6C_hOBkjd#6WW(u(T!#R{@$3csn6l8tMPt8I zS{=2hSEglU;WU5Pi zs<00zj-nl$ND6EGgQMgIlBR#T{W1gd?l}JyW6z_J0t}JKGwE}>4A80T(n;qmIc?+a z@Z8(BDy`6283*q_ap6Q0*2@ag?a`d*S*ELipntp4U}izRA`>y2UYWj& zX6F3&^SegnGi_Fhj(F^&RFR8Cc9b<}7AJ$rg7mqlEbqDEMM>EQ)3;QuDjey;&Ma<) zDs?W?%S$~hrMq@y=a+0TCRPehKItCeGH-2W1;NRNnK~=H#d7aXDhuNt(!q=A2(`y8 z!>Z3f%NLz<*o<1td-II$tW1JMagusDN1GImvX=uHhF}CJWWCA-T8pl#Y)` zDqi#x(L|V?=Bvv+c5d#Kkg%R_WGfo;&EFD6++WF6>N>}1W+Ek+ANuy)JH1S#o&&l@ zjWeb*ZzWhPulYITeHL`cRGFKHh|?Yxqo8j`6?e{Y-LLVf0pH7hHI7Ycj)64xJ(-Sv zNo48iU3_0q>{O#%X?)XoJqRAf(mU^`J>6k%(orb1ofzZ#xL@^T1D-V-!VfmGh5pX$l)TO6|++>zq^qMdqnI$boo>ikWW-B zZ(!CYzISALG)7WEq~fI9N#e>#&#aI#-A?`za*(JGK4v!0CnI!8*;L^?41lo59~dXn zc#3UJ0k)_-Z)_yB{4w_+j>U2(HxJ9)!iVB)_De$opwSI`wHZ|)Iu#tT?ryW7+_90@ z#A9PHn@Ngac5gEI4L%wD88^p#v94iI@g_04B@Rbyt%~$w$Xxr*ak#hroUT5j(;+;` z1YU8|H;cu6H&vo(a}}X-R_7q_NX9Gp zcYRk2fwZHH#XQvW#7eK#U1ojbbbFZ<$CDOEw6~|vQnU~}c~`MKwV7Xvf6f{oXMLwb z#dVwq0XXvxQ4VxI*OW&$%xuN>-3C!wI#E$>swXwhaKIadDPk>M#>$s$-ihN=wDE8Y&x2 zA^TNtqhS9Y_$%Y7^rZv-WY1yU$u&IdoZ^BsYdVXnw4})rdt>emsYX7TP3g8^@Yi_6 zY>qU_-s6;Nyx>BvLwp;x_aNqD7puZ_22uW5eEogN(I|~+;}RU}_9bKjnV&i~+fS~w zClKuk7j9~#=2y@!^M*e#I(}L}J4&s%Cw`hdWBZkh85^w62|x4l)nm@(MP=nD6`c+< zL|$SqbPE1lTco?3VDyyy>d~{-UW4OB%T4LDa`=#t`Tn6#Ih~$)xYlFJa<@{u2is5-B&^jgB0a-JlymEmGk;%5;3+(mF|ssmYY#;F-+ohfP0()Tme zY+gtL*%#pbg-LCC_~}be>+q%`>xJQWwNkjsLR>VTi?BUNBXgIzot#_Lgac`*?~UUg zGE-iO_Ha!V=wiuhHvOryGm7#u%UOljJT4Z!{gdW#Q$;J|v{@yu&t$j$&$P*vwDf3$ zlv)9uw_iP4zAY*}O**&%(oZ}?EXJLHwOc&wI__0psE;Rg+oH0hyWBS0!W82M5&5>1 z(T5N6EH((;Qro!}c89Kcy0JhJn}LAgGjzCgC=*vls+{Xb*mYxHZwL#jbaD+83aQr> zn?c(V<5N85-PUqQf<>PW zC<1X?jd~|sC%^8|i$5FI#u*^2`!d>#f+E_IZh6W$H+%zsI&uvg;To}j1>R2@=Rn!~ zk$z%xRA@zLF?3+gws-x#(I=VH7)tSE@}7#oJK0^T5+<-O`@^e13pqL`9v9iQTW{i%8VccV`DyNs*@ zJ&iR8ab_R4B#F@Ya?blIJWfOpBij8ht98IYpw=nA*WAnVGnbVZwC_1vs|K29Q|_SF z(m?U|Kd1#{c-;tHa+^jl-D!?|(cU~_-`I-2JWedzxd(pwP(c<4xbfOvvA8Ig}DB{}?ySVRg! zAmCb3&5-4S=^%d>_P5saK|vwL&=Q)$|KT!!x!7Mt5MzGg{F{-*ML&!T#qWUe zk1l}7_hI6L|F^_Hx@z;Mei=RH4ZDOHs0@%Pun$16JEc*niSwKl(#DBzvj=}iPc1NxA zE&IJDW7jm1f39eTa7#K3SRO4Q%E3>L=r6nd-O>0MVVdI$x5BJG&aBBxc^3K?g+8AB cy7dmka&6Ct2eNAY>Ek0NEG<+ipzHtt0I>BVZ2$lO literal 0 HcmV?d00001 diff --git a/Docs/images/GinanWorkshop2025WindowsSetUp-TN2.png b/Docs/images/GinanWorkshop2025WindowsSetUp-TN2.png new file mode 100644 index 0000000000000000000000000000000000000000..b1c8d27166e7114abee9f41c544ddbbe6dd27a0a GIT binary patch literal 31994 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 literal 0 HcmV?d00001 diff --git a/Docs/images/ICRF-75pc-20250121.png b/Docs/images/ICRF-75pc-20250121.png new file mode 100644 index 0000000000000000000000000000000000000000..d8d24d8f25743e5d42af9150a78879044a480712 GIT binary patch literal 131649 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 literal 0 HcmV?d00001 diff --git a/Docs/images/ICRF-75pc.png b/Docs/images/ICRF-75pc.png deleted file mode 100644 index dc91848cd24be1c7168a5cbe93ef9cfbf5fed328..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 190887 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�ZkQWk2HUZd~^B6Dj) z>f9!ag-3O0_GJEI2?#&(He&NfVukhl{y%xdPkD7HYfPgV-OR%ON}nu)ml> z8%a1e$v0X3?`moN!2KKZu|4KD=RYr}_RYRIm`L(4O(~;bNaEKUy3tFDsr}@X>s4%j zO^?!N)xJm$330K?;}ZwrBE?adxBUEhRqiv*mP0Yc3wBx`?Hr^Edb%H_r>7${E^j;} z@mhFM!ZAHl=J@T&bDur62Q|J6@DcI~3dYhtT3A@Xx1C+dPo7{~`V9kKd-o=o#qew9 z_go*J)yI@{n{PUaa#mM)Wu<_CK$5n=8x!F$Mt1fOFFuz2o}IG6i~?NXGt0`#c&S*z z$8SkK=jP^yQt{?l9*tOd{#Ar%&g@h_b?SvlNe*l;BE$*}ZyERtG#R7KM~VY@$TfW* zSV9>$y$wejSF~TXj|{mN)TcbKpaMKdH1Y;MzC1T4C?diP;@G<6E9w~tg(B6jgfqd@ z@^h{(4?&bY@|2_A-`#varl*B97!#BTPqMSmq{+*Qh(tt3@&G-K5DTjB+#G-g-VXj< z-Zd64dTnfM7F%9FiYh3u!-D}K7*~;fSmvuQ{Yf@m$S`QW#6}7jwqhNYb zy+AY)BGjQFg%ESqKdZ{@#fgIl4>lS5`qo^LlXlqAwpoY46%-|}Ob;hK%@p+6Cw(Ci z-`e})qEY%O5AlWe^r(AUR8ybFKh!If)wrlI;LSmFcJ}O96q9GqGBg=8pTvQ6_F&5B z(#R7L5m9m}{nFpE{G(1duCZDodtLK&_7e%)W6l+Itjvu4{?pSh-?Q3eQ8xv)UDv3+ z@4famKjP5j(cIKx$6xF!QH-BBF(a}*PJe07XF*?!% z2n%W?|L7j`*eCbyQEh$g?KR5%G$=_HK|c#k5{QY$#xN?!--mR-?B!;rI%=Ngf^^lX$Lk(N{1NkI=-U;O(lT(~cRN zB&F9$7X!rvJ{DWNm~1yDICm(l2!?go{s&yQoHSo9}bwIwYBvb0uS19xTl7S2?RfQ_^|ZRl^M66BQ{EX zIZM6=gICwqWX*G|OdM;(lkG;%4J5wo>G^DQo_$c^KCgJ@K)m9Tx9{1rDCu7$Bp3nd z_4qbc$#XJBDCWXRj)za$&R`@0F>hO8a{9jU-Po3zhUCvs)|WCT zCMKedLcfMOW%77preKP+q*F>t3bC`$A%}XCh5%5os^{eiRn?m}Z)$33Az$LlzrZ@8 zXu_KuRj{{wq3(^=e+M<5mBRp{F+4Oh6kQuCs0GxLTnW=~e1RcSVto9U%U7SaKlZ0e(f!88o{-_JvS%M@WTxmsYeAV4}5A4>>uw7y+T?0XXLM6^<-@+?WyNBSNs!cFpDD@8h1Y)C<}^zDcUyR&uIY{k)J}QX+8# zKhXdD-OrnRYU6S)H`qm=-dIjx7v7Ux@whTTFLNcrmAWE9DMv&Kz)Q=Af+3fm3!jcU zL{+bhUt%(>uJXnV!N-Fg;}0J6UVkMc+&ZW1-?4M$RwUQRkaRo4NY>KgqSootoZ?pR z;bjc3S=dm~zoK%exIjfmJu_5bbk%E3rPMe%Y*;_;9c$KK^>0Ni7rD}h;SGd}H%-ak zLhk{}Iik~3+4K*?bjWsb$RPuUlu;7QRY6as-#>3n++87$L!*(kwH>%04#RK$VW=$? zr9HMwgolTdWWZbxj}rKn4jlB}+O5V5#^u&0b3fhCoklHn^wYN_khAP1on@NaUPSDR_D^n-=Wyl+&zEqJZYX6!?)XoShhfj|$jLeawMo>6l=4{{TgucY{J$>yK&)cA< z0>0{nfK!HD7!0w9G-+v$;)%};XB@a`4dQk$^Q!UbEbDMCFfI$9S>&f!+ap6D*yzbh-Js1Qy=Gf_u5 zlzjE1un9g3Fr3IJ#zc3DS)0Urb(AG#@KPa_vW`_&%4tG120j+mtYJYCWkb- z{+5>fMo%jfIw~sB?S6ddAvl9Que&=ddQfvsfHZ0(?VY^I=8S;s=MnX&PEn^CkUfBpdM zQlpw*S&?RsOu2TWMfzf}^o5YN1Ir2yYI;A`gdvTfq1fRxTb|1jp8fLWeX%dzSD(^n zDUp3sdtEhKlxSw*H_@E_eRh_Il@(a$UN*KVXX6L_8hQ^fx_!v#NZH~C_Mt*=-KW(P zW^xM_k3<=+b69>=e=kH5NeCvb`8xgci``t_jcAWQ-?W&I#W$_H9E|o^`#K!H@I3CK z{Vp0Vr8g_ZS9M58s<02_Th+?m^7|ga z^$l;DKmC^$%Kwb=De6!_S{Scq`|86f-8~ZzM`EvAKdc*lc^-Tl5KZu305!E@>!<&& z_a+?X`8Rjb!pm`V;^>!xf(wmKr~c!DbO~Kwug)3hUep;%_mEoJFarhO*VmVqCkS*X zZHt@FN-7!c8aq4twD3Zf{Busl3oc@N)6&WI&)&I`FtX$y^tSHfo-koQOIF>sTMCbR z>qa^SO^UNGo>oYCHCE*$HSk<^$Rd`Ez@DI-Ak|fR=UPB;#*r+Nos9`w`+NQgDpjvW zsW5SCYX4&7=Kjvb`GcIwgu|Xqc6=@8?v=@zu!4I|98Q0B)?GXXku0$OPu<;qO)^2M zCu(J#JI|e!@m=6IxOBVn8%lt&@sH?rj+(Kau_fRxR##U$JI_X{ zXZ2jKc%6k-OhTfh`kCa>+Pa;+Z;mrn530PlO8YqJBktV23#-mm z?6W`yk~ES;vw*h}Ska`X!iHEbPkiaeMv2nGpX zrvSr}kvZG!Pi`|bGD<+4iI1mvUCjO4L1FBxQh41t^3uqW?=+^nMeiE0IZw#l=NZ+L z@O-x_#$-bgQdatv{l&$Kxvxs`78RaJLpf)wh6%^K^4q!Vsjj%R>O)`rdXEKw6!DZEl0dw@@sr(X=y<^?&vrwEPV4=-6wjLmq(xVlkY95J4ce;<9v*V zF!H%H-T5TF)3%TV%b@hCN+bwV{Z~`NJe-@38$70WsV)`9aj?9Sv(XjaPGL@%>J77b zGk1RDbUH~*KmBz7rOzYn=*HAftuIAvaCA#P^ zZH4%o$Nm*pzCAA)k`b0M)iyCnxlYR|GY0}hAQ-oC^Rvq^Hl3j?Nk`8p)qWK`EXp(| zU}7IXZp01d5Pb>o(*Yq(v_GmY3BVK(9kFt7%;Z1>jIJFuP)E8_Eub&R zBktb4OVE|`W&Rr-@*w=IDLIM7Sv~69=c<=vjrm{xQ(JkgbK#bfn2knAxtiwb>8=_( z!|K@78}q$sS+|;_fZN%cn?HBzvutnQTDqt2drhvWhiykCMPrmZ_0RWY7p^pR2(@1+ zifJ8KIQmBM#=Pd^E{#d6&h?GT15I~UezM1~?v0I4tJ&@hb(!pNQACjhl)~BB88CsG z8kz3ph0gua5k9u96SNmOzm@I%?87OXG+Oq)JoWzl`wt#q3KYa`Qo^Z!V#x-co~tWQ z^tKMnEiYS?UOx|(2rU?T4}Xo!q`JB~ti}rt4&MCC1Y0Z=*L5zZP>%2)I51xwh?m0a z5A8Kb@sAF;s^H10y_bZQ^v|7>M44(9T9%o)@5m8HAd&JaU0m&Fc94>q_CBoI0|%KA>RCk+2%S=7 z-fn^Ar#)RojjtM)(#CIbL~V;Ksa5IhEScCoESEz~*!1Y1v{N!qN^l$RIB3zRAH<+; ze4nGByxQO0=oXy?_i=}|O(W?S79KC3R`N-{w`-kS<=RgB*HXnayVL!QthW_VL-1!h zXU@Q>0xf5BL667F`2I57OVI4i07WR`2wd*^nHMQts~%q|M}A=7Z^;# z85dP=XSA&JzJ1@Zjt!j3_tGy;M~VLNU|?WN`u1{>{NK(KAp2lyoPI5mxbFkz1W4h2 z)1lD{%f`Sc@b|Ti$lTn6!)SyH6B2rfyAUvK-NbrDL_)#^UTt zE{42#o$Xh(@;0=@2+BM3PenKleSOBMI+K%=3I4yYuO~{hT8~vY^jv-My==b*)J~vt zhQZO`GLVd*t@f_jLYs9Uc#aFCvkfNVV5SIrjdw)s3i2<)D$sQZ=B+!@evgh8dCq?a zCF=){0PQ&OQdPN5T`n~xwoF7rdIUtGW)eMTy7#nDQ2iW)!l12ZA=}zTLoQTz*@kk&PEI z5x>^We~KQ#2{(vH>;bYSvHXwCTbQ56YJiYT!T;EW{AbU8RD8T(ZVs10BowbPbUXmT zER5Qb$v7ZDW(w0XqCHbJf3$V1K^X$=c^Fb>FxfxLb8uOaq(_DrPt#j~eR zWM2AChHGqJ+e_ikM&Lej)`FU2rJ^gFSx2xUM--1Q)W$H~BD&Kfjeu@t!LACB#{T}>;G1x@ z-~bpOA5UEB5Oo_A&=D!AWw@u0JXBwmC|WuoFE9TG=nweg6+C4C_K5wEq-`h2ZjuoM zF)n$}m&mRVA|Q;NIB^0=Ohy;Zy%$j#;`=xl8Q(_#8m@)Jz#rm+Wz}H<&XR|RM@LJ` zXttK4s^v*B;Qg3H?tL>da<$>2=O}sbS*%0BkD{KxnWE_X4ShC>icPySXU@2~{soE! zW(4ULCX+*8FWMKOHDTiB{`2F9B#3+vZopk|X|YP^2z&yFzJ>2XnNUb$)e0*EgDAFi zG*65&d?vFngxypw0+i-nvpI5-#=8!OrbWc3tPp-D;8*RK$Aw}CvuE(KP|0&vxdJB?6( z;i!hd$f1@sJOc$3J=`19QRERZok1>u9V{gh2Fk zLzF*Q+2Dqd{^?mxo}0($Z)%O$Q&=SvMiRuIET=N%Zn@pysI5zSx^VPM^-8YIY3T>b zYAlizgt|=bJRh_5&GmklF&m%tO%Aiy+bdY97rE17q4j;%@wAz*$IJ`r4GWg$>QC(8 z@ezHzT^C{3i;V+;N>ISzU8GT+`*$*LwMWdp?QoT9 zpjlsJe2Uiq&4w(2o|?J}S~FBkxVR*BDrX72GBTd95y$Tqh$C7~%_88U_MOq<7Z8As z*AdrIeut*BGZIn=Lg*F3BPD-7sK-J5=V&M11VLI01tZaK1?dNM9)X~q)deXZ4%$Jq zJ;W`Q5!?!u85z7{VlniV*yA-dH3e)@ewzz{9}lrNOx6kaA3gGf;Nimu&A1k5kg5Tk zf-uBdta3y<KkUV?&-dqIim^m!~HLL9pOgR8a86 zuuOFuo>CNr@07N&(5XOgX3SY!}zlgW@}KqL}u}X z{HS;b6W0sep}v0xWc{L(_Ve=c9yo9ny$}eZ6iY4z@4q-CM6)YEwZv*#Dj6ctRHf-k-pPWkSEhDc=V!iDo@EQ%MRhOWm%`h-WK{f@ z$cxj5c{(;G&X{Coc1sN_ZH_Ds-0G-U85p>g2JrV(gB;mywH;)t7FS3~Wvz|E-)r`b zQx9mxuH2aEQ2x7k_)fD9SIX=}-p1h2zopbcJ1GYW0&ple%e$lDCzAhYG~t`^dwN=v zpYdvKxW9zrohwc_BhZ^iD{Zl|vU;KuKmcoMZeCpTK@fs^8&3*yCwFuLwkc!fC@j_P z=;Ur=77uEHctCsPd7i02}@H?;jSJ5C}&S;4FVvZC`(X zKd2O96*e*PrL&XRJ!-PsU%$SKn%b=BawCotqOrrJOVG^v(+J#F=6!b=yRpbqxZ>+A ze~gdI%E_^_vfA3(zIy%o4IcZg&9ym7!IYraMMb*_o&EjcYBDoYy?Ev=8}l8D92_g-^-{kN8)faVu4QG>4RIz%FjCbrkF zzx^6RMoN<0Ccqf#oYVp|x;!E#CMGZt{FnGM4mWrA>({P9Ge*xlUmJazioG}ng^L z?-T`P+YM~a^C{~~cWb7T{?@D7aJ^Il-FJ8U+43g&k`_rdR zox-DphwJUzK+SC2ZPVo)F+v(Vd(ry&;ie%xq5bPfku@+hBtq2s`hoS9@QWc3COgt! z6c(l;O`>{5v2Y?=KRd{`UTlj9ZhZZ^GhGre7$mH8QV$!p-}*btq@a}OcqDd0R~I&} zZcv)TSTV+R4Q-tD8`M4{OFsva+(EU`0+O1VE==`TTi%UEK(=E!nl}Lu+davY2ovM_wK6Q zYs9t@FlYF!K(VmW0*^EW1qJi2#=broA));2?2{r@TgyiqPL++bv$3I_Vc9Y}e|}G> znm8*BRUmq$7ORD4CMA`XmEeAF$e4raPt{QZW`N3>m#X{Ir=UI5t8b0$><)!DGKQ+5 zXbOvn5IJ-x&ND88fE}BtiJ|}eHB_?!i7bBkTz+C^=6CeyJDh4jT;nOVm#Tu#s3bpl zoW{#VU}DnJhVT?-mo-I2(aCc=SzA*OoZa2i(rn6M_f~Eh=1gv5k!Dj%t1J >!uC8PR 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%0DGRO^`Wq&sfS{G0;d2_J@|ehI<9b?=47Q4;^j4i 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

    HICPf&{_QJwv4y;!APd1vtXkvkeFOl# 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

    ^zrT-+*G25lhi)%3Amt8COMv6W?;tve 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 literal 0 HcmV?d00001 diff --git a/Docs/scripts.index b/Docs/scripts.index index 8d961183a..6efe0afc3 100644 --- a/Docs/scripts.index +++ b/Docs/scripts.index @@ -1,4 +1,3 @@ ginanEDA.md autoDownload.md - s3_filehandler.md - flexPower.md \ No newline at end of file + s3_filehandler.md \ No newline at end of file diff --git a/Docs/theory.md b/Docs/theory.md index b7a55c597..b5d3334d4 100644 --- a/Docs/theory.md +++ b/Docs/theory.md @@ -14,7 +14,7 @@ Well fortunately for the vast majority of applications we only need to know wher ## The International Celestial Reference Frame (ICRF) -![The International Celestial Reference Frame (ICRF)](images/ICRF-75pc.png) +![The International Celestial Reference Frame (ICRF)](images/ICRF-75pc-20250121.png) *The International Celestial Reference Frame (ICRF). J2000.0 is a standard Julian equinox and epoch - January 1, 2000 at 12:00 TT.* diff --git a/README.md b/README.md index ff5818f7f..e0ebba329 100755 --- a/README.md +++ b/README.md @@ -1,274 +1,465 @@ -# ![gn_logo](https://raw.githubusercontent.com/GeoscienceAustralia/ginan/gh-pages/images/GinanLogo273-with-background.png) +# ![Ginan Logo](https://raw.githubusercontent.com/GeoscienceAustralia/ginan/gh-pages/images/GinanLogo273-with-background.png) -# Ginan: Software toolkit and service +# Ginan: GNSS Analysis Software Toolkit -#### `Ginan v3.1.0` +[![Version](https://img.shields.io/badge/version-v4.0.0-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) + +**Ginan** is a powerful, open-source software toolkit for processing Global Navigation Satellite System (GNSS) observations for geodetic applications. Developed by Geoscience Australia, Ginan provides state-of-the-art capabilities for precise positioning, orbit determination, and atmospheric modeling. + +## How to cite + +If you use Ginan in a publication, please cite: +``` +McClusky, Simon; Hammond, Aaron; Maj, Ronald; Allgeyer, Sébastien; Harima, Ken; Yeo, Mark; Du, Eugene; Riddell, Anna, "Precise Point Positioning with Ginan: Geoscience Australia’s Open-Source GNSS Analysis Centre Software," Proceedings of the ION 2024 Pacific PNT Meeting, Honolulu, Hawaii, April 2024, pp. 248-280. https://doi.org/10.33012/2024.19598 +``` + +## Table of Contents + +- [Quick Start](#quick-start) +- [Overview](#overview) + - [How to cite](#how-to-cite) + - [Supported GNSS Constellations](#supported-gnss-constellations) + - [Key Features and Capabilities](#key-features-and-capabilities) + - [Architecture](#architecture) +- [Installation](#installation) + - [Using Ginan with Docker](#using-ginan-with-docker) + - [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) + - [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) + - [Running Your First Example](#running-your-first-example) + - [Adding Ginan to PATH](#adding-ginan-to-path) +- [Additional Tools and Scripts](#additional-tools-and-scripts) +- [Documentation](#documentation) + - [User Documentation](#user-documentation) + - [Developer Documentation](#developer-documentation) + - [Generating Code Documentation](#generating-code-documentation) +- [Contributing](#contributing) + - [Reporting Issues](#reporting-issues) + - [Contributing Code](#contributing-code) + - [Development Setup](#development-setup) +- [Support](#support) + - [Getting Help](#getting-help) +- [License](#license) + - [Third-Party Components](#third-party-components) +- [Acknowledgements](#acknowledgements) + +## Quick Start + +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.0.0 bash + +# Verify installation +pea --help + +# Run a basic example +cd /ginan/exampleConfigs +pea --config ppp_example.yaml +``` ## Overview -Ginan is a processing package being developed to process GNSS observations for geodetic applications. +Ginan is a comprehensive processing package for GNSS observations in geodetic applications, supporting multiple satellite constellations and providing advanced analysis capabilities. + -We currently support the processing of: -* the United States' Global Positioning System (**GPS**); -* the European Union's Galileo system (**Galileo**); -* the Russian GLONASS system (**GLONASS**)\*; -* the Chinese Navigation Satellite System (**BeiDou**)\*; -* the Japanese QZSS develop system (**QZSS**)\*. +### Supported GNSS Constellations -We are actively developing Ginan to have the following capabilities and features: +We currently support processing of: -* Precise Orbit & Clock determination of GNSS satellites (GNSS POD); -* Precise Point Positioning (PPP) of GNSS stations in network and individual mode; -* Real-Time corrections for PPP users; -* Analyse full, single and multi-frequency, multi-GNSS data; -* Delivering atmospheric products such as ionosphere and troposphere models; -* Servicing a wide range of users and receiver types; -* Delivering outputs usable and accessible by non-experts; -* Providing both a real-time and off-line processing capability; -* Delivering both position and integrity information; -* Routinely produce IGS final, rapid, ultra-rapid and real-time (RT) products; -* Model Ocean Tide Loading (OTL) displacements. +- **GPS** - United States' Global Positioning System +- **Galileo** - European Union's Galileo system +- **GLONASS** - Russian GLONASS system* +- **BeiDou** - Chinese Navigation Satellite System* +- **QZSS** - Japanese Quasi-Zenith Satellite System* + +*\*Development ongoing* + +### Key Features and Capabilities + +Ginan provides the following advanced capabilities: + +- **Precise Orbit & Clock Determination** (POD) of GNSS satellites +- **Precise Point Positioning** (PPP) for stations in network and individual modes +- **Real-time corrections** generation for PPP users +- **Multi-frequency, multi-GNSS** data analysis +- **Atmospheric products** including ionosphere and troposphere models +- **Low Earth Orbiter** orbit modeling capabilities +- **Satellite Laser Ranging** processing capabilites +- Support for **wide range of users and receiver types** +- **User-friendly outputs** accessible by non-experts +- **Real-time and offline** processing capabilities +- **IGS products** generation (final, rapid, ultra-rapid, and real-time) +- **Ocean Tide Loading** (OTL) displacement modeling + +### Architecture The software consists of three main components: -* Network Parameter Estimation Algorithm (PEA), and -* Various scripts for combination and analysis of solutions -## Using Ginan with an AppImage +- **Parameter Estimation Algorithm (PEA)** - Core processing engine incorporating Precise Orbit Determination +- **Analysis Scripts** - Tools for data preparation, solution combination and analysis +- **Visualization Tools** - Python-based plotting and comparison utilities + +## Installation -You can quickly download a precompiled binary of Ginan's pea from the `develop-weekly-appimage` branch of github. -This allows you to run Ginan without the need for installing external dependencies. -It contains no python scripts or example data, but is possible to run immediately on linux and windows systems as simply as: +Choose the installation method that best fits your needs: - git clone -b develop-weekly-appimage --depth 1 --single-branch https://github.com/GeoscienceAustralia/ginan.git +### Using Ginan with Docker - ginan/Ginan-x86_64.AppImage +**Recommended for most users** - Get started quickly with a pre-configured environment: -or on windows: +```bash +# Run Ginan container with data volume mounting +docker run -it -v ${pwd}:/data gnssanalysis/ginan:v4.0.0 bash +``` + +This command: - wsl --install -d ubuntu - ginan/Ginan-x86_64.AppImage +- Mounts your current directory (`${pwd}`) to `/data` in the container +- Provides access to all Ginan tools and dependencies +- Opens an interactive bash shell -If the image fails to run, first ensure it is executable and all requires libraries are available +**Prerequisites:** [Docker](https://docs.docker.com/get-docker/) must be installed on your system. + +**Verify installation:** +```bash +pea --help +``` - chmod 777 ginan/Ginan-x86_64.AppImage - apt install fuse libfuse2 +### Precompiled binaries +Precompiled binaries for **Ginan** and **GinanUI** are available on the project's GitHub Releases page: https://github.com/GeoscienceAustralia/ginan/releases +We publish builds for the following platforms: -## Using Ginan with Docker +- Linux (x86_64) +- macOS (arm64 and x86_64) +- Windows (x86_64) -You can quickly download a ready-to-run Ginan environment using docker by running: +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. - docker run -it -v ${host_data_folder}:/data gnssanalysis/ginan:v3.1.0 bash +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. -This command connects the `${host_data_folder}` directory on the host (your pc), with the `/data` directory in the container, to allow file access between the two systems, and opens a command line (`bash`) for executing commands. +### Installation from Source -You will need to have [docker](https://docs.docker.com/get-docker/) installed to use this method. +**For developers and advanced users** who need to modify the source code or require specific configurations. -To verify you have the Ginan executables available once at the Ginan command line, run: +#### Tested Platforms - pea --help +| Platform | Tested Versions | Notes | +|----------|-----------------|-------| +| **Linux** | Ubuntu 22.04, 24.04 | Primary development platform | +| **macOS** | 10.15+ (x86) | Limited testing | +| **Windows** | 10+ | Limited testing| +#### Prerequisites -## Installation from source -### Supported Platforms +##### System Dependencies -Ginan is supported and tested on the following platforms +**Compilers:** -* Linux: tested on Ubuntu 18.04 and 20.04 and 22.04 -* MacOS: tested on 10.15 (x86) -* Windows: via docker or WSL on Windows 10 and above +- GCC/G++ (recommended, tested and supported) or equivalent C/C++ compiler -### Dependencies +**Required Dependencies:** -If instead you wish to build Ginan from source, there are several software dependencies: +- **CMake** ≥ 3.0 -* 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 -* YAML > 0.6 -* Boost >= 1.73 (tested on 1.73). On Ubuntu 22.04 which uses gcc-11, you need Boost >= 1.74.0 -* MongoDB -* Mongo_C >= 1.17.1 -* Mongo_cxx >= 3.6.0 -* Eigen3 > 3.4 -* netCDF4 -* Python >= 3.7 +- **YAML** ≥ 0.6 -If using gcc verion 11 or about, the minimum version of libraries are: -* Boost >= 1.74.0 -* Mongo_cxx = 3.7.0 +- **Boost** ≥ 1.75 + -Scripts to install dependencies for Ubuntu 18.04/20.04, 22.04, Fedora 38 are available on the `scripts/installation` directory. Users on other system might need to have a look at the `scripts/installation/generic.md` file, which contains the major steps. +- **Eigen3** ≥ 3.4 -### Python +- **OpenBLAS** (provides BLAS and LAPACK) -We use Python for automated process (download), postprocessing and visualisation. To use the developed tools, we recommand to use a virtual-environement (or Anaconda equivalent). A requirements file is available in the `scripts/` directory and can be run via -```python -pip3 install -r requirements.txt +**Optional Dependencies:** + +- **Mongo C Driver** ≥ 1.17.1 + +- **Mongo C++ Driver** ≥ 3.6.0 (= 3.7.0 for GCC 11+) + +- **MongoDB** (for database features) + +- **netCDF4** (for tidal loading computation) + +- **Python** ≥ 3.9 + +#### Build Process using `vcpkg` + CMake presets (Recommanded) + +We recommend using `vcpkg` for dependency management together with the repository CMake presets. + +1. Bootstrap and install `vcpkg` (from repository root): + +```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`. ``` -### Build -Prepare a directory to build in - it's better practice to keep this separated from the source code. -From the Ginan git root directory: +2. Configure and build with a CMake preset (run from `src`): ```bash -mkdir -p src/build +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 -cd src/build -cmake ../ +# Or build the preset directory directly (example for Linux): +cmake --build build/linux-Release --parallel $(nproc) +``` + +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. + +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. + +#### Legacy: manual `cmake` + `make` instructions + +##### Quick Installation Scripts (legacy) + +Pre-written installation scripts are available in `scripts/installation/` for systems where you prefer distro-specific package installation instead of `vcpkg`: + +```bash +# Ubuntu 24.04 +./scripts/installation/ubuntu24.sh + +# Ubuntu 22.04 +./scripts/installation/ubuntu22.sh + +# Ubuntu 20.04 +./scripts/installation/ubuntu20.sh + +# Fedora 38 +./scripts/installation/fedora38.sh + +# Generic instructions +cat scripts/installation/generic.md ``` -To build every package simply run `make` or `make -jX` , where X is a number of parallel threads you want to use for the compilation: +**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. + +The older manual flow is still available for users who prefer it: + +1. Create build directory: ```bash -make -j2 +mkdir -p src/build +cd src/build ``` -Alternatively, to build only a specific package (`pea`, `make_otl_blq`, `interpolate_loading`), run as below: +2. Configure with CMake (legacy): ```bash -make pea -j2 +cmake ../ ``` -This should create executables in the `bin` directory of Ginan. +3. Compile (legacy): + +```bash +# Build everything (parallel compilation recommended) +make -j$(nproc) + +# Build specific components +make pea -j$(nproc) # Core PEA executable +make make_otl_blq -j$(nproc) # Ocean tide loading +make interpolate_loading -j$(nproc) # Loading interpolation +``` -Check to see if you can execute the PEA from the exampleConfigs directory +4. Verify installation: ```bash cd ../../exampleConfigs - ../bin/pea --help ``` -and you should see something similar to: +Expected output: ``` -PEA starting... (main ginan-v3.0.0 from Mon Feb 05 15:15:22 2024) - +PEA starting... (main ginan-v4.0.0 from ...) Options: - -h [ --help ] Help - -q [ --quiet ] Less output - -v [ --verbose ] More output - -V [ --very-verbose ] Much more output - . - . - . - --dump-config-only Dump the configuration and exit - --walkthrough Run demonstration code interactively with - commentary - -PEA finished + -h [ --help ] Help + -q [ --quiet ] Less output + ... ``` - -Then download all of the example data using the scripts and filelists provided. From the Ginan git root directory: +5. Download example data: ```bash -cd inputData/data +cd ../inputData/data ./getData.sh -cd ../products +cd ../products ./getProducts.sh ``` -### Directory Structure - -Upon installation, the `ginan` directory should have the following structure: - - ginan/ - ├── README.md ! General README information - ├── LICENSE.md ! Software License information - ├── ChangeLOG.md ! Release Change history - ├── aws/ ! Amazon Web Services config - ├── bin/ ! Binary executables directory* - ├── Docs/ ! Documentation directory - ├── inputData/ ! Input data for examples - │ ├── data/ ! Example dataset (rinex files)** - │ └── products/ ! Example products and aux files** - ├── exampleConfigs ! Example configuration files - │ ├── ppp_example.yaml ! Basic user-mode example - │ └── pod_example.yaml ! Basic network-mode example - ├── lib/ ! Compiled object library directory* - ├── scripts/ ! Auxiliary Python and Shell scripts and libraries - └── src/ ! Source code directory - ├── cpp/ ! Ginan source code - ├── cmake/ - ├── doc_templates/ - ├── build/ ! Cmake build directory* - └── CMakeLists.txt - -*\*created during installation process* - -*\*\* contents retrieved with getData.sh, getProducts.sh scripts* +### Python Environment Setup +Ginan uses Python for automation, post-processing, and visualization: -## Documentation +```bash +# Create virtual environment (recommended) +python3 -m venv ginan-env +source ginan-env/bin/activate + +# Install Python dependencies +pip3 install -r scripts/requirements.txt +``` + + +## Getting Started with the examples + +Congratulations! Ginan is now ready to use. The examples in `exampleConfigs/` provide a great starting point. -Ginan documentation consists of two parts: these documents, and separate Doxygen-generated documentation that shows the actual code infrastructure. -It can be found [here](https://geoscienceaustralia.github.io/ginan/codeDocs/index.html), or generated manually as below. +- **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: + ```bash + OMP_NUM_THREADS=1 ../bin/pea --config ppp_example.yaml + ``` -### Doxygen -The Doxygen documentation for Ginan requires `doxygen` and `graphviz`. If not already installed, type as follows: +### Running Your First Example + +1. **Navigate to examples directory:** + ```bash + cd exampleConfigs + ``` + +2. **Run basic PPP example:** + ```bash + ../bin/pea --config ppp_example.yaml + ``` + +3. **Check outputs:** + + The processing will create an directory named `outputs/ppp_example/` or similar containing: + - `*.trace` files with station processing details + - `Network*.trace` with Kalman filter results + - Other auxiliary outputs as configured + + +### Adding Ginan to PATH + +For convenience, add Ginan binaries to your system PATH: ```bash -sudo apt -y install doxygen graphviz +# Add to ~/.bashrc or ~/.zshrc +export PATH="/path/to/ginan/bin:$PATH" + +# Then run from anywhere +pea --config /path/to/config.yaml ``` -On success, proceed to the build directory and call make with `docs` target: -```bash -cd ../src/build -cmake ../ +## Additional Tools and Scripts -make docs -``` +Beyond the core PEA executable, Ginan includes [comprehensive scripts](https://geoscienceaustralia.github.io/ginan/page.html?c=on&p=scripts.index) for: -The documentation can then be found at `Docs/codeDocs/index.html`. +- **Data downloading** and preprocessing +- **Output visualization** and analysis +- **Solution comparison** and validation +- **Performance monitoring** and reporting -Note that documentation is also generated automatically if `make` is called without arguments and `doxygen` and `graphviz` dependencies are satisfied. +## Documentation + +Ginan documentation is available in multiple formats: + +### User Documentation +- **Online Manual:** [geoscienceaustralia.github.io/ginan](https://geoscienceaustralia.github.io/ginan/) +- **Configuration Guide:** [Detailed parameter explanations and examples](https://geoscienceaustralia.github.io/ginan/page.html?c=on&p=ginanConfiguration.md) +- **FAQ:** [Ginan FAQ](https://geoscienceaustralia.github.io/ginan/page.html?p=ginanFAQ.html) -## Ready! -Congratulations! You are now ready to trial the examples from the `exampleConfigs` directory. See Ginan's manual for detailed explanation of each example. Note that examples have relative paths to files in them and rely on the presence of `products` and `data` directories inside the `inputData` directory. Make sure you've run `s3_filehandler.py` from the Build step of these instructions. +### Developer Documentation -The paths are relative to the `exampleConfigs` directory and hence all the examples must be run from the `exampleConfigs` directory. +- **Code Documentation:** [API Reference](https://geoscienceaustralia.github.io/ginan/codeDocs/index.html) -NB: Examples may be configured to use mongoDB. If you have not installed it, please set `mongo: enable` to false in the pea config files. +### Generating Code Documentation -To run the first example of the PEA: +Requirements: `doxygen` and `graphviz` ```bash -cd ../exampleConfigs +# Install dependencies (Ubuntu/Debian) +sudo apt install doxygen graphviz -../bin/pea --config ppp_example.yaml +# Generate documentation +cd src/build +cmake ../ +make docs + +# View documentation +open ../../Docs/codeDocs/index.html ``` -This should create `outputs/ppp_example` directory with various `*.trace` files, which contain details about stations processing, a `Network*.trace` file, which contains the results of Kalman filtering, and other auxiliary output files as configured in the yamls. +## Contributing -You can remove the need for path specification to the executable by using the symlink within `exampleConfigs`, or by adding Ginan's bin directory to `~/.bashrc` file: -``` -PATH="path_to_ginan_bin:$PATH" -``` +We welcome contributions from the community! Here's how to get involved: + +### Reporting Issues +- Use [GitHub Issues](https://github.com/GeoscienceAustralia/ginan/issues) for bug reports +- Provide detailed reproduction steps and system information +- Check existing issues before creating new ones + +### Contributing Code +1. Fork the repository +2. Create a feature branch: `git checkout -b feature-name` +3. Follow our [coding standards](Docs/codingStandard.md) +4. Submit a pull request with clear description -NB: For PPP positioning of a single station, we have noted that limiting the number of cores to 1 can reduce processing times. This can be achieved via setting the environment variable `OMP_NUM_THREADS`: +### Development Setup +- Follow the source installation instructions above +- Review `Docs/codingStandard.md` for guidelines +- Run tests before submitting. - OMP_NUM_THREADS=1 ginan/Ginan-x86_64.AppImage +## Support +### Getting Help +- **Documentation:** Check the [online manual](https://geoscienceaustralia.github.io/ginan/) first +- **Issues:** Report bugs and feature requests on [GitHub](https://github.com/GeoscienceAustralia/ginan/issues) +- **Discussions:** Join community discussions on [GitHub Discussions](https://github.com/GeoscienceAustralia/ginan/discussions) +## License -## Scripts -In addition to the Ginan binaries, [scripts](https://geoscienceaustralia.github.io/ginan/page.html?c=on&p=scripts.index) are available to assist with downloading input files, and viewing and comparing generated outputs. +Ginan is released under the **Apache License 2.0**. See [LICENSE.md](LICENSE.md) for details. +### Third-Party Components +This software incorporates components from several open-source projects. See [Acknowledgements](#acknowledgements) below for detailed attribution. +## Acknowledgements -### Acknowledgements: -We have used routines obtained from RTKLIB, released under a BSD-2 license, these routines have been preserved with modifications in the folder `cpp/src/rtklib`. The original source code from RTKLib can be obtained from https://github.com/tomojitakasu/RTKLIB. +Ginan incorporates code from several excellent open-source projects: -We have used routines obtained from Better Enums, released under the BSD-2 license, these routines have been preserved in the folder `cpp/src/3rdparty` The original source code from Better Enums can be obtained from http://github.com/aantron/better-enums. +| Project | License | Purpose | Original Source | +|---------|---------|---------|-----------------| +| **Magic Enum** | MIT | Enhanced enum support | [github.com/Neargye/magic_enums](https://github.com/Neargye/magic_enums) | +| **EGM96** | zlib | Earth gravitational model | [github.com/emericg/EGM96](https://github.com/emericg/EGM96) | +| **IERS2010**| Public Domain | Tidal displacement computation | [github.com/xanthospap/iers2010](https://github.com/xanthospap/iers2010) +| **JPL Ephemeris** | GPL-3 | Planetary ephemeris | [github.com/Bill-Gray/jpl_eph](https://github.com/Bill-Gray/jpl_eph) | +| **NRLMSISE** | Public Domain | Atmospheric modeling | [github.com/c0d3runn3r/nrlmsise](https://github.com/c0d3runn3r/nrlmsise/tree/master) | +| **RTKLIB** | BSD-2-Clause | GNSS processing routines | [github.com/tomojitakasu/RTKLIB](https://github.com/tomojitakasu/RTKLIB) | +| **SLR** | Public Domain | SLR input file managements | [ilrs.gsfc.nasa.gov](https://ilrs.gsfc.nasa.gov/data_and_products/formats/crd.html)* +| **SOFA** | SOFA License | Astronomical computations | [iausofa.org](https://www.iausofa.org/) | -We have used routines obtained from EGM96, released under the zlib license, these routines have been preserved in the folder `cpp/src/3rdparty/egm96` The original source code from EGM96 can be obtained from https://github.com/emericg/EGM96. +All incorporated code has been preserved with appropriate modifications in the `cpp/src/` directory structure, maintaining original licensing and attribution requirements. -We have used routines obtained from SOFA, released under the SOFA license, these routines have been preserved in the folder `cpp/src/3rdparty/sofa` The original source code from SOFA can be obtained from https://www.iausofa.org/. +--- -We have used routines obtained from project Pluto, released under the GPL-3 license, these routines have been preserved in the folder `cpp/src/3rdparty/jplephem` The original source code from jpl ephem can be obtained from https://github.com/Bill-Gray/jpl_eph. +**Developed by [Geoscience Australia](https://www.ga.gov.au/)** | **Version 4.0.0** | **[GitHub Repository](https://github.com/GeoscienceAustralia/ginan)** diff --git a/debugConfigs/pod_rt_example1.yaml b/debugConfigs/pod_rt_example1.yaml index 2553d6c9b..461457fc4 100644 --- a/debugConfigs/pod_rt_example1.yaml +++ b/debugConfigs/pod_rt_example1.yaml @@ -106,7 +106,7 @@ inputs: - "TID100AUS0" - "MSSA00JPN0" - "ONS100SWE0" -# + outputs: outputs_root: outputs/ @@ -126,6 +126,15 @@ outputs: 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: @@ -148,12 +157,8 @@ satellite_options: enable: true code_bias: enable: true - default_bias: 0 - undefined_sigma: 0 phase_bias: enable: false - default_bias: 0 - undefined_sigma: 0 orbit_propagation: albedo: cannonball @@ -216,12 +221,8 @@ receiver_options: # Options to c enable: true # (bool) Enable modelling of phase center variations code_bias: enable: true # (bool) Enable modelling of code biases - default_bias: 0 # (float) Bias to use when no code bias is found - undefined_sigma: 0 # (float) Uncertainty sigma to apply to default code biases phase_bias: enable: false # (bool) Enable modelling of phase biases - default_bias: 0 # (float) Bias to use when no phase bias is found - undefined_sigma: 0 # (float) Uncertainty sigma to apply to default phase biases pos: enable: true # (bool) Enable modelling of position ionospheric_components: # Ionospheric models produce frequency-dependent effects diff --git a/debugConfigs/record_ssr_stream.yaml b/debugConfigs/record_ssr_stream.yaml index be47993a8..40982949a 100644 --- a/debugConfigs/record_ssr_stream.yaml +++ b/debugConfigs/record_ssr_stream.yaml @@ -2,7 +2,7 @@ inputs: - inputs_root: ./products/RAP/ + inputs_root: ./products/latest/ atx_files: - igs20.atx @@ -10,6 +10,8 @@ inputs: 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/" @@ -36,6 +38,10 @@ outputs: output_rotation: period: 86400 + log: + output: true + filename: __LOG.json + rtcm_nav: output: true filename: __NAV.rtcm @@ -62,12 +68,27 @@ outputs: 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: 1 - wait_next_epoch: 1 + epoch_interval: 10 + wait_next_epoch: 10 + max_rec_latency: 1 sleep_milliseconds: 1 gnss_general: diff --git a/debugConfigs/sp3_ecef2eci.yaml b/debugConfigs/sp3_ecef2eci.yaml new file mode 100644 index 000000000..462f32be5 --- /dev/null +++ b/debugConfigs/sp3_ecef2eci.yaml @@ -0,0 +1,51 @@ +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/docker/Dockerfile b/docker/Dockerfile index 1a066d3b1..447cee469 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,8 +1,6 @@ # ------ Docker build target: Run heavyweight build environment locally, on top of ginan-env image ------ # FROM gnssanalysis/ginan-env:latest as ginan-build -# ^ Recent build with patched (old) version of Doxygen. -# TODO switch from custom build to latest, once Doxygen build is incorporated into it. # NOTE: the doc build step later on requires a version of ginan-env with Doxygen installed in it. # This is added by (internal) Ginan PR #520, around Apr 2024 @@ -21,6 +19,11 @@ WORKDIR /ginan # Code assets for building PEA ADD src /ginan/src +# Add .git directory so build process can extract current tag / commit hash to embed in build. +# This data will not be published. The build image is not pushed, only the built artefacts (which are copied +# into the minimal image) +ADD .git /ginan/.git + # Docs, example configs, data and products. All relied on by Doxygen to generate documentation. # Add exampleConfigs and logical subdirs (ADD doesn't seem to follow symlinks). These are used by Doxygen. ADD exampleConfigs /ginan/exampleConfigs @@ -35,12 +38,14 @@ ARG TARGET=pea # Values >2 may cause memory issues on BitBucket pipelines. On development machines, set higher for speed (eg 4). ARG BUILD_THREADS=2 -# Main outputs are ginan/bin and (if TARGET = pea or ALL) ginan/Docs. Don't need to keep intermediaries in 'build'. +# Main outputs are ginan/bin and (if TARGET = pea or ALL) ginan/Docs. +# NOTE: while the cmake line appears to enable doc building, docs don't actually get written unless the +# make target on the following line is set to 'docs' or 'ALL'. RUN \ mkdir -p src/build \ && cd src/build \ - && cmake -DENABLE_OPTIMISATION=TRUE -DENABLE_PARALLELISATION=TRUE -DBUILD_DOC=TRUE .. \ - && make -j $BUILD_THREADS $TARGET \ + && cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_DOC=TRUE .. \ + && make -j$BUILD_THREADS $TARGET \ && cd - \ && rm -rf src/build @@ -54,19 +59,61 @@ FROM scratch as ginan-build-cache COPY --from=ginan-build /root/.ccache /root/.ccache -# ------ Docker build target: minimal image with just PEA and its dependencies (TODO base on Ubuntu) ------ # -FROM gnssanalysis/ginan-env:latest as ginan-minimal -# TODO this should really be based on Ubuntu:24.04, with only those packages installed that are necessary to run pea -# and utilities. - -ENV PATH "/ginan/bin:${PATH}" +# ------ Docker build target: minimal image with just PEA and its dependencies ------ # +FROM ubuntu:24.04 as ginan-minimal + +# Was 1 thread, before being broken out into an argument +ARG BUILD_THREADS=1 + +# Install runtime dependencies, then clean up apt artifacts/caches to avoid saving them in the image +# Curl and gpg only required here to set up Mongo repo file. +RUN apt-get update -y \ + && apt-get upgrade -y --no-install-recommends \ + && apt-get install -y --no-install-recommends \ + curl \ + gpg \ + liblapacke-dev \ + libopenblas0 \ + libyaml-cpp0.8 \ + libncurses6 \ + libgomp1 \ + libmongoc-1.0-0 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + + #TODO check which of these libraries we can remove, if we build PEA with the static version of them instead. + +# Install MongoDB 7, via third party repo, clean up apt caches +RUN curl -fsSL https://pgp.mongodb.com/server-7.0.asc | gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor \ + && echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list \ + && apt update -y \ + && apt-get install -y --no-install-recommends mongodb-org \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean -ADD scripts /ginan/scripts # Ensure our Python environment is installed so we can run gnssanalysis and relevant scripts. -# Somewhat redundant while image is based on ginan-env, but will still get updates to gnssanalysis applied quicker. COPY scripts/requirements.txt /tmp/requirements.txt WORKDIR /tmp/ RUN python3 -m pip install -r requirements.txt --no-cache-dir --break-system-packages -COPY --from=ginan-build /ginan/bin/ /ginan/bin +ADD scripts /ginan/scripts + +# Copy the built PEA binary from the build stage +COPY --from=ginan-build /ginan/bin/ /usr/bin/ +# TODO: we could update the build process to output to /usr/bin/ instead +# For backwards compatibility, put a link at /ginan/bin/pea pointing to /usr/bin/pea: +RUN mkdir /ginan/bin/ \ + && ln -s /usr/bin/pea /ginan/bin/pea \ + && chmod go-w /usr/bin/pea +# Permissions update probably not important (all processes have the same access level in the container) + +# Copy Mongocxx driver (including symlinks) from what is effectively the pre-build stage (ginan-env.Dockerfile) +COPY --from=gnssanalysis/ginan-env:latest /opt/mongocxx/lib/ /usr/local/lib/ +# Alternatively, if we built mongocxx at the pea build stage above: +# COPY --from=ginan-build /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 diff --git a/docker/ginan-env.Dockerfile b/docker/ginan-env.Dockerfile index 7c78ded96..8e3a9f3cc 100644 --- a/docker/ginan-env.Dockerfile +++ b/docker/ginan-env.Dockerfile @@ -1,4 +1,7 @@ -FROM ubuntu:24.04 +FROM ubuntu:24.04 as ginan-env +# These are set for Bitbucket pipelines memory ceiling. You can set higher on a development machine if you have the memory for it. +ARG BUILD_THREADS_DOXYGEN=4 +ARG BUILD_THREADS_GENERAL=2 ARG DEBIAN_FRONTEND=noninteractive ENV HOME /root @@ -6,6 +9,7 @@ ENV HOME /root RUN apt-get update -y \ && apt-get upgrade -y --no-install-recommends \ && apt-get install -y --no-install-recommends \ + ca-certificates \ git \ gcc \ g++ \ @@ -19,100 +23,97 @@ RUN apt-get update -y \ make \ gzip \ vim \ + libboost1.83-all-dev \ + libeigen3-dev \ libopenblas-dev \ liblapack-dev \ + liblapacke-dev \ libssl-dev \ + libmongoc-1.0-0 \ libnetcdf-dev \ libnetcdf-c++4-dev \ libncurses-dev \ libzstd-dev \ libssl-dev \ libgomp1 \ - python3-pip \ - python3-dev \ + libyaml-cpp-dev \ ssh \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean +# As of Dec 2024, Ubuntu 24.04 has: +# - libeigen3-dev: version 3.4.0-4 +# - boost 1.74 and 1.83 (we previously built version 1.75 from source) +# Note: ca-certificates is a dependency of python3-pip (so would commonly be installed as a result of that). +# It is needed to validate github's cert. + RUN ulimit -c unlimited RUN mkdir -p /tmp/build WORKDIR /tmp/build -COPY scripts/requirements.txt /tmp/build/requirements.txt -RUN python3 -m pip install -r requirements.txt --no-cache-dir --break-system-packages - ENV CFLAGS="-fno-omit-frame-pointer" ENV CXXFLAGS="-fno-omit-frame-pointer" ENV CMAKE_CXX_STANDARD="20" -RUN git clone --depth 1 --branch 0.8.0 https://github.com/jbeder/yaml-cpp.git \ - && mkdir -p yaml-cpp/cmake-build \ - && cd yaml-cpp/cmake-build \ - && cmake .. -DCMAKE\_INSTALL\_PREFIX=/usr/local/ -DYAML\_CPP\_BUILD\_TESTS=OFF \ - && make install yaml-cpp \ - && cd - \ - && rm -rf yaml-cpp - -RUN git clone --depth 1 --branch 3.4.0 https://gitlab.com/libeigen/eigen.git \ - && mkdir -p eigen/cmake-build \ - && cd eigen/cmake-build \ - && cmake .. \ - && make install \ - && cd - \ - && rm -rf eigen -RUN git clone --depth 1 --branch 1.26.1 https://github.com/mongodb/mongo-c-driver.git \ - && mkdir -p mongo-c-driver/cmake-build \ - && cd mongo-c-driver/cmake-build \ - && cmake -DENABLE_AUTOMATIC_INIT_AND_CLEANUP=OFF -DENABLE_EXTRA_ALIGNMENT=OFF .. \ - && cmake --build . \ - && cmake --build . --target install \ - && cd - \ - && rm -rf mongo-c-driver +# We currently avoid building this (installing via apt above). But the tradeoff is being +# slightly behind. The latest on Ubuntu 24.04 is currently from Feb (1.26.0). +# RUN git clone --depth 1 --branch 1.26.1 https://github.com/mongodb/mongo-c-driver.git \ +# && mkdir -p mongo-c-driver/cmake-build \ +# && cd mongo-c-driver/cmake-build \ +# && cmake -DENABLE_AUTOMATIC_INIT_AND_CLEANUP=OFF -DENABLE_EXTRA_ALIGNMENT=OFF .. \ +# && cmake --build . -j${BUILD_THREADS_GENERAL} \ +# && cmake --build . --target install -j${BUILD_THREADS_GENERAL} \ +# && cd - \ +# && rm -rf mongo-c-driver +# Amazingly, this is not available as a built library. Our only realistic option is to build it +# See here: https://www.mongodb.com/docs/languages/cpp/cpp-driver/current/get-started/download-and-install/ RUN git clone --depth 1 --branch r3.10.1 https://github.com/mongodb/mongo-cxx-driver.git \ && mkdir -p mongo-cxx-driver/cmake-build \ && cd mongo-cxx-driver/cmake-build \ && cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \ - && cmake --build . \ - && cmake --build . --target install \ + && cmake --build . -j${BUILD_THREADS_GENERAL} \ + && cmake --build . --target install -j${BUILD_THREADS_GENERAL} \ && cd - \ && rm -rf mongo-cxx-driver -ARG BOOST_VER=1.75.0 -RUN BOOST_NAME=$(echo boost_${BOOST_VER} | tr . _); \ - wget -c --no-verbose https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VER}/source/${BOOST_NAME}.tar.gz \ - && tar xf ${BOOST_NAME}.tar.gz \ - && rm ${BOOST_NAME}.tar.gz \ - && cd ${BOOST_NAME}/ \ - && ./bootstrap.sh \ - && ./b2 -j6 install \ - && cd - \ - && rm -rf ${BOOST_NAME}/ +# Copy the built MongoCXX driver library into a dedicated directory from which it can be picked up +# by the Ginan minimal image build. Also copy libbsoncxx, a dependency(?) of mongocxx. +# We use the following somewhat convoluted command because: +# - The Dockerfile COPY directive can't filter based on a glob +# - The default shell is sh, so we can't use shopt -s dotglob +RUN mkdir -p /opt/mongocxx/lib/ \ + && cd /usr/local/lib \ + && find -mindepth 1 -maxdepth 1 -name "libmongocxx*" -exec cp --parents -t /opt/mongocxx/lib/ {} + \ + && find -mindepth 1 -maxdepth 1 -name "libbsoncxx*" -exec cp --parents -t /opt/mongocxx/lib/ {} + + +# TODO: We're making the assumption we don't need the cmake (ie /usr/local/lib/cmake/mongocxx*) and pkgconfig +# subdirs, but that assumption may be wrong. +# Leaving out -maxdepth 1 could also allow us to grab other relevant files from their respective dirs, +# and keep dir structure. But it might not find everything... + # Note there should be no actual data left in this directory apart from requirements.txt. # This is a cosmetic rather than space saving cleanup step. - RUN rm -rf /tmp/build # Install a forked version of doxygen that has some nice features made just for ginan docs # To avoid an extra container image layer, we also copy the patch to the parent directory not the doxygen working dir - +# TODO: revist where to install this fork from, and whether we need it here. RUN apt-get update -y \ && apt-get install -y --no-install-recommends flex bison \ && git clone --depth 1 --branch customColors https://github.com/polytopes-design/doxygen.git \ && mkdir -p doxygen/build \ && cd doxygen/build \ && cmake -G "Unix Makefiles" .. \ - && make -j4 \ - && make install \ + && make -j${BUILD_THREADS_DOXYGEN} \ + && make -j${BUILD_THREADS_GENERAL} install \ && cd - \ && rm -rf doxygen \ && apt-get remove -y flex bison \ - && apt-get autoremove -y m4 \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean # For clean up, we uninstall flex and bison (apparently build-only dependencies for doxygen). -# We also remove m4 (required by bison/flex) but do this conditionally on whether anything else still needs it. -# Finally, we delete the apt package list, and cached packages. +# Finally, we delete the apt package list, and cached packages. \ No newline at end of file diff --git a/docker/tags b/docker/tags deleted file mode 100644 index 45f958d00..000000000 --- a/docker/tags +++ /dev/null @@ -1,5 +0,0 @@ - -PEA="7b1de342" -POD="a03f60a" -PEAPOD="fa2b375" -OTHER="817ccee" diff --git a/exampleConfigs/LEO_dynPOD.yaml b/exampleConfigs/LEO_dynPOD.yaml new file mode 100644 index 000000000..126cd8041 --- /dev/null +++ b/exampleConfigs/LEO_dynPOD.yaml @@ -0,0 +1,340 @@ +# Yaml config file for Reduced-dynamic POD of GRACE-FO C satellite; Date: 2023 12 01 +inputs: + inputs_root: ./products/ + atx_files: [igs20.atx] # Antenna models for receivers and satellites in ANTEX format + erp_files: [tables/finals.data.iau2000.txt] + egm_files: [tables/EGM2008.gfc] # Earth gravity model coefficients file + planetary_ephemeris_files: [tables/DE436.1950.2050] # JPL planetary and lunar ephemerides file + hfeop_files: [tables/desai_model_jgrb51665-sup-0002-ds01.txt] + tides: + atmos_tide_potential_files: [tables/atmosTide_AOD1bRL06.potential.iers.txt] + ocean_tide_potential_files: [tables/fes2014b_Cnm-Snm.dat] + ocean_pole_tide_potential_files: [tables/oceanPoleTide_desai2004.txt] + atmos_ocean_dealiasing_files: + - LEO/AOD1B_2019-02-18_X_06.asc + - LEO/AOD1B_2019-02-19_X_06.asc + - LEO/AOD1B_2019-02-20_X_06.asc + + snx_files: + # - "*.SNX" # use a wild card to include all files matching the description in the directory + - tables/igs_satellite_metadata_2203_plus.snx + #- tables/sat_yaw_bias_rate.snx + #- tables/qzss_yaw_modes.snx + #- tables/bds_yaw_modes.snx + - LEO.snx + + satellite_data: + sp3_files: [LEO/2019_02/COD0R03FIN_20190450000_01D_05M_ORB.SP3] # satellite orbit files in SP3 format + clk_files: [LEO/2019_02/COD0R03FIN_20190450000_01D_05S_CLK.CLK] # satellite clock files in RNX CLK format + bsx_files: [LEO/2019_02/COD0R03FIN_20190450000_01D_01D_OSB.BIA] # daily signal biases files + obx_files: [LEO/2019_02/SCA1B_2019-02-14_C_04.OBX] + + gnss_observations: + gnss_observations_root: LEO/2019_02/ + rnx_inputs: + - L64: + - "GPS1B_2019-02-14_C_04.rnx" + +outputs: + outputs_root: ./outputs/ + + trace: + level: 5 + output_receivers: true + output_satellites: true + output_network: true + receiver_filename: _.Reciever + satellite_filename: _.Sat + network_filename: _.TRACE + output_residuals: true + output_residual_chain: true + output_config: true + + metadata: + config_description: dynPOD_v3_20190214_LEO1 + analysis_agency: GAA + analysis_centre: Geoscience Australia -----FILE NOT FOR OPERATIONAL USE----- + analysis_software: Ginan v3 + rinex_comment: AUSNETWORK1 + #gradient_mapping_function: Chen & Herring, 1992 # (string) Name of mapping function used for mapping horizontal troposphere gradients + ocean_tide_loading_model: FES2004 # (string) Ocean tide loading model applied + reference_system: igs20 # (string) Terrestrial Reference System Code + time_system: G # (string) Time system - e.g. "G", "UTC" + + network_statistics: + output: true # (bool) Enable exporting network statistics data to file + directory: ./ # (string) Directory to export network statistics data + filename: _network_statistics.json # (string) Network statistics data filename + + sp3: + output: true # (bool) Enable exporting SP3 data to file + directory: ./ # (string) Directory to export SP3 data + # filename: _.sp3 # (string) SP3 data filename +satellite_options: + global: + antenna_boresight: [0, 0, +1] + antenna_azimuth: [0, +1, 0] + L64: + orbit_propagation: + solar_radiation_pressure: NONE + antenna_thrust: false + albedo: NONE + empirical: true + planetary_perturbations: + [ + moon, + sun, + mercury, + venus, + mars, + jupiter, + saturn, + uranus, + neptune, + pluto, + ] + + mass: 601.214 + area: 0.9551567 + srp_cr: 1.25 + +receiver_options: # Options to configure individual stations or global configs + global: + rec_reference_system: gps + models: + eop: + enable: true + L64: + antenna_type: "ANTTYPE" + receiver_type: "TRIG" + models: + eccentricity: + enable: true + offset: [0.2602, -0.0013, -0.4862] # ENU + + phase_windup: + enable: true + relativity2: + enable: true + relativity: + enable: true + sagnac: + enable: true + tides: + enable: false + attitude: + enable: true # (bool) Enables non-nominal attitude types + sources: [PRECISE, MODEL, NOMINAL] # List of sourecs to use for attitudes + troposphere: + enable: false + pco: + enable: true # (bool) Enable modelling of phase center offsets + eop: + enable: true + ionospheric_components: + enable: true + antenna_boresight: [0, 0, -1] + antenna_azimuth: [+1, 0, 0] + sat_id: "L64" + rinex2: + rnx_code_conversions: + P1: l1w + P2: l2w + rnx_phase_conversions: + L1: l1w + L2: l2w + + error_model: elevation_dependent # uniform, elevation_dependent + elevation_mask: 1 + code_sigma: 0.2 + phase_sigma: 0.002 # F0, F1, F2, F5, F6, F7, F8 + clock_codes: [l1w, l2w] + +processing_options: + orbit_propagation: + central_force: true + indirect_J2: true + egm_field: true + solid_earth_tide: true + ocean_tide: true + atm_tide: true + aod: true + general_relativity: true + pole_tide_ocean: true + pole_tide_solid: true + + egm_degree: 120 + integrator_time_step: 10 + process_modes: + preprocessor: true + spp: true + ppp: true + ionosphere: false # Compute Ionosphere models based on GNSS measurements + slr: false # Process SLR observations + + epoch_control: + epoch_interval: 10 #seconds + 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. + max_rec_latency: 10 + #max_epochs: 50 + #start_epoch: 2019-02-14 00:00:00 + #end_epoch: 2019-02-14 00:59:59 + + gnss_general: + minimise_sat_clock_offsets: + enable: false + sys_options: + gps: + process: true + ambiguity_resolution: true + reject_eclipse: false + code_priorities: [L1W, L2W] + #network_amb_pivot: false # Constrain: set of ambiguities, to eliminate network rank deficiencies + #receiver_amb_pivot: false # Constrain: set of ambiguities, to eliminate receiver rank deficiencies + + leo: + process: true + + model_error_handling: + meas_deweighting: # Measurements that are outside the expected confidence bounds may be deweighted so that outliers do not contaminate the filtered solution + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all rejected measurement + state_deweighting: # Any "state" errors cause deweighting of all measurements that reference the state + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all referencing measurements + ambiguities: + phase_reject_limit: 2 # Reset ambiguity after 2 large fractional residuals are found (replaces phase_reject_count:) + reset_on: # Reset ambiguities when slip is detected by the following + gf: true # GF test + lli: false # LLI test + mw: true # MW test + scdia: true # SCDIA test + exclusions: + gf: true # (bool) Exclude measurements that fail GF slip test in preprocessor + lli: false # (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 + + preprocessor: # Configurations for the kalman filter and its sub processes + cycle_slips: # Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised + mw_process_noise: 0 # Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips + slip_threshold: 0.5 # Value used to determine when a slip has occurred + preprocess_all_data: true + + spp: + always_reinitialise: false # Reset SPP state to zero to avoid potential for lock-in of bad states + max_lsq_iterations: 12 # Maximum number of iterations of least squares allowed for convergence + outlier_screening: + raim: + enable: true # Enable Receiver Autonomous Integrity Monitoring + max_gdop: 30 # Maximum dilution of precision before error is flagged + + ambiguity_resolution: # Warning: lambda_bie is NOT recommended for network processing, use bootst option instead + elevation_mask: 15 + lambda_set_size: 200 + mode: off + success_rate_threshold: 0.99 + solution_ratio_threshold: 30 + fix_and_hold: true + # once_per_epoch: false + + ppp_filter: + outlier_screening: + chi_square: + enable: false # Enable Chi-square test + prefit: + max_iterations: 3 # Maximum number of measurements to exclude using prefit checks before attempting to filter + omega_test: false # Enable omega-test + sigma_check: true # Enable sigma check + state_sigma_threshold: 4 # Sigma threshold + meas_sigma_threshold: 4 # Sigma threshold + postfit: + max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter + sigma_check: true # Enable sigma check + state_sigma_threshold: 4 # Sigma threshold + meas_sigma_threshold: 4 # Sigma threshold + ionospheric_components: # Slant ionospheric components + common_ionosphere: true # Use the same ionosphere state for code and phase observations + use_gf_combo: false # Combine 'uncombined' measurements to simulate a geometry-free solution + use_if_combo: true # Combine 'uncombined' measurements to simulate an ionosphere-free solution + corr_mode: iono_free_linear_combo + + chunking: + by_receiver: false # Split large filter and measurement matrices blockwise by receiver ID to improve processing speed + by_satellite: false # Split large filter and measurement matrices blockwise by satellite ID to improve processing speed + size: 0 + + rts: # Rauch-Tung-Striebel (RTS) backwards smoothing + enable: true + lag: -1 + filename: _.rts + +estimation_parameters: + receivers: + L64: + orbit: + estimated: [true] + sigma: [1, 1, 1, 0.1, 0.1, 0.1] + process_noise: [0] + apriori_value: + [ + 5618061.478891041, + 2452804.124419954, + -3139258.743281315, + -3239.732805501764, + -1244.60090822831, + -6762.166783502897, + ] + + emp_r_0: + estimated: [true] + sigma: [50] + apriori_value: [0] + process_noise: [20] + + emp_t_0: + estimated: [true] + sigma: [50] + apriori_value: [0] + process_noise: [20] + + emp_n_0: + estimated: [true] + sigma: [50] + apriori_value: [0] + process_noise: [20] + + clock: + estimated: [true] + sigma: [500] + process_noise: [500] + + ambiguities: + estimated: [true] + sigma: [60] + process_noise: [0] + outage_limit: [50] + + ion_stec: + estimated: [true] + sigma: [1000] + process_noise: [100] + +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: + primary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison + # secondary_database: + # secondary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison + # secondary_uri: mongodb://localhost:27017 # Location and port of the mongo database to connect to + # output_config: NONE # Output config {none,primary,secondary,both} + 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} + output_trace: primary + +debug: + # output_mincon: true + # mincon_only: true diff --git a/exampleConfigs/LEO_kinPOD.yaml b/exampleConfigs/LEO_kinPOD.yaml new file mode 100644 index 000000000..6b8f01392 --- /dev/null +++ b/exampleConfigs/LEO_kinPOD.yaml @@ -0,0 +1,328 @@ +# Yaml config file for Reduced-dynamic POD of GRACE-FO C satellite; Date: 2023 12 01 +inputs: + inputs_root: ./products/ + atx_files: [igs20.atx] # Antenna models for receivers and satellites in ANTEX format + erp_files: [tables/finals.data.iau2000.txt] + egm_files: [tables/EGM2008.gfc] # Earth gravity model coefficients file + planetary_ephemeris_files: [tables/DE436.1950.2050] # JPL planetary and lunar ephemerides file + hfeop_files: [tables/desai_model_jgrb51665-sup-0002-ds01.txt] + tides: + #ocean_tide_loading_blq_files: [ OLOAD_GO.BLQ ] # required if ocean loading is applied + #atmos_tide_loading_blq_files: [ 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 + atmos_tide_potential_files: [tables/atmosTide_AOD1bRL06.potential.iers.txt] + ocean_tide_potential_files: [tables/fes2014b_Cnm-Snm.dat] + ocean_pole_tide_potential_files: [tables/oceanPoleTide_desai2004.txt] + + snx_files: + # - "*.SNX" # use a wild card to include all files matching the description in the directory + - tables/igs_satellite_metadata_2203_plus.snx + #- tables/sat_yaw_bias_rate.snx + #- tables/qzss_yaw_modes.snx + #- tables/bds_yaw_modes.snx + - LEO.snx + + satellite_data: + sp3_files: [LEO/2019_02/COD0R03FIN_20190450000_01D_05M_ORB.SP3] # satellite orbit files in SP3 format + clk_files: [LEO/2019_02/COD0R03FIN_20190450000_01D_05S_CLK.CLK] # satellite clock files in RNX CLK format + bsx_files: [LEO/2019_02/COD0R03FIN_20190450000_01D_01D_OSB.BIA] # daily signal biases files + obx_files: [LEO/2019_02/SCA1B_2019-02-14_C_04.OBX] + + gnss_observations: + gnss_observations_root: LEO/2019_02/ + rnx_inputs: + - L64: + - "GPS1B_2019-02-14_C_04.rnx" + +outputs: + outputs_root: ./outputs/ + + trace: + level: 5 + output_receivers: true + output_satellites: true + output_network: true + receiver_filename: _.Reciever + satellite_filename: _.Sat + network_filename: _.TRACE + output_residuals: true + output_residual_chain: true + output_config: true + + metadata: + config_description: kinPOD_v3_20190214_LEO1 + analysis_agency: GAA + analysis_centre: Geoscience Australia -----FILE NOT FOR OPERATIONAL USE----- + analysis_software: Ginan + rinex_comment: AUSNETWORK1 + #gradient_mapping_function: Chen & Herring, 1992 # (string) Name of mapping function used for mapping horizontal troposphere gradients + ocean_tide_loading_model: FES2004 # (string) Ocean tide loading model applied + reference_system: igs20 # (string) Terrestrial Reference System Code + time_system: G # (string) Time system - e.g. "G", "UTC" + + network_statistics: + output: true # (bool) Enable exporting network statistics data to file + directory: ./ # (string) Directory to export network statistics data + filename: _network_statistics.json # (string) Network statistics data filename + + pos: + output: true # (bool) Enable exporting position data to file + directory: ./ # (string) Directory to export position data + # filename: _pos.csv # (string) Position data filename + +satellite_options: + global: + antenna_boresight: [0, 0, +1] + antenna_azimuth: [0, +1, 0] + # L64: + # orbit_propagation: + # solar_radiation_pressure: NONE + # drag: false + # antenna_thrust: false + # albedo: NONE + # empirical: true #true/false => false/ecom/srf + # planetary_perturbations: + # [ + # moon, + # sun, + # mercury, + # venus, + # mars, + # jupiter, + # saturn, + # uranus, + # neptune, + # pluto, + # ] + + # mass: 601.214 + # area: 0.9551567 + # srp_cr: 1.25 + # drag_cd: 2.2 + +receiver_options: # Options to configure individual stations or global configs + global: + rec_reference_system: GPS + models: + eop: + enable: true + L64: + antenna_type: "ANTTYPE" + receiver_type: "TRIG" + models: + eccentricity: + enable: true + offset: [0.2602, -0.0013, -0.4862] # ENU + + phase_windup: + enable: true + relativity2: + enable: true + relativity: + enable: true + sagnac: + enable: true + tides: + enable: false + attitude: + enable: true # (bool) Enables non-nominal attitude types + sources: [PRECISE, MODEL, NOMINAL] #[PRECISE, MODEL, NOMINAL] # List of sourecs to use for attitudes + troposphere: + enable: false + pco: + enable: true # (bool) Enable modelling of phase center offsets + eop: + enable: true + ionospheric_components: + enable: true + antenna_boresight: [0, 0, -1] + antenna_azimuth: [+1, 0, 0] + sat_id: "L64" + rinex2: + rnx_code_conversions: + P1: l1w + P2: l2w + rnx_phase_conversions: + L1: l1w + L2: l2w + + error_model: elevation_dependent # uniform, elevation_dependent + elevation_mask: 1 + code_sigma: 0.2 + phase_sigma: 0.002 # F0, F1, F2, F5, F6, F7, F8 + clock_codes: [l1w, l2w] + +processing_options: + # orbit_propagation: + # central_force: true + # indirect_J2: true + # egm_field: true + # solid_earth_tide: true + # ocean_tide: true + # atm_tide: true + # aod: false + # general_relativity: true + # pole_tide_ocean: true + # pole_tide_solid: true + + # egm_degree: 120 + # integrator_time_step: 10 + process_modes: + preprocessor: true + spp: true + ppp: true + ionosphere: false # Compute Ionosphere models based on GNSS measurements + slr: false # Process SLR observations + + epoch_control: + epoch_interval: 10 #seconds + 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. + max_rec_latency: 10 + #max_epochs: 50 + #start_epoch: 2019-02-14 00:00:00 + #end_epoch: 2019-02-14 00:59:59 + + gnss_general: + minimise_sat_clock_offsets: + enable: false + sys_options: + gps: + process: true + ambiguity_resolution: true + reject_eclipse: false + code_priorities: [L1W, L2W] + #network_amb_pivot: false # Constrain: set of ambiguities, to eliminate network rank deficiencies + #receiver_amb_pivot: false # Constrain: set of ambiguities, to eliminate receiver rank deficiencies + + leo: + process: true + + model_error_handling: + meas_deweighting: # Measurements that are outside the expected confidence bounds may be deweighted so that outliers do not contaminate the filtered solution + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all rejected measurement + state_deweighting: # Any "state" errors cause deweighting of all measurements that reference the state + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all referencing measurements + ambiguities: + phase_reject_limit: 2 # Reset ambiguity after 2 large fractional residuals are found (replaces phase_reject_count:) + reset_on: # Reset ambiguities when slip is detected by the following + gf: true # GF test + lli: false # LLI test + mw: true # MW test + scdia: true # SCDIA test + exclusions: + gf: true # (bool) Exclude measurements that fail GF slip test in preprocessor + lli: false # (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 + + preprocessor: # Configurations for the kalman filter and its sub processes + cycle_slips: # Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised + mw_process_noise: 0 # Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips + slip_threshold: 0.5 # Value used to determine when a slip has occurred + preprocess_all_data: true + + spp: + always_reinitialise: false # Reset SPP state to zero to avoid potential for lock-in of bad states + max_lsq_iterations: 12 # Maximum number of iterations of least squares allowed for convergence + outlier_screening: + raim: + enable: true # Enable Receiver Autonomous Integrity Monitoring + max_gdop: 30 # Maximum dilution of precision before error is flagged + + ambiguity_resolution: # Warning: lambda_bie is NOT recommended for network processing, use bootst option instead + elevation_mask: 15 + lambda_set_size: 200 + mode: off + success_rate_threshold: 0.99 + solution_ratio_threshold: 30 + fix_and_hold: true + # once_per_epoch: false + + ppp_filter: + outlier_screening: + chi_square: + enable: false # Enable Chi-square test + mode: INNOVATION # Chi-square test mode {none,innovation,measurement,state} + prefit: + max_iterations: 3 # Maximum number of measurements to exclude using prefit checks before attempting to filter + omega_test: false # Enable omega-test + sigma_check: true # Enable sigma check + state_sigma_threshold: 4 # Sigma threshold + meas_sigma_threshold: 4 # Sigma threshold + + postfit: + max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter + sigma_check: true # Enable sigma check + state_sigma_threshold: 4 # Sigma threshold + meas_sigma_threshold: 4 # Sigma threshold + + ionospheric_components: # Slant ionospheric components + common_ionosphere: true # Use the same ionosphere state for code and phase observations + use_gf_combo: false # Combine 'uncombined' measurements to simulate a geometry-free solution + use_if_combo: true # Combine 'uncombined' measurements to simulate an ionosphere-free solution + corr_mode: iono_free_linear_combo + + chunking: + by_receiver: false # Split large filter and measurement matrices blockwise by receiver ID to improve processing speed + by_satellite: false # Split large filter and measurement matrices blockwise by satellite ID to improve processing speed + size: 0 + + rts: # Rauch-Tung-Striebel (RTS) backwards smoothing + enable: true + lag: -1 + filename: _.rts + +estimation_parameters: + receivers: + L64: + pos: + estimated: [true] + sigma: [30] + pos_rate: + estimated: [true] + sigma: [5000] + process_noise: [1000] + + clock: + estimated: [true] + sigma: [500] + process_noise: [500] # [100] + # apriori_values: [60] + + ambiguities: + estimated: [true] + sigma: [60] + process_noise: [0] + outage_limit: [50] + # process_noise_dt: day + + ion_stec: + estimated: [true] + sigma: [1000] + process_noise: [100] + #process_noise_dt: second + #apriori_value: [0] + #comment: [""] + #mu: [0] + #tau: [-1] + +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: + primary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison + # secondary_database: + # secondary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison + # secondary_uri: mongodb://localhost:27017 # Location and port of the mongo database to connect to + # output_config: NONE # Output config {none,primary,secondary,both} + 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} + output_trace: primary + +debug: + # output_mincon: true + # mincon_only: true diff --git a/exampleConfigs/brdc2sp3.yml b/exampleConfigs/brdc2sp3.yml index c36579f1d..c3b17458d 100644 --- a/exampleConfigs/brdc2sp3.yml +++ b/exampleConfigs/brdc2sp3.yml @@ -1,70 +1,61 @@ inputs: + inputs_root: products/ - inputs_root: products/ - - atx_files: # Antenna models for receivers and satellites in ANTEX format + atx_files: # Antenna models for receivers and satellites in ANTEX format - igs14_2045_plus.atx - snx_files: # SINEX files for meta data and initial position + snx_files: # SINEX files for meta data and initial position - igs19P2062.snx - tables/igs_satellite_metadata_2203_plus.snx - tables/sat_yaw_bias_rate.snx - tables/qzss_yaw_modes.snx - tables/bds_yaw_modes.snx - - satellite_data: - nav_files: # broadcast navigation files - - brdm1990.19p + satellite_data: + nav_files: # broadcast navigation files + - brdm1990.19p outputs: - - outputs_root: outputs// - - metadata: - config_description: brdc2sp3 - analysis_agency: GAA - analysis_centre: Geoscience Australia - analysis_program: AUSACS - rinex_comment: AUSNETWORK1 - - sp3: - output: true - filename: --.sp3 - output_interval: 900 - output_velocities: true - # output_inertial: true - + outputs_root: outputs// + + metadata: + config_description: brdc2sp3 + analysis_agency: GAA + analysis_centre: Geoscience Australia + analysis_program: AUSACS + rinex_comment: AUSNETWORK1 + + sp3: + output: true + filename: --.sp3 + output_interval: 900 + output_velocities: true + # output_inertial: true satellite_options: - - global: - models: - attitude: - enable: true - sources: [ MODEL ] - + global: + models: + attitude: + enable: true + sources: [MODEL] receiver_options: - - global: - elevation_mask: 7 # degrees - + global: + elevation_mask: 7 # degrees processing_options: - - process_modes: - preprocessor: false - - epoch_control: - start_epoch: 2019-07-18 00:00:00 - end_epoch: 2019-07-18 23:45:00 - epoch_interval: 900 # seconds - require_obs: false - - gnss_general: - sys_options: - gps: { process: true } - gal: { process: true } - glo: { process: true } - bds: { process: true } - qzs: { process: true } \ No newline at end of file + process_modes: + preprocessor: false + + epoch_control: + start_epoch: 2019-07-18 00:00:00 + end_epoch: 2019-07-18 23:45:00 + epoch_interval: 900 # 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/exampleConfigs/compare_orbits.yaml b/exampleConfigs/compare_orbits.yaml index 46d26d110..99fbc44bc 100644 --- a/exampleConfigs/compare_orbits.yaml +++ b/exampleConfigs/compare_orbits.yaml @@ -1,53 +1,53 @@ - debug: - compare_orbits: true - + compare_orbits: true inputs: - inputs_root: ./products + inputs_root: ./products - satellite_data: - sp3_files: - - gag20624.sp3 - - igs20624.sp3 + satellite_data: + sp3_files: + - IGS2R03FIN_20191990000_01D_05M_ORB.SP3 + - TUG0R03FIN_20191990000_01D_05M_ORB.SP3 outputs: - outputs_root: ./outputs/compare_orbits - - trace: - level: 3 - output_config: true - output_network: true - output_residuals: true - network_filename: compare_orbits.TRACE + outputs_root: ./outputs/compare_orbits + trace: + level: 3 + output_config: true + output_network: true + output_residuals: true + network_filename: compare_orbits.TRACE satellite_options: - global: - mincon_scale_apriori_sigma: 0.01 - + global: + exclude: true + mincon_scale_apriori_sigma: 1 + GPS: + exclude: false processing_options: - minimum_constraints: - - enable: true - - translation: { estimated: [true] } - rotation: { estimated: [true] } - # scale: { estimated: [true] } - delay: { estimated: [true] } - - outlier_screening: - chi_square: - enable: false # (bool) Enable Chi-square test - mode: none # (enum) Chi-square test mode - innovation, measurement, state {NONE,INNOVATION,MEASUREMENT,STATE} - prefit: - max_iterations: 3 # Maximum number of measurements to exclude using prefit checks before attempting to filter - sigma_check: true # (bool) Enable prefit sigma check - state_sigma_threshold: 3.0 # (float) sigma threshold for states - meas_sigma_threshold: 3.0 # (float) sigma threshold for measurements - postfit: - max_iterations: 20 # Maximum number of measurements to exclude using postfit checks while iterating filter - sigma_check: true # (bool) Enable postfit sigma check - state_sigma_threshold: 3.0 # (float) sigma threshold for states - meas_sigma_threshold: 3.0 # (float) sigma threshold for measurements + minimum_constraints: + enable: true + # once_per_epoch: true + translation: { estimated: [true], sigma: [10] } + rotation: { estimated: [true], sigma: [10] } + # translation_rate: { estimated: [true], sigma: [100]} + # rotation_rate: { estimated: [true], sigma: [100]} + # scale: { estimated: [true], sigma: [10]} + # delay: { estimated: [true], sigma: [10]} + + outlier_screening: + chi_square: + enable: false # (bool) Enable Chi-square test + mode: none # (enum) Chi-square test mode - innovation, measurement, state {NONE,INNOVATION,MEASUREMENT,STATE} + prefit: + max_iterations: 3 # Maximum number of measurements to exclude using prefit checks before attempting to filter + sigma_check: true # (bool) Enable prefit sigma check + state_sigma_threshold: 3.0 # (float) sigma threshold for states + meas_sigma_threshold: 3.0 # (float) sigma threshold for measurements + postfit: + max_iterations: 20 # Maximum number of measurements to exclude using postfit checks while iterating filter + sigma_check: true # (bool) Enable postfit sigma check + state_sigma_threshold: 3.0 # (float) sigma threshold for states + meas_sigma_threshold: 3.0 # (float) sigma threshold for measurements diff --git a/exampleConfigs/fit_sp3_pseudoobs.yaml b/exampleConfigs/fit_sp3_pseudoobs.yaml index 3ce1b31dc..01ad598fc 100644 --- a/exampleConfigs/fit_sp3_pseudoobs.yaml +++ b/exampleConfigs/fit_sp3_pseudoobs.yaml @@ -1,191 +1,203 @@ inputs: + include_yamls: [products/boxwing.yaml] # required if using boxwing model - include_yamls: [ products/boxwing.yaml ] # required if using boxwing model + inputs_root: ./products/ - inputs_root: ./products/ + atx_files: [igs20.atx] + erp_files: [igs96p02.erp] + egm_files: [tables/EGM2008.gfc] # Earth gravity model coefficients file + planetary_ephemeris_files: [tables/DE436.1950.2050] # JPL planetary and lunar ephemerides file - atx_files: [ igs20.atx ] - erp_files: [ igs96p02.erp ] - egm_files: [ tables/EGM2008.gfc ] # Earth gravity model coefficients file - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file + 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] - 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 ] - - snx_files: + snx_files: - IGS1R03SNX_20191950000_07D_07D_CRD.SNX - tables/igs_satellite_metadata_2203_plus.snx - tables/sat_yaw_bias_rate.snx - tables/qzss_yaw_modes.snx - tables/bds_yaw_modes.snx - - satellite_data: - nav_files: - - brdm1990.19p - sp3_files: - - IGS2R03FIN_20191980000_01D_05M_ORB.SP3 - - IGS2R03FIN_20191990000_01D_05M_ORB.SP3 - - IGS2R03FIN_20192000000_01D_05M_ORB.SP3 - - pseudo_observations: - eci_pseudoobs: false - sp3_inputs: - - IGS2R03FIN_20191980000_01D_05M_ORB.SP3 - - IGS2R03FIN_20191990000_01D_05M_ORB.SP3 - - IGS2R03FIN_20192000000_01D_05M_ORB.SP3 + satellite_data: + nav_files: + - brdm1990.19p + sp3_files: + - IGS2R03FIN_20191980000_01D_05M_ORB.SP3 + - IGS2R03FIN_20191990000_01D_05M_ORB.SP3 + - IGS2R03FIN_20192000000_01D_05M_ORB.SP3 + + pseudo_observations: + eci_pseudoobs: false + sp3_inputs: + - IGS2R03FIN_20191980000_01D_05M_ORB.SP3 + - IGS2R03FIN_20191990000_01D_05M_ORB.SP3 + - IGS2R03FIN_20192000000_01D_05M_ORB.SP3 outputs: - - outputs_root: outputs// - - metadata: - config_description: fit_sp3_pseudoobs - analysis_agency: GAA - analysis_centre: Geoscience Australia - analysis_program: Ginan - rinex_comment: AUSNETWORK1 - - trace: - output_receivers: true - output_network: true - level: 3 - directory: ./ - receiver_filename: _.TRACE - network_filename: _.TRACE - output_residuals: true - output_config: true - - log: - output: true - directory: ./ - filename: log_.json - - orbit_ics: - output: true - directory: ./orbit_ics - filename: __orbits.yaml - - sp3: - output: true - directory: ./ - filename: __.sp3 - output_interval: 300 - output_velocities: true - output_inertial: true - orbit_sources: [ KALMAN ] - clock_sources: [ PRECISE ] - - output_rotation: - period: 1 - period_units: day - - -satellite_options: # Options to configure individual satellites, systems, or global configs - - global: - pseudo_sigma: 1 - 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: true - solar_radiation_pressure: boxwing - mass: 1000 - area: 15 - srp_cr: 1.75 - power: 20 - models: - pos: - enable: true - sources: [KALMAN, PRECISE, BROADCAST] - attitude: - enable: true # (bool) Enables non-nominal attitude types - sources: [ NOMINAL ] # List of sourecs to use for attitudes - + outputs_root: outputs// + + metadata: + config_description: fit_sp3_pseudoobs_ + analysis_agency: GAA + analysis_centre: Geoscience Australia + analysis_software: Ginan + rinex_comment: AUSNETWORK1 + + trace: + output_receivers: true + output_network: true + level: 3 + directory: ./ + receiver_filename: _.TRACE + network_filename: _.TRACE + output_residuals: true + output_config: true + + log: + output: true + directory: ./ + filename: log_.json + + orbit_ics: + output: true + directory: ./orbit_ics + filename: __orbits.yaml + + sp3: + output: true + directory: ./ + filename: __.sp3 + output_interval: 300 + output_velocities: true + output_inertial: true + orbit_sources: [KALMAN] + clock_sources: [PRECISE] + + output_rotation: + period: 1 + period_units: day + +satellite_options: # Options to configure individual satellites, systems, or global configs + global: + pseudo_sigma: 1 + 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: true + solar_radiation_pressure: boxwing + mass: 1000 + area: 15 + srp_cr: 1.75 + power: 20 + models: + pos: + enable: true + sources: [KALMAN, PRECISE, BROADCAST] + attitude: + enable: true # (bool) Enables non-nominal attitude types + sources: [NOMINAL] # List of sourecs to use for attitudes receiver_options: - - global: - models: - eop: - enable: true - + global: + models: + eop: + enable: true processing_options: - - epoch_control: - epoch_interval: 300 # seconds - - - process_modes: - ppp: true - preprocessor: false - spp: false - - gnss_general: - sys_options: - gps: - process: true - - orbit_propagation: - central_force: 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 - integrator_time_step: 900 - egm_degree: 15 - - model_error_handling: - orbit_errors: - enable: true - pos_process_noise: 100 - vel_process_noise: 1 - vel_process_noise_trail: 1 - vel_process_noise_trail_tau: 900 - + epoch_control: + epoch_interval: 300 # seconds + + process_modes: + ppp: true + preprocessor: false + spp: false + + gnss_general: + sys_options: + gps: + process: true + + orbit_propagation: + central_force: 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 + integrator_time_step: 900 + egm_degree: 15 + + ppp_filter: + outlier_screening: + prefit: + sigma_check: false + omega_test: false + postfit: + max_iterations: 10 + sigma_check: false + omega_test: true + + model_error_handling: + satellite_errors: + enable: true + pos_process_noise: 100 + vel_process_noise: 1 + vel_process_noise_trail: 1 + vel_process_noise_trail_tau: 900 estimation_parameters: + global_models: + eop: + estimated: [true] + sigma: [10] + eop_rates: + estimated: [true] + sigma: [10] + + satellites: + global: + orbit: + estimated: [true] + sigma: + [1] + #cr: + #estimated: [true] + #sigma: [1] + emp_d_0: { estimated: [true], sigma: [1e3] } + emp_y_0: { estimated: [true], sigma: [1e3] } + emp_b_0: { estimated: [true], sigma: [1e3] } - global_models: - eop: - estimated: [ true ] - sigma: [ 10 ] - eop_rates: - estimated: [ true ] - sigma: [ 10 ] - - satellites: - global: - orbit: - estimated: [true] - sigma: [1] - - 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_b_1: { estimated: [true], sigma: [1e3]} - - emp_d_2: { estimated: [true], sigma: [1e2]} + emp_d_1: { estimated: [true], sigma: [1e3] } + emp_b_1: { estimated: [true], sigma: [1e3] } - emp_d_4: { estimated: [true], sigma: [1e3]} + emp_d_2: { estimated: [true], sigma: [1e2] } + emp_d_4: { estimated: [true], sigma: [1e3] } mongo: - enable: primary - primary_database: - output_measurements: primary - output_states: primary - delete_history: primary \ No newline at end of file + enable: primary + primary_database: + output_measurements: primary + output_states: primary + delete_history: primary diff --git a/exampleConfigs/pod_example.yaml b/exampleConfigs/pod_example.yaml index 94a6cce8e..820af8907 100644 --- a/exampleConfigs/pod_example.yaml +++ b/exampleConfigs/pod_example.yaml @@ -1,497 +1,513 @@ 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: [finals.data.iau2000.txt] - planetary_ephemeris_files: [tables/DE436.1950.2050] - - troposphere: - gpt2grid_files: [tables/gpt_25.grd] - - tides: - ocean_tide_loading_blq_files: [tables/OLOAD_GO.BLQ] # required if ocean loading is applied - atmos_tide_loading_blq_files: [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 - - tables/igs_satellite_metadata_2203_plus.snx - - tables/sat_yaw_bias_rate.snx - - tables/qzss_yaw_modes.snx - - tables/bds_yaw_modes.snx - - IGS1R03SNX_20191950000_07D_07D_CRD.SNX - - satellite_data: - nav_files: [brdm1990.19p] - clk_files: [IGS2R03FIN_20191990000_01D_30S_CLK.CLK] - - gnss_observations: - gnss_observations_root: ../data/ - rnx_inputs: - - AREG00PER_R_20191990000_01D_30S_MO.rnx - - ASCG00SHN_R_20191990000_01D_30S_MO.rnx - - CEDU00AUS_R_20191990000_01D_30S_MO.rnx - - COCO00AUS_R_20191990000_01D_30S_MO.rnx - - CPVG00CPV_R_20191990000_01D_30S_MO.rnx - - DARW00AUS_R_20191990000_01D_30S_MO.rnx - - DGAR00GBR_R_20191990000_01D_30S_MO.rnx - - DJIG00DJI_R_20191990000_01D_30S_MO.rnx - - FAIR00USA_R_20191990000_01D_30S_MO.rnx - - HERS00GBR_R_20191990000_01D_30S_MO.rnx - - HOB200AUS_R_20191990000_01D_30S_MO.rnx - - IISC00IND_R_20191990000_01D_30S_MO.rnx - - JFNG00CHN_R_20191990000_01D_30S_MO.rnx - - KARR00AUS_R_20191990000_01D_30S_MO.rnx - - KIRI00KIR_R_20191990000_01D_30S_MO.rnx - - KOKV00USA_R_20191990000_01D_30S_MO.rnx - - LHAZ00CHN_R_20191990000_01D_30S_MO.rnx - - LMMF00MTQ_R_20191990000_01D_30S_MO.rnx - - MAW100ATA_R_20191990000_01D_30S_MO.rnx - - MBAR00UGA_R_20191990000_01D_30S_MO.rnx - - METG00FIN_R_20191990000_01D_30S_MO.rnx - - MGUE00ARG_R_20191990000_01D_30S_MO.rnx - - NICO00CYP_R_20191990000_01D_30S_MO.rnx - - NKLG00GAB_R_20191990000_01D_30S_MO.rnx - - OHI300ATA_R_20191990000_01D_30S_MO.rnx - - POAL00BRA_R_20191990000_01D_30S_MO.rnx - - QUIN00USA_R_20191990000_01D_30S_MO.rnx - - REYK00ISL_R_20191990000_01D_30S_MO.rnx - - RGDG00ARG_R_20191990000_01D_30S_MO.rnx - - SAMO00WSM_R_20191990000_01D_30S_MO.rnx - - SEY200SYC_R_20191990000_01D_30S_MO.rnx - - SOLO00SLB_R_20191990000_01D_30S_MO.rnx - - TONG00TON_R_20191990000_01D_30S_MO.rnx - - TOPL00BRA_R_20191990000_01D_30S_MO.rnx - - TOW200AUS_R_20191990000_01D_30S_MO.rnx - - USN700USA_R_20191990000_01D_30S_MO.rnx - - VACS00MUS_R_20191990000_01D_30S_MO.rnx - - ZIM200CHE_R_20191990000_01D_30S_MO.rnx - - CUSV00THA_R_20191990000_01D_30S_MO.rnx + 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/igrf14coeffs.txt] + erp_files: [finals.data.iau2000.txt] + planetary_ephemeris_files: [tables/DE436.1950.2050] + + troposphere: + gpt2grid_files: [tables/gpt_25.grd] + + tides: + ocean_tide_loading_blq_files: [tables/OLOAD_GO.BLQ] # required if ocean loading is applied + atmos_tide_loading_blq_files: [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 + - tables/igs_satellite_metadata_2203_plus.snx + - tables/sat_yaw_bias_rate.snx + - tables/qzss_yaw_modes.snx + - tables/bds_yaw_modes.snx + - IGS1R03SNX_20191950000_07D_07D_CRD.SNX + + satellite_data: + nav_files: [brdm1990.19p] + clk_files: [IGS2R03FIN_20191990000_01D_30S_CLK.CLK] + + pseudo_observations: # Use data from pre-processed data products as observations + eci_pseudoobs: false # Pseudo observations are provided in eci frame rather than standard ECEF SP3 files + sp3_inputs: [IGS2R03FIN_20191990000_01D_05M_ORB.SP3] + + gnss_observations: + gnss_observations_root: ../data/ + rnx_inputs: + - AREG00PER_R_20191990000_01D_30S_MO.rnx + - ASCG00SHN_R_20191990000_01D_30S_MO.rnx + - CEDU00AUS_R_20191990000_01D_30S_MO.rnx + - COCO00AUS_R_20191990000_01D_30S_MO.rnx + - CPVG00CPV_R_20191990000_01D_30S_MO.rnx + - DARW00AUS_R_20191990000_01D_30S_MO.rnx + - DGAR00GBR_R_20191990000_01D_30S_MO.rnx + - DJIG00DJI_R_20191990000_01D_30S_MO.rnx + - FAIR00USA_R_20191990000_01D_30S_MO.rnx + - HERS00GBR_R_20191990000_01D_30S_MO.rnx + - HOB200AUS_R_20191990000_01D_30S_MO.rnx + - IISC00IND_R_20191990000_01D_30S_MO.rnx + - JFNG00CHN_R_20191990000_01D_30S_MO.rnx + - KARR00AUS_R_20191990000_01D_30S_MO.rnx + - KIRI00KIR_R_20191990000_01D_30S_MO.rnx + - KOKV00USA_R_20191990000_01D_30S_MO.rnx + - LHAZ00CHN_R_20191990000_01D_30S_MO.rnx + - LMMF00MTQ_R_20191990000_01D_30S_MO.rnx + - MAW100ATA_R_20191990000_01D_30S_MO.rnx + - MBAR00UGA_R_20191990000_01D_30S_MO.rnx + - METG00FIN_R_20191990000_01D_30S_MO.rnx + - MGUE00ARG_R_20191990000_01D_30S_MO.rnx + - NICO00CYP_R_20191990000_01D_30S_MO.rnx + - NKLG00GAB_R_20191990000_01D_30S_MO.rnx + - OHI300ATA_R_20191990000_01D_30S_MO.rnx + - POAL00BRA_R_20191990000_01D_30S_MO.rnx + - QUIN00USA_R_20191990000_01D_30S_MO.rnx + - REYK00ISL_R_20191990000_01D_30S_MO.rnx + - RGDG00ARG_R_20191990000_01D_30S_MO.rnx + - SAMO00WSM_R_20191990000_01D_30S_MO.rnx + - SEY200SYC_R_20191990000_01D_30S_MO.rnx + - SOLO00SLB_R_20191990000_01D_30S_MO.rnx + - TONG00TON_R_20191990000_01D_30S_MO.rnx + - TOPL00BRA_R_20191990000_01D_30S_MO.rnx + - TOW200AUS_R_20191990000_01D_30S_MO.rnx + - USN700USA_R_20191990000_01D_30S_MO.rnx + - VACS00MUS_R_20191990000_01D_30S_MO.rnx + - ZIM200CHE_R_20191990000_01D_30S_MO.rnx + - CUSV00THA_R_20191990000_01D_30S_MO.rnx outputs: - outputs_root: ./outputs/ - - metadata: - config_description: pod_example_ - analysis_agency: GAA - analysis_centre: Geoscience Australia -----FILE NOT FOR OPERATIONAL USE----- - analysis_software: Ginan v3.0 - rinex_comment: AUSNETWORK1 - gradient_mapping_function: Chen & Herring, 1992 # (string) Name of mapping function used for mapping horizontal troposphere gradients - ocean_tide_loading_model: FES2014 # (string) Ocean tide loading model applied - reference_system: igs20 # (string) Terrestrial Reference System Code - time_system: G # (string) Time system - e.g. "G", "UTC" - - trace: - level: 3 - output_receivers: true - output_network: true - receiver_filename: __.TRACE - network_filename: __.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - output_json: true - - network_statistics: - output: true # (bool) Enable exporting network statistics data to file - directory: ./ # (string) Directory to export network statistics data - filename: _network_statistics.json # (string) Network statistics data filename - - gpx: - output: true - pos: - output: true - filename: __.POS - sinex: - output: true - directory: ./ - filename: _.SNX - - sp3: - output: true - filename: _.SP3 - output_interval: 300 # (int) Update interval for sp3 records - - clocks: - output: true - directory: ./ - filename: _.CLK - - # erp: - # output: true - # directory: ./ - # filename: _.ERP - - orbex: - output: true - filename: _.OBX - attitude_sources: [MODEL, NOMINAL] + outputs_root: ./outputs/ + + metadata: + config_description: pod_example_ + analysis_agency: GAA + analysis_centre: Geoscience Australia -----FILE NOT FOR OPERATIONAL USE----- + analysis_software: Ginan v3.0 + rinex_comment: AUSNETWORK1 + gradient_mapping_function: Chen & Herring, 1992 # (string) Name of mapping function used for mapping horizontal troposphere gradients + ocean_tide_loading_model: FES2014 # (string) Ocean tide loading model applied + reference_system: igs20 # (string) Terrestrial Reference System Code + time_system: G # (string) Time system - e.g. "G", "UTC" + + trace: + level: 3 + output_receivers: true + output_network: true + receiver_filename: __.TRACE + network_filename: __.TRACE + output_residuals: true + output_residual_chain: false + output_config: true + output_json: false + + network_statistics: + output: true # (bool) Enable exporting network statistics data to file + directory: ./ # (string) Directory to export network statistics data + filename: _network_statistics.json # (string) Network statistics data filename + + gpx: + output: true + pos: + output: true + filename: __.POS + sinex: + output: true + directory: ./ + filename: _.SNX + + sp3: + output: true + filename: _.SP3 + output_interval: 300 # (int) Update interval for sp3 records + + clocks: + output: true + directory: ./ + filename: _.CLK + + # erp: + # output: true + # directory: ./ + # filename: _.ERP + + orbex: + output: true + filename: _.OBX + attitude_sources: [MODEL, NOMINAL] 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 - default_bias: 0 - undefined_sigma: 0 - phase_bias: - enable: false - default_bias: 0 - undefined_sigma: 0 - - 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 } + 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 - USN7: - aliases: [PIVOT] + USN7: + aliases: [PIVOT] + + global: + error_model: elevation_dependent # uniform, elevation_dependent + elevation_mask: 10 + code_sigma: 0.4 + phase_sigma: 0.002 + 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 - global: - error_model: elevation_dependent # uniform, elevation_dependent - elevation_mask: 10 - code_sigma: 0.4 - phase_sigma: 0.002 - 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 - default_bias: 0 # (float) Bias to use when no code bias is found - undefined_sigma: 0 # (float) Uncertainty sigma to apply to default code biases - phase_bias: - enable: false # (bool) Enable modelling of phase biases - default_bias: 0 # (float) Bias to use when no phase bias is found - undefined_sigma: 0 # (float) Uncertainty sigma to apply to default 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 - - 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 - #ABMF: {mincon_scale_apriori_sigma: 3 } - #ALBH: {mincon_scale_apriori_sigma: 3 } - #ALGO: {mincon_scale_apriori_sigma: 3 } + 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 + # ABMF: {mincon_scale_apriori_sigma: 3 } + # ALBH: {mincon_scale_apriori_sigma: 3 } + # ALGO: {mincon_scale_apriori_sigma: 3 } processing_options: - process_modes: - preprocessor: true - spp: true - ppp: true - ionosphere: false - - epoch_control: - epoch_interval: 300 - 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. - max_rec_latency: 1 - - gnss_general: - minimise_sat_clock_offsets: true - pivot_receiver: - sys_options: - gps: - process: true - ambiguity_resolution: false - reject_eclipse: false - code_priorities: [L1W, L1C, L2W] - # gal: - # process: true - # ambiguity_resolution: false - # reject_eclipse: false - # code_priorities: [ L1C, L5Q, L1X, L5X ] - # glo: - # process: true - # ambiguity_resolution: false - # reject_eclipse: true - # code_priorities: [ L1P, L1C, L2P, L2C ] - # qzs: - # process: true - # ambiguity_resolution: false - # reject_eclipse: true - # code_priorities: [ L1C, L2L, L2X ] - - spp: - always_reinitialise: false - max_lsq_iterations: 12 - outlier_screening: - max_gdop: 30 - postfit: - sigma_check: true - - ppp_filter: - ionospheric_components: - common_ionosphere: true - use_if_combo: false - outlier_screening: - prefit: - max_iterations: 2 - sigma_check: true - state_sigma_threshold: 5 # Sigma threshold for states - meas_sigma_threshold: 5 # Sigma threshold for measurements - omega_test: false - postfit: - max_iterations: 10 - sigma_check: true - state_sigma_threshold: 3 # Sigma threshold for states - meas_sigma_threshold: 3 # Sigma threshold for measurements - - rts: - enable: true - - 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 - - minimum_constraints: + process_modes: + preprocessor: true + spp: true + ppp: true + ionosphere: false + + epoch_control: + epoch_interval: 300 + 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. + max_rec_latency: 1 + + gnss_general: + minimise_sat_clock_offsets: + enable: true + pivot_receiver: + sys_options: + gps: + process: true + ambiguity_resolution: false + reject_eclipse: false + code_priorities: [L1W, L1C, L2W] + # gal: + # process: true + # ambiguity_resolution: false + # reject_eclipse: false + # code_priorities: [L1C, L5Q, L1X, L5X] + # glo: + # process: true + # ambiguity_resolution: false + # reject_eclipse: true + # code_priorities: [L1P, L1C, L2P, L2C] + # qzs: + # process: true + # ambiguity_resolution: false + # reject_eclipse: true + # code_priorities: [L1C, L2L, L2X] + + spp: + outlier_screening: + chi_square: enable: true - rotation: - estimated: [true] - scale: - estimated: [true] - translation: - estimated: [true] - application_mode: weight_matrix - # once_per_epoch: true - constrain_orbits: false - outlier_screening: # Statistical checks allow for detection of outliers that exceed their confidence intervals - postfit: - max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter - sigma_check: true # Enable sigma check - state_sigma_threshold: 3 # Sigma threshold for states - meas_sigma_threshold: 3 # Sigma threshold for measurements - prefit: - max_iterations: 2 # Maximum number of measurements to exclude using prefit checks before attempting to filter - omega_test: false # Enable omega-test - sigma_check: true # Enable sigma check - state_sigma_threshold: 5 # Sigma threshold for states - meas_sigma_threshold: 5 # Sigma threshold for measurements - - orbit_propagation: - integrator_time_step: 60 # 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 + sigma_threshold: 5 + least_square: + max_iterations: 1 + sigma_check: false + omega_test: true + meas_sigma_threshold: 5 + raim: + enable: true + max_iterations: 2 + ppp_filter: + ionospheric_components: + common_ionosphere: true + use_if_combo: false + outlier_screening: + prefit: + max_iterations: 2 + sigma_check: false + omega_test: false + state_sigma_threshold: 5 # Sigma threshold for states + meas_sigma_threshold: 5 # Sigma threshold for measurements + postfit: + max_iterations: 20 + sigma_check: false + omega_test: true + state_sigma_threshold: 8 # Sigma threshold for states + meas_sigma_threshold: 6 # Sigma threshold for measurements + + rts: + enable: true + filename: Filter---.rts + + model_error_handling: + error_accumulation: # Any receivers or satellites that are consistently getting many measurement rejections may be reinitialiased + enable: true # Enable reinitialisation of receivers upon many rejections + receiver_error_count_threshold: 0 # Number of errors for a receiver to be considered in error for a single epoch + receiver_error_epochs_threshold: 0 # Number of consecutive epochs with receiver in error before it is removed and reinitialised + satellite_error_count_threshold: 0 # Number of errors for a satellite to be considered in error for a single epoch + satellite_error_epochs_threshold: 0 # Number of consecutive epochs with satellite in error before it is reinitialised using the orbit_errors configs + state_error_count_threshold: 3 # Number of consecutive epochs with satellite in error before it is reinitialised using the orbit_errors configs + meas_deweighting: + deweight_factor: 10000 + state_deweighting: + deweight_factor: 10000 + ambiguities: + phase_reject_limit: 2 + reset_on: + gf: true + lli: true + mw: true + scdia: true + retrack: true + single_freq: true + exclusions: + gf: true + lli: false + mw: true + scdia: true + eclipse: false + retrack: false + single_freq: true + ionospheric_components: + outage_reset_limit: 300 + satellite_errors: + enable: false + pos_process_noise: 10 + vel_process_noise: 1 + vel_process_noise_trail: 0 + vel_process_noise_trail_tau: 0 + + minimum_constraints: + enable: true + rotation: + sigma: [400] + estimated: [true] + scale: + sigma: [400] + estimated: [true] + translation: + sigma: [400] + estimated: [true] + application_mode: weight_matrix + once_per_epoch: false + constrain_orbits: false + outlier_screening: # Statistical checks allow for detection of outliers that exceed their confidence intervals + postfit: + max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter + sigma_check: true # Enable sigma check + state_sigma_threshold: 3 # Sigma threshold for states + meas_sigma_threshold: 3 # Sigma threshold for measurements + prefit: + max_iterations: 2 # Maximum number of measurements to exclude using prefit checks before attempting to filter + omega_test: false # Enable omega-test + sigma_check: true # Enable sigma check + state_sigma_threshold: 5 # Sigma threshold for states + meas_sigma_threshold: 5 # Sigma threshold for measurements + + orbit_propagation: + integrator_time_step: 60 # 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 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: [1000] - 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] - 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: [100] - #mu: [10000] - code_bias: - estimated: [true] - sigma: [10] - process_noise: [0] - orbit: # Orbital state - estimated: [true] # [bools] Estimate state in kalman filter - sigma: [10, 10, 10, 0.01] - process_noise: [0] - - emp_d_0: { estimated: [true], sigma: [10] } - emp_y_0: { estimated: [true], sigma: [1] } - emp_b_0: { estimated: [true], sigma: [1] } - - # emp_d_1: { estimated: [true], sigma: [1]} - # emp_y_1: { estimated: [true], sigma: [1]} - emp_b_1: { estimated: [true], sigma: [1] } - - emp_d_2: { estimated: [true], sigma: [1] } - # emp_y_2: { estimated: [true], sigma: [1]} - # emp_b_2: { estimated: [true], sigma: [1]} - - # emp_d_3: { estimated: [true], sigma: [1]} - # emp_y_3: { estimated: [true], sigma: [1]} - # emp_b_3: { estimated: [true], sigma: [1]} - - # emp_d_4: { estimated: [true], sigma: [1]} - # emp_y_4: { estimated: [true], sigma: [1]} - # emp_b_4: { estimated: [true], sigma: [1]} + 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: [1000] + process_noise: [10] # [100] + ambiguities: + estimated: [true] + sigma: [1000] + process_noise: [0] + outage_limit: [900] + # 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] + outage_limit: [900] + 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: [100] + # mu: [10000] + code_bias: + estimated: [true] + sigma: [10] + process_noise: [0] + orbit: # Orbital state + estimated: [true] # [bools] Estimate state in kalman filter + sigma: [10, 10, 10, 0.01] + process_noise: [0] + + emp_d_0: { estimated: [true], sigma: [10] } + emp_y_0: { estimated: [true], sigma: [1] } + emp_b_0: { estimated: [true], sigma: [1] } + + # emp_d_1: { estimated: [true], sigma: [1]} + # emp_y_1: { estimated: [true], sigma: [1]} + emp_b_1: { estimated: [true], sigma: [1] } + + emp_d_2: { estimated: [true], sigma: [1] } + # emp_y_2: { estimated: [true], sigma: [1]} + # emp_b_2: { estimated: [true], sigma: [1]} + + # emp_d_3: { estimated: [true], sigma: [1]} + # emp_y_3: { estimated: [true], sigma: [1]} + # emp_b_3: { estimated: [true], sigma: [1]} + + # emp_d_4: { estimated: [true], sigma: [1]} + # emp_y_4: { estimated: [true], sigma: [1]} + # emp_b_4: { estimated: [true], sigma: [1]} mongo: - enable: primary - #enable: none - output_components: primary - output_states: primary - output_measurements: primary - output_test_stats: none - output_trace: primary - delete_history: primary - -debug: - # instrument: true - #output_mincon: true - #mincon_filename: preMinconState.bin - #mincon_only: true - # mincon_only: true + enable: primary + # enable: none + output_components: primary + output_states: primary + output_measurements: primary + output_test_stats: none + output_trace: primary + delete_history: primary +# debug: +# instrument: true +# output_mincon: true +# mincon_filename: preMinconState.bin +# mincon_only: true diff --git a/exampleConfigs/ppp_example.yaml b/exampleConfigs/ppp_example.yaml index 7a5ad0e81..f800a0d7b 100644 --- a/exampleConfigs/ppp_example.yaml +++ b/exampleConfigs/ppp_example.yaml @@ -1,299 +1,310 @@ inputs: - inputs_root: ./products/ + inputs_root: ./products/ - atx_files: [igs20.atx] # required - igrf_files: [tables/igrf13coeffs.txt] - erp_files: [finals.data.iau2000.txt] - planetary_ephemeris_files: [tables/DE436.1950.2050] + atx_files: [igs20.atx] # required + igrf_files: [tables/igrf14coeffs.txt] + erp_files: [finals.data.iau2000.txt] + planetary_ephemeris_files: [tables/DE436.1950.2050] - troposphere: - gpt2grid_files: [tables/gpt_25.grd] + troposphere: + gpt2grid_files: [tables/gpt_25.grd] - tides: - ocean_tide_loading_blq_files: [tables/OLOAD_GO.BLQ] # required if ocean loading is applied - atmos_tide_loading_blq_files: [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 + tides: + ocean_tide_loading_blq_files: [tables/OLOAD_GO.BLQ] # required if ocean loading is applied + atmos_tide_loading_blq_files: [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 - snx_files: - # - "*.SNX" # use a wild card to include all files matching the description in the directory - - igs_satellite_metadata.snx - - tables/sat_yaw_bias_rate.snx - - tables/qzss_yaw_modes.snx - - tables/bds_yaw_modes.snx - - IGS1R03SNX_20191950000_07D_07D_CRD.SNX + snx_files: + # - "*.SNX" # use a wild card to include all files matching the description in the directory + - igs_satellite_metadata.snx + - tables/sat_yaw_bias_rate.snx + - tables/qzss_yaw_modes.snx + - tables/bds_yaw_modes.snx + - IGS1R03SNX_20191950000_07D_07D_CRD.SNX - satellite_data: - # nav_files: - # - "*.rnx" - clk_files: - # - "*.CLK" - - IGS2R03FIN_20191990000_01D_30S_CLK.CLK - bsx_files: - # - "*.BIA" - - IGS2R03FIN_20191990000_01D_01D_OSB.BIA - sp3_files: - # - "*.SP3" - - IGS2R03FIN_20191990000_01D_05M_ORB.SP3 + satellite_data: + nav_files: + - brdm1990.19p + clk_files: + # - "*.CLK" + - IGS2R03FIN_20191990000_01D_30S_CLK.CLK + bsx_files: + # - "*.BIA" + - IGS2R03FIN_20191990000_01D_01D_OSB.BIA + # - TUG0R03FIN_20191990000_01D_01D_OSB.BIA + sp3_files: + # - "*.SP3" + - IGS2R03FIN_20191990000_01D_05M_ORB.SP3 - gnss_observations: - gnss_observations_root: ../data/ - rnx_inputs: - # - "*.rnx" - - ALIC00AUS_R_20191990000_01D_30S_MO.rnx - # - DARW00AUS_R_20191990000_01D_30S_MO.rnx - # - HOB200AUS_R_20191990000_01D_30S_MO.rnx - # - "M*.rnx" + gnss_observations: + gnss_observations_root: ../data/ + rnx_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 + # - "M*.rnx" outputs: - metadata: - config_description: ppp_example_ + metadata: + config_description: ppp_example_ - outputs_root: ./outputs/ + outputs_root: ./outputs/ - gpx: - output: true - filename: __.GPX - pos: - output: true - filename: __.POS - trace: - level: 2 - output_receivers: true - output_network: true - receiver_filename: __.TRACE - network_filename: __.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - output_initialised_states: true - output_predicted_states: true + gpx: + output: true + filename: __.GPX + pos: + output: true + filename: .POS + trace: + level: 2 + output_receivers: true + output_network: true + receiver_filename: __.TRACE + network_filename: __.TRACE + output_residuals: true + output_residual_chain: true + output_config: true + output_initialised_states: false + output_predicted_states: false satellite_options: - global: - error_model: ELEVATION_DEPENDENT # {uniform,elevation_dependent} - code_sigma: 0 # Standard deviation of code measurements - phase_sigma: 0 # Standard deviation of phase measurmeents - models: - phase_bias: - enable: false + global: + error_model: ELEVATION_DEPENDENT # {uniform,elevation_dependent} + code_sigma: 0 # Standard deviation of code measurements + phase_sigma: 0 # Standard deviation of phase measurmeents + models: + phase_bias: + enable: true - # E05: - # exclude: true # Exclude satellites - # E06: - # exclude: true + # E05: + # exclude: true # Exclude satellites + # E06: + # exclude: true 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 - clock_codes: [AUTO, AUTO] - zero_dcb_codes: [AUTO, AUTO] - rec_reference_system: GPS - models: - phase_bias: - enable: false - troposphere: # Tropospheric modelling accounts for delays due to refraction of light in water vapour - enable: true - models: [gpt2] # List of models to use for troposphere [standard,sbas,vmf3,gpt2,cssr] - tides: - atl: true # Enable atmospheric tide loading - enable: true # Enable modelling of tidal disaplacements - opole: true # Enable ocean pole tides - otl: true # Enable ocean tide loading - solid: true # Enable solid Earth tides - spole: true # Enable solid Earth pole tides - # ALIC: - # receiver_type: "LEICA GR25" # (string) - # antenna_type: "LEIAR25.R3 NONE" # (string) - # apriori_position: [-4052052.7254, 4212835.9872,-2545104.6139] # [floats] - # aliases: [PIVOT] # set as pivot station - # models: - # eccentricity: - # enable: true - # offset: [0.0000, 0.0000, 0.0015] # [floats] + 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) + clock_codes: [AUTO, AUTO] + zero_dcb_codes: [AUTO, AUTO] + rec_reference_system: GPS + models: + phase_bias: + enable: false + troposphere: # Tropospheric modelling accounts for delays due to refraction of light in water vapour + enable: true + models: [gpt2] # List of models to use for troposphere [standard,sbas,vmf3,gpt2,cssr] + tides: + enable: true # Enable modelling of tidal disaplacements + solid: true # Enable solid Earth tides + otl: true # Enable ocean tide loading + atl: true # Enable atmospheric tide loading + spole: true # Enable solid Earth pole tides + opole: true # Enable ocean pole tides + ionospheric_components: + use_2nd_order: true + use_3rd_order: true + + # ALIC: + # receiver_type: "LEICA GR25" # (string) + # antenna_type: "LEIAR25.R3 NONE" # (string) + # apriori_position: [-4052052.7254, 4212835.9872, -2545104.6139] # [floats] + # aliases: [PIVOT] # set as pivot station + # models: + # eccentricity: + # enable: true + # offset: [0.0000, 0.0000, 0.0015] # [floats] processing_options: - process_modes: - preprocessor: true # Preprocessing and quality checks - spp: true # Perform SPP on receiver data - ppp: true # Perform PPP network or end user mode - ionosphere: false # Compute Ionosphere models based on GNSS measurements - slr: false # Process SLR observations + process_modes: + preprocessor: true # Preprocessing and quality checks + spp: true # Perform SPP on receiver data + ppp: true # Perform PPP network or end user mode + ionosphere: false # Compute Ionosphere models based on GNSS measurements + slr: false # Process SLR observations - epoch_control: - # start_epoch: 2019-07-18 00:00:00 - # end_epoch: 2019-07-18 23:59:30 - # max_epochs: 2880 - epoch_interval: 30 # seconds - wait_next_epoch: 3600 # seconds (make large for post-processing) + epoch_control: + # start_epoch: 2019-07-18 00:00:00 + # end_epoch: 2019-07-18 23:59:30 + #max_epochs: 30 + epoch_interval: 30 # seconds + wait_next_epoch: 3600 # seconds (make large for post-processing) - gnss_general: - add_eop_component: true - sys_options: - gps: - process: true - reject_eclipse: false - # clock_codes: [ L1W, L2W ] - code_priorities: [L1W, L1C, L2W] + gnss_general: + add_eop_component: true + use_primary_signals: true + sys_options: + gps: + process: true + reject_eclipse: false + code_priorities: [L1W, L1C, L2W, L2S] + # code_priorities: [L1W, L1C, L2W, L2S, L5Q, L5X] - gal: - # process: true - reject_eclipse: false - code_priorities: [L1C, L5Q, L1X, L5X] + gal: + process: true + reject_eclipse: false + code_priorities: [L1C, L1X, L5Q, L5X] - preprocessor: # Configurations for the kalman filter and its sub processes - cycle_slips: # Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised - mw_process_noise: 0 # Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips - slip_threshold: 0.05 # Value used to determine when a slip has occurred - preprocess_all_data: true + glo: + process: true + reject_eclipse: false + code_priorities: [L1C, L1P, L2C, L2P] - spp: - # always_reinitialise: false # Reset SPP state to zero to avoid potential for lock-in of bad states - max_lsq_iterations: 12 # Maximum number of iterations of least squares allowed for convergence - outlier_screening: - raim: true # Enable Receiver Autonomous Integrity Monitoring - max_gdop: 30 # Maximum dilution of precision before error is flagged + # phase_measurements: + # process: false - ppp_filter: - outlier_screening: - chi_square: - enable: false # Enable Chi-square test - mode: innovation # Chi-square test mode {none,innovation,measurement,state} - prefit: - max_iterations: 3 # Maximum number of measurements to exclude using prefit checks before attempting to filter - omega_test: false # Enable omega-test - sigma_check: true # Enable sigma check - state_sigma_threshold: 4 # Sigma threshold for states - meas_sigma_threshold: 4 # Sigma threshold for measurements - postfit: - max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter - sigma_check: true # Enable sigma check - state_sigma_threshold: 4 # Sigma threshold for states - meas_sigma_threshold: 4 # Sigma threshold for measurements + preprocessor: # Configurations for the kalman filter and its sub processes + cycle_slips: # Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised + mw_process_noise: 0 # Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips + slip_threshold: 0.05 # Value used to determine when a slip has occurred + preprocess_all_data: true - ionospheric_components: # Slant ionospheric components - common_ionosphere: true # Use the same ionosphere state for code and phase observations - use_gf_combo: false # Combine 'uncombined' measurements to simulate a geometry-free solution - use_if_combo: false # Combine 'uncombined' measurements to simulate an ionosphere-free solution + spp: + outlier_screening: + chi_square: + enable: true + sigma_threshold: 5 + least_square: + max_iterations: 1 + sigma_check: false + omega_test: true + meas_sigma_threshold: 5 + raim: + enable: true + max_iterations: 2 + ppp_filter: + outlier_screening: + chi_square: + enable: false # Enable Chi-square test + mode: innovation # Chi-square test mode {none,innovation,measurement,state} + prefit: + max_iterations: 3 # Maximum number of measurements to exclude using prefit checks before attempting to filter + omega_test: false # Enable omega-test + sigma_check: false # Enable sigma check + state_sigma_threshold: 4 # Sigma threshold for states + meas_sigma_threshold: 4 # Sigma threshold for measurements + postfit: + max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter + omega_test: true # Enable omega-test + sigma_check: false # Enable sigma check + state_sigma_threshold: 8 # Sigma threshold for states + meas_sigma_threshold: 6 # Sigma threshold for measurements - chunking: - by_receiver: true # Split large filter and measurement matrices blockwise by receiver ID to improve processing speed - by_satellite: false # Split large filter and measurement matrices blockwise by satellite ID to improve processing speed - size: 0 + ionospheric_components: # Slant ionospheric components + common_ionosphere: true # Use the same ionosphere state for code and phase observations + use_gf_combo: false # Combine 'uncombined' measurements to simulate a geometry-free solution + use_if_combo: false # Combine 'uncombined' measurements to simulate an ionosphere-free solution - rts: # Rauch-Tung-Striebel (RTS) backwards smoothing - 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 + chunking: + by_receiver: true # Split large filter and measurement matrices blockwise by receiver ID to improve processing speed + size: 0 - periodic_reset: - # enable: true - # interval: 86400 - # states: [REC_POS] + rts: # Rauch-Tung-Striebel (RTS) backwards smoothing + enable: true + lag: -1 + # interval: 86400 + filename: _.rts - model_error_handling: - meas_deweighting: # Measurements that are outside the expected confidence bounds may be deweighted so that outliers do not contaminate the filtered solution - deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors - enable: true # Enable deweighting of all rejected measurement - state_deweighting: # Any "state" errors cause deweighting of all measurements that reference the state - deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors - enable: true # Enable deweighting of all referencing measurements - error_accumulation: - enable: true - receiver_error_count_threshold: 4 - receiver_error_epochs_threshold: 4 - ambiguities: - outage_reset_limit: 300 - phase_reject_limit: 2 # Reset ambiguity after 2 large fractional residuals are found (replaces phase_reject_count:) - reset_on: # Reset ambiguities when slip is detected by the following - gf: true # GF test - lli: true # LLI test - mw: true # MW test - scdia: true # SCDIA test + # periodic_reset: + # enable: true + # interval: 86400 + # states: [REC_POS] -estimation_parameters: - global_models: - eop: - # estimated: [true] # Estimate state in kalman filter - sigma: [1000] # Apriori sigma values - process_noise: [0] # Process noise sigmas - eop_rates: - # estimated: [true] # Estimate state in kalman filter - sigma: [1000] # Apriori sigma values - process_noise: [0] # Process noise sigmas - ion: - estimated: [false] # Estimate state in kalman filter - sigma: [-1] # Apriori sigma values - process_noise: [0] # Process noise sigmas + model_error_handling: + meas_deweighting: # Measurements that are outside the expected confidence bounds may be deweighted so that outliers do not contaminate the filtered solution + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all rejected measurement + state_deweighting: # Any "state" errors cause deweighting of all measurements that reference the state + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all referencing measurements + error_accumulation: + enable: true + receiver_error_count_threshold: 0 + receiver_error_epochs_threshold: 0 + state_error_count_threshold: 3 + ambiguities: + phase_reject_limit: 2 # Reset ambiguity after 2 large fractional residuals are found (replaces phase_reject_count:) + reset_on: # Reset ambiguities when slip is detected by the following + gf: true # GF test + lli: true # LLI test + mw: true # MW test + scdia: true # SCDIA test + retrack: true - receivers: - global: - pos: - estimated: [true] - sigma: [100] - # process_noise: [30] - pos_rate: # Velocity - estimated: [false] # [bools] Estimate state in kalman filter - sigma: [0] # [floats] Apriori sigma values - process_noise: [0] # [floats] Process noise sigmas - # process_noise_dt: SECOND # (enum) Time unit for process noise - sqrt_sec, sqrt_day etc - # apriori_val: [0] # [floats] Apriori state values - # mu: [0] # [floats] Desired mean value for gauss markov states - # tau: [-1] # [floats] Correlation times for gauss markov noise, defaults to -1 -> inf (Random Walk) - clock: - estimated: [true] - sigma: [1000] - process_noise: [100] - ant_delta: - estimated: [true] - sigma: [10] - process_noise: [1] - tau: [100] - clock_rate: - estimated: [false] - sigma: [0.005] - process_noise: [1e-4] - 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 - 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: [20] - process_noise: [0] - phase_bias: - estimated: [false] - sigma: [10] - process_noise: [0] +estimation_parameters: + receivers: + global: + pos: + estimated: [true] + sigma: [100] + # process_noise: [30] + pos_rate: # Velocity + estimated: [false] # [bools] Estimate state in kalman filter + sigma: [0] # [floats] Apriori sigma values + process_noise: [0] # [floats] Process noise sigmas + # process_noise_dt: SECOND # (enum) Time unit for process noise - sqrt_sec, sqrt_day etc + # apriori_val: [0] # [floats] Apriori state values + # mu: [0] # [floats] Desired mean value for gauss markov states + # tau: [-1] # [floats] Correlation times for gauss markov noise, defaults to -1 -> inf (Random Walk) + clock: + estimated: [true] + sigma: [1000] + process_noise: [100] + clock_rate: + estimated: [false] + sigma: [0.005] + process_noise: [1e-4] + ambiguities: + estimated: [true] + sigma: [1000] + process_noise: [0] + outage_limit: [120] + ion_stec: # Ionospheric slant delay + estimated: [true] # Estimate state in kalman filter + sigma: [200] # Apriori sigma values + process_noise: [10] # Process noise sigmas + outage_limit: [120] + sigma_limit: [1000] + 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] + sigma: [20] + process_noise: [0] + phase_bias: + estimated: [false] + sigma: [10] + process_noise: [0] + gps: + l5q: + phase_bias: + estimated: [true] + sigma: [10] + 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} - 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 - # secondary_database: - # secondary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison - # secondary_uri: mongodb://localhost:27017 # Location and port of the mongo database to connect to - # output_config: primary # Output config {none,primary,secondary,both} - 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} - output_trace: primary + 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 + # secondary_database: + # secondary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison + # secondary_uri: mongodb://localhost:27017 # Location and port of the mongo database to connect to + # output_config: primary # Output config {none,primary,secondary,both} + 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} + output_trace: primary diff --git a/exampleConfigs/record_snr.yaml b/exampleConfigs/record_snr.yaml index 7af7755a0..3c30b4ac1 100644 --- a/exampleConfigs/record_snr.yaml +++ b/exampleConfigs/record_snr.yaml @@ -2,11 +2,11 @@ inputs: gnss_observations: gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" rtcm_inputs: - - ALIC00AUS0 - - DARW00AUS0 - - MOBS00AUS0 - - TKBG00JPN0 - - TLSE00FRA0 + - ALIC00AUS0 + - DARW00AUS0 + - MOBS00AUS0 + - TKBG00JPN0 + - TLSE00FRA0 outputs: metadata: @@ -50,5 +50,3 @@ mongo: primary_database: "" primary_suffix: "" delete_history: primary - - diff --git a/exampleConfigs/record_streams.yaml b/exampleConfigs/record_streams.yaml index f8cc94c87..7a66c3035 100644 --- a/exampleConfigs/record_streams.yaml +++ b/exampleConfigs/record_streams.yaml @@ -1,78 +1,124 @@ # Record and decode streams inputs: + inputs_root: ./products/ - inputs_root: ./products/ - - atx_files: + atx_files: - igs20.atx - snx_files: - # - tables/igs_satellite_metadata.snx - - tables/igs_satellite_metadata_2203_plus.snx - - gnss_observations: - gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - - ALIC00AUS0 - - satellite_data: - satellite_data_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - ssr_antenna_offset: APC - rtcm_inputs: - - BCEP00BKG0 - - SSRA00CNE0 + planetary_ephemeris_files: + - tables/DE436.1950.2050 + + snx_files: + - igs_satellite_metadata.snx + - tables/sat_yaw_bias_rate.snx + - tables/bds_yaw_modes.snx + - tables/qzss_yaw_modes.snx + # - "*_CRD.SNX" + + gnss_observations: + gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" + rtcm_inputs: + - ALIC00AUS0 + + satellite_data: + satellite_data_root: "https://:@ntrip.data.gnss.ga.gov.au/" + rtcm_inputs: + ssr_antenna_offset: APC + rtcm_inputs: + - BCEP00BKG0 + - SSRA00CNE0 outputs: - - metadata: - config_description: record_streams - - outputs_root: ./outputs// - - rtcm_nav: - output: true - - rtcm_obs: - output: true - - decoded_rtcm: - output: true - - rinex_nav: - output: true - - rinex_obs: - output: true - - sp3: - output: true - filename: --.sp3 - orbit_sources: [ SSR ] - output_interval: 300 + metadata: + config_description: record_streams + user: your_NTRIP_username + pass: your_NTRIP_password + reference_system: IGS20 + + outputs_root: ./outputs// + + log: + output: true + filename: __LOG.json + + rtcm_obs: + output: true + filename: __OBS.rtcm + + rtcm_nav: + output: true + filename: __NAV.rtcm + + decoded_rtcm: + output: true + filename: __DEC.json + + rinex_obs: + output: true + filename: __OBS.rnx + + 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: # Required if write out SP3 files with SSRA streams + global: + models: + pos: + enable: true + sources: [SSR] + clock: + enable: true + sources: [SSR] + +receiver_options: # Receiver and antenna information to write to Rinex file headers (can use valid Sinex files instead) + 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] processing_options: - - epoch_control: - # max_epochs: 60 - epoch_interval: 1 - wait_next_epoch: 1 - max_rec_latency: 0 - require_obs: false - sleep_milliseconds: 1 - - gnss_general: - common_sat_pco: true - delete_old_ephemerides: true - sys_options: - gps: - process: true - gal: - process: true - glo: - process: false - bds: - process: false - qzs: - process: false + process_modes: + spp: false + + epoch_control: + require_obs: false + epoch_interval: 10 + wait_next_epoch: 10 + max_rec_latency: 1 + + gnss_general: + common_sat_pco: true + delete_old_ephemerides: true + gpst_utc_leap_seconds: 18 + sys_options: + gps: + process: true + gal: + process: true + glo: + process: true + bds: + process: true + code_priorities: [L1P, L5P, L2I, L6I, L7I, L7D] + qzs: + process: true diff --git a/exampleConfigs/rt_ppp_example.yaml b/exampleConfigs/rt_ppp_example.yaml index 04ea5a9ec..29b81cab3 100644 --- a/exampleConfigs/rt_ppp_example.yaml +++ b/exampleConfigs/rt_ppp_example.yaml @@ -1,204 +1,333 @@ 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: [tables/gpt_25.grd] - - tides: - ocean_tide_loading_blq_files: [tables/OLOAD_GO.BLQ] # required if ocean loading is applied - atmos_tide_loading_blq_files: [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: [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: - satellite_data_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - ssr_antenna_offset: APC - rtcm_inputs: - - BCEP00BKG0 - - SSRA00BKG0 + inputs_root: ./products/ + + atx_files: [igs20.atx] # required + igrf_files: [tables/igrf14coeffs.txt] + # erp_files: [finals.data.iau2000.txt] + # planetary_ephemeris_files: [tables/DE436.1950.2050] + + troposphere: + gpt2grid_files: [tables/gpt_25.grd] + + tides: + ocean_tide_loading_blq_files: [tables/OLOAD_GO.BLQ] # required if ocean loading is applied + atmos_tide_loading_blq_files: [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 + + snx_files: + - igs_satellite_metadata.snx + - tables/sat_yaw_bias_rate.snx + - tables/bds_yaw_modes.snx + - tables/qzss_yaw_modes.snx + # - "*_CRD.SNX" + + satellite_data: + satellite_data_root: "https://:@ntrip.data.gnss.ga.gov.au/" + rtcm_inputs: + ssr_antenna_offset: APC + rtcm_inputs: + - BCEP00BKG0 + - SSRA00CNE0 + + gnss_observations: + gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" + rtcm_inputs: + - ALIC00AUS0 + - MAW100ATA0 + - DARW00AUS0 + - STR200AUS0 outputs: - metadata: - config_description: rt_ppp_example + metadata: + config_description: rt_ppp_example_ + user: your_ntrip_user + pass: your_ntrip_pass + + outputs_root: ./outputs/ - outputs_root: ./outputs/ + trace: + level: 2 + output_receivers: true + output_network: true + receiver_filename: __.TRACE + network_filename: __.TRACE + output_residuals: true + output_residual_chain: true + output_config: true + output_initialised_states: false + output_predicted_states: false - trace: - level: 4 - output_receivers: true - output_network: true - receiver_filename: __.TRACE - network_filename: __.TRACE - output_residuals: true - output_residual_chain: true - output_config: true + gpx: + output: true + filename: __.GPX - gpx: - output: true - filename: __.GPX + pos: + output: true + filename: __.POS 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 + global: + error_model: ELEVATION_DEPENDENT # {uniform,elevation_dependent} + code_sigma: 0 # Standard deviation of code measurements + phase_sigma: 0 # Standard deviation of phase measurmeents + models: + pos: + enable: true + sources: [SSR] + clock: + enable: true + sources: [SSR] + phase_bias: + enable: true 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] + 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) + clock_codes: [AUTO, AUTO] + zero_dcb_codes: [AUTO, AUTO] + rec_reference_system: GPS + models: + phase_bias: + enable: false + troposphere: # Tropospheric modelling accounts for delays due to refraction of light in water vapour + enable: true + models: [gpt2] # List of models to use for troposphere [standard,sbas,vmf3,gpt2,cssr] + tides: + enable: true # Enable modelling of tidal disaplacements + solid: true # Enable solid Earth tides + otl: true # Enable ocean tide loading + atl: true # Enable atmospheric tide loading + spole: true # Enable solid Earth pole tides + opole: true # Enable ocean pole tides + 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] + + MAW1: + receiver_type: "SEPT POLARX5" + antenna_type: "AOAD/M_T AUST" + apriori_position: [1111287.1380, 2168911.2970, -5874493.6440] + models: + eccentricity: + enable: true + offset: [0.0000, 0.0000, 0.0035] + + DARW: + receiver_type: "SEPT POLARX5" + antenna_type: "JAVRINGANT_DM NONE" + apriori_position: [-4091359.6055, 4684606.4197, -1408579.1195] + models: + eccentricity: + enable: true + offset: [0.0000, 0.0000, 0.0000] + + STR2: + receiver_type: "TRIMBLE ALLOY" + antenna_type: "LEIAR25.R3 NONE" + apriori_position: [-4467075.2351, 2683011.8470, -3667007.0408] + models: + eccentricity: + enable: true + offset: [0.0000, 0.0000, 0.0000] processing_options: - process_modes: - ppp: true - - epoch_control: - epoch_interval: 20 - max_rec_latency: 1 - - 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 + process_modes: + preprocessor: true # Preprocessing and quality checks + spp: true # Perform SPP on receiver data + ppp: true # Perform PPP network or end user mode + ionosphere: false # Compute Ionosphere models based on GNSS measurements + slr: false # Process SLR observations + + epoch_control: + epoch_interval: 10 + max_rec_latency: 1 + + gnss_general: + add_eop_component: true + use_primary_signals: true + sys_options: + gps: + process: true + reject_eclipse: false + code_priorities: [L1W, L1C, L2W, L2S] + # code_priorities: [L1W, L1C, L2W, L2S, L5Q, L5X] + + gal: + process: true + reject_eclipse: false + code_priorities: [L1C, L1X, L5Q, L5X] + + glo: + process: true + reject_eclipse: false + code_priorities: [L1C, L1P, L2C, L2P] + + # phase_measurements: + # process: false + + preprocessor: # Configurations for the kalman filter and its sub processes + cycle_slips: # Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised + mw_process_noise: 0 # Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips + slip_threshold: 0.05 # Value used to determine when a slip has occurred + preprocess_all_data: true + + spp: + # always_reinitialise: false # Reset SPP state to zero to avoid potential for lock-in of bad states + max_lsq_iterations: 12 # Maximum number of iterations of least squares allowed for convergence + outlier_screening: + chi_square: + enable: true + sigma_threshold: 5 + least_square: + max_iterations: 1 + sigma_check: false + omega_test: true + meas_sigma_threshold: 5 + raim: + enable: true + max_iterations: 2 + max_gdop: 30 # Maximum dilution of precision before error is flagged + + ppp_filter: + outlier_screening: + chi_square: + enable: false # Enable Chi-square test + mode: innovation # Chi-square test mode {none,innovation,measurement,state} + prefit: + max_iterations: 3 # Maximum number of measurements to exclude using prefit checks before attempting to filter + omega_test: false # Enable omega-test + sigma_check: false # Enable sigma check + state_sigma_threshold: 4 # Sigma threshold for states + meas_sigma_threshold: 4 # Sigma threshold for measurements + postfit: + max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter + omega_test: true # Enable omega-test + sigma_check: false # Enable sigma check + state_sigma_threshold: 8 # Sigma threshold for states + meas_sigma_threshold: 6 # Sigma threshold for measurements + + ionospheric_components: # Slant ionospheric components + common_ionosphere: true # Use the same ionosphere state for code and phase observations + use_gf_combo: false # Combine 'uncombined' measurements to simulate a geometry-free solution + use_if_combo: false # Combine 'uncombined' measurements to simulate an ionosphere-free solution + + chunking: + by_receiver: true # Split large filter and measurement matrices blockwise by receiver ID to improve processing speed + size: 0 + + rts: # Rauch-Tung-Striebel (RTS) backwards smoothing + enable: false + lag: -1 + # interval: 86400 + filename: _.rts + + # periodic_reset: + # enable: true + # interval: 86400 + # states: [REC_POS] + + model_error_handling: + meas_deweighting: # Measurements that are outside the expected confidence bounds may be deweighted so that outliers do not contaminate the filtered solution + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all rejected measurement + state_deweighting: # Any "state" errors cause deweighting of all measurements that reference the state + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all referencing measurements + error_accumulation: + enable: true + receiver_error_count_threshold: 0 + receiver_error_epochs_threshold: 0 + state_error_count_threshold: 3 + ambiguities: + phase_reject_limit: 2 # Reset ambiguity after 2 large fractional residuals are found (replaces phase_reject_count:) + reset_on: # Reset ambiguities when slip is detected by the following + gf: true # GF test + lli: true # LLI test + mw: true # MW test + scdia: true # SCDIA test + retrack: true 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] + receivers: + global: + pos: + estimated: [true] + sigma: [100] + # process_noise: [30] + pos_rate: # Velocity + estimated: [false] # [bools] Estimate state in kalman filter + sigma: [0] # [floats] Apriori sigma values + process_noise: [0] # [floats] Process noise sigmas + # process_noise_dt: SECOND # (enum) Time unit for process noise - sqrt_sec, sqrt_day etc + # apriori_val: [0] # [floats] Apriori state values + # mu: [0] # [floats] Desired mean value for gauss markov states + # tau: [-1] # [floats] Correlation times for gauss markov noise, defaults to -1 -> inf (Random Walk) + clock: + estimated: [true] + sigma: [1000] + process_noise: [100] + clock_rate: + estimated: [false] + sigma: [0.005] + process_noise: [1e-4] + ambiguities: + estimated: [true] + sigma: [1000] + process_noise: [0] + outage_limit: [120] + ion_stec: # Ionospheric slant delay + estimated: [true] # Estimate state in kalman filter + sigma: [200] # Apriori sigma values + process_noise: [10] # Process noise sigmas + outage_limit: [120] + sigma_limit: [1000] + 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] + sigma: [20] + process_noise: [0] + phase_bias: + estimated: [false] + sigma: [10] + process_noise: [0] + gps: + l5q: + phase_bias: + estimated: [true] + sigma: [10] + 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} - 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 + 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: + primary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison + # secondary_database: + # secondary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison + # secondary_uri: mongodb://localhost:27017 # Location and port of the mongo database to connect to + # output_config: primary # Output config {none,primary,secondary,both} + 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} + output_trace: primary diff --git a/exampleConfigs/rt_rtk_example.yaml b/exampleConfigs/rt_rtk_example.yaml index fcc41f00b..39a2c3b8f 100644 --- a/exampleConfigs/rt_rtk_example.yaml +++ b/exampleConfigs/rt_rtk_example.yaml @@ -1,204 +1,191 @@ inputs: + inputs_root: ./products/ - inputs_root: ./products/ + atx_files: [igs20.atx] + egm_files: [tables/EGM2008.gfc] + igrf_files: [tables/igrf14coeffs.txt] + erp_files: [finals.data.iau2000.txt] + planetary_ephemeris_files: [tables/DE436.1950.2050] - 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: [tables/gpt_25.grd] - troposphere: - gpt2grid_files: [ tables/gpt_25.grd ] + tides: + ocean_tide_loading_blq_files: [tables/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] - tides: - ocean_tide_loading_blq_files: [ tables/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: + snx_files: - tables/igs_satellite_metadata_2203_plus.snx - IGS1R03SNX_20191950000_07D_07D_CRD.SNX - meta_gather_20210721.snx - gnss_observations: - gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - - NWCS00AUS0 - - NEWE00AUS0 - - - satellite_data: - satellite_data_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - ssr_antenna_offset: APC - rtcm_inputs: - - BCEP00BKG0 + gnss_observations: + gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" + rtcm_inputs: + - NWCS00AUS0 + - NEWE00AUS0 + satellite_data: + satellite_data_root: "https://:@ntrip.data.gnss.ga.gov.au/" + rtcm_inputs: + ssr_antenna_offset: APC + rtcm_inputs: + - BCEP00BKG0 outputs: - metadata: - config_description: rt_rtk_example + metadata: + config_description: rt_rtk_example - outputs_root: ./outputs/ + outputs_root: ./outputs/ - trace: - level: 6 - output_receivers: true - output_network: true - receiver_filename: __.TRACE - network_filename: __.TRACE - output_residuals: true - output_residual_chain: true - output_config: true + trace: + level: 6 + output_receivers: true + output_network: true + receiver_filename: __.TRACE + network_filename: __.TRACE + output_residuals: true + output_residual_chain: true + output_config: true - gpx: - output: true - filename: __.GPX + gpx: + output: true + filename: __.GPX satellite_options: - - global: - models: - pos: - enable: true - clock: - enable: true - code_bias: - enable: true - undefined_sigma: 3 - phase_bias: - enable: true - undefined_sigma: 3 - + global: + models: + pos: + enable: true + clock: + enable: true + 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 - - NEWE: - apriori_position: [-4721191.0941328, 2535299.6155289,-3447499.6521773] - aliases: [BASE] - + 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 + + NEWE: + apriori_position: [-4721191.0941328, 2535299.6155289, -3447499.6521773] + aliases: [BASE] processing_options: - - process_modes: - ppp: true - - epoch_control: - epoch_interval: 1 - max_rec_latency: 1 - - gnss_general: - # use_rtk_combo: true - equate_tropospheres: true - equate_ionospheres: true - sys_options: - gps: - process: true - reject_eclipse: false - # clock_codes: [ L1W, L2W ] - code_priorities: [ L1C, L2W ] - ambiguity_resolution: true - # network_amb_pivot: true - # receiver_amb_pivot: true - # gal: - # process: true - # code_priorities: [ L1C, L5Q, L1X, L5X ] - # - ambiguity_resolution: - # mode: LAMBDA_bie - success_rate_threshold: 0.999999 - once_per_epoch: true - - - ppp_filter: - ionospheric_components: - common_ionosphere: true + process_modes: + ppp: true + + epoch_control: + epoch_interval: 1 + max_rec_latency: 1 + + gnss_general: + # use_rtk_combo: true + equate_tropospheres: true + equate_ionospheres: true + sys_options: + gps: + process: true + reject_eclipse: false + # clock_codes: [ L1W, L2W ] + code_priorities: [L1C, L2W] + ambiguity_resolution: true + # network_amb_pivot: true + # receiver_amb_pivot: true + # gal: + # process: true + # code_priorities: [ L1C, L5Q, L1X, L5X ] + # + ambiguity_resolution: + # mode: LAMBDA_bie + success_rate_threshold: 0.999999 + once_per_epoch: true + + ppp_filter: + ionospheric_components: + common_ionosphere: true estimation_parameters: + satellites: + global: + clock: + estimated: [true] + sigma: [1000] + process_noise: [-1] + phase_bias: + estimated: [true] + sigma: [1] + # process_noise: [-1] + code_bias: + estimated: [true] + sigma: [100] + # process_noise: [-1] + + receivers: + BASE: + pos: + estimated: [false] + clock: + estimated: [false] + phase_bias: + estimated: [false] - satellites: - global: - clock: - estimated: [true] - sigma: [1000] - process_noise: [-1] - phase_bias: - estimated: [true] - sigma: [1] - # process_noise: [-1] - code_bias: - estimated: [true] - sigma: [100] - # process_noise: [-1] - - - receivers: - BASE: - pos: - estimated: [false] - clock: - estimated: [false] - phase_bias: - estimated: [false] - - global: - pos: - estimated: [true] - sigma: [1000] - process_noise: [0.001] - # process_noise_dt: MINUTE - 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: [1] - 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} - 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} - output_trace: primary + global: + pos: + estimated: [true] + sigma: [1000] + process_noise: [0.001] + # process_noise_dt: MINUTE + 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: [1] + 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} + 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} + output_trace: primary debug: - # explain_measurements: true - # instrument: true + # explain_measurements: true + # instrument: true diff --git a/exampleConfigs/rtk_example.yaml b/exampleConfigs/rtk_example.yaml index 00164781b..fd531e74a 100644 --- a/exampleConfigs/rtk_example.yaml +++ b/exampleConfigs/rtk_example.yaml @@ -1,201 +1,197 @@ inputs: + inputs_root: ./products/ - inputs_root: ./products/ + troposphere: + gpt2grid_files: [tables/gpt_25.grd] - troposphere: - gpt2grid_files: [ tables/gpt_25.grd ] - - snx_files: + snx_files: - tables/igs_satellite_metadata_2203_plus.snx - IGS1R03SNX_20191950000_07D_07D_CRD.SNX - meta_gather_20210721.snx - gnss_observations: - rnx_inputs: - - ALEX: - - "/rtk/A*.rnx" - - BRAD: - - "/rtk/B*.rnx" - - CHAZ: - - "/rtk/C*.rnx" - - DAVE: - - "/rtk/D*.rnx" - # - FRAN: - # - "/rtk/F*.rnx" - # - GARY: - # - "/rtk/G*.rnx" - - satellite_data: - sp3_files: - - "/rtk/*.SP3" + gnss_observations: + rnx_inputs: + - ALEX: + - "/rtk/A*.rnx" + - BRAD: + - "/rtk/B*.rnx" + - CHAZ: + - "/rtk/C*.rnx" + - DAVE: + - "/rtk/D*.rnx" + # - FRAN: + # - "/rtk/F*.rnx" + # - GARY: + # - "/rtk/G*.rnx" + + satellite_data: + sp3_files: + - "/rtk/*.SP3" outputs: - metadata: - config_description: rtk_A + metadata: + config_description: rtk_A - outputs_root: ./outputs/ + outputs_root: ./outputs/ - trace: - level: 5 - output_receivers: true - output_network: true - output_residuals: true - output_residual_chain: true - output_initialised_states: true + trace: + level: 5 + output_receivers: true + output_network: true + output_residuals: true + output_residual_chain: true + output_initialised_states: true satellite_options: - - global: - models: - code_bias: - enable: true - pco: - enable: false - pcv: - enable: false + global: + models: + code_bias: + enable: true + pco: + enable: false + pcv: + enable: false receiver_options: - - global: - elevation_mask: 10 - code_sigma: 0.3 - # code_sigma: 3000 - phase_sigma: 0.003 # Standard deviation of phase measurmeents, m - models: - phase_bias: - enable: true - code_bias: - enable: true - tides: - enable: false - pco: - enable: false - pcv: - enable: false - eccentricity: - enable: false - ionospheric_components: - iono_sigma_limit: 10000000 - apriori_position: [-4454625.6083166, 2669830.4257644,-3691275.8084798] - - ALEX: - models: - code_bias: - enable: true - - BRAD: - aliases: [BASE] - FRAN: - # aliases: [BASE] - apriori_position: [ -4454695.3171, 2669791.4467, -3691215.6759 ] - - GARY: - # aliases: [BASE] - apriori_position: [ -4454622.6297, 2669912.5450, -3691219.1597 ] - - BASE: - code_sigma: 0.3 + global: + elevation_mask: 10 + code_sigma: 0.3 + # code_sigma: 3000 + phase_sigma: 0.003 # Standard deviation of phase measurmeents, m + models: + phase_bias: + enable: true + code_bias: + enable: true + tides: + enable: false + pco: + enable: false + pcv: + enable: false + eccentricity: + enable: false + ionospheric_components: + iono_sigma_limit: 10000000 + apriori_position: [-4454625.6083166, 2669830.4257644, -3691275.8084798] + + ALEX: + models: + code_bias: + enable: true + + BRAD: + aliases: [BASE] + FRAN: + # aliases: [BASE] + apriori_position: [-4454695.3171, 2669791.4467, -3691215.6759] + + GARY: + # aliases: [BASE] + apriori_position: [-4454622.6297, 2669912.5450, -3691219.1597] + + BASE: + code_sigma: 0.3 processing_options: + process_modes: + ppp: true - process_modes: - ppp: true - - epoch_control: - epoch_interval: 1 + epoch_control: + epoch_interval: 1 - gnss_general: - phase_measurements: - process: false + gnss_general: + phase_measurements: + process: false - # equate_tropospheres: true - # equate_ionospheres: true - # use_rtk_combo: true - sys_options: - gps: - process: true - code_priorities: [ L1C, L2W ] + # equate_tropospheres: true + # equate_ionospheres: true + # use_rtk_combo: true + sys_options: + gps: + process: true + code_priorities: [L1C, L2W] - ppp_filter: - ionospheric_components: - common_ionosphere: false - # use_if_combo: true + ppp_filter: + ionospheric_components: + common_ionosphere: false + # use_if_combo: true - advanced_postfits: true + advanced_postfits: true estimation_parameters: - satellites: - global: - clock: - estimated: [true] - sigma: [10000] - process_noise: [10000] - # process_noise: [-1] - phase_bias: - estimated: [true] - sigma: [1] - # process_noise: [-1] - code_bias: - estimated: [true] - sigma: [10000] - process_noise: [1] - - receivers: - BASE: - phase_bias: - # estimated: [false] - pos: - estimated: [false] - code_bias: - estimated: [false] - ALEX: - code_bias: - apriori_value: [20] - CHAZ: - code_bias: - # apriori_value: [-0.9] - DAVE: - code_bias: - # apriori_value: [0] - global: - pos: - estimated: [true] - sigma: [1000] - process_noise: [10] - clock: - estimated: [true] - sigma: [1000] - process_noise: [10000] - ion_stec: - estimated: [true] - sigma: [2000] - process_noise: [-1] - trop: - estimated: [true] - sigma: [4] - process_noise: [0.001] - phase_bias: - estimated: [true] - sigma: [100] - process_noise: [0] - ambiguities: - estimated: [true] - sigma: [100000] - process_noise: [0] - code_bias: - # estimated: [false, false, true] - estimated: [true] - sigma: [10000] - process_noise: [0] - # apriori_value: [1.5, 1.5, -1.5] + satellites: + global: + clock: + estimated: [true] + sigma: [10000] + process_noise: [10000] + # process_noise: [-1] + phase_bias: + estimated: [true] + sigma: [1] + # process_noise: [-1] + code_bias: + estimated: [true] + sigma: [10000] + process_noise: [1] + + receivers: + BASE: + phase_bias: + # estimated: [false] + pos: + estimated: [false] + code_bias: + estimated: [false] + ALEX: + code_bias: + apriori_value: [20] + CHAZ: + code_bias: + # apriori_value: [-0.9] + DAVE: + code_bias: + # apriori_value: [0] + global: + pos: + estimated: [true] + sigma: [1000] + process_noise: [10] + clock: + estimated: [true] + sigma: [1000] + process_noise: [10000] + ion_stec: + estimated: [true] + sigma: [2000] + process_noise: [-1] + trop: + estimated: [true] + sigma: [4] + process_noise: [0.001] + phase_bias: + estimated: [true] + sigma: [100] + process_noise: [0] + ambiguities: + estimated: [true] + sigma: [100000] + process_noise: [0] + code_bias: + # estimated: [false, false, true] + estimated: [true] + sigma: [10000] + process_noise: [0] + # apriori_value: [1.5, 1.5, -1.5] mongo: - enable: primary - output_components: primary - output_cumulative: primary - output_states: primary - output_measurements: primary - delete_history: primary + enable: primary + output_components: primary + output_cumulative: primary + output_states: primary + output_measurements: primary + delete_history: primary debug: - explain_measurements: true + explain_measurements: true diff --git a/exampleConfigs/slr_pod_with_pseudoobs_gal.yaml b/exampleConfigs/slr_pod_with_pseudoobs_gal.yaml index d7d45ecce..5ec3a79ea 100644 --- a/exampleConfigs/slr_pod_with_pseudoobs_gal.yaml +++ b/exampleConfigs/slr_pod_with_pseudoobs_gal.yaml @@ -1,239 +1,240 @@ 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 + 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_pod_with_pseudoobs_gal - - outputs_root: outputs// - colourise_terminal: true - - 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 + metadata: + config_description: slr_pod_with_pseudoobs_gal + + outputs_root: outputs// + colourise_terminal: true + + 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: "" + 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 - 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] + 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 - global: - elevation_mask: 10 # degrees - error_model: elevation_dependent - laser_sigma: 0.10 - - models: - eop: - enable: true + 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: 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: - 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 + epoch_control: + start_epoch: 2019-07-17 00:00:00 + end_epoch: 2019-07-19 23:55:00 + 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: + 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] - global_models: - eop: - estimated: [true] - sigma: [10] - - eop_rates: - estimated: [true] - sigma: [10] + eop_rates: + estimated: [true] + sigma: [10] - receivers: - global: - pos: - estimated: [false] - sigma: [1.0] + receivers: + global: + pos: + estimated: [false] + sigma: [1.0] - slr_range_bias: - estimated: [false] - sigma: [0.01] + slr_range_bias: + estimated: [false] + sigma: [0.01] - slr_time_bias: - estimated: [false] - sigma: [0.00001] + 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) + 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_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_b_1: { estimated: [true], sigma: [1e3] } + emp_d_1: { estimated: [true], sigma: [1e3] } + emp_b_1: { estimated: [true], sigma: [1e3] } - emp_d_2: { estimated: [true], sigma: [1e3] } + emp_d_2: { estimated: [true], sigma: [1e3] } - emp_d_4: { estimated: [true], sigma: [1e3] } + emp_d_4: { estimated: [true], sigma: [1e3] } diff --git a/exampleConfigs/slr_pod_with_pseudoobs_lag.yaml b/exampleConfigs/slr_pod_with_pseudoobs_lag.yaml index 1a3e35244..72dd502d6 100644 --- a/exampleConfigs/slr_pod_with_pseudoobs_lag.yaml +++ b/exampleConfigs/slr_pod_with_pseudoobs_lag.yaml @@ -1,211 +1,206 @@ 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 + 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_pseudoobs_lag - - outputs_root: outputs// - colourise_terminal: true - - 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 + metadata: + config_description: slr_pod_with_pseudoobs_lag + + outputs_root: outputs// + colourise_terminal: true + + 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: "" + 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 - 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] + 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 - global: - elevation_mask: 10 # degrees - error_model: elevation_dependent - laser_sigma: 0.10 - - models: - eop: - enable: true + 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 + 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] - global_models: - eop: - estimated: [false] - sigma: [10] + eop_rates: + estimated: [false] + sigma: [10] - eop_rates: - estimated: [false] - sigma: [10] - - receivers: - global: - pos: - estimated: [false] - sigma: [1.0] + receivers: + global: + pos: + estimated: [false] + sigma: [1.0] - slr_range_bias: - estimated: [false] - sigma: [0.01] + slr_range_bias: + estimated: [false] + sigma: [0.01] - slr_time_bias: - estimated: [false] - sigma: [0.00001] + 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) + satellites: + global: + orbit: + estimated: [true] + sigma: [1] # posX/Y/Z, velX/Y/Z (final element repeated as necessary) - emp_t_0: { estimated: [true], sigma: [1e3] } + emp_t_0: { estimated: [true], sigma: [1e3] } - emp_t_1: { estimated: [true], sigma: [1e3] } - emp_n_1: { estimated: [true], sigma: [1e3] } + emp_t_1: { estimated: [true], sigma: [1e3] } + emp_n_1: { estimated: [true], sigma: [1e3] } diff --git a/ginan.code-workspace b/ginan.code-workspace new file mode 100644 index 000000000..876a1499c --- /dev/null +++ b/ginan.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/inputData/products/products.list b/inputData/products/products.list index b5faca813..5864fe0de 100644 --- a/inputData/products/products.list +++ b/inputData/products/products.list @@ -52,13 +52,18 @@ ./IGS_Orbit_Test_sp3/tug_eci_not_nst_esocSettings.sp3 ./IGS_Orbit_Test_sp3/tug_eci_wot.sp3 ./IGS_Orbit_Test_sp3/tug_eci_wot_esocSettings.sp3 -./LEO/SCA1B_2022-01-01_C_04.OBXa -./LEO/cut00010.22na -./LEO/igs21906.clk_30sa -./LEO/igs21906.sp3a -./LEO/igs21907.erpa -./LEO/leoAtt_2023.001.001.01.OBXa -./LEO/leoAtt_2023.001.124.11.OBXa +./LEO/2019_02/GPS1B_2019-02-14_C_04.rnx.gz +./LEO/2019_02/COD0R03FIN_20190450000_01D_01D_OSB.BIA +./LEO/2019_02/COD0R03FIN_20190450000_01D_05M_ORB.SP3 +./LEO/2019_02/GNV1B_2019-02-14_C_04.txt +./LEO/2019_02/GNI1B_2019-02-14_C_04.sp3 +./LEO/2019_02/SCA1B_2019-02-14_C_04.OBX +./LEO/2019_02/GPS1B_2019-02-14_C_04.rnx +./LEO/2019_02/COD0R03FIN_20190450000_01D_05S_CLK.CLK +./LEO/AOD1B_2019-02-19_X_06.asc +./LEO/AOD1B_2019-02-20_X_06.asc +./LEO/AOD1B_2019-02-18_X_06.asc +./LEO.snx ./M20.ATX ./NGS0R03FIN_20191990000_01D_01D_ERP.ERP ./NGS0R03FIN_20191990000_01D_15M_ORB.SP3 @@ -286,7 +291,6 @@ ./tables/boxwing.yaml ./tables/DE405.1950.2050 ./tables/DE436.1950.2050 -./tables/EGM2008.gfc ./tables/EOP_14_C04_IAU2000A_one_file_1962-now.txt ./tables/OLOAD_GO.BLQ ./tables/ascp1950.430 @@ -296,13 +300,18 @@ ./tables/goco05s.gfc ./tables/gpt_25.grd ./tables/header.430_229 -./tables/igrf13coeffs.txt +./tables/igrf14coeffs.txt ./tables/igs_metadata_2063.snx ./tables/igs_metadata_2081.snx ./tables/igs_metadata_2081_GA.snx ./tables/igs_satellite_metadata_2203_plus.snx ./tables/igs_satellite_metadata_2219.snx ./tables/leap.second +./tables/EGM2008.gfc +./tables/atmosTide_AOD1bRL06.potential.iers.txt +./tables/desai_model_jgrb51665-sup-0002-ds01.txt +./tables/finals.data.iau2000.txt +./tables/oceanPoleTide_desai2004.txt ./tables/opoleloadcoefcmcor.txt ./tables/qzss_ohi/ohi-qzs1.txt ./tables/qzss_ohi/ohi-qzs1r.txt @@ -311,5 +320,4 @@ ./tables/qzss_ohi/ohi-qzs4.txt ./tables/qzss_yaw_modes.snx ./tables/sat_yaw_bias_rate.snx -./tables/satinfo.dat -./test.sh \ No newline at end of file +./tables/satinfo.dat \ No newline at end of file diff --git a/scripts/GinanEDA/backend/data/measurements.py b/scripts/GinanEDA/backend/data/measurements.py index 937b7cbae..d96a60f3c 100644 --- a/scripts/GinanEDA/backend/data/measurements.py +++ b/scripts/GinanEDA/backend/data/measurements.py @@ -25,6 +25,8 @@ ValueError: Raised when the input dictionary to the `Measurements` class constructor does not contain any data. """ + +import copy import logging import concurrent.futures @@ -85,6 +87,7 @@ def __init__( self.info = {} self.subset = slice(None, None, None) self.gaps = [] + self._yaxis = [] @classmethod def from_dictionary( @@ -205,21 +208,26 @@ def __sub__(self, other): ] ) raise ValueError(f"differencing Apples with oranges: {diffs}") - self_keys = set(self.data.keys()) other_keys = set(other.data.keys()) - common_keys = self_keys & other_keys - missing_keys = (self_keys | other_keys) - common_keys + common_keys = set(self._yaxis) if self._yaxis else self_keys.intersection(other_keys) + missing_keys_self = self_keys - common_keys + missing_keys_other = other_keys - common_keys + print(f"Common keys: {common_keys}") + print(f"Missing keys: {missing_keys_self}") + print(f"Missing keys: {missing_keys_other}") if len(common_keys) == 0: raise ValueError("Warning: no common keys found between dictionaries") - results = self + results = copy.deepcopy(self) _common, in_self, in_t = np.intersect1d(self.epoch, other.epoch, return_indices=True) results.epoch = self.epoch[in_self] results.data = {key: self.data[key][in_self] - other.data[key][in_t] for key in common_keys} + results.data.update({key: self.data[key][in_self] for key in missing_keys_self}) + # results.data.update({key: other.data[key][in_t] for key in missing_keys_other}) - if len(missing_keys) > 0: + if len(missing_keys_self) + len(missing_keys_other) > 0: logger.warning("Warning: keys not present in both dictionaries:") logger.warning(f"Present in self.data only: {sorted(self_keys - other_keys)}") logger.warning(f"Present in other.data only: {sorted(other_keys - self_keys)}") @@ -338,14 +346,13 @@ def select_range(self, tmin: int = None, tmax: int = None) -> None: if tmin is None: first_index = 0 else: - first_index = np.searchsorted(self.epoch, tmin, side='left') + first_index = np.searchsorted(self.epoch, tmin, side="left") if tmax is None: last_index = len(self.epoch) else: - last_index = np.searchsorted(self.epoch, tmax, side='right') + last_index = np.searchsorted(self.epoch, tmax, side="right") self.subset = slice(first_index, last_index) - def trim(self) -> None: """ trim the data to remove the nan at the extremities @@ -378,6 +385,7 @@ def __init__(self) -> None: self.arr = [] self.tmin = None self.tmax = None + self.yaxis = [] self.difference_check = False def __iter__(self): @@ -433,6 +441,16 @@ def from_mongolist(cls, data_lst: list) -> "MeasurementArray": logger.info("skipping this one") return temporary_loader + @property + def yaxis(self) -> list: + return self._yaxis + + @yaxis.setter + def yaxis(self, yaxis: list) -> None: + self._yaxis = yaxis + for data in self.arr: + data._yaxis = yaxis + def find_minmax(self): """ find_minmax determine the minimum and maximum time of all series in the array @@ -502,7 +520,7 @@ def merge(self, other) -> None: if _data.id["sat"] == _other.id["sat"] and _data.id["site"] == _other.id["site"]: common_time = np.union1d(_data.epoch, _other.epoch) data = {} - for key in set(_data.data) | set(_other.data): + for key in set(_data.data).union(set(_other.data)): data[key] = np.full_like(common_time, np.nan, dtype="float64") for name, val in _data.data.items(): mask = ~np.isnan(val) diff --git a/scripts/GinanEDA/backend/dbconnector/mongo.py b/scripts/GinanEDA/backend/dbconnector/mongo.py index 48a903c33..91b989b35 100644 --- a/scripts/GinanEDA/backend/dbconnector/mongo.py +++ b/scripts/GinanEDA/backend/dbconnector/mongo.py @@ -126,7 +126,7 @@ def get_data( "$push": {"$cond": [{"$eq": [{"$ifNull": [f"${key}", None]}, None]}, float("nan"), f"${key}"]} } logger.info(agg_pipeline) - cursor = self.mongo_client[self.mongo_db][collection].aggregate(agg_pipeline) + cursor = self.mongo_client[self.mongo_db][collection].aggregate(agg_pipeline, allowDiskUse=True) # check if cursor is empty if not cursor.alive: raise ValueError("No data found") @@ -205,7 +205,8 @@ def get_arbitrary(self, collection, match_thing, group_thing, yvalue): "_id": groupObj, "Epoch": {"$first": "$Epoch" }, "y": {"$addToSet": "$" + yvalue }, - "fields": {"$mergeObjects": "$id" } + "fields": {"$mergeObjects": "$id" }, + "other": {"$mergeObjects": "$val" } } }) pipeline.append({"$sort": sortObj}) diff --git a/scripts/GinanEDA/eda/routes/dbConnection.py b/scripts/GinanEDA/eda/routes/dbConnection.py index 6d6ae72d4..7afb73697 100644 --- a/scripts/GinanEDA/eda/routes/dbConnection.py +++ b/scripts/GinanEDA/eda/routes/dbConnection.py @@ -132,8 +132,11 @@ def handle_load_request(form_data): states_series += [f"{database}\{series}" for series in client.mongo_content["Series"]] measurements_series += [f"{database}\{series}" for series in client.mongo_content["Series"]] else: - states_series += [f"{database}\{series}" for series in client.mongo_content["StateSeries"]] - measurements_series += [f"{database}\{series}" for series in client.mongo_content["MeasurementsSeries"]] + try: + states_series += [f"{database}\{series}" for series in client.mongo_content["StateSeries"]] + measurements_series += [f"{database}\{series}" for series in client.mongo_content["MeasurementsSeries"]] + except Exception as e: + current_app.logger.warning(f"Error getting series {database}: {e}") if client.mongo_content["Has_measurements"]: mesurements += client.mongo_content["Measurements"] geometry += client.mongo_content["Geometry"] diff --git a/scripts/GinanEDA/eda/routes/measurements.py b/scripts/GinanEDA/eda/routes/measurements.py index 78c884247..f2f479e3b 100644 --- a/scripts/GinanEDA/eda/routes/measurements.py +++ b/scripts/GinanEDA/eda/routes/measurements.py @@ -10,39 +10,33 @@ from . import eda_bp -@eda_bp.route("/measurements", methods=["GET", "POST"]) -def measurements(): +@eda_bp.route("/measurements_diff", methods=["GET", "POST"]) +def measurements_diff(): if request.method == "POST": return handle_post_request() else: - return init_page(template="measurements.jinja") + template = "measurements_diff.jinja" + return init_page(template=template) -def handle_post_request(): - form_data = request.form - form = {} - form["plot"] = form_data.get("type") - form["series"] = form_data.getlist("series") - form["sat"] = form_data.getlist("sat") - form["site"] = form_data.getlist("site") - form["xaxis"] = form_data.get("xaxis") - form["yaxis"] = form_data.getlist("yaxis") - form["exclude"] = form_data.get("exclude") - if form["exclude"] == "": - form["exclude"] = 0 - else: - form["exclude"] = int(form["exclude"]) - form["exclude_tail"] = form_data.get("exclude_tail") - if form["exclude_tail"] == "": - form["exclude_tail"] = 0 +@eda_bp.route("/measurements", methods=["GET", "POST"]) +def measurements(): + if request.method == "POST": + return handle_post_request() else: - form["exclude_tail"] = int(form["exclude_tail"]) + template = "measurements.jinja" + return init_page(template=template) + +def log_and_set_session(form, session_key): current_app.logger.info( - f"GET {form['plot']}, {form['series']}, {form['sat']}, {form['site']}, {form['xaxis']}, {form['yaxis']}, {form['yaxis']+[form['xaxis']]}, exclude {form['exclude']} mintues" + f"GET {form['plot']}, {form['series']}, {form['sat']}, {form['site']}, {form['xaxis']}, {form['yaxis']}, {form['yaxis']+[form['xaxis']]}, exclude {form['exclude']} minutes" ) - session["measurements"] = form + session[session_key] = form current_app.logger.info("Getting Connection") + + +def retrieve_data(form): data = MeasurementArray() data2 = MeasurementArray() for series in form["series"]: @@ -50,26 +44,17 @@ def handle_post_request(): get_data(db_, "Measurements", None, form["site"], form["sat"], [series_], form["yaxis"] + [form["xaxis"]], data) if any([yaxis in session["list_geometry"] for yaxis in form["yaxis"] + [form["xaxis"]]]): get_data(db_, "Geometry", None, form["site"], form["sat"], [""], form["yaxis"] + [form["xaxis"]], data2) + return data, data2 + +def process_data(data, data2, form): if len(data.arr) + len(data2.arr) == 0: - return render_template( - "measurements.jinja", - # content=client.mongo_content, - selection=session["measurements"], - extra=extra, - message="Error getting data: No data", - ) + return None, "Error getting data: No data" - # try: - if len(data.arr) == 0 : + if len(data.arr) == 0: data = data2 else: data.merge(data2) - # except Exception as e: - # current_app.logger.warning(f"Merging error data {e}") - # pass - - # exit(0) data.sort() data.find_minmax() @@ -77,13 +62,17 @@ def handle_post_request(): for data_ in data: data_.find_gaps() data.get_stats() + return data, None + + +def generate_plots(data, form): mode = "markers" if form["plot"] == "Scatter" else "lines" if form["plot"] == "QQ": data.compute_qq() mode = "markers" trace = [] table = {} - current_app.logger.warning("starting plots") + current_app.logger.debug("starting plots") for _data in data: for _yaxis in form["yaxis"]: try: @@ -100,6 +89,8 @@ def handle_post_request(): x_hover_template = "%{x}
    " else: _y = _data.data[_yaxis][_data.subset] + if isinstance(_y[0], float) and np.sum(~np.isnan(_y)) == 0: + continue legend = _data.id legend["yaxis"] = _yaxis smallLegend = [legend[a] for a in legend] @@ -118,18 +109,62 @@ def handle_post_request(): pass except Exception as e: current_app.logger.warning(f"Error plotting {_data.id} {form['xaxis']}{_yaxis} {e}") - current_app.logger.warning("end plots") + return trace, table + + +def handle_post_request(): + form_data = request.form + form = { + "plot": form_data.get("type"), + "series": form_data.getlist("series"), + "sat": form_data.getlist("sat"), + "site": form_data.getlist("site"), + "xaxis": form_data.get("xaxis"), + "yaxis": form_data.getlist("yaxis"), + "exclude": form_data.get("exclude", "0"), + "exclude_tail": form_data.get("exclude_tail", "0"), + } + for label in ["exclude", "exclude_tail"]: + form[label] = int(form[label]) if form[label] else 0 + session_key = "measurements_diff" if "series_base" in form_data else "measurements" + template_name = "measurements_diff.jinja" if session_key == "measurements_diff" else "measurements.jinja" + log_and_set_session(form, session_key) + data, data2 = retrieve_data(form) + data, error_message = process_data(data, data2, form) + if error_message: + return render_template( + template_name, + selection=session[session_key], + extra=extra, + message=error_message, + ) + + if session_key == "measurements_diff": + form["series_base"] = [form_data.get("series_base")] + req = form.copy() + req["series"] = [form_data.get("series_base")] + data_base, data2_base = retrieve_data(req) + data_base, error_message = process_data(data_base, data2_base, form) + print("getting base data", form_data.get("series_base")) + print({"series": [form_data.get("series_base")], **form}) + data.yaxis = form["yaxis"] + data = data - data_base + session[session_key] = form + data.get_stats() + else: + session[session_key] = form + print(session[session_key]) + trace, table = generate_plots(data, form) table_agg = aggregate_stats(data) return render_template( - "measurements.jinja", - # content=client.mongo_content, + template_name, extra=extra, graphJSON=generate_fig(trace), mode="plotly", - selection=session["measurements"], + selection=session[session_key], table_data=table, table_headers=["RMS", "mean"], tableagg_data=table_agg, diff --git a/scripts/GinanEDA/eda/routes/states.py b/scripts/GinanEDA/eda/routes/states.py index 9a4cf09fa..f5c242111 100644 --- a/scripts/GinanEDA/eda/routes/states.py +++ b/scripts/GinanEDA/eda/routes/states.py @@ -9,51 +9,34 @@ from . import eda_bp -@eda_bp.route("/states", methods=["GET", "POST"]) -def states(): +@eda_bp.route("/states_diff", methods=["GET", "POST"]) +def states_diff(): if request.method == "POST": return handle_post_request() else: - return init_page(template="states.jinja") - - -def handle_post_request() -> str: - """ - handle_post_request Code to process the POST request and generate the HTML code + template = "states_diff.jinja" + return init_page(template=template) - :return str: webpage code - """ - current_app.logger.info("Entering request") - form_data = request.form - form = { - "type": form_data.get("type"), - "series": form_data.getlist("series"), - "sat": form_data.getlist("sat"), - "site": form_data.getlist("site"), - "state": form_data.getlist("state"), - "xaxis": form_data.get("xaxis"), - "yaxis": form_data.getlist("yaxis"), - "exclude": form_data.get("exclude"), - "exclude_tail": form_data.get("exclude_tail"), - "process": form_data.get("process"), - "degree": form_data.get("degree"), - } - if form["exclude"] == "": - form["exclude"] = 0 +@eda_bp.route("/states", methods=["GET", "POST"]) +def states(): + if request.method == "POST": + return handle_post_request() else: - form["exclude"] = int(form["exclude"]) + template = "states.jinja" + return init_page(template=template) - if form["exclude_tail"] == "": - form["exclude_tail"] = 0 - else: - form["exclude_tail"] = int(form["exclude_tail"]) +def log_and_set_session(form, session_key): current_app.logger.info( f"GET {form['type']}, {form['series']}, {form['sat']}, {form['site']}, {form['state']}, {form['xaxis']}, {form['yaxis']}, " f"{form['yaxis']+[form['xaxis']]}, exclude {form['exclude']} minutes" ) - session["states"] = form + session[session_key] = form + current_app.logger.info("Getting Connection") + + +def retrieve_data(form): data = MeasurementArray() data2 = MeasurementArray() for series in form["series"]: @@ -72,20 +55,24 @@ def handle_post_request() -> str: ) if any([yaxis in session["list_geometry"] for yaxis in form["yaxis"] + [form["xaxis"]]]): get_data(db_, "Geometry", None, form["site"], form["sat"], [""], [form["xaxis"]], data2) + return data, data2 + +def process_data(data, data2, form): if len(data.arr) == 0: - return render_template( - "states.jinja", - # content=client.mongo_content, - selection=session["states"], - extra=extra, - message="Error getting data: No data", - ) + return None, "Error getting data: No data" data.merge(data2) data.sort() data.find_minmax() data.adjust_slice(minutes_min=form["exclude"], minutes_max=form["exclude_tail"]) + for data_ in data: + data_.find_gaps() + data.get_stats() + return data, None + + +def generate_plots(data, form): trace = [] mode = "markers" if form["type"] == "Scatter" else "lines" table = {} @@ -96,7 +83,6 @@ def handle_post_request() -> str: for _data in data: _data.polyfit(degree=int(form["degree"])) - data.get_stats() for _data in data: for _yaxis in _data.data: if _yaxis != form["xaxis"]: @@ -125,16 +111,63 @@ def handle_post_request() -> str: table[f"{_data.id}"]["Fit"] = np.array2string( _data.info["Fit"][_yaxis][::-1], precision=2, separator=", " ) + return trace, table + + +def handle_post_request(): + form_data = request.form + form = { + "type": form_data.get("type"), + "series": form_data.getlist("series"), + "sat": form_data.getlist("sat"), + "site": form_data.getlist("site"), + "state": form_data.getlist("state"), + "xaxis": form_data.get("xaxis"), + "yaxis": form_data.getlist("yaxis"), + "exclude": form_data.get("exclude", "0"), + "exclude_tail": form_data.get("exclude_tail", "0"), + "process": form_data.get("process"), + "degree": form_data.get("degree"), + } + for label in ["exclude", "exclude_tail"]: + form[label] = int(form[label]) if form[label] else 0 + + session_key = "states_diff" if "series_base" in form_data else "states" + template_name = "states_diff.jinja" if session_key == "states_diff" else "states.jinja" + log_and_set_session(form, session_key) + data, data2 = retrieve_data(form) + data, error_message = process_data(data, data2, form) + if error_message: + return render_template( + template_name, + selection=session[session_key], + extra=extra, + message=error_message, + ) + + if session_key == "states_diff": + form["series_base"] = [form_data.get("series_base")] + req = form.copy() + req["series"] = [form_data.get("series_base")] + data_base, data2_base = retrieve_data(req) + data_base, error_message = process_data(data_base, data2_base, form) + list_keys = list(set(key for data_base_ in data_base.arr for key in data_base_.data.keys())) + data.yaxis = list_keys + data = data - data_base + session[session_key] = form + data.get_stats() + else: + session[session_key] = form + trace, table = generate_plots(data, form) table_agg = aggregate_stats(data) return render_template( - "states.jinja", - # content=client.mongo_content, + template_name, extra=extra, graphJSON=generate_fig(trace), mode="plotly", - selection=session["states"], + selection=session[session_key], table_data=table, table_headers=["RMS", "mean", "Fit"], tableagg_data=table_agg, diff --git a/scripts/GinanEDA/eda/routes/trace.py b/scripts/GinanEDA/eda/routes/trace.py index fc6cdf18f..86430d1c5 100644 --- a/scripts/GinanEDA/eda/routes/trace.py +++ b/scripts/GinanEDA/eda/routes/trace.py @@ -139,12 +139,16 @@ def handle_post_request(): table = {} current_app.logger.warning("starting plots") for datax in datas: + # print("Printing datax") + # print(datax) tracesX = [] for label, traceData in datax.items(): traceX = [] traceY = [] for element in traceData: + # print("Printing element") + # print(element) element["y"] = element["y"][0] if xaxis[0] == "_": x = element["_id"][xaxis[1:]] @@ -163,8 +167,9 @@ def handle_post_request(): ybak = y if type(y) == int or type(y) == float: - lpf = lpf + (y - lpf) * float(form["fCoeff"]) - hpf = y - lpf + if form["filter"] == "HPF" or form["filter"] == "LPF": + lpf = lpf + (y - lpf) * float(form["fCoeff"]) + hpf = y - lpf if form["filter"] == "HPF": y = hpf if form["filter"] == "LPF": @@ -180,13 +185,14 @@ def handle_post_request(): traceX.append(x) traceY.append(y) x_hover_template = "%{x}
    " + metadata = [a + ": " + str(element["other"][a]) for a in element["other"]] tracesX.append( go.Scatter( x=traceX, y=traceY, mode=mode, name=f"{label}", - hovertemplate=x_hover_template + "%{y:.4e%}
    " + str(element["y"]) + "
    " + f"{label}", + hovertemplate=x_hover_template + "%{y:.4e%}
    " + str(element["y"]) + "
    " + f"{label}" + "
    " + f"{metadata}", legendgroup="group1", ) ) diff --git a/scripts/GinanEDA/eda/utilities.py b/scripts/GinanEDA/eda/utilities.py index 29e8d4b08..0296efb18 100644 --- a/scripts/GinanEDA/eda/utilities.py +++ b/scripts/GinanEDA/eda/utilities.py @@ -31,15 +31,20 @@ def init_page(template: str) -> str: content = [] return render_template(template, content=content, extra=extra, exlcude=0, selection=session[template.split(".")[0]]) + def initialize_session(): - if not session.get('session_initialized'): - session['measurements'] = { + if not session.get("session_initialized"): + session["measurements"] = {"plot": "Line", "series": [], "site": [], "sat": [], "xaxis": "Epoch"} + + session["measurements_diff"] = { "plot": "Line", "series": [], + "series_base": "", "site": [], "sat": [], - "xaxis": "Epoch" + "xaxis": "Epoch", } + session["states"] = { "type": "Line", "series": [], @@ -50,24 +55,30 @@ def initialize_session(): "degree": "0", "process": "None", } - session["position"] = { + session["states_diff"] = { "type": "Line", "series": [], "series_base": "", + "site": [], + "sat": [], + "xaxis": "Epoch", + "yaxis": "x", + "degree": "0", + "process": "None", } - session["clocks"] = { - "series": "", + session["position"] = { + "type": "Line", + "series": [], "series_base": "", - "subset": [], - "modes": [], - "clockType": "" } + session["clocks"] = {"series": "", "series_base": "", "subset": [], "modes": [], "clockType": ""} session["orbits"] = { "orbitType": "", "series": [], "sat": [], } - session['session_initialized'] = True + session["session_initialized"] = True + def generate_fig(trace): fig = go.Figure(data=trace) @@ -82,17 +93,14 @@ def generate_fig(trace): def generate_figs(traces): - fig = make_subplots(rows=max(1,len(traces)), cols=1, - shared_xaxes=True, - vertical_spacing=0.2 - ) - for i in range( len(traces)): + fig = make_subplots(rows=max(1, len(traces)), cols=1, shared_xaxes=True, vertical_spacing=0.2) + for i in range(len(traces)): for trace in traces[i]: fig.add_trace(trace, row=i + 1, col=1) fig.update_layout( - xaxis={"rangeslider":{"visible":True}, "showgrid":current_app.config["EDA_GRID"]}, - yaxis={"fixedrange":False, "tickformat":".3e", "showgrid":current_app.config["EDA_GRID"]}, + xaxis={"rangeslider": {"visible": True}, "showgrid": current_app.config["EDA_GRID"]}, + yaxis={"fixedrange": False, "tickformat": ".3e", "showgrid": current_app.config["EDA_GRID"]}, height=1200, # template=current_app.config["EDA_THEME"], ) @@ -189,6 +197,7 @@ def get_distinct_vals(ip, port, db, coll, element, reshape_on=None): current_app.logger.warning(err) pass + def extract_database_series(series): db_, series_ = series.split("\\") - return db_,series_ \ No newline at end of file + return db_, series_ diff --git a/scripts/GinanEDA/templates/measurements_diff.jinja b/scripts/GinanEDA/templates/measurements_diff.jinja new file mode 100644 index 000000000..c4e39c22b --- /dev/null +++ b/scripts/GinanEDA/templates/measurements_diff.jinja @@ -0,0 +1,211 @@ +{% extends 'base.jinja'%} +{# {% block header %} +

    {% block title %} {{ plotingpage }} {% endblock %}

    +{% endblock %} #} + +{% block title%} +Measurements +{% endblock %} +{% block menuselection %} + +
    +
    + +
    + +
    + + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + + +
    + +
    + + +
    + +
    +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/scripts/GinanEDA/templates/sidebar.jinja b/scripts/GinanEDA/templates/sidebar.jinja index a93ca7be5..c3c7de8a0 100644 --- a/scripts/GinanEDA/templates/sidebar.jinja +++ b/scripts/GinanEDA/templates/sidebar.jinja @@ -10,6 +10,8 @@ {% endif %} {#
    #} +
    + @@ -19,8 +21,9 @@ +
  1. Position analysis @@ -28,12 +31,19 @@
  2. Orbits analysis
  3. +
    + + + +
    + - \ No newline at end of file diff --git a/scripts/GinanEDA/templates/states_diff.jinja b/scripts/GinanEDA/templates/states_diff.jinja new file mode 100644 index 000000000..9cce8cd7d --- /dev/null +++ b/scripts/GinanEDA/templates/states_diff.jinja @@ -0,0 +1,238 @@ +{% extends 'base.jinja'%} + +{% block title%} +States +{% endblock %} + +{% block menuselection %} +
    +
    + +
    + + +
    + + + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    + + +
    + +
    + +
    +
    + +
    + +
    +
    +
    + + +
    + +
    + + +
    +
    + + +
    + +
    +{% endblock %} + + +{% block scripts %} + + + +{% endblock %} diff --git a/scripts/GinanUI/README.md b/scripts/GinanUI/README.md new file mode 100644 index 000000000..d7b3e3701 --- /dev/null +++ b/scripts/GinanUI/README.md @@ -0,0 +1,9 @@ +# Ginan-UI + +An intelligent and user-friendly interface for using the Geoscience Australia GNSS processing tool Ginan. Made using PySide6 by students of the 2025 ANU TechLauncher program. + +[User manual available here](./docs/USER_GUIDE.md) + +## Installation + +Please read the user manual above for installation instructions. \ No newline at end of file diff --git a/scripts/GinanUI/__init__.py b/scripts/GinanUI/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/GinanUI/app/__init__.py b/scripts/GinanUI/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/GinanUI/app/controllers/__init__.py b/scripts/GinanUI/app/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/GinanUI/app/controllers/input_controller.py b/scripts/GinanUI/app/controllers/input_controller.py new file mode 100644 index 000000000..aa109eb26 --- /dev/null +++ b/scripts/GinanUI/app/controllers/input_controller.py @@ -0,0 +1,1516 @@ +# app/controllers/input_controller.py +""" +UI input flow controller for the Ginan-UI. +""" +from __future__ import annotations + +import os +import re +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Callable, List +from decimal import Decimal, InvalidOperation + +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 +from PySide6.QtWidgets import ( + QFileDialog, + QDialog, + QFormLayout, + QDoubleSpinBox, + QHBoxLayout, + QVBoxLayout, + QDateTimeEdit, + QInputDialog, + QMessageBox, + QComboBox, + QLineEdit, + QPushButton, + QLabel +) + +from scripts.GinanUI.app.models.execution import Execution, GENERATED_YAML, INPUT_PRODUCTS_PATH +from scripts.GinanUI.app.models.rinex_extractor import RinexExtractor +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 +from scripts.GinanUI.app.utils.toast import show_toast + + +class InputController(QObject): + """ + UI controller class InputController. + """ + + ready = Signal(str, str) # rnx_path, output_path + pea_ready = Signal() # emitted when PEA processing should start + + def __init__(self, ui, parent_window, execution: Execution): + """ + UI handler: init. + + Arguments: + ui (Any): Main window UI instance (generated from Qt .ui). + parent_window (Any): Parent widget/window to anchor dialogs. + execution (Execution): Backend execution bridge used to read/apply UI config. + """ + super().__init__() + self.ui = ui + self.parent = parent_window + self.execution = execution + + 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) + + ### Bind: configuration drop-downs / UIs ### + + self._bind_combo(self.ui.Mode, self._get_mode_items) + + # PPP_provider, project and series + self.ui.PPP_provider.currentTextChanged.connect(self._on_ppp_provider_changed) + self.ui.PPP_project.currentTextChanged.connect(self._on_ppp_project_changed) + self.ui.PPP_series.currentTextChanged.connect(self._on_ppp_series_changed) + + # Constellations + self._bind_multiselect_combo( + self.ui.Constellations_2, + 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() + + # 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") + + # 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) + + # Run buttons + self.ui.showConfigButton.clicked.connect(self.on_show_config) + self.ui.showConfigButton.setCursor(Qt.CursorShape.PointingHandCursor) + self.ui.processButton.clicked.connect(self.on_run_pea) + + # CDDIS credentials dialog + self.ui.cddisCredentialsButton.clicked.connect(self._open_cddis_credentials_dialog) + + self.setup_tooltips() + + def setup_tooltips(self): + """ + UI handler: setup tooltips and visual style for key controls. + """ + + # Consistent tooltip style for all elements + tooltip_style = """ + QToolTip { + background-color: #2c5d7c; + color: #ffffff; + border: 1px solid #999999; + padding: 4px; + border-radius: 3px; + font:13pt "Segoe UI"; + } + """ + + # 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 + + # 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 + 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.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." + ) + + self.ui.processButton.setToolTip( + "Start the Ginan (pea) PPP processing using the configured parameters.\n" + "Ensure all required fields are filled before processing." + ) + + # 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.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.Mode.setToolTip( + "Processing mode:\n" + "• Static: For stationary receivers\n" + "• Kinematic: For moving receivers\n" + "• Dynamic: For high-dynamic applications" + ) + + self.ui.Constellations_2.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.PPP_provider.setToolTip( + "Analysis centre that provides PPP products\n" + "Options populated based on your observation time window" + ) + + self.ui.PPP_project.setToolTip( + "PPP product project type.\n" + "Different projects types offer varying GNSS constellation PPP products." + ) + + self.ui.PPP_series.setToolTip( + "PPP product series:\n" + "• ULT: Ultra-rapid (lower latency)\n" + "• RAP: Rapid \n" + "• FIN: Final (highest accuracy)" + ) + + # Receiver/Antenna fields + self.ui.Receiver_type.setToolTip( + "Receiver model extracted from RINEX header\n" + "Click to manually edit if needed" + ) + + self.ui.Antenna_type.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" + ) + + # 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)") + + 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 + Metadata Extraction + PPP product selection + def load_rnx_file(self) -> ExtractedInputs | None: + """ + UI handler: choose a RINEX file, extract metadata, update UI, and start PPP products query. + """ + path = self._select_rnx_file(self.parent) + if not path: + return 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), + products_dir=INPUT_PRODUCTS_PATH + ) + # Disable until new providers found + if current_rinex_path != getattr(self, "last_rinex_path", None): + self.ui.processButton.setEnabled(False) + self._on_cddis_ready(pd.DataFrame(), False) # Clears providers until worker completes + + self.last_rinex_path = current_rinex_path + self.rnx_file = str(current_rinex_path) + + Logger.terminal(f"📄 RINEX file selected: {self.rnx_file}") + + try: + extractor = RinexExtractor(self.rnx_file) + result = extractor.extract_rinex_data(self.rnx_file) + + # Verify antenna_type against .atx file + if not self.parent.atx_required_for_rnx_extraction: + Logger.terminal( + "⚠️ ANTEX (.atx) file not installed yet. Antenna type verification will be skipped.") + else: + self.verify_antenna_type(result) + + Logger.terminal("🔍 Scanning CDDIS archive for PPP products. Please wait...") + + # Show toast notification + show_toast(self.parent, "🔍 Scanning CDDIS archive for PPP products...", duration=15000) + + # Show waiting cursor during CDDIS scan + self.parent.setCursor(Qt.CursorShape.WaitCursor) + + # Retrieve valid analysis centers + start_epoch = str_to_datetime(result['start_epoch']) + end_epoch = str_to_datetime(result['end_epoch']) + 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) # Restore cursor when done + self.worker.finished.connect(self.worker.deleteLater) + self.worker.finished.connect(self.metadata_thread.quit) + self.metadata_thread.finished.connect(self.metadata_thread.deleteLater) + 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.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"]))) + + self.ui.Receiver_type.clear() + self.ui.Receiver_type.addItem(result["receiver_type"]) + self.ui.Receiver_type.setCurrentIndex(0) + self.ui.Receiver_type.lineEdit().setText(result["receiver_type"]) + + self.ui.Antenna_type.clear() + self.ui.Antenna_type.addItem(result["antenna_type"]) + self.ui.Antenna_type.setCurrentIndex(0) + self.ui.Antenna_type.lineEdit().setText(result["antenna_type"]) + + self._update_constellations_multiselect(result["constellations"]) + + 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) + + except Exception as e: + Logger.terminal(f"Error extracting RNX metadata: {e}") + return None + + # Always update MainWindow's state + self.parent.rnx_file = self.rnx_file + + if self.output_dir: + self.ready.emit(str(self.rnx_file), str(self.output_dir)) + + return result + + 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 + + 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 + + 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"). + + """ + from PySide6.QtGui import QStandardItemModel, QStandardItem + + constellations = [c.strip() for c in constellation_str.split(",") if c.strip()] + combo = self.ui.Constellations_2 + + # 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) + + 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)) + + 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.PPP_provider.clear() + self.ui.PPP_provider.addItem("None") + self.ui.PPP_series.clear() + self.ui.PPP_series.addItem("None") + return + + self.valid_analysis_centers = list(get_valid_analysis_centers(self.products_df)) + + if len(self.valid_analysis_centers) == 0: + if log_messages: + Logger.terminal("⚠️ No valid PPP providers found.") + self.ui.PPP_provider.clear() + self.ui.PPP_provider.addItem("None") + self.ui.PPP_series.clear() + self.ui.PPP_series.addItem("None") + return + + self.ui.PPP_provider.blockSignals(True) + self.ui.PPP_provider.clear() + self.ui.PPP_provider.addItems(self.valid_analysis_centers) + self.ui.PPP_provider.setCurrentIndex(0) + + # Update PPP_series based on default PPP_provider + self.ui.PPP_provider.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.PPP_provider.clear() + self.ui.PPP_provider.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 _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.PPP_project.blockSignals(True) + self.ui.PPP_series.blockSignals(True) + + self.ui.PPP_project.clear() + self.ui.PPP_series.clear() + + self.ui.PPP_project.addItems(project_options) + self.ui.PPP_series.addItems(series_options) + + self.ui.PPP_project.setCurrentIndex(0) + self.ui.PPP_series.setCurrentIndex(0) + + # Unblock signals now that the population is complete + self.ui.PPP_project.blockSignals(False) + self.ui.PPP_series.blockSignals(False) + + except Exception as e: + self.ui.PPP_series.clear() + self.ui.PPP_series.addItem("None") + self.ui.PPP_project.clear() + self.ui.PPP_project.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.PPP_project.blockSignals(True) + self.ui.PPP_project.clear() + self.ui.PPP_project.addItems(valid_projects) + self.ui.PPP_project.setCurrentIndex(0) + self.ui.PPP_project.blockSignals(False) + + 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.PPP_series.blockSignals(True) + self.ui.PPP_series.clear() + self.ui.PPP_series.addItems(valid_series) + self.ui.PPP_series.setCurrentIndex(0) + self.ui.PPP_series.blockSignals(False) + + Logger.terminal(f"[UI] Filtered PPP_series for project '{selected_project}': {valid_series}") + + 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.Receiver_type.setEditable(True) + self.ui.Receiver_type.lineEdit().setReadOnly(True) + self.ui.Antenna_type.setEditable(True) + self.ui.Antenna_type.lineEdit().setReadOnly(True) + + # Receiver type free text + def _ask_receiver_type(): + current_text = self.ui.Receiver_type.currentText().strip() + text, ok = QInputDialog.getText( + self.ui.Receiver_type, + "Receiver Type", + "Enter receiver type:", + text=current_text # prefill with current + ) + if ok and text: + self.ui.Receiver_type.clear() + self.ui.Receiver_type.addItem(text) + self.ui.Receiver_type.lineEdit().setText(text) + self.ui.receiverTypeValue.setText(text) + + self.ui.Receiver_type.showPopup = _ask_receiver_type + + # Antenna type free text + def _ask_antenna_type(): + current_text = self.ui.Antenna_type.currentText().strip() + text, ok = QInputDialog.getText( + self.ui.Antenna_type, + "Antenna Type", + "Enter antenna type:", + text=current_text # prefill with current + ) + if ok and text: + self.ui.Antenna_type.clear() + self.ui.Antenna_type.addItem(text) + self.ui.Antenna_type.lineEdit().setText(text) + self.ui.antennaTypeValue.setText(text) + + self.ui.Antenna_type.showPopup = _ask_antenna_type + + # ========================================================== + # Antenna offset popup + # ========================================================== + def _open_antenna_offset_dialog(self): + """ + UI handler: open antenna offset dialog (E, N, U) with high-precision spin boxes. + """ + dlg = QDialog(self.ui.antennaOffsetButton) + dlg.setWindowTitle("Antenna Offset") + + # Parse existing "E, N, U" + try: + e0, n0, u0 = [float(x.strip()) for x in self.ui.antennaOffsetValue.text().split(",")] + except Exception: + e0 = n0 = u0 = 0.0 + + form = QFormLayout(dlg) + + class DecimalSpinBox(QDoubleSpinBox): + def __init__(self, parent=None, top=10000, bottom=-10000, precision=15, step_size=0.1): + super().__init__(parent) + self.setRange(bottom, top) + + self.setDecimals(precision) # fallback precision + # up down arrow Step size + # note there is some float point inaccuracy when useing steps + self.setSingleStep(step_size) + + def textFromValue(self, value: float) -> str: + """Format value dynamically with Decimal for more precision""" + # Convert through Decimal to avoid scientific notation + d = Decimal(str(value)) + return str(d.normalize()) # trims trailing zeros + + def valueFromText(self, text: str) -> float: + """Parse text back into a float""" + try: + return float(Decimal(text)) + except InvalidOperation: + raise ValueError(f"Failed to convert Antenna offset to float: {text}") + + sb_e = DecimalSpinBox(dlg) + sb_e.setValue(e0) + + sb_n = DecimalSpinBox(dlg) + sb_n.setValue(n0) + + sb_u = DecimalSpinBox(dlg) + sb_u.setValue(u0) + + form.addRow("E:", sb_e) + form.addRow("N:", sb_n) + form.addRow("U:", sb_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(sb_e, sb_n, sb_u, dlg)) + cancel_btn.clicked.connect(dlg.reject) + + dlg.exec() + + def _set_antenna_offset(self, sb_e, sb_n, sb_u, dlg: QDialog): + """ + UI handler: apply antenna offset values back to UI. + + Arguments: + sb_e (QDoubleSpinBox): East (E) spin box. + sb_n (QDoubleSpinBox): North (N) spin box. + sb_u (QDoubleSpinBox): Up (U) spin box. + dlg (QDialog): Dialog to accept/close. + """ + e, n, u = sb_e.value(), sb_n.value(), sb_u.value() + text = f"{e}, {n}, {u}" + self.ui.antennaOffsetButton.setText(text) + self.ui.antennaOffsetValue.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.timeWindowValue) + dlg.setWindowTitle("Select start / end time") + + # 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() + + vbox = QVBoxLayout(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") + + vbox.addWidget(start_edit) + vbox.addWidget(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) + vbox.addLayout(btn_row) + + ok_btn.clicked.connect(lambda: self._set_time_window(start_edit, end_edit, dlg)) + cancel_btn.clicked.connect(dlg.reject) + + 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: prompt for data interval (seconds) and update UI. + """ + # Extract current value from button text ("30 s" → 30) + current_text = self.ui.dataIntervalButton.text().replace(" s", "").strip() + try: + current_val = int(current_text) + except ValueError: + current_val = 1 # fallback if parsing fails + + val, ok = QInputDialog.getInt( + self.ui.dataIntervalButton, + "Data interval", + "Input interval (seconds):", + current_val, # prefill with current value + 1, + 999_999, + ) + if ok: + text = f"{val} s" + self.ui.dataIntervalButton.setText(text) + self.ui.dataIntervalValue.setText(text) + + # endregion + + # region Config and PEA Processing + + def extract_ui_values(self, rnx_path): + """ + Extract current UI values, parse/normalize 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.Mode.currentText() if self.ui.Mode.currentText() != "Select one" else "Static" + + # Get constellations from the actual dropdown selections, not the label + constellations_raw = "" + combo = self.ui.Constellations_2 + 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 + 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 + ppp_provider = self.ui.PPP_provider.currentText() if self.ui.PPP_provider.currentText() != "Select one" else "" + ppp_series = self.ui.PPP_series.currentText() if self.ui.PPP_series.currentText() != "Select one" else "" + ppp_project = self.ui.PPP_project.currentText() if self.ui.PPP_project.currentText() != "Select one" else "" + + # Parsed values + start_epoch, end_epoch = self.parse_time_window(time_window_raw) + antenna_offset = self.parse_antenna_offset(antenna_offset_raw) + epoch_interval = int(epoch_interval_raw.replace("s", "").strip()) + marker_name = self.extract_marker_name(rnx_path) + mode = self.determine_mode_value(mode_raw) + + # Returned the values found as a dataclass for easier access + return self.ExtractedInputs( + marker_name=marker_name, + start_epoch=start_epoch, + end_epoch=end_epoch, + epoch_interval=epoch_interval, + antenna_offset=antenna_offset, + mode=mode, + constellations_raw=constellations_raw, + receiver_type=receiver_type, + antenna_type=antenna_type, + ppp_provider=ppp_provider, + ppp_series=ppp_series, + ppp_project=ppp_project, + rnx_path=rnx_path, + output_path=str(self.output_dir), + ) + + 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() + + # 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) + + @staticmethod + def _select_rnx_file(parent) -> str: + """ + Open a file dialog to select a RINEX observation file. + + Arguments: + parent (Any): Parent widget. + + Returns: + str: Selected file path or empty string. + + """ + path, _ = QFileDialog.getOpenFileName( + parent, + "Select RINEX Observation File", + "", + "RINEX Observation Files (*.rnx *.rnx.gz *.[0-9][0-9]o *.[0-9][0-9]o.gz *.obs *.obs.gz);;All Files (*.*)" + ) + return path or "" + + @staticmethod + def _select_output_dir(parent) -> str: + """ + Open a directory dialog to select the output folder. + + Arguments: + parent (Any): Parent widget. + + Returns: + str: Selected directory path or empty string. + + """ + path = QFileDialog.getExistingDirectory(parent, "Select Output Directory") + return path or "" + + @staticmethod + def determine_mode_value(mode_raw: str) -> int: + """ + Map a mode label to its numeric value used by backend. + + Arguments: + mode_raw (str): One of 'Static', 'Kinematic', 'Dynamic'. + + Returns: + int: 0 for Static, 30 for Kinematic, 100 for Dynamic. + + Example: + >>> determine_mode_value("Static") + 0 + """ + if mode_raw == "Static": + return 0 + elif mode_raw == "Kinematic": + return 30 + elif mode_raw == "Dynamic": + return 100 + else: + raise ValueError(f"Unknown mode: {mode_raw!r}") + + @staticmethod + def extract_marker_name(rnx_path: str) -> str: + """ + Extract a 4-char site code (marker) from a RINEX filename. + + Arguments: + rnx_path (str): RNX file path. If empty/invalid, returns 'TEST'. + + Returns: + str: Upper-cased 4-char marker or 'TEST' when not found. + + Example: + >>> extract_marker_name("ALIC00AUS_R_20250190000_01D_30S_MO.rnx.gz") + 'ALIC' + """ + if not rnx_path: + return "TEST" + stem = Path(rnx_path).stem # drops .gz/.rnx + m = re.match(r"([A-Za-z]{4})", stem) + return m.group(1).upper() if m else "TEST" + + @staticmethod + def parse_time_window(time_window_raw: str): + """ + Convert 'start_time to end_time' into (start_epoch, end_epoch) strings. + + Arguments: + 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. + + Example: + >>> parse_time_window("2025-01-01_00:00:00 to 2025-01-02_00:00:00") + ('2025-01-01 00:00:00', '2025-01-02 00:00:00') + """ + try: + start, end = map(str.strip, time_window_raw.split("to")) + + # Replace underscores with spaces in datetime strings + start = start.replace("_", " ") + end = end.replace("_", " ") + return start, end + except ValueError: + raise ValueError("Invalid time_window format. Expected: 'start_time to end_time'") + + @staticmethod + def parse_antenna_offset(antenna_offset_raw: str): + """ + Convert 'e, n, u' string into [e, n, u] floats. + + Arguments: + antenna_offset_raw (str): e.g., '0.0, 0.0, 1.234'. + + Returns: + list[float]: [e, n, u] in metres. + + Example: + >>> parse_antenna_offset("0.1, -0.2, 1.0") + [0.1, -0.2, 1.0] + """ + try: + e, n, u = map(str.strip, antenna_offset_raw.split(",")) + return [float(e), float(n), float(u)] + except ValueError: + raise ValueError("Invalid antenna offset format. Expected: 'e, n, u'") + + @dataclass + class ExtractedInputs: + """ + Dataclass container for parsed UI values and raw strings. + """ + # Parsed / derived values + marker_name: str + start_epoch: str + end_epoch: str + epoch_interval: int + antenna_offset: list[float] + mode: int + + # Raw strings / controls that are needed downstream + constellations_raw: str + receiver_type: str + antenna_type: str + ppp_provider: str + ppp_series: str + ppp_project: str + + # File paths associated to this run + rnx_path: str + output_path: str + + # 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. + + Returns: + list[str]: Provider names; empty when products list is not yet available. + + Example: + >>> ctrl._get_ppp_provider_items() + """ + 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]: + """ + 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 + + +class CredentialsDialog(QDialog): + """ + UI controller class CredentialsDialog. + """ + + def __init__(self, parent=None): + """ + UI handler: initialize credential input widgets and layout. + + Arguments: + parent (Any): Optional parent widget. + """ + super().__init__(parent) + self.setWindowTitle("CDDIS Credentials") + + layout = QVBoxLayout() + + # Username + layout.addWidget(QLabel("Username:")) + self.username_input = QLineEdit() + layout.addWidget(self.username_input) + + # Password + layout.addWidget(QLabel("Password:")) + self.password_input = QLineEdit() + self.password_input.setEchoMode(QLineEdit.Password) + layout.addWidget(self.password_input) + + # Confirm button + self.confirm_button = QPushButton("Save") + self.confirm_button.clicked.connect(self.save_credentials) + layout.addWidget(self.confirm_button) + + self.setLayout(layout) + + def save_credentials(self): + """ + UI handler: validate username/password, save to netrc, and close dialog. + """ + username = self.username_input.text().strip() + password = self.password_input.text().strip() + + if not username or not password: + QMessageBox.warning(self, "Error", "Username and password cannot be empty") + return + + # ✅ Save correctly in one go (Windows will write both %USERPROFILE%\\.netrc and %USERPROFILE%\\_netrc; + # macOS/Linux will write ~/.netrc and automatically chmod 600; both URS and CDDIS entries are written) + try: + paths = save_earthdata_credentials(username, password) + except Exception as e: + QMessageBox.critical(self, "Save failed", f"❌ Failed to save credentials:\n{e}") + return + + QMessageBox.information(self, "Success", + "✅ 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: + if hasattr(self, "worker"): + _safe_call_stop(self.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 diff --git a/scripts/GinanUI/app/controllers/visualisation_controller.py b/scripts/GinanUI/app/controllers/visualisation_controller.py new file mode 100644 index 000000000..0519207d2 --- /dev/null +++ b/scripts/GinanUI/app/controllers/visualisation_controller.py @@ -0,0 +1,320 @@ +# 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. +""" +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.QtGui import QDesktopServices +from PySide6.QtWidgets import QTextEdit, QPushButton, QComboBox, QApplication +from PySide6.QtWebEngineWidgets import QWebEngineView +from scripts.GinanUI.app.utils.logger import Logger + +HERE = Path(__file__).resolve() +ROOT = HERE.parents[2] +DEFAULT_OUT_DIR = ROOT / "tests" / "resources" / "outputData" / "visual" + + +class VisualisationController(QObject): + """ + Manage 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 + [] + """ + + def __init__(self, ui, parent_window): + """ + Initialize 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 + self.parent = parent_window + self.html_files: List[str] = [] # paths of available visualisations + self.current_index: Optional[int] = None + self.external_base_url: Optional[str] = None + self._selector: Optional[QComboBox] = None + self._open_button: Optional[QPushButton] = None + + # --------------------------------------------------------------------- + # 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(paths) + # Refresh selector if bound + if self._selector: + self._refresh_selector() + if self.html_files: + self.display_html(0) + # Enable widgets once we have plots + if self._selector: + self._selector.setEnabled(True) + if self._open_button: + self._open_button.setEnabled(True) + else: + # Disable widgets if no plots available + if self._selector: + self._selector.setEnabled(False) + if self._open_button: + self._open_button.setEnabled(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 + """ + if not isinstance(index, int) or not (0 <= index < len(self.html_files)): + return + file_path = self.html_files[index] + self.current_index = index + self._embed_html(file_path) + + 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 + """ + if self.current_index is None: + return + path = self.html_files[self.current_index] + try: + url = QUrl.fromLocalFile(Path(path).resolve()) + + # Open the file with the appropriate method for the operating system + if platform.system() == "Windows": + # sys._MEIPASS and some dll file need to be changed + QDesktopServices.openUrl(url) + + elif platform.system() == "Darwin": + # sys._MEIPASS but might also work without any changes + QDesktopServices.openUrl(url) + + else: + # 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", url.url()], env=env) + except Exception as e: + Logger.console(f"Error occurred trying to open in browser: {e}") + + # ------------------------------------------------------------------ + # Helpers for wiring additional UI elements + # ------------------------------------------------------------------ + def bind_open_button(self, button: QPushButton): + """ + 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) + + def bind_selector(self, combo: QComboBox): + """ + Bind a QComboBox selector to manage and display HTML visualisations. + + 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 + + def safe_display(): + data = combo.currentData() + if isinstance(data, int): # Only proceed if it's a valid index + self.display_html(data) + + combo.currentIndexChanged.connect(lambda _: safe_display()) + combo.setEnabled(False) + self._refresh_selector() + + 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 + # ------------------------------------------------------------------ + + def _embed_html(self, file_path: str): + """ + Embed an HTML file inside the dedicated QWebEngineView in the UI. + + Arguments: + file_path (str): Absolute or relative path to a local HTML file to display. + """ + # Use the QWebEngineView that is defined in main_window.ui + webview: QWebEngineView = self.ui.webEngineView + + # Resolve to an absolute path so QWebEngineView can load it reliably + url = QUrl.fromLocalFile(str(Path(file_path).resolve())) + webview.setUrl(url) + + # Optional zoom factor + webview.setZoomFactor(0.8) + + # Install event filter if you still want to intercept events later + webview.installEventFilter(self) + + # 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 + + existing_html_paths = self._find_existing_html_files() + + all_html_paths = list(set(new_html_paths + existing_html_paths)) + + all_html_paths.sort(key=lambda x: os.path.basename(x)) + + self.set_html_files(all_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 + """ + existing_files = [] + + default_visual_dir = DEFAULT_OUT_DIR + if default_visual_dir.exists(): + for html_file in default_visual_dir.glob("*.html"): + existing_files.append(str(html_file)) + + if self.external_base_url: + pass + + return existing_files \ No newline at end of file diff --git a/scripts/GinanUI/app/main_window.py b/scripts/GinanUI/app/main_window.py new file mode 100644 index 000000000..430b9d47a --- /dev/null +++ b/scripts/GinanUI/app/main_window.py @@ -0,0 +1,497 @@ +from pathlib import Path +from typing import Optional + +from PySide6.QtCore import QUrl, Signal, QThread, Slot, Qt, QRegularExpression +from scripts.GinanUI.app.utils.logger import Logger +from PySide6.QtWidgets import QMainWindow, QDialog, QVBoxLayout, QPushButton, QComboBox +from PySide6.QtWebEngineWidgets import QWebEngineView +from PySide6.QtGui import QTextCursor, QTextDocument + +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.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.logger import Logger + +# Optional toggle for development visualization testing +test_visualisation = False + + +def setup_main_window(): + import sys + IS_FROZEN = getattr(sys, 'frozen', False) + + if not IS_FROZEN: + # Only compile UI during development + compile_ui() + + from scripts.GinanUI.app.views.main_window_ui import Ui_MainWindow + return Ui_MainWindow() + +class MainWindow(QMainWindow): + log_signal = Signal(str) + + def __init__(self): + super().__init__() + + # Setup UI + self.ui = setup_main_window() + self.ui.setupUi(self) + + # Add rounded corners to UI elements + self.setStyleSheet(""" + QPushButton { + border-radius: 4px; + } + QPushButton:disabled { + border-radius: 4px; + } + QTextEdit { + border-radius: 4px; + } + QComboBox { + border-radius: 4px; + } + """) + + # Fix macOS tab widget styling + self._fix_macos_tab_styling() + + # Initialize the Logger system for easy logging throughout the app + Logger.initialise(self) + + # Controllers + self.execution = Execution() + self.inputCtrl = InputController(self.ui, self, self.execution) + self.visCtrl = VisualisationController(self.ui, self) + + # Connect ready signals + self.inputCtrl.ready.connect(self.on_files_ready) + self.inputCtrl.pea_ready.connect(self._on_process_clicked) + + # State + self.rnx_file: Optional[str] = None + self.output_dir: Optional[str] = None + self.download_progress: dict[str, int] = {} # track per-file progress + self.is_processing = False + self.atx_required_for_rnx_extraction = False # File required to extract info from RINEX + self.metadata_downloaded = False + self.offline_mode = False # Track if running without internet + + # Visualisation widgets + + self.visCtrl.bind_open_button(self.ui.openInBrowserBtn) + + self.visCtrl.bind_selector(self.ui.visSelector) + + archive_products(INPUT_PRODUCTS_PATH, "startup_archival", True) + + # Validate connection then start metadata download in a separate thread + self._validate_cddis_credentials_once() + + # Only start metadata download if we have internet connection + if not self.offline_mode: + self.metadata_thread = QThread() + self.metadata_worker = DownloadWorker() + self.metadata_worker.moveToThread(self.metadata_thread) + + # Signals + self.metadata_thread.started.connect(self.metadata_worker.run) + self.metadata_worker.progress.connect(self._on_download_progress) + self.metadata_worker.finished.connect(self._on_metadata_download_finished) + self.metadata_worker.atx_downloaded.connect(self._on_atx_downloaded) + + # Cleanup + self.metadata_worker.finished.connect(self.metadata_thread.quit) + self.metadata_worker.finished.connect(self.metadata_worker.deleteLater) + self.metadata_thread.finished.connect(self.metadata_thread.deleteLater) + self.metadata_thread.start() + else: + Logger.terminal("⚠️ 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: + self.ui.stopAllButton.clicked.connect(self.on_stopAllClicked) + 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 _set_processing_state(self, processing: bool): + """Enable/disable UI elements during processing""" + self.is_processing = processing + + # Disable/enable the process button + self.ui.processButton.setEnabled(not processing) + + # Optionally disable other critical UI elements during processing + self.ui.observationsButton.setEnabled(not processing) + self.ui.outputButton.setEnabled(not processing) + self.ui.showConfigButton.setEnabled(not processing) + + # Update button text to show processing state + if processing: + self.ui.processButton.setText("Processing...") + # Set cursor to waiting cursor for visual feedback + self.setCursor(Qt.CursorShape.WaitCursor) + else: + self.ui.processButton.setText("Process") + self.setCursor(Qt.CursorShape.ArrowCursor) + + def on_files_ready(self, rnx_path: str, out_path: str): + self.rnx_file = rnx_path + self.output_dir = out_path + + def _on_process_clicked(self): + if not self.rnx_file or not self.output_dir: + Logger.terminal("⚠️ 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)") + from scripts.GinanUI.app.utils.toast import show_toast + show_toast(self, "⚠️ Processing requires internet connection", 4000) + + from PySide6.QtWidgets import QMessageBox + msg = QMessageBox(self) + msg.setIcon(QMessageBox.Icon.Warning) + msg.setWindowTitle("Offline Mode") + msg.setText("Processing requires an internet connection to download PPP products from CDDIS.") + msg.setInformativeText("Please check your internet connection and restart Ginan-UI.") + msg.setStandardButtons(QMessageBox.StandardButton.Ok) + msg.exec() + return + + # Prevent multiple simultaneous processing + if self.is_processing: + Logger.terminal("⚠️ Processing already in progress. Please wait...") + return + + # Lock the "Process" button and set processing state + self._set_processing_state(True) + + # Get PPP params from UI + ac = self.ui.PPP_provider.currentText() + project = self.ui.PPP_project.currentText() + series = self.ui.PPP_series.currentText() + + # Archive old products if needed + current_selection = {"ppp_provider": ac, "ppp_project": project, "ppp_series": series} + archive_dir = archive_products_if_selection_changed( + current_selection, getattr(self, "last_ppp_selection", None), INPUT_PRODUCTS_PATH + ) + self.last_ppp_selection = current_selection + if archive_dir: + Logger.terminal(f"📦 Archived old PPP products → {archive_dir}") + + output_archive = archive_old_outputs(Path(self.output_dir), archive_dir) + if output_archive: + Logger.terminal(f" Archived old outputs → {output_archive}") + + # List products to be downloaded + x = self.inputCtrl.products_df + products = x.loc[(x["analysis_center"] == ac) & (x["project"] == project) & (x["solution_type"] == series)].drop_duplicates() + + # Reset progress + self.download_progress.clear() + + # Start download in background + self.download_thread = QThread() + self.download_worker = DownloadWorker(products=products, start_epoch=self.inputCtrl.start_time, end_epoch=self.inputCtrl.end_time) + self.download_worker.moveToThread(self.download_thread) + + # Signals + self.download_thread.started.connect(self.download_worker.run) + self.download_worker.progress.connect(self._on_download_progress) + self.download_worker.finished.connect(self._on_download_finished) + + # Cleanup + self.download_worker.finished.connect(self.download_thread.quit) + self.download_worker.finished.connect(self.download_worker.deleteLater) + self.download_thread.finished.connect(self.download_thread.deleteLater) + + Logger.terminal("📡 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.""" + self.download_progress[filename] = percent + + total_length = 20 + filled_length = int(percent/100 * total_length) + bar = '[' + "█" * filled_length + "░" * (total_length - filled_length) + ']' + output = f"{filename[:30]} {bar} {percent:3d}%" + search_pattern = QRegularExpression(f"^{filename[:30]}.+%$") + + # Work with cursor & doc + cursor = self.ui.terminalTextEdit.textCursor() + cursor.movePosition(QTextCursor.End) + flags = QTextDocument.FindFlag.FindBackward + found_cursor = self.ui.terminalTextEdit.document().find(search_pattern, cursor, flags) + + on_latest_5_lines = self.ui.terminalTextEdit.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) + cursor.insertText("\n" + output) + cursor.movePosition(QTextCursor.End) + self.ui.terminalTextEdit.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.") + + def _on_metadata_download_finished(self, message): + Logger.terminal(message) + self.metadata_downloaded = True + self.inputCtrl.try_enable_process_button() + + def _on_download_finished(self, message): + Logger.terminal(message) + self._start_pea_execution() + + def _on_download_error(self, msg): + Logger.terminal(f"⚠️ PPP download error: {msg}") + self._set_processing_state(False) + + def _start_pea_execution(self): + Logger.terminal("⚙️ Starting PEA execution in background...") + + self.thread = QThread() + self.worker = PeaExecutionWorker(self.execution) + self.worker.moveToThread(self.thread) + + self.thread.started.connect(self.worker.run) + self.worker.finished.connect(self._on_pea_finished) + + self.worker.finished.connect(self.thread.quit) + self.worker.finished.connect(self.worker.deleteLater) + self.thread.finished.connect(self.thread.deleteLater) + + self.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): + try: + Logger.terminal("📊 Generating plots from PEA output...") + html_files = self.execution.build_pos_plots() + if html_files: + self.visCtrl.set_html_files(html_files) + else: + Logger.terminal("⚠️ No plots found.") + except Exception as err: + Logger.terminal(f"⚠️ Plot generation failed: {err}") + + if test_visualisation: + try: + Logger.terminal("[Dev] Testing static visualisation...") + test_output_dir = Path(__file__).resolve().parents[1] / "tests" / "resources" / "outputData" + test_visual_dir = test_output_dir / "visual" + test_visual_dir.mkdir(parents=True, exist_ok=True) + self.visCtrl.build_from_execution() + Logger.terminal("[Dev] Static plot generation complete.") + except Exception as err: + Logger.terminal(f"[Dev] Test plot generation failed: {err}") + + def _validate_cddis_credentials_once(self): + """ + Validate CDDIS credentials and connectivity. + If no internet, app continues in offline mode with warning. + """ + ok, where = gui_validate_netrc() + if not ok and hasattr(self.ui, "cddisCredentialsButton"): + Logger.terminal("⚠️ 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}") + return + Logger.terminal(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}") + 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( + 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.") + + # Connection successful - set email + write_email(email_candidate) + Logger.terminal(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}") + self._show_offline_warning("No internet connection", error_msg) + return + + def _show_offline_warning(self, title: str, details: str): + """ + Show a warning dialog when Ginan-UI starts without internet. + The app can continue to run, but very limited (some features are unavailable) + """ + from PySide6.QtWidgets import QMessageBox + + # Mark as offline mode + self.offline_mode = True + + msg = QMessageBox(self) + msg.setIcon(QMessageBox.Icon.Warning) + msg.setWindowTitle("Ginan-UI - No Internet Connection") + msg.setText( + "Ginan-UI requires internet access to function properly

    " + "The following features will be unavailable:" + ) + msg.setInformativeText( + "- Downloading PPP products from CDDIS
    " + "- Scanning for available analysis centers
    " + "- Retrieving GNSS data products

    " + "The application will continue to run in offline mode.
    " + "You can still view configurations and access local files." + ) + msg.setDetailedText(f"Error details:\n{title}: {details}") + msg.setStandardButtons(QMessageBox.StandardButton.Ok) + msg.setDefaultButton(QMessageBox.StandardButton.Ok) + + # Show the dialog + msg.exec() + + # Also show a toast notification + from scripts.GinanUI.app.utils.toast import show_toast + show_toast(self, "⚠️ Running in offline mode - limited functionality", 8000) + + # Added: unified stop entry, wired to an optional UI button + @Slot() + def on_stopAllClicked(self): + Logger.terminal("🛑 Stop requested — stopping all running tasks...") + + # Stop the metadata worker in InputController, if present + try: + if hasattr(self, "inputCtrl") and hasattr(self.inputCtrl, "stop_all"): + self.inputCtrl.stop_all() + except Exception: + pass + + # Stop PPP downloads, if running + try: + if hasattr(self, "download_worker") and self.download_worker is not None and hasattr(self.download_worker, "stop"): + self.download_worker.stop() + except Exception: + pass + + # Stop PEA execution, if running + try: + if hasattr(self, "worker") and self.worker is not None and hasattr(self.worker, "stop"): + self.worker.stop() + except Exception: + pass + + # Best-effort: ask Execution to stop any external process if supported + try: + if hasattr(self, "execution") and self.execution is not None and hasattr(self.execution, "stop_all"): + self.execution.stop_all() + except Exception: + pass + + # Restore UI state immediately + try: + self._set_processing_state(False) + except Exception: + pass + + 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.tabWidget.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.tabWidget.setStyleSheet(tab_bar_stylesheet) \ No newline at end of file diff --git a/scripts/GinanUI/app/models/__init__.py b/scripts/GinanUI/app/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/GinanUI/app/models/archive_manager.py b/scripts/GinanUI/app/models/archive_manager.py new file mode 100644 index 000000000..3fe4e6893 --- /dev/null +++ b/scripts/GinanUI/app/models/archive_manager.py @@ -0,0 +1,161 @@ +# app/utils/archive_manager.py + +from pathlib import Path +import shutil +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 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. + """ + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archive_dir = output_dir / "archive" / timestamp + archive_dir.mkdir(parents=True, exist_ok=True) + + # Move .pos, .log, .txt, etc. from output_dir + moved_files = 0 + for ext in [".pos", ".POS", ".log", ".txt", ".json"]: + for file in output_dir.glob(f"*{ext}"): + shutil.move(str(file), archive_dir / file.name) + moved_files += 1 + + # Move HTML visual files (optional) + if visual_dir and visual_dir.exists(): + visual_archive = archive_dir / "visual" + visual_archive.mkdir(parents=True, exist_ok=True) + for html_file in visual_dir.glob("*.html"): + shutil.move(str(html_file), visual_archive / html_file.name) + moved_files += 1 + + 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(products_dir: Path = INPUT_PRODUCTS_PATH, reason: str = "manual", startup_archival: bool = False, + include_patterns: Optional[list[str]] = None) -> Optional[Path]: + """ + Archive GNSS product files from products_dir into a timestamped subfolder + under products_dir/archived/. + + :param products_dir: Directory containing GNSS product files + :param reason: String describing why the archive is happening (e.g., "rinex_change", "ppp_selection_change") + :param startup_archival: If True, archives files which are meant to be archived only once during app startup + :param include_patterns: Optional list of glob patterns to include when archiving + :return: Path to the archive folder if files were archived, else None + """ + if not products_dir.exists(): + Logger.console(f"Products dir {products_dir} does not exist.") + return None + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archive_dir = products_dir / "archived" / f"{reason}_{timestamp}" + archive_dir.mkdir(parents=True, exist_ok=True) + + product_patterns = [ + "*.SP3", # precise orbit + "*.CLK", # clock files + "*.BIA", # biases + "*.ION", # ionosphere products (if used) + "*.TRO", # troposphere products (if used) + ] + + if startup_archival: + startup_patterns = [ + "finals.data.iau2000.txt", + "BRDC*.rnx", + "igs_satellite_metadata*.snx", + "igs20*.atx", + "tables/ALOAD*", + "tables/OLOAD*", + "tables/gpt_*.grd", + "tables/qzss_*" + "tables/igrf*coeffs.txt", + "tables/DE436*", + "tables/fes2014*.dat", + "tables/opoleloadcoefcmcor.txt", + "tables/sat_yaw*.snx", + "tables/bds_yaw*.snx", + "tables/qzss_yaw*.snx", + "tables/bds_yaw*.snx" + ] + # Ensure directories exist + products_dir.mkdir(parents=True, exist_ok=True) + (products_dir / "tables").mkdir(parents=True, exist_ok=True) + + # Scans every file and checks created within 7 days + for pattern in startup_patterns: + for file in products_dir.glob(pattern): + creation_time = datetime.fromtimestamp(file.stat().st_ctime) + if (datetime.now() - creation_time).days > 7: + Logger.console(f"Startup archival: {file.name} is older than 7 days, archiving.") + product_patterns.append(pattern) + + # Include explicit patterns if provided + if include_patterns: + product_patterns.extend(include_patterns) + + archived_files = [] + for pattern in product_patterns: + for file in products_dir.glob(pattern): + try: + target = archive_dir / file.name + shutil.move(str(file), str(target)) + archived_files.append(file.name) + except Exception as e: + Logger.console(f"Failed to archive {file.name}: {e}") + + if archived_files: + Logger.console(f"Archived {', '.join(archived_files)} → {archive_dir}") + return archive_dir + else: + Logger.console("No matching product files found to archive.") + 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", include_patterns=["BRDC*.rnx*"]) + + +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") + diff --git a/scripts/GinanUI/app/models/dl_products.py b/scripts/GinanUI/app/models/dl_products.py new file mode 100644 index 000000000..fa726cf87 --- /dev/null +++ b/scripts/GinanUI/app/models/dl_products.py @@ -0,0 +1,489 @@ +import gzip, os, shutil, unlzw3, requests +import pandas as pd +import numpy as np +from bs4 import BeautifulSoup, SoupStrainer +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional, Callable, 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 scripts.GinanUI.app.utils.gn_functions import GPSDate +from scripts.GinanUI.app.utils.logger import Logger + +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) + +METADATA = [ + "https://files.igs.org/pub/station/general/igs_satellite_metadata.snx", + "https://files.igs.org/pub/station/general/igs20.atx", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/OLOAD_GO.BLQ.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/ALOAD_GO.BLQ.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/igrf14coeffs.txt.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/opoleloadcoefcmcor.txt.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/fes2014b_Cnm-Snm.dat.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/DE436.1950.2050.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/gpt_25.grd.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/bds_yaw_modes.snx.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/qzss_yaw_modes.snx.gz", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/sat_yaw_bias_rate.snx.gz", + "https://datacenter.iers.org/data/latestVersion/finals.data.iau2000.txt" +] + + +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 + + +def str_to_datetime(date_time_str): + """ + :param date_time_str: YYYY-MM-DD_HH:mm:ss + :returns datetime: datetime.strptime() + """ + try: + # YYYY-dddHHmm format through datetime.strptime(date_time,"%Y%j%H%M") + return datetime.strptime(date_time_str, "%Y-%m-%d_%H:%M:%S") + 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. + Filter the DataFrame (i.e. for a specific ppp provider) then use download_products() to download the files. + + :param start_time: the start of the time window (start_epoch) + :param end_time: the start of the time window (end_epoch) + :param target_files: list of target files to filter for, defaulted to ["CLK","BIA","SP3"] + :returns: dataframe of products, columns: "analysis_center", "project", "date", "solution_type", "period", + "resolution", "content", "format" + """ + if target_files is None: + target_files = ["CLK", "BIA", "SP3"] + else: + target_files = [file.upper() for file in target_files] + + products = pd.DataFrame( + columns=["analysis_center", "project", "date", "solution_type", "period", "resolution", "content", "format"]) + + # 1. Retrieve available options + gps_weeks = range(date_to_gpswk(start_time), date_to_gpswk(end_time) + 1) + for gps_week in gps_weeks: + url = f"https://cddis.nasa.gov/archive/gnss/products/{gps_week}/" + try: + week_files = requests.get(url, timeout=10) + week_files.raise_for_status() + except requests.RequestException as e: + raise requests.RequestException(f"Failed to fetch files for GPS week {gps_week}: {e}") + + # 2. Extract data from available options + soup = BeautifulSoup(week_files.content, "html.parser", + parse_only=SoupStrainer("div", class_="archiveItemTextContainer")) + # Above SoupStrainer makes it that only relevant items are stored in memory + for div in soup: + filename = div.get_text().split(" ")[0] + try: + if gps_week < 2237: + # Format convention changed in week 2237 + # AAAWWWWD.TYP.Z + center = filename[0:3].upper() # e.g. "COD" + _type = "FIN" # pre-2237 were probably always final solutions :shrug: + day = int(filename[7]) # e.g. "0", 0-indexed, 7 indicates weekly + _format = filename[9:12].upper() # e.g. "snx", "ssc", "sum", "erp" + project = "OPS" + sampling_resolution = None + content = None + date = gpswk_to_date(gps_week) + if 0 < day < 7: + date += timedelta(days=day) + period = timedelta(days=1) + else: + period = timedelta(days=7) + + else: + # e.g. GRG0OPSFIN_20232620000_01D_01D_SOL.SNX.gz + # AAA0OPSSNX_YYYYDDDHHMM_LEN_SMP_CNT.FMT.gz + center = filename[0:3] # e.g. "COD" + project = filename[4:7] # e.g. "OPS" or "RNN" unused + _type = filename[7:10] # e.g. "FIN" + year = int(filename[11:15]) # e.g. "2023" + day_of_year = int(filename[15:18]) # e.g. "262" + hour = int(filename[18:20]) # e.g. "00" + minute = int(filename[20:22]) # e.g. "00" + intended_period = filename[23:26] # eg "01D" + sampling_resolution = filename[27:30] # eg "01D" + content = filename[31:34] # e.g. "SOL" + _format = filename[35:38] # e.g. "SNX" + + date = datetime(year, 1, 1, hour, minute) + timedelta(day_of_year - 1) + period = timedelta(days=int(intended_period[:-1])) # Assuming all periods are in days :shrug: + + if _format in target_files and start_time <= date <= end_time: + products.loc[len(products)] = { + "analysis_center": center, + "project": project, + "date": date, + "solution_type": _type, + "period": period, + "resolution": sampling_resolution, + "content": content, + "format": _format + } + except (ValueError, IndexError): + # Skips md5 sums and other non-conforming files + continue + products = products.drop_duplicates(inplace=False) # resets indexes too + return products + + +def get_valid_analysis_centers(data: pd.DataFrame) -> set[str]: + """ + Analyzes dataframe for valid analysis centers (those that provide contiguous coverage) + AND have all required file types (SP3, BIA, CLK) for at least one series. + + :param data: products dataframe, see: get_product_dataframe(), requires columns: "analysis_center", "project", + "date", "solution_type", "period", "resolution", "content", "format" + :returns: set of valid analysis centers that have all required files for at least one series + """ + # Required file types for PPP processing + REQUIRED_FILES = {"SP3", "BIA", "CLK"} + + # 1. Check for any gaps in coverage and remove + for (center, _type, _format), group in data.groupby(["analysis_center", "solution_type", "format"]): + # Time window is filtered for in get_product_dataframe; only need to check they're contiguous + group = group.sort_values("date").reset_index(drop=True) + for i in range(len(group) - 1): + if group.loc[i]["date"] + group.loc[i]["period"] < group.loc[i + 1]["date"]: + Logger.console( + f"Gap detected for {center} {_type} {_format} between {group.loc[i, 'date']} and {group.loc[i + 1, 'date']}") + data = data[ + ~((data["analysis_center"] == center) and + (data["solution_type"] == _type) and + (data["format"] == _format))] + + # 2. Filter for centers that have all required files for at least one series + valid_centers = set() + for analysis_center in data["analysis_center"].unique(): + center_data = data[data["analysis_center"] == analysis_center] + + # Check each series to see if it has all required files + for series in center_data["solution_type"].unique(): + series_data = center_data[center_data["solution_type"] == series] + available_files = set(series_data["format"].unique()) + + # If this series has all required files, the center is valid + if REQUIRED_FILES.issubset(available_files): + valid_centers.add(analysis_center) + break # No need to check other series for this center + + # 3. Only show series with all required files + centers = set() + for analysis_center in sorted(valid_centers): + centers.add(analysis_center) + center_products = data.loc[data["analysis_center"] == analysis_center] + + # Build a dict of file types to their available series + file_series_map = {} + for _format in center_products["format"].unique(): + series_list = center_products.loc[center_products["format"] == _format, "solution_type"].unique() + file_series_map[_format] = set(series_list) + + # Find series that have all required files + valid_series_for_center = set() + for series in center_products["solution_type"].unique(): + series_data = center_products[center_products["solution_type"] == series] + available_files = set(series_data["format"].unique()) + if REQUIRED_FILES.issubset(available_files): + valid_series_for_center.add(series) + + # Build output string showing only complete series + offerings = "" + for _format in sorted(REQUIRED_FILES): + if _format in file_series_map: + # Only show series that have all three file types + complete_series = sorted(file_series_map[_format].intersection(valid_series_for_center)) + if complete_series: + offerings += f"{_format}:({'/'.join(complete_series)}) " + + Logger.console(f"Analysis centre {analysis_center} offers: {offerings.strip()}") + + 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. + + :param data: products dataframe from get_product_dataframe() + :param provider: analysis center name (e.g., "COD", "GRG") + :returns: sorted list of valid series codes (e.g., ["FIN", "RAP"]) + """ + REQUIRED_FILES = {"SP3", "BIA", "CLK"} + + # Filter for this provider + provider_data = data[data["analysis_center"] == provider] + + # Find series that have all required files + valid_series = [] + for series in provider_data["solution_type"].unique(): + series_data = provider_data[provider_data["solution_type"] == series] + available_files = set(series_data["format"].unique()) + + if REQUIRED_FILES.issubset(available_files): + valid_series.append(series) + + return sorted(valid_series) + +def get_valid_providers_with_series(data: pd.DataFrame) -> dict: + """ + Get a mapping of providers to their valid series (with all required files). + + :param data: products dataframe from get_product_dataframe() + :returns: dict mapping provider names to lists of valid series + """ + provider_series_map = {} + + for provider in data["analysis_center"].unique(): + valid_series = get_valid_series_for_provider(data, provider) + if valid_series: # Only include providers with at least one valid series + provider_series_map[provider] = valid_series + + return provider_series_map + +def extract_file(filepath: Path) -> Path: + """ + Extracts [".gz", ".gzip", ".Z"] files with gzip and unlzw3 respectively. + Deletes compressed file after extraction. + + :param filepath: compressed file path + :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) + filepath.unlink() + return Path(finalpath) + + +def download_file(url: str, session: requests.Session, download_dir: Path = INPUT_PRODUCTS_PATH, + progress_callback: Optional[Callable] = None, + stop_requested: Callable = None) -> Path: + """ + Checks if file already exists (additionally in compressed or .part forms). + Uses provided session for CDDIS files (session made during startup). + Downloads in chunks to a file with the same file path suffixed by .part. + Deletes .part file after download. + Automatically calls extract_file() on compressed files. + + :param url: download url + :param session: requests Session preloaded with users CDDIS credentials + :param download_dir: dir to download to + :param progress_callback: reports, on every chunk, an int percentage of total download + :param stop_requested: bool callback. Raises a RuntimeError if occurred during download + :raises RuntimeError: Stop requested during download + :raises Exception: Max retries reached + :return: + """ + + filepath = Path(download_dir / url.split("/")[-1]) # Download dir + filename + # 1. When file already exists, extract if possible, then return + if filepath.exists(): + if filepath.suffix in COMPRESSED_FILETYPE: + return extract_file(filepath) + else: + return filepath + + # 2. Check if an extracted version of this file already exists + if filepath.suffix in COMPRESSED_FILETYPE: + potential_decompressed = filepath.with_suffix('') # Remove one suffix + if potential_decompressed.exists(): + return potential_decompressed + + # 3. Download the file in chunks (.part) + for i in range(MAX_RETRIES): + _partial = filepath.with_suffix(filepath.suffix + ".part") + + 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}") + else: + # Download whole file + headers = {"Range": "bytes=0-"} + Logger.terminal(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 + ensure_file_exists = open(_partial, "w") + ensure_file_exists.close() + + try: + if url.startswith(BASE_URL): + # Download files from CDDIS with authorized session + resp = session.get(url, headers=headers, stream=True, timeout=30) + else: + resp = requests.get(url, headers=headers, stream=True, timeout=30) + resp.raise_for_status() + + if resp.status_code == 206: + # Received partial content as expected + mode = 'ab' if _partial.exists() else 'wb' + total_size = int(resp.headers.get("content-length")) + _partial.stat().st_size + else: + # likely 200 OK, server is sending the entire file again + mode = 'wb' + total_size = int(resp.headers.get("content-length")) + + with open(_partial, mode) as partial_out: + downloaded = _partial.stat().st_size + for _chunk in resp.iter_content(chunk_size=CHUNK_SIZE): + if stop_requested and stop_requested(): + raise RuntimeError("Stop requested during download.") + + if _chunk: # Filters keep-alives + partial_out.write(_chunk) + downloaded += len(_chunk) + + if progress_callback: + percent = int(downloaded / total_size * 100) + progress_callback(filepath.name, percent) + + os.rename(_partial, filepath) + + if filepath.suffix in COMPRESSED_FILETYPE: + return extract_file(filepath) + else: + return filepath + except requests.RequestException as e: + Logger.terminal(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]: + """ + 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 download_metadata(download_dir: Path = INPUT_PRODUCTS_PATH, + progress_callback: Optional[Callable] = None, atx_callback: Optional[Callable] = None): + """ + Calls download_products() with args to download standard metadata files. Calls atx_callback("igs20.atx") + once "igs20.atx" is downloaded. Won't install duplicate files. + + :param download_dir: dir to download to + :param progress_callback: reports, on every chunk, an int percentage of total download + :param atx_callback: Optional callback function when igs20.atx is downloaded (downloaded_file) + :raises Exception: Max retries reached + """ + for download in download_products(products=pd.DataFrame(), download_dir=download_dir, + progress_callback=progress_callback, dl_urls=METADATA): + if atx_callback and download.name == "igs20.atx": + atx_callback(download.name) + + +def download_products(products: pd.DataFrame, download_dir: Path = INPUT_PRODUCTS_PATH, + dl_urls: list = None, progress_callback: Optional[Callable] = None, + stop_requested: Optional[Callable] = None) -> Generator[Path, None, None]: + """ + Creates download URLs for products and subsequently calls download_file() on them. Won't install duplicate files. + + :param pd.DataFrame products: (from get_product_dataframe) of all products to download + :param download_dir: dir to download to + :param dl_urls: Optional list of additional URLs to download (e.g. BRDC files) + :param progress_callback: reports, on every chunk, an int percentage of total download + :param stop_requested: bool callback. Raises a RuntimeError if occurred during download + :returns: Generator with paths to downloaded files + :raises RuntimeError: Stop requested during download + :raises Exception: Max retries reached + """ + + # 1. Generate filenames from the DataFrame + downloads = [] + for _, row in products.iterrows(): + gps_week = date_to_gpswk(row.date) + if gps_week < 2237: + # AAAWWWWD.TYP.Z + # e.g. COD22360.FIN.SNX.gz + if row.period == timedelta(days=7): + day = 7 + else: + day = int((row.date - gpswk_to_date(gps_week)).days) + filename = f"{row.analysis_center.lower()}{gps_week}{day}.{row.format.lower()}.Z" + else: + # e.g. GRG0OPSFIN_20232620000_01D_01D_SOL.SNX.gz + # AAA0OPSSNX_YYYYDDDHHMM_LEN_SMP_CNT.FMT.gz + filename = f"{row.analysis_center}0{row.project}{row.solution_type}_{row.date.strftime('%Y%j%H%M')}_{row.period.days:02d}D_{row.resolution}_{row.content}.{row.format}.gz" + + url = f"{BASE_URL}/gnss/products/{gps_week}/{filename}" + downloads.append(url) + + if dl_urls: + downloads.extend(dl_urls) + + Logger.terminal(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) + _sesh = requests.Session() + _sesh.auth = get_netrc_auth() + for url in downloads: + _x = url.split("/") + if len(_x) < 2: + fin_dir = download_dir + else: + fin_dir = download_dir / "tables" if _x[-2] == "tables" else download_dir + yield download_file(url, _sesh, fin_dir, progress_callback, stop_requested) + + +if __name__ == "__main__": + # Test whole file download + sesh = requests.Session() + sesh.auth = get_netrc_auth() + x = Path(f"{INPUT_PRODUCTS_PATH}/COD0MGXFIN_20191950000_01D_01D_OSB.BIA.gz") + if x.exists(): + x.unlink() + if x.with_suffix('').exists(): + x.with_suffix('').unlink() + if x.with_suffix(x.suffix + ".part").exists(): + x.with_suffix(x.suffix + ".part").unlink() + + download_file(f"{BASE_URL}/gnss/products/2062/{x.name}", sesh, INPUT_PRODUCTS_PATH) + + # Test resuming a partial download + os.remove(x.with_suffix('')) # should extract file + y = x.with_suffix(x.suffix + ".part") + req = sesh.get(f"{BASE_URL}/gnss/products/2062/{x.name}", headers={"Range": f"bytes=0-{CHUNK_SIZE}"}, stream=True) + with open(y, "wb") as z: + for chunk in req.iter_content(chunk_size=CHUNK_SIZE): + if chunk: # Filters keep-alives + z.write(chunk) + Logger.console(f"Downloaded {y.stat().st_size} bytes to {y}.\nAttempting to resume full download...") + download_file(f"{BASE_URL}/gnss/products/2062/{x.name}", sesh, INPUT_PRODUCTS_PATH) + Logger.console(f"Success!") + x.unlink() \ No newline at end of file diff --git a/scripts/GinanUI/app/models/execution.py b/scripts/GinanUI/app/models/execution.py new file mode 100644 index 000000000..da46da21b --- /dev/null +++ b/scripts/GinanUI/app/models/execution.py @@ -0,0 +1,456 @@ +import os +import platform +import shutil +import subprocess +import signal +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 scripts.plot_pos import plot_pos_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(): + """ + Checks system platform and returns a Path to the respective executable. Also searches for "pea" on PATH. + + :return: Path to executable or str of PATH callable + :raises RuntimeError: If PEA binary cannot be found + """ + import sys + + # 1. Check if running in PyInstaller bundle + if getattr(sys, 'frozen', False): + # Running in bundled mode + base_path = Path(sys._MEIPASS) + + # On macOS .app bundles, binaries are in Resources/bin/ + if platform.system().lower() == "darwin": + # Try Resources/bin first (macOS .app structure) + pea_path = base_path.parent / "Resources" / "bin" / "pea" + if pea_path.exists(): + print(f"[Execution] Found bundled PEA binary at: {pea_path}") + return pea_path + # Fallback to _internal/bin + pea_path = base_path / "bin" / "pea" + if pea_path.exists(): + print(f"[Execution] Found bundled PEA binary at: {pea_path}") + return pea_path + + # Linux/Windows: binaries in _internal/bin + else: + # Windows uses .exe extension + exe_name = "pea.exe" if platform.system().lower() == "windows" else "pea" + pea_path = base_path / "bin" / exe_name + if pea_path.exists(): + return pea_path + + print(f"[Execution] Bundled binary not found in expected locations") + # Fall through to try other methods + + # 2. Check if 'pea' is on PATH (most reliable if user has configured their environment) + if shutil.which("pea"): + executable = "pea" + Logger.console(f"✅ Found PEA on PATH: {shutil.which('pea')}") + return executable + + # 3. Try to find PEA relative to this script's location + # Current file: ginan/scripts/GinanUI/app/models/execution.py + # Target file: ginan/bin/pea + try: + current_file = Path(__file__).resolve() + # Navigate from: "ginan/scripts/GinanUI/app/models/execution.py" to "ginan/" + ginan_root = current_file.parents[4] # Go up: models -> app -> GinanUI -> scripts -> ginan + + # Check for the binary in ginan/bin/pea + pea_binary = ginan_root / "bin" / "pea" + + if pea_binary.exists() and pea_binary.is_file(): + # Make sure it's executable (permissions are set up right) + if not os.access(pea_binary, os.X_OK): + Logger.console(f"✅ Found PEA at {pea_binary} but it's not executable. Attempting to fix...") + try: + pea_binary.chmod(pea_binary.stat().st_mode | 0o111) # Add "execute" permissions + Logger.console(f"✅ Made PEA executable") + except Exception as e: + Logger.console(f"⚠️ Could not make PEA executable: {e}") + raise RuntimeError(f"⚠️ PEA binary found at {pea_binary} but is not executable and cannot be fixed") + + Logger.console(f"✅ Found PEA binary at: {pea_binary}") + return pea_binary + else: + Logger.console(f"⚠️ Expected PEA binary at {pea_binary} but not found") + + except Exception as e: + Logger.console(f"⚠️ Error while searching for PEA relative to script location: {e}") + + # 4. Platform-specific fallbacks (optional - can be removed if not needed) + system = platform.system().lower() + + if system == "windows": + # Windows may have pea.exe set up + if shutil.which("pea.exe"): + executable = "pea.exe" + Logger.console(f"✅ Found pea.exe on PATH: {shutil.which('pea.exe')}") + return executable + raise RuntimeError( + "PEA executable not found. Please:\n" + "1. Build the PEA binary (see ginan build instructions)\n" + "2. Add ginan/bin to your PATH, or\n" + "3. Run from within the ginan directory structure" + ) + + # 5. If nothing found, provide a helpful error message + raise RuntimeError( + f"PEA executable not found. Please ensure:\n" + f"1. You have built the PEA binary (should be at ginan/bin/pea)\n" + f"2. You are running GinanUI from within the ginan directory structure, or\n" + f"3. The 'pea' executable is available on your system PATH\n" + f"\nSearched locations:\n" + f" - System PATH\n" + f" - {ginan_root / 'bin' / 'pea' if 'ginan_root' in locals() else 'Could not determine ginan root'}" + ) + + +class Execution: + def __init__(self, config_path: Path = GENERATED_YAML): + """ + Caches config changes, interacts with config file, and finally can call pea executable. + + :param config_path: Path to a config file, defaulted to 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._procs = [] + self._stop_event = threading.Event() + + template_file = Path(TEMPLATE_PATH) + + if config_path.exists(): + Logger.console(f"Using existing config file: {config_path}") + else: + Logger.console( + f"Existing config not found, copying default template: {template_file} → {config_path}") + try: + config_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(template_file, config_path) + except Exception as e: + raise RuntimeError(f"❌ Failed to copy default config: {e}") + + self.config = load_yaml(config_path) + + def reload_config(self): + """ + Force reload of the YAML config from disk into memory. + This allows any manual edits to be picked up before GUI changes are applied. + + :raises RuntimeError: Any error occurred during load_yaml(config_path) + """ + try: + self.config = load_yaml(self.config_path) + except Exception as e: + raise RuntimeError(f"❌ Failed to reload config from {self.config_path}: {e}") + + def edit_config(self, key_path: str, value, add_field=False): + """ + Edits the cached config while preserving YAML formatting and comments. + + :param key_path: Dot-separated YAML key path (e.g., "inputs.gnss_observations.rnx_inputs") + :param value: New value to assign (will be converted to ruamel-safe types) + :param add_field: Whether to add the field if it doesn't exist + :raises KeyError if path doesn't exist and add_field is False + """ + self.changes = True # Mark config as changed + keys = key_path.split(".") + node = self.config + + for key in keys[:-1]: + if key not in node: + if add_field: + node[key] = CommentedMap() + else: + raise KeyError(f"Key '{key}' not found in {node}") + node = node[key] + + final_key = keys[-1] + value = normalise_yaml_value(value) + + # Preserve any existing comment on the final_key + if final_key in node: + old_value = node[final_key] + if hasattr(old_value, 'ca') and not hasattr(value, 'ca'): + value.ca = old_value.ca + + if not add_field and final_key not in node: + raise KeyError(f"Key '{final_key}' not found in {key_path}") + + node[final_key] = value + + def apply_ui_config(self, inputs): + """ + Applies UI settings to **cached** config. **Call write_cached_changes()** to write them. + + :param inputs: + """ + self.changes = True + + # 1. Set core inputs / outputs + self.edit_config("inputs.inputs_root", str(INPUT_PRODUCTS_PATH) + "/", False) + + # Extract directory and filename from RINEX path + rnx_path = Path(inputs.rnx_path) + rnx_directory = str(rnx_path.parent) + rnx_filename = rnx_path.name + + # Set gnss_observations_root to the directory containing the RINEX file + self.edit_config("inputs.gnss_observations.gnss_observations_root", rnx_directory, False) + + # Use only the filename (relative path) for rnx_inputs + rnx_val = normalise_yaml_value(rnx_filename) + + # 1a. Set rnx_inputs safely, preserving formatting + try: + existing = self.config["inputs"]["gnss_observations"].get("rnx_inputs") + if isinstance(existing, CommentedSeq): + existing.clear() + existing.append(rnx_val) + existing.fa.set_block_style() + else: + new_seq = CommentedSeq([rnx_val]) + new_seq.fa.set_block_style() + self.config["inputs"]["gnss_observations"]["rnx_inputs"] = new_seq + except Exception as e: + Logger.console(f"[apply_ui_config] Error setting rnx_inputs: {e}") + + # Normalise outputs_root + out_val = normalise_yaml_value(inputs.output_path) + self.edit_config("outputs.outputs_root", out_val, False) + + # 2. Replace 'TEST' receiver block with real marker name + if "TEST" in self.config.get("receiver_options", {}): + self.config["receiver_options"][inputs.marker_name] = self.config["receiver_options"].pop("TEST") + + # 3. Include UI-extracted values + self.edit_config("processing_options.epoch_control.start_epoch", PlainScalarString(inputs.start_epoch), False) + self.edit_config("processing_options.epoch_control.end_epoch", PlainScalarString(inputs.end_epoch), False) + self.edit_config("processing_options.epoch_control.epoch_interval", inputs.epoch_interval, False) + self.edit_config(f"receiver_options.{inputs.marker_name}.receiver_type", inputs.receiver_type, True) + self.edit_config(f"receiver_options.{inputs.marker_name}.antenna_type", inputs.antenna_type, True) + self.edit_config(f"receiver_options.{inputs.marker_name}.models.eccentricity.offset", inputs.antenna_offset, + True) + + # Always format process_noise as a list + self.edit_config("estimation_parameters.receivers.global.pos.process_noise", [inputs.mode], False) + + # 4. GNSS constellation toggles + all_constellations = ["gps", "gal", "glo", "bds", "qzs"] + for const in all_constellations: + self.edit_config(f"processing_options.gnss_general.sys_options.{const}.process", False, False) + + # Then enable only the selected constellations + if inputs.constellations_raw: + selected = [c.strip().lower() for c in inputs.constellations_raw.split(",") if c.strip()] + for const in selected: + if const in all_constellations: + self.edit_config(f"processing_options.gnss_general.sys_options.{const}.process", True, False) + + def write_cached_changes(self): + write_yaml(self.config_path, self.config) + self.changes = False + + def execute_config(self): + """ + If changes were made since last write, writes config, then executes pea with config. + All PEA output is logged to the console widget. + """ + # Check if executable is available + if self.executable is None: + raise RuntimeError("❌ PEA executable not configured yet. Cannot run processing.") + + # clear stop flag before each run + self.reset_stop_flag() + + if self.changes: + self.write_cached_changes() + self.changes = False + + command = [self.executable, "--config", str(self.config_path)] + workdir = str(Path(self.config_path).parent) + + Logger.console(f"🚀 Starting PEA: {' '.join(str(c) for c in command)}") + Logger.console(f"📂 Working directory: {workdir}") + Logger.console("=" * 60) + + try: + # spawn process with process group + p = self.spawn_process(command, cwd=workdir) + + # forward stdout/stderr line by line to console, can be stopped at any time + assert p.stdout is not None and p.stderr is not None + + # Use a separate thread to read stderr so we don't miss any output + stderr_lines = [] + + def read_stderr(): + for line in p.stderr: + if line: + stderr_lines.append(line.rstrip()) + + stderr_thread = threading.Thread(target=read_stderr, daemon=True) + stderr_thread.start() + + while True: + if self._stop_event.is_set(): + # UI clicked "stop", exit loop, cleanup handled by stop_all() + Logger.console("🛑 PEA execution stopped by user") + break + + line = p.stdout.readline() + if line: + # Log each line of PEA output to console + Logger.console(line.rstrip()) + else: + # no new output, check if process has ended + if p.poll() is not None: + # Process finished, log any remaining stderr + stderr_thread.join(timeout=1.0) + for err_line in stderr_lines: + if err_line: + Logger.console(f"⚠️ {err_line}") + + if p.returncode != 0: + Logger.console(f"❌ PEA exited with code {p.returncode}") + e = subprocess.CalledProcessError(p.returncode, command) + e.add_note("Error executing PEA command") + raise e + else: + Logger.console("=" * 60) + Logger.console("✅ PEA execution completed successfully") + break + + # slight sleep to avoid busy polling + time.sleep(0.01) + + finally: + # after execution, clean up finished processes + self._procs = [proc for proc in self._procs if proc.poll() is None] + + def spawn_process(self, args, cwd=None, env=None) -> subprocess.Popen: + """ + Unified process spawning: use independent process groups for easy kill (macOS/Linux) + """ + p = subprocess.Popen( + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + start_new_session=True, # critical: new session = new process group + ) + self._procs.append(p) + return p + + def stop_all(self): + """ + One-click stop: set stop flag + terminate all child process groups + """ + self._stop_event.set() + + # try graceful termination first + for p in list(self._procs): + try: + if p.poll() is None: + os.killpg(p.pid, signal.SIGTERM) + except Exception: + pass + + time.sleep(0.5) # give it a little time + + # if still not exited, force kill + for p in list(self._procs): + try: + if p.poll() is None: + os.killpg(p.pid, signal.SIGKILL) + except Exception: + pass + + def reset_stop_flag(self): + self._stop_event.clear() + + def build_pos_plots(self, out_dir=None): + """ + Search for .pos and .POS files directly under outputs_root (not in archive/visual), + and generate one .html per file in outputs_root/visual. + Return a list of generated html paths (str). + """ + try: + outputs_root = self.config["outputs"]["outputs_root"] + root = Path(outputs_root).expanduser().resolve() + except Exception: + # Fallback to default + root = Path(__file__).resolve().parents[2] / "tests" / "resources" / "outputData" + root = root.resolve() + + # Set output dir for HTML plots + if out_dir is None: + out_dir = root / "visual" + else: + out_dir = Path(out_dir).expanduser().resolve() + out_dir.mkdir(parents=True, exist_ok=True) + + # Only look in the top-level of outputs_root + pos_files = list(root.glob("*.pos")) + list(root.glob("*.POS")) + + if pos_files: + Logger.terminal(f"📂 Found {len(pos_files)} .pos files in {root}:") + for f in pos_files: + Logger.terminal(f" • {f.name}") + else: + Logger.terminal(f"⚠️ No .pos files found in {root}") + + htmls = [] + for pos_path in pos_files: + try: + base_name = pos_path.stem + save_prefix = out_dir / f"plot_{base_name}" + + html_files = plot_pos_files( + input_files=[str(pos_path)], + save_prefix=str(save_prefix) + ) + htmls.extend(html_files) + except Exception as e: + Logger.terminal(f"[plot_pos] ❌ Failed for {pos_path.name}: {e}") + + # Final summary + if htmls: + Logger.terminal(f"✅ Generated {len(htmls)} plot(s) → saved in {out_dir}") + else: + Logger.terminal("⚠️ No plots were generated.") + + return htmls \ No newline at end of file diff --git a/scripts/GinanUI/app/models/rinex_extractor.py b/scripts/GinanUI/app/models/rinex_extractor.py new file mode 100644 index 000000000..b9918affa --- /dev/null +++ b/scripts/GinanUI/app/models/rinex_extractor.py @@ -0,0 +1,323 @@ +import re +from datetime import datetime + + +class RinexExtractor: + def __init__(self, rinex_path: str): + self.rinex_path = rinex_path + + def load_rinex_file(self, rinex_path: str): + self.rinex_path = rinex_path + + def extract_rinex_data(self, rinex_path: str): + """ + Opens a .RNX file and extracts the corresponding YAML config information + + Supports RINEX v2, v3, and v4 formats + + :param rinex_path: File path for .RNX file to extract from e.g. "resources/input/ALIC.rnx" + :raises FileNotFoundError if .RNX file is not found + :raises ValueError if required metadata cannot be extracted + """ + + system_mapping = { + "G": "GPS", + "E": "GAL", + "R": "GLO", + "C": "BDS", + "J": "QZS", + } + found_constellations = set() + + def format_time(year, month, day, hour, minute, second): + """ + Helper function to format the parameters into a usable time string for RNX extraction + + :param year: The year + :param month: The month + :param day: The day + :param hour: The hour + :param minute: The minute + :param second: The second + :returns: Formatting string in format: "[YEAR]-[MONTH]-[DAY]_[HOUR]:[MIN}:[SEC]" + i.e. "2000-10-27_16:57:49" + """ + return f"{year:04d}-{month:02d}-{day:02d}_{hour:02d}:{minute:02d}:{(int(second)):02d}" + + def normalize_year_v2(y: int) -> int: + """ + RINEX v2 sometimes uses 2-digit years in the header/body, sometimes 4-digit. + - If y >= 1000 (already 4-digit), return as is. + - Else map YY < 80 -> 2000+YY; YY >= 80 -> 1900+YY. + """ + if y >= 1000: + return y + return 2000 + y if y < 80 else 1900 + y + + def chunk_sat_ids(s: str): + """ + Given a string like 'G01G02G10R07R08' (no spaces), + return ['G01','G02','G10','R07','R08']. + Used in RINEX v2 body parsing. + """ + s = s.strip() + out = [] + for i in range(0, len(s), 3): + chunk = s[i:i+3] + if len(chunk) == 3 and chunk[0].isalpha(): + out.append(chunk) + return out + + rinex_version = None + previous_observation_dt = None + epoch_interval = None + in_header = True + start_epoch = None + end_epoch = None + marker_name = None + receiver_type = None + antenna_type = None + antenna_offset = None + + with open(rinex_path, "r", errors="replace") as f: + lines = f.readlines() + + i = 0 + n = len(lines) + + # ---------- Header ---------- + while i < n: + line = lines[i] + i += 1 + label = line[60:].strip() if len(line) >= 61 else "" + + if rinex_version is None and "RINEX VERSION / TYPE" in label: + try: + rinex_version = float(line[0:9].strip()) + except Exception: + pass + + if in_header: + # ----- RINEX v2 header ----- + if rinex_version and rinex_version < 3.0: + if label == "# / TYPES OF OBSERV": + pass + elif label == "TIME OF FIRST OBS": + parts = line.split() + if len(parts) >= 6: + y_raw, m, d, hh, mm = map(int, parts[:5]) + ss = float(parts[5]) + y = normalize_year_v2(y_raw) + start_epoch = format_time(y, m, d, hh, mm, ss) + elif label == "TIME OF LAST OBS": + parts = line.split() + if len(parts) >= 6: + y_raw, m, d, hh, mm = map(int, parts[:5]) + ss = float(parts[5]) + y = normalize_year_v2(y_raw) + end_epoch = format_time(y, m, d, hh, mm, ss) + elif label == "INTERVAL": + try: + epoch_interval = int(float(line[0:10].strip())) + except Exception: + pass + elif label == "MARKER NAME": + 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 == "REC # / TYPE / VERS": + receiver_type = line[20:40].strip() + elif label == "ANT # / TYPE": + antenna_type = line[20:40].strip() + second_half = line[40:60].strip() + if second_half: + antenna_type += f" {second_half}" + elif label == "ANTENNA: DELTA H/E/N": + try: + h = float(line[0:14].strip()) + e = float(line[14:28].strip()) + nnn = float(line[28:42].strip()) + antenna_offset = [e, nnn, h] + except Exception: + pass + elif label == "END OF HEADER": + in_header = False + break + # ----- RINEX v3/v4 header ----- + else: + if label == "SYS / # / OBS TYPES": + system_id = line[0] if line else "" + if system_id in system_mapping: + found_constellations.add(system_mapping[system_id]) + elif label == "TIME OF FIRST OBS": + try: + y = int(line[0:6]); m = int(line[6:12]); d = int(line[12:18]) + hh = int(line[18:24]); mm = int(line[24:30]); ss = float(line[30:43]) + start_epoch = format_time(y, m, d, hh, mm, ss) + except Exception: + pass + elif label == "TIME OF LAST OBS": + try: + y = int(line[0:6]); m = int(line[6:12]); d = int(line[12:18]) + hh = int(line[18:24]); mm = int(line[24:30]); ss = float(line[30:43]) + end_epoch = format_time(y, m, d, hh, mm, ss) + except Exception: + pass + elif label == "INTERVAL": + try: + epoch_interval = int(float(line[0:10])) + except Exception: + pass + elif label == "MARKER NAME": + marker_name = line[0:60].strip() + elif label == "REC # / TYPE / VERS": + receiver_type = line[20:40].strip() + elif label == "ANT # / TYPE": + antenna_type = line[20:40].strip() + second_half = line[40:60].strip() + if second_half: + antenna_type += f" {second_half}" + elif label == "ANTENNA: DELTA H/E/N": + try: + h = float(line[0:14].strip()) + e = float(line[14:28].strip()) + nnn = float(line[28:42].strip()) + antenna_offset = [e, nnn, h] + except Exception: + pass + elif label == "END OF HEADER": + in_header = False + break + else: + break # safety + + if rinex_version is None: + raise ValueError("Could not determine RINEX version.") + + # Detector for a v2 epoch start (now supports 2 or 4 digit years) + epoch_v2_head_re = re.compile( + r'^\s*\d{2,4}\s+\d{1,2}\s+\d{1,2}\s+\d{1,2}\s+\d{1,2}\s+[0-9.]' + ) + + if rinex_version < 3.0: + # ---------- RINEX v2 body ---------- + # YY or YYYY MM DD hh mm ss.sssssss FLAG NSAT [SATLIST...] + epoch_re = re.compile( + r'^\s*(\d{2,4})\s+(\d{1,2})\s+(\d{1,2})\s+(\d{1,2})\s+(\d{1,2})\s+([0-9.]+)\s+(\d)\s*(\d+)?(.*)$' + ) + + while i < n: + line = lines[i] + m = epoch_re.match(line.rstrip("\n")) + if not m: + i += 1 + continue + + y_raw = int(m.group(1)) + mo = int(m.group(2)); dd = int(m.group(3)) + hh = int(m.group(4)); mmn = int(m.group(5)); ssf = float(m.group(6)) + flag = int(m.group(7)) # currently unused + nsat = m.group(8) + nsat = int(nsat) if nsat is not None else 0 + rest = m.group(9) or "" + + year = normalize_year_v2(y_raw) + + # Epoch interval from first two epochs + if previous_observation_dt and epoch_interval is None: + t1 = datetime(*previous_observation_dt) + t2 = datetime(year, mo, dd, hh, mmn, int(ssf)) + epoch_interval = int((t2 - t1).total_seconds()) + + end_epoch = format_time(year, mo, dd, hh, mmn, ssf) + if start_epoch is None: + # Header didn't contain TIME OF FIRST OBS + start_epoch = end_epoch + + previous_observation_dt = (year, mo, dd, hh, mmn, int(ssf)) + + # Satellites from this line + continuation lines + sats = chunk_sat_ids(rest) + + # Continuations: until we have nsat satellites or hit next epoch + j = i + 1 + while len(sats) < nsat and j < n: + nxt = lines[j].rstrip("\n") + if epoch_v2_head_re.match(nxt): + break # next epoch encountered + sats.extend(chunk_sat_ids(nxt)) + j += 1 + + for sid in sats[:nsat]: + sys = sid[0] + if sys in system_mapping: + found_constellations.add(system_mapping[sys]) + + i = j + + else: + # ---------- RINEX v3/v4 body ---------- + while i < n: + line = lines[i] + i += 1 + if not line.startswith(">"): + continue + + parts = line[1:].split() + if len(parts) < 6: + continue + + y, mo, dd, hh, mmn = map(int, parts[:5]) + ssf = float(parts[5]) + + if previous_observation_dt and epoch_interval is None: + t1 = datetime(*previous_observation_dt) + t2 = datetime(y, mo, dd, hh, mmn, int(ssf)) + epoch_interval = int((t2 - t1).total_seconds()) + + end_epoch = format_time(y, mo, dd, hh, mmn, ssf) + if start_epoch is None: + start_epoch = end_epoch + + previous_observation_dt = (y, mo, dd, hh, mmn, int(ssf)) + + sats = [] + if len(parts) > 8: + sats.extend(parts[8:]) + + j = i + while j < n and not lines[j].startswith(">"): + extra = lines[j].strip().split() + if extra and not extra[0][0].isalpha(): + break + for token in extra: + if token and token[0] in system_mapping and len(token) >= 2: + sats.append(token) + j += 1 + + for sid in sats: + sys = sid[0] + if sys in system_mapping: + found_constellations.add(system_mapping[sys]) + + i = j + + # ---------- Safety checks ---------- + if not start_epoch: + raise ValueError("TIME OF FIRST OBS not found (header or body)") + if not end_epoch: + raise ValueError("TIME OF LAST OBS or last observation not found") + if epoch_interval is None: + raise ValueError("Epoch interval could not be determined") + + return { + "rinex_version": rinex_version, + "start_epoch": start_epoch, + "end_epoch": end_epoch, + "epoch_interval": epoch_interval, + "marker_name": marker_name, + "receiver_type": receiver_type, + "antenna_type": antenna_type, + "antenna_offset": antenna_offset, + "constellations": ", ".join(sorted(found_constellations)) if found_constellations else "Unknown", + } \ No newline at end of file diff --git a/scripts/GinanUI/app/resources/Yaml/default_config.yaml b/scripts/GinanUI/app/resources/Yaml/default_config.yaml new file mode 100644 index 000000000..15c085890 --- /dev/null +++ b/scripts/GinanUI/app/resources/Yaml/default_config.yaml @@ -0,0 +1,303 @@ +inputs: + inputs_root: #USER_SET + + atx_files: [igs20.atx] # Required + igrf_files: [tables/igrf14coeffs.txt] + erp_files: [finals.data.iau2000.txt] + planetary_ephemeris_files: [tables/DE436.1950.2050] + + troposphere: + gpt2grid_files: [tables/gpt_25.grd] + + tides: + ocean_tide_loading_blq_files: [tables/OLOAD_GO.BLQ] # Required if ocean loading is applied + atmos_tide_loading_blq_files: [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 + + snx_files: + # Use a wild card (*) to include all files matching the description in the directory + - igs_satellite_metadata.snx + - tables/sat_yaw_bias_rate.snx + - tables/qzss_yaw_modes.snx + - tables/bds_yaw_modes.snx + #- "*.SNX" + + satellite_data: + nav_files: # Not required + - "BRDC*" + + clk_files: + - "*.CLK" + + bsx_files: + - "*.BIA" + + sp3_files: + - "*.SP3" + + + gnss_observations: + gnss_observations_root: #USER_SET + rnx_inputs: + # - "*.rnx" + - #USER_SET + + +outputs: + metadata: + config_description: default_config_ #USER_SET [default_config]_ + outputs_root: #USER_SET + + gpx: + output: true + filename: __.GPX + pos: + output: true + filename: __.POS + + + + + +satellite_options: + global: + error_model: ELEVATION_DEPENDENT # {uniform,elevation_dependent} + code_sigma: 0 # Standard deviation of code measurements + phase_sigma: 0 # Standard deviation of phase measurmeents + models: + phase_bias: + enable: true + +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 + clock_codes: [AUTO, AUTO] + zero_dcb_codes: [AUTO, AUTO] + + rec_reference_system: GPS + models: + phase_bias: + enable: false + troposphere: # Tropospheric modelling accounts for delays due to refraction of light in water vapour + enable: true + models: [gpt2] # List of models to use for troposphere [standard,sbas,vmf3,gpt2,cssr] + tides: + atl: true # Enable atmospheric tide loading + enable: true # Enable modelling of tidal disaplacements + opole: true # Enable ocean pole tides + otl: true # Enable ocean tide loading + solid: true # Enable solid Earth tides + spole: true # Enable solid Earth pole tides + ionospheric_components: + use_2nd_order: true + use_3rd_order: true + gps: + rinex2: + rnx_code_conversions: + P1: L1W + P2: L2W + C1: L1C + C2: L2C + rnx_phase_conversions: + L1: [ L1W, L1C ] + L2: [ L2W, L2C ] + glo: + rinex2: + rnx_code_conversions: + P1: L1P + P2: L2P + C1: L1C + C2: L2C + rnx_phase_conversions: + L1: [ L1P, L1C ] + L2: [ L2P, L2C ] + + TEST: #change to header name of RNX + receiver_type: # #USER_SET (string) + antenna_type: # #USER_SET (string) + models: + eccentricity: + enable: true + offset: [ 0.0000, 0.0000, 0.0000 ] # [floats] #USER_SET + +processing_options: + process_modes: + preprocessor: true # Preprocessing and quality checks + spp: true # Perform SPP on receiver data + ppp: true # Perform PPP network or end user mode + ionosphere: false # Compute Ionosphere models based on GNSS measurements + slr: false # Process SLR observations + + epoch_control: + start_epoch: #RNX + end_epoch: #RNX + #max_epochs: 2880 # Future user set. + epoch_interval: #USER SET + wait_next_epoch: 3600 #USER_SET seconds (make large for post-processing) + + gnss_general: + #eop_comp + add_eop_component: true + use_primary_signals: true + sys_options: + gps: + process: false + reject_eclipse: false + code_priorities: [L1W, L1C, L1X, L2W, L2C, L2X, L2S, L2L, L5Q, L5X] + gal: + process: false + reject_eclipse: false + # clock_codes: [] + code_priorities: [L1C, L1X, L5Q, L5X, L6C, L6X, L7Q, L7X] + + bds: + process: false + reject_eclipse: false + # clock_codes: [] + code_priorities: [L2I, L7I, L6I, L5P] + + + glo: + process: false + reject_eclipse: false + # clock_codes: [] + code_priorities: [L1P, L1C, L2P, L2C] + + qzs: + process: false + reject_eclipse: false + # clock_codes: [] + code_priorities: [L1C, L1X, L2L, L2X, L5Q, L5X] + + + + preprocessor: # Configurations for the kalman filter and its sub processes + cycle_slips: # Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised + mw_process_noise: 0 # Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips + slip_threshold: 0.05 # Value used to determine when a slip has occurred + preprocess_all_data: true + + spp: + # always_reinitialise: false # Reset SPP state to zero to avoid potential for lock-in of bad states + max_lsq_iterations: 12 # Maximum number of iterations of least squares allowed for convergence + outlier_screening: + raim: + enable: true # Enable Receiver Autonomous Integrity Monitoring + max_gdop: 30 # Maximum dilution of precision before error is flagged + + ppp_filter: + outlier_screening: + chi_square: + enable: false # Enable Chi-square test + mode: innovation # Chi-square test mode {none,innovation,measurement,state} + prefit: + max_iterations: 3 # Maximum number of measurements to exclude using prefit checks before attempting to filter + omega_test: false # Enable omega-test + sigma_check: true # Enable sigma check + state_sigma_threshold: 4 # Sigma threshold for states + meas_sigma_threshold: 4 # Sigma threshold for measurements + postfit: + max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter + sigma_check: true # Enable sigma check + state_sigma_threshold: 4 # Sigma threshold for states + meas_sigma_threshold: 4 # Sigma threshold for measurements + + ionospheric_components: # Slant ionospheric components + common_ionosphere: true # Use the same ionosphere state for code and phase observations + use_gf_combo: false # Combine 'uncombined' measurements to simulate a geometry-free solution + use_if_combo: false # Combine 'uncombined' measurements to simulate an ionosphere-free solution + + chunking: + by_receiver: false # Split large filter and measurement matrices blockwise by receiver ID to improve processing speed + size: 0 + + rts: # Rauch-Tung-Striebel (RTS) backwards smoothing + 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 + # interval: 86400 + # states: [REC_POS] + + model_error_handling: + meas_deweighting: # Measurements that are outside the expected confidence bounds may be deweighted so that outliers do not contaminate the filtered solution + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all rejected measurement + state_deweighting: # Any "state" errors cause deweighting of all measurements that reference the state + deweight_factor: 1000 # Factor to downweight the variance of measurements with statistically detected errors + enable: true # Enable deweighting of all referencing measurements + error_accumulation: + enable: true + receiver_error_count_threshold: 10 + receiver_error_epochs_threshold: 4 + ambiguities: + phase_reject_limit: 2 # Reset ambiguity after 2 large fractional residuals are found (replaces phase_reject_count:) + reset_on: # Reset ambiguities when slip is detected by the following + gf: true # GF test + lli: true # LLI test + mw: true # MW test + scdia: true # SCDIA test + +estimation_parameters: + + receivers: + global: + pos: + estimated: [true] + sigma: [100] + process_noise: [] #USER_SET [int] + pos_rate: # Velocity + estimated: [false] # [bools] Estimate state in kalman filter + sigma: [0] # [floats] Apriori sigma values + process_noise: [0] # [floats] Process noise sigmas + # process_noise_dt: SECOND # (enum) Time unit for process noise - sqrt_sec, sqrt_day etc + # apriori_val: [0] # [floats] Apriori state values + # mu: [0] # [floats] Desired mean value for gauss markov states + # tau: [-1] # [floats] Correlation times for gauss markov noise, defaults to -1 -> inf (Random Walk) + + clock: + estimated: [true] + sigma: [1000] + process_noise: [100] + clock_rate: + estimated: [false] + sigma: [0.005] + process_noise: [1e-4] + ambiguities: + estimated: [true] + sigma: [1000] + process_noise: [0] + outage_limit: [300] + ion_stec: # Ionospheric slant delay + estimated: [true] # Estimate state in kalman filter + sigma: [200] # Apriori sigma values + 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: [20] + process_noise: [0] + phase_bias: + estimated: [false] + sigma: [10] + process_noise: [0] + gps: + l5q: + phase_bias: + estimated: [true] + sigma: [10] + process_noise: [0.001] + diff --git a/scripts/GinanUI/app/resources/__init__.py b/scripts/GinanUI/app/resources/__init__.py new file mode 100644 index 000000000..1b82a7c45 --- /dev/null +++ b/scripts/GinanUI/app/resources/__init__.py @@ -0,0 +1,3 @@ +import sys +from . import ginan_logo_rc as _rc +sys.modules.setdefault("ginan_logo_rc", _rc) \ No newline at end of file diff --git a/scripts/GinanUI/app/resources/ginan_logo.qrc b/scripts/GinanUI/app/resources/ginan_logo.qrc new file mode 100644 index 000000000..7b6f1e180 --- /dev/null +++ b/scripts/GinanUI/app/resources/ginan_logo.qrc @@ -0,0 +1,5 @@ + + + ginan-logo.png + + diff --git a/scripts/GinanUI/app/resources/ginan_logo_rc.py b/scripts/GinanUI/app/resources/ginan_logo_rc.py new file mode 100644 index 000000000..91f3002b3 --- /dev/null +++ b/scripts/GinanUI/app/resources/ginan_logo_rc.py @@ -0,0 +1,249 @@ +# Resource object code (Python 3) +# Created by: object code +# Created by: The Resource Compiler for Qt version 6.9.1 +# WARNING! All changes made in this file will be lost! + +from PySide6 import QtCore + +qt_resource_data = b"\ +\x00\x00\x0d\x1b\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00<\x00\x00\x00<\x08\x06\x00\x00\x00:\xfc\xd9r\ +\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09pHYs\x00\x00.#\x00\x00\ +.#\x01x\xa5?v\x00\x00\x00\x07tIME\x07\ +\xe8\x05\x02\x04\x00\x17\xd0\xf0Y\xaf\x00\x00\x0c\xa8ID\ +ATh\xde\xedZ{t\x95\xd5\x95\xff\xed}\xbe\xc7\ +\xbd77/\x92\xf0\x06\x8bB\x15\x92\x12Pq\x90.\ +\xacv\xecT\x88\x04\xa9uam\x1d\x1f\x9d\xcej\xc7\ +G\xb5\xb6\xd6\x99\xaeYV\xdbN\xed\xa2\xed\xcc\xb4\xab\ +\xd3Q;C\x95Jg:-JHP\xabuX\xea\ +R[\xb0IH\x02\xc8C@y\xe5\x01\x97<\xee\xf3\ +\xfb\xce\xd9\xf3\xc7\xbd\x97\xdc\x5c\x12y%\x81ay\xfe\ +\xb9Y9\xfb\xdb\xdf\xf9}g\x9f\xdf\xde\xfb\xf7}\xc0\ +\x87\xe3\xdc\x1cw\xde\xfd\x0d\x1a\x0e?t\xae\x03\x0dV\ +]?M\x8b\xc03\xb2\xc6a\xfa2\x80\x82d[\xc3\ ++\xa7\xeb\x8f\xcfu\xc06\xd3\xb5\x00v\x05\x15w\xfb\ +\x82z\x01V\x9c\x89\xbfs\x1ap\xa8\xaafJ\xd47\ +\xd7\xbbL+c\xad\xf5W\x17\xd9|]\xcaH\x11f\ +.\xbe\xe5\xbc\x04\x1c\xf3M\xdcanM\x1a\x99\xf8\x17\ +\x9f\xbe\x83\x12Z&:L\x1d\x10y\xef\xbc=\xc3\x98\ +\xb9\xf8kA\xc5\xf7\x12P\xec\x8btxF\x94l]\ +?=;\xfd\x91+\x96\x95\x00D{\xfe\xb4&r^\ +\x00\xb6+k\x0a\x82\x0a\xd2\xe3\xc9\xcf!\xf2\xfd\x8a`\ + \xd2\xd9\xf8l{YumA\xaf\xaf\x97\x84-k\ +A\x5c\xebRE\xb4\xae4`\xfd\xfe\xfd\x8d\xcf\x1e\xfd\ +\xff\xbd\xc3C\x8c\x89\x97-\x9d\x12I\xeaf\x87\xa9\xc9\ +3b|\x91\xf9\x04T%\xdb\x1a\xf6|\xd0u\xd6H\ +.\xaa\xea\xaa\xe5\xc1\xd6W\xff;~\xb2\xf6c\xe7.\ +-Ij3+e\xe4\xb6\x90\xb2\x1e 2^WS\ +]r0\xdb\xc3\x09\xff_\x00\x1c\xee\xde\x5c\xffI\x00\ +\xe0Y\x8b\xb7+\xa2\x9f\x01\xa8\x19\x11\xd2\x9a\xf7\xa9\xdb\ +\x86|X\xe5sn\x98\x1a\xa8\xac\xa9\xdb\xda\xd5\xd7:\ +u\xde\x8d\xa1\x93\xf1W}\xcd\xe7\x0a\x8e$\xfdmI\ +#\xff\x994\xe6\xe6\x1e\xdfk\x8e\xfb\xe6\xf6\xa1\xec]\ +\xc5O\x1b\x81\x15\xac\xaa\x99\x81\x99\x8b\xa7\xd9\xc4\xc1\x00\ +\xf3/\x86\x9d\xa5/\x9a\xbf\xbc\xc0\xae\xacyu\xe3\xbe\ +N/Tu\xfd\xaa\xdc\xb9+\xaf\xbb\x8dK\xabk\xbf\ +\x1eIy[\x13F\x96h\x91\x0b;b\x89GN\xc6\ +\xef\xe6C\xdd\x0b\xb5\xc8\xc1B\xcb\xba;\xa4x\x82\x11\ +D|\xc1\x98\x8a9\xb5E\x83\xd9\xfb\x06\xdb5\xc4\x8d\ +ky\x9e\x09\x1bR\xc6\x88\x11yw\xd8\x01\xef\xef\x8b\ +.\xf5\x8c,\x84\x001m\xbe\x10\xaaZ\xf2\xf9LE\ +t\xf9\x9b{;\xdf\x8e\xa4\xfc\x15Z$\x04I\xdb'\ +\x8c\xdc\x8f\x99\x8b\xcbN\xc8\x9e\x84*\x22L\x22\x92\xe6\ +\xbe\x96\xfa\x98\x00\xef&\x8d\xb9\xf2p\xca\x1f3h\xca\ +j]\xb7\xf5\xe21\xe1\xe9e\x8e\xf5pX\xf1\xf7g\ +\x96\x17_\x12m\xado\x1ev\x96.\xad^rI\x8f\ +gZ\x05\xa2$\x0d\xaa}\x8ck]s4\xa5\xdb\xb4\ +\x08e\x9d\x12\x01\x16\xd1N\x06\xfe6\xd1\xd6\xf0\xbf'\ +\xf2[>gi\xe8H\xca\xdf\x08\x88\x0b`\x0b\x80%\ +e\xae\xbd\xb4\xb3qm]\xae\xdd\xf8K\x97M\x04\xcc\ +\xc7D\xe8\x22\xdf\xc8\x8b\x87\x9b\xd7\xee:\x95\xf5\xabS\ +\x05\x9ch\xdf\xde\xe5\x8e\xfdhX@\x1f\x07\x01\x02\x84\ +m\xe62\x11XZ\xa4\x82\x00(\xa2dP\xa9\xc7\xa6\ +\x97\x85o9\xb0\xf1\xd9\x1d'Ud\x1cz\xc7+\x9f\ +\x5c\x19a\xc8\xcbDXTb;\x9f\xe9Jz;\xd1\ +\xb5\xa3;\xd7n\xcc\xe4\x99\x93\x0e\xc4Ro\xc5\xb5\xae\ +Q\xc4\x1b\x92\x1d\xefl;\x95\xf5\x9f\x16KO)*\ +~\xb43\xda\xbbs(\x16o\xec\x8aE\xbf2\x22\xed\ +\xe1\x1b\xcf?\xa5\x89\xf0V\xee\xee\x84\x14\xbfY\xe2\xa8\ +\xb9\xdd\x9b\xd7}\xeb@O\xfc\x87\xbe1\x95\xe5\xae\xb3\ +\x1c\x82\xd7\x0a,~(\xa1\xcdO\xf6t\x1d]\x1eI\ +\xf9{\x03\x8a\xb6\xda\x94\x09#9\x96o\xe10\xc1\x22\ + \xa0\x186\xd3\x803M\x00\x98\x08.\x13\x98do\ +\xdb\xfe\x9e\xab\x00\xd9\xc7\x84\x07\xab\xc7\x8d\x1bK\x84\xce\ +>\xdf\x0f\x8d\x9b\xbbt\xcc\x88\xf4\xc3\x05\x8a\xefcB\ +\x0f\x01=%\x8e\xba\xbb\xb7e\xdd\x82\x8e\xc6\xba\x96\xa2\ +\xd9K\x1eH\x19sWJ\xe4\xcbA\x9b\xb68\x8a\xf6\ +'\x8d,*v\x9cgz9j\x80Kf\xd7^q\xd4\xf3\xffxVuj\ +\xc5\xd5\xf1\xd6\xfa\xcd\xa7{\xfd)\xd5\xd2Q\xadk\x03\ +L0\x00\x5c&\xf8\x02$\x8d\x81\x91\xfe21S+\ +C\x11\x904\x02B\xfa7'\xfc\xe12\xc3\x88\xc0a\ +\x82'\x82\xa4\xce\x9caI\x13V\xd6\x07S\xff\xb5\xa9\ +c\xbff\x19\x80\xd3\x06|J\xfd\xb0\x08\xaeN\x89\xc0\ +\x17A\x5c\x0b\x8c \x16R\xfc#\x02\xbed1=\xc1\ +\x84\x14\x80v_\xe4\xe1\xb8\x96M\xbeH\xbd\x01\xbeM\ +@<\x1d\x96\xfc]#\xa8K\x19\x03O\x04\xbe\xe0?\ +\x14\xe8\xa7\xd9\xc7\xc1\x8c\xf5\x00\x1e&P$a\x04\x9e\ +\xe0\xdf\x5c\xa6\xdf\xfa\x03S\xd7U\xa3)\xc4_.\x02\ +\x88\x00\x06\x82B[-\x0aY\xfc=\x87\xc9w\x18\xf7\ +;\xcc_ePG\xaa\xad\xe1Q_\xe4m#h\xf0\ +\xda\x1a\x1ea\xa2\x7f\x06\x80\xcb\xa7L}\x0c\x22\xf5\xe9\ +`\xa0\xc8\xacq\xc5wE[\xeb\xef\x0d0\xef\x01\x00\ +\x9b\xe8\x05\xd9\xba\xfe\xd1\x02K}[ \xe0\xf1\x92\ +\x80\xfd\xbb\x5c\xbcL4oT\x00O\xbfry\xb1/\ +\xe2f\xa3W\x84\x9a\xbb\x9a\xd6\xbe\xda\xe7\x99\x1f'\x8d\ +\xacL\x19\xfcLDJ$'\x1c\xb2C\x8b\xfc\x13\x13\ +\x1d\x22J_l\x04p\x99~\xb3\xa3\xb3\xc7\xb6+\x17\ +O\x02\xe1\x97\xd9*\x0c\x00.\x99P\xf2s\x02\xed\xc8\ +m\xaa\xb2\xc33Rx\xc5_}10\xe2\x80{\x13\ +\xc9\xe9\x03\x08\xcc\xe1C\x00\x10\xd3\xa6\x0a\xc03\xbe\xc8\ +\xeb\x06\xe8\x19DH/+s\xec\xcb-\xc2?\x0cx\ +\x8db\xa9_i\x91O\xb9\xac>[\xe6Z\x03\xd2\xdc\ +\x96\x03\x91\xdaB\x8b\x1f\x1cj-\xbb::g\x8c8\ +\xe0\xf6xj\x80:\xd2\xe7\x99\xa2L\xc3p\x10\xc04\ +\x00;\xa7\x15\x87V\xe5'\x00\x02\xa4\xcf\xd7?\xf9\xcb\ +\xcai\xbfLy\xf1\x04\xd2\x84\xb7\xa7\xabi\xed\xeb1\ +\xdft\xc4\xb4i\xde\xff\xf6s\xbb\x1d\xa6\xd7\xb2[\xd9\ +\xe7\xeb\xc7\xc2\x8e\xbb~Zi\xe9\xce\xc1\xd6r$\xe9\ +\xab\x11\x07<\xa3\xb4\xe0\xe0\xb1\x0a(]\x05]z\xc1\ +\x157}\xa4\xd4Q\x7f\x0fAk\xb1m\xf5)\xa6c\ +\x0e\xad\x9c\xa6 i\xcc\xec7\xb6\xed\xbdc\xd3\xcb+\ +5\x88`1=u\xd9\xb5w*\x97\xe9&E\xb8\xa1\ +|Nmy@\xf1\xd39\xa1;\xfdp\x22q\xcf\x96\ +\xd7V\xc52\x0fu`\xd0\x94\x17\xed\x1b\xf1<|\xcd\ +\xd2/\xd1\x86\xed\xfb\x8d\xab\xd2up\x80\x09\x00\xed\x22\ +\xc27\xbb=\xbd\xc7\x22\x9a\xa8\x88\xee6\x90\x8bJ\x1c\ +ukR\xcb\x83qmv\x04\x98\xfe\xd0\xeb\x9b\x17\x14\ +\xd1A-r#\x01\xb5c\x03N[R\xeb\xa0\x16y\ +\xc2\x13\x81o\xf0\x90\xc5\xd4\xec\x19\xb9V \xbf%\xe0\ +\xcd\xa0\xe2\x9e\xb0\xad\x96\x0a\xb00\x92\xf4\x1fM\x19\xc9\ +\xa6>\x8d\xad\xeb\xad\x11\x07\x0c\x00NeM\x9b\x11\xcc\ +BZ\x00\x80\x06\x06\x14\xfe\xb9\xcd\x01\x03\xf02\xed^\ +~\xf3`1A$\xe3C\xd2\xf3y\x02\x00,\xea\xf7\ +\x91+\x22\xd8L\xcd^[\xc3\x9c\xd1JKo)\xca\ +H\xb0L\x03%\xcf\x9c~\x973\xf3\x9c\xf9;O\x7f\ +\x06g@\xa9\x8cM~?\x9c\x0d\xe3\xc1|\x18\xc1\xeb\ +\xa3\x96\x87-\xa2:O\x04Z\x04\x09-\xf0rw\x86\ +\xfa\xb3\x91g\xd2\xf3:S\xa4\xe4\x15/\x99\xa2#]\ +a\xe5\xfb\x90\x8cM*\xc7Gn\xb7\x15R\xf4\xfc\xa8\ +\x01\x9e5a\xec\x0b.sDe\x14\x09\x8b\xa8\xffL\ +\xe4\xec\xb0\xcd\x04We\xc8\x8d\x8e?5v\xe6\xff.\ +\x13\xec<\x1f\x94\xa3\x8a\xb8\x8a\xc0\xe8\xf7\xc1\x84\xce\xf2\ +\x90\xf3\xe2\x99\x00>%z?\xf0n\xa3\xe6\x8a\x19\xc5\ +\xbe\x91\x85>\x04&\xb7(\xa0~\xdc&snM\xa6\ +\xc8\x18L\x9d5\x02\xf8\x19e#-\x0a\xd2\x0e\x22\xda\ +\xcaDeZ\xe0\x18\xc8q>\x14\xd1\x0f\x8e4\xd5m\ +\x18\xcd\xd2\x12\xa5\x8e\xbbB\x80\xa3C\x81\xc9\x9c3d\ +IU\x86\x98\xcf\x82\xce\xbc\x92\xb9\xb3\xcc\xb5>aD\ +\xee\x9bZTp\x01\x80\xd7\xf2}\x10\xd0Y\xe2X\xff\ +z\xa6\xdd\xd6)\x03no\x5c\x13\x09[\xfc\xcd\x02\xc5\ +p\xf9\xf8\x90f\x02\x02\x8aP\x90\x11\xe8\xec<\xd6\xa2\ +L\xa7es\xda&\xac\xd4\xaf\x89\xd0\xd4\x9e\xf0v\x0a\ +\xb0i_ot\xe5\x05\x85\xc1;\x82\x8aQ\xa0\x18\x16\ +\xa5m-\xa6ot5\xad\xed\x19u\xc0\x00\xd0\xd7R\ +\xff\x84/X7\x18i\x99,\xe1\x98\xb4V\xe5\xe7\x85\ +\x81 MZZ$\xe6\x89\xbc\x1e\xd3f\xb5\xc3t;\ +\x80\x00\x00\xe3\x19\xa9)\x0e\xb9\x9d\x0c\xfcF\x03\xdb\xb5\ +\x08\x5c\xe6g\xbd\xb6\x86\xa7FE\x88\x1fjT\xcc]\ +\x1a\xeeI\xf9o%\x8dT\x0e\xe5T>\xe0\xa6%\x8e\ +\xf5\xb9>_7{F\x86|a>>h\xef\xec\xf5\ +\xf4\x0a\x06\xee\xefm\xa9\x8f\x9eU\xc0\x00P>\xa7v\ +B$\xa5_\xd2\x92\x07ZN\xca\xfb<\x00\x8f\x03\xb8\ +t\xa8\xf7NS\x0a\xdd[\xc6\x15\x07\xffk\xd3\xcb\xab\ +e\xb8\x14\x933\xfaN\xab\xab\xa9\xee\xe0\xf8\xa0\xbd\xc0\ +az\xe9\xb8\xc7H\xc3\xb2\x152\x9c`\x81a\xfa\x8a\ +\xe7\x9a\xda/\xd2\xc6\xdd\x1d_Oh\xf3\x88/\x12\xa4\ +A\xd2P\xfeM\x1d\xe6\xe5\x01\xa5\xb6*\xa2!\x15\xce\ +\xa0\xcd-\xfb7\xad\xe9>\xe7\x00g\xc7\xd4y7N\ +>\x10M|\xcb\xce\x90\x90\x97!\xac\xc1\x84x\x02\x92\ +aK\xfd\xb94\x14\xb8\xe1\xbd\x9e\xe8+\xa9\x0c\x17\xa4\ +\x1f\x06\xdd\x9e\x1c&\x92\x1a\xd6\x90\xce\x1f\xefm\xfc\xdd\ +>\x7fK\xc3W\x00L\x0e\xdb\xea.&\xbc`3E\ +\xf3\xdf-e*'\xd73\xa62\xe5{\xbdL46\ +7\x14\x92Z\x9c\x91R=G\xe5\xc3\xb4qsk\xa7\ +\xc7}\xa9\xf0D\xc6\x19\x11q\x98\xdb\x83\x16uv4\ +\xd6\xed\xca\xa8\x22\x03\xa2\xbe\xc0\xe2\x87\xa2-\xf5?\x18\ +\x89\xb5X\xa3\x01\xb8\xbd\xb1n'\x80c\xeaE\x12@\ +\xef\x07\xd8\xa7\x8c\x8c\x1f\xa9\xb5\x9c\xf5\xafi/\x9c\xff\ +\xd9\xb1\xf9\xff\xf3\x8c\x14\x9c\xb7\x80\xa3I\xbf\x9c\x8e\x17\ +\x10\xf8\xbc\x05|$\xe5\x85\xf2S\x98\xcbt\xe1y\x0b\ +\xb8\xd4\xb18_\xf1Hh\x09\x9f\xb7\x80)-\xf1\xe6\ +\xe7\x0c\xeb\xbc\x05\xdc\x95\xf4\x0b%/\xa4\x89P4R\ +\xf7\xb3\xce6`\x8b\xe8\x1dm\xe4\xf1\xbc]H\xe2\xc3\ +\xf1\xe18\xad\xf1\x7f\x05C\xfd\x0d\x0b\x86\x1c\x9c\x00\x00\ +\x00\x00IEND\xaeB`\x82\ +" + +qt_resource_name = b"\ +\x00\x03\ +\x00\x00p7\ +\x00i\ +\x00m\x00g\ +\x00\x0e\ +\x05r\x14\xa7\ +\x00g\ +\x00i\x00n\x00a\x00n\x00-\x00l\x00o\x00g\x00o\x00.\x00p\x00n\x00g\ +" + +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\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x99\xa2O\xd1\xe2\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/scripts/GinanUI/app/utils/__init__.py b/scripts/GinanUI/app/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/GinanUI/app/utils/cddis_credentials.py b/scripts/GinanUI/app/utils/cddis_credentials.py new file mode 100644 index 000000000..056050472 --- /dev/null +++ b/scripts/GinanUI/app/utils/cddis_credentials.py @@ -0,0 +1,119 @@ +# app/utils/cddis_credentials.py +from __future__ import annotations +import os, platform, stat, shutil +from pathlib import Path +import netrc + +URS = "urs.earthdata.nasa.gov" +CDDIS = "cddis.nasa.gov" + +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()))) + +def netrc_candidates() -> tuple[Path, ...]: + """ + Return possible credential file paths on this OS. + + Returns: + tuple[Path, ...]: Candidate paths to search/write `.netrc`-style credentials. On Windows: (%USERPROFILE%\\.netrc, %USERPROFILE%\\_netrc); on macOS/Linux: (~/.netrc,). + + Example: + >>> tuple(map(lambda p: p.name, netrc_candidates())) # doctest: +ELLIPSIS + ('...netrc',) or ('...netrc', '_netrc') + """ + if platform.system().lower().startswith("win"): + return (_win_user_home() / ".netrc", _win_user_home() / "_netrc") + 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. + + Arguments: + username (str): Earthdata (URS/CDDIS) account username. + password (str): Earthdata (URS/CDDIS) account password. + + Returns: + tuple[Path, ...]: The list of credential files written. Also sets environment variable NETRC to the preferred file. + + Example: + >>> paths = save_earthdata_credentials("user", "pass") # doctest: +SKIP + >>> len(paths) >= 1 + True + """ + content = ( + f"machine {URS} login {username} password {password}\n" + f"machine {CDDIS} login {username} password {password}\n" + ) + written: list[Path] = [] + for p in netrc_candidates(): + _write_text_secure(p, content) + written.append(p) + 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. + + Arguments: + required (tuple[str, ...]): Hostnames that must have valid entries. + + Returns: + tuple[bool, str]: If valid, (True, path-to-netrc). If invalid, (False, reason). + + Example: + >>> ok, info = validate_netrc() # doctest: +SKIP + >>> ok in (True, False) + True + """ + _ensure_windows_mirror() + candidates = netrc_candidates() + p = next((c for c in candidates if c.exists()), candidates[0]) + if not p.exists(): + return False, f"not found: {p}" + try: + n = netrc.netrc(p) + for host in required: + auth = n.authenticators(host) + if not auth or not auth[0] or not auth[2]: + return False, f"missing credentials for {host} in {p}" + 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 diff --git a/scripts/GinanUI/app/utils/cddis_email.py b/scripts/GinanUI/app/utils/cddis_email.py new file mode 100644 index 000000000..3408e8ac4 --- /dev/null +++ b/scripts/GinanUI/app/utils/cddis_email.py @@ -0,0 +1,220 @@ +# app/utils/cddis_email.py +""" +Utilities for managing the EMAIL used by the CDDIS flow and for quick connectivity/auth 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. +""" + +from __future__ import annotations +import os +import platform +import time +from pathlib import Path +from typing import Tuple +import netrc +import requests + +ENV_FILE = Path(__file__).resolve().parent / "CDDIS.env" +EMAIL_KEY = "EMAIL" + +# ------------------------------ +# Select the .netrc/_netrc path (for compatibility with different implementations) +# ------------------------------ +def _pick_netrc() -> Path: + """ + Select a `.netrc`-style credential file path to use. + + Returns: + Path: Resolved path to the preferred credential file. + + Example: + >>> isinstance(_pick_netrc(), Path) + True + """ + try: + from app.utils.cddis_credentials import netrc_path as _netrc_path # type: ignore + except Exception: + _netrc_path = None + if _netrc_path: + try: + return _netrc_path() + except Exception: + pass + try: + from app.utils.cddis_credentials import netrc_candidates as _netrc_candidates # type: ignore + cands = _netrc_candidates() + for p in cands: + if p.exists(): + return p + return cands[0] + except Exception: + if platform.system().lower().startswith("win"): + return Path(os.environ.get("USERPROFILE", str(Path.home()))) / ".netrc" + return Path.home() / ".netrc" + +def read_email() -> str | None: + """ + Read the EMAIL used by CDDIS utilities. + + Returns: + str | None: EMAIL value if found. Lookup order: env var EMAIL → CDDIS.env → None. + + Example: + >>> os.environ.pop("EMAIL", None) + >>> _ = ENV_FILE.write_text('EMAIL="user@example.com"\\n', encoding="utf-8") + >>> read_email() + 'user@example.com' + """ + v = os.environ.get(EMAIL_KEY, "").strip() + if v: + return v + if ENV_FILE.exists(): + for line in ENV_FILE.read_text(encoding="utf-8").splitlines(): + s = line.strip() + if not s or s.startswith("#"): + continue + k, _, val = s.partition("=") + if k.strip() == EMAIL_KEY: + return val.strip().strip('"').strip("'") + return None + +def write_email(email: str) -> Path: + """ + Persist the EMAIL value to `CDDIS.env` and update the process env. + + Arguments: + email (str): Email address to store. + + Returns: + Path: Path to the written CDDIS.env file. + + Example: + >>> p = write_email("user@example.com") + >>> p.exists() + True + >>> read_email() + 'user@example.com' + """ + ENV_FILE.parent.mkdir(parents=True, exist_ok=True) + ENV_FILE.write_text(f'{EMAIL_KEY}="{email}"\n', encoding="utf-8") + os.environ[EMAIL_KEY] = email + return ENV_FILE + +def get_username_from_netrc(prefer_host: str = "urs.earthdata.nasa.gov") -> Tuple[bool, str]: + """ + Read the username from `.netrc/_netrc`, assuming username equals EMAIL. + + Arguments: + prefer_host (str): Primary host to query in netrc (fallback to cddis.nasa.gov). + + Returns: + tuple[bool, str]: (True, username) if found; otherwise (False, reason). + + Example: + >>> ok, val = get_username_from_netrc() # doctest: +SKIP + >>> ok in (True, False) + True + """ + p = _pick_netrc() + if not p.exists(): + return False, f"no netrc at {p}" + try: + n = netrc.netrc(p) + auth = n.authenticators(prefer_host) or n.authenticators("cddis.nasa.gov") + if not auth or not auth[0]: + return False, f"no authenticators for {prefer_host} or cddis.nasa.gov in {p}" + return True, auth[0] + except Exception as e: + return False, f"parse netrc failed: {e}" + +def ensure_email_from_netrc(prefer_host: str = "urs.earthdata.nasa.gov") -> Tuple[bool, str]: + """ + Ensure that EMAIL is available, deriving it from netrc if necessary. + + Arguments: + prefer_host (str): Primary host to read username from in netrc. + + Returns: + tuple[bool, str]: (True, email) if resolved; otherwise (False, reason). + + Example: + >>> ok, email = ensure_email_from_netrc() # doctest: +SKIP + >>> ok in (True, False) + True + """ + existing = read_email() + if existing: + os.environ[EMAIL_KEY] = existing + return True, existing + ok, user = get_username_from_netrc(prefer_host=prefer_host) + if not ok: + return False, user + write_email(user) + return True, user + +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 + +def test_cddis_connection(timeout: int = 15) -> tuple[bool, str]: + """ + Test CDDIS connectivity and Earthdata authentication in two phases. + + Arguments: + timeout (int): Overall timeout in seconds for the restricted request phase. + + Returns: + tuple[bool, str]: (True, 'AUTH OK, took X.XXX seconds') on success; otherwise (False, reason). + + Example: + >>> ok, msg = test_cddis_connection() # doctest: +SKIP + >>> ok in (True, False) + True + """ + print("Testing connectivity to cddis.nasa.gov...") + start_time = time.perf_counter() + r = requests.get("https://cddis.nasa.gov/robots.txt", timeout=(5, timeout)) + if r.status_code != 200: + return False, f"HTTP {r.status_code} on robots.txt" + print(f"Connectivity OK. Took {time.perf_counter() - start_time:.3f} seconds\nTesting authentication using .netrc...") + + start_time = time.perf_counter() + creds = get_netrc_auth() + if not creds: + return False, "no usable credentials in .netrc" + session = requests.Session() + session.auth = creds + url = "https://cddis.nasa.gov/archive/00readme" + resp = session.get(url, timeout=(5, timeout), allow_redirects=True) + head = resp.text[:1200] + if resp.status_code == 200 and "Earthdata Login" not in head: + return True, f"AUTH OK, took {time.perf_counter() - start_time:.3f} seconds" + return False, f"HTTP {resp.status_code} or login page returned" + +if __name__ == "__main__": + print(test_cddis_connection()) \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/common_dirs.py b/scripts/GinanUI/app/utils/common_dirs.py new file mode 100644 index 000000000..3c0c9515a --- /dev/null +++ b/scripts/GinanUI/app/utils/common_dirs.py @@ -0,0 +1,17 @@ +import sys +from pathlib import Path + +def get_base_path(): + """Get the base path for resources, handling both development and PyInstaller bundled modes.""" + if getattr(sys, 'frozen', False): + # Running in PyInstaller bundle - sys._MEIPASS is _internal/ + # and app folder is at _internal/app/ + return Path(sys._MEIPASS) / "app" + else: + # Running in development mode - __file__ is in app/utils/ + return Path(__file__).parent.parent + +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" diff --git a/scripts/gn_functions.py b/scripts/GinanUI/app/utils/gn_functions.py similarity index 99% rename from scripts/gn_functions.py rename to scripts/GinanUI/app/utils/gn_functions.py index 690a35f93..49e7595f9 100644 --- a/scripts/gn_functions.py +++ b/scripts/GinanUI/app/utils/gn_functions.py @@ -1,7 +1,6 @@ """Base time conversion functions""" from datetime import datetime as _datetime -from pathlib import Path as _Path import logging import shutil import os as _os diff --git a/scripts/GinanUI/app/utils/logger.py b/scripts/GinanUI/app/utils/logger.py new file mode 100644 index 000000000..e15afb268 --- /dev/null +++ b/scripts/GinanUI/app/utils/logger.py @@ -0,0 +1,98 @@ +""" +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. + +Usage: + # In main_window.py initialisation: + Logger.initialise(main_window_instance) + + # Anywhere in your code: + Logger.terminal("Message for terminal") + Logger.console("Message for console") + Logger.both("Message for both channels") +""" + +from PySide6.QtCore import QObject, Signal +from typing import Optional + + +class LoggerSignals(QObject): + """Signal container for thread-safe logging""" + terminal_signal = Signal(str) + console_signal = Signal(str) + + +class Logger: + """ + Static logger class for easy logging throughout the application. + + All methods are thread-safe and can be called from worker threads. + """ + _signals: Optional[LoggerSignals] = None + _main_window = None + + @classmethod + def initialise(cls, main_window): + """ + Initialise the logger with the main window instance. + + :param main_window: MainWindow instance with log_message method + """ + cls._main_window = 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.console_signal.connect( + lambda msg: main_window.log_message(msg, channel = "console") + ) + + @classmethod + def terminal(cls, message: str): + """ + Log a message to the terminal widget. + Thread-safe. + + :param message: Message to log + """ + if cls._signals is None: + print(f"[Logger not initialised - terminal] {message}") + return + + # Simply emit the signal - Qt handles thread safety automatically + cls._signals.terminal_signal.emit(message) + + @classmethod + def console(cls, message: str): + """ + Log a message to the console widget. + Thread-safe. + + :param message: Message to log + """ + if cls._signals is None: + print(f"[Logger not initialised - console] {message}") + return + + # Simply emit the signal - Qt handles thread safety automatically + cls._signals.console_signal.emit(message) + + @classmethod + def both(cls, message: str): + """ + Log a message to both terminal and console widgets. + Thread-safe. + + :param message: Message to log + """ + cls.terminal(message) + cls.console(message) + + @classmethod + def is_initialised(cls) -> bool: + """Check if the logger has been initialised""" + return cls._signals is not None \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/toast.py b/scripts/GinanUI/app/utils/toast.py new file mode 100644 index 000000000..421c1d993 --- /dev/null +++ b/scripts/GinanUI/app/utils/toast.py @@ -0,0 +1,198 @@ +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) + + # Makes it a child widget so it moves with the program + self.setWindowFlags(Qt.WindowType.Widget) + self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating) + + # Styling + self.setStyleSheet(""" + QLabel { + background-color: #2c5d7c; + color: #ffffff; + padding: 14px 40px 14px 24px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + border: 1px solid rgba(255, 255, 255, 0.2); + } + """) + self.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Font + font = QFont() + font.setPointSize(12) + font.setWeight(QFont.Weight.Medium) + self.setFont(font) + + # Close button + self.close_button = QPushButton("×", self) + self.close_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: rgba(255, 255, 255, 0.7); + border: none; + font-size: 20px; + font-weight: bold; + padding: 0px; + margin: 0px; + } + QPushButton:hover { + color: rgba(255, 255, 255, 1.0); + background-color: rgba(255, 255, 255, 0.1); + } + """) + self.close_button.setFixedSize(24, 24) + self.close_button.setCursor(Qt.CursorShape.PointingHandCursor) + self.close_button.clicked.connect(self._on_close_clicked) + self.close_button.hide() + + # Opacity effect for fade animations + self.opacity_effect = QGraphicsOpacityEffect(self) + self.setGraphicsEffect(self.opacity_effect) + self.opacity_effect.setOpacity(0.0) + + # Animation for fade in and out + self.fade_animation = QPropertyAnimation(self.opacity_effect, b"opacity") + self.fade_animation.setDuration(300) # 300ms fade + self.fade_animation.setEasingCurve(QEasingCurve.Type.InOutQuad) + + # Timer for auto-hide + self.hide_timer = QTimer(self) + self.hide_timer.setSingleShot(True) + self.hide_timer.timeout.connect(self._fade_out) + + # Track if fade_out signal is connected + self._fade_connected = False + + # Install event filter on parent to reposition on resize + if parent: + parent.installEventFilter(self) + + def eventFilter(self, obj, event): + """Reposition toast when parent window is resized or moved.""" + if obj == self.parent() and event.type() in (QEvent.Type.Resize, QEvent.Type.Move): + if self.isVisible(): + self._update_position() + return super().eventFilter(obj, event) + + def _update_position(self): + """Update the toast position to stay centered at bottom of the program.""" + if self.parent(): + parent_rect = self.parent().rect() + x = (parent_rect.width() - self.width()) // 2 + y = parent_rect.height() - self.height() - 50 # 50px from bottom + self.move(x, y) + + # Position close button at top-right corner of toast + self.close_button.move( + self.width() - self.close_button.width() - 8, # 8px from right edge + 8 # 8px from top edge + ) + + def show_message(self, message: str, duration: int = 3000): + """ + Show a toast message for a specified duration. + + Arguments: + message (str): Text to display + duration (int): How long to show the message in milliseconds (default 3000ms = 3s) + """ + # Stop any ongoing animation and timer to prevent flashing + self.fade_animation.stop() + self.hide_timer.stop() + + # Disconnect any previous finished signal connections (only if connected) + if self._fade_connected: + try: + self.fade_animation.finished.disconnect() + self._fade_connected = False + except: + pass + + # Reset width constraints so the toast can resize for new message + self.setMinimumWidth(0) + self.setMaximumWidth(16777215) # Qt's default maximum - just really big + + self.setText(message) + self.adjustSize() + + # Ensure a minimum width + if self.width() < 250: + self.setFixedWidth(250) + + # Position at bottom-center of parent window + self._update_position() + + # Show close button + self.close_button.show() + self.close_button.raise_() + + # Fade in + self.show() + self.raise_() # Bring to front + self.fade_animation.setStartValue(self.opacity_effect.opacity()) # Start from current opacity + self.fade_animation.setEndValue(1.0) + self.fade_animation.start() + + # Schedule fade out + self.hide_timer.start(duration) + + def _on_close_clicked(self): + """Handle close button click - fade out immediately.""" + self._fade_out() + + def _fade_out(self): + """Fade out the toast and hide it.""" + self.fade_animation.stop() + + # Hide close button + self.close_button.hide() + + # Disconnect previous connections to avoid multiple hide() calls + if self._fade_connected: + try: + self.fade_animation.finished.disconnect() + self._fade_connected = False + except: + pass + + self.fade_animation.setStartValue(self.opacity_effect.opacity()) + self.fade_animation.setEndValue(0.0) + self.fade_animation.finished.connect(self.hide) + self._fade_connected = True + self.fade_animation.start() + + +def show_toast(parent, message: str, duration: int = 3000): + """ + User feedback function to show a small toast notification at the bottom of the program. + + Arguments: + parent: Parent widget (typically "main_window.py) + message (str): Message to display + duration (int): Display duration in milliseconds (default 3000ms) + + Returns: + Toast: The toast instance (not required, but this does allow handling of the toast instance) + + Example: + show_toast(self.main_window, "Scanning CDDIS archive...", 5000) + """ + # Reuse existing toast if available + if not hasattr(parent, '_toast_widget'): + parent._toast_widget = Toast(parent) + + parent._toast_widget.show_message(message, duration) + return parent._toast_widget \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/ui_compilation.py b/scripts/GinanUI/app/utils/ui_compilation.py new file mode 100644 index 000000000..31160d264 --- /dev/null +++ b/scripts/GinanUI/app/utils/ui_compilation.py @@ -0,0 +1,48 @@ +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" + + # Ensure compiler exists + if shutil.which("pyside6-uic"): + with open(output_file, 'w') as f: + f.write("# This file is auto-generated. Do not edit.\n") + result = subprocess.run(["pyside6-uic", ui_file, "-o", output_file], capture_output=True) + if result.returncode != 0: + print(f"Error compiling UI: {result.stderr.decode()}") + else: + print("UI compiled successfully.") + print(result.stdout.decode()) + else: + raise ImportError("Ensure pyside6-uic is installed and available on PATH.") + + # Manually fix the file path to the logo resource + with open(output_file, 'r') as f: + lines = f.readlines() + for i, line in enumerate(lines): + if line == "import ginan_logo_rc\n": + lines[i] = "from scripts.GinanUI.app.resources import ginan_logo_rc" + break + with open(output_file, 'w') as f: + f.writelines(lines) + +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 new file mode 100644 index 000000000..22bcbfbd5 --- /dev/null +++ b/scripts/GinanUI/app/utils/workers.py @@ -0,0 +1,128 @@ +# app/utils/workers.py +import traceback +from datetime import datetime +from pathlib import Path +from typing import Optional + +import pandas as pd +from PySide6.QtCore import QObject, Signal, Slot + +from scripts.GinanUI.app.models.dl_products import get_product_dataframe, download_products, get_brdc_urls, METADATA, download_metadata +from scripts.GinanUI.app.utils.common_dirs import INPUT_PRODUCTS_PATH + +from scripts.GinanUI.app.utils.logger import Logger + +class PeaExecutionWorker(QObject): + """ + Executes execute_config() method of a given PEAExecution instance. + The 'execution' object is expected to implement: + - execute_config() + - stop_all() (optional but recommended: terminate underlying process) + """ + finished = Signal(object) + + def __init__(self, execution): + super().__init__() + self.execution = execution + + @Slot() + def stop(self): + try: + Logger.terminal("🛑 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") + except Exception: + tb = traceback.format_exc() + Logger.terminal(f"⚠️ Exception during stop:\n{tb}") + + @Slot() + def run(self): + try: + self.execution.execute_config() + self.finished.emit("✅ Execution finished successfully.") + except Exception: + tb = traceback.format_exc() + Logger.terminal(f"⚠️ Error launching Execution! Exception:\n{tb}") + + +class DownloadWorker(QObject): + """ + Downloads PPP and BRDC products for a specified date range or retrieves valid analysis centers. + + :param products: DataFrame of products to download. (See get_product_dataframe()) + :param download_dir: Directory to save downloaded products. + :param start_epoch: Start datetime for BRDC files. + :param end_epoch: End datetime for BRDC files. + :param analysis_centers: Set to true to retrieve valid analysis centers, ensure start and end date specified + """ + finished = Signal(object) + progress = Signal(str, int) + atx_downloaded = Signal(str) + + def __init__(self, start_epoch: Optional[datetime]=None, end_epoch: Optional[datetime]=None, + download_dir: Path=INPUT_PRODUCTS_PATH, products: pd.DataFrame=pd.DataFrame(), analysis_centers=False): + super().__init__() + self.products = products + self.download_dir = download_dir + self.start_epoch = start_epoch + self.end_epoch = end_epoch + self.analysis_centers = analysis_centers + self._stop = False + + @Slot() + def stop(self): + self._stop = True + + @Slot() + 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") + return + Logger.terminal(f"📦 Retrieving valid products") + try: + valid_products = get_product_dataframe(self.start_epoch, self.end_epoch) + self.finished.emit(valid_products) + except Exception as e: + tb = traceback.format_exc() + Logger.terminal(f"⚠️ Error whilst retrieving valid products:\n{tb}") + Logger.terminal(f"⚠️ {e}") + return + + # 2. Install metadata + elif self.products.empty: + try: + 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}") + return + + self.finished.emit("📦 Downloaded metadata successfully.") + + + # 3. Install products + else: + try: + def check_stop(): + return self._stop + # Disregard generator output + for _ in download_products(self.products, download_dir=self.download_dir, + dl_urls=get_brdc_urls(self.start_epoch, self.end_epoch), + progress_callback=self.progress.emit, stop_requested=check_stop): + pass + except RuntimeError as e: + Logger.terminal(f"⚠️ {e}") + return + except Exception as e: + tb = traceback.format_exc() + Logger.terminal(f"⚠️ Error whilst downloading products:\n{tb}") + Logger.terminal(f"⚠️ {e}") + return + + self.finished.emit("📦 Downloaded all products successfully.") \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/yaml.py b/scripts/GinanUI/app/utils/yaml.py new file mode 100644 index 000000000..52b94bf19 --- /dev/null +++ b/scripts/GinanUI/app/utils/yaml.py @@ -0,0 +1,144 @@ +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. + +Key functions: +- load_yaml(file_path): Load YAML into memory, converting path-like strings + into pathlib.Path objects where appropriate. +- write_yaml(file_path): Write YAML safely. Falls back to normalising values + if ruamel.yaml raises a RepresenterError. +- update_yaml_values(): Update values in-place, preserving comments/formatting. +- normalise_yaml_value(): Normalise a single value (Path → PlainScalarString, + 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. +""" + +# Configure YAML parser +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 + +def load_yaml(file_path: Path) -> CommentedMap: + """ + Load a YAML file and return its contents, preserving structure and comments. + Paths are left as plain strings for consistency. + """ + with file_path.open('r', encoding='utf-8') as f: + data = yaml.load(f) + if data is None: + raise ValueError(f"Failed to parse or empty YAML file: {file_path}") + return _normalise_inplace(data) # ✅ ensure values are normalised immediately + +def write_yaml(file_path: Path, config, debug: bool = False): + """ + Write a YAML config dictionary to file with clean formatting. + All Path objects are normalised to plain strings before dumping. + """ + # Proactively normalise everything + _normalise_inplace(config) + + with file_path.open('w', encoding='utf-8') as f: + yaml.dump(config, f) + + if debug: + with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".yaml") as tmp_file: + yaml.dump(config, tmp_file) + tmp_file.seek(0) + Logger.console("[DEBUG] YAML OUTPUT (from temp file):\n" + tmp_file.read()) + +def update_yaml_values(file_path: Path, updates: list[tuple[str, str]]): + """ + Update several YAML keys in-place without destroying comments or formatting. + Values are passed through normalise_yaml_value() for safety. + """ + with file_path.open('r', encoding='utf-8') as f: + data = yaml.load(f) + if data is None: + raise ValueError(f"Failed to parse YAML from {file_path}") + + for key_path, new_value in updates: + keys = key_path.split(".") + node = data + for k in keys[:-1]: + if k not in node: + raise KeyError(f"Path segment '{k}' not found in {key_path}") + node = node[k] + + final_key = keys[-1] + if final_key not in node: + raise KeyError(f"Final key '{final_key}' not found in {key_path}") + + # Normalise + node[final_key] = normalise_yaml_value(new_value) + + with file_path.open("w", encoding='utf-8') as f: + yaml.dump(data, f) + + +def normalise_yaml_value(val): + """ + Ensure values are safe for ruamel.yaml dumping: + - Path → PlainScalarString + - str (no newlines) → PlainScalarString + - list → CommentedSeq with block style + """ + if isinstance(val, Path): + return PlainScalarString(str(val)) + elif isinstance(val, str) and "\n" not in val: + return PlainScalarString(val) + elif isinstance(val, list) and not isinstance(val, CommentedSeq): + seq = CommentedSeq(val) + seq.fa.set_block_style() + return seq + return val + +def _normalise_inplace(obj): + """ + Recursively normalise values in-place using normalise_yaml_value(). + Intended for internal use as a safety net in write_yaml() and load_yaml(). + """ + if isinstance(obj, dict): + for k, v in list(obj.items()): + obj[k] = normalise_yaml_value(v) + _normalise_inplace(obj[k]) + elif isinstance(obj, list): + for i, v in enumerate(list(obj)): + obj[i] = normalise_yaml_value(v) + _normalise_inplace(obj[i]) + return obj diff --git a/scripts/GinanUI/app/views/__init__.py b/scripts/GinanUI/app/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/GinanUI/app/views/main_window.ui b/scripts/GinanUI/app/views/main_window.ui new file mode 100644 index 000000000..1c54b0ce3 --- /dev/null +++ b/scripts/GinanUI/app/views/main_window.ui @@ -0,0 +1,1171 @@ + + + MainWindow + + + + 0 + 0 + 1200 + 800 + + + + GINAN GNSS Processing + + + + + + + + + + 60 + 60 + + + + :/img/ginan-logo.png + + + true + + + + + + + + 16 + true + + + + GINAN GNSS PROCESSING GUI + + + + + + + + 0 + 0 + + + + + Segoe UI + 11 + false + false + + + + Qt::LayoutDirection::LeftToRight + + + + color: gray; + padding: 0px 8px; + font: 11pt "Segoe UI"; + text-align: right; + + + Ginan-UI v4.0.0 + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTop|Qt::AlignmentFlag::AlignTrailing + + + + + + + + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + QPushButton { + background-color: rgb(24, 24, 24); + selection-background-color: rgb(12, 17, 109); + color: rgb(255, 255, 255); + border-color: rgb(189, 189, 189); +} +QPushButton:hover { + background-color: rgb(40, 40, 40); +} +QPushButton:pressed { + background-color: rgb(12, 12, 12); +} +QPushButton:disabled { + background-color: rgb(89, 89, 89); +} + + + Observations + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + 367 + 16777215 + + + + QPushButton { + background-color: rgb(24, 24, 24); + selection-background-color: rgb(12, 17, 109); + color: rgb(255, 255, 255); + border-color: rgb(189, 189, 189); +} +QPushButton:hover { + background-color: rgb(40, 40, 40); +} +QPushButton:pressed { + background-color: rgb(12, 12, 12); +} +QPushButton:disabled { + background-color: rgb(89, 89, 89); +} + + + Output + + + + + + + + + + 0 + 0 + + + + background-color: rgb(24, 24, 24); +color: rgb(255, 255, 255); + + + QFrame::Shape::NoFrame + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + 5 + + + 5 + + + 5 + + + 5 + + + 8 + + + 10 + + + + + + 0 + 0 + + + + false + + + Receiver Type + + + + + + + + 0 + 0 + + + + false + + + background:transparent;border:none; + + + Antenna Offset + + + true + + + + + + + Receiver Type + + + + + + + Data Interval + + + + + + + Antenna Offset + + + + + + + + 0 + 0 + + + + false + + + Data interval + + + + + + + PPP Series + + + + + + + + 0 + 0 + + + + false + + + PPP Series + + + + + + + + 0 + 0 + + + + false + + + Open Yaml config in editor + + + + + + + + 0 + 0 + + + + QPushButton { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QPushButton:hover { + background-color: #214861; +} +QPushButton:pressed { + background-color: #1a3649; +} +QPushButton:disabled { + background-color: rgb(120, 120, 120); +} + + + 0.0, 0.0, 0.0 + + + + + + + + 0 + 0 + + + + false + + + Time Window + + + + + + + Time Window + + + + + + + + 0 + 0 + + + + false + + + + + + + Antenna Type + + + + + + + + 0 + 0 + + + + false + + + PPP Provider + + + + + + + + 0 + 0 + + + + QComboBox { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QComboBox:hover { + background-color: #214861; +} +QComboBox:disabled { + background-color: rgb(120, 120, 120); +} + + + + Select one + + + + + + + + PPP Provider + + + + + + + + 0 + 0 + + + + QPushButton { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QPushButton:hover { + background-color: #214861; +} +QPushButton:pressed { + background-color: #1a3649; +} +QPushButton:disabled { + background-color: rgb(120, 120, 120); +} + + + Interval (Seconds) + + + + + + + false + + + Static + + + + + + + + 0 + 0 + + + + QComboBox { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QComboBox:hover { + background-color: #214861; +} +QComboBox:disabled { + background-color: rgb(120, 120, 120); +} + + + + Import text + + + + + + + + + 0 + 0 + + + + QComboBox { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QComboBox:hover { + background-color: #214861; +} +QComboBox:disabled { + background-color: rgb(120, 120, 120); +} + + + + Import text + + + + + + + + + 0 + 0 + + + + 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 + + + + + + + Constellations + + + + + + + + 0 + 0 + + + + QComboBox { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QComboBox:hover { + background-color: #214861; +} +QComboBox:disabled { + background-color: rgb(120, 120, 120); +} + + + + Select one or more + + + + + + + + + 0 + 0 + + + + QPushButton { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QPushButton:hover { + background-color: #214861; +} +QPushButton:pressed { + background-color: #1a3649; +} +QPushButton:disabled { + background-color: rgb(120, 120, 120); +} + + + Start / End + + + + + + + false + + + Constellations + + + + + + + + 0 + 0 + + + + Mode + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + PPP Project + + + + + + + + 0 + 0 + + + + QComboBox { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QComboBox:hover { + background-color: #214861; +} +QComboBox:disabled { + background-color: rgb(120, 120, 120); +} + + + + Select one + + + + + + + + + 0 + 0 + + + + QComboBox { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QComboBox:hover { + background-color: #214861; +} +QComboBox:disabled { + background-color: rgb(120, 120, 120); +} + + + + Select one + + + + + + + + + 0 + 0 + + + + QComboBox { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 13pt "Segoe UI"; + text-align: left; +} +QComboBox:hover { + background-color: #214861; +} +QComboBox:disabled { + background-color: rgb(120, 120, 120); +} + + + + Select one + + + + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + QPushButton { + color: black; + font-weight: bold; + background-color: rgb(21, 134, 19); + border-color: rgb(189, 189, 189); +} +QPushButton:hover { + background-color: rgb(17, 107, 15); +} +QPushButton:pressed { + background-color: rgb(13, 80, 12); +} +QPushButton:disabled { + background-color: rgb(89, 89, 89); +} + + + Process + + + + + + + Qt::LayoutDirection::LeftToRight + + + +QPushButton { background-color: #d32f2f; color: white; font-weight: bold; } +QPushButton:hover { background-color: #b71c1c; } +QPushButton:pressed { background-color: #9a0007; } +QPushButton:disabled { background-color: rgb(120,120,120); } + + + + Stop + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 150 + 30 + + + + 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); +} + + + CDDIS Credentials + + + + + + + + + + 0 + 0 + + + + + 870 + 0 + + + + + 12 + false + false + false + PreferDefault + true + Medium + + + + false + + + background-color:#2c5d7c;color:white; + + + QTabWidget::TabPosition::North + + + QTabWidget::TabShape::Rounded + + + 0 + + + + 16 + 16 + + + + Qt::TextElideMode::ElideNone + + + false + + + false + + + false + + + + background-color:#2c5d7c;color:white; + + + Workflow + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + background-color:#2c5d7c;color:white; + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont'; font-size:13pt;">Workflow Terminal</span></p></body></html> + + + + + + + + background-color: rgb(24, 24, 24); +color:white; + + + Console + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + background-color: rgb(24, 24, 24); +color:white; + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont'; font-size:13pt;">PEA Console Log</span></p></body></html> + + + + + + + + + + + + 14 + true + + + + Visualisation + + + + + + + + 0 + 0 + + + + + about:blank + + + + + + + + QPushButton { + background-color: #2c5d7c; + color: white; + padding: 2px 8px; + font: 11pt "Segoe UI"; + text-align: center; +} +QPushButton:hover { + background-color: #214861; +} +QPushButton:pressed { + background-color: #1a3649; +} +QPushButton:disabled { + background-color: rgb(120, 120, 120); +} + + + Open in Browser + + + + + + + QComboBox { + background-color: #2c5d7c; + color: white; + padding: 0px 8px; + font: 11pt "Segoe UI"; + text-align: left; +} +QComboBox:hover { + background-color: #214861; +} +QComboBox:on { + background-color: #214861; + color: white; +} +QComboBox:disabled { + background-color: rgb(120, 120, 120); +} +QComboBox QAbstractItemView { + background-color: #2c5d7c; + color: white; + selection-background-color: #214861; + selection-color: white; +} + + + + + + + + + + + + + + + QWebEngineView + QWidget +
    QtWebEngineWidgets/QWebEngineView
    +
    +
    + + cddisCredentialsButton + observationsButton + outputButton + terminalTextEdit + consoleTextEdit + Mode + Constellations_2 + timeWindowButton + dataIntervalButton + Receiver_type + Antenna_type + antennaOffsetButton + PPP_provider + PPP_series + PPP_project + showConfigButton + processButton + stopAllButton + tabWidget + + + + + +
    diff --git a/scripts/GinanUI/docs/USER_GUIDE.md b/scripts/GinanUI/docs/USER_GUIDE.md new file mode 100644 index 000000000..9e5469e89 --- /dev/null +++ b/scripts/GinanUI/docs/USER_GUIDE.md @@ -0,0 +1,566 @@ +# Ginan-UI +## User Manual +### This guide is written to aid those using the Ginan-UI extension software. +### Version: Release 1.0 +### Last Updated: 12th December 2025 + +## 1. Introduction + +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 the Ginan software by simplifying the users interaction with the software away from a command-line interface. On top of this, it automatically fills the .YAML configuration based on a user-provided .RNX file, automatically downloads all static and dynamic products required for execution, and also executes Ginan and visualises its plot output visualisation in an HTML format. + +This tool is designed for both new users to the Ginan software who are not comfortable using Ginan in its command-line interface form, and experienced Ginan users who want to streamline their use process. + +## 2. System Requirements & Installation + +### 2.1 Minimum System Requirements + +- OS: Mac, Linux, Windows +- CPU: 1 core, 2 threads +- Storage: 4GB +- Memory: 2GB +- Internet connection + +### 2.2 Installation Guide + +**It is required** to have registered credentials to access the CDDIS Archives. This is necessary to automatically download the auxiliary products and data. Once registered, enter the credentials into the CDDIS Credentials pop-up that opens on first-time launch. + +If this does not open for you and the program instead opens to the main screen, check the top-right for a button that reads: "CDDIS Credentials". + +#### Installation From an Executable + +##### Windows + +1. Download the latest Windows release from [GitHub Releases](https://github.com/GeoscienceAustralia/ginan/releases) + +2. Extract the ZIP archive to your desired location + +3. Run `Ginan-UI.exe` + +**Windows Security Warning:** On first-time launch, Windows Defender SmartScreen may display a warning because the executable is not code-signed. This is expected behaviour for unsigned open-source software. To proceed: +1. Click **"More info"** +2. Click **"Run anyway"** + +Ginan-UI is safe to run - the complete source code is available in this repository for verification. + +##### MacOS + +1. Download the latest macOS release from [GitHub Releases](https://github.com/GeoscienceAustralia/ginan/releases) + +2. Extract the archive to your desired location + +3. Remove the file from macOS quarantine: + +``` +bash +xattr -dr com.apple.quarantine /path/to/ginan-ui +``` + +4. Run the startup script, which configures environment variables and launches Ginan-UI: + +``` +bash +./run.sh +``` + +##### Linux + +1. Download the latest Linux release from [GitHub Releases](https://github.com/GeoscienceAustralia/ginan/releases) + +2. Extract the archive: + +``` +bash +tar -xf ginan-ui-linux-x64.tar.gz +cd ginan-ui +``` + +3. Make the executable runnable (if needed): + +``` +bash +chmod +x ginan-ui +``` + +4. Run Ginan-UI: + +``` +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+ + +``` +Install and navigate to the root of the Ginan repository: +cd /ginan +pip install -r scripts/GinanUI/requirements.txt +python -m scripts.GinanUI.main +``` + +## 3. Getting Started (Quick Start) + +When you open Ginan-UI for the first time, you will be taken to the main dashboard interface. The workflow is straightforward and only requires a few inputs from the user before Ginan can begin processing. + +

    Dashboard of Ginan-UI

    + +![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

    + +!["CDDIS Credentials" button highlighted](./images/cddis_credentials_button.jpg) + +Then, enter your CDDIS credentials and click “Save”. + +

    Type in your CDDIS login credentials here

    + +![CDDIS Credentials screen displaying username and password fields](./images/cddis_credentials_screen.jpg) + +Next, click the “Observations” button in the top-left and select the RINEX observation data file you want to process. Afterward, click the adjacent “Output” button and choose an output location for where Ginan will store its results after processing. + +

    Select your RINEX observation file and your output location

    + +!["Observations" and "Output" buttons highlighted](./images/observations_output_buttons.jpg) + +Once your RINEX observation file is set, most of the UI fields should autofill based on extracted data from the RINEX file, however the user still needs to set the “Mode” parameter. This defines how much noise should be expected in the data (i.e. “Static” = stationary GNSS receiver, “Kinematic” = a moving car, “Dynamic” = a moving plane). Set this field now. + +More experienced users may recognise this parameter as the `estimation_parameters.receivers.global.pos.process_noise` config value. By default, “Static” = 0, “Kinematic” = 30, and “Dynamic” = 100. + +

    Select a "Mode" to set the `process_noise`

    + +!["Mode" dropdown showing the options: "Static", "Kinematic", and "Dynamic"](./images/mode_dropdown.jpg) + +Once everything is configured and all fields have been autofilled by Ginan-UI, simply click “Process” to start Ginanʼs PEA processing. Ginan-UI will begin downloading the required products from the CDDIS servers for Ginan to process, and then will execute Ginan automatically. Ginanʼs processing progress will be displayed in the accompanying "Console" tab next to the default "Workflow" tab. + +

    Click "Process" when ready!

    + +!["Process" button highlighted](./images/process_button.jpg) + +

    Ginan-UI will automatically start downloading the necessary products

    + +![Automatically began downloading dynamic products for PEA processing](./images/product_downloading.jpg) + +

    Ginan's PEA tool will then begin processing using the selected configuration

    + +![PEA processing within the "Console" tab](./images/pea_processing.jpg) + +When Ginan finishes processing, you can view the generated position plot within Ginan-UI, or alternatively open the generated HTML output to review the results by clicking “Open in Browser”. + +

    Once PEA finishes processing, Ginan-UI will plot the results

    + +![Plot visualisation within Ginan-UI](./images/plot_visualisation.jpg) + +

    Visualisation plot enlarged in the web-browser

    + +![Plot visualisation opened in web-browser](./images/plot_visualisation_web.jpg) + +And that is it! Check out Section 6 for more in-depth tooling. + +## 4. User Interface Reference + +### 4.1 Input Configuration (Left Panel) + +The left-hand panel contains all the configuration options required to set up Ginan to commence processing. + +#### "Observations" Button + +- Opens a file picker to select your RINEX v2/v3/v4 observation file (optionally can be compressed). + +- Ginan-UI will automatically extract metadata from your provided RINEX file, including the time window, available constellations, and receiver and antenna type information. This metadata is then used to autopopulate the several fields below. + +#### "Output" Button + +- Opens a file picker to select where PEA will save its processing results (`.pos`, `.log`, HTML plots). + +- Remains disabled until a valid "Observations" file has been selected. + +#### Mode + +- **Critical parameter** that must be set by the user. + +- Defines expected receiver motion and sets the process noise parameter for position estimation: + - **Static** (0): Stationary GNSS receiver (e.g. reference station) + - **Kinematic** (30): Moving ground vehicle (e.g. a car) + - **Dynamic** (100): Fast-moving vehicle (e.g. an airplane) + +- Corresponds to the `estimation_parameters.receivers.global.pos.process_noise` YAML field + +#### Constellations + +- Drop-down showing GNSS systems detected from your RINEX file. Displays which constellations are available: GPS, GAL (Galileo), GLO (GLONASS), BDS (BeiDou), QZS (QZSS) + +#### Time Window + +- Displays the detected start and end epochs. + +- Useful is you only want to process a subset of your observation period + +#### Data Interval + +- Set the interval to downsample your observation data (i.e. process every 120 seconds, instead of 30 seconds). + +#### Receiver Type / Antenna Type + +- Used internally for antenna phase centre corrections + +#### Antenna Offset + +- View / edit ENU (East-North-Up) offset values. This allows manual adjustment if your antenna has a known offset position from its reference point. + +#### PPP Provider / Project / Series + +- Three drop-downs that filter available products based on the provided time window + +- **Provider:** Analysis centre or organisation (e.g. IGS, COD, GFZ, JPL) + +- **Series:** Solution type (e.g. Ultra-rapid, Rapid, Final) + +- **Project:** Product line within the provider (e.g. IGS_MGEX, CODE_MGEX) + +- Changing the provider will filter the available series and projects. + +- These fields are populated after a valid observation file has been loaded. + +#### "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.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. + +#### "CDDIS Credentials" Button + +- Opens a dialog to enter your NASA EarthData username and password. + +- This is required for downloading product from CDDIS archives. + +- Credentials are validated against CDDIS servers before being saved. On success credentials are stored in a `.netrc` or `_netrc` file in your home directory (depending on the platform) + +#### "Workflow" Tab + +- Logs Ginan-UI workflow and automation messages as well as any warnings or errors with usage. Product download progress is displayed within progress bars. + +- Text is read-only but can be selected and copied for reporting any issues. + +#### "Console" Tab + +- Streams the complete `stdout` / `stderr` from the Ginan PEA executable as it processes, as well as the relevant log messages. + +- Text is read-only but can be selected and copied for reporting any issues. + +#### "Visualisation" Section + +- 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. + +### 4.3 Process Control + +#### "Process" Button + +- The green button in the bottom-left. Initiates Ginan's processing. + +- Will remain disabled until all required inputs are configured: Valid Observation file, Output directory, Mode parameter, and PPP products available. + +- Will disable when processing commences. + +#### "Stop" Button + +- The red button in the bottom-left. Requests a graceful termination of product downloads and PEA's execution. + +- After the stop completes, the "Process" button will re-enable again. + +## 5. Understanding the Ginan-UI Workflow + +### 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 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. + +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. + +### 5.2 Product Downloading (Static vs. Dynamic) + +Ginan-UI automatically downloads all required products for GNSS processing from NASA's CDDIS (Crustal Dynamics Data Information System) archives. These products are split into two categories: **static** and **dynamic**. + +#### 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: + +- **ATX** (Antenna exchange format) - Antenna phase centre corrections + +- **ALOAD** (Atmospheric loading) - Atmospheric pressure loading models + +- **IGRF** (International Geomagnetic Reference Field) - Geomagnetic field models + +- **OLOAD** (Ocean loading) - Ocean tide loading models + +- **OPOLE** (Ocean pole tide) - Ocean pole tide models + +- **PLANET** (Planetary ephemeris) - Solar system body positions + +- **SAT-META** (Satellite metadata) - Satellite characteristics and properties + +- **YAW** (Yaw attitude) - Satellite attitude models + +- **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. + +#### Dynamic Products (Observation-Specific) + +Dynamic products are files specific to the provided RINEX observations and change based on the observation's time window and chosen PPP provider. These are downloaded each time you click "Process" and include: + +- **CLK** (Clock products) - Precise satellite and station clock corrections + +- **SP3** (Precise ephemeris) - Precise satellite orbit positions + +- **BIA** (Bias products) - Code and phase biases for multi-GNSS processing + +- **NAV** (Navigation/broadcast) - Broadcast navigation messages (BRDC files) + +Ginan-UI will automatically determine which dynamic products you need based on: + +1. The time window provided (either manually set or extracted from your RINEX observation file) + +2. The PPP provider / series / project selected in the UI + +3. The constellations present in your data (GPS, GLONASS, Galileo, BeiDou, QZSS) + +#### Download Process + +When you click "Process", Ginan-UI will: + +1. Check your CDDIS credentials are valid + +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 + +4. 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. + +All downloaded products are stored in `scripts/GinanUI/app/resources/inputData/products/` alongside the archived products from previous processing iterations in timestamped archive folders. + +### 5.3 Product Archival + +Ginan-UI will automatically archive both products and output files to prevent conflicts between processing runs and to keep your directories clean and organised. + +#### Product Archival + +Product files are automatically archived in the follow 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. + +- **When Loading a New RINEX File:** If you select a different RINEX observation file, all dynamic products from the previous processing iteration are archived with the tag `rinex_change_[timestamp]`. This prevents incompatible products from different time windows being mixed up. + +- **When Changing PPP Selections:** If you change your PPP provider, series, or project selection the relevant dynamic products will be archived with the tag `PPP_selection_change_[timestamp]`. However, reusable files like broadcast navigation messages will be preserved. + +#### Output Archival + +When you start a new processing run, existing output files in your selected output directory are automatically moved to `output/archive/[timestamp]/` before PEA processing commences. This includes: + +- `.pos` files (position solutions) +- `.log` files (PEA execution logs) +- `.txt` and `.json` files (configuration artifacts) +- `.html` visualisation files (if a visualisation directory was used) + +This makes sure that every processing iteration produces a clean output and does not overwrite results from previous iterations. + +### 5.4 Where Files are Stored + +Ginan-UI has several important directories for its operation. All paths are relative to the Ginan installation directory unless explicitly specified by the user (observation and output directories). + +#### Product Storage + +- **Location:** `scripts/GinanUI/app/resources/inputData/products/` + +- **Contents:** All static and dynamic products downloaded from NASA's CDDIS Earthdata archives. + +- **Subdirectories:** + - `tables/` - Static metadata files (ALOAD, OLOAD, GPT2) + - `archived/` - Timestamped folders containing archived products + +#### Configuration Files + +- **Template:** `scripts/GinanUI/app/resources/Yaml/default_config.yaml` + +- **Generated Config:** `scripts/GinanUI/app/resources/ppp_generated.yaml` + +- **CDDIS Credentials:** Platform-specific (See Section 4.2) + - Windows: `%USERPROFILE%\.netrc` or `%USERPROFILE%\_netrc` + - MacOS / Linux: `~/.netrc` + +#### Output Files + +- **Location:** User-selected via the "Output" button +- **Contents:** PEA-generated `.pos` files, `.log files`, and processing artifacts +- **Visualisations:** HTML plot files generated by `plot_pos.py` +- **Subdirectories:** `archive/` - Timestamped folders containing previous run outputs + +#### Observation Data + +- **Location:** Selected by the user via the "Observations" button +- Ginan-UI will read but does not modify your RINEX files + +### 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. + +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. + +## 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. + +#### Persistence of Manual Changes + +Manual user edits are preserved across most operations as Ginan-UI will only update specific fields when necessary: + +- RINEX metadata (time windows, constellations, receiver / antenna information) + +- Product file paths for downloaded PPP products + +- Output directory paths + +All other parameters like processing strategies, filter settings, quality control thresholds, satellite-specific options will all remain untouched. + +**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. + +#### Resetting to Default + +If you experience any configuration errors and want to start fresh: + +1. Delete `scripts/GinanUI/app/resources/ppp_generated.yaml` + +2. On the next processing run, a clean configuration file will be generated from the template at `scripts/GinanUI/app/resources/Yaml/default_config.yaml` + +**For executable releases of Ginan-UI**, the config is located at `_internal/app/resources/ppp_generated.yaml` + +**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. + +## 7. Troubleshooting + +### 7.1 Common Issues + +| Issue | Cause | Fix | +|-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Ginan-UI launched to a black screen, then crashed | Ginan-UI's usage of the Qt framework can rarely cause race conditions causing a segmentation fault. | Try launching Ginan-UI again, it almost always fixes itself after first-time launch. | +| Missing library errors on Linux (e.g., `libxcb`, `libGL`, Qt libraries) | Required Qt dependencies not installed on your distribution. | Install Qt dependencies for your distribution:
    **Ubuntu / Debian:** `sudo apt install libxcb-xinerama0 libxcb-cursor0 libgl1`
    **Fedora:** `sudo dnf install qt6-qtbase qt6-qtwebengine`
    **Arch:** `sudo pacman -S qt6-base qt6-webengine` | +| Process button greyed out | One of the following:
    - Observations not selected (`.rnx`)
    - Output directory not selected
    - Analysis centers not processed yet | Ensure you have selected an observation file and output directory. After the observation file has been selected, the analysis centers will automatically begin processing. Once the "PPP Provider" field has been populated, the button will unlock. | +| Missing products or downloading duplicate products when process clicked | Output directory same as product directory or log message suggesting a file is downloaded when its just being existence checked. | Ensure output directory is separate from the product directory. If there's no product directory selection made, the product directory path is `ginan/scripts/GinanUI/app/resources/inputData/products`. | +| Program crash when clicking "Process" | "`Core dump whilst thread ''`" occurs when the user uses the "Stop" button before the first download has started and subsequently clicked the process button again before the thread has a chance to exit. | The thread cannot exit whilst raising a request for status. Wait a few seconds for the "stopped thread" message in the Console before clicking Process again. | +| Connection reset errors | CDDIS server timeouts and / or network problems. | Wait 30 seconds and then try again. If the issue persists, check your network connection. Note: CDDIS servers experienced reliability issues during the 2025 US government shutdown. | +| CDDIS authentication failed | Invalid or expired Earthdata credentials, or credentials not properly saved to `.netrc` / `_netrc` file. | Re-enter your credentials via the "CDDIS Credentials" button. Verify your account is active at [Earthdata Login](https://urs.earthdata.nasa.gov). Check that `.netrc` or `_netrc` exists in your home directory with correct permissions (0600 on Linux/macOS). | +| PEA configuration error / YAML syntax error | Manual edits to the YAML config contain syntax errors (incorrect indentation, mismatched quotes, malformed lists). | Verify your YAML formatting in the config editor. If errors persist, delete `ppp_generated.yaml` to reset to default template (see Section 6.1). | +| Plots not appearing in Visualisation panel | PEA processing failed before generating `.pos` files, or `plot_pos.py` script encountered errors. | Check the Console for PEA errors. Verify that `.pos` files exist in your output directory. If files exist but plots don't render, check for Qt WebEngine issues in the Console. | +| 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. | + +### 7.2 Log Message Interpretation + +The "Workflow" / "Console" log in the right panel displays real-time output from Ginan-UI's processing. These logs redirect what would normally appear in the terminal. + +#### What You Will See + +The logs stream messages from them: + +- **Product downloading:** URLs being fetched, file names, and download progress. + +- **Ginan PEA execution:** The complete `stdout / stderr` from the PEA executable as it processes your data. + +- **Toast notifications:** User feedback messages about the status of operations. + +#### When Things Go Wrong + +Common issues you may see in the logs: + +- **Network / Connection errors:** CDDIS server timeouts or network problems. Wait 30 seconds and retry. + +- **Missing products:** The selected PPP Provider may not have products available for your time window, or they haven't been published yet. + +- **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. + +#### Tips + +- The logs are read-only, but you can select and copy text for reporting issues. + +- It auto-scrolls to the newest output. + +- Messages will persist until you start a new processing run. + +- The raw PEA output can be verbose (very verbose), this is normal for GNSS processing tools. + +If you encounter persistent errors, please copy the relevant log outputs when reporting issues (See Section 7.3) + +### 7.3 Where To Get Help + +If you encounter issues not covered in this troubleshooting guide, or need assistance with Ginan-UI: + +#### Primary Contact + +Sam Greenwood (Ginan-UI Engineer) - samuel.greenwood@ga.gov.au + +#### Additional Resources + +GitHub Issues: Report bugs or request features at the [Ginan-UI](https://github.com/GeoscienceAustralia/ginan) repository issue tracker + +Ginan Documentation: For questions about Ginan itself (not the UI), consult the main [Ginan documentation](https://geoscienceaustralia.github.io/ginan/) + +CDDIS Support: For issues with NASA Earthdata credentials or archive access, visit the [CDDIS help page](https://www.earthdata.nasa.gov/centers/cddis-daac/contact) + +When reporting issues, please include: +- Your operating system and version +- The steps you took before encountering the problem +- 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) + +## 8. FAQ + +Here are some answers to the frequently asked questions: + +**Q:** *"Where are products downloaded to?"* + +**A:** Products are downloaded to: `ginan/scripts/GinanUI/app/resources/inputData/products`. Current static products are stored here. Dynamic products are downloaded to the same folder but are moved to an archive folder on app-startup and when the `.rnx` file changes. + +**Q:** *"Where is the `.yaml` config file stored?"* + +**A:** : The `.yaml` config file used by PEA is in `ginan/scripts/GinanUI/app/resources/ppp_generated.yaml` which can be edited with the "Show Config" button. The template file in `ginan/scripts/GinanUI/app/resources/Yaml/default_config.yaml` is copied and used when no `ppp_generated.yaml` can be found. + +**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, delete the file in `ginan/scripts/GinanUI/app/resources/ppp_generated.yaml` and then run the app again. + +**Q:** *"Where can I learn more about Ginan itself?"* + +**A:** Visit Ginan's GitHub [here](https://github.com/GeoscienceAustralia/ginan) to learn more about the tool! + +## 9. Acknowledgements +This project was designed during the Australian National University's TechLauncher program in 2025. Ginan-UI was created for Geoscience Australia by: + +- Sam Greenwood +- Ryan Foote +- Harry Baard +- Kenita Tan +- Yuliang Yang +- 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 diff --git a/scripts/GinanUI/docs/images/cddis_credentials_button.jpg b/scripts/GinanUI/docs/images/cddis_credentials_button.jpg new file mode 100644 index 0000000000000000000000000000000000000000..263fb1370ff23e72aac550d3f6700b542ec539f6 GIT binary patch literal 168480 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%0A@cvCCn3zS!%PPU1Ci*fri3rvtnI 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 WK{XIr=Xw?G(jNf%(s;m+;eP><@TV{U literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/docs/images/cddis_credentials_screen.jpg b/scripts/GinanUI/docs/images/cddis_credentials_screen.jpg new file mode 100644 index 0000000000000000000000000000000000000000..18e2893a06f78938e939c51ccd6d202de21624ec GIT binary patch literal 10471 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 CH}3KP literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/docs/images/ginan_ui_dashboard.jpg b/scripts/GinanUI/docs/images/ginan_ui_dashboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d1b7468016c898b8e74fcb9331137074b8af1df4 GIT binary patch literal 161791 zcmeFZXHZjZ*C>n)1w{n}1O#t^(2I1Xd5aVYBoH9tPxpIZ(f5Pu^npZT}uKpALdA@cD*J-c+6K>Jc-k_zsMNdz6i;j-|&b>SIx9{Gj zqhnxXxO?yZea8FrcbJ%&?lWKF_y5V{%0DTuUB7inc>gxt?aRCWAK~{G8pd1K{=EMA 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℘g)Q1LT8Bj=r2{Bk+cJyPSH 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 K+kw!7f&T)kMJCt) literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/docs/images/mode_dropdown.jpg b/scripts/GinanUI/docs/images/mode_dropdown.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32000fc24246de582df68df9444c3061b0e8f2d2 GIT binary patch literal 8017 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 HHuwJk_coEw literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/docs/images/observations_output_buttons.jpg b/scripts/GinanUI/docs/images/observations_output_buttons.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5705fa9897a60ab77e04d2ef3e3b6b1e4e8b9d3e GIT binary patch literal 161039 zcmeFYcT`)+mp_Qx8QV0;M1z|QBAJ}+HqjCwOpq`lxWOPSK;)dco!yz;e|F}~{$_sXyXQ>l=)HH|t*ZNZ_m!$Xb??>X=;aR@ zdXP3yo95cJUuXobJ~Wr(G#WJ5fB8rG$MO1AxpC{pKgyk3w{G6Ldxw_x?wz}LY487f zpY|Tzy}NfGFg&39m7bn~p7uT?6C*v-)iwP;I=S|bme+6GxoSv%@9w>;yZ^~@`8N&2 zo$J5f_;&po1I;fC*RC^MyKJH1rnyFQ?Z)+M*J%E&+`4|_=IvkZ+`UG7C3mBzp}F}F z=uEVCXm8%Red{*OwO_6(+`7%c$i#Dp`5_A%uherxBM8*gIxZm*Jvc;WWtTRwaf?S~ z4&3EaH1^KQrKsz?vi~cLSl&9%FQedDCLnL?kyBFIB&(_I?)9NjPU8jq{m1NPtq>ul zH&-gTuE6|qh3Nl*>H3Xpzg(%jbN5PYd4>K8!u7j1uG7-qzJ2}L-G3lrxWUMCh2tg@ z^Ft$Rx42B+ze)yfNpIg~;ggY7H;x~iVdFQkdE@>*A*&H$mt8tEJ9kH3<3nOulbq*{ zmQH93tB0VbZSy6S=Kl37?F`o$Xr9q*NZu%-eHGA1phG-Cc(S=z4rW$tcvsH>E%1!c zcm+<4FxclNgES39M=6kfvCJse@p|B@sYNjTb))LJ?LRqCBacI9$VHWBzum%mwa?JV zi55M(a}96lcKrJT&{}L0jl=&`{vT`n&)E1=>cBDn!*hN>)$6~&LQk)CCx&?iQlrXP zgsnwq8yNJv8x^(zvu?*053BWGHJ#%HcI5aBb5I=P`W!~u50 zu`1OF5Tp|lN~sPQNmiF0Een;f?@hTDrS@H8ItAcjK~>TmWAAN|O53h7r(HSz#8qg{ zyuYqA<4{N;tEF8}F|tswsJO8l8BIM&p~OdFIjn{v5u2vqzW$&z%6c*KK39KJ5KAkG zcftGbG_|~>iAbr@?^xt(W2ZGaU3Gc8g8d5n67i+)0_^+NIH>e1KLahOOhORqw(R(9 z9fnl(S_f&1QWAnap$v7}mC2;w>?XEUB*dNT6a4AK2c4bNqv7Abz>&(x0?GhOQBUCu zKip&a`1jiBVOj)e(P8?RWaD_RY&9;0n{&zf)00W{M-5S`^A-q@GJfgEwsUP`l~`W; zZPYZDSo^Z3NY_g3+KGo}p%H?KD-az)W_~*FBOt38|2?t3YXEJZ%<(WC#7p1gx9#!} z2`y3~+m;!>8;_pq6&nz+>$UNVCP3aTX?M9@bR&G${*D;0m3TYC!T{M}uX(Bos$&@K zL_JTCQBUmNjc+#M46yGf7LaYTbehz^`?13XL;U(;lkCilBj;FEiy<=t0%g*Om3$Z_ zki6D-Ni&ll|GLj)&uT(0GIAWX1Fi3GS=WjF$n48#sEvXVp!pAs-tJctK#!8en+&lW z!?x(}K=om4`aD)Jpr2Q#&QOzL5x-pLVzGox~#b2P1HRGVdtlA{8icNVUoV zZM5CjZtjt;l~{C1lY%LmDRZvalC*w9`K8RHWbUO%v0kX3PQEJ6qzriy z@Yx)cYTYKAjLFG1b^zi9QT!)-a<+Kon4>6>z3@U0u>sH&!1TzBus+P1`o;^P9Dy;= zD{ONgX_ou|UKw|$rOYr`70+xKurADyp-#)5$q+dAhu3;t@5AHeoVX+OuwvD=Iw?TP zva~c*gDq^E=s}^9k@mi|y#gDEtoowi@=g(r9MeU_W&-#`4t-j)kwx7MFJiSKq4x9u zlg)1p5wa_8)C6#XUS&jH_#G>_ZMGmMzi_13e#%gTdn}I{Od`@XV;DR+C2}zj#hF1t zu2$WqTjsKkM=NivOpf&?Ol(JrKw<0}3(C_~sTH-id7r=FZ~N9{BkEqj9nH zK%d9Yy{c&3v7$&_^qBc;M+11b1+`Fql_>XdMkrnd8O-5WIE5ZqE7LabXs>+|t`%;X z1j+x_^OH_1oNGKdE4-A2de>2Ymh+MITaaF&qhK$;PVyK1e58nr{xSdh_D5ljJ5)J7mFU;6R^v`llSiX0oXc_si<-&_ zRlOB|7<}Ge@ibe*md+bVdzggC>erQ-*%GB^ld3eel4&V`@m~jj8;n6apX6?MRuGTm za?e8}Jhwi@a3dyb%#d}(_Ejp*-oip5Pg#u@TS;pphPn7GT7mjjZXbHPpZb|j@$(q% zAr<{BeG#`N)kMvJ)-mJWPbdz=ipX;z$Hkb@={NB|ii27eO)9wMR@EMv6cv|TcvWDt z$D56LeBRWvyrP6NbH>aC;XlWBhD%( z^ZyoGhX?l8DnCkizxD{JD!!jE(&%u`(77;Wv>F2@M1wX(4Op`d-mcW%^PX>glLxgK zW_?xFIcuKS8n^RtRJDPvb18m5c^(-*mQnWZFtCQAqcx8JusnrQqDCty3aPMg}KjDAH$_Ei1BTgc}wAr(v?GvK0 zX7jTci4UQ14t%^lq_n3m8#PvL-2!{=cAMThJbF5R?7B5JFkfVZ-#1O1;edFwYZ*!s zrqGLX)w;ov`gKE(S)XbC`Cs*J{2&;*Fr94 z@rEw4`!8PMj3A<}f^bT0xLdLY| zN72^6W>fNIKaAR7j;c%jCbvzAouy~5Pc{R~3>ugBnp6Rc{_-woijc@Hk0o*EEu_`q zG%1*JNpr7%;gY7sv+z8|;pkk)#cQLmBwkT@>act(*y7mS&1j~%O8g_<^M!|{+uiKG z;OEu5nD5k5dn5F!k>zb>1TX)VFog5R>K}{yiFm@6$y_q%sfd1_kC}YwnoM3bL={pV zfpPS}blU&e6}$K;S|wdr*O+*22o(L$R5(HF~%y=$N3&5X`MlM4mC1X|vVMT+Km8IW%V&AS50Ew&Z z#Fv>)9LlaAm_MLKgQPRnV*1uRB8?0hAn;0gN5_oYC1nh3wF{OB;xMO(GteVN`rjzn zu221psd&0-tP)ol2#rGUf`0Yam9wkO`I@cmxp+|i)+A^k0*Gy03F=ypsc^uzB8~x{ zcRawoN0I?08TdcsK$3T8pFV+ky^6yl+w+tlIki|uRztrKVz@q|ku3;j6jD>Cv_6>C zt8JtD)JeJ^J3o;l^lEHXwducuR8CjxkFO!>oXUUV#7pAcx1!!C7H{atC9#b?%P$Vc zS<8#F@MSZZXIl4`7)Jl%C~cnAgRa;VnglXg1V)F23#)$`)=41nsfL@`rz48s51FCy zK)W-_hTy^+N|B(29A0~Fyj>dR((BXvbGU0THk9hT<&E}Fj^b+ys3x~4Pwu_Sp+bp( zLbb}sudu<_{*S^nR|=L?h<1qy)qZ_mwLMV>!bf*S?6BGP-_wtAjA~|0=&0tcUJernsL7ebt)|+Cv%i?81<^7 znIy_G=MT~j#t-bXD`MS{{h=0;ML3nCyHY;%4#ShDCehAKmE!y0KdbGew+KUX z!0_Lap`{rQb2E@l=Fe!xVGo2F{20*rv}tB0VO6tP2j!)m<&FsPq9a`oXi1WgQ9Mqv z7j3%{Fey7!-#d(`TQli4b-i_v(@`Et_x5n%>w!pRC(10`gFKyxKJLm$VAree%nbl2 zJQ{R=#0RX*=6#Wph*DKgy5Q(a0m2NUn2j85VdaS^YJ}Adh|TKD#CK|oh>G)MASqd` z*u%|Rf4b$=9Ws`YGsfv zyZ`B@oaL@pO4ieN7R*gx@km_7aJ6B1c|BS}J3mZ1ohR6-XzHhyDF1NVp=$56u9KZv zE%%m^Jd}U@6Mn$A_eU=SJ&TJ|WKa_b5Re?5ponJ(IV& zOBbI#$^gAWf@Pq(pQ4%rN#%3n(qf+?^Q4=gP#W8RU8DXN&UXq&;1fO%wW^3w zX4MJhWp;qCV!-3dnwW-MnHSr2j#1vN!*_A~>O)bUfVxEJ(iS^@`fp zQBNu=QsjiJ()?D)W(#P$fqxJjpQt1c&sDpT@QOEyd8mAdm4?Zwv+mA~LU5v2*O96aV&q8U z@;Unz!O>p}1{2fH%MRyq6Ec?SpJB3jS6y`HRQC|I3zp4(RHgdTMw zVd9d3bp)hd8{vXKk7dYYbd0!wTyC*r8`iHO`FD+*Xj)F?tIu5l2iHi zKZEI?v##=<){KIA{+ncLOk#lRR~4%S7hXcm`MCcsD#u{@ z>6PbZ)wG7}AlJ|QdXL%@esw**N# zIjgWk<)u%TG$GS;%U^t)DBG*U8E#Xoo;NzewPo{?|J1H5dM4>Ef-Mk*&v4dkOFLI1 zK_8=tdqK*fba_2bViVpcW+`{-VHRiOh=$z$9iROp`AR}E!THYqtE6%LEvuV>5QoGR z!1N8!hA;^mxznc@TX3?rU2-`>1Oa%z7cPwyEF0DadMzM;DUn7|rD{5< z3?W!!%tvDJw{+tu#8O6C?vTZ{dDDIM=B`94r7!R0%I>nsiGT9P*9j(CRsq`ODp11? zr^L=`)YtI4(i1v)BtDTH6rODMree#hpz`hW&}<`2ZKe28%^ZF-PP6*oeRcno!fW4S z7<>%wr<$~M6CNykF28aNtq3oqwzjK=zr-1La`cX*!|rFOr>O0u0OK5FmEq>yJ{e;q<&)iA?J-9^iKPM@7BpMorJ4qHzc?TfcDV+a80!Kw-e9D1=jf1z_VR z`JzuFx-xW47N8`Oi%znzo6j591+U0=`b$8IlZ6x~+Z`q@#)+GNflO5ST{B&iqvDG5 zfW9P%=);sy%gzIi{pygGO8a)p&!k6YOWWZNYUU>@?GP;i+ZYXZcM67d{TTUX^EPBo z@WGt)?AHon!aj%`+8I-EVmpY2SNxjyR$A;R#l$=&m+zVe0~aKN*&;Kx_#-Nz`bWQx zGnigh4_w1z?KSz)JKBL<01+W3t(2~|of|IAH#;~Qvxk>-cI1g?d507Qq?vIT>GJ+) zzQ6LE9F-EUzdnR43>NbCMHvN7WTha*tJmu`{@&2IFZD2Nw$-k}%(u%wW~HnzdXUf3y$kg23yGJdq=ZaJqJD-c}U zRCB6Yon6vFPF(HEKAYZCdQ<{wN<9o72cDqa?at!@eE)MNq#%1^Vfl%MVh(3~JZutnO!St{fK4d9braSuH%7UdN ztmPvl&q7w-93J1w0gA?THpfruP3RfhMxMhR?OH@7-{&OX#m}yoAhb3batH7JSRJZ~ zA9Jw`rFgv8mjCW(TK9`x16OBd5YUG7N`o4YDdXA7_k z9i+u2|3T0;ihK>RJm$jvQ26vE<729@jjKaz_~(2qW0GJ7SmBl35q^_b#^`ex_APb2 zLaI|m4R#MC%{!=p>0-uHnmr?6j!QMxgMUNtx=aJBYq%|{zqjhnGR^h;t2vqJ@#X|m zvoZwG_@(mUT7`KWXL{^G+?$Gc1?I8+R2di}XoVpetrLeb%m0!JQ5AUBN7)Q80-Y93 zO|7|z4;tdseZxhmoxU|SM`Pltuf3wxWd8Io#Hb7M&c3_nk1F&4>VnlE8Dc3>J-B;@ zawM;Pafg0sN`5Kog*ne1Y5EB3~@zHzv3arm)D^BV@qiDx1Su*`mwh+0OY#Afo9O9tCg9ltCj zmz~bLq}OT5qY<~3FDtl881hI7K8XiSa2-BXcJlV|gT|`B7(Nhp8;iYUrz1*zEaV0V zh0*@A`n$Km|i9YsM;iiQQOUpkjA8Uk4h+5O+3)wzai668P?^q{GBH zDJPimuxI&VoNI`}Tyjvg3CdVYdMq-7X{#=`9>kLw zKkDC12aWPLh|ectB`S+{eruPeM@up(X<5nIOh=V21|M(=?|769q|Svj;l#PLld4zz zl>F?r1T#Lgvo2YUDj799CLhuL_{ME=n|>#qJSz(Ah6e1Z$Ik`Ls%vdRQr!YELn1X( z51u{bdR#BRa`q4p{u*w{$O|>&);1vn-ne~hH1@8;Oa|0wy$;=+es9hxq1F6rSUMnpXr)iu z)Ez#JehvBYsP$^IUcbW>dZ10I;x-R}5m!Q5Y!b8NFhIZd@8zQIrO<+YJ~cHSuRb(K ztu=7v-L1iKXAa*{9GwRfu&=d+igY5x_Qr~VDIUwVIa{({ z$~azH6o(e-tO7xk{cy(Mp!oZ^kHSr4xO&p>2 zHoeoH<7_4i%F_OKwULbqBD!5k-Hd~NHE+4 zF4S*wr}8MfH=pY2w!aB9+L?4bb_R^#l|=GSkY)bZ+>sxL>pN;jb>)rCtCuvaL9-VA zvW1x38B8)Z)V9!)3A(OVB znLz6jrW%T6agbSN;NHC-XxuUb-**qyKHX5v*=YBvs$1{7TdvD+vjoJlS&f>T$@1Qs zu^B#A*12_55ZV2q_^uyGz&#IHWtgM&C26pWm|g4 z|MdXps2hP5(DjJK9`*JDK&kv|x(FvSP>#i(0{G%2E9FI?xYsE=1AX$k1&%Gd1ID-_ zj%v%LWSXt>bj0&b3|*=^;Uss%kw|~F;fOhTW}6He5&1=cHChrXY63*vXv#CjEutqq zfCZoDY_l~!!Csd3^BUd%)Ul`9sl(Yb`$oOM<*wjaq1VJgwPXG2xOX~JMke5!cPetN z>y!N-9+^!$fAtVa|7k0DM( z+crq70{7l)XEXvLux^O-T_~RBF#2HVr=X(3)#cs1KfKq0eyM!*pH52o`Z^GnK^agVX(h{G>po`YVx~*+Mi3fz& z_2bZP6=`K+8iWhUe-)DTahxt|T2gnN#r!q$aKM*Ed3K>2!R)ux(Wl+wZOYc$?wvx+ z^!J>Ps%Bq(`nB*clarewuc@U&HGq-6g(*W1i%h6UfC9SiXY^5wNdAn^4bkWIbR!2t zrp+hk4xagSKDP6IFug%4hm~e1Pd1E8{3m0y2%l36sEDmiDS?yCe3Ns95#sJIRbX@1%(+W;g3mSjDINOp^?o=cl1Vx6rl zb9j0_Zj~~11Rfr(6dnE9$Cj*ac*ecvZ(coIW?Dbey6OCgtTpzC{oDIcgBFC(d$v3^ zRFDyQr5}_c$JpsV(&y4Ckk=8fX|~B}nu_aYmeCRI7_wX$q8x7=eQ9cwnm;L$uJ*86 za7YQL@Qn**_hI23pAdG6$Ytxzh|F>}g~ZD9+Z5-aZghPm`bP>Y0f_dC^o0$l zt~A0AtuV;k;TjZm%pPvqVpONtXCOhK&cTZkwg%L^Ra_K&?m5}we?V`Uy5!$O=9gez zinPrFUpQ-Y4T9OZ=LXY29Y^drpN{C@5;@uNiCn3Z#f@g2r|ShZlYL< zbNRZH-o=w1d0ztl-u=;fCLY;z{gEQb6u~6Q0L~AW>>&5tS%klJ8?kibkMHIiOuGwY zXJA{ld8?7IaZ?mDP!nj2N@6(x9Z^>1K%stfsX3H6?}CODs`EJGz~)%7dy6aCM!cqR z=~mwUBQuqs^!X3(Y)kmB3dZPfgKk}^fg1os{VDN4 zF!LszLGeS;Q^!d!`6O@j@s@(mj7%Kwvs?L`ano+twALHZFAQ^bXZ!a%4BLZW8=StI zSK_k*CtSolW=na^DyXNeux+3&P@7oudquP@n4K|do=^28yWHSn$0jMZDYGAcN#nE9 zuon26zm?L$_y1sX_1`bP-Y?m-f~0U_XTo^@1`YP|2CfDa#lc_rkxSPek4^pDf%R7@?oy>9a3V)}aF;IXHK$w#bq6NjxCN`#_Z2$?5OnAOQ6l{FPi+H!kF-}V{D za%wyF+G>J$Sx1D%f`ZVx6GN9YqRh%oTdT;rgV~fz8kHoc3wqj#kG8Fqu6;LQUmz(0 z&Np-%0KgXSG_D+;!T>%wA}(3?M4A_rxZ*MlfAUiawu4Gb**ksJwICD#QZO~>3`&kR z{Z+I-PSn^N0#GBbzxd2msN*?~T;ziozeL`P%ogD9nL^k)bOQa%4t{km3O!tDZZ$`I z?2?`-ejIejRCLGZfli3d%pFHo7%2{DedQy|)vz1#V!-He3%?gw0oxJXisC%o>E~>| zjdD92jn+Bqen@D2^K;z%9X1x$o~$NJ<3;+YaGUn%j)m;|NO8TiUZk$c=zZlG`Q#Tc z&&p1*BLSWG2ZDYqh>o7Jo_umN=+!41JTiJ{4n9=9rSGRu-$~6@1SKvTXo)ROx=^_6>n+- zNVS`W`W93;Lr05-%s1=`k5J!8e;cA6;9IqP_=n6TgV6k6K7_F4Pl>u(89iB@``lZs zL7E-T(aNpii3lj@1Xd@dApz>LaqQ_16XqwsoCD$ltE}qI!-xSHWoj3@X>WD$DA543 zd=-~6H%IU;B2>}bhwyPq*n;&b_i)5up)syi?J& z?Pm!ht-Id5(Ow5waqeiGVV%M{MQ+w|L8Lp^dUfEPsRY3bZ$5{M2`dO~lsNqRf|I47 z+p52q;v~QJ0K#iBYdjU-Bix?Xo0O`q_c@21!*8xvE9b-p3@jE?!4FDI@EE=>KURWG zn2*{09B0WJLWyYC@&F#E2_qWuZ5Q|a0~n{XB?@95C>}JKa~Aj56;1er(RGN74Pra9 zK{js+#1cbfhn=5ff!Ngp_{g?hm^H8$ygwaa_Qe3Cd-)IM;Oj%vrGs1LBzCJC zs$BUbI@Z}Kt~XD)NAf;P0NrSx2_RxMU3*0Wbk~taW_ViJs}OtKLi!w&DVF0b36$cU zYA0BUL$i4QIIvt@MnfzmpoI|8#+;gzUKPU6ryGy2KmOa4MoQzRqLHQNY13eVoNlOQ z@+~>!OD?^;4J!=2Wr#g+D92|QF+QrBk8es_pwV>ciJVd^TEAVV6<$|abiUf}XU?Ui z!K91v%fk3#ytpRkmFT>b?#bu9O)pCBws@SA4{gaJ&-W zTHk&qjs3AP+33G$g9WFz;0RLq(r~gM}t_P0{% ziSO~ch5Yen>-lX@J8dh zYP(~QFV{MN6(7KJs+7v8*spIgKjLq9vvrLe+Ai6bs0<-sXIAOT%&Y3U@lW=V8zQ2G zYt#>yG=iFy-rI^*E2B|u0)I@#K?{bQeG(VIX*z$dqncC9$(y}7$qCL3DP`Tf(4w%M z&r9w`zIwJI&+wi@6DZ2;vAM(M&>lElP?td`t$|c>v>mLHQMKVTb;cv*!7bj6hM;=F zHbnO2U!+@6N!rdV)DIPoE=eD*8wu82fYp>l4OJ@g{ zrYYIMCY`J#o2y>`&;@KVPdJG6`}llpM%D{u?ubV$-2TP9EMNLjKJ+(OYK&i7I2fQ8 zqWK-3F(XXQnjBJXPFgJS+>y@0^x*93CRGrv+fp5$mnp*9gbGv6?W-iJMyCe1A@1v` z55zF(orRF&=ReBLQs!axeYB#;{y(#$OP??5rF=%=%m{utCwA%Up=0^3nc$Zd@X4d? zigY{8Rb2+f?p3P%PHk~m>3PGj%7p?HD ziFil%(ZOJj*2!s;{eitXom_XPM^iAo=ybq%fRCC0muv;bOsXwVq+Rqir|^Wd@3S!v zha%|LGTvfm{fLUiHrN-z3B>5?;7t5yO>N*(e5p*gSQIo96HlOgt*iEMfi#wjQ;Qv` z2Hx#u<`0owCL$(DWkxImN0z>7YDukTxT3fyt=#RZna)?Fbqwa#!c1MZ?iZuQj=XPs zN|NG&TXUVXo}GeO>h$Rmv4#t^6`7$}RrQvUq*7F2eP1tjYQ5ZFG6rDdVElD?Fna!I za|m85S3@0>o{?-Mv1}i)h;!3VJQ5DX#36qVH%I{S4(|x6nBO4SCv)bRE6L}4Jzz0Q z(vco!%O^^emoP@pK`J(?I?V{kNza23=T^^eP~)QoO&sG711KQNP#O`NCftL>WGx#W7JLs;Fr8( z%*>g zKP4)zBoiG!Z7#$QrJDsZF7JPB?3TENV%Hu7T5gS^>W0PVh695@1E@JaUzM&S6)C32 z$C}1Z@-cwFf2bd@U|4W~8>eqqHi^KGWk`g%RA+8dLX7ez9F2}~Guwi$I94psc~B!G z>rZ6|b%Ww{{mmg2(zHOaRqmdINCUeqjKTv?bFSmUFwD1Y9D9j*2&FhumhS%UlL*xB zpL?huL}Dr*bjT-Si$RvUn$F7F`tu=-K8xAiGMqoQ-Y+@g-oc^Kha{Fn(|dKrnn}Tv zQnA=3W^sEjKUa!My^?ofVS`@1{PY>`j`uYF)tB(UfgWp{74xqk$K)!BR3;fUQN%ot zU9z)U|67@5w0Wq4PDYdkw;95x^l-d_s+Ka<+UM~lHQ_L`5IxD=n?!Mg9x(($evHR> zkKBk-ELkvvtWLUN&_VhqNC0QqM6Qlk*7wkLpk~^C)~erCwr3DubmY&N@|3z(`bvfVBvAL(8~& zs?boutGfI3bu-tVA1aY#!@>$;EnOJeSOaeZK5Tr)gp3AIMnkvXklT!5 zO}sUZwl*?QP&rNHTuYdEB;e&awdQ=`^1*J?*zsXV8svbADBKtuD^i%AE?(k%9kO_H z4jI?<3#y4kvwYJ{4~b4N$-yd54u^RI&dk_F+J=XL(mGF3EPkF{s^Y*Oq7Z1E()5VK zTshppZr@+r4Vvoq1!m|x^xi6F<%Bq#;xvu)UU7|>VQtF3zau~YW68s8M?C!Ht1t6; zu@#-tWRx;V-=$*ODsTlJP;Ij$I288yp)rGyopASgr@$h0!nC~);u_bC^~Sza;QVPG zJ}JTED_!8B&p%qFY2}E2R`!8uc-Iu+AQ9VCf zZJdR={)0U{**~h3um7u1lj)M5Lp{Jz4304!%TG@_%FzkWTG3%MakTKQQtS7@CgTkrV~)zOM*Xd_b9G zNWxW;yWi(S#Xv1pJ((f<*cR(mYaBZ3d>-Cb(Y0Gwc>4QdE`zBy`!=?Dmv(XKDDycixt`6O-!KnYUx_IP})sv)cMq==)bi=cN>Y{!?@Nke#HL35G@% z1BUvV^T_Wx%19A$T zqg-eca`v8&rGQf%#L7&y-(!vy6?ydX9_`N*e4(Q7dL`zm=%@=MJ~^<(R-(wHRlsTL zQ|?b0+R5FMG92qK2x#mK`0LAMy?k+rNYl)hKT1i^8DbN{ZSLxiM{ulXFk@6t&z{WF zPpfTv^TSiam;-x&_eT-_3c;i-HEZs-`+xp(+U`F_b?Za^wooIeO`h*DHn*)b-l+TtiS)q-T%|o`1KY6 zl1CKIOA;qRLo4;;yk1E1gCt1RG%%-1nf8ID>nyYc$dsiCRe5@11a$)f4 z9Q1BSr&^q8wygfW+Ha@*?cOtsmo&ET?rddEckVpK8(ath{Xb{nNa%ca;)-$g0kC^kgGrs!BcS0?}*k{Mt|E4+U|5E%c z#Am=|{y3J5yQEpH!Z)S zXsxK{2YA7#ziO2%QZ$>~?ye>87-wm&0~FwRfXs^;geRt(Eu_3S(U5hdH1A3{NV^4Y zlBs8Xvl2%0=>`iK)$W;HvOKVR*Sy%iVAwvPM+^GZl0NevGj6B zri^9flt&Au4H@XHS_EWnV>aKb+c1ff2@segx_u|fx z#*D(G(dk~UP7z(viB{XYDM{+%;MZ;q12dO2`|+1F@72B^>mQlapRL5LeIV4wxn&$h z9)6ronsC|oeRs~^@FUjqDN_(dvO(F_|HtZ9j)ndC?0M$V!V%{=_v1?%pyV;%V!X9a@whKEx;s6tQ3{vA>E(md4hOGvp6T} zUhzv7_7g#t+A!i`;1T$%Aj&A z`xy2t{qO(nQ$Ls2mG@MBtLd~!9A)=+7ITbNnF$JjXI~K+-0PPge~To+j>~c#{Q)DK3qS20QDW&nk22>Jl%e=8{Ja0z@~q9{t~v z|5GsYKllH|K>wt6;C~g=e4PoPfTfG|X)L!Ja-h8MBq==xCpR4>y{xMKLI@ z!4a}(yYyXp8$ZD`4x?1t%}7Ns3~xn5bOCxc*=D#yXjExh-gJFxKOKCxEfzSv{BB|B zBKx@I{3_D+cI%XIH9cZPAKyc=u3Se<=#8h%N$F{qaJ_{Fn_fBtX(xqBLi zMW#r7C+#BvI)`0f_X8u3@xEuVlHJqMu^)ey>3_$|1*8s~!>YlR=)7P3I%hE&dx4tzsnk=2jqOgP!yLk8dtK^>5aWJGnA%7^(V1n#_PB)jUVPq_I zqK7J&T~&>Du9&5tHL|Zn;lKf9Y_IYbU#42Pl%8_Vcuv)|Sv{z%GLA=;d)E!l!4pV- z^;xk~VAgs9x>IIdgUnx#aD4x?*G6uldiKpl3b91M! zgXw*WE>+)s8WSoi7Nujr%2A|bvwo<8XghUraTB*-%F4kB<8>VB>mNx@dfat1FYoRu zrO-}?w+SeN%vQ~K9#q8lg2f#=g+aQ0$?2M6eL0BdWK5~K88l4^l7hSGr^YyIS0pMB z8Jx24oNzHjD8w<2$Ki)8-DYK;hBJ8POv7R|b=G!X4Dm~n*IptfJiOF!r3 zh40Is)+o`mMup;Wroo7J$>Ow<3@~QC3UGJW70c>q+UU>c=lwRGVRAFFOO+NCOkfOu z5rvnrSN*gz28`pFFr3#)p4X~95%jwc0!g_CFd~lye3r$%lekp7;_%&ZELa|DA{!x> z2O$4AK)3Y%@C#X@rnau3ee#xg3tZLDAbr z#}RFI6dK@@*&ZMIEq|@lp2Hs~tv%hr)aQj#V<3I!YJ~XeAQf5=nT^))%@Y8ixU%nb zfya?-RVqGXh$)1EB>ahKVyX?vQ826MNVMGN?-Xs~w}`V3uD>^$ocZfp{Q7A)W?fS76M$JOcF5eiT%lB&<&u#6jUz;fuXh-jZ& zcqJs16Gt&+<%0uGd+m|*JPP#QKV^>y}T=L z&faQb>$bT<)M&Km)puWxkfB&~~hr1zF?OsD*W zmG4)_4-dN`R-=j$ts3T4;tE%jSwdZfcE9e_YMa7DaP@lEo0kMnglj$-aWHzog8r#3 z{H^b~8o?Y0gIq)v#J}y&a8WIH9@cNv?=|OtBXnL{Wi-2|D|CN_FlIp&(Qnn7mQ7XV zpia9-=N`@bly62w<37Y~-ZU?VODe0`Bs{Ir%^qgY1?YN+M10Bqj!OKrtRB@#ht7>A zGse~KO7~6DN~Hr(SI*&X3A@tO5!fox#}qcU4KOVfw$7(~91gK4I2M=8Mb`k=f(+dh zXtSlymtGJZm^o`#B0u%`8JfOC9`~uJW@qMpUlYTQn7|VWM*25Ey|k3?*&n>5eEp?$ zsi`x}!)*&-_tN@~Qx(gc6x}y=XBLPVw}&>9*>`%0N5Xv!HI9yE6{5MA03-4IR}WmZ zyeju2^T&!+C*g9xSldSCH~1!r~{`{G!WHemU zhy&3it=67$&(FDs6xC){jBV(zZOKMcFXq^jy|ml1rC0xysd%0wPT%k|W1Fn_8u;$S z^7Z;9O>J!cc}v6A)a^@}E7Akcoan!#x&G{8{oRMy2bVO>Kypv`p#=@cX&U*u?dw-3Ad-U%kWE+UY9TGai_$`rSCch> z&v-^yGb=u08L;eoZ4S~<&4dZKt%fv_CvD9;D=9$#rvU3#>DDXL)Ln5|Wc~c$9um7& z-DXYTjq`aP3MqM%^gyy-+35?)?5>~Ql|OfQ*g&2thXo>=y5ZUh270mo2Yc@w)#TRh zi?Y_TD<~)+Ag}}i(xrD;QYG}zOhS=Pf&u9Q0&A&A2MGxs6ha6A0#Xu6C`#`&0qN2^ zNbh*^?R~y|_Zj1kea{`^?z8V1XXT%~Z|2Nw?>nFQJkNZ7zbR2Plvl1|0q$z}rrQx9 zBbCz=RSudL!Vaj;elFlF_l=i{55-|(0);v1j1OikU8OfpLcqbzy5j!fevSl5!7 znQU!QPX?xVV)P2AQ#si6GIk478^p=Vtu)m$IoLXj>81pE)Tw%@EQM*7iqu_8AW=D^g-GWxwQHp zM(Wxq_{rm@isy-&124#Y?L!$(BuURrzZl!t&FRbKwGkUZa}H04TYqxc9R2dIU%%ds zfKyH1gzoXsJW=0|8-{LoWuI$pC!CsI^r{_NGXA8SF#1X7p0M%!AL{u3a-H z3ePm=VQyO-c2(;Ah<>#A%F4CJJ0^t@pGRy35*oA6L3H&9CRx>mj5 zk9i)S0zb}bgOuH?6$03DHk49Aa-*aPyZ34?CRUd`nziEHm&P zU_V>iBspGAMP)><|J{s}Bmprl*D~q(B;nVneKLR2;5-SDXH^-?H)Mb;8_toW1i0iT zrp}8xEU|%N?~^-v>BU#cmEX~4ov$+ujf>WFA&fDbhi)vW*HE`7&(f{_3jd~KTzTiD zAV$qBkWF%+sysk)5H^V9r355vh|cO^;|qf`Cd6kprp|}QwCB=)xzwPm^lB`VFddw^ z5ZEG_7&>ODvSwU54>9lL;YjAuQ_>t-WsqMr&O0znNH@$PXg)Pbm@oEu$GZG_0;VU; zVPX;lD)%=Ev=}~R+3BhA&R^3Ip;%>*_^B1hIpGyI>rIIx?uvr)APA*}m4D4|ZeS&X zx5I-VMw*Mk6kthVjI2f??vrr4#k*Y#$_*O)V3Y#mR$fO9;*%8b&H(|fYRRjRBLHq;nNtnUaQM+1&0>QEz z+3A^Gh3`;g$+>{yKTHc{07EOVxVLfM92(;Q4sdR!r=g=u{_#GSc2L{x>$fsewjZCl z$YI6?2M>P4_1)Z1xgraHo%fll#`{1aLAM~_F}h-skabRaQ5aMBLdDh0 z%e}LsbrcdfZs|CjL@cp4)h)4?N@?;R8=9$)c271c;t^35(v7|%=}@IFnmNh~@?jKI zyM_u_N?GbK{Rl85>KtjKr_M&vBW;Fjn~Kttz}Ey|7~qJHiX2TAuBf$1+aQy6uf8yQ zK6V6lHqV!n35+qdLe&(ku$&0wIihME^*g}zqX~B%W6~t0Qy=w(IAlj0lon^qVwvxc z$~h5sWiaIW*bNIExm&2mR`Yyw0)qU5WDAlx5e&7Vul!Q#z80=kSqh)|{9Wm(>)O5Y zKPOV~h$DVtLm}|CN?NoGU)dB=W%QbrK#v`aiay5|;%Ghx+e;hpfyIo4A2aBOd{;Wq3vo#*A6zq$QrO8{%II!?@6 zdRZU2${^~Ft)MU>y=``Qln-CevB6qnuj-1ruxzxYr`m(?B7P6eJf4~XPm^9PlOtO> zh6<&jX{qm7={G?6WeX48J3qBzu${2S>rV2Sg`E&@NlN0_UHDQNk8wQ8g&ifq?7;av zt$Q*J<#S*`9!?jp@NxtglGxJ|i8`!kVH}%^_lx@2X+&MKSh)2e_}am{{R7MjLHLh>q)rV-kLc>k>y5 z`|f1;;1uWZ@8FaK(8g6`_lAyd52UG6Q|Pr5IqyP-5cOtR5Q{M_9G+&QLUvzX?%t_r`H zG=;1_+CUCJfK7^AI2B( zszaXGDrqQbXgav6wQFtd=2?()5W_7P=o0;Wr8k06GUm4_0B)-VRc}!e~PwOwzpVW1=30-C~3w|@(nndwqX3NEa#(4nC zFZWpu1X6V=85vW^=x7o$gyNWs9)ECqZ~_$Oo6&%N+?QNQq3r& z$Lh<(v*isZ%ijQ9v4%44)AJlU-jg~O3AcJ3n9OD$%xiDAql((@cfPm@kMMZrd? z3QQcGPF^%nu49g;XNn3WTK6tCVdGXOuq2b_iA$%s+!O_?wZ&_sP}r%BFN5*laSDi< zq2n?)kzDe^ZM#G0Uy9Z8W(}5@@+)YM;n|u=doekkg$;V1fBlGpn_1I$!0 zS7-#DQtad*Q1RD?2te8J@ahZGXC3C5)&mXgmYkgUbi%k_Tf3-Sn!3;HMkz9?oau z=EU^7rnwd+7F|Dz#rmL+s!5eL#~Z=Jb(!n-+qQQ`o5`8vumZD`phx-|risp|jxx$9 z8^=!WT|{D^uySnR-Bf_WqN8&_jHp7NR_as-Sd?kNe_U#ge-gcJ*ZJd}%z>$;`^IGD zdrnPG=9>Z>0Tp_pl?WoZvCX2Gd@()ZaA?P-w3Vk z>zwwRK0S>4N!Lj$CeO+5ZO!X^1RedSUn=NaHlU>izY5(uiItEMGPg#R%8F6Oz$+%A zHxZ4z{T2C@Y*?*?R5rHzT@~3#D^n{|OUoUE>QZZWW35w`(u)yq!?A$eGtdvI1W!fI z%O{#ahT~?6PLcs;S@w>-l6xaQ%N--2_wC51G5|i5fGAvL(Sh4~EU&cd(I_mJn!8m@ zfLcd&8B{`rYNgS$Tb0kqZHCuLr(2ay1#Qipmj!(yDuhAaW@AC<%TMjC&Bbd@oF6A~ z0cx;|HJhE@oO7h2; zmGp{60l5@i>Z%}X@Svbn3zo-C38v2FzT@H)e)VFq?Yn&QOU-)jRe6-WB((GBj<_E_qEYD9DFMDv{~gUpAslUjkjlK(7g!JbT&HD=x(o%O4&+C+L3&+ z$?2p7kK@-qWE(^8Mo^>dOyGys9f}#6nkIa`!NXpn3%!EcnbVJ>)3E8P<{`3{{7Y^N z9jW7ClTp3)s)A$#1J3cOHLFX~`guYHAOqx!Xr3p$7MXm|vM!PL03d39=~3QPWtr3s z>Pm|`6HHVjy)-6!#J$TNMj;enZ4yQL5g8HUicRm|c1f>3Nth2{+5CY7yF4hPGU_Pv z;W*i+yo)C)Gy7M$s|(Ew$gv4XR#m;Sd&n*J?j82VOhsFe;49uhw6OUgqJd@9pXanW z^25v!cCu1X#4;x-;+LnOp0i1ckbS+Pab}Y(AG>;tVs&8wFKmDM`MB8jxvgk1XxsHkRZfMa;foFjFy@F-oCZv!Ek}>fV*GCx2 z!;rK3J;E<*Zkn*899>eAT&TA`1BJRqM<>aK>}*~I;|OwH-Rb@uzQvpdU(|@IHU~%< zg3$EkkRFLxAO-WH=9-iVBdvQ*DWlTZOvGsL$%~!pLXc@et8X3C|h6Cd8BxZnjXc6cXrYTtR{fQmGeC^&t??Py%@2cNH-?qtV0 zX{0giNdy~wAY%VTTRhAStIs&Z4<@gtih6}c&GexUaHOlV2S!FEhZh!wd zOwqp`+3?#xUiq^8@k*A_gr+M1r6?ASOHFAbzWqIdlI@hI`E8y7jC)?{ESN36=4aI* zzlPjOzsx%LaJF8c0Mo(MPpAA#vwR}|z%%$g%UlsGKs)J!o22N?=>(t3568&^kiE}G z9l#2dg=HbkBmVB8z~nA6KqDz6wm-XrA^?HcLSuLZE2?V>4x-#gV75+Uvu;y=jGSIq z>qtJl9Xg!CJ*2Up+WVz z6k;w06%~Nt-5=-XmLoL5smW%Z^&cF5psN*gX)CJxm>~Z}*_JB$@b?peuCvs=zROXq z-`z}cTwXhwf~`V(1pvQUBpD{^R}nY+lkSKXp>*hQ_8{Q)Pdbf%(YC&aME#^YphdlH z{-pa!7b(;8D~3Kwk6uXvC8}5s%;1q+ipQqVXhn6lkATXVhRpC5ncxS=Q1Ic$%#gL8 zbehMZsEgD&&u1d^aMVK2m@!sCYYZ*DPJ#HraK)5?5A#H|x$QG}$oA-({_U0Qldi?r zBU^V@j83E&&wGqUA0*+dOSf(IqbaWQV{;^hZ z=(iKskh0_WS|hC5a<|T*jGCht0$W)-E7e_y-?$B#ceTEe7Q*DE~^foX9KE6~M-=9}JWh>Tr&M+FYH1Sz=j}K)Od5j-K0pa!!0c?RB;J?1C9y zSL1ic9oQ(5R>=PG8##$Ppy%fBRUN{&5PhFC72;yJv?`sySJRe4cHo!gee&&&xI`UN zZU18U7lzGe!AsKO&p5d%?T#e|T@Oam9`qmmGTC*zb}?-2w|^;F(SU1gS0jy^PN~}N zFr&ZjxliC^_r7+_@&L1P~=+5lDLvL_ulCz-5n=8x4I7@`qbGU zYdRoR*M-fEH=Ls)9`bf4U$Y`3&L7mK#-+bM=X>3M==YUjJxMO(@%eKfeVyfn`8I>5 z5TvSW5czqypB_i={gBU!{JeW!I}Uj>4Z7UYDwCfz$bi_TTWZfZ?=qe!1V{|MoX(9; zOSiX- z#3R*S!>P{E>J?7)lg^YkJ6Ta>j*nGHD zE1f@m)0V7V;O7-)vPVx`tj_1!4~wt9Fw9g1FLsMtaPm|lj!Osf4_J{8`tJWSA$PlW zG;Fmg*J7hVR#AqlaYrL{j!xMbY-~*9PlS1SC71WQ-Gqqz1VOni z*;w|M{xP7COP{r$(sAB9{H*rz_GrnjLsKw~y58u5`#7Ge*64C>c`}9iyP%j(?ofI5cD* zo&WO%FhAoDrG{?MHsRy{EtNYUlxCBKU)cSf_P^1%e3)YD70kNx?admnpJ?{`@57m$ z(s>D&+RH_c8Sy&j{HiS-fisw`qw;Mv;gzUl^7mLoo6~F9R-OaK(OayI6TOYAPYwG( ze&A=>nswsLqQ34qYRew9k^;V2wwlaU4bAEii=&K|^9k$eKL$dhmCM_a&^}L^%Kk@l zh8?S*Yf{OVZqq~)s7-Xsx@eqZMJg7Q#mn2?1&%3H(J&P2uJTNq69i5<2Qra*C~3$t z-X}h0)|*Eyb3wPt%3kZaD+$jT1xvA&N%+aHm&B^n#_AB)c{DVw z-`H-Q5V#?BS-XA@-<_Uk%*&RWVH%i(b7aA?--_wegNa2%lCX|IWS1X!r{T%E?Rl|I z(am}h*jEP2WtC{!Olsf%%fBU7{$c0&)zZ*|YOc>>N2Rz+3bvmP#4kp8hc8dFNPiC( z-s5WdNjEkh_=P;%nsH|#*xAtgoL9y;04Z%+B92;OjmK6Z%KtJY8>8z5b)R zcwfHM#F~D3`JvS5NnD;8UgnA!)Tsys#CBV}K2A+Yz~lx?=%E~G!JS91P5S4na8c2f z+;3GBxvcg-{by(h|6!S2EI*=Vm>X|IHr~9OUt#6jtQf5NF~B0HIg({5V@P6Da|!u= zT-};4`?@Y8)6)pr=TG*-w4iq-JMWwOlw3~wfBXC9|JP%1HQV-$<=t&@CKYP%fCR&0 zO#o-Rsz66UMdSn&)<#NKDWhU6-nk@!R}6!HrL&he%(8kYentk|Jk7~cF#=rZ*Zrhp z31F~1!=Kh)8<8H}G8K}5n@|%=xiywmbC5Zbr%!!hy4r9fr(Uh6OK;=v`8jJPIGb%} zZB&b2BaojEDlgyL7xsEJY5qudLj1h1O}sAmwnd0Yg_sm2LE^>`(Zu~Z-{ACtOFxbRv~yia z3zvftFup;*DT2@b8exw!rD;m_M3ME`N;jGsuJ} zNQvnbN^lY$V~p;`jK@8E8mEPmW6f`EC(0kmUVVu}u{`Sz`t0)DFMZlCJ>xWhQ+rst6@u;CNB?HFu#n4M2_} zXth0|ccI_ttKm#nnB0TQ3RTzulaV0mIYQ}(W=#y|e|%&}>x9M7uI1;E@)G~crFUu! z+3lKH5D=LSs}iD3t%Ho1@COdZ**_5bY4_!5;Xj+s;Cv*kJw8v-yAXl}4>n=fUAOAS z4lG8A!fW{UFrj<-jU)gWctEVs9oszhHPWi)=6#+EzW%9st}F^R=|w0wM)o7xM$^m; zD|;7_vXMhevCx6RWqQFm2LX2EAXDC|n4+!Z5wtm}_*Y|794A~mC)^u!QZh9(1-AYM z)~;AdCTi=i6l!smvnQ*Yh_X8Y22E=wmIAh#nV5T5rpi{_*#}!Fcy_@OcSCUUtu&=j z81_2?dKGxLXm-b;xZB{a6m(hmVlnB010oYP=Nf;?S|tte<8O zzhXRwHrHm)BVkfrz<>f}AJYspkxv{rVS{Gr$N{eaxG)nzmChmxz052$o=+wI&5cT@ zX;!!O-;Kkbb}^gJhmBcSj<}V5FRnafef&~gR4Hjmf@NDNAYD6~EAycztTW3){-9j_ z0}`Up#OIRw@rhQI&d|LT5Z=I=;-#x1GF!Y=>D=cj@WivwB4Z)kVVIvK{AX2yxm{tBux|X96+gd>K=%? zG3q8-x5D;JWZ#b6tJdkN^adfZd?9C^n1D^L>GH{^u!nzj!xCL>Yfbgx4 z{z^`aQT=S#Ru8CVsAzGHhn5M;+_2V4LJg>PwD|G^5zzh$+ExtGIe^Va%gWkCSv1fz z4y*$ho-BWBpi0m3!;D$kkA3e`G$_< zhS|P{v`uuhn*Ue!29vmiE?N5N7<0YiHsXt+yc~8ybApn@ZMap-?-ifnq5z!pTj`|T z<6#H9rMUcM=rkfT-x58)Z{ zWrSfSRA~vE5{|+O9^L~Bh~f#5M2c$Uo~F-Z>E&Jg8rzCHBM}umHhRKj*=9z#q34Jn zosL1*mC!iyWK&L~?=6EwYr@JMpNWi{ta(8a&29M}Bvl^3Wgh0jfqn>N3>6o!C%4IA z($vcg>&@L2a$kXrt&@W$EB2*K*{*Bj<9_p-*QZk)s-wMmc$bCw?;B~Nx}FHIUiG*K z=WL#ECa$N)X0V6#tIej$%H|B&;1+@q$+*la+GhFj4~2gBSKhyOMWjLj+0{1zGzaHL z0Wt&4K!`f+wqQ~~A{e({`H%KJA!QkN4Z#oCt_5m|0vBLb;%O+^~m`wYQ}6 zS81T;A+tGPg_O1^kw79>92IVgAYQl@L;Fjv02q>*AUfZ-&=&wNKY)}?|9zSpsHhnTuYkOe59t~Y$c9|L` z5MQY2y9*kFrQ3rU;ATs@*I*1qWg@Yn(qoDO{Zf4)>Q85o&3%la1oHdJO&$26pV*PVvqjuDC$lN4`vjfP+q8Dsi#c&nSE z8QoGX!ISR)rP_o!WoRPD(}kBppUcFwvJrzC>=EQqj?^M6hlx33uXoIBBpHxZdREtm z*H&%J@fO%?_Tp*RwO7ZmWh+~HdP$V=-5Z5|bxdQ;>;^qKltjK2_rc*tG(T_ghAflp zdx$>zqiW3IqvNSRr$qyOlY}xec3O0mMyuA{LQbKSHU)Jv?0YmtN{)q)DhOHG47%IE z*`kDF>*RdTx~NGeXqK;NeJ%J{vc$XoQrPPmR5X@!6jOR!TC@Cn)ch#E+P^$EA@Mx$V zQ@;C??ol-B`Gv=!{Zu<*#f1(*peevy5sguLT+et-$9B zC7~X#ekuC$KbGvYaWmM`dO(jf@Oz3m5XhVO9*i{5`S6im=VJkW8g@mfQU@bMs*C&b z@lv6`+VP?BMO2{5^DSS1*wATEyu!Z%y;Z#d<8^fu)e@Z|hFt7esK?8-lREUlVVon` zzp^Hry1f4v1w}U6*yIegEUTZ|?oM_;I5a+~WjJGWl&Iluuc3kPThaN z(f^6I?(p04@V#9)u=X2X(C_2;gyKbsQ*hl}s@UYlX7)a<_5a%Va9i4W<(0Mn$25IM zCE@`GJ}K;@N%hLei^yo)Loq^f4xkKVpqbeUR5{MCc~dWdw9`(+v#H#aHw6E8p!dee zcl!(Z&_bHD{q0>K({$Km6MJ|g{~_$#AeX}3*+(SWztarl<5hs7qk^h;C&^O#%Bjc7 zpsk}U9dv_WV(|1O*rv_*1F`JyZbO_m_5SLreHHi5`mm5R7SP!ZOAN+W6 z#eOHRFpQ+0`q?D%Im^~Ai$hv;=a1AC?SV@Id&&b$RjB7^>M>CaF@2>tD{9KY z^~JRv$NZ%kT~;mp!Re|qYEA!*Phx6W1-n@olea|$BMY2|`H{K#kKJah)3pxr2H3}~ zrE3(L?goWFYi`P+#46MgAmcK_7mSu(Q%HZvomHp5f-P2L*w2}|i_P$6-jex7;f4Xr z1CkBwD&>!Vv)b<7;#4jL4y$5K;?LB#1TIXuNwe{SA$n@0J%7zyXGg68 ziFJnnCEg}TYg6^hlYw$7f2^lHPiobywqEa7=eW7j4yvr4p0Wd&t&o?ePdlRzleDcZ zUrnveZ`?L0oEqm$*K0j}VY7@{$=@C7;!h-&I8v$895Q9|+Nti=%=wZ+K5pcCjY zJ`8mCOy`o4>@D>mmEWzFaTFSgNHMs9rm(1buedxPx|LX2d zM)ZRJ&Vcm~!xNSHkOYIGiMytTp9Y;N*rJ%h=BH}`O+3q~06u4a6vYCj#B=RyPkx(4 zXR(PG3%u?ODbjTJj0rY~0=E~9`~AmD*h3O2S-=2FxG^tPhs z*pQ9W0=7{$`ueH)SPUCu;pa8VfzbY0kFMWzv^-Y2_gzBKu%0ARlOID7&7PmFg#}l^ucz5wCP#Oypu64gH)x>148q90@|NADLy?B#Db=XgB#4 zkm!4hG=%tG%i;}#PZXXc;oJ<0gUUej4jRtGxHJfF73kiuE<#~d%%^a$=Y|kgE44-_ zJ^s2A3gY3cy^vZlPELx~>xm)p5a2!wXkbwuZej;aIHm~}3q9vTs3%F{DuZ)(`4-sn zO!(!7o-ej-EQNz@L2!^X}pSUe3JOy879MrB5XH=W$JJjnx@y0ilVY6#$b=Eoz5ml z(|NvLFi2BKUwhU{7rg_a)(#?Z7pz@wzU}|^vRxuyGZHZ(-Nu7lXo0%b9Y5(%ZPP!T zxIkw~AzM*O?~+kOCes0__R4r$Zdh*oy**18JJ{aM#X`Lh2F1~Y^qeQyy3f_~>!Zo{ z$6b5g<)|~GkTOi5rxxWZ(6V+f2^qYWVKJ9QVWl{15)mJAh~vo20I-+8#74xfyda-_ z+Xih;5TQj*k8bB++-5f1yGib=OTzsLB=^4{_DUws*ag9<$DJvV_K~U{=N!QA?a`kc z0tbDz%>gF4x#)6X+ygvV2}?SJdKB*h8O>k5#9iheLhq2Jw|u7JP9S~0gsLc`B9P;^ zGB;*+_2;G!nrA~a?-gb6v&ouaE3}avnp&yuBJsI$jbiY67BNerg4@>nysA+}il>)J zx%d0bgTmm~x|yen29nm>K3ZjBopn6qD`sB!YTLMdPJQtpF%s%C*tO%zJ2y&3*>cpq z9O^Y=Jb+D5Yc=adCuA+yqriO__Ct}`iPd^mP^*&-GJt(hjc|kNiH_6_+-O)~?dj93 zbV~_|EEjz$+wE-e($TS7z<6LgR~c#I3N8U>_pf1 zLI8$GHO}cnqB3j_F1Wmq&_?5^8E@>Jhcn?jC_8Qml%B}|Yunbv zYYpxuakf4seB?1w`k<_6?%9d;_B$Gt$hp6@XjE#iM%>-_Ry$o!lhjwc$|$X(j;nQi zIU>=Y1ZQ2?G*~U-sf!1MI9(;;_p-dh3hSmJlnqO4D51AdfT&f4nDO^Z?jB)5H(3_j zkpv1#P0@1ld&ka3wb%>!#ZSa7wJ>B%`RaY5&Ak;NHgke>UGLq7FNE+yMXKA9f1Lu2 zriDgVB)eXdC_1u|yn%I?OlD?~VT)KVp!ZW06pU|)WN7nEeBM(OGeQuisTEQyAF$Ta zg@c8F2fdJraae;B&NoOHGA?Sr$UPRvj=f(k|7aZsE-A__Iv`J07WgaX%p*aRTmhhd zcYAps&nm;9A0$I}4CF4H@_ik)RqF z4ijV$%(Yd8*)-776T?ITg7d)f?`JMjccwVBtchpy9}6w_)v_~ihu2Pi=}h*X-@opO z6KtGX?h-%JO|1Q0{5YvQNF5`HTPdZc@e%+Wx!L@tG0sFF)LjyHu_f>B@ijie zLX{5K{49mwt(}wE(jKBCGUiKB`8=0yMH#!5kTlv5mK3nLe7{6*MUJ03u3S$;#GnUS z7j9Si4_VPqJnk%j&d4{=&S>s$epz0#LGI(+)Qh4LGz(0ffruBnF*AY;e0UUvErCV} zL7ENmc~qmV(#J!iz$HFvWzLzZakd<9JXDq2!z}9z+UOAkXMZqfbISNECA+|f_xV|{ zCETo{8C?0j9s0&X-lAi3;b+$*`r?1@K1sio}@Osq0OaZy*?r?w@g| z&f#a4;=bx!|nD(h62H-?G2}%crZ^X(D?4$v(a9 zGEFeA-Q#7?NPpjKH*}EUfIg%GZ+8Za_TziJviNBfL6<=$$v&`36Wi7-^GzAeQ;l1M zBa3}g<^opzn#m2;#5l1J^L}Hvwg4*jCRi~{aK8|k1mk@X5fd#XF@R;ODY+b0X57U= zCi>soa6&M_xB8dNQ36Id`{GF*eq`BzZr187D zx?Q6&_l8J|->D-kSU1}>DAX!pG{q~%^s68xoinxn(c`T!3ILe@D7X68-^}LZIDN*{ zbz4|<1$EhD+9PTltjyh`A+fJb2h?fD>12qYr$*)wC7Q86D1p{Ol*f@%Ago)%B9)8~ zjXZn_A(iM{c9VB9J|yP zGshWV2g8plm_@;C1JiZMg#;BsYGzin#(n1SI9XukJjM_q=TuOJ5O&=^@LL!@tj{Po zm^gFEeNN@FcGfVoIDVce_PWJ0Jl*N;1K$xAC!A}U`n_P0K77jXk1w;rl(YlbVBRE} zB=`bayf^K(C8g@ksgC-69JOXBEJdb*fLnT_h=CV8sPwy6wjc<5yep8L%|o5tT}7Bp z$IRvp8LG1fw3>~>_^oy2(MuX5?Z?-u8p@4qF2#L+m{^b&-+QYAgIN;f=H~!WdR-! zsxo8+P<$$HfEL&jz77=)_7x&$?#r;pdJNPla}7=?D5CXWs5>tZ7b{#1^)XtpbWv(_16EuyrWKJ`Go<23y6RN~QJ0{ga2CJpJU{)Yq$&V~Q zyti^6{H$5YCOP_5tZsAHYPO$l07`5k#*39j*_+>Qkkixo8Ty_Z5>|N4Q`-S?=Kmoe!t+tZZ0yA%2r^^_xY9=t9v&IU1pae^&{E3XLn zA2l8hHD!Z`avud6GXpCY$71Z*a0r9|2Y=O=#iUDdiv#10j<2N8Gq12bT276AZL7d44z(B0Rc~~8rL#?b(~e;p9ji{8m%xW(>7tMgsNoayzB_@?gRx;=8@Bx zM1U!Wq3k&DaC(0Mchh4uXiJ!a6H&d&Sp2y#X*}I{y!DhcHeP}cQoHdo4&i)UiU`ac z_FYp4XuFwTkB)np=P4!SiE(-az(^w_k}H(E%cXJ3QD|jno>xyD*ugY0?g5kGz>zI7 zi9>o@Vw5j4*|ABN6`T){fnfQT&OQS_FcIMkY{|Q2Ct4n{j5T!`txDw40=eAvBLd^b z@E_=bgW)XNojou^N4K8V9g&kai$b|8(|i~Xtp>LsT`~gdCs0g|x(|7YFPaf_^KkJR zvW~Z|f#vOF+NUjWyp7e4TYESAeO7T!&=JfOd&ewM^2d$UvS!Q1VIa+tI_^vt?G4w$ zMrQ{t;0va~Zec~SqStpBgN_(kzc=)r)~#Rrqn`rmITXCI}$lFIIAh>wpt%4)sM z9`q;O+5V5LGpwh}-eTQ(-S;cy|6|L3`MqhiR*`-~tBAtl8qeV1A@2yD*R=0t$c2r> zU_Tee;)E&y*AVAR&bOH#FR6?W)x=J$X>S|dUY%q-ECAE#^FW6NcHG2=Ta!)eqwD|l z=~TfkhCgYh0TsDcQ+&sXO3UQstNNHR8`ric8&iuh z($mW#czG&|l^F=nIWHpa@zfH))hg6BV>woxpjQI zsM)bl@XGr2y{wXuy41NV?&7(+&p)2I_zLx8gR>HxYk~WU*u#tENuw(anUJm~kC0BO z>}I)H!AvH}e{?^?MmHM|qII~hj9lH;Jx%Cn+qn(!Dy~|WQEV7_=d`Wg11eQL&4HM% zLl9r)%oI~aa+_t-!H<&iD0RXh16RrmVpgKa9!s#qSomQ5vlxt^g;$rGbNQ$riSS}P zMBvZX^X7{;4);bZThq~-E7cnC^0F9|AFSLi)s1DyI2NH?C-XUha84NR!^jD%shJu)_+A9jI0>6e~h@@^kFY4b2R7S=Zk)1 z(jINCLtZ1YXle~`z7UwadY#<|W|94?FWzGdu4s zWw9}301UJQuGAM620a3lH&o5R=d7K3%IwBj#%$iI(sxG3rbY3&Qe&! zX=Rd9yoP6OSlr(9hf;Jl2&;1s^Qfv*tWPR-lR7v6ohEWiwb6Kn<~IN8FoTIWIXAQ2zal~uYn;nXw(*Bg%7ZhI4`omZC`=v{U#{rwZS?j)gfKC)RTpQj zY);IE%5()WeBQR0-Sm;|0hvYB;Dn>Gjo#0{a=4EEK8#9TmC5hmgS_*g2L%NLRLctUqRC@1p$s}&CK#ggb^aqy~{4CN?vEUkm>WuYsI@S9Y=54 z>xbWcG}57q$W9{ldSfxN7*b62)(ltB4oB0?%aMgPfvvEj+ItxpG_u=_`Kcc1aeJW@ zyxvu#o170gcS9{#VP`Gn)=xu3W|=${_Y-~Ea(H5|eT!#embtx=$fP6+sF2kg%!DRp z1?mN)wBEk;<1-x8!$0m3JAEuH?pd$IQi{HZd4LxNRM!0%8xPq*CkO9jYm)QNqUK&# z4BkLjjISS6&W-uQk`hNQ3Plxgn5>{O1yAW3E%~83f_&G$2d@6~=}TgbjHSg$tpNA1 zo~`vaq{!xV=;COZk(ltrxU zxYeslvu{kPpd~z2L(@s#@vV%2oB`D6oiojW!WF<$-qe|cNrMcbu;uPfZvN2eP*c}g z-m8{;J{e|yQ^P2ypshMG=`{N4Rvn3@xyeS$sl~0tqH1$m1*&~#xXT6S2a%!{;*zW- z06^tM9_Fom`#E>HBsZFQ7=a(|Q1=nt$FkYJHwSoxc&O_S%e~lkrnE3vNG;d{WW^ch z8cuZX*@?o(UL5y8yt{N4vY;otx^^WGfDLn*Iy0FvTg20n353uwCOo!*=~Jzjo?OkT z+<0wS-s}?ZvLBWMeU1}m)fnL)id|@xegAdcYoOQZ^HIQmWADAAnp*pHQNQIXifxIa z(sW7hNC`@HiGU_F2@p(TV2Kb&LO_rr)wQg2BqTwq0tq36fRuz9_!b>PO$bOrKza>G z7f{#9cg8s5tbNA4t@BHh{95eGRc{As)Jt)gpxvj8o)V&m1fL7b9fZ z^W9m^gzFT*nrQ71;LX2lSi&8<(yM50cE5owT=yOF?YCz$Q7qexS();wwfVBWw}Yo@ zZ>fc(5A-?Gp3siJ6;_{mZuep4(#^l!{1mA`=Z}o2=Dtg^an1DuRJG4WjX!DP!;j(i z?t9P50ZiC?pA!E%G;TNNXyVaweNlewI`<;HCHmgYC&N{Z>Xq~27|%&@jHfyz+~{-^ zs@8eyc|J=kNZsMQRcuG(sa@bKC<5|ac@?#98ga>N_WFV5SIIW_yK>fO-Iu<777iL7 zQ#!IRVvbFAT~^1?-eH2CS^0~*#arQ|9ZY1YsZ{z&(@>b0bL89NC981V1xGDgoU!R8 z&g`kuA$>L5@~90k?#nXNdXc#pB;PXs@ozKxKi>R)`5%8B5?H72-?0An`FFLAS5w{} z`(CMhv#`*dio9TkT0L}lbEG+PgV6Sto4LEQ`@2lfziW)xL`?#-Iw#qO6{n$~i}Skm z*P)mPa}%~yuMQi`WSLvg3$YtG(H(%19U7XdI=FBT`!i*+yV^#*19pZ}%{&G+r4 z(cbkWgwxhUUenD8r2C!OBi~N>dH!|CR1B=U`gCHldLux{U0tY^a@jZ7LQ>X~9zaSw zram{H+RCBEAlpP=ZBuu===s-=F=tkHe!p_)5}2TE^;~JFU`!m{eslPvxIg2|#{yB< zDj9r%!oSj57I}v(!~_AUM4Mk_n2DeNHK4<*1eEfkxk~uy*?Nn2-;wTCP=Ls@YJHul zmr?M14;?BycYIXfu6$xo4nek9?W%O|t@@%&(|KxJ=u`7jX~=c9a&DzTVB0E2 z3bWo(q4m>9_b{2~)`!oU~?g6H-fm|k}oiB~)R@B6?0YwG`-$A9<{B9S`Xq`qa7$jJ_A z;owQt8A%1_jS8fwCG#?YV!0X{uM*7rT`8$GiJ0{OS~()+58IMYIZbi9c+;l$;OlN{ z(o17*4?dc3j|$ui{>QnK^tMoo!}KdTj3win^#?yFZpVE3|KeruR%~k$4DxO(tY!Xf z0Y0ZC0;)?*3=Afy93{OIvw26NfxWEa+dYFt)o4z>S)K8zAGW6d0ZB3eEZOiG;s>_D z_SLT$#{Nlu9%@0s%AwOo$zR>nns)Q;f9mTW`Tg%Z2YcMOx$8_8d0eXca-hocR;-T$ zyr*tibUP$Gi!RyQ&Q0?sR-M+?>`76W*hqz)kl4Mo^(OP@|D{q5fPIUJsWORB>1yZ2SY0l@;u zC*GyFaGNWSm%8-=G;_9r(O3X_5MHuYk2r>Ta%$1#iw|wo)kPkr+%OCE>dGic$yRX> zJdvB$6W4-Lif}Yb+%VhFd*eB4;rO_Uu0`Ed;k$iG+Qigi^3J}0efRUE|3BqA<~^Hh)kj9-$#`VHnO$GSslsz%1Q7jfnJSV6%sARVoZkUh~e4>;YcW z0;mt$Gl@GZtDMown3)6g5}C&}76wW~}T2@e!KAq9!apsx}{+BXB8?1MH&wsTl313EEEvQ=>ywluFzUENCjN%KG zWsR*7=H6lQSd2%c{W(U1Ma2b*E48Z1O@=;*g~90D2UHaTy?5x z*b30zBXa0qfE=NsIQblt3C26KL=0{1auFAS8lb2(pWGuK-)&5H>czkg&bT!Se37e5 zuH|gjwgclnuSk<}hg7TfCcCSHKx|n@p|li{gL6pKP=Wc_x>tB>won5{q-9swKe128 z?#B4X55s{EE|^q3kfoZqVZ}7LBSE9s1Etlqu=0wHj^@vX@i~;B(j5J?yJX>6HLhn@ z>CH?^=&E#PvgvKx!Q=Q*uU8?`xP4?w3{s6NkV>WqvL9^}x4Jg^GT+KG65Z=-rm2!QEI#glKqh8v5Q zRzq2(j6vwKzifcpDd_$8+qC+HsjAROm7USWwg4{TB%sHrf*-Mv=j1_Gpn5fOU61Nd zpq%C@NeTvqTohL0x2_IJl(Kd&u-{y@2J^g3%eKJnuR|wG@YuaBAP6{t>|CN3L?T`) zo|2KE>wdX?LkyD4`!!pcdi!Ral8j@n(q>2QJTT|NBQZGhc_F=E zM>~We>R|}!gNC1i=OHfY2oT`1!LM`*sOu0K3vB!(?>V_;6zYh(V! zwi}u|TA0#4^GKqT8X`g8SUjyTOfd#LyPjOq^`8xf<{B?hhL6WLF7TA+sihTobr$iIG;AV0tRE-!mF z2d)xDoY>xQKQJ1dWyZ0Bht&oOhfwwNc!Ps`lVo?gKdKN{#Qz|quemkV`^02zp7kVU zV%tyW@=E=x;qoP7BIa54wM%I53@^9LH7E5SS#e8p)^L+-6F>>|o$>)Vob0wY5@zG7Dpp{lRl>D==J1vo0FX=apTf(yIR%(Kf*WuaQI(Lj ze4CRA*7>j@jS@8dpPXOD!j6Rwx;{|#&9&(W(P6(HJr{xVX|t>7*B^qncazFB61`=O zYTI*@Os_TTzL=Q}!^%0A$8nR--Hmf5dY|uYqwj{{)1QMOjon!e0h2j;=k-?n_?5(s zU)FJH>?GskCN_(Cx+?FAX60}`eJi~t+o%7F#1>S!&`J!&Ii!yo~avEGHyv^|N4{j%^z)P{f>Z}1SauDt9 zaCILw;&dr;jUW`$Jx55}v%hp)= z$+q#Ye13o2tyVcd3e2xN#L(ip%Nobt{;@%&<+0p_nrWeTtp)pL-CO~jl`h`L+HTV zhw*YQT|;BfurCMc8it#TT|dGSd#aB+us{-Es?Q2l6Gg+l$SsS?Uu7_OWtc9pue&+7 zhyVc2M+c+9b%PjRTL&W^+Jn2&W0u!d>~JrL`e(#YhWh>k*Ux8WZ$#Igc$Z{w0gF9N zRRMvRR{6^6#!rq+Q(*;^YplMUob1qEm69DnbS{;iUe3}uOMnHzdGHSXbRJnHH%nKX z+rz_0EX^g_DrrA!UkhF!5Yg2v>%jXKU+u}aejl|}0Xs0Ax*Rou8JTfd;*&oYj~UO) z)GF}`eo#lz_IJC>C_K*YM;9FF{YGipFgV@~2D=hbk107QNj=>l(Tjc}S-Hq7JT28B z6TXx^oa>U9qD0ru!}~@2h)i?NKw8s6kuBZy+r?>&Ob2#Y-(cjNWy!*S=`JdbV-^%Sy*Oz18<5wB@tp8nADiO6n^t{%mpNP!G zvFb$_L5u#T%_xFDiP=T68dgLfdz?8W5bn`(usA6K?lqrT%I;kp6QY1K)Yp&v&i2mZBmKSqdI zJ94&~_Se1?>IMbO-Z}7B3!==VeG=D!$DHzd?D;L{Mkz#+!BOKlv_{jZ9#u&$V@BHb3xgDXSdo zP+NI}W_c{Xfc?&zYcO`Ai1t1$twJsFe|1z1j_XyIYk=4Wo<-?TzMTR7b*RVes_^is z{td(V4|wmt4vC}qsr!e&^+r(!EOMRlBE_t^&KvjU#awR+(;q2Yet+8&8XcgsjgQ$4 zx40Mk7jCI+JW&sNc`uj&YuEL9yDCV3^3?aUoA6-Kk>ims&i2+$6l`TC3X(wVWpKhE z6zkx|eA2Uu&mWqi#HY8x{^3j>nj-(WDS0o#TwhbtJp`wZ@^2KNLU^hMox=rU!vENV zF*VB^Yp_JG(Qs)Oi>{sfYBT0>JVCwgQJB4V7)Ye)J1+38UbSvsf4LWQkLu5|dN}Xv zQkd?~#kZ_p27wW9mG)kT)S9OD%#h7?ScP86m7zsaUI;dmbv4ptp$ev{eg1H`=Te^o zI4rb zHzL(xg5XhX=@5iHuVG!n0D-7HF25r0`a9tg0yn7ce8(P0hbg|u; zeFx6YYmPX)wJYb`b}?FCYsPR22Lub$Pn3Cy$0UIWhtV*?mOzQEX9ainTzzP zG+dh)9M^-Nay%WijIOLiFC*mXAv8f-)}2NUevXuABe*QVem_}pj%Cqz0Iua#h6K~3ySRJ8~caUY;}MkBEmKuomZ z2aE*wxiO;>K{fTjmPLB_6Gsqa)iQYI%}pDOcq+J*!vnE`40# zv^egQF`o`PVK_XBd@{^hn_I)al!CUkQMMtjlRmQHMl}cQ#m_n6LwslO(&-ZpH(ZV9 z2kqhoYaRMGWDw!nY00_v@8WEoiYmsuGv;4eo&=|(giE3jtr2GHCR$V-UKJ>%ke{|+ z%(jm7e#j6-dSjJq{XQhbyhWi`W*a9e?0gQpKvecREs2 zT2I2Wpl%x`ef^>zI{gAV?L_rBzCBXaBSc$r0&EYu-kw?6v)Qj#W1XMeI2U5`}(9}Lieep*G;w*F2KkII&?)f4Dlx&D6izSRwuYnYXZl&9r z)i+x^9Mj1{XtjgZ=#-p#arsxn7H9WTbX=tu*ihJ;Fum?06aobtM^` zfd-sB9?H1wmlck_=||Rty|#i4+2nU7CML$VBF+&qM_jRGo?#Uu_K)w17rQPD2GCA` zFs~NYy9?g5m-_2^cUCD{4p12?^WB819-*y0^#p*T-I^B2lb#FDOslC{!v${+Jr{5^ zU}4?-16;*=)NSg}+~hTKNyYM?noO2RTpU&WbPb*nB#$r7^=H{yOSSpICr)USs~E&w z+|M}Pr)r7|hAbvu*M%vDx`31RsW#8|IOCRdQQA;@w1Vv7wxEr;jp6O} z3X*R8-U< z?NTc&A-;CdgZWipQ2|z7^e-+ozT4;|cl75?#!=m&A*j)?t8&FAw!0@a7n}W}(w&pnyiFo~Z-6x;q=qeRdZu zKN|=~C1x597i5E;x!Sn3!C-^R75bLj>M8{aiWU$1fm?Qot(z2?c+RBS0SSlOB~Mk~ zEt9~#*jNjU`OF-l#h9)Jkvz`39xY1bXe=kj8p!AVsa$n2JsYcC3oY5MmJ*UL>89=?77;xX0VVP1rG@>cuU;D(1VloW8HQ`#bS`Rq z*FkVhBWLm@+>ss^gI;&4mxh^D%JXJZe zIv-1Ci86<%8t;Q=F^GDoawsm+G{2#Kv~Pi>Ig-%ZRVHuVApn$&C)p-Qb>K@F>bwg6 z*`*Y%ZLOj3x$rf!EwW|KrJ|A=#OPz~gt4UJDGi9n^RTsw`t?W;)|<#-L5U5Z+aiu>_}pn&OVv0_H~v-aGNhs@z9ulk5V0ldwzd>?heEwI&s2Ap;4=cTfqIoX56^kd`I`Vhp6 zSC3yQ4AdYevm+6dwV$*XU1B#WauN98_N#chV?#A=8m*MawdAA;6qVkwV>Z(Pj&JQj zTbMpU8A!qj&7V!fLaVpsa|ONyxdnMXvMzbY))>H=E_3Y+wFQm`-1U7s=eLh^Py%`i zz<$6HePF0d)fs$x&{=#PG#!``KLb5Q6n4e=_Cd+bD6U1I3T}ZkgLgHqp_EYDoqEe{ zv+>@Q@N2h5St9eszIekiZXM@LC3EEbkHbP85@Ymny)Ftdfw<#*|GxaFRmmieuI%Xk zv#Wg3#*&WJ)3JDXa=C&=H|QRuaL5*T`>IL2gO`^R#KEbBLm0xBpT_dn1B1`NB6S41 zKKb_MbhhQ_vTIWIJzqnDNNGi&FP0(W#60;zOW z7X9|f<;y;_bcYJk0BWMr-Q+_)2<#1!0n3nruwfmU6Baq&J&13&{bToTQQfkWJ+a_S zsR%zvZoXl}@7Y1x>ML^ly{>(hz4gPhY>_XwR2**sy%^72{4+ooQcsIb3iHOKcGhC?~SjGWFK z*8S0t<%o%TB+P4_fgN*`DAJo_s~}Bf(67gWmqHxGa@$D80#y69H?+(tlZ~EeD^UOH zS4A~{+}E&JYYMlP^(`q)V`P`nFSXRPAkme{*@~7@l5UL^#PqNjUbVGm><&aOe+;62 z{bkjRT*5``XrrDu-bsZq;_~t4n+sqs6-XPFjt04`Z!S758O~VIzrfAh7@sMnALe@> zFJiG$%1?T;1ET4-KDSWn`Ly!^eRs$?<^g;Cn#|AjaF4( z(Lu!;DKaik^;6}9@rxiJ6{Q`Y4SS6PoQYr1BF*_|5U${(sZ`4m{$fS^dU8#8Y)ibdx~dgG6Ah} z&$3tJ#zmi88yo0c^vSsRnuv0`4d9p6OnVD;I=RH!7);X$f}j}wGAmkEtt$UdZY}n9 z30Grh*5X6V$FyJ5|1l3?w-V$~KTgGVju~Dgsny9;=(CG1=NOx0&q!^7#^uby(wwPM z_S`IHb=@ah5A9?7-pOAcZx<6CEvY)9}O{V7i;Hc4lQMa2QWPqopDw^85yxc z- z@j(@@e6hMlOt|z3Rbvo7ZJ6^ShaM9A*P+0M%NK^86O-D*+*p~3jhfCc^w|Y%lsi0= z)E@&~@7iSqksz7zH4CuB*i64M2PB?|M1x}c1CRO`ErHHgK!nlQ=+MQoI4Khp*i}1p ztsr#JGz=~J=$VrbXSS)hy89Wr*}J+e4j!3NKfc0OFnHKospv#=@=l!{Tu9WY%)Px5 zl(XFFRp~7EHDOD1_tp|Yy1}A^tqJ~)Q(P5HuDTuBhj?N@F#;5H=C zGUjt9;|Gc8z}_-OX%7Euqv!KHbOpQomi^gA`Z`Fy>7l?C#xi=DX_sm8!HG;g zX(A9^V@Y2e=nC~w@aiO#hq`J=XM@xbUuFx^Xsg1@c(6~z`%Sj(ZAWx^1)wURgx2kkIzI8W-S zdxn<8Pm8ei_jF4q!pV0IdMLwEOkOxuNx^{Pv-b?RD(nJ%&?7aMJG3uPK$xDU*#cr~-61M})5-w9rHN=jnfz7uj|JA8mHfL*l__H|Mkr~T# zC^6dW9nJ>ng|ql0oV#yC;yM8}}gZJVS9I7wnqoa}56Dn{q8S=xJ)cgBx z%2TMogiX*g@8ZGggKly4M+7@X&>aR-MgEX|BC3b59&F6N2$WHcO9b z@lthXF|f+G^LG1Mik1b=WQdZf)U#XvG&CeMh_5iI7*_0gWe~;AA=cTO=Mnz7YsqK|kG>IL-lZ=yYu_b+s=SRr z6}~;oMNw+2{IWoLa0M=+7g|_j$U*HYB(a`M5aXvyxh&d{tCcf5Id~~;Fo|G>SLWLE zT9d@tY>@tEtLLfnsV6qO0)zn}YXQfW{Y^xMfsUaE2P?cUe*8~J-W_VbLl*NP|j@Qd4(F+z6N z*+NgLuTIPdqpruMKBfg&85Wx>FJ-ZxcTvn$UY>wSdS}e0`uDVN`jhapH1U_j3VFVm zQ-4w}_~r@%hW8J)*NutFo*Le@ckI6g;whwZy^ikUZ-YS>9Eb=2x@VYg$9Zvai#VnK z3pg;$F*0T^+GkpO4Kt1*0Em33ZqSDYDFU$#do1o+Wqg|dwe;sbc6AeFzxs{FxrA0K zJ1a87=ZNEgq|{wxvRpB$tZXHqffL7Ws8>lV z+jwV6Mk~k^S*xC{^8c4~-drCT3uHkOY7M#7@z}>k?swK5&(L3JLxvR*XVZcV9ugm- zH!c8f1(;{4=6PqHp~9R8v~Q&b$ad9Iq|rHL6JGgc?Snv_8DfhS9)<}K3B$xvm*Zs# zBPeOu@YK+B0(iI*pbip4g7#IA0H8Hg)<|5v_4Y?Q>E%=3!Z>2kU(?1LR#y|@rl=;b z`z_9~{>;d?db8}DG5^+_>_zZ?`zf{^mk`uL8u_CE znO{*unF>mY96qD)Zrl0|!Dh)nZR5?GK`NsLI#a%t6rxv2@&o)KnCX-{>GNOySqo$VK&c#yY^z<&#%Xo(}O3TrkxPEP}x zpXR@`6S*xAE+7HfN;9}h#<0jJZ2t}4+%rTXp9jvIG&)cTM+4P06cxq$AyOPr>SPyr z+Tio2wVf5ffjE3H#>Gp+Etonq%MmN757nfYq`&n)19BKe^#hBB$rr6NW~{v6`-g9H ztMFO=0^J(Iu)mJFax`;dXVqAK(Z!T;KD8sE0}eZ9zW`wyzIzR4_zdWjS=|T6Kc}t| z1J=cnJ^2#|n_20dpuY~i9R7?BEJReiyJ2##4q-w$p5rX41TfSxp|eE;By|m~4K21= zio%$I1{xA6!};3o3d#B?6f4+LKj-eq)FA1_%!{_>LgVlu->Q=%lu{$xe_4J zr=d>TSZQ4Q>yT{OBgyRu%$DH*!WMns;Jm4?SLDq=pi-_+F4PNQx4D>9%gEG+6Idg) z$w3bK318;2*4_5-D1a1L!)fVu+&QNX_X?zQq)qxbIKs;ay;!OeH%j?ut#x#(74nh- z^`%|!G-%|Ew^WYbWHvud0t0}nRF6%ty8bEL5e{{p@I8H6;EqdYB0 zqHLor4csMxWpV$sbpP8+@uyz{j&|emA6JH{bn5G=D_i9*WXlGZJSDC@!f;EB$hODY zcnZlwrRWCJm^8V!Z;gk_l2-C}Ym7LF)z^{mY*Oic#EKA{bKB~| zsd~uGKC2i2Om_XAtvbLg%DdVgzW|h`f!RPWAEK8zN9?ZI*Lwr&3#%ej>7`kN@R%&e zzOoZe-kiN$S7dG-svtCiUZ3gn)BEOaB;({$mzuTCqFeVm``U|aVl)#KA=QuB{^~eO zLtSuExGHdo{Y$b!7C4LvcVDEr4~vO?AX8hgH^hsidtkTT{7MHA7Uto~_yl8}1U3tV zu3^4oiSg;uu^G#_I=oA>TDLyPeHuB}azhNppcmSd23N2C5L-Ak8wnM-aO>Sb{cuJx zgok<0ZgBxSfp+a_hP<@OFc{QawlU=vHFW2^%LBgNY9*B;)B*NTfbp((ech&x=7ztJ zZCW&8cCbiA3BZd;8v}_!!eekH8l?N6u>$YMtJ<$e5LrF8r<83%bkY(qXl5#ywN@%j z@Q73!dC!@B6>{~)vw*G#IA^joMNh>q>|Sp=o(0aq&v-wvmQHmZOvB)e&Y%DmsS>mIyE z4UHms5bBA-*}$CenpZ^T06)BkGrHvd_3I&8tP8x6W`sCXw_~u%nt1x*bD{eE9ABF6 zhB1QSgU&E5VT|w44Sf@@KM z#?l9N)M&he@Ogar0EG=jTNT;YkQytnejm~T*qXaEtOZ94lgzM*nT%~ULJbZV_F3^0 zeNiE;Bzz%XQ1dL@D^1bxD)rt*Sb!D}-JFDHvbu~!?cJ-wgC8^u=uug88cLHDzuNt^ zHTg?(L{427q^P%C<+V^J!{kOGMS4+7E_Q_q3Vg0BE~G0(DXV`E=RkA4d?Z_yA;I%? zkKzXfw=^|ZSVZ<}gk*yu86%|LCT!iF2auq8kp0Qu8P2r+7sHOh4ABav>)5o%NME){nCD!sA5n! zd&}*ye`q-6!orTZ?1%9w-}~M&#egb%d;Ze0wy~`%15H>ZZdt#r(oq{RFnUz=*P%V{ zU4G)5xNwGA?jr?Tw7wJ0Ei*PrL*Fo0?93`E}fW_*?nQ5$w^2dzUX}`}N}<^3yMtowNc+RrFRX87^g_tZZz&7^({6k=04+ zQ|i2M&uzxGJtEMJ@pVMlCdq4Y&CkmGu(HCf{3wd_i+pTxyre{f#Rc16`S@$^omvI( zp7yD@Kx|<@MqKuD!qnl>=s>JwU_4)k|1huTz^=5lZQn99Yd}tkO49CO5t}+y59b5Qqf7+8w zElKoKb_qIgF&WlWxs<*B>I*IW)N4DkG0xGKJ{MVil;Y}~U1=_vu(m^*YVhabU=wXW zb4g3h#*k7lV!Y>p>ZF$dyNC&}YrYrSi{?^;H0q_89!9VCPf!HW6c`lwN;ZTnCZ%A% zBNee54IE0)6FT!g!CXg{dE^%WfNB#=&x)w4v__6v=QD!84_>Y#_}Ie+gWw^9RXOzh(8lraE8kHE zU${kfS+8*U@oCU9np-6oyCY#Fe}kQoz;Wv^5D8^7g5IwWK(lX!QT};2mNTFADK(e z)M}SXIv7PO20owFN;{ir$szng64}EYqE)Q0l6L(HkZ_;OZSPxr_TzDl-4Q;@mrlD_=hbVZ4d1!e3uK_s51o=z);52`Rty{7Wmmq$Xjk)0@A7k{9rWtO=`bZa z8HW{4-14o)0HS4UFBFct{Q!QUFq=H2LM&2$gow@^I|r#dU5HfZAMLCxbwmfL2UBnpPAY~ z7zi%$nyZs>#=lmM7+}Qj1#%d_;3CpeH`{XMeKoSV5#s_;r2EKdq$+u>fP2$-fNCO6 zPQwn$F#JU<`hZgovpMI!j{LRI)HX3hBEIXyul@znUD%8QkLfqIFQ>l;_S*Q9zqf+s z-stE0gT%ssyO9Lu0o-t1ug8Tg4PS<_BYP_b1*M{^rD`*eog)b_b9uwhnn5mZ$Vz1? z-}vWDnSOKy(P2g3<`(VkSY(Rgg}d?o`UDYiqz<*jfte!&70i+Mqm#wzHb#vYelK8! z#&WKx;7xe)@cqYiT}D9TGhUByfyTc$N?Gc)Of|iowwi14$rVLxEeUfY|8OguR(Bso z1`{ zF?X_F3i_`tfp6m|lHGAuqbMc1O}LuaB32`67%zEv;%sk)MFggC{fBq`%9Y;Z?oa~g zYTb^mUIgF`({aJsFj36(y=(l;Kzm&Wk~C?n2oFua!w&p1qis#q%rD8G8grliSJ39a z==cuVDJ15Ta)FC;9*Xvj2+(0Z$pJcnrKQ zoD_!qD9ONpVUK_^6$(8Yi!)Xp12^5eFQhZw6K1@F{H1*JmcAkT8A}xymkR{r6zbwB zrQVTq;Z%&WwDjAkp2|z-XBSMO$gqRiC&MFO`WM9XKoq%hv81`NNΞ`jW8PQLJT7 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

    rvD6wW^Hp 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 literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/docs/images/pea_processing.jpg b/scripts/GinanUI/docs/images/pea_processing.jpg new file mode 100644 index 0000000000000000000000000000000000000000..151665cd1f424a301dfc36775f38a523d2d58d96 GIT binary patch literal 39717 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�`2~>N5pxzMCe-RhiQ|Pz`8KA% zF`kx!PRv(lr}wteW>IjrPEL)&8B5S9M#q2)N5rh47^|HP7mdS*bjbxNcg5$`xklU@ zOC(J3z#!alR>G#}|NF7M&XxDo*s$cIDwO>f-g)EG2 zZS7?b3}_2jC+E%JA`N$miSG<`o*3ZD7Oqdz6a}f&^2`HY8k5NmP7lQI8KQnY&%KH1 zPTUBjN~_@LWp9a=Utc6d%T7`r$eJxuch@pob{TJN z=JDjkxndv?u!&b!z1zP+8Xz}*w4qz_VE`(VTJtp|U>#Y^uU?wjMo>G|hquu5um^pO zpD%EI=bmvXt=--KvOmtj3fX}FHe{7zSz>3CQGof*h2H42qid-`tass5S@Wf@xH(>3d>BuQ~LUe3)NiE#$q>XbUw`26&si2aW4)I0G+r z1GakzKL;Sjy`ksp*#+;&UYh6{-3mFiDcSeSUGMP1gXFIqkYy(ut+>|*isco5k*)Vm zI_%0C_e8{qTH5S5k;}tj$E|4uC=_ zIhU1;(yZ0HncjpS_4-oaSnj^5XyNf`Tg>0Tu{XhoPHci^W%RM+XK=9)CzX$I-+k*W zfPDLxF9+qRO(oeh5lLNIpX|KqpHETq#N#PU#N?=s>TfEbBEYWg@9I-CFXw)%=GpTI zH?;_I?JLK@5%BdPH-58TgfkISro0O$wY{ZmH$<_ntzMJTFG6^(yZPzbzyA7UG5Nv-#AseTqXFS!X=Mwv#EIaAT=iZ& zWYo|09C7qGzcDE0ZpI+}5KrVsTf_fHXA*&JfjOK$*qj_O_P*`d+1Q_EZeZF#t661> zo%ufs1uLvDNscHm*%b$-JG{)0?%s`jU!CCdxGS>I&5Qd;>9iO>o_)&&nD-27mTscm zGa)Pd6>D-&isL9I93IGPyQOIMuoEeArNp4@E0Ty;l~@H^&5f#<&78{d34`ztCD?mA0wt{c zhP88kZUzLX1$E#Ji+O!{4@9c_nLyX#pF3%))_rK?Ot%rEkd;zxe0z)K$_Nb0YXciu z-?XLT%^uWWx5!3w;rX+ZjD|?AMnHnFX0k0ed;=^Nj5WuzMt&Er%JMvxENv@EVVo-} z<}ws*UcMIL&CPQ=d4~-mYmX#v^XrRVM}TM7+w90L#j3uNN+VSueinKb-s_D^LJBrG z%rGdd)nWYL3uad8&We)1jWAmS_n) z9O23#fnt+4W=~{mD3b>%0BS;i9w=WN;`thQ>9M$Efjh>bb;w*oyM3$~wk zqzXAQhxLJF1G>EN#2lrFXgwPmnSB(sG4k=+dOCFfoi)P?n9?h$zGqbNmr{ez|HqQs z<2r~v^fY16=!C#S8*tDthXR*@J##;ip<^hu!eHwYu%jM2$%2FQt$13c!1I)iX zHUr#%u6g6>iYGU&1yzs9^F8gEm}G5{v`W!2A)mjifquBGb*eX+r+|jD%j|Xf*&JM~>8iJ< zOeY9F&MP^ZF*x}@jV&xi@x(dqI0u>2ULI>Pm-?*^H()dyV||z4v zEs~UbCrqwHeTQjPf2;d@J7#EfG#oig0ysMq!g3`|S;2t<*~UR&CC;LFyw%pp~>ND?@-)x!m)p zOG|MW`bA=g2cknvS1t-XnrB;*!_)qH?La|tky~k!9Ul~=BE}H+Qvj{o;q?tziR5!=?SkDFf5`|a99OU~=tI?**Lq%AOQf|q)b31Ws~eKav|E~u4*eJ>tt;MEaqGC^s<@(xPg~M8~dma*4S8H~#J-xYm!^LFJTFRO!2ir;w z$w{xvVmjXb7k`=h#YA~MN++=3V)VDeaxR9ai4Ly~n~L5253waU>ImlQQqYPQ`z7Ex zPM^@a*!B=ZTb`ct`Mk*0<_t}{fHaX>!B8D#-^~Zqg9gsqL((_Zus1ZScyP;N^RZ4v{duBKp8t*>{!jB7h^hGIK-A z3LHVzO10~$9LHZ!wWouz;ER;D_fFyg$sTDUcs|v}y|i4Uz8E`h*_n5uSz8PI$P&Z#>E{k0JDwQu zL9IB{+`Q&AKSAWioF|p^Rf$9{7DB0Qz_i=jsUby+F;U+Luz+J*-_h?8Vv#vMigpYs z_&-hsLD4_k@MRqqY#Xi;ZGZFMZTQg#Qbxskc(eB$c~oZ(`nl1mC(HjHwsCtCvXHN4 zl2_eDSqitywZQ^JsM2B_y6#fRM7`mRJeHK(*Op-dZWH{kMFZ1UlD-!+zN`J)YESUz z$NxmB413f-ryM=H*-x^vt&x%RBNEJN^@YKdC@&+!kjd|m_dfU@jv%aR z{%@P$xgZOk6Ly=Dho?d2n1C$4S*HJU*p&Yl4Pk%F;lNYcbtM>0%d0}O2L8<66D_M7 zW$vC9Zq#?A}A`y*ty_I|$=^JpF}KKDoOs?#|XGzzhie z0IshLXjm{yVR@tADQ|Io1!k9R0~+#$1rOAMqbQ$y+Wl$`9AI>=CfSvoo?0m4N`;S( zKviy9t%1_Ni8RWYuHtVx1mqZ7=+*2bks8AjInI}F`%a}A6Q$%iXW{de^f4K0PvDfjlQyFainu-R|&hMrH=TJv{nmroy=q0ylLA&kGcWrq7OD zsI?E*#e_FtysYgffv@PML!@k)wy58Ii@&~PVseG|iu8V9BU^ni+#vSn?X_%ms2n&a z^my*mP>{#v?*kY2wb$0Q$D&fYYU>qbqM@wrZZvcuch7@zMD+e2INxu;rxl>1&c z$bA0b!ITu78*A!1lRr+bo8}_eI1-xD<|SsDbNdAWe*5;NyV=QJ4h{RJ{9SofLE=>C ziX2k65?SAKn=ed_L)3q%z&~m)9QbrdAUomG38Rxljo%5ie@yHIM5{W= z`qc^j^^K0Uo&CV_47tt3d#*(?wxhAA5y>7o{jrif;oI{LWGkbSz0>w$?58Vr`xqX_ z^Sdy@q{BKNOOYc3K$!>d3w@Yd(VCYO92Z~LP$i`5`yur1bwxsPO-wNK>WSrm^(nX$ zB_kaS$Ha343O0R~k2XT!kX?h)o{v9;>}Tf7UhB5>Sx-Mzsl@!!p=GWyE3EN7S{>KE z6+OnJF>UQD@s{*Jdi?~bs=C6Ia;eZx?cl>dC#}CLvw6wKrVb|(S!il+zCMx5+2n?T zt|w>ETcIQSI{gvF7i1QCuBz45bK+H4x{^odHdU*fK26T`yXpM%!w~VB&LU(F`gl!v z>qtV4hf$nhM@|ml%UaHy4Sc<3@kS%lcJ99EcU&s9Q+F+0J@Q=DL>$`#%i1}w; zSY-EcTHf~vbjYyybd-Ak-FM<#R$x?puSEi>O>JZVh3SK(LVClOn`zmCf(O;g#k8x##Gph_&K23oF{Gu76>hkMBt8Q{`s&c?>9C#)vw&6;`T|>qn;~fOl4KeN^ ztRlzbB;TA;tqW>c0~okY$jH<4^#?(lNSz7wud@)8#pAmeCP-BwDVF7N|pDsa9VZyt7 zC9LQhJ+#;JfXSf>?+p{mL(7GXKO%?|5)aE_J8kS<_xG*TBTa4a#9WLm1?OK3c`8G* zCQ5#gNXM6Li3UOZZ8$sM$!MIQmm6P?4y?2zPE`riz)4dymCe_tpbTGly!*vD-rc+< znrbjpiJO4da}DtZFSI2Rx!3IQ)h=6)_!=V_B@ksRq_i6xCKX{VVJ}`E_{MYX`%h#n zvEHuvliAQ-fxhF0@Pl`cCH+euqZ0r2HOc;kE#TQxVOU!0OZm`nPkkSbM;) zq}B%C?b4~E)d4*r-|BSban@R3z_P_p zyMV5E_9W0t;f&btS4Q0rXEXp?t?Kf_Eb@5PBLG}v_Qn%$-`iu{d@a-IckU%(r`&MO zNhFYvznRVjX4@~4hCC67wl|nd1eIWXeAB@gn{6= zIGdpg2T}bx{KjGMsp##9M(>uT1VArx6w?6}p<(>nKzTIV$&>g*P0TD}vEIjCqH*i> z%hq;UsWauVPV5W68;mo<_+-~;$vu+7CnzeU`vyO9x;^rGwWnv*(u9`O%tKe8w*Mq1 z%NsLR7qT0cg&Rctqbd?mO)3;O*otgAAtA5bP>376@wq1)!m<+%)N``))cnTlboY8A z>a;)E!@fvK=>rH$VgBHzUTb4CyxLECK73<0u-w^x%kDPC>Rl4oax{s~IB_(zaHuXZgQnztc?J%eko^ zN(NnqeeldF^7ZeZ8|p6bMQP)Y(OzdyytgLBS2w0!4QJ;HP?6+^a1XjQIBt9NYr~u| z%j4Y(0zAIwsNX)WToQVB_$&0b#_L|6dpL_kuTka=IwP4ZZ(mwpGfUL#k#B`LkZpx4 z#{Z}rBNWO`>HA7pUEd7BS;OMZig)hZd(AHK63$>IDe2;a`u8A1%|i|-6=Tw)Vjw+l zg4vi#sjcR|?Y^*!{9P8RNkB9}yH9zeZFWlx;LzkC8|MTn@ zBLvljag#Yv^m3+cU?mlQ_q|Z(0=ngooIn=u0!ECr6WuEz_DO@uuD8KGy6c2o)~~cQ z9N`U~3IoH#E_0kbv z%`&sgb%EBA;W`o_@b2ISRtd|YT90Oy&@F#<%GAb4U)Z(hV^~CmDc3%5 zXss#H2H+Xx&Ip3lL~!LZ(=N!|ZfJK9$@f;DbV)LHI!QuSZd#PHxRz$WoCyL5d_AHm zakdgU<5+q7djqe{fi=+&7>rfz12S$7HC}#IHJjZV|N6TNBL0@~Y?g7%vnxAe5xf+f zhVVvjKg3Wv*5?!WwRy&aw#bv!#v^l>0-C-ZneoQb6G=u23%(ED*lAr*`F}gYipeQIX#+gypyOKpbK48?#8{sR zeq8X~FLp)_b`Q9?opbb|b^4`~#LLqbSQ~ZOM7o9j+4*{@7+DjE=_mS{w%jf7TGuF= zd^%%(lTA%`b|5ee^FE*UW;ubDytPP)vcXwCE~8hpzxr-U&zY`7%7{EIAIEZD{7?b+T5CGV*4DAXKT z-6pc$T>xxTJzVRClrhZ1WU#k6(%nzpu}T@7o70wJ=smH2x!@k>_nX3WJ>Tm%PVUZO zlDp~p!JGVREQ<#jzz$sZSk4}KERuR)UjAjqhX^f|tl-cNw|3ij*1?8h){Uc4iM!#4 z2voRxuwid9Zo9bCOW{*89( z5$deOkKFAS)oW<6xILmJay_Talg^QLNQCHUdsqM3>%jL{ClH?(jzqD=LsRn&ZFKm? zlz-2JpOVa{sk!#tt#xoEq1&KN+$68~kl(iJ^6+Tld&_B_& z{SE52OVG4$kdrYRL5Ohtc3Yw(<8eHQ`@F5;rrz`VBe{WR$qVG~SKVjJ7km;!ox?=~ zOwdi{Y8S~f1w5?$VNuhfu_| zH}+qgTybZ~i6?NXG8E4AFd#WMB(_g9xnc`J6&{N_OKmuzNxxUiMff&{F8Be(ri4kp z^7I!!sJSE3xntP7%I$@lVQMZBjelt`Ku*gGi$( zCRM3=ve`<`Zd^>(e^*6rkT^4Qd)&B6$kB}*Tiq4f{xloKHA$5UY}=iA_02e5EFj#? ztIY6otEL9{Br0F`yo}!Oz@b_9^Mc7&6+FS>n?^hr&`NSF%K_t+&8TK5p z>a<5ThTWMFW9Ub@yI7f?a%>FYrtrl%Yt2o3S{7L&p6R(5G-zg`S0yVtr@sz&aGJUc z8K*%b(WudaFyHan%(!MK9G3SucSXHfJXiET2^QWnBpikZJ_047S{G{lK+F$XMwyU6q*8j)!`U7mOHT+KZt8V#cW?+ zIF|+p$4DCnML&SW0tjkv$v1uzW=>uA80#wYn4%Hhsg0+7*JLxixR`L$Z0Nhf&%M-4 zPs#ix{bn-jUM%;DzZj~R0IpB_xo?Oye?@j?O%ydzS98Lq@6Immf19N^alSoik+;Nh zQSF8!0W7$h->LLuL1TvS>8BiRRN@Kq*;8{L^0s5Xr=$!Cd5QCrtIq#9r?JEGLA5KU z^DAXDFU81+!3M`wBl~l5BPNG<%T-P(^{5iLv2CX^x-?bCoBh7%$XtWb8XV6=L!pp$5oi(3c;`fO-^;2=km9cFWD9^ zL}zs+k5}hD%IvR(|LAJK0bS*(Hb1lodNSr#%K}uojK5d{i^0^WE9a@9p#Ur_D`=-R zLiqZ!@1TWoVYT@sfA4SNnUpIfG*C4j|L>}?T!6#a876&kKq_fT`w{+=LSs#ZLKCzu zXY5oDJ0R4&<-*NkenO{^v{IhbOu2#I@L6nmgU0Xi-otv`o1V1~MoEGGg=NW3*;UWE zUYojt4?`El2RE2b@*}EZdZ%J;0ByWm^}S!l9xBI+u;ZV}@$O1-&i;o5(RGOE|TYfwlKcu#msuDx#O~@OtgQis05^4)L2}XNkrgi6LmX@|WLN7rY zUAf)tXUPpXm>yz}H~o?i!+>3oG9Jh7=<`RviR(4UuXCCVNft=VaAD|Rsc*#(=LvPy zb+OxrWu+>I>;BDQMe8!15kGh|VZZkJk4CkR=u$J{GSZQ5OYY>_PQQW05 z=f`UYXRRuF;X)`_5ER_TRq4F|>Fi?lo?)~TF|6zkZ<`fzu zgdHC4H_%@w@k(KB)QNr1Pc9O29`EUAEU^JRm3ko&G$P_2^ETS2^woHacHBbAj`Sts`)a-o5$==Arqa8wm<^FEbM9)Voj#~DBrzA4#@6A=;WP{3u{T~Nn{JW6eJ3Sn zoj@jGF+@gLr69qS1v~bL2{>32E<=ZO2}HXohaD{Uz~uWz+Mqj*zOlN!Gj4pRpKv8Y zkHVx5Ly}?cIcUj2t;TWMdXH)daq{p%u9-W(jP&Adog03k9;`yOyUtkj=J`QtAg^<* zT+Oyt9<{`|&LldTKV6kHfC@c8{rd)azglzu^1+(!g8BisCwqeaOdO?Lm9OAw)Q@&>j~_GytTb}5TgTJHn-uV1VnQ@QCz^U z!#1Hr8r7tI7sA3H#YE#wBipsP5A3@pmK~+2s=^3K47n0lU78&fgsaMKFhGk1(|I@U zsnu0I8Xgva0jtI4@vPd2O-Qkc3n2|jP0NCwOgtA(MC^6Gy4iaPOZJ|wM#+z*9fRzg zvf3{XH%XsxO9-%srVPL2yeqsIx4czXSv))k7RL-7+=nbw^jcINGQ&(hozjE zDB%4$jL?ttgK9@%*8%m{q0#N#Zc?hab=7@AQNcFx7O<{5XZs!=BZ#T_06gtcW8ITh zLADTuA<4+!E{aH=hREE|6-MsYbyQH~MR_=Wq)5xVjt}U*zQyVm4*Qv0*T4kKBTw;g zKeU8{B3yV3Ct2*D#KS?k)(Vc1?O{ipH1q@yyYYaTR+mGc-sS5&ql%I4Sgerz>y#yo zieVQ4tn;`DPwW*_2tNnZheK^95H66-Y;iu16WJ&;&kE7&dLMo?Fo!b zq@+#1&ex5(-4&jRQtOib$dmK(q zp#*-o`3B@Nra%&Ay4Zu+E10Y9=>YNR@K>|#cyGMF*>iw!iR!zLQ<_zXtm0yO;hRaH7b#16a=Vau93<9DLAPfY zZqUPIb;OJlC|DSSw>vm2^l@gBKS*Y?pCp1WPj!R%b8t<>E2_5F#tZoqqgpZa&oPSU zVuMt>Dn%0xiY@E?_oI(q%nus={)UMdv?k6)xzx+G!X2KSl3teReN^I+t1fRb=UosD z(5d9YU2OGuGUh0bkx%KS!{e|GgzC2F9f>6|#R=Y1z9fmKoeYV%el>E>OEyyC4&ky= z5@Z?6_UY9OeNsDgayacn^>E(bHLQnTX8Bvzd0*ir@}KQ>30uCvV+{W;2wr`RX*Mv@ zuded~w+F6U^d}|g7zOm#EK_hNDWGvEVEEn9C|B8z-ljPe$8C;siFTH;D4uq#tohWb;=`j@c&uRl{Zp^AIt_8yLPd}uhGKg0Vb3;g}OE4BOauucmh3~GG zrM-OKBf{QdRr(;~yV8A6#D4%R(JfY64VMhhLHC5LYLHw%Ce6=#J9?DBABf>8rVmYL zT3*;g>RQG-#|Z^e#mS$o&yBD5>mwd#x8yGdHSThn%D z-(`zc_$pSchmt+yu$`M3>SK1sQlh%eb2r6MbQ5hk3w4U|q6_dST6=di3+|u2ajDTN zd%vHayOdz7!o38?9GWd`QaYEXx^OojbbYh?LHmS!{K&EYqx%n=jK~|J*96OzDdC8fT$EHYpT$VOvbFFgenK=eonalFDzX zx5_@DTf{&lCV+?uIv^Hj<_<|8Uo_tIj;j#lTYt>&byz*&O5!)8h>q0G=!el z8{NMn5~lE-SXF@4c%cu+^P~Rs*5brDwaQ^+bN~=gz4xG;&L-;a%1q$qX(r+s%f| z*F6e~g41AV-7oFCwB>Of@>~sR?v%I9?~m9zOCg802UW^F15|E@#pBw)nZt|M+-tTarW0m&lDMmZwvYaG2 z821XF{sj7Aa*-*rT+YHbBX9x1h_if=UT^~a3h8S8yxJw?&VmiGHFq%4JiH!q*y#`L zR;%*08C&X+yCrw%+TCv|M#{qeCd7Iz@BpUULadmx^MRhBQnec@rf~bMV0@dnAzgah z(1UvO;EZf-L1u3`s)#%FS|&Tzxxv}5v3C;dyWYGikqIWEq#;x8bLBhusuX_vc|nVc%7rl5)!N5Wx{PDAq@a4$e+T+6G67lugsS-t812A&^xX7 z0H~+D?X%2##0LHztWL|r{iDadu$_=)CT=*v`bE>5via&DLsjvrgztzIDl!0maiN)Z zx3}aS%?UIhDzDiGpO!{J(bZ6skGD7dazZ_m5~a(B#UJ?mZy7UxH5QgWXN<8zTUZpf z_g6|i?38`FGC=&{T)i&o=Lf#Qr(e(Jgh9VIiXHh5%9t@PrdI(@G&med%gy{@ROqH7 zOA{V0VtesR+^lxg75Y}#P4-kP^5BS#`ieuVcxhOuMG_im1mYjf6e7DRKg3R)=Y)P@ z{6tVhd_BU(rNu&Lr}!fEQjl@;Zg;T?&mDks?MRa9l>B?Sy5G(B!peX&QyJ;t?54o~ zj`nDpEMnxzC^aK0W4q@-VYuCt_e{BqYPND0=3Jxlf42ylL>DeuD&fNbQQ3j$;uc#I zxCFg;IR#scsioc74)gqSv$a7g58WMLal^6fc?$BR{!Mms%VWKak7ELbY)wR+t+fZ) zEIL@j^NaI)O}Sh*aJl)T7Ez~LvjUx;8-#zVkGh9#HAPEF7;JAh7jtaK zc?|%2cimKrKoYQ?$>+n|@$^?$*Uj}5lYK{5g(r>5S3{!cPTF2qStiBP0meMO7A((4 zr-Q?bfq~>&YlkmEeH&XCQV@cv=;j-y$otE1l$Kt-D^K!BcCkIvY$q_4O>L_u?RPy3O>jafqVp z7cw4RVOuv_9@4RP7#^_U=Z3BEe#ZnEPO?3ixv#(26b6lDpV$1zr_i!jMH?63^ZQN0 z?I$Sn)oSZiwS4n~;m(GX5(@~F0I<0J4ZRs?W4&OMNZK$>=lI4HX1=}LniE@ltiyjR zWpViSydwzNGJe0T#|QT%a}KWqdf|E32L<4pMsKm9dVa03$wM#a?%Z+e@kk1@kE>V; zF&TaG5)Ni2NVl3N13mcRGjsHdPc=%94|`&bvjT>jUblNf8wf0}$_c+^}^Hl;-tN>JmbT5}7na6_ErgHqmfU>WN3rpb?~#Q}b{ z>UqyD`7$}bc^)_A`&>h2YR=U}ygn^gv>AVFdWfC#1X3O3?=s{v7hgu!+0GMO^r?9k zbq)1&)tc?}nxYl&f1{{D1&}Xak=7I1K76K8_9cupM}G0^hvmRKCgNg%J?i=4dspvU zMz9FvMKf3=jK2h)UxYKR%GgL}L%f>25VP8BX!g98l8+R0k>{Eu7UNsTZjO!?-3MYl zX=%9XFccj2UV!$EHP&c!XIqAKjk!P|#@1!QSENYSC)tg)_|8w?zg=sAH+5FgzduGC z3jQ8retpD^!vCbsWNK1v0dg@IH#Fq-qn6)%4nU2y|7QN-X;%btx7>V6y4GsO0{Q(l ze75`T`jC?4=bxqL{#`DWDa_KXyK~2incsb;U@sLv6a0DE$Wr&S=Be)Q?iIf*#uT7<;M=g_q$9FrkIoiZRVGy$13Ry`@m*}7aCinDAzha#FD zc*KzT#$m(jGxmb119!>7U0nNP+BI|##6MMd04uCc?3W`kzONp7rWb9Ri(z?@(|&ic zU4$6JHEMLXv;lRT(E5L7bNi>y#pT9RRf7yJn;7P7dn2O!am_rNp32a;lvlo~=ig=C z1nx^*|NX4_{Y#78`5A!gv+M2jex0~c|3ZC=QjuVxg@s|1yNip8`DYIA|ivro5S%x~{UhQMg`3_5E_0=*nMyGtRsfuVuJ+dRryG;k(m^e-`me z9Sr~Y+&ia2pDUwTyk3t zxbCSExVB7t?gZ3TZrsPVWgMKZ9d=<+W-tfuQG+iDz}*?Z9U@3;+;)Wl*T@0EpTk9L zM{@4%X}xgag3{XHB_^y4tB>_QeQ|56OaA@1OK*V-#CEFi-rTy{6<$0sfLr$-XuRYC29Sr?`s2};LSdWVCIEM< zg98pkP-WlY6ZuU=PrY~!GaUF(u_w$k+yfCtK~sb~w(4-ZdxnFyxkA8`pwvk9KY<9`CNOz}DY`!ncTVt{$qU7zf}!n*}DUkkAK_ z;ul^932PwRRH@~a#RS~m177V1A}sw~J$(yK-ACSz`RDUL`K?ziwr_v!P{05Lp00i_ I>zopr0FiZb6aWAK literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/docs/images/plot_visualisation_web.jpg b/scripts/GinanUI/docs/images/plot_visualisation_web.jpg new file mode 100644 index 0000000000000000000000000000000000000000..291bb5691be931698e0f7b1bba5f0f61b41a5003 GIT binary patch literal 244808 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

    L8^ot>3w5zkoH*md=~6BBjz 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

    lIa`PQ|y}>u@JrH;N*F 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

    thT?vf@5(G5PmWo?{2z+zT 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%0A^s!{dcz}m(6f|&Nq3&J9LRxDFv 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

  4. =B{kWn023IIIXe+or}ImLdq0Htyh<#qh) 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 literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/docs/images/process_button.jpg b/scripts/GinanUI/docs/images/process_button.jpg new file mode 100644 index 0000000000000000000000000000000000000000..686ac3319ea696194b2ca6433e76631f3afec2c8 GIT binary patch literal 219748 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 DIE>&q literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/docs/images/product_downloading.jpg b/scripts/GinanUI/docs/images/product_downloading.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d4622ae6f172615600f5f5266337d33524532871 GIT binary patch literal 218439 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�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

    "; + string nowStr = ""; + boost::posix_time::ptime nowptime = boost::posix_time::second_clock::local_time(); + replaceTimes(todayStr, nowptime); + replaceTimes(nowStr, nowptime); + + string commitDateStr = ginanCommitDate().substr(0, 10); + boost::erase_all(commitDateStr, "-"); + + char* home = std::getenv("HOME"); + + bool repeat = true; + + while (repeat) + { + repeat = false; + + for (auto& [alias, value] : userAliases) + { + repeat |= replaceString(str, alias, value); + } + + repeat |= replaceString(str, "", acsConfig.sat_data_root); + repeat |= replaceString(str, "", acsConfig.gnss_obs_root); + repeat |= replaceString(str, "", acsConfig.pseudo_obs_root); + repeat |= replaceString(str, "", acsConfig.rtcm_inputs_root); + repeat |= replaceString(str, "", acsConfig.sisnet_inputs_root); + repeat |= replaceString(str, "", acsConfig.outputs_root); + repeat |= replaceString(str, "", acsConfig.root_stream_url); + repeat |= replaceString(str, "", ginanCommitHash()); + repeat |= replaceString(str, "", commitDateStr); + repeat |= replaceString(str, "", ginanBranchName()); + repeat |= replaceString(str, "", todayStr); + repeat |= replaceString(str, "", nowStr); + repeat |= replaceString(str, "", acsConfig.analysis_agency); + repeat |= replaceString(str, "", acsConfig.analysis_software.substr(0, 3)); + repeat |= replaceString(str, "", acsConfig.inputs_root); + repeat |= replaceString(str, "", acsConfig.trace_directory); + repeat |= replaceString(str, "", acsConfig.bias_sinex_directory); + repeat |= replaceString(str, "", acsConfig.clocks_directory); + repeat |= + replaceString(str, "", acsConfig.decoded_rtcm_json_directory); + repeat |= + replaceString(str, "", acsConfig.encoded_rtcm_json_directory); + repeat |= replaceString(str, "", acsConfig.erp_directory); + repeat |= replaceString(str, "", acsConfig.ionex_directory); + repeat |= replaceString(str, "", acsConfig.ionstec_directory); + repeat |= replaceString(str, "", acsConfig.sinex_directory); + repeat |= replaceString(str, "", acsConfig.log_directory); + repeat |= replaceString(str, "", acsConfig.gpx_directory); + repeat |= replaceString(str, "", acsConfig.pos_directory); + repeat |= replaceString(str, "", acsConfig.spp_directory); + repeat |= replaceString(str, "", acsConfig.ntrip_log_directory); + repeat |= replaceString( + str, + "", + acsConfig.network_statistics_json_directory + ); + repeat |= replaceString(str, "", acsConfig.sp3_directory); + repeat |= replaceString(str, "", acsConfig.orbit_ics_directory); + repeat |= replaceString(str, "", acsConfig.orbex_directory); + repeat |= replaceString(str, "", acsConfig.cost_directory); + repeat |= replaceString(str, "", acsConfig.rinex_nav_directory); + repeat |= replaceString(str, "", acsConfig.rinex_obs_directory); + repeat |= replaceString(str, "", acsConfig.rtcm_nav_directory); + repeat |= replaceString(str, "", acsConfig.rtcm_obs_directory); + repeat |= replaceString(str, "", acsConfig.raw_custom_directory); + repeat |= replaceString(str, "", acsConfig.raw_ubx_directory); + repeat |= replaceString(str, "", acsConfig.slr_obs_directory); + repeat |= replaceString(str, "", acsConfig.trop_sinex_directory); + repeat |= replaceString(str, "", acsConfig.ems_directory); + repeat |= replaceString(str, "", acsConfig.pppOpts.rts_directory); + repeat |= replaceString(str, "", acsConfig.stream_user); + repeat |= replaceString(str, "", acsConfig.stream_pass); + repeat |= replaceString(str, "", acsConfig.config_description); + repeat |= replaceString(str, "
    ", acsConfig.config_details); + repeat |= replaceString(str, "", std::filesystem::current_path().string()); + repeat |= replaceString(str, "", std::to_string(getpid())); + if (home) + repeat |= replaceString(str, "~", home); + } } -void replaceTags( - vector& strs) +void replaceTags(vector& strs) { - for (auto& str : strs) - { - replaceTags(str); - } + for (auto& str : strs) + { + replaceTags(str); + } } -void replaceTags( - map>& strs) +void replaceTags(map>& strs) { - for (auto& [id, str] : strs) - { - replaceTags(str); - } + for (auto& [id, str] : strs) + { + replaceTags(str); + } } -bool checkGlob( - string str1, - string str2) -{ - vector tokens; - - std::stringstream strstream(str1); - string bit; - while (getline(strstream, bit, '*')) - { - tokens.push_back(bit); - } - - bool first = true; - int start = 0; - for (auto& token : tokens) - { - auto pos = str2.find(token, start); - - if ( first - &&pos != 0) - { - return false; - } - else if (pos == string::npos) - { - return false; - } - - start = pos + token.size(); - first = false; - } - - if (tokens.back() == "") - { - return true; - } - - int strlen = str2.size(); - if (start != strlen) - { - return false; - } - return true; +bool checkGlob(string str1, string str2) +{ + vector tokens; + + std::stringstream strstream(str1); + string bit; + while (getline(strstream, bit, '*')) + { + tokens.push_back(bit); + } + + bool first = true; + int start = 0; + for (auto& token : tokens) + { + auto pos = str2.find(token, start); + + if (first && pos != 0) + { + return false; + } + else if (pos == string::npos) + { + return false; + } + + start = pos + token.size(); + first = false; + } + + if (tokens.back() == "") + { + return true; + } + + int strlen = str2.size(); + if (start != strlen) + { + return false; + } + return true; } -void globber( - vector& files) +void globber(vector& files) { - vector newFiles; + vector newFiles; - for (auto& fileName : files) - { - if (fileName.find('*') == string::npos) - { - newFiles.push_back(fileName); - continue; - } + for (auto& fileName : files) + { + if (fileName.find('*') == string::npos) + { + newFiles.push_back(fileName); + continue; + } - std::filesystem::path filePath(fileName); - std::filesystem::path searchDir = filePath.parent_path(); - string searchGlob = filePath.filename().string(); + std::filesystem::path filePath(fileName); + std::filesystem::path searchDir = filePath.parent_path(); + string searchGlob = filePath.filename().string(); - if (std::filesystem::is_directory(searchDir) == false) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Invalid input directory " - << searchDir; + if (std::filesystem::is_directory(searchDir) == false) + { + BOOST_LOG_TRIVIAL(error) << "Invalid input directory " << searchDir; - continue; - } + continue; + } - vector globFiles; + vector globFiles; - for (auto dir_file : std::filesystem::directory_iterator(searchDir)) - { - // Skip if not a file - if (std::filesystem::is_regular_file(dir_file) == false) - continue; + for (auto dir_file : std::filesystem::directory_iterator(searchDir)) + { + // Skip if not a file + if (std::filesystem::is_regular_file(dir_file) == false) + continue; - string dir_fileName = dir_file.path().filename().string(); + string dir_fileName = dir_file.path().filename().string(); - if (checkGlob(searchGlob, dir_fileName)) - { - globFiles.push_back(dir_file.path().string()); - } - } + if (checkGlob(searchGlob, dir_fileName)) + { + globFiles.push_back(dir_file.path().string()); + } + } - std::sort(globFiles.begin(), globFiles.end()); + std::sort(globFiles.begin(), globFiles.end()); - newFiles.insert(newFiles.end(), globFiles.begin(), globFiles.end()); - } + newFiles.insert(newFiles.end(), globFiles.begin(), globFiles.end()); + } - files = newFiles; + files = newFiles; } -void globber( - map>& files) +void globber(map>& files) { - for (auto& [id, file] : files) - { - globber(file); - } + for (auto& [id, file] : files) + { + globber(file); + } } -void dumpConfig( - Trace& trace) +void dumpConfig(Trace& trace) { - for (auto& filename : acsConfig.includedFilenames) - { - Block block(trace, (string)"FILE/RAW_CONFIG " + filename); - - std::ifstream config(filename); - - string str; - while (std::getline(config, str)) - { - trace << str << "\n"; - } - } + for (auto& filename : acsConfig.includedFilenames) + { + Block block(trace, (string) "FILE/RAW_CONFIG " + filename); + + std::ifstream config(filename); + + string str; + while (std::getline(config, str)) + { + trace << str << "\n"; + } + } } -string stringify( - string& value) +string stringify(string& value) { - return ((string) "\"") + value + "\""; + return ((string) "\"") + value + "\""; } -template -string stringify( - TYPE value) +template +string stringify(TYPE value) { - std::stringstream ss; - ss << std::boolalpha << value; - - return ss.str(); + // Check if TYPE is an enum + if constexpr (std::is_enum_v) + { + return enum_to_string(value); + } + else + { + std::stringstream ss; + ss << std::boolalpha << value; + return ss.str(); + } } -template -string stringify( - vector vec) +template +string stringify(vector vec) { - string output; - output += "["; - - for (int i = 0; i < vec.size(); i++) - { - output += stringify(vec[i]); - - if (i < vec.size() - 1) - { - output += ", "; - } - } - output += "]"; - return output; + string output; + output += "["; + + for (int i = 0; i < vec.size(); i++) + { + output += stringify(vec[i]); + + if (i < vec.size() - 1) + { + output += ", "; + } + } + output += "]"; + return output; } -string nonNumericStack( - const string& stack, - string& cutstr, - bool colon = true) -{ - string token; - - stringstream ss(stack); - string newStack; - - while (getline(ss, token, ':')) - { - size_t found = token.find_first_not_of("0123456789!@#: "); - if (found != std::string::npos) - { - cutstr = token.substr(0, found); - token = token.substr(found); - newStack += token; - if (colon) - newStack += ":"; - } - } - - return newStack; +string nonNumericStack(const string& stack, string& cutstr, bool colon = true) +{ + string token; + + stringstream ss(stack); + string newStack; + + while (getline(ss, token, ':')) + { + size_t found = token.find_first_not_of("0123456789!@#: "); + if (found != std::string::npos) + { + cutstr = token.substr(0, found); + token = token.substr(found); + newStack += token; + if (colon) + newStack += ":"; + } + } + + return newStack; } /** Helper object to allow push-pop style indentation in outputs */ struct Indentor { - int indentation = 0; - int width; - char indent; - - Indentor( - char c = ' ', - int width = 4) - : width {width}, - indent {c} - { - - } - - Indentor operator++(int) - { - Indentor old = *this; - indentation += width; - return old; - } - - Indentor& operator--() - { - indentation -= width; - return *this; - } - - operator string() const - { - return string(indentation, indent); - } - - friend std::ostream& operator<<(std::ostream& os, const Indentor& dt); + int indentation = 0; + int width; + char indent; + + Indentor(char c = ' ', int width = 4) : width{width}, indent{c} {} + + Indentor operator++(int) + { + Indentor old = *this; + indentation += width; + return old; + } + + Indentor& operator--() + { + indentation -= width; + return *this; + } + + operator string() const { return string(indentation, indent); } + + friend std::ostream& operator<<(std::ostream& os, const Indentor& dt); }; std::ostream& operator<<(std::ostream& os, const Indentor& indentor) { - os << string(indentor.indentation, indentor.indent); - return os; + os << string(indentor.indentation, indentor.indent); + return os; } -/** Helper object to temporarily and automatically disable and reenable a stream when the object goes out of scope +/** Helper object to temporarily and automatically disable and reenable a stream when the object + * goes out of scope */ struct TempStreamDisabler { - std::ostream& stream; - bool disabled = false; - - TempStreamDisabler( - std::ostream& stream) - : stream {stream} - { - - } - - void disable() - { - if (stream.fail() == false) - { - stream.setstate(std::ios_base::failbit); - disabled = true; - } - } - - ~TempStreamDisabler() - { - if (disabled) - stream.clear(); - } + std::ostream& stream; + bool disabled = false; + + TempStreamDisabler(std::ostream& stream) : stream{stream} {} + + void disable() + { + if (stream.fail() == false) + { + stream.setstate(std::ios_base::failbit); + disabled = true; + } + } + + ~TempStreamDisabler() + { + if (disabled) + stream.clear(); + } }; /** Recursive function to output the default values of all siblings with a common root. -*/ -template + */ +template void outputDefaultSiblings( - int level, ///< Level of complexity for outputs - std::ostream& html, ///< Html file stream to output configurator to - std::ostream& md, ///< Markdown file stream to output configurator to - TYPE& it, ///< Iterator over the default values map - Indentor& indentor, ///< Helper to maintain and output indentation for default yaml output - Indentor& htmlIndentor, ///< Helper to maintain and output indentation for internal html output - Indentor& mdIndentor, ///< Helper to maintain and output indentation for markdown output - const string& root = "") ///< Common root to determine extent of siblings relationship -{ - //keep going until the end of the file, or this function returns due to the next iterator not being a sibling - while (it != acsConfig.yamlDefaults.end()) - { - auto& [itStack, defaults] = *it; - - // Check the name of this parameter against the root - bool itIsSibling = (itStack.substr(0, root.length()) == root); - - // Exit this level of recursion once a non-sibling is found - if (itIsSibling == false) - { - return; - } - - //do this one, and bump the iterator - { - TempStreamDisabler disableCout (std::cout); - TempStreamDisabler disableMd (md); - - auto& [stack, defaultVals] = *it; - auto& defaultVal = defaultVals.defaultValue; - auto& type = defaultVals.typeName; - auto& comment = defaultVals.comment; - - it++; - - //check if it has children - if the iterator allows it - bool nextIsChild = false; - if (it != acsConfig.yamlDefaults.end()) - { - auto& [nextStack, dummy] = *it; - - nextIsChild = (nextStack.substr(0, stack.length()) == stack); - } - - //split the name into tokens so that ordering numerals can be removed - size_t pos_start = 0; - size_t pos_end; - - string token; - string flatStack; - int optionLevel = 4; - //find each part of the stack for this entry and make a list of them - while ((pos_end = stack.find(":", pos_start)) != string::npos) - { - token = stack.substr(pos_start, pos_end - pos_start); - pos_start = pos_end + 1; - string cutstr; - token = nonNumericStack(token, cutstr); - flatStack += token; - - if (cutstr.find('!') != string::npos) optionLevel = 1; - else if (cutstr.find('@') != string::npos) optionLevel = 2; - else if (cutstr.find('#') != string::npos) optionLevel = 3; - - if (optionLevel > level) - { - disableCout .disable(); - disableMd .disable(); - } - } - - //output the boilerplate of the name, and comment up to the point where the children are nested - tracepdeex(0, std::cout, "\n%s%s\t%-30s", ((string)indentor).c_str(), token.c_str(), (defaultVal).c_str()); - - html << "\n" << htmlIndentor++ << "
    "; - html << "\n" << htmlIndentor << ""; - html << "\n" << htmlIndentor++ << "
    " - << (nextIsChild ? "" : "") << token - << (nextIsChild ? " ⯆" : ""); - - mdIndentor++; - if (mdIndentor.indentation == 2) - { - md << "\n" << mdIndentor << " " << token << "\n"; - } - - string link; - - if (defaults.enumName.empty() == false) - { - string linkName = defaults.enumName; - - if (defaultVal.find('[') == string::npos) link += "[`" + linkName + "`]"; - else link += "[`[" + linkName + "]`]"; - - link += "(#" + boost::algorithm::to_lower_copy(linkName) + ") "; - - } - - md << "\n" << "###### **`" << flatStack << "`**"; - - md << "\n" << " " << link << "`" << defaultVal << " `" << "\n"; - - if (comment.empty() == false) - { - std::cout << "\t# " << comment.substr(0, comment.find('.')); - html << "\n" << htmlIndentor << "# " << comment << ""; - - auto period = comment.find('.'); - md << "\n" << "\n"; - - if (nextIsChild) - md << "> "; - - md << comment;//.substr(0, period); -// if ( period != string::npos -// &&period + 2 < comment.size()) -// { -// md << "\n" << "\n" << comment.substr(period + 2); -// } - } - - html << "\n" << -- htmlIndentor << "
    "; - - - md << "\n" << "\n" << "---" << "\n"; - - // - bool firstChild = false; - if (nextIsChild) - { - if (firstChild == false) - { - //initiate the section for embedding children nodes - firstChild = true; - - html << "\n" << htmlIndentor++ << "
    "; - } - - // recurse to do children of this node - indentor++; - outputDefaultSiblings(level, html, md, it, indentor, htmlIndentor, mdIndentor, stack); - --indentor; - } - - if (firstChild) - { - //finalise the child section - html << "\n" << -- htmlIndentor << "
    "; - } - else - { - //this has no children, output the default value of this parameter instead - according to its commented parameter type - - for (auto once : {1}) - { - //booleans - if (type == typeid(bool).name()) - { - - html << "\n" << htmlIndentor++ << ""; - break; - } - - auto begin = comment.find('{'); - auto end = comment.find('}', begin); - - //enums - if ( begin != string::npos - &&end != string::npos) - { - string enums = comment.substr(begin + 1, end - begin - 1); - size_t pos_start = 0; - size_t pos_end; - - html << "\n" << htmlIndentor++ << ""; - - break; - } - - //general parameters - { - html << "\n" << htmlIndentor << ""; - } - - } - } - - html << "\n" << --htmlIndentor << "
    "; - - --mdIndentor; - } - } + int level, ///< Level of complexity for outputs + std::ostream& html, ///< Html file stream to output configurator to + std::ostream& md, ///< Markdown file stream to output configurator to + TYPE& it, ///< Iterator over the default values map + Indentor& indentor, ///< Helper to maintain and output indentation for default yaml output + Indentor& htmlIndentor, ///< Helper to maintain and output indentation for internal html output + Indentor& mdIndentor, ///< Helper to maintain and output indentation for markdown output + const string& root = "" ///< Common root to determine extent of siblings relationship +) +{ + // keep going until the end of the file, or this function returns due to the next iterator not + // being a sibling + while (it != acsConfig.yamlDefaults.end()) + { + auto& [itStack, defaults] = *it; + + // Check the name of this parameter against the root + bool itIsSibling = (itStack.substr(0, root.length()) == root); + + // Exit this level of recursion once a non-sibling is found + if (itIsSibling == false) + { + return; + } + + // do this one, and bump the iterator + { + TempStreamDisabler disableCout(std::cout); + TempStreamDisabler disableMd(md); + + auto& [stack, defaultVals] = *it; + auto& defaultVal = defaultVals.defaultValue; + auto& type = defaultVals.typeName; + auto& comment = defaultVals.comment; + + it++; + + // check if it has children - if the iterator allows it + bool nextIsChild = false; + if (it != acsConfig.yamlDefaults.end()) + { + auto& [nextStack, dummy] = *it; + + nextIsChild = (nextStack.substr(0, stack.length()) == stack); + } + + // split the name into tokens so that ordering numerals can be removed + size_t pos_start = 0; + size_t pos_end; + + string token; + string flatStack; + int optionLevel = 4; + // find each part of the stack for this entry and make a list of them + while ((pos_end = stack.find(":", pos_start)) != string::npos) + { + token = stack.substr(pos_start, pos_end - pos_start); + pos_start = pos_end + 1; + string cutstr; + token = nonNumericStack(token, cutstr); + flatStack += token; + + if (cutstr.find('!') != string::npos) + optionLevel = 1; + else if (cutstr.find('@') != string::npos) + optionLevel = 2; + else if (cutstr.find('#') != string::npos) + optionLevel = 3; + + if (optionLevel > level) + { + disableCout.disable(); + disableMd.disable(); + } + } + + // output the boilerplate of the name, and comment up to the point where the children + // are nested + tracepdeex( + 0, + std::cout, + "\n%s%s\t%-30s", + ((string)indentor).c_str(), + token.c_str(), + (defaultVal).c_str() + ); + + html << "\n" << htmlIndentor++ << "
    "; + html << "\n" << htmlIndentor << ""; + html << "\n" + << htmlIndentor++ << "
    " + << (nextIsChild ? "" : "") << token << (nextIsChild ? " ⯆" : ""); + + mdIndentor++; + if (mdIndentor.indentation == 2) + { + md << "\n" << mdIndentor << " " << token << "\n"; + } + + string link; + + if (defaults.enumName.empty() == false) + { + string linkName = defaults.enumName; + + if (defaultVal.find('[') == string::npos) + link += "[`" + linkName + "`]"; + else + link += "[`[" + linkName + "]`]"; + + link += "(#" + boost::algorithm::to_lower_copy(linkName) + ") "; + } + + md << "\n" + << "###### **`" << flatStack << "`**"; + + md << "\n" + << " " << link << "`" << defaultVal << " `" << "\n"; + + if (comment.empty() == false) + { + std::cout << "\t# " << comment.substr(0, comment.find('.')); + html << "\n" + << htmlIndentor << "# " << comment << ""; + + auto period = comment.find('.'); + md << "\n" + << "\n"; + + if (nextIsChild) + md << "> "; + + md << comment; //.substr(0, period); + // if ( period != string::npos + // &&period + 2 < comment.size()) + // { + // md << "\n" << "\n" << comment.substr(period + 2); + // } + } + + html << "\n" << --htmlIndentor << "
    "; + + md << "\n" + << "\n" + << "---" << "\n"; + + // + bool firstChild = false; + if (nextIsChild) + { + if (firstChild == false) + { + // initiate the section for embedding children nodes + firstChild = true; + + html << "\n" << htmlIndentor++ << "
    "; + } + + // recurse to do children of this node + indentor++; + outputDefaultSiblings( + level, + html, + md, + it, + indentor, + htmlIndentor, + mdIndentor, + stack + ); + --indentor; + } + + if (firstChild) + { + // finalise the child section + html << "\n" << --htmlIndentor << "
    "; + } + else + { + // this has no children, output the default value of this parameter instead - + // according to its commented parameter type + + for (auto once : {1}) + { + // booleans + if (type == typeid(bool).name()) + { + html << "\n" << htmlIndentor++ << ""; + break; + } + + auto begin = comment.find('{'); + auto end = comment.find('}', begin); + + // enums + if (begin != string::npos && end != string::npos) + { + string enums = comment.substr(begin + 1, end - begin - 1); + size_t pos_start = 0; + size_t pos_end; + + html << "\n" << htmlIndentor++ << ""; + + break; + } + + // general parameters + { + html << "\n" + << htmlIndentor << ""; + } + } + } + + html << "\n" << --htmlIndentor << "
    "; + + --mdIndentor; + } + } } /** Outputs default configuration and a configurator -* The default values, and descriptions of each parameter configured is output to the command line. -* A configurator is generated that can be used to edit the default configuration via interactive html scripts -*/ -void ACSConfig::outputDefaultConfiguration( - int level) + * The default values, and descriptions of each parameter configured is output to the command line. + * A configurator is generated that can be used to edit the default configuration via interactive + * html scripts + */ +void ACSConfig::outputDefaultConfiguration(int level) { - std::cout << "\n" << "Default configuration values:\n\n"; - - std::ofstream html ("GinanYamlInspector.html"); - std::ofstream md ("defaultConfiguration.md"); - - html << - #include "htmlHeaderTemplate.html" - << "\n"; - - auto it = acsConfig.yamlDefaults.begin(); - - Indentor indentor; - Indentor htmlIndentor; - Indentor mdIndentor('#', 1); - mdIndentor++; - md << "\n" << mdIndentor << " Default Configuration" << "\n"; - - md << "\n" << "This document outlines the major configuration options available in ginan that are most applicable to general users. " - << "For more advanced configuration options and their defaults, use the `-Y ` option at the command line to view increasing levels of advanced configurations."; - - outputDefaultSiblings(level, html, md, it, indentor, htmlIndentor, mdIndentor); - - std::cout << "\n" << "\n"; - - std::cout << "An interactive configuration inspector has been generated and saved to GinanYamlInspector.html" << "\n"; - - html << - #include "htmlFooterTemplate.html" - << "\n"; - - md << "\n" << mdIndentor << " Enum Details" << "\n"; - - for (auto& [enumName, details] : enumDetailsMap) - { - md << "\n" << "---"; - - md << "\n" << "\n" << "### " + enumName; - - md << "\n" << "\n" << "Valid enum values are:"; - for (auto& value : details.enums) - { - md << "\n" << "- `" << value << "`"; - - if (docs[value].empty() == false) - md << " : " << docs[value]; - } - - md << "\n" << "\n" << "For options:" << "\n"; - for (auto& caller : details.usingOptions) - { - string dummy; - md << "\n" << "- [`" << caller << "`](#" << nonNumericStack(caller, dummy, false) << ")"; - } - } + std::cout << "\n" + << "Default configuration values:\n\n"; + + std::ofstream html("GinanYamlInspector.html"); + std::ofstream md("defaultConfiguration.md"); + + html << htmlHeaderTemplate << "\n"; + + auto it = acsConfig.yamlDefaults.begin(); + + Indentor indentor; + Indentor htmlIndentor; + Indentor mdIndentor('#', 1); + mdIndentor++; + md << "\n" << mdIndentor << " Default Configuration" << "\n"; + + md << "\n" + << "This document outlines the major configuration options available in ginan that are most " + "applicable to " + "general users. " + << "For more advanced configuration options and their defaults, use the `-Y ` option " + "at the command line " + "to view increasing levels of advanced configurations."; + + outputDefaultSiblings(level, html, md, it, indentor, htmlIndentor, mdIndentor); + + std::cout << "\n" + << "\n"; + + std::cout << "An interactive configuration inspector has been generated and saved to " + "GinanYamlInspector.html" + << "\n"; + + html << htmlFooterTemplate << "\n"; + + md << "\n" << mdIndentor << " Enum Details" << "\n"; + + for (auto& [enumName, details] : enumDetailsMap) + { + md << "\n" + << "---"; + + md << "\n" + << "\n" + << "### " + enumName; + + md << "\n" + << "\n" + << "Valid enum values are:"; + for (auto& value : details.enums) + { + md << "\n" + << "- `" << value << "`"; + + if (docs[value].empty() == false) + md << " : " << docs[value]; + } + + md << "\n" + << "\n" + << "For options:" << "\n"; + for (auto& caller : details.usingOptions) + { + string dummy; + md << "\n" + << "- [`" << caller << "`](#" << nonNumericStack(caller, dummy, false) << ")"; + } + } } void defaultConfigs() { -// acsConfig.recOptsMap["GPS"].rinex23Conv.codeConv = -// { -// {E_Sys::GPS,{ -// {E_ObsCode2::P1, E_ObsCode::L1W}, -// {E_ObsCode2::P2, E_ObsCode::L2W}, -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C5, E_ObsCode::L5X}, -// {E_ObsCode2::L1, E_ObsCode::L1W}, -// {E_ObsCode2::L2, E_ObsCode::L2C}, -// {E_ObsCode2::L5, E_ObsCode::L5X}} -// }, -// -// {E_Sys::GAL,{ -// {E_ObsCode2::P1, E_ObsCode::L1P}, -// {E_ObsCode2::P2, E_ObsCode::L2P}, -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C5, E_ObsCode::L5I}, -// {E_ObsCode2::C7, E_ObsCode::L7Q}, -// {E_ObsCode2::C8, E_ObsCode::L8Q}, -// {E_ObsCode2::L1, E_ObsCode::L1C}, -// {E_ObsCode2::L2, E_ObsCode::L2P}, -// {E_ObsCode2::L5, E_ObsCode::L5I}, -// {E_ObsCode2::L7, E_ObsCode::L7Q}, -// {E_ObsCode2::L8, E_ObsCode::L8Q}} -// }, -// -// {E_Sys::GLO,{ -// {E_ObsCode2::P1, E_ObsCode::L1P}, -// {E_ObsCode2::P2, E_ObsCode::L2P}, -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C3, E_ObsCode::L3Q}, -// {E_ObsCode2::C5, E_ObsCode::L5I}, -// {E_ObsCode2::L1, E_ObsCode::L1C}, -// {E_ObsCode2::L2, E_ObsCode::L2P}, -// {E_ObsCode2::L3, E_ObsCode::L3Q}, -// {E_ObsCode2::L5, E_ObsCode::L5I}} -// }, -// -// {E_Sys::BDS,{ -// {E_ObsCode2::C2, E_ObsCode::L2I}, -// {E_ObsCode2::C6, E_ObsCode::L6I}, -// {E_ObsCode2::C7, E_ObsCode::L7I}, -// {E_ObsCode2::C8, E_ObsCode::L8X}, -// {E_ObsCode2::L2, E_ObsCode::L2X}, -// {E_ObsCode2::L6, E_ObsCode::L6I}, -// {E_ObsCode2::L7, E_ObsCode::L7I}, -// {E_ObsCode2::L8, E_ObsCode::L8X}} -// }, -// -// {E_Sys::QZS,{ -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C5, E_ObsCode::L5I}, -// {E_ObsCode2::C6, E_ObsCode::L6X}, -// {E_ObsCode2::L1, E_ObsCode::L1C}, -// {E_ObsCode2::L2, E_ObsCode::L2C}, -// {E_ObsCode2::L5, E_ObsCode::L5I}, -// {E_ObsCode2::L6, E_ObsCode::L6X}} -// }, -// -// {E_Sys::SBS,{ -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C5, E_ObsCode::L5I}, -// {E_ObsCode2::L1, E_ObsCode::L1C}, -// {E_ObsCode2::L2, E_ObsCode::L2C}, -// {E_ObsCode2::L5, E_ObsCode::L5I}} -// } -// }; -// -// acsConfig.recOptsMap["0"].rinex23Conv.phasConv = -// { -// {E_Sys::GPS,{ -// {E_ObsCode2::P1, E_ObsCode::L1W}, -// {E_ObsCode2::P2, E_ObsCode::L2W}, -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C5, E_ObsCode::L5X}, -// {E_ObsCode2::L1, E_ObsCode::L1W}, -// {E_ObsCode2::L2, E_ObsCode::L2C}, -// {E_ObsCode2::L5, E_ObsCode::L5X}} -// }, -// -// {E_Sys::GAL,{ -// {E_ObsCode2::P1, E_ObsCode::L1P}, -// {E_ObsCode2::P2, E_ObsCode::L2P}, -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C5, E_ObsCode::L5I}, -// {E_ObsCode2::C7, E_ObsCode::L7Q}, -// {E_ObsCode2::C8, E_ObsCode::L8Q}, -// {E_ObsCode2::L1, E_ObsCode::L1C}, -// {E_ObsCode2::L2, E_ObsCode::L2P}, -// {E_ObsCode2::L5, E_ObsCode::L5I}, -// {E_ObsCode2::L7, E_ObsCode::L7Q}, -// {E_ObsCode2::L8, E_ObsCode::L8Q}} -// }, -// -// {E_Sys::GLO,{ -// {E_ObsCode2::P1, E_ObsCode::L1P}, -// {E_ObsCode2::P2, E_ObsCode::L2P}, -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C3, E_ObsCode::L3Q}, -// {E_ObsCode2::C5, E_ObsCode::L5I}, -// {E_ObsCode2::L1, E_ObsCode::L1C}, -// {E_ObsCode2::L2, E_ObsCode::L2P}, -// {E_ObsCode2::L3, E_ObsCode::L3Q}, -// {E_ObsCode2::L5, E_ObsCode::L5I}} -// }, -// -// {E_Sys::BDS,{ -// {E_ObsCode2::C2, E_ObsCode::L2I}, -// {E_ObsCode2::C6, E_ObsCode::L6I}, -// {E_ObsCode2::C7, E_ObsCode::L7I}, -// {E_ObsCode2::C8, E_ObsCode::L8X}, -// {E_ObsCode2::L2, E_ObsCode::L2I}, -// {E_ObsCode2::L6, E_ObsCode::L6I}, -// {E_ObsCode2::L7, E_ObsCode::L7I}, -// {E_ObsCode2::L8, E_ObsCode::L8X}} -// }, -// -// {E_Sys::QZS,{ -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C5, E_ObsCode::L5I}, -// {E_ObsCode2::C6, E_ObsCode::L6X}, -// {E_ObsCode2::L1, E_ObsCode::L1C}, -// {E_ObsCode2::L2, E_ObsCode::L2C}, -// {E_ObsCode2::L5, E_ObsCode::L5I}, -// {E_ObsCode2::L6, E_ObsCode::L6X}} -// }, -// -// {E_Sys::SBS,{ -// {E_ObsCode2::C1, E_ObsCode::L1C}, -// {E_ObsCode2::C2, E_ObsCode::L2C}, -// {E_ObsCode2::C5, E_ObsCode::L5I}, -// {E_ObsCode2::L1, E_ObsCode::L1C}, -// {E_ObsCode2::L2, E_ObsCode::L2C}, -// {E_ObsCode2::L5, E_ObsCode::L5I}} -// } -// }; + // acsConfig.recOptsMap["GPS"].rinex23Conv.codeConv = + // { + // {E_Sys::GPS,{ + // {E_ObsCode2::P1, E_ObsCode::L1W}, + // {E_ObsCode2::P2, E_ObsCode::L2W}, + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C5, E_ObsCode::L5X}, + // {E_ObsCode2::L1, E_ObsCode::L1W}, + // {E_ObsCode2::L2, E_ObsCode::L2C}, + // {E_ObsCode2::L5, E_ObsCode::L5X}} + // }, + // + // {E_Sys::GAL,{ + // {E_ObsCode2::P1, E_ObsCode::L1P}, + // {E_ObsCode2::P2, E_ObsCode::L2P}, + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C5, E_ObsCode::L5I}, + // {E_ObsCode2::C7, E_ObsCode::L7Q}, + // {E_ObsCode2::C8, E_ObsCode::L8Q}, + // {E_ObsCode2::L1, E_ObsCode::L1C}, + // {E_ObsCode2::L2, E_ObsCode::L2P}, + // {E_ObsCode2::L5, E_ObsCode::L5I}, + // {E_ObsCode2::L7, E_ObsCode::L7Q}, + // {E_ObsCode2::L8, E_ObsCode::L8Q}} + // }, + // + // {E_Sys::GLO,{ + // {E_ObsCode2::P1, E_ObsCode::L1P}, + // {E_ObsCode2::P2, E_ObsCode::L2P}, + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C3, E_ObsCode::L3Q}, + // {E_ObsCode2::C5, E_ObsCode::L5I}, + // {E_ObsCode2::L1, E_ObsCode::L1C}, + // {E_ObsCode2::L2, E_ObsCode::L2P}, + // {E_ObsCode2::L3, E_ObsCode::L3Q}, + // {E_ObsCode2::L5, E_ObsCode::L5I}} + // }, + // + // {E_Sys::BDS,{ + // {E_ObsCode2::C2, E_ObsCode::L2I}, + // {E_ObsCode2::C6, E_ObsCode::L6I}, + // {E_ObsCode2::C7, E_ObsCode::L7I}, + // {E_ObsCode2::C8, E_ObsCode::L8X}, + // {E_ObsCode2::L2, E_ObsCode::L2X}, + // {E_ObsCode2::L6, E_ObsCode::L6I}, + // {E_ObsCode2::L7, E_ObsCode::L7I}, + // {E_ObsCode2::L8, E_ObsCode::L8X}} + // }, + // + // {E_Sys::QZS,{ + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C5, E_ObsCode::L5I}, + // {E_ObsCode2::C6, E_ObsCode::L6X}, + // {E_ObsCode2::L1, E_ObsCode::L1C}, + // {E_ObsCode2::L2, E_ObsCode::L2C}, + // {E_ObsCode2::L5, E_ObsCode::L5I}, + // {E_ObsCode2::L6, E_ObsCode::L6X}} + // }, + // + // {E_Sys::SBS,{ + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C5, E_ObsCode::L5I}, + // {E_ObsCode2::L1, E_ObsCode::L1C}, + // {E_ObsCode2::L2, E_ObsCode::L2C}, + // {E_ObsCode2::L5, E_ObsCode::L5I}} + // } + // }; + // + // acsConfig.recOptsMap["0"].rinex23Conv.phasConv = + // { + // {E_Sys::GPS,{ + // {E_ObsCode2::P1, E_ObsCode::L1W}, + // {E_ObsCode2::P2, E_ObsCode::L2W}, + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C5, E_ObsCode::L5X}, + // {E_ObsCode2::L1, E_ObsCode::L1W}, + // {E_ObsCode2::L2, E_ObsCode::L2C}, + // {E_ObsCode2::L5, E_ObsCode::L5X}} + // }, + // + // {E_Sys::GAL,{ + // {E_ObsCode2::P1, E_ObsCode::L1P}, + // {E_ObsCode2::P2, E_ObsCode::L2P}, + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C5, E_ObsCode::L5I}, + // {E_ObsCode2::C7, E_ObsCode::L7Q}, + // {E_ObsCode2::C8, E_ObsCode::L8Q}, + // {E_ObsCode2::L1, E_ObsCode::L1C}, + // {E_ObsCode2::L2, E_ObsCode::L2P}, + // {E_ObsCode2::L5, E_ObsCode::L5I}, + // {E_ObsCode2::L7, E_ObsCode::L7Q}, + // {E_ObsCode2::L8, E_ObsCode::L8Q}} + // }, + // + // {E_Sys::GLO,{ + // {E_ObsCode2::P1, E_ObsCode::L1P}, + // {E_ObsCode2::P2, E_ObsCode::L2P}, + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C3, E_ObsCode::L3Q}, + // {E_ObsCode2::C5, E_ObsCode::L5I}, + // {E_ObsCode2::L1, E_ObsCode::L1C}, + // {E_ObsCode2::L2, E_ObsCode::L2P}, + // {E_ObsCode2::L3, E_ObsCode::L3Q}, + // {E_ObsCode2::L5, E_ObsCode::L5I}} + // }, + // + // {E_Sys::BDS,{ + // {E_ObsCode2::C2, E_ObsCode::L2I}, + // {E_ObsCode2::C6, E_ObsCode::L6I}, + // {E_ObsCode2::C7, E_ObsCode::L7I}, + // {E_ObsCode2::C8, E_ObsCode::L8X}, + // {E_ObsCode2::L2, E_ObsCode::L2I}, + // {E_ObsCode2::L6, E_ObsCode::L6I}, + // {E_ObsCode2::L7, E_ObsCode::L7I}, + // {E_ObsCode2::L8, E_ObsCode::L8X}} + // }, + // + // {E_Sys::QZS,{ + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C5, E_ObsCode::L5I}, + // {E_ObsCode2::C6, E_ObsCode::L6X}, + // {E_ObsCode2::L1, E_ObsCode::L1C}, + // {E_ObsCode2::L2, E_ObsCode::L2C}, + // {E_ObsCode2::L5, E_ObsCode::L5I}, + // {E_ObsCode2::L6, E_ObsCode::L6X}} + // }, + // + // {E_Sys::SBS,{ + // {E_ObsCode2::C1, E_ObsCode::L1C}, + // {E_ObsCode2::C2, E_ObsCode::L2C}, + // {E_ObsCode2::C5, E_ObsCode::L5I}, + // {E_ObsCode2::L1, E_ObsCode::L1C}, + // {E_ObsCode2::L2, E_ObsCode::L2C}, + // {E_ObsCode2::L5, E_ObsCode::L5I}} + // } + // }; } /** Print out the configuration data that has been read in. -*/ -void ACSConfig::info( - Trace& s) ///< Trace file to output to -{ - std::stringstream ss; - - ss << "\n\n"; - ss << "===============================\n"; - ss << "Configuration Summary...\n"; - ss << "===============================\n"; - ss << "Inputs:\n"; - - if (!nav_files .empty()) { ss << "\tnav_files: "; for (auto& a : nav_files) ss << a << " "; ss << "\n"; } - if (!snx_files .empty()) { ss << "\tsnx_files: "; for (auto& a : snx_files) ss << a << " "; ss << "\n"; } - if (!atx_files .empty()) { ss << "\tatx_files: "; for (auto& a : atx_files) ss << a << " "; ss << "\n"; } - if (!dcb_files .empty()) { ss << "\tdcb_files: "; for (auto& a : dcb_files) ss << a << " "; ss << "\n"; } - if (!clk_files .empty()) { ss << "\tclk_files: "; for (auto& a : clk_files) ss << a << " "; ss << "\n"; } - if (!bsx_files .empty()) { ss << "\tbsx_files: "; for (auto& a : bsx_files) ss << a << " "; ss << "\n"; } - if (!ion_files .empty()) { ss << "\tion_files: "; for (auto& a : ion_files) ss << a << " "; ss << "\n"; } - if (!igrf_files .empty()) { ss << "\tigrf_files: "; for (auto& a : igrf_files) ss << a << " "; ss << "\n"; } - if (!ocean_tide_loading_blq_files .empty()) { ss << "\tocean_tide_loading_blq_files: "; for (auto& a : ocean_tide_loading_blq_files) ss << a << " "; ss << "\n"; } - if (!atmos_tide_loading_blq_files .empty()) { ss << "\tatmos_tide_loading_blq_files: "; for (auto& a : atmos_tide_loading_blq_files) ss << a << " "; ss << "\n"; } - if (!ocean_pole_tide_loading_files .empty()) { ss << "\tocean_pole_tide_loading_files: "; for (auto& a : ocean_pole_tide_loading_files) ss << a << " "; ss << "\n"; } - if (!pseudo_filter_files .empty()) { ss << "\tpseudo_filter_files: "; for (auto& a : pseudo_filter_files) ss << a << " "; ss << "\n"; } - if (!erp_files .empty()) { ss << "\terp_files: "; for (auto& a : erp_files) ss << a << " "; ss << "\n"; } - if (!sp3_files .empty()) { ss << "\tsp3_files: "; for (auto& a : sp3_files) ss << a << " "; ss << "\n"; } - if (!obx_files .empty()) { ss << "\tobx_files: "; for (auto& a : obx_files) ss << a << " "; ss << "\n"; } - if (!egm_files .empty()) { ss << "\tegm_files: "; for (auto& a : egm_files) ss << a << " "; ss << "\n"; } - if (!planetary_ephemeris_files .empty()) { ss << "\tplanetary_ephemeris_files: "; for (auto& a : planetary_ephemeris_files) ss << a << " "; ss << "\n"; } - if (!ocean_tide_potential_files .empty()) { ss << "\tocean_tide_potential_files: "; for (auto& a : ocean_tide_potential_files) ss << a << " "; ss << "\n"; } - if (!atmos_tide_potential_files .empty()) { ss << "\tatmos_tide_potential_files: "; for (auto& a : atmos_tide_potential_files) ss << a << " "; ss << "\n"; } - if (!cmc_files .empty()) { ss << "\tcmc_files: "; for (auto& a : cmc_files) ss << a << " "; ss << "\n"; } - if (!hfeop_files .empty()) { ss << "\thfeop_files: "; for (auto& a : hfeop_files) ss << a << " "; ss << "\n"; } - if (!atmos_oceean_dealiasing_files .empty()) { ss << "\tatmos_oceean_dealiasing_files: "; for (auto& a : atmos_oceean_dealiasing_files) ss << a << " "; ss << "\n"; } - if (!ocean_pole_tide_potential_files.empty()) { ss << "\tocean_pole_tide_potential_files: "; for (auto& a : ocean_pole_tide_potential_files) ss << a << " "; ss << "\n"; } - if (!sid_files .empty()) { ss << "\tsid_files: "; for (auto& a : sid_files) ss << a << " "; ss << "\n"; } - if (!vmf_files .empty()) { ss << "\tvmf_files: "; for (auto& a : vmf_files) ss << a << " "; ss << "\n"; } - if (!com_files .empty()) { ss << "\tcom_files: "; for (auto& a : com_files) ss << a << " "; ss << "\n"; } - if (!orography_files .empty()) { ss << "\torography_files: "; for (auto& a : orography_files) ss << a << " "; ss << "\n"; } - if (!gpt2grid_files .empty()) { ss << "\tgpt2grid_files: "; for (auto& a : gpt2grid_files) ss << a << " "; ss << "\n"; } - if (!nav_rtcm_inputs .empty()) { ss << "\trtcm_inputs: "; for (auto& a : nav_rtcm_inputs) ss << a << " "; ss << "\n"; } - if (!qzs_rtcm_inputs .empty()) { ss << "\tqzl6_inputs: "; for (auto& a : qzs_rtcm_inputs) ss << a << " "; ss << "\n"; } - if (!sisnet_inputs .empty()) { ss << "\tsisnet_inputs: "; for (auto& a : sisnet_inputs) ss << a << " "; ss << "\n"; } - if (!rnx_inputs .empty()) { ss << "\trnx_inputs: "; for (auto& [z, A] : rnx_inputs) for (auto& a : A) ss << a << " "; ss << "\n"; } - if (!pseudo_sp3_inputs .empty()) { ss << "\tsp3_inputs: "; for (auto& [z, A] : pseudo_sp3_inputs) for (auto& a : A) ss << a << " "; ss << "\n"; } - if (!pseudo_snx_inputs .empty()) { ss << "\tsnx_inputs: "; for (auto& [z, A] : pseudo_snx_inputs) for (auto& a : A) ss << a << " "; ss << "\n"; } - if (!obs_rtcm_inputs .empty()) { ss << "\trtcm_inputs: "; for (auto& [z, A] : obs_rtcm_inputs) for (auto& a : A) ss << a << " "; ss << "\n"; } - - ss << "\n"; - - ss << "Outputs:\n"; - if (1) { ss << "\ttrace level: " << trace_level << "\n"; } - if (output_satellite_trace) { ss << "\tsatellite trace filename: " << satellite_trace_filename << "\n"; } - if (output_receiver_trace) { ss << "\treceiver trace filename: " << receiver_trace_filename << "\n"; } - if (output_json_trace) { ss << "\tjson trace filename: " << receiver_trace_filename + "_json" << "\n"; } - if (output_network_trace) { ss << "\tnetwork trace filename: " << network_trace_filename << "\n"; } - if (output_ionosphere_trace) { ss << "\tionosphere trace filename: " << ionosphere_trace_filename << "\n"; } - if (output_clocks) { ss << "\tclocks filename: " << clocks_filename << "\n"; } - if (output_ionex) { ss << "\tionex filename: " << ionex_filename << "\n"; } - if (output_sinex) { ss << "\tsinex filename: " << sinex_filename << "\n"; } - if (output_ionstec) { ss << "\tionstec filename: " << ionstec_filename << "\n"; } - if (output_bias_sinex) { ss << "\tbias sinex filename: " << bias_sinex_filename << "\n"; } - if (output_cost) { ss << "\tcost filename: " << cost_filename << "\n"; } - if (output_trop_sinex) { ss << "\ttrop sinex filename: " << trop_sinex_filename << "\n"; } - if (output_gpx) { ss << "\tgpx filename: " << gpx_filename << "\n"; } - if (output_pos) { ss << "\tpos filename: " << pos_filename << "\n"; } - if (output_sp3) { ss << "\tsp3 filename: " << sp3_filename << "\n"; } - if (output_decoded_rtcm_json) { ss << "\tdecoded rtcm json filename: " << decoded_rtcm_json_filename << "\n"; } - if (output_encoded_rtcm_json) { ss << "\tencoded rtcm json filename: " << encoded_rtcm_json_filename << "\n"; } - if (output_sbas_ems) { ss << "\tSBAS EMS filename: " << ems_filename << "\n"; } - - ss << "\n"; - - ss << "Process Modes:\n"; - ss << "\tPreprocessor: " << process_preprocessor << "\n"; - ss << "\tSPP " << process_spp << "\n"; - ss << "\tPPP: " << process_ppp << "\n"; - ss << "\tMinimum Constraints: " << process_minimum_constraints << "\n"; - ss << "\tIonospheric: " << process_ionosphere << "\n"; - ss << "\tRTS Smoothing: " << process_rts << "\n"; - ss << "\n"; - - ss << "Systems:\n"; - ss << "\tGPS: " << process_sys[E_Sys::GPS] << "\n"; - ss << "\tGLONASS: " << process_sys[E_Sys::GLO] << "\n"; - ss << "\tGALILEO: " << process_sys[E_Sys::GAL] << "\n"; - ss << "\tBEIDOU: " << process_sys[E_Sys::BDS] << "\n"; - ss << "\tQZSS: " << process_sys[E_Sys::QZS] << "\n"; - ss << "\tLEO: " << process_sys[E_Sys::LEO] << "\n"; - ss << "\n"; - - ss << "Epochs:\n"; - if (epoch_interval > 0) { ss << "\tepoch_interval: " << epoch_interval << "\n"; } - if (max_epochs > 0) { ss << "\tmax_epochs: " << max_epochs << "\n"; } - if (!start_epoch .is_not_a_date_time()) { ss << "\tepoch start: " << start_epoch << "\n"; } - if (!end_epoch .is_not_a_date_time()) { ss << "\tepoch end: " << end_epoch << "\n"; } - - ss << "\n"; - ss << "===============================\n"; - ss << "...End Configuration Summary\n"; - ss << "===============================\n"; - ss << "\n"; - - BOOST_LOG_TRIVIAL(info) << ss.str(); + */ +void ACSConfig::info(Trace& s) ///< Trace file to output to +{ + std::stringstream ss; + + ss << "\n\n"; + ss << "===============================\n"; + ss << "Configuration Summary...\n"; + ss << "===============================\n"; + ss << "Inputs:\n"; + + if (!nav_files.empty()) + { + ss << "\tnav_files: "; + for (auto& a : nav_files) + ss << a << " "; + ss << "\n"; + } + if (!ems_files.empty()) + { + ss << "\tems_files: "; + for (auto& a : ems_files) + ss << a << " "; + ss << "\n"; + } + if (!snx_files.empty()) + { + ss << "\tsnx_files: "; + for (auto& a : snx_files) + ss << a << " "; + ss << "\n"; + } + if (!atx_files.empty()) + { + ss << "\tatx_files: "; + for (auto& a : atx_files) + ss << a << " "; + ss << "\n"; + } + if (!dcb_files.empty()) + { + ss << "\tdcb_files: "; + for (auto& a : dcb_files) + ss << a << " "; + ss << "\n"; + } + if (!clk_files.empty()) + { + ss << "\tclk_files: "; + for (auto& a : clk_files) + ss << a << " "; + ss << "\n"; + } + if (!bsx_files.empty()) + { + ss << "\tbsx_files: "; + for (auto& a : bsx_files) + ss << a << " "; + ss << "\n"; + } + if (!ion_files.empty()) + { + ss << "\tion_files: "; + for (auto& a : ion_files) + ss << a << " "; + ss << "\n"; + } + if (!igrf_files.empty()) + { + ss << "\tigrf_files: "; + for (auto& a : igrf_files) + ss << a << " "; + ss << "\n"; + } + if (!ocean_tide_loading_blq_files.empty()) + { + ss << "\tocean_tide_loading_blq_files: "; + for (auto& a : ocean_tide_loading_blq_files) + ss << a << " "; + ss << "\n"; + } + if (!atmos_tide_loading_blq_files.empty()) + { + ss << "\tatmos_tide_loading_blq_files: "; + for (auto& a : atmos_tide_loading_blq_files) + ss << a << " "; + ss << "\n"; + } + if (!ocean_pole_tide_loading_files.empty()) + { + ss << "\tocean_pole_tide_loading_files: "; + for (auto& a : ocean_pole_tide_loading_files) + ss << a << " "; + ss << "\n"; + } + if (!pseudo_filter_files.empty()) + { + ss << "\tpseudo_filter_files: "; + for (auto& a : pseudo_filter_files) + ss << a << " "; + ss << "\n"; + } + if (!erp_files.empty()) + { + ss << "\terp_files: "; + for (auto& a : erp_files) + ss << a << " "; + ss << "\n"; + } + if (!sp3_files.empty()) + { + ss << "\tsp3_files: "; + for (auto& a : sp3_files) + ss << a << " "; + ss << "\n"; + } + if (!obx_files.empty()) + { + ss << "\tobx_files: "; + for (auto& a : obx_files) + ss << a << " "; + ss << "\n"; + } + if (!egm_files.empty()) + { + ss << "\tegm_files: "; + for (auto& a : egm_files) + ss << a << " "; + ss << "\n"; + } + if (!planetary_ephemeris_files.empty()) + { + ss << "\tplanetary_ephemeris_files: "; + for (auto& a : planetary_ephemeris_files) + ss << a << " "; + ss << "\n"; + } + if (!ocean_tide_potential_files.empty()) + { + ss << "\tocean_tide_potential_files: "; + for (auto& a : ocean_tide_potential_files) + ss << a << " "; + ss << "\n"; + } + if (!atmos_tide_potential_files.empty()) + { + ss << "\tatmos_tide_potential_files: "; + for (auto& a : atmos_tide_potential_files) + ss << a << " "; + ss << "\n"; + } + if (!cmc_files.empty()) + { + ss << "\tcmc_files: "; + for (auto& a : cmc_files) + ss << a << " "; + ss << "\n"; + } + if (!hfeop_files.empty()) + { + ss << "\thfeop_files: "; + for (auto& a : hfeop_files) + ss << a << " "; + ss << "\n"; + } + if (!atmos_ocean_dealiasing_files.empty()) + { + ss << "\tatmos_oceean_dealiasing_files: "; + for (auto& a : atmos_ocean_dealiasing_files) + ss << a << " "; + ss << "\n"; + } + if (!ocean_pole_tide_potential_files.empty()) + { + ss << "\tocean_pole_tide_potential_files: "; + for (auto& a : ocean_pole_tide_potential_files) + ss << a << " "; + ss << "\n"; + } + if (!space_weather_files.empty()) + { + ss << "\tspace_weather_files: "; + for (auto& a : space_weather_files) + ss << a << " "; + ss << "\n"; + } + if (!sid_files.empty()) + { + ss << "\tsid_files: "; + for (auto& a : sid_files) + ss << a << " "; + ss << "\n"; + } + if (!vmf_files.empty()) + { + ss << "\tvmf_files: "; + for (auto& a : vmf_files) + ss << a << " "; + ss << "\n"; + } + if (!com_files.empty()) + { + ss << "\tcom_files: "; + for (auto& a : com_files) + ss << a << " "; + ss << "\n"; + } + if (!orography_files.empty()) + { + ss << "\torography_files: "; + for (auto& a : orography_files) + ss << a << " "; + ss << "\n"; + } + if (!gpt2grid_files.empty()) + { + ss << "\tgpt2grid_files: "; + for (auto& a : gpt2grid_files) + ss << a << " "; + ss << "\n"; + } + if (!nav_rtcm_inputs.empty()) + { + ss << "\trtcm_inputs: "; + for (auto& a : nav_rtcm_inputs) + ss << a << " "; + ss << "\n"; + } + if (!qzs_rtcm_inputs.empty()) + { + ss << "\tqzl6_inputs: "; + for (auto& a : qzs_rtcm_inputs) + ss << a << " "; + ss << "\n"; + } + if (!sisnet_inputs.empty()) + { + ss << "\tsisnet_inputs: "; + for (auto& a : sisnet_inputs) + ss << a << " "; + ss << "\n"; + } + if (!rnx_inputs.empty()) + { + ss << "\trnx_inputs: "; + for (auto& [z, A] : rnx_inputs) + for (auto& a : A) + ss << a << " "; + ss << "\n"; + } + if (!pseudo_sp3_inputs.empty()) + { + ss << "\tsp3_inputs: "; + for (auto& [z, A] : pseudo_sp3_inputs) + for (auto& a : A) + ss << a << " "; + ss << "\n"; + } + if (!pseudo_snx_inputs.empty()) + { + ss << "\tsnx_inputs: "; + for (auto& [z, A] : pseudo_snx_inputs) + for (auto& a : A) + ss << a << " "; + ss << "\n"; + } + if (!obs_rtcm_inputs.empty()) + { + ss << "\trtcm_inputs: "; + for (auto& [z, A] : obs_rtcm_inputs) + for (auto& a : A) + ss << a << " "; + ss << "\n"; + } + + ss << "\n"; + + ss << "Outputs:\n"; + if (1) + { + ss << "\ttrace level: " << trace_level << "\n"; + } + if (output_satellite_trace) + { + ss << "\tsatellite trace filename: " << satellite_trace_filename << "\n"; + } + if (output_receiver_trace) + { + ss << "\treceiver trace filename: " << receiver_trace_filename << "\n"; + } + if (output_json_trace) + { + ss << "\tjson trace filename: " << receiver_json_filename << "\n"; + } + if (output_network_trace) + { + ss << "\tnetwork trace filename: " << network_trace_filename << "\n"; + } + if (output_ionosphere_trace) + { + ss << "\tionosphere trace filename: " << ionosphere_trace_filename << "\n"; + } + if (output_clocks) + { + ss << "\tclocks filename: " << clocks_filename << "\n"; + } + if (output_ionex) + { + ss << "\tionex filename: " << ionex_filename << "\n"; + } + if (output_sinex) + { + ss << "\tsinex filename: " << sinex_filename << "\n"; + } + if (output_ionstec) + { + ss << "\tionstec filename: " << ionstec_filename << "\n"; + } + if (output_bias_sinex) + { + ss << "\tbias sinex filename: " << bias_sinex_filename << "\n"; + } + if (output_cost) + { + ss << "\tcost filename: " << cost_filename << "\n"; + } + if (output_trop_sinex) + { + ss << "\ttrop sinex filename: " << trop_sinex_filename << "\n"; + } + if (output_gpx) + { + ss << "\tgpx filename: " << gpx_filename << "\n"; + } + if (output_pos) + { + ss << "\tpos filename: " << pos_filename << "\n"; + } + if (output_spp) + { + ss << "\tspp filename: " << spp_filename << "\n"; + } + if (output_sp3) + { + ss << "\tsp3 filename: " << sp3_filename << "\n"; + } + if (output_decoded_rtcm_json) + { + ss << "\tdecoded rtcm json filename: " << decoded_rtcm_json_filename << "\n"; + } + if (output_encoded_rtcm_json) + { + ss << "\tencoded rtcm json filename: " << encoded_rtcm_json_filename << "\n"; + } + if (output_sbas_ems) + { + ss << "\tSBAS EMS filename: " << ems_filename << "\n"; + } + + ss << "\n"; + + ss << "Process Modes:\n"; + ss << "\tPreprocessor: " << process_preprocessor << "\n"; + ss << "\tSPP " << process_spp << "\n"; + ss << "\tPPP: " << process_ppp << "\n"; + ss << "\tMinimum Constraints: " << process_minimum_constraints << "\n"; + ss << "\tIonospheric: " << process_ionosphere << "\n"; + ss << "\tRTS Smoothing: " << process_rts << "\n"; + ss << "\n"; + + ss << "Systems:\n"; + ss << "\tGPS: " << process_sys[E_Sys::GPS] << "\n"; + ss << "\tGLONASS: " << process_sys[E_Sys::GLO] << "\n"; + ss << "\tGALILEO: " << process_sys[E_Sys::GAL] << "\n"; + ss << "\tBEIDOU: " << process_sys[E_Sys::BDS] << "\n"; + ss << "\tQZSS: " << process_sys[E_Sys::QZS] << "\n"; + ss << "\tLEO: " << process_sys[E_Sys::LEO] << "\n"; + ss << "\n"; + + ss << "Epochs:\n"; + if (epoch_interval > 0) + { + ss << "\tepoch_interval: " << epoch_interval << "\n"; + } + if (max_epochs > 0) + { + ss << "\tmax_epochs: " << max_epochs << "\n"; + } + if (!start_epoch.is_not_a_date_time()) + { + ss << "\tepoch start: " << start_epoch << "\n"; + } + if (!end_epoch.is_not_a_date_time()) + { + ss << "\tepoch end: " << end_epoch << "\n"; + } + + ss << "\n"; + ss << "===============================\n"; + ss << "...End Configuration Summary\n"; + ss << "===============================\n"; + ss << "\n"; + + BOOST_LOG_TRIVIAL(info) << ss.str(); } -void addAvailableOptions( - const string& stack) +void addAvailableOptions(const string& stack) { - string dummy; - auto& available = acsConfig.availableOptions[nonNumericStack(stack, dummy)]; + string dummy; + auto& available = acsConfig.availableOptions[nonNumericStack(stack, dummy)]; - if (available) - { - //already recursed this bit - return; - } + if (available) + { + // already recursed this bit + return; + } - available = true; + available = true; - auto pos = stack.find_last_of(':', stack.size() - 2); + auto pos = stack.find_last_of(':', stack.size() - 2); - if (pos == string::npos) - { - return; - } + if (pos == string::npos) + { + return; + } - addAvailableOptions(stack.substr(0, pos + 1)); + addAvailableOptions(stack.substr(0, pos + 1)); } /** Get an object within a hierarchy of yaml structure using a vector of nodes. -* This will also set default values and comments for the final object in the hierarchy as required. -* The descriptors may have numeric prefixes attached for ordering parameters in the default output, -* these are removed before searching for them in the hierarchy -*/ + * This will also set default values and comments for the final object in the hierarchy as required. + * The descriptors may have numeric prefixes attached for ordering parameters in the default output, + * these are removed before searching for them in the hierarchy + */ NodeStack stringsToYamlObject( - NodeStack yamlBase, ///< Yaml node to search within - const vector& yamlNodeDescriptor, ///< List of strings of keys to trace hierarchy - const string& comment = "", ///< Optional comment to append to default values output - const string& defaultValue = "", ///< Optional default value - const string& type = "") ///< Optional type of variable + NodeStack yamlBase, ///< Yaml node to search within + const vector& yamlNodeDescriptor, ///< List of strings of keys to trace hierarchy + const string& comment = "", ///< Optional comment to append to default values output + const string& defaultValue = "", ///< Optional default value + const string& type = "" ///< Optional type of variable +) { - YAML::Node currentNode; + YAML::Node currentNode; - auto [node, stack] = yamlBase; - currentNode.reset(node); + auto [node, stack] = yamlBase; + currentNode.reset(node); - //this function is fiddly - re-ordering or simplifying will likely lead to default configuration output issues + // this function is fiddly - re-ordering or simplifying will likely lead to default + // configuration output issues - for (int i = 0; i < yamlNodeDescriptor.size(); i++) - { - auto desc = yamlNodeDescriptor[i]; + for (int i = 0; i < yamlNodeDescriptor.size(); i++) + { + auto desc = yamlNodeDescriptor[i]; - if (desc.empty()) - { - continue; - } + if (desc.empty()) + { + continue; + } - boost::algorithm::to_lower(desc); + boost::algorithm::to_lower(desc); - string dummy; - string shortDesc = nonNumericStack(desc, dummy, false); + string dummy; + string shortDesc = nonNumericStack(desc, dummy, false); - bool test = false; - if (currentNode[shortDesc]) - test = true; + bool test = false; + if (currentNode[shortDesc]) + test = true; - currentNode.reset(currentNode[shortDesc]); - stack += desc + ":"; + currentNode.reset(currentNode[shortDesc]); + stack += desc + ":"; -// string test = currentNode.Scalar(); + // string test = currentNode.Scalar(); - if (acsConfig.yamlDefaults.find(stack) == acsConfig.yamlDefaults.end()) - { - if (i == yamlNodeDescriptor.size() - 1) acsConfig.yamlDefaults[stack] = {defaultValue, comment, type}; - else acsConfig.yamlDefaults[stack] = {"", ""}; - acsConfig.yamlDefaults[stack].found = test; - } + if (acsConfig.yamlDefaults.find(stack) == acsConfig.yamlDefaults.end()) + { + if (i == yamlNodeDescriptor.size() - 1) + acsConfig.yamlDefaults[stack] = {defaultValue, comment, type}; + else + acsConfig.yamlDefaults[stack] = {"", ""}; + acsConfig.yamlDefaults[stack].found = test; + } - //override comment if it hasnt been set yet - if ( i == yamlNodeDescriptor.size() - 1 - &&acsConfig.yamlDefaults[stack].comment.empty() - &&comment.empty() == false) - { - acsConfig.yamlDefaults[stack].comment = comment; - } - } + // override comment if it hasnt been set yet + if (i == yamlNodeDescriptor.size() - 1 && acsConfig.yamlDefaults[stack].comment.empty() && + comment.empty() == false) + { + acsConfig.yamlDefaults[stack].comment = comment; + } + } - addAvailableOptions(stack); + addAvailableOptions(stack); - return {currentNode, stack}; + return {currentNode, stack}; } /** Set an output from yaml object if found -*/ -template + */ +template bool tryGetFromYaml( - TYPE& output, ///< Variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& yamlNodeDescriptor, ///< List of strings of keys to trace hierarcy - const string& comment = "") ///< Description to provide to user for automatic documentation -{ - auto [optNode, stack] = stringsToYamlObject(yamlBase, yamlNodeDescriptor, comment, stringify(output), typeid(output).name()); - - addAvailableOptions(stack); - - string dummy; - - auto& yamlDefault = acsConfig.yamlDefaults[stack]; - try - { -// yamlDefault.foundValue = yamlDefault.defaultValue; - - output = optNode.template as(); - -// yamlDefault.foundValue = stringify(output); - - if (stringify(output) == yamlDefault.defaultValue) - { - BOOST_LOG_TRIVIAL(debug) - << "Yaml entry " << nonNumericStack(stack, dummy) << " is configured with its default value, deleting it entirely would simplify the configuration file."; - } - - yamlDefault.found = true; - return true; - } - catch (...) - { - if ( (optNode.IsSequence()) - ||(optNode.IsScalar() && optNode.Scalar(). empty() == false)) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Yaml entry '" << nonNumericStack(stack, dummy) << "' was found but its value is incorrectly formatted"; - } - } - - return false; + TYPE& output, ///< Variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& yamlNodeDescriptor, ///< List of strings of keys to trace hierarcy + const string& comment = "" ///< Description to provide to user for automatic documentation +) +{ + auto [optNode, stack] = stringsToYamlObject( + yamlBase, + yamlNodeDescriptor, + comment, + stringify(output), + typeid(output).name() + ); + + addAvailableOptions(stack); + + string dummy; + + auto& yamlDefault = acsConfig.yamlDefaults[stack]; + try + { + // yamlDefault.foundValue = yamlDefault.defaultValue; + + output = optNode.template as(); + + // yamlDefault.foundValue = stringify(output); + + if (stringify(output) == yamlDefault.defaultValue) + { + BOOST_LOG_TRIVIAL(debug) << "Yaml entry " << nonNumericStack(stack, dummy) + << " is configured with its default value, deleting it " + "entirely would simplify the configuration file."; + } + + yamlDefault.found = true; + return true; + } + catch (...) + { + if ((optNode.IsSequence()) || (optNode.IsScalar() && optNode.Scalar().empty() == false)) + { + BOOST_LOG_TRIVIAL(warning) << "Yaml entry '" << nonNumericStack(stack, dummy) + << "' was found but its value is incorrectly formatted"; + } + } + + return false; } /** Set an output from command line options if found -*/ -template + */ +template bool tryGetFromOpts( - TYPE& output, ///< Variable to output to - boost::program_options::variables_map& commandOpts, ///< Command line object to search within - const vector& nodeDescriptor) ///< List of strings of keys to trace hierarcy -{ - string dummy; - string name = nodeDescriptor.back(); - name = nonNumericStack(name, dummy, false); - if (commandOpts.count(name)) - { - try - { - output = commandOpts[name].as(); - return true; - } - catch (...) {} - } - return false; + TYPE& output, ///< Variable to output to + boost::program_options::variables_map& commandOpts, ///< Command line object to search within + const vector& nodeDescriptor ///< List of strings of keys to trace hierarcy +) +{ + string dummy; + string name = nodeDescriptor.back(); + name = nonNumericStack(name, dummy, false); + if (commandOpts.count(name)) + { + try + { + output = commandOpts[name].as(); + return true; + } + catch (...) + { + } + } + return false; } /** Set an output from any config source if found -*/ -template + */ +template bool tryGetFromAny( - TYPE& output, ///< Variable to output to - boost::program_options::variables_map& commandOpts, ///< Command line object to search within - NodeStack& yamlBase, ///< Yaml node to search within - const vector& nodeDescriptor, ///< List of strings of keys to trace hierarcy - const string& comment = "") ///< Description to provide to user for automatic documentation -{ - bool found = false; - found |= tryGetFromYaml(output, yamlBase, nodeDescriptor, comment); - found |= tryGetFromOpts(output, commandOpts,nodeDescriptor); - return found; + TYPE& output, ///< Variable to output to + boost::program_options::variables_map& commandOpts, ///< Command line object to search within + NodeStack& yamlBase, ///< Yaml node to search within + const vector& nodeDescriptor, ///< List of strings of keys to trace hierarcy + const string& comment = "" ///< Description to provide to user for automatic documentation +) +{ + bool found = false; + found |= tryGetFromYaml(output, yamlBase, nodeDescriptor, comment); + found |= tryGetFromOpts(output, commandOpts, nodeDescriptor); + return found; } -template -void addEnumDetails( - string& stack) +template +void addEnumDetails(string& stack) { - string enumName = ENUM::_name(); + string enumName = string(magic_enum::enum_type_name()); - auto names = ENUM::_names(); + auto values = magic_enum::enum_values(); - if (enumDetailsMap.find(enumName) == enumDetailsMap.end()) - for (int i = 0; i < ENUM::_size(); i++) - { - string enumOption = boost::algorithm::to_lower_copy((string) names[i]); - enumDetailsMap[enumName].enums.push_back(enumOption); - } + if (enumDetailsMap.find(enumName) == enumDetailsMap.end()) + for (auto val : values) + { + string enumOption = boost::algorithm::to_lower_copy(enum_to_string(val)); + enumDetailsMap[enumName].enums.push_back(enumOption); + } - string dummy; + string dummy; - string newStack = nonNumericStack(stack, dummy); - enumDetailsMap[enumName].usingOptions.push_back(newStack); - acsConfig.yamlDefaults[stack].enumName = enumName; + string newStack = nonNumericStack(stack, dummy); + enumDetailsMap[enumName].usingOptions.push_back(newStack); + acsConfig.yamlDefaults[stack].enumName = enumName; } /** Get a list of available enum values as readable string */ template -string getEnumOpts( - bool vec = false) +string getEnumOpts(bool vec = false) { - string enumOptions; - if (vec) enumOptions = " ["; - else enumOptions = " {"; - - auto names = ENUM::_names(); - for (int i = 0; i < ENUM::_size(); i++) - { - string enumOption = boost::algorithm::to_lower_copy((string) names[i]); - - if (i != 0) - enumOptions += ", "; - enumOptions += enumOption; - } - - if (vec) enumOptions += "]"; - else enumOptions += "}"; - - return enumOptions; + string enumOptions; + if (vec) + enumOptions = " ["; + else + enumOptions = " {"; + + auto values = magic_enum::enum_values(); + int i = 0; + for (auto val : values) + { + string enumOption = boost::algorithm::to_lower_copy(enum_to_string(val)); + + if (i != 0) + enumOptions += ", "; + enumOptions += enumOption; + i++; + } + + if (vec) + enumOptions += "]"; + else + enumOptions += "}"; + + return enumOptions; } template -void warnAboutEnum( - const string& wrong, - const string& option, - ENUM enumValue) -{ - BOOST_LOG_TRIVIAL(error) - << "\nError: " << wrong << " is not a valid entry for option: " << option << ".\n" - << "Valid options include:"; - - for (const char* name : ENUM::_names()) - { - BOOST_LOG_TRIVIAL(error) << name; - } +void warnAboutEnum(const string& wrong, const string& option, ENUM enumValue) +{ + BOOST_LOG_TRIVIAL(error) << wrong << " is not a valid entry for option: " << option << ".\n" + << "Valid options include:"; + + for (auto val : magic_enum::enum_values()) + { + BOOST_LOG_TRIVIAL(error) << enum_to_string(val); + } } /** Set an enum from yaml, decoding strings to ints -*/ + */ template bool tryGetEnumOpt( - ENUM& out, ///< Variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& yamlNodeDescriptor, ///< List of strings of keys to trace hierarcy - const string& comment = "") ///< Description to provide to user for automatic documentation -{ - string enumOptions = getEnumOpts(); - - auto [optNode, stack] = stringsToYamlObject(yamlBase, yamlNodeDescriptor, comment + enumOptions, out._to_string()); - - addAvailableOptions(stack); - - addEnumDetails(stack); - - if ( optNode.IsSequence() - ||optNode.IsMap()) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Map or sequence found for scalar option " << yamlNodeDescriptor.back(); - } - - string value; - try - { - value = optNode.template as(); -// std::cout << stack << " was found\n"; - } - catch (...) - { -// std::cout << stack << " not found\n"; - return false; - } - - try - { - out = ENUM::_from_string_nocase(value.c_str()); - return true; - } - catch (...) - { - warnAboutEnum(value, yamlNodeDescriptor.back(), out); - return false; - } + ENUM& out, ///< Variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& yamlNodeDescriptor, ///< List of strings of keys to trace hierarcy + const string& comment = "" ///< Description to provide to user for automatic documentation +) +{ + string enumOptions = getEnumOpts(); + + auto [optNode, stack] = stringsToYamlObject( + yamlBase, + yamlNodeDescriptor, + comment + enumOptions, + enum_to_string(out) + ); + + addAvailableOptions(stack); + + addEnumDetails(stack); + + if (optNode.IsSequence() || optNode.IsMap()) + { + BOOST_LOG_TRIVIAL(error) << "Map or sequence found for scalar option " + << yamlNodeDescriptor.back(); + } + + string value; + try + { + value = optNode.template as(); + // std::cout << stack << " was found\n"; + } + catch (...) + { + // std::cout << stack << " not found\n"; + return false; + } + + try + { + out = string_to_enum_nocase_throw(value.c_str()); + return true; + } + catch (...) + { + warnAboutEnum(value, yamlNodeDescriptor.back(), out); + return false; + } } template bool tryGetEnumVec( - vector& enumVector, ///< Output vector for enum configurations - NodeStack yamlBase, ///< Yaml node to search within - const vector& yamlNodeDescriptor, ///< List of strings of keys to trace hierarcy - const string& comment = "") ///< Description to provide to user for automatic documentation + vector& enumVector, ///< Output vector for enum configurations + NodeStack yamlBase, ///< Yaml node to search within + const vector& yamlNodeDescriptor, ///< List of strings of keys to trace hierarcy + const string& comment = "" ///< Description to provide to user for automatic documentation +) { - string enumOptions = getEnumOpts(true); - - auto [optNode, stack] = stringsToYamlObject(yamlBase, yamlNodeDescriptor, comment + enumOptions, stringify(enumVector)); //do this twice to populate the defaults before using strings - - vector enumStrings; - bool found = tryGetFromYaml(enumStrings, yamlBase, yamlNodeDescriptor, comment + enumOptions); - - addEnumDetails(stack); - - if (found == false) - return false; - - enumVector.clear(); - - for (auto& enumString : enumStrings) - { - try - { - auto a = ENUM::_from_string_nocase(enumString.c_str()); - enumVector.push_back(a); - } - catch (...) - { - ENUM enumValue; - warnAboutEnum(enumString, yamlNodeDescriptor.back(), enumValue); - continue; - } - } - - return true; + string enumOptions = getEnumOpts(true); + + auto [optNode, stack] = stringsToYamlObject( + yamlBase, + yamlNodeDescriptor, + comment + enumOptions, + stringify(enumVector) + ); // do this twice to populate the defaults before using strings + + vector enumStrings; + bool found = tryGetFromYaml(enumStrings, yamlBase, yamlNodeDescriptor, comment + enumOptions); + + addEnumDetails(stack); + + if (found == false) + return false; + + enumVector.clear(); + + for (auto& enumString : enumStrings) + { + try + { + auto a = string_to_enum_nocase_throw(enumString.c_str()); + enumVector.push_back(a); + } + catch (...) + { + ENUM enumValue = ENUM(); + warnAboutEnum(enumString, yamlNodeDescriptor.back(), enumValue); + continue; + } + } + + return true; } /** Use pointer arithmetic to keep track of variables that have been initialised */ -template -void setInited( - BASE& base, - COMP& comp, - bool init = true) +template +void setInited(BASE& base, COMP& comp, bool init = true) { - if (init == false) - { - return; - } + if (init == false) + { + return; + } - int offset = (char*)(&comp) - (char*)(&base); + int offset = (char*)(&comp) - (char*)(&base); - base.initialisedMap[offset] = true; + base.initialisedMap[offset] = true; } /** Set the variables associated with kalman filter states from yaml -*/ + */ void tryGetKalmanFromYaml( - KalmanModel& output, ///< Variable to output to - NodeStack& yaml, ///< Yaml node to search within - const string& key, ///< Key of yaml object - const string& comment = "", ///< Description to provide to user for automatic documentation - bool skippable = false) ///< Optionally skip this when yaml object not found in file -{ - auto newYaml = stringsToYamlObject(yaml, {key}, comment); - - auto& [optNode, stack] = newYaml; - - if ( skippable - && !optNode) - { - return; - } - - E_Period proc_noise_dt = E_Period::SECOND; - - { - }{auto& thing = output.estimate ; setInited(output, thing, tryGetFromYaml(thing, newYaml, {"0! estimated" }, "Estimate state in kalman filter")); - }{auto& thing = output.use_remote_sigma ; setInited(output, thing, tryGetFromYaml(thing, newYaml, {"4@ use_remote_sigma" }, "Use remote filter sigma for initial sigma")); - - }{auto& thing = output.sigma ; setInited(output, thing, tryGetFromYaml(thing, newYaml, {"1! sigma" }, "Apriori sigma values - if zero, will be initialised using least squares")); - }{auto& thing = output.apriori_value ; setInited(output, thing, tryGetFromYaml(thing, newYaml, {"3! apriori_value" }, "Apriori state values")); - }{auto& thing = output.process_noise ; setInited(output, thing, tryGetFromYaml(thing, newYaml, {"2! process_noise" }, "Process noise sigmas")); - }{auto& thing = output.tau ; setInited(output, thing, tryGetFromYaml(thing, newYaml, {"@ tau" }, "Correlation times for gauss markov noise, defaults to -1 -> inf (Random Walk)")); - }{auto& thing = output.mu ; setInited(output, thing, tryGetFromYaml(thing, newYaml, {"@ mu" }, "Desired mean value for gauss markov states")); - }{auto& thing = output.comment ; setInited(output, thing, tryGetFromYaml(thing, newYaml, {"@ comment" }, "Comment to apply to the state")); - - }{auto& thing = proc_noise_dt ; tryGetEnumOpt( thing, newYaml, {"2@ process_noise_dt" }, "Time unit for process noise"); - } - - if (isInited(output, output.process_noise)) - { - for (auto& proc : output.process_noise) - { - proc /= sqrt((int)proc_noise_dt); - } - } - - if (isInited(output, output.tau)) - { - for (auto& tau : output.tau) - { - tau *= (int)proc_noise_dt; - } - } + KalmanModel& output, ///< Variable to output to + NodeStack& yaml, ///< Yaml node to search within + const string& key, ///< Key of yaml object + const string& comment = "", ///< Description to provide to user for automatic documentation + bool skippable = false ///< Optionally skip this when yaml object not found in file +) +{ + auto newYaml = stringsToYamlObject(yaml, {key}, comment); + + auto& [optNode, stack] = newYaml; + + if (skippable && !optNode) + { + return; + } + + E_Period proc_noise_dt = E_Period::SECOND; + double proc_noise_dt_f = 1.0; // in seconds + + { + auto& thing = output.estimate; + setInited( + output, + thing, + tryGetFromYaml(thing, newYaml, {"0! estimated"}, "Estimate state in kalman filter") + ); + } + { + auto& thing = output.use_remote_sigma; + setInited( + output, + thing, + tryGetFromYaml( + thing, + newYaml, + {"4@ use_remote_sigma"}, + "Use remote filter sigma for initial sigma" + ) + ); + } + { + auto& thing = output.sigma; + setInited( + output, + thing, + tryGetFromYaml( + thing, + newYaml, + {"1! sigma"}, + "Apriori sigma values - if zero, will be initialised using least squares" + ) + ); + } + { + auto& thing = output.apriori_value; + setInited( + output, + thing, + tryGetFromYaml(thing, newYaml, {"3! apriori_value"}, "Apriori state values") + ); + } + { + auto& thing = output.process_noise; + setInited( + output, + thing, + tryGetFromYaml(thing, newYaml, {"2! process_noise"}, "Process noise sigmas") + ); + } + { + auto& thing = output.sigma_limit; + setInited( + output, + thing, + tryGetFromYaml( + thing, + newYaml, + {"@ sigma_limit"}, + "Maximum sigma before the state is removed" + ) + ); + } + { + auto& thing = output.outage_limit; + setInited( + output, + thing, + tryGetFromYaml( + thing, + newYaml, + {"@ outage_limit"}, + "Maximum unestimated time before the state is removed" + ) + ); + } + { + auto& thing = output.tau; + setInited( + output, + thing, + tryGetFromYaml( + thing, + newYaml, + {"@ tau"}, + "Correlation times for gauss markov noise, defaults to -1 -> inf (Random Walk)" + ) + ); + } + { + auto& thing = output.mu; + setInited( + output, + thing, + tryGetFromYaml(thing, newYaml, {"@ mu"}, "Desired mean value for gauss markov states") + ); + } + { + auto& thing = output.comment; + setInited( + output, + thing, + tryGetFromYaml(thing, newYaml, {"@ comment"}, "Comment to apply to the state") + ); + } + { + auto& thing = proc_noise_dt; + tryGetEnumOpt(thing, newYaml, {"2@ process_noise_dt"}, "Time unit for process noise"); + proc_noise_dt_f = periodToSeconds(proc_noise_dt); + } + + if (isInited(output, output.process_noise)) + { + for (auto& proc : output.process_noise) + { + proc /= sqrt(proc_noise_dt_f); + } + } + + if (isInited(output, output.tau)) + { + for (auto& tau : output.tau) + { + tau *= proc_noise_dt_f; + } + } } bool tryGetMappedList( - map>& mappedList, - boost::program_options::variables_map& commandOpts, ///< Command line object to search within - NodeStack& yaml, - const string& key, - const string& prefix, - const string& comment = "") + map>& mappedList, + boost::program_options::variables_map& commandOpts, ///< Command line object to search within + NodeStack& yaml, + const string& key, + const string& prefix, + const string& comment = "" +) { - auto [outStreamNode, outStreamString] = stringsToYamlObject(yaml, {key}); + auto [outStreamNode, outStreamString] = stringsToYamlObject(yaml, {key}); - bool found = false; + bool found = false; - for (auto outLabelYaml : outStreamNode) - { - found = true; + for (auto outLabelYaml : outStreamNode) + { + found = true; - if (outLabelYaml.IsScalar()) - { - string value = outLabelYaml.as(); + if (outLabelYaml.IsScalar()) + { + string value = outLabelYaml.as(); - conditionalPrefix(prefix, value); + conditionalPrefix(prefix, value); - mappedList[""].push_back(value); - } - if (outLabelYaml.IsMap()) - { - for (auto it = outLabelYaml.begin(); it != outLabelYaml.end(); it++) - { - string key = it->first.as(); + mappedList[""].push_back(value); + } + if (outLabelYaml.IsMap()) + { + for (auto it = outLabelYaml.begin(); it != outLabelYaml.end(); it++) + { + string key = it->first.as(); - mappedList[key] = it->second.as>(); - conditionalPrefix(prefix, mappedList[key]); - } - } - } + mappedList[key] = it->second.as>(); + conditionalPrefix(prefix, mappedList[key]); + } + } + } - vector optsList; - found |= tryGetFromOpts(optsList, commandOpts, {key}); + vector optsList; + found |= tryGetFromOpts(optsList, commandOpts, {key}); - for (auto& value : optsList) - { - conditionalPrefix(prefix, value); + for (auto& value : optsList) + { + conditionalPrefix(prefix, value); - if (std::find(mappedList[""].begin(), mappedList[""].end(), value) != mappedList[""].end()) - { - continue; - } + if (std::find(mappedList[""].begin(), mappedList[""].end(), value) != + mappedList[""].end()) + { + continue; + } - mappedList[""].push_back(value); - } + mappedList[""].push_back(value); + } - return found; + return found; } - /** Set the variables associated with an output stream -*/ + */ void tryGetStreamFromYaml( - SsrBroadcast& outStreamData, ///< Variable to output to - NodeStack& yaml, ///< Yaml node to search within - const string& id) ///< Label associated with the stream + SsrBroadcast& outStreamData, ///< Variable to output to + NodeStack& yaml, ///< Yaml node to search within + const string& id ///< Label associated with the stream +) { - auto outStreamsYaml = stringsToYamlObject(yaml, {id}); - - tryGetFromYaml(outStreamData.url, outStreamsYaml, {"0@ url"}, "Url of caster to send messages to"); - - for (auto msgType : RtcmMessageType::_values()) - { - if (msgType == +RtcmMessageType::IGS_SSR) - for (auto subType : IgsSSRSubtype::_values()) - { - string str = (boost::format("@ rtcm_%4d_%03d") % msgType._to_integral() % subType._to_integral()).str(); - - auto msgOptions = stringsToYamlObject(outStreamsYaml, {"0@ messages", str}, "Message type to output"); - - bool found = tryGetFromYaml(outStreamData.rtcmMsgOptsMap[msgType].igs_udi[subType], msgOptions, {"0@ udi"}, "Update interval"); - if (found) - outStreamData.rtcmMsgOptsMap[msgType].udi = 1; - } - - else if (msgType == +RtcmMessageType::COMPACT_SSR) - for (auto subType : CompactSSRSubtype::_values()) - { - string str = (boost::format("@ rtcm_%4d_%02d") % msgType._to_integral() % subType._to_integral()).str(); - - auto msgOptions = stringsToYamlObject(outStreamsYaml, {"0@ messages", str}, "Message type to output"); - - bool found = tryGetFromYaml(outStreamData.rtcmMsgOptsMap[msgType].comp_udi[subType], msgOptions, {"0@ udi"}, "Update interval"); - if (found) - outStreamData.rtcmMsgOptsMap[msgType].udi = 1; - } - - else - { - string str = "@ rtcm_" + std::to_string(msgType); - - auto msgOptions = stringsToYamlObject(outStreamsYaml, {"0@ messages", str}, "Message type to output"); - - tryGetFromYaml(outStreamData.rtcmMsgOptsMap[msgType].udi, msgOptions, {"0@ udi"}, "Update interval"); - } - } - - tryGetFromYaml(outStreamData.itrf_datum, outStreamsYaml, {"itrf_datum" }); - tryGetFromYaml(outStreamData.provider_id, outStreamsYaml, {"provider_id" }); - tryGetFromYaml(outStreamData.solution_id, outStreamsYaml, {"solution_id" }); + auto outStreamsYaml = stringsToYamlObject(yaml, {id}); + + tryGetFromYaml( + outStreamData.url, + outStreamsYaml, + {"0@ url"}, + "Url of caster to send messages to" + ); + + for (auto msgType : magic_enum::enum_values()) + { + if (msgType == RtcmMessageType::IGS_SSR) + for (auto subType : magic_enum::enum_values()) + { + string str = (boost::format("@ rtcm_%4d_%03d") % static_cast(msgType) % + static_cast(subType)) + .str(); + + auto msgOptions = stringsToYamlObject( + outStreamsYaml, + {"0@ messages", str}, + "Message type to output" + ); + + int udi = 0; + bool found = tryGetFromYaml(udi, msgOptions, {"0@ udi"}, "Update interval"); + if (found) + { + outStreamData.rtcmMsgOptsMap[msgType].igs_udi[subType] = udi; + outStreamData.rtcmMsgOptsMap[msgType].udi = 1; + } + } + + else if (msgType == RtcmMessageType::COMPACT_SSR) + for (auto subType : magic_enum::enum_values()) + { + string str = (boost::format("@ rtcm_%4d_%02d") % static_cast(msgType) % + static_cast(subType)) + .str(); + + auto msgOptions = stringsToYamlObject( + outStreamsYaml, + {"0@ messages", str}, + "Message type to output" + ); + + int udi = 0; + bool found = tryGetFromYaml(udi, msgOptions, {"0@ udi"}, "Update interval"); + if (found) + { + outStreamData.rtcmMsgOptsMap[msgType].comp_udi[subType] = udi; + outStreamData.rtcmMsgOptsMap[msgType].udi = 1; + } + } + + else + { + string str = "@ rtcm_" + std::to_string(static_cast(msgType)); + + auto msgOptions = + stringsToYamlObject(outStreamsYaml, {"0@ messages", str}, "Message type to output"); + + int udi = 0; + bool found = tryGetFromYaml(udi, msgOptions, {"0@ udi"}, "Update interval"); + if (found) + { + outStreamData.rtcmMsgOptsMap[msgType].udi = udi; + } + } + } + + tryGetFromYaml(outStreamData.itrf_datum, outStreamsYaml, {"itrf_datum"}); + tryGetFromYaml(outStreamData.provider_id, outStreamsYaml, {"provider_id"}); + tryGetFromYaml(outStreamData.solution_id, outStreamsYaml, {"solution_id"}); } - -const string estimation_parameters_str = "4! estimation_parameters"; -const string processing_options_str = "2! processing_options"; - +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. + * 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< - typename CONTAINER, - typename ELEMENT> -bool initIfNeeded( - CONTAINER& thisContainer, - const CONTAINER& thatContainer, - ELEMENT& thisElement) +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)); + 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; + auto& thatElement = *thatElement_ptr; - if (isInited(thatContainer, thatElement)) - { - thisElement = thatElement; + if (isInited(thatContainer, thatElement)) + { + thisElement = thatElement; - setInited(thisContainer, thisElement); + setInited(thisContainer, thisElement); - return true; - } + return true; + } - return false; + return false; } - - - - - - -CommonOptions& CommonOptions::operator+=( - const CommonOptions& rhs) +CommonOptions& CommonOptions::operator+=(const CommonOptions& rhs) { - initIfNeeded(*this, rhs, exclude ); - initIfNeeded(*this, rhs, pseudo_sigma ); - initIfNeeded(*this, rhs, laser_sigma ); + initIfNeeded(*this, rhs, exclude); + initIfNeeded(*this, rhs, pseudo_sigma); + initIfNeeded(*this, rhs, laser_sigma); - initIfNeeded(*this, rhs, clock_codes ); - initIfNeeded(*this, rhs, apriori_sigma_enu ); - initIfNeeded(*this, rhs, mincon_scale_apriori_sigma ); - initIfNeeded(*this, rhs, mincon_scale_filter_sigma ); + initIfNeeded(*this, rhs, clock_codes); + initIfNeeded(*this, rhs, apriori_sigma_enu); + initIfNeeded(*this, rhs, mincon_scale_apriori_sigma); + initIfNeeded(*this, rhs, mincon_scale_filter_sigma); - initIfNeeded(*this, rhs, antenna_boresight ); - initIfNeeded(*this, rhs, antenna_azimuth ); + initIfNeeded(*this, rhs, antenna_boresight); + initIfNeeded(*this, rhs, antenna_azimuth); - initIfNeeded(*this, rhs, ellipse_propagation_time_tolerance ); + initIfNeeded(*this, rhs, ellipse_propagation_time_tolerance); - initIfNeeded(*this, rhs, posModel.enable ); - initIfNeeded(*this, rhs, posModel.sources ); - initIfNeeded(*this, rhs, clockModel.enable ); - initIfNeeded(*this, rhs, clockModel.sources ); + initIfNeeded(*this, rhs, posModel.enable); + initIfNeeded(*this, rhs, posModel.sources); + initIfNeeded(*this, rhs, clockModel.enable); + initIfNeeded(*this, rhs, clockModel.sources); - initIfNeeded(*this, rhs, attitudeModel.enable ); - initIfNeeded(*this, rhs, attitudeModel.sources ); - initIfNeeded(*this, rhs, attitudeModel.model_dt ); + initIfNeeded(*this, rhs, attitudeModel.enable); + initIfNeeded(*this, rhs, attitudeModel.sources); + initIfNeeded(*this, rhs, attitudeModel.model_dt); - initIfNeeded(*this, rhs, codeBiasModel.enable ); - initIfNeeded(*this, rhs, codeBiasModel.default_bias ); - initIfNeeded(*this, rhs, codeBiasModel.undefined_sigma ); - initIfNeeded(*this, rhs, phaseBiasModel.enable ); - initIfNeeded(*this, rhs, phaseBiasModel.default_bias ); - initIfNeeded(*this, rhs, phaseBiasModel.undefined_sigma ); + initIfNeeded(*this, rhs, codeBiasModel.enable); + initIfNeeded(*this, rhs, codeBiasModel.default_bias); + initIfNeeded(*this, rhs, codeBiasModel.undefined_sigma); + initIfNeeded(*this, rhs, phaseBiasModel.enable); + initIfNeeded(*this, rhs, phaseBiasModel.default_bias); + initIfNeeded(*this, rhs, phaseBiasModel.undefined_sigma); - initIfNeeded(*this, rhs, pcoModel.enable ); - initIfNeeded(*this, rhs, pcvModel.enable ); - initIfNeeded(*this, rhs, phaseWindupModel.enable ); + initIfNeeded(*this, rhs, pcoModel.enable); + initIfNeeded(*this, rhs, pcvModel.enable); + initIfNeeded(*this, rhs, phaseWindupModel.enable); - return *this; + return *this; } - -OrbitOptions& OrbitOptions::operator+=( - const OrbitOptions& rhs) +OrbitOptions& OrbitOptions::operator+=(const OrbitOptions& rhs) { - initIfNeeded(*this, rhs, mass ); - initIfNeeded(*this, rhs, area ); - initIfNeeded(*this, rhs, power ); - initIfNeeded(*this, rhs, srp_cr ); - - initIfNeeded(*this, rhs, planetary_perturbations ); - initIfNeeded(*this, rhs, empirical ); - initIfNeeded(*this, rhs, antenna_thrust ); - initIfNeeded(*this, rhs, albedo ); - initIfNeeded(*this, rhs, solar_radiation_pressure ); - - initIfNeeded(*this, rhs, empirical_dyb_eclipse ); - initIfNeeded(*this, rhs, empirical_rtn_eclipse ); - - initIfNeeded(*this, rhs, surface_details ); - - initIfNeeded(*this, rhs, pseudoPulses.enable ); - initIfNeeded(*this, rhs, pseudoPulses.interval ); - initIfNeeded(*this, rhs, pseudoPulses.pos_proc_noise ); - initIfNeeded(*this, rhs, pseudoPulses.vel_proc_noise ); - - return *this; + initIfNeeded(*this, rhs, mass); + initIfNeeded(*this, rhs, area); + initIfNeeded(*this, rhs, power); + initIfNeeded(*this, rhs, srp_cr); + initIfNeeded(*this, rhs, drag_cd); + + initIfNeeded(*this, rhs, planetary_perturbations); + initIfNeeded(*this, rhs, empirical); + initIfNeeded(*this, rhs, antenna_thrust); + initIfNeeded(*this, rhs, albedo); + initIfNeeded(*this, rhs, solar_radiation_pressure); + initIfNeeded(*this, rhs, drag); + + initIfNeeded(*this, rhs, empirical_dyb_eclipse); + initIfNeeded(*this, rhs, empirical_rtn_eclipse); + + initIfNeeded(*this, rhs, surface_details); + + initIfNeeded(*this, rhs, pseudoPulses.enable); + initIfNeeded(*this, rhs, pseudoPulses.interval); + initIfNeeded(*this, rhs, pseudoPulses.epochs); + initIfNeeded(*this, rhs, pseudoPulses.pos_proc_noise); + initIfNeeded(*this, rhs, pseudoPulses.vel_proc_noise); + + return *this; } - -KalmanModel& KalmanModel::operator+=( - const KalmanModel& rhs) +KalmanModel& KalmanModel::operator+=(const KalmanModel& rhs) { - initIfNeeded(*this, rhs, sigma ); - initIfNeeded(*this, rhs, apriori_value ); - initIfNeeded(*this, rhs, process_noise ); - initIfNeeded(*this, rhs, tau ); - initIfNeeded(*this, rhs, mu ); - initIfNeeded(*this, rhs, use_remote_sigma); - initIfNeeded(*this, rhs, estimate ); - initIfNeeded(*this, rhs, comment ); - - return *this; + initIfNeeded(*this, rhs, sigma); + initIfNeeded(*this, rhs, sigma_limit); + initIfNeeded(*this, rhs, outage_limit); + initIfNeeded(*this, rhs, apriori_value); + initIfNeeded(*this, rhs, process_noise); + initIfNeeded(*this, rhs, tau); + initIfNeeded(*this, rhs, mu); + initIfNeeded(*this, rhs, use_remote_sigma); + initIfNeeded(*this, rhs, estimate); + initIfNeeded(*this, rhs, comment); + + return *this; } - -SatelliteOptions& SatelliteOptions::operator+=( - const SatelliteOptions& 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 ); - initIfNeeded(*this, rhs, phase_sigma ); + initIfNeeded(*this, rhs, error_model); + initIfNeeded(*this, rhs, code_sigma); + initIfNeeded(*this, rhs, phase_sigma); - inheritedFrom[rhs.id] = (SatelliteOptions*) &rhs; + inheritedFrom[rhs.id] = (SatelliteOptions*)&rhs; - return *this; + return *this; } - -ReceiverOptions& ReceiverOptions::operator+=( - const ReceiverOptions& rhs) -{ - ReceiverKalmans ::operator+=(rhs); - CommonOptions ::operator+=(rhs); - - rinex23Conv += rhs.rinex23Conv; - - initIfNeeded(*this, rhs, kill ); - initIfNeeded(*this, rhs, zero_dcb_codes ); - initIfNeeded(*this, rhs, apriori_pos ); - initIfNeeded(*this, rhs, antenna_type ); - initIfNeeded(*this, rhs, receiver_type ); - initIfNeeded(*this, rhs, sat_id ); - initIfNeeded(*this, rhs, elevation_mask_deg ); - initIfNeeded(*this, rhs, receiver_reference_system ); - - initIfNeeded(*this, rhs, eccentricityModel.enable ); - initIfNeeded(*this, rhs, eccentricityModel.eccentricity ); - - initIfNeeded(*this, rhs, tropModel.enable ); - initIfNeeded(*this, rhs, tropModel.models ); - - initIfNeeded(*this, rhs, tideModels.enable ); - initIfNeeded(*this, rhs, tideModels.solid ); - initIfNeeded(*this, rhs, tideModels.otl ); - initIfNeeded(*this, rhs, tideModels.atl ); - initIfNeeded(*this, rhs, tideModels.spole ); - initIfNeeded(*this, rhs, tideModels.opole ); - - initIfNeeded(*this, rhs, range ); - initIfNeeded(*this, rhs, relativity ); - initIfNeeded(*this, rhs, relativity2 ); - initIfNeeded(*this, rhs, sagnac ); - initIfNeeded(*this, rhs, integer_ambiguity ); - initIfNeeded(*this, rhs, ionospheric_component ); - initIfNeeded(*this, rhs, ionospheric_component2 ); - initIfNeeded(*this, rhs, ionospheric_component3 ); - initIfNeeded(*this, rhs, ionospheric_model ); - initIfNeeded(*this, rhs, tropospheric_map ); - initIfNeeded(*this, rhs, eop ); - - initIfNeeded(*this, rhs, mapping_function ); - initIfNeeded(*this, rhs, geomagnetic_field_height ); - initIfNeeded(*this, rhs, mapping_function_layer_height ); - initIfNeeded(*this, rhs, iono_sigma_limit ); - - initIfNeeded(*this, rhs, error_model ); - initIfNeeded(*this, rhs, code_sigma ); - initIfNeeded(*this, rhs, phase_sigma ); - - inheritedFrom[rhs.id] = (ReceiverOptions*) &rhs; - - return *this; +ReceiverOptions& ReceiverOptions::operator+=(const ReceiverOptions& rhs) +{ + ReceiverKalmans ::operator+=(rhs); + CommonOptions :: operator+=(rhs); + + rinex23Conv += rhs.rinex23Conv; + + initIfNeeded(*this, rhs, kill); + initIfNeeded(*this, rhs, zero_dcb_codes); + initIfNeeded(*this, rhs, apriori_pos); + initIfNeeded(*this, rhs, antenna_type); + initIfNeeded(*this, rhs, receiver_type); + initIfNeeded(*this, rhs, domes_number); + initIfNeeded(*this, rhs, site_description); + + initIfNeeded(*this, rhs, sat_id); + initIfNeeded(*this, rhs, elevation_mask_deg); + initIfNeeded(*this, rhs, receiver_reference_system); + + initIfNeeded(*this, rhs, eccentricityModel.enable); + initIfNeeded(*this, rhs, eccentricityModel.eccentricity); + + initIfNeeded(*this, rhs, tropModel.enable); + initIfNeeded(*this, rhs, tropModel.models); + + initIfNeeded(*this, rhs, tideModels.enable); + initIfNeeded(*this, rhs, tideModels.solid); + initIfNeeded(*this, rhs, tideModels.otl); + initIfNeeded(*this, rhs, tideModels.atl); + initIfNeeded(*this, rhs, tideModels.spole); + initIfNeeded(*this, rhs, tideModels.opole); + + initIfNeeded(*this, rhs, range); + initIfNeeded(*this, rhs, relativity); + initIfNeeded(*this, rhs, relativity2); + initIfNeeded(*this, rhs, sagnac); + initIfNeeded(*this, rhs, integer_ambiguity); + initIfNeeded(*this, rhs, ionospheric_component); + initIfNeeded(*this, rhs, ionospheric_component2); + initIfNeeded(*this, rhs, ionospheric_component3); + initIfNeeded(*this, rhs, ionospheric_model); + initIfNeeded(*this, rhs, tropospheric_map); + initIfNeeded(*this, rhs, eop); + + initIfNeeded(*this, rhs, mapping_function); + initIfNeeded(*this, rhs, geomagnetic_field_height); + initIfNeeded(*this, rhs, mapping_function_layer_height); + + initIfNeeded(*this, rhs, error_model); + initIfNeeded(*this, rhs, code_sigma); + initIfNeeded(*this, rhs, phase_sigma); + + inheritedFrom[rhs.id] = (ReceiverOptions*)&rhs; + + return *this; } /** Set inertial force options from yaml -*/ + */ void tryGetKalmanFromYaml( - InertialKalmans& inertialOpts, ///< Inertail options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& descriptorVec) ///< List of strings of keys of yaml hierarchy -{ - auto inrNode = stringsToYamlObject(yamlBase, descriptorVec); - - tryGetKalmanFromYaml(inertialOpts.orientation, inrNode, "7@ orientation"); - tryGetKalmanFromYaml(inertialOpts.gyro_bias, inrNode, "7@ gyro_bias"); - tryGetKalmanFromYaml(inertialOpts.accelerometer_bias, inrNode, "7@ accelerometer_bias"); - tryGetKalmanFromYaml(inertialOpts.gyro_scale, inrNode, "7@ gyro_scale"); - tryGetKalmanFromYaml(inertialOpts.accelerometer_scale, inrNode, "7@ accelerometer_scale"); - tryGetKalmanFromYaml(inertialOpts.imu_offset, inrNode, "7@ imu_offset"); + InertialKalmans& inertialOpts, ///< Inertail options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) +{ + auto inrNode = stringsToYamlObject(yamlBase, descriptorVec); + + tryGetKalmanFromYaml(inertialOpts.orientation, inrNode, "7@ orientation"); + tryGetKalmanFromYaml(inertialOpts.gyro_bias, inrNode, "7@ gyro_bias"); + tryGetKalmanFromYaml(inertialOpts.accelerometer_bias, inrNode, "7@ accelerometer_bias"); + tryGetKalmanFromYaml(inertialOpts.gyro_scale, inrNode, "7@ gyro_scale"); + tryGetKalmanFromYaml(inertialOpts.accelerometer_scale, inrNode, "7@ accelerometer_scale"); + tryGetKalmanFromYaml(inertialOpts.imu_offset, inrNode, "7@ imu_offset"); } /** Set empirical force options from yaml -*/ + */ void tryGetKalmanFromYaml( - EmpKalmans& empOpts, ///< Empirical options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& descriptorVec) ///< List of strings of keys of yaml hierarchy -{ - auto empNode = stringsToYamlObject(yamlBase, descriptorVec); - - tryGetKalmanFromYaml(empOpts.emp_d_0, empNode, "6@ emp_d_0", "Empirical accleration direct bias "); - tryGetKalmanFromYaml(empOpts.emp_d_1, empNode, "6@ emp_d_1", "Empirical accleration direct 1 per rev"); - tryGetKalmanFromYaml(empOpts.emp_d_2, empNode, "6@ emp_d_2", "Empirical accleration direct 2 per rev"); - tryGetKalmanFromYaml(empOpts.emp_d_3, empNode, "6@ emp_d_3", "Empirical accleration direct 3 per rev"); - tryGetKalmanFromYaml(empOpts.emp_d_4, empNode, "6@ emp_d_4", "Empirical accleration direct 4 per rev"); - - tryGetKalmanFromYaml(empOpts.emp_y_0, empNode, "6@ emp_y_0", "Empirical accleration Y bias "); - tryGetKalmanFromYaml(empOpts.emp_y_1, empNode, "6@ emp_y_1", "Empirical accleration Y 1 per rev"); - tryGetKalmanFromYaml(empOpts.emp_y_2, empNode, "6@ emp_y_2", "Empirical accleration Y 2 per rev"); - tryGetKalmanFromYaml(empOpts.emp_y_3, empNode, "6@ emp_y_3", "Empirical accleration Y 3 per rev"); - tryGetKalmanFromYaml(empOpts.emp_y_4, empNode, "6@ emp_y_4", "Empirical accleration Y 4 per rev"); - - tryGetKalmanFromYaml(empOpts.emp_b_0, empNode, "6@ emp_b_0", "Empirical accleration B bias "); - tryGetKalmanFromYaml(empOpts.emp_b_1, empNode, "6@ emp_b_1", "Empirical accleration B 1 per rev"); - tryGetKalmanFromYaml(empOpts.emp_b_2, empNode, "6@ emp_b_2", "Empirical accleration B 2 per rev"); - tryGetKalmanFromYaml(empOpts.emp_b_3, empNode, "6@ emp_b_3", "Empirical accleration B 3 per rev"); - tryGetKalmanFromYaml(empOpts.emp_b_4, empNode, "6@ emp_b_4", "Empirical accleration B 4 per rev"); - - tryGetKalmanFromYaml(empOpts.emp_r_0, empNode, "6@ emp_r_0", "Empirical accleration radial bias "); - tryGetKalmanFromYaml(empOpts.emp_r_1, empNode, "6@ emp_r_1", "Empirical accleration radial 1 per rev"); - tryGetKalmanFromYaml(empOpts.emp_r_2, empNode, "6@ emp_r_2", "Empirical accleration radial 2 per rev"); - tryGetKalmanFromYaml(empOpts.emp_r_3, empNode, "6@ emp_r_3", "Empirical accleration radial 3 per rev"); - tryGetKalmanFromYaml(empOpts.emp_r_4, empNode, "6@ emp_r_4", "Empirical accleration radial 4 per rev"); - - tryGetKalmanFromYaml(empOpts.emp_t_0, empNode, "6@ emp_t_0", "Empirical accleration tangential bias "); - tryGetKalmanFromYaml(empOpts.emp_t_1, empNode, "6@ emp_t_1", "Empirical accleration tangential 1 per rev"); - tryGetKalmanFromYaml(empOpts.emp_t_2, empNode, "6@ emp_t_2", "Empirical accleration tangential 2 per rev"); - tryGetKalmanFromYaml(empOpts.emp_t_3, empNode, "6@ emp_t_3", "Empirical accleration tangential 3 per rev"); - tryGetKalmanFromYaml(empOpts.emp_t_4, empNode, "6@ emp_t_4", "Empirical accleration tangential 4 per rev"); - - tryGetKalmanFromYaml(empOpts.emp_n_0, empNode, "6@ emp_n_0", "Empirical accleration normal bias "); - tryGetKalmanFromYaml(empOpts.emp_n_1, empNode, "6@ emp_n_1", "Empirical accleration normal 1 per rev"); - tryGetKalmanFromYaml(empOpts.emp_n_2, empNode, "6@ emp_n_2", "Empirical accleration normal 2 per rev"); - tryGetKalmanFromYaml(empOpts.emp_n_3, empNode, "6@ emp_n_3", "Empirical accleration normal 3 per rev"); - tryGetKalmanFromYaml(empOpts.emp_n_4, empNode, "6@ emp_n_4", "Empirical accleration normal 4 per rev"); - - tryGetKalmanFromYaml(empOpts.emp_p_0, empNode, "6@ emp_p_0", "Empirical accleration P bias "); - tryGetKalmanFromYaml(empOpts.emp_p_1, empNode, "6@ emp_p_1", "Empirical accleration P 1 per rev"); - tryGetKalmanFromYaml(empOpts.emp_p_2, empNode, "6@ emp_p_2", "Empirical accleration P 2 per rev"); - tryGetKalmanFromYaml(empOpts.emp_p_3, empNode, "6@ emp_p_3", "Empirical accleration P 3 per rev"); - tryGetKalmanFromYaml(empOpts.emp_p_4, empNode, "6@ emp_p_4", "Empirical accleration P 4 per rev"); - - tryGetKalmanFromYaml(empOpts.emp_q_0, empNode, "6@ emp_q_0", "Empirical accleration Q bias "); - tryGetKalmanFromYaml(empOpts.emp_q_1, empNode, "6@ emp_q_1", "Empirical accleration Q 1 per rev"); - tryGetKalmanFromYaml(empOpts.emp_q_2, empNode, "6@ emp_q_2", "Empirical accleration Q 2 per rev"); - tryGetKalmanFromYaml(empOpts.emp_q_3, empNode, "6@ emp_q_3", "Empirical accleration Q 3 per rev"); - tryGetKalmanFromYaml(empOpts.emp_q_4, empNode, "6@ emp_q_4", "Empirical accleration Q 4 per rev"); + EmpKalmans& empOpts, ///< Empirical options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) +{ + auto empNode = stringsToYamlObject(yamlBase, descriptorVec); + + tryGetKalmanFromYaml( + empOpts.emp_d_0, + empNode, + "6@ emp_d_0", + "Empirical accleration direct bias " + ); + tryGetKalmanFromYaml( + empOpts.emp_d_1, + empNode, + "6@ emp_d_1", + "Empirical accleration direct 1 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_d_2, + empNode, + "6@ emp_d_2", + "Empirical accleration direct 2 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_d_3, + empNode, + "6@ emp_d_3", + "Empirical accleration direct 3 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_d_4, + empNode, + "6@ emp_d_4", + "Empirical accleration direct 4 per rev" + ); + + tryGetKalmanFromYaml(empOpts.emp_y_0, empNode, "6@ emp_y_0", "Empirical accleration Y bias "); + tryGetKalmanFromYaml( + empOpts.emp_y_1, + empNode, + "6@ emp_y_1", + "Empirical accleration Y 1 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_y_2, + empNode, + "6@ emp_y_2", + "Empirical accleration Y 2 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_y_3, + empNode, + "6@ emp_y_3", + "Empirical accleration Y 3 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_y_4, + empNode, + "6@ emp_y_4", + "Empirical accleration Y 4 per rev" + ); + + tryGetKalmanFromYaml(empOpts.emp_b_0, empNode, "6@ emp_b_0", "Empirical accleration B bias "); + tryGetKalmanFromYaml( + empOpts.emp_b_1, + empNode, + "6@ emp_b_1", + "Empirical accleration B 1 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_b_2, + empNode, + "6@ emp_b_2", + "Empirical accleration B 2 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_b_3, + empNode, + "6@ emp_b_3", + "Empirical accleration B 3 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_b_4, + empNode, + "6@ emp_b_4", + "Empirical accleration B 4 per rev" + ); + + tryGetKalmanFromYaml( + empOpts.emp_r_0, + empNode, + "6@ emp_r_0", + "Empirical accleration radial bias " + ); + tryGetKalmanFromYaml( + empOpts.emp_r_1, + empNode, + "6@ emp_r_1", + "Empirical accleration radial 1 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_r_2, + empNode, + "6@ emp_r_2", + "Empirical accleration radial 2 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_r_3, + empNode, + "6@ emp_r_3", + "Empirical accleration radial 3 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_r_4, + empNode, + "6@ emp_r_4", + "Empirical accleration radial 4 per rev" + ); + + tryGetKalmanFromYaml( + empOpts.emp_t_0, + empNode, + "6@ emp_t_0", + "Empirical accleration tangential bias " + ); + tryGetKalmanFromYaml( + empOpts.emp_t_1, + empNode, + "6@ emp_t_1", + "Empirical accleration tangential 1 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_t_2, + empNode, + "6@ emp_t_2", + "Empirical accleration tangential 2 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_t_3, + empNode, + "6@ emp_t_3", + "Empirical accleration tangential 3 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_t_4, + empNode, + "6@ emp_t_4", + "Empirical accleration tangential 4 per rev" + ); + + tryGetKalmanFromYaml( + empOpts.emp_n_0, + empNode, + "6@ emp_n_0", + "Empirical accleration normal bias " + ); + tryGetKalmanFromYaml( + empOpts.emp_n_1, + empNode, + "6@ emp_n_1", + "Empirical accleration normal 1 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_n_2, + empNode, + "6@ emp_n_2", + "Empirical accleration normal 2 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_n_3, + empNode, + "6@ emp_n_3", + "Empirical accleration normal 3 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_n_4, + empNode, + "6@ emp_n_4", + "Empirical accleration normal 4 per rev" + ); + + tryGetKalmanFromYaml(empOpts.emp_p_0, empNode, "6@ emp_p_0", "Empirical accleration P bias "); + tryGetKalmanFromYaml( + empOpts.emp_p_1, + empNode, + "6@ emp_p_1", + "Empirical accleration P 1 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_p_2, + empNode, + "6@ emp_p_2", + "Empirical accleration P 2 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_p_3, + empNode, + "6@ emp_p_3", + "Empirical accleration P 3 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_p_4, + empNode, + "6@ emp_p_4", + "Empirical accleration P 4 per rev" + ); + + tryGetKalmanFromYaml(empOpts.emp_q_0, empNode, "6@ emp_q_0", "Empirical accleration Q bias "); + tryGetKalmanFromYaml( + empOpts.emp_q_1, + empNode, + "6@ emp_q_1", + "Empirical accleration Q 1 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_q_2, + empNode, + "6@ emp_q_2", + "Empirical accleration Q 2 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_q_3, + empNode, + "6@ emp_q_3", + "Empirical accleration Q 3 per rev" + ); + tryGetKalmanFromYaml( + empOpts.emp_q_4, + empNode, + "6@ emp_q_4", + "Empirical accleration Q 4 per rev" + ); } /** Set common options from yaml -*/ + */ void tryGetKalmanFromYaml( - CommonKalmans& comOpts, ///< Receiver options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& descriptorVec) ///< List of strings of keys of yaml hierarchy -{ - auto comNode = stringsToYamlObject(yamlBase, descriptorVec); - - tryGetKalmanFromYaml(comOpts.clk, comNode, "1! clock", "Clocks"); - tryGetKalmanFromYaml(comOpts.clk_rate, comNode, "1@ clock_rate", "Clock rates"); - tryGetKalmanFromYaml(comOpts.pos, comNode, "1! pos", "Position"); - tryGetKalmanFromYaml(comOpts.pos_rate, comNode, "1! pos_rate", "Velocity"); - tryGetKalmanFromYaml(comOpts.orbit, comNode, "2@ orbit", "Orbital state"); - tryGetKalmanFromYaml(comOpts.pco, comNode, "3# pco", "Phase Center Offsets (experimental)"); - tryGetKalmanFromYaml(comOpts.code_bias, comNode, "4! code_bias", "Code bias"); - tryGetKalmanFromYaml(comOpts.phase_bias, comNode, "4! phase_bias", "Phase bias"); - tryGetKalmanFromYaml(comOpts.ant_delta, comNode, "4! ant_delta", "Antenna delta (body frame)"); + CommonKalmans& comOpts, ///< Receiver options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) +{ + auto comNode = stringsToYamlObject(yamlBase, descriptorVec); + + tryGetKalmanFromYaml(comOpts.clk, comNode, "1! clock", "Clocks"); + tryGetKalmanFromYaml(comOpts.clk_rate, comNode, "1@ clock_rate", "Clock rates"); + tryGetKalmanFromYaml(comOpts.pos, comNode, "1! pos", "Position"); + tryGetKalmanFromYaml(comOpts.pos_rate, comNode, "1! pos_rate", "Velocity"); + tryGetKalmanFromYaml(comOpts.orbit, comNode, "2@ orbit", "Orbital state"); + tryGetKalmanFromYaml(comOpts.pco, comNode, "3# pco", "Phase Center Offsets (experimental)"); + tryGetKalmanFromYaml(comOpts.code_bias, comNode, "4! code_bias", "Code bias"); + tryGetKalmanFromYaml(comOpts.phase_bias, comNode, "4! phase_bias", "Phase bias"); + tryGetKalmanFromYaml(comOpts.ant_delta, comNode, "4! ant_delta", "Antenna delta (body frame)"); +#ifdef _ESTIMATE_CRCD + tryGetKalmanFromYaml(comOpts.cr, comNode, "2@ cr", "SRP coefficient"); + tryGetKalmanFromYaml(comOpts.cd, comNode, "2@ cd", "Drag coefficient"); +#endif } - - - - - /** Set satellite options from yaml -*/ + */ void getKalmanFromYaml( - SatelliteKalmans& satOpts, ///< Satellite options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& descriptorVec) ///< List of strings of keys of yaml hierarchy + SatelliteKalmans& satOpts, ///< Satellite options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) { - auto satNode = stringsToYamlObject(yamlBase, descriptorVec); + auto satNode = stringsToYamlObject(yamlBase, descriptorVec); - tryGetKalmanFromYaml((InertialKalmans&) satOpts, satNode, {}); - tryGetKalmanFromYaml((CommonKalmans&) satOpts, satNode, {}); - tryGetKalmanFromYaml((EmpKalmans&) satOpts, satNode, {}); + tryGetKalmanFromYaml((InertialKalmans&)satOpts, satNode, {}); + tryGetKalmanFromYaml((CommonKalmans&)satOpts, satNode, {}); + tryGetKalmanFromYaml((EmpKalmans&)satOpts, satNode, {}); } /** Set receiver options from yaml -*/ + */ void getKalmanFromYaml( - ReceiverKalmans& recOpts, ///< Receiver options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& descriptorVec) ///< List of strings of keys of yaml hierarchy -{ - auto recNode = stringsToYamlObject(yamlBase, descriptorVec); - - tryGetKalmanFromYaml((InertialKalmans&) recOpts, recNode, {}); - tryGetKalmanFromYaml((CommonKalmans&) recOpts, recNode, {}); - tryGetKalmanFromYaml((EmpKalmans&) recOpts, recNode, {}); - - tryGetKalmanFromYaml(recOpts.strain_rate, recNode, "8@ strain_rate", "Velocity (large gain, for geodetic timescales)"); - tryGetKalmanFromYaml(recOpts.ambiguity, recNode, "1! ambiguities", "Integer phase ambiguities"); - tryGetKalmanFromYaml(recOpts.pcv, recNode, "3# pcv", "Antenna phase center variations (experimental)"); - tryGetKalmanFromYaml(recOpts.ion_stec, recNode, "1! ion_stec", "Ionospheric slant delay"); - tryGetKalmanFromYaml(recOpts.ion_model, recNode, "3@ ion_model", "Ionospheric mapping"); - tryGetKalmanFromYaml(recOpts.slr_range_bias, recNode, "9@ slr_range_bias", "Satellite Laser Ranging range bias"); - tryGetKalmanFromYaml(recOpts.slr_time_bias, recNode, "9@ slr_time_bias", "Satellite Laser Ranging time bias"); - tryGetKalmanFromYaml(recOpts.trop, recNode, "1! trop", "Troposphere corrections"); - tryGetKalmanFromYaml(recOpts.trop_grads, recNode, "1! trop_grads", "Troposphere gradients"); - tryGetKalmanFromYaml(recOpts.trop_maps, recNode, "1@ trop_maps", "Troposphere ZWD mapping"); + ReceiverKalmans& recOpts, ///< Receiver options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) +{ + auto recNode = stringsToYamlObject(yamlBase, descriptorVec); + + tryGetKalmanFromYaml((InertialKalmans&)recOpts, recNode, {}); + tryGetKalmanFromYaml((CommonKalmans&)recOpts, recNode, {}); + tryGetKalmanFromYaml((EmpKalmans&)recOpts, recNode, {}); + + tryGetKalmanFromYaml( + recOpts.strain_rate, + recNode, + "8@ strain_rate", + "Velocity (large gain, for geodetic timescales)" + ); + tryGetKalmanFromYaml(recOpts.ambiguity, recNode, "1! ambiguities", "Integer phase ambiguities"); + tryGetKalmanFromYaml( + recOpts.pcv, + recNode, + "3# pcv", + "Antenna phase center variations (experimental)" + ); + tryGetKalmanFromYaml(recOpts.ion_stec, recNode, "1! ion_stec", "Ionospheric slant delay"); + tryGetKalmanFromYaml(recOpts.ion_model, recNode, "3@ ion_model", "Ionospheric mapping"); + tryGetKalmanFromYaml( + recOpts.slr_range_bias, + recNode, + "9@ slr_range_bias", + "Satellite Laser Ranging range bias" + ); + tryGetKalmanFromYaml( + recOpts.slr_time_bias, + recNode, + "9@ slr_time_bias", + "Satellite Laser Ranging time bias" + ); + tryGetKalmanFromYaml(recOpts.trop, recNode, "1! trop", "Troposphere corrections"); + tryGetKalmanFromYaml(recOpts.trop_grads, recNode, "1! trop_grads", "Troposphere gradients"); + tryGetKalmanFromYaml(recOpts.trop_maps, recNode, "1@ trop_maps", "Troposphere ZWD mapping"); } - - - - - - - - - /** Set common options from yaml -*/ + */ void getOptionsFromYaml( - OrbitOptions& orbOpts, ///< Satellite options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& descriptorVec) ///< List of strings of keys of yaml hierarchy -{ - auto comNode = stringsToYamlObject(yamlBase, descriptorVec); - - auto& [node, stack] = comNode; - - auto orbitsNode = stringsToYamlObject(comNode, {"@ orbit_propagation" }, "Enable specific orbit propagation models"); - auto pseudo_pulses = stringsToYamlObject(orbitsNode, {"@ pseudo_pulses" }, "Apply process noise to simulate pseudo-stochastic pulses commonly applied in least squares solutions"); - - { - }{ auto& thing = orbOpts.mass ; setInited(orbOpts, thing, tryGetFromYaml (thing, orbitsNode, {"0! mass" }, "Satellite mass for use if not specified in the SINEX metadata file")); - }{ auto& thing = orbOpts.area ; setInited(orbOpts, thing, tryGetFromYaml (thing, orbitsNode, {"0! area" }, "Satellite area for use in solar radiation and albedo calculations")); - }{ auto& thing = orbOpts.power ; setInited(orbOpts, thing, tryGetFromYaml (thing, orbitsNode, {"0@ power" }, "Transmission power use if not specified in the SINEX metadata file")); - }{ auto& thing = orbOpts.srp_cr ; setInited(orbOpts, thing, tryGetFromYaml (thing, orbitsNode, {"0@ srp_cr" }, "Coefficient of reflection of the satellite")); - - - }{ auto& thing = orbOpts.planetary_perturbations ; setInited(orbOpts, thing, tryGetEnumVec (thing, orbitsNode, {"@ planetary_perturbations" }, "Acceleration due to third celestial bodies")); - }{ auto& thing = orbOpts.solar_radiation_pressure ; setInited(orbOpts, thing, tryGetEnumOpt (thing, orbitsNode, {"@ solar_radiation_pressure" }, "Model accelerations due to solar radiation pressure")); - }{ auto& thing = orbOpts.empirical ; setInited(orbOpts, thing, tryGetFromYaml (thing, orbitsNode, {"@ empirical" }, "Model accelerations due to empirical accelerations")); - }{ auto& thing = orbOpts.antenna_thrust ; setInited(orbOpts, thing, tryGetFromYaml (thing, orbitsNode, {"@ antenna_thrust" }, "Model accelerations due to the emitted signal from the antenna")); - }{ auto& thing = orbOpts.albedo ; setInited(orbOpts, thing, tryGetEnumOpt (thing, orbitsNode, {"@ albedo" }, "Model accelerations due to the albedo effect from Earth (Visible and Infra-red)")); - - }{ auto& thing = orbOpts.empirical_dyb_eclipse ; setInited(orbOpts, thing, tryGetFromYaml (thing, orbitsNode, {"@ empirical_dyb_eclipse" }, "Turn on/off the eclipse on each axis (D, Y, B)")); - }{ auto& thing = orbOpts.empirical_rtn_eclipse ; setInited(orbOpts, thing, tryGetFromYaml (thing, orbitsNode, {"@ empirical_rtn_eclipse" }, "Turn on/off the eclipse on each axis (R, T, N)")); - - }{ auto& thing = orbOpts.pseudoPulses.enable ; setInited(orbOpts, thing, tryGetFromYaml (thing, pseudo_pulses, {"@ enable" }, "Enable applying process noise impulses to orbits upon state errors")); - }{ auto& thing = orbOpts.pseudoPulses.interval ; setInited(orbOpts, thing, tryGetFromYaml (thing, pseudo_pulses, {"@ interval" }, "Interval between applying pseudo pulses")); - }{ auto& thing = orbOpts.pseudoPulses.pos_proc_noise ; setInited(orbOpts, thing, tryGetFromYaml (thing, pseudo_pulses, {"@ pos_process_noise" }, "Sigma to add to orbital position states")); - }{ auto& thing = orbOpts.pseudoPulses.vel_proc_noise ; setInited(orbOpts, thing, tryGetFromYaml (thing, pseudo_pulses, {"@ vel_process_noise" }, "Sigma to add to orbital velocity states")); - } - - - bool surfaceFound = false; - vector surface_details; - - auto [surfacesNode, surfacesString] = stringsToYamlObject(comNode, {"6@ surface_details"}, "List of details for srp and drag surfaces"); - - for (auto surfacesYaml : surfacesNode) - { - SurfaceDetails surface; - - tryGetFromYaml(surface.rotation_axis, {surfacesYaml, ""}, {"rotation_axis" }); - tryGetFromYaml(surface.normal, {surfacesYaml, ""}, {"normal" }); - tryGetFromYaml(surface.shape, {surfacesYaml, ""}, {"shape" }); - tryGetFromYaml(surface.area, {surfacesYaml, ""}, {"area" }); - tryGetFromYaml(surface.reflection_visible, {surfacesYaml, ""}, {"reflection_visible" }); - tryGetFromYaml(surface.diffusion_visible, {surfacesYaml, ""}, {"diffusion_visible" }); - tryGetFromYaml(surface.absorption_visible, {surfacesYaml, ""}, {"absorption_visible" }); - tryGetFromYaml(surface.thermal_reemission, {surfacesYaml, ""}, {"thermal_reemission" }); - tryGetFromYaml(surface.reflection_infrared, {surfacesYaml, ""}, {"reflection_infrared" }); - tryGetFromYaml(surface.diffusion_infrared, {surfacesYaml, ""}, {"diffusion_infrared" }); - tryGetFromYaml(surface.absorption_infrared, {surfacesYaml, ""}, {"absorption_infrared" }); - - if ( surface.rotation_axis.empty() == false - &&surface.rotation_axis.size() != 3) - { - - BOOST_LOG_TRIVIAL(warning) << "Warning: rotation_axis is not a vector of size 3 for surface " << stack; - continue; - } - - if ( surface.normal.empty() == false - &&surface.normal.size() != 3) - { - BOOST_LOG_TRIVIAL(warning) << "Error: boxwing surface.normal is not a vector of size 3 for surface " << stack; - continue; - } - - surface_details.push_back(surface); - surfaceFound = true; - } - - if (surfaceFound) - { - orbOpts.surface_details = surface_details; - setInited(orbOpts, orbOpts.surface_details); - } + OrbitOptions& orbOpts, ///< Satellite options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) +{ + auto comNode = stringsToYamlObject(yamlBase, descriptorVec); + + auto& [node, stack] = comNode; + + auto orbitsNode = stringsToYamlObject( + comNode, + {"@ orbit_propagation"}, + "Enable specific orbit propagation models" + ); + auto pseudo_pulses = stringsToYamlObject( + orbitsNode, + {"@ pseudo_pulses"}, + "Apply process noise to simulate pseudo-stochastic pulses commonly applied in least " + "squares solutions" + ); + + { + } + { + auto& thing = orbOpts.mass; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"0! mass"}, + "Satellite mass for use if not specified in the SINEX metadata file" + ) + ); + } + { + auto& thing = orbOpts.area; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"0! area"}, + "Satellite area for use in solar radiation and albedo calculations" + ) + ); + } + { + auto& thing = orbOpts.power; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"0@ power"}, + "Transmission power use if not specified in the SINEX metadata file" + ) + ); + } + { + auto& thing = orbOpts.srp_cr; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"0@ srp_cr"}, + "Coefficient of reflection of the satellite" + ) + ); + } + { + auto& thing = orbOpts.drag_cd; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"0@ drag_cd"}, + "Coefficient of drag of the satellite" + ) + ); + } + { + auto& thing = orbOpts.planetary_perturbations; + setInited( + orbOpts, + thing, + tryGetEnumVec( + thing, + orbitsNode, + {"@ planetary_perturbations"}, + "Acceleration due to third celestial bodies" + ) + ); + } + { + auto& thing = orbOpts.solar_radiation_pressure; + setInited( + orbOpts, + thing, + tryGetEnumOpt( + thing, + orbitsNode, + {"@ solar_radiation_pressure"}, + "Model accelerations due to solar radiation pressure" + ) + ); + } + { + auto& thing = orbOpts.drag; + setInited( + orbOpts, + thing, + tryGetFromYaml(thing, orbitsNode, {"@ drag"}, "Model accelerations due to drag") + ); + } + { + auto& thing = orbOpts.empirical; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"@ empirical"}, + "Model accelerations due to empirical accelerations" + ) + ); + } + { + auto& thing = orbOpts.antenna_thrust; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"@ antenna_thrust"}, + "Model accelerations due to the emitted signal from the antenna" + ) + ); + } + { + auto& thing = orbOpts.albedo; + setInited( + orbOpts, + thing, + tryGetEnumOpt( + thing, + orbitsNode, + {"@ albedo"}, + "Model accelerations due to the albedo effect from Earth (Visible and Infra-red)" + ) + ); + } + { + auto& thing = orbOpts.empirical_dyb_eclipse; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"@ empirical_dyb_eclipse"}, + "Turn on/off the eclipse on each axis (D, Y, B)" + ) + ); + } + { + auto& thing = orbOpts.empirical_rtn_eclipse; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + orbitsNode, + {"@ empirical_rtn_eclipse"}, + "Turn on/off the eclipse on each axis (R, T, N)" + ) + ); + } + { + auto& thing = orbOpts.pseudoPulses.enable; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + pseudo_pulses, + {"@ enable"}, + "Enable applying process noise impulses to orbits upon state errors" + ) + ); + } + { + auto& thing = orbOpts.pseudoPulses.interval; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + pseudo_pulses, + {"@ interval"}, + "Interval between applying pseudo pulses" + ) + ); + } + { + } + { + auto& thing = orbOpts.pseudoPulses.epochs; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + pseudo_pulses, + {"@ epochs"}, + "epochs where pseudo pulses are applied [second of day since midnight GPST]" + ) + ); + } + { + auto& thing = orbOpts.pseudoPulses.pos_proc_noise; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + pseudo_pulses, + {"@ pos_process_noise"}, + "Sigma to add to orbital position states" + ) + ); + } + { + auto& thing = orbOpts.pseudoPulses.vel_proc_noise; + setInited( + orbOpts, + thing, + tryGetFromYaml( + thing, + pseudo_pulses, + {"@ vel_process_noise"}, + "Sigma to add to orbital velocity states" + ) + ); + } + + bool surfaceFound = false; + vector surface_details; + + auto [surfacesNode, surfacesString] = stringsToYamlObject( + comNode, + {"6@ surface_details"}, + "List of details for srp and drag surfaces" + ); + + for (auto surfacesYaml : surfacesNode) + { + SurfaceDetails surface; + + tryGetFromYaml(surface.rotation_axis, {surfacesYaml, ""}, {"rotation_axis"}); + tryGetFromYaml(surface.normal, {surfacesYaml, ""}, {"normal"}); + tryGetFromYaml(surface.shape, {surfacesYaml, ""}, {"shape"}); + tryGetFromYaml(surface.area, {surfacesYaml, ""}, {"area"}); + tryGetFromYaml(surface.reflection_visible, {surfacesYaml, ""}, {"reflection_visible"}); + tryGetFromYaml(surface.diffusion_visible, {surfacesYaml, ""}, {"diffusion_visible"}); + tryGetFromYaml(surface.absorption_visible, {surfacesYaml, ""}, {"absorption_visible"}); + tryGetFromYaml(surface.thermal_reemission, {surfacesYaml, ""}, {"thermal_reemission"}); + tryGetFromYaml(surface.reflection_infrared, {surfacesYaml, ""}, {"reflection_infrared"}); + tryGetFromYaml(surface.diffusion_infrared, {surfacesYaml, ""}, {"diffusion_infrared"}); + tryGetFromYaml(surface.absorption_infrared, {surfacesYaml, ""}, {"absorption_infrared"}); + + if (surface.rotation_axis.empty() == false && surface.rotation_axis.size() != 3) + { + BOOST_LOG_TRIVIAL(warning) + << "Rotation_axis is not a vector of size 3 for surface " << stack; + continue; + } + + if (surface.normal.empty() == false && surface.normal.size() != 3) + { + BOOST_LOG_TRIVIAL(warning) + << "Boxwing surface.normal is not a vector of size 3 for surface " << stack; + continue; + } + + surface_details.push_back(surface); + surfaceFound = true; + } + + if (surfaceFound) + { + orbOpts.surface_details = surface_details; + setInited(orbOpts, orbOpts.surface_details); + } } - - /** Set common options from yaml -*/ + */ void getOptionsFromYaml( - CommonOptions& comOpts, ///< Satellite options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& descriptorVec) ///< List of strings of keys of yaml hierarchy -{ - auto comNode = stringsToYamlObject(yamlBase, descriptorVec); - - auto modelsNode = stringsToYamlObject(comNode, {"9@ models" }, "Enable specific models"); - - vector antenna_boresight; - vector antenna_azimuth; - { - }{ auto& thing = comOpts.exclude ; setInited(comOpts, thing, tryGetFromYaml (thing, comNode, {"0! exclude" }, "Exclude receiver from processing")); - }{ auto& thing = comOpts.pseudo_sigma ; setInited(comOpts, thing, tryGetFromYaml (thing, comNode, {"0@ pseudo_sigma" }, "Standard deviation of pseudo measurmeents")); - }{ auto& thing = comOpts.laser_sigma ; setInited(comOpts, thing, tryGetFromYaml (thing, comNode, {"0@ laser_sigma" }, "Standard deviation of SLR laser measurements")); - }{ auto& thing = comOpts.clock_codes ; setInited(comOpts, thing, tryGetEnumVec (thing, comNode, {"3@ clock_codes" }, "Codes for IF combination based clocks")); - }{ auto& thing = comOpts.apriori_sigma_enu ; setInited(comOpts, thing, tryGetFromYaml (thing, comNode, {"4@ apriori_sigma_enu" }, "Sigma applied for weighting in mincon transformation estimation. (Lower is stronger weighting, Negative is unweighted, ENU separation unsupported for satellites)")); - }{ auto& thing = comOpts.mincon_scale_apriori_sigma ; setInited(comOpts, thing, tryGetFromYaml (thing, comNode, {"4@ mincon_scale_apriori_sigma" }, "Scale applied to apriori sigmas while weighting in mincon transformation estimation")); - }{ auto& thing = comOpts.mincon_scale_filter_sigma ; setInited(comOpts, thing, tryGetFromYaml (thing, comNode, {"4@ mincon_scale_filter_sigma" }, "Scale applied to filter sigmas while weighting in mincon transformation estimation")); - - }{ auto& thing = antenna_boresight ; tryGetFromYaml (thing, comNode, {"@ antenna_boresight" }, "Antenna boresight (Up) in satellite body-fixed frame"); - }{ auto& thing = antenna_azimuth ; tryGetFromYaml (thing, comNode, {"@ antenna_azimuth" }, "Antenna azimuth (North) in satellite body-fixed frame"); - - }{ auto& thing = comOpts.ellipse_propagation_time_tolerance; setInited(comOpts, thing, tryGetFromYaml (thing, comNode, {"@ ellipse_propagation_time_tolerance" }, "Time gap tolerance under which the ellipse propagator can be used for orbit prediction")); - - }{ auto& thing = comOpts.posModel.enable ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ pos", "@ enable" }, "Enable modelling of position")); - }{ auto& thing = comOpts.posModel.sources ; setInited(comOpts, thing, tryGetEnumVec (thing, modelsNode, {"@ pos", "@ sources" }, "Enable modelling of position")); - - }{ auto& thing = comOpts.clockModel.enable ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ clock", "@ enable" }, "Enable modelling of clocks")); - }{ auto& thing = comOpts.clockModel.sources ; setInited(comOpts, thing, tryGetEnumVec (thing, modelsNode, {"@ clock", "@ sources" }, "List of sources to use for clocks")); - }{ auto& thing = comOpts.attitudeModel.enable ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ attitude", "@ enable" }, "Enables non-nominal attitude types")); - }{ auto& thing = comOpts.attitudeModel.sources ; setInited(comOpts, thing, tryGetEnumVec (thing, modelsNode, {"@ attitude", "@ sources" }, "List of sourecs to use for attitudes")); - }{ auto& thing = comOpts.attitudeModel.model_dt ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ attitude", "@ model_dt" }, "Timestep used in modelling attitude")); - }{ auto& thing = comOpts.codeBiasModel.enable ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ code_bias", "@ enable" }, "Enable modelling of code biases")); - }{ auto& thing = comOpts.codeBiasModel.default_bias ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ code_bias", "@ default_bias" }, "Bias to use when no code bias is found")); - }{ auto& thing = comOpts.codeBiasModel.undefined_sigma ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ code_bias", "@ undefined_sigma" }, "Uncertainty sigma to apply to default code biases")); - }{ auto& thing = comOpts.phaseBiasModel.enable ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ phase_bias", "@ enable" }, "Enable modelling of phase biases. Required for AR")); - }{ auto& thing = comOpts.phaseBiasModel.default_bias ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ phase_bias", "@ default_bias" }, "Bias to use when no phase bias is found")); - }{ auto& thing = comOpts.phaseBiasModel.undefined_sigma ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ phase_bias", "@ undefined_sigma" }, "Uncertainty sigma to apply to default phase biases")); - - }{ auto& thing = comOpts.pcoModel.enable ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ pco", "@ enable" }, "Enable modelling of phase center offsets")); - }{ auto& thing = comOpts.pcvModel.enable ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ pcv", "@ enable" }, "Enable modelling of phase center variations")); - - }{ auto& thing = comOpts.phaseWindupModel.enable ; setInited(comOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ phase_windup", "@ enable" }, "Model phase windup due to relative rotation of circularly polarised antennas")); - } - - if (antenna_boresight .size() == 3) { comOpts.antenna_boresight = Vector3d(antenna_boresight.data()); setInited(comOpts, comOpts.antenna_boresight); } - if (antenna_azimuth .size() == 3) { comOpts.antenna_azimuth = Vector3d(antenna_azimuth .data()); setInited(comOpts, comOpts.antenna_azimuth); } + CommonOptions& comOpts, ///< Satellite options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) +{ + auto comNode = stringsToYamlObject(yamlBase, descriptorVec); + + auto modelsNode = stringsToYamlObject(comNode, {"9@ models"}, "Enable specific models"); + + vector antenna_boresight; + vector antenna_azimuth; + { + } + { + auto& thing = comOpts.exclude; + setInited( + comOpts, + thing, + tryGetFromYaml(thing, comNode, {"0! exclude"}, "Exclude receiver from processing") + ); + } + { + auto& thing = comOpts.pseudo_sigma; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + comNode, + {"0@ pseudo_sigma"}, + "Standard deviation of pseudo measurmeents" + ) + ); + } + { + auto& thing = comOpts.laser_sigma; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + comNode, + {"0@ laser_sigma"}, + "Standard deviation of SLR laser measurements" + ) + ); + } + { + auto& thing = comOpts.clock_codes; + setInited( + comOpts, + thing, + tryGetEnumVec( + thing, + comNode, + {"3@ clock_codes"}, + "Codes for IF combination based clocks" + ) + ); + } + { + auto& thing = comOpts.apriori_sigma_enu; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + comNode, + {"4@ apriori_sigma_enu"}, + "Sigma applied for weighting in mincon transformation estimation. (Lower is " + "stronger weighting, " + "Negative is unweighted, ENU separation unsupported for satellites)" + ) + ); + } + { + auto& thing = comOpts.mincon_scale_apriori_sigma; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + comNode, + {"4@ mincon_scale_apriori_sigma"}, + "Scale applied to apriori sigmas while weighting in mincon transformation " + "estimation" + ) + ); + } + { + auto& thing = comOpts.mincon_scale_filter_sigma; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + comNode, + {"4@ mincon_scale_filter_sigma"}, + "Scale applied to filter sigmas while weighting in mincon transformation estimation" + ) + ); + } + { + auto& thing = antenna_boresight; + tryGetFromYaml( + thing, + comNode, + {"@ antenna_boresight"}, + "Antenna boresight (Up) in satellite body-fixed frame" + ); + } + { + auto& thing = antenna_azimuth; + tryGetFromYaml( + thing, + comNode, + {"@ antenna_azimuth"}, + "Antenna azimuth (North) in satellite body-fixed frame" + ); + } + { + auto& thing = comOpts.ellipse_propagation_time_tolerance; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + comNode, + {"@ ellipse_propagation_time_tolerance"}, + "Time gap tolerance under which the ellipse propagator can be used for orbit " + "prediction" + ) + ); + } + { + auto& thing = comOpts.posModel.enable; + setInited( + comOpts, + thing, + tryGetFromYaml(thing, modelsNode, {"@ pos", "@ enable"}, "Enable modelling of position") + ); + } + { + auto& thing = comOpts.posModel.sources; + setInited( + comOpts, + thing, + tryGetEnumVec(thing, modelsNode, {"@ pos", "@ sources"}, "Enable modelling of position") + ); + } + { + auto& thing = comOpts.clockModel.enable; + setInited( + comOpts, + thing, + tryGetFromYaml(thing, modelsNode, {"@ clock", "@ enable"}, "Enable modelling of clocks") + ); + } + { + auto& thing = comOpts.clockModel.sources; + setInited( + comOpts, + thing, + tryGetEnumVec( + thing, + modelsNode, + {"@ clock", "@ sources"}, + "List of sources to use for clocks" + ) + ); + } + { + auto& thing = comOpts.attitudeModel.enable; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ attitude", "@ enable"}, + "Enables non-nominal attitude types" + ) + ); + } + { + auto& thing = comOpts.attitudeModel.sources; + setInited( + comOpts, + thing, + tryGetEnumVec( + thing, + modelsNode, + {"@ attitude", "@ sources"}, + "List of sourecs to use for attitudes" + ) + ); + } + { + auto& thing = comOpts.attitudeModel.model_dt; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ attitude", "@ model_dt"}, + "Timestep used in modelling attitude" + ) + ); + } + { + auto& thing = comOpts.codeBiasModel.enable; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ code_bias", "@ enable"}, + "Enable modelling of code biases" + ) + ); + } + { + auto& thing = comOpts.codeBiasModel.default_bias; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ code_bias", "@ default_bias"}, + "Bias to use when no code bias is found" + ) + ); + } + { + auto& thing = comOpts.codeBiasModel.undefined_sigma; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ code_bias", "@ undefined_sigma"}, + "Uncertainty sigma to apply to default code biases" + ) + ); + } + { + auto& thing = comOpts.phaseBiasModel.enable; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ phase_bias", "@ enable"}, + "Enable modelling of phase biases. Required for AR" + ) + ); + } + { + auto& thing = comOpts.phaseBiasModel.default_bias; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ phase_bias", "@ default_bias"}, + "Bias to use when no phase bias is found" + ) + ); + } + { + auto& thing = comOpts.phaseBiasModel.undefined_sigma; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ phase_bias", "@ undefined_sigma"}, + "Uncertainty sigma to apply to default phase biases" + ) + ); + } + { + auto& thing = comOpts.pcoModel.enable; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ pco", "@ enable"}, + "Enable modelling of phase center offsets" + ) + ); + } + { + auto& thing = comOpts.pcvModel.enable; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ pcv", "@ enable"}, + "Enable modelling of phase center variations" + ) + ); + } + { + auto& thing = comOpts.phaseWindupModel.enable; + setInited( + comOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ phase_windup", "@ enable"}, + "Model phase windup due to relative rotation of circularly polarised antennas" + ) + ); + } + + if (antenna_boresight.size() == 3) + { + comOpts.antenna_boresight = Vector3d(antenna_boresight.data()); + setInited(comOpts, comOpts.antenna_boresight); + } + if (antenna_azimuth.size() == 3) + { + comOpts.antenna_azimuth = Vector3d(antenna_azimuth.data()); + setInited(comOpts, comOpts.antenna_azimuth); + } } - /** Set satellite options from yaml -*/ + */ void getOptionsFromYaml( - SatelliteOptions& satOpts, ///< Satellite options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - const vector& descriptorVec) ///< List of strings of keys of yaml hierarchy + SatelliteOptions& satOpts, ///< Satellite options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + const vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) { - auto satNode = stringsToYamlObject(yamlBase, descriptorVec); - - getOptionsFromYaml((CommonOptions&) satOpts, satNode, {}); - getOptionsFromYaml((OrbitOptions&) satOpts, satNode, {}); - - { - }{ auto& thing = satOpts.error_model ; setInited(satOpts, thing, tryGetEnumOpt (thing, satNode, {"1@ error_model" })); - }{ auto& thing = satOpts.code_sigma ; setInited(satOpts, thing, tryGetFromYaml (thing, satNode, {"2@ code_sigma" }, "Standard deviation of code measurements")); - }{ auto& thing = satOpts.phase_sigma ; setInited(satOpts, thing, tryGetFromYaml (thing, satNode, {"2@ phase_sigma" }, "Standard deviation of phase measurmeents")); - } + auto satNode = stringsToYamlObject(yamlBase, descriptorVec); + + getOptionsFromYaml((CommonOptions&)satOpts, satNode, {}); + getOptionsFromYaml((OrbitOptions&)satOpts, satNode, {}); + + { + } + { + auto& thing = satOpts.error_model; + setInited(satOpts, thing, tryGetEnumOpt(thing, satNode, {"1@ error_model"})); + } + { + auto& thing = satOpts.code_sigma; + setInited( + satOpts, + thing, + tryGetFromYaml( + thing, + satNode, + {"2@ code_sigma"}, + "Standard deviation of code measurements" + ) + ); + } + { + auto& thing = satOpts.phase_sigma; + setInited( + satOpts, + thing, + tryGetFromYaml( + thing, + satNode, + {"2@ phase_sigma"}, + "Standard deviation of phase measurmeents" + ) + ); + } } /** Set receiver options from yaml -*/ + */ void getOptionsFromYaml( - ReceiverOptions& recOpts, ///< Receiver options variable to output to - NodeStack yamlBase, ///< Yaml node to search within - vector& descriptorVec) ///< List of strings of keys of yaml hierarchy -{ - auto recNode = stringsToYamlObject(yamlBase, descriptorVec); - - getOptionsFromYaml((CommonOptions&) recOpts, recNode, {}); - - vector eccentricity; - vector apriori_pos; - - auto modelsNode = stringsToYamlObject(recNode, {"9@ models" }, "Enable specific models"); - - //get option classes just to add comments - { - auto ionospheric_component = stringsToYamlObject(modelsNode, {"@ ionospheric_components" }, "Ionospheric models produce frequency-dependent effects"); - auto ionospheric_model = stringsToYamlObject(modelsNode, {"@ ionospheric_model" }, "Coherent ionosphere models can improve estimation of biases and allow use with single frequency receivers"); - auto troposhpere = stringsToYamlObject(modelsNode, {"@ troposphere" }, "Tropospheric modelling accounts for delays due to refraction of light in water vapour"); - auto tides = stringsToYamlObject(modelsNode, {"@ tides" }); - auto eop = stringsToYamlObject(modelsNode, {"@ eop" }); - } - - { - }{ auto& thing = recOpts.kill ; setInited(recOpts, thing, tryGetFromYaml (thing, recNode, {"0@ kill" }, "Remove receiver from future processing")); - }{ auto& thing = recOpts.zero_dcb_codes ; setInited(recOpts, thing, tryGetEnumVec (thing, recNode, {"3@ zero_dcb_codes" })); - }{ auto& thing = apriori_pos ; tryGetFromYaml (thing, recNode, {"4@ apriori_position" }, "Apriori position in XYZ ECEF frame"); - }{ auto& thing = recOpts.antenna_type ; setInited(recOpts, thing, tryGetFromYaml (thing, recNode, {"4@ antenna_type" }, "Antenna type and radome in 20 character string as per sinex")); - }{ auto& thing = recOpts.receiver_type ; setInited(recOpts, thing, tryGetFromYaml (thing, recNode, {"4@ receiver_type" }, "Type of gnss receiver hardware")); - }{ auto& thing = recOpts.sat_id ; setInited(recOpts, thing, tryGetFromYaml (thing, recNode, {"4@ sat_id" }, "Id for receivers that are also satellites")); - }{ auto& thing = recOpts.elevation_mask_deg ; setInited(recOpts, thing, tryGetFromYaml (thing, recNode, {"0! elevation_mask" }, "Minimum elevation for satellites to be processed")); - }{ auto& thing = recOpts.receiver_reference_system ; setInited(recOpts, thing, tryGetEnumOpt (thing, recNode, {"@ rec_reference_system" }, "Receiver will use this system as reference clock")); - - - }{ auto& thing = recOpts.error_model ; setInited(recOpts, thing, tryGetEnumOpt (thing, recNode, {"1! error_model" })); - }{ auto& thing = recOpts.code_sigma ; setInited(recOpts, thing, tryGetFromYaml (thing, recNode, {"2! code_sigma" }, "Standard deviation of code measurements")); - }{ auto& thing = recOpts.phase_sigma ; setInited(recOpts, thing, tryGetFromYaml (thing, recNode, {"2! phase_sigma" }, "Standard deviation of phase measurmeents")); - - - }{ auto& thing = recOpts.eccentricityModel.enable ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ eccentricity", "enable" }, "Enable antenna eccentrities")); - }{ auto& thing = eccentricity ; tryGetFromYaml (thing, modelsNode, {"@ eccentricity", "offset" }, "Antenna offset in ENU frame"); - - }{ auto& thing = recOpts.tropModel.enable ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ troposphere", "enable" }, "Model tropospheric delays")); - }{ auto& thing = recOpts.tropModel.models ; setInited(recOpts, thing, tryGetEnumVec (thing, modelsNode, {"@ troposphere", "models" }, "List of models to use for troposphere")); - - }{ auto& thing = recOpts.tideModels.enable ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ tides", "@ enable" }, "Enable modelling of tidal displacements")); - }{ auto& thing = recOpts.tideModels.solid ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ tides", "@ solid" }, "Enable solid Earth tides")); - }{ auto& thing = recOpts.tideModels.otl ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ tides", "@ otl" }, "Enable ocean tide loading")); - }{ auto& thing = recOpts.tideModels.atl ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ tides", "@ atl" }, "Enable atmospheric tide loading")); - }{ auto& thing = recOpts.tideModels.spole ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ tides", "@ spole" }, "Enable solid Earth pole tides")); - }{ auto& thing = recOpts.tideModels.opole ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ tides", "@ opole" }, "Enable ocean pole tides")); - - }{ auto& thing = recOpts.range ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ range", "@ enable" }, "Enable modelling of signal time of flight time due to range")); - }{ auto& thing = recOpts.relativity ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ relativity", "@ enable" }, "Enable modelling of relativistic effects")); - }{ auto& thing = recOpts.relativity2 ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ relativity2", "@ enable" }, "Enable modelling of secondary relativistic effects")); - }{ auto& thing = recOpts.sagnac ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ sagnac", "@ enable" }, "Enable modelling of sagnac effect")); - }{ auto& thing = recOpts.integer_ambiguity ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ integer_ambiguity", "@ enable" }, "Model ambiguities due to unknown integer number of cycles in phase measurements")); - }{ auto& thing = recOpts.ionospheric_component ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ ionospheric_components", "enable" }, "Enable ionospheric modelling")); - }{ auto& thing = recOpts.ionospheric_component2 ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ ionospheric_components", "use_2nd_order" })); - }{ auto& thing = recOpts.ionospheric_component3 ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ ionospheric_components", "use_3rd_order" })); - }{ auto& thing = recOpts.mapping_function ; setInited(recOpts, thing, tryGetEnumOpt (thing, modelsNode, {"@ ionospheric_components", "@ mapping_function" }, "Mapping function if not specified in the data or model")); - }{ auto& thing = recOpts.geomagnetic_field_height ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ ionospheric_components", "@ geomagnetic_field_height" }, "ionospheric pierce point layer height if not specified in the data or model (km)")); - }{ auto& thing = recOpts.mapping_function_layer_height ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ ionospheric_components", "@ mapping_function_layer_height" }, "mapping function layer height if not specified in the data or model (km)")); - }{ auto& thing = recOpts.iono_sigma_limit ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ ionospheric_components", "@ iono_sigma_limit" }, "Ionosphere states are removed when their sigma exceeds this value")); - }{ auto& thing = recOpts.ionospheric_model ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ ionospheric_model", "enable" }, "Compute ionosphere maps from a network of receivers")); - }{ auto& thing = recOpts.tropospheric_map ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ tropospheric_map", "enable" }, "Compute tropospheric maps from a network of receivers")); - }{ auto& thing = recOpts.eop ; setInited(recOpts, thing, tryGetFromYaml (thing, modelsNode, {"@ eop", "enable" }, "Enable modelling of eops")); - - }{ auto& thing = recOpts.eccentricityModel.eccentricity; if (eccentricity .size() == 3) { thing = Vector3d(eccentricity .data()); setInited(recOpts, thing); } - }{ auto& thing = recOpts.apriori_pos ; if (apriori_pos .size() == 3) { thing = Vector3d(apriori_pos .data()); setInited(recOpts, thing); } - - } - - - for (E_ObsCode2 obsCode2 : E_ObsCode2::_values()) - { - { - }{ auto& thing = recOpts.rinex23Conv.codeConv[obsCode2] ; setInited(recOpts, thing, tryGetEnumOpt(thing, recNode, {"@ rinex2", "@ rnx_code_conversions", obsCode2._to_string()})); - }{ auto& thing = recOpts.rinex23Conv.phasConv[obsCode2] ; setInited(recOpts, thing, tryGetEnumOpt(thing, recNode, {"@ rinex2", "@ rnx_phase_conversions", obsCode2._to_string()})); - } - } + ReceiverOptions& recOpts, ///< Receiver options variable to output to + NodeStack yamlBase, ///< Yaml node to search within + vector& descriptorVec ///< List of strings of keys of yaml hierarchy +) +{ + auto recNode = stringsToYamlObject(yamlBase, descriptorVec); + + getOptionsFromYaml((CommonOptions&)recOpts, recNode, {}); + + vector eccentricity; + vector apriori_pos; + + auto modelsNode = stringsToYamlObject(recNode, {"9@ models"}, "Enable specific models"); + + // get option classes just to add comments + { + auto ionospheric_component = stringsToYamlObject( + modelsNode, + {"@ ionospheric_components"}, + "Ionospheric models produce frequency-dependent effects" + ); + auto ionospheric_model = stringsToYamlObject( + modelsNode, + {"@ ionospheric_model"}, + "Coherent ionosphere models can improve estimation of biases and allow use with single " + "frequency receivers" + ); + auto troposhpere = stringsToYamlObject( + modelsNode, + {"@ troposphere"}, + "Tropospheric modelling accounts for delays due to refraction of light in water vapour" + ); + auto tides = stringsToYamlObject(modelsNode, {"@ tides"}); + auto eop = stringsToYamlObject(modelsNode, {"@ eop"}); + } + + { + } + { + auto& thing = recOpts.kill; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, recNode, {"0@ kill"}, "Remove receiver from future processing") + ); + } + { + auto& thing = recOpts.zero_dcb_codes; + setInited(recOpts, thing, tryGetEnumVec(thing, recNode, {"3@ zero_dcb_codes"})); + } + { + auto& thing = apriori_pos; + tryGetFromYaml( + thing, + recNode, + {"4@ apriori_position"}, + "Apriori position in XYZ ECEF frame" + ); + } + { + auto& thing = recOpts.antenna_type; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + recNode, + {"4@ antenna_type"}, + "Antenna type and radome in 20 character string as per sinex" + ) + ); + } + { + auto& thing = recOpts.receiver_type; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, recNode, {"4@ receiver_type"}, "Type of gnss receiver hardware") + ); + } + { + auto& thing = recOpts.domes_number; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, recNode, {"4@ domes_number", ""}, "Domes number for the receiver") + ); + } + { + auto& thing = recOpts.site_description; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + recNode, + {"4@ site_description", ""}, + "description of the receiver" + ) + ); + } + { + auto& thing = recOpts.sat_id; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + recNode, + {"4@ sat_id"}, + "Id for receivers that are also satellites" + ) + ); + } + { + auto& thing = recOpts.elevation_mask_deg; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + recNode, + {"0! elevation_mask"}, + "Minimum elevation for satellites to be processed" + ) + ); + } + { + auto& thing = recOpts.receiver_reference_system; + setInited( + recOpts, + thing, + tryGetEnumOpt( + thing, + recNode, + {"@ rec_reference_system"}, + "Receiver will use this system as reference clock" + ) + ); + } + { + auto& thing = recOpts.error_model; + setInited(recOpts, thing, tryGetEnumOpt(thing, recNode, {"1! error_model"})); + } + { + auto& thing = recOpts.code_sigma; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + recNode, + {"2! code_sigma"}, + "Standard deviation of code measurements" + ) + ); + } + { + auto& thing = recOpts.phase_sigma; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + recNode, + {"2! phase_sigma"}, + "Standard deviation of phase measurmeents" + ) + ); + } + { + auto& thing = recOpts.eccentricityModel.enable; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ eccentricity", "enable"}, + "Enable antenna eccentrities" + ) + ); + } + { + auto& thing = eccentricity; + tryGetFromYaml( + thing, + modelsNode, + {"@ eccentricity", "offset"}, + "Antenna offset in ENU frame" + ); + } + + { + auto& thing = recOpts.tropModel.enable; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ troposphere", "enable"}, + "Model tropospheric delays" + ) + ); + } + { + auto& thing = recOpts.tropModel.models; + setInited( + recOpts, + thing, + tryGetEnumVec( + thing, + modelsNode, + {"@ troposphere", "models"}, + "List of models to use for troposphere" + ) + ); + } + { + auto& thing = recOpts.tideModels.enable; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ tides", "@ enable"}, + "Enable modelling of tidal displacements" + ) + ); + } + { + auto& thing = recOpts.tideModels.solid; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, modelsNode, {"@ tides", "@ solid"}, "Enable solid Earth tides") + ); + } + { + auto& thing = recOpts.tideModels.otl; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, modelsNode, {"@ tides", "@ otl"}, "Enable ocean tide loading") + ); + } + { + auto& thing = recOpts.tideModels.atl; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ tides", "@ atl"}, + "Enable atmospheric tide loading" + ) + ); + } + { + auto& thing = recOpts.tideModels.spole; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ tides", "@ spole"}, + "Enable solid Earth pole tides" + ) + ); + } + { + auto& thing = recOpts.tideModels.opole; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, modelsNode, {"@ tides", "@ opole"}, "Enable ocean pole tides") + ); + } + { + auto& thing = recOpts.range; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ range", "@ enable"}, + "Enable modelling of signal time of flight time due to range" + ) + ); + } + { + auto& thing = recOpts.relativity; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ relativity", "@ enable"}, + "Enable modelling of relativistic effects" + ) + ); + } + { + auto& thing = recOpts.relativity2; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ relativity2", "@ enable"}, + "Enable modelling of secondary relativistic effects" + ) + ); + } + { + auto& thing = recOpts.sagnac; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ sagnac", "@ enable"}, + "Enable modelling of sagnac effect" + ) + ); + } + { + auto& thing = recOpts.integer_ambiguity; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ integer_ambiguity", "@ enable"}, + "Model ambiguities due to unknown integer number of cycles in phase measurements" + ) + ); + } + { + auto& thing = recOpts.ionospheric_component; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ ionospheric_components", "enable"}, + "Enable ionospheric modelling" + ) + ); + } + { + auto& thing = recOpts.ionospheric_component2; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, modelsNode, {"@ ionospheric_components", "use_2nd_order"}) + ); + } + { + auto& thing = recOpts.ionospheric_component3; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, modelsNode, {"@ ionospheric_components", "use_3rd_order"}) + ); + } + { + auto& thing = recOpts.mapping_function; + setInited( + recOpts, + thing, + tryGetEnumOpt( + thing, + modelsNode, + {"@ ionospheric_components", "@ mapping_function"}, + "Mapping function if not specified in the data or model" + ) + ); + } + { + auto& thing = recOpts.geomagnetic_field_height; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ ionospheric_components", "@ geomagnetic_field_height"}, + "ionospheric pierce point layer height if not specified in the data or model (km)" + ) + ); + } + { + auto& thing = recOpts.mapping_function_layer_height; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ ionospheric_components", "@ mapping_function_layer_height"}, + "mapping function layer height if not specified in the data or model (km)" + ) + ); + } + { + auto& thing = recOpts.ionospheric_model; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ ionospheric_model", "enable"}, + "Compute ionosphere maps from a network of receivers" + ) + ); + } + { + auto& thing = recOpts.tropospheric_map; + setInited( + recOpts, + thing, + tryGetFromYaml( + thing, + modelsNode, + {"@ tropospheric_map", "enable"}, + "Compute tropospheric maps from a network of receivers" + ) + ); + } + { + auto& thing = recOpts.eop; + setInited( + recOpts, + thing, + tryGetFromYaml(thing, modelsNode, {"@ eop", "enable"}, "Enable modelling of eops") + ); + } + { + auto& thing = recOpts.eccentricityModel.eccentricity; + if (eccentricity.size() == 3) + { + thing = Vector3d(eccentricity.data()); + setInited(recOpts, thing); + } + } + { + auto& thing = recOpts.apriori_pos; + if (apriori_pos.size() == 3) + { + thing = Vector3d(apriori_pos.data()); + setInited(recOpts, thing); + } + } + + for (E_ObsCode2 obsCode2 : magic_enum::enum_values()) + { + { + auto& thing = recOpts.rinex23Conv.codeConv[obsCode2]; + setInited( + recOpts, + thing, + tryGetEnumOpt( + thing, + recNode, + {"@ rinex2", "@ rnx_code_conversions", enum_to_string(obsCode2)} + ) + ); + } + { + // Handle phase conversions as arrays + auto& thing = recOpts.rinex23Conv.phasConv[obsCode2]; + vector yamlPath = { + "@ rinex2", + "@ rnx_phase_conversions", + enum_to_string(obsCode2) + }; + + auto [phaseNode, stack] = stringsToYamlObject(recNode, yamlPath, ""); + + if (phaseNode.IsDefined()) + { + thing.clear(); + if (phaseNode.IsSequence()) + { + // Handle array of values + for (auto item : phaseNode) + { + try + { + string codeStr = item.as(); + E_ObsCode obsCode = + string_to_enum_nocase_throw(codeStr.c_str()); + thing.push_back(obsCode); + } + catch (...) + { + // Skip invalid enum values + } + } + } + else + { + // Handle single value (backward compatibility) + try + { + string codeStr = phaseNode.as(); + E_ObsCode obsCode = string_to_enum_nocase_throw(codeStr.c_str()); + thing.push_back(obsCode); + } + catch (...) + { + // Skip invalid enum values + } + } + setInited(recOpts, thing, !thing.empty()); + } + } + } } - /** Set satellite options for a specific satellite using a hierarchy of sources -*/ + */ SatelliteOptions& ACSConfig::getSatOpts( - SatSys Sat, ///< Satellite to search for options for - const vector& suffixes) ///< Optional suffix to get more specific versions + SatSys Sat, ///< Satellite to search for options for + const vector& suffixes ///< Optional suffix to get more specific versions +) { - DOCS_REFERENCE(Aliases_And_Inheritance__); - - string fullId = Sat.id(); - for (auto& suffix : suffixes) - { - fullId += "."; - fullId += suffix; - } - - lock_guard guard(configMutex); - - auto& satOpts = satOptsMap[fullId]; - - //return early if possible - if (satOpts._initialised) - return satOpts; - - satOpts.id = fullId; - - BOOST_LOG_TRIVIAL(debug) << "Getting sat config for " << fullId; - - vector aliases; - - aliases.push_back("! global"); + DOCS_REFERENCE(Aliases_And_Inheritance__); + + string fullId = Sat.id(); + for (auto& suffix : suffixes) + { + fullId += "."; + fullId += suffix; + } + + lock_guard guard(configMutex); - for (int i = 0; i < yamls.size(); i++) - { - auto& yaml = yamls[i]; + auto& satOpts = satOptsMap[fullId]; + + // return early if possible + if (satOpts._initialised) + return satOpts; + + satOpts.id = fullId; - vector yamlAliases; - tryGetFromYaml(yamlAliases, {yaml, ""}, {"3! satellite_options", Sat.id(), "@ aliases"}, "Aliases for this satellite"); + BOOST_LOG_TRIVIAL(debug) << "Getting sat config for " << fullId; - for (auto& alias : yamlAliases) - { - aliases.push_back(alias); - } - } + vector aliases; - //add global and this id on either side of the aliases - aliases.push_back(Sat.sysName()); - if (Sat.blockType() .empty() == false) aliases.push_back(Sat.blockType()); - aliases.push_back(Sat.id()); - if (Sat.svn() .empty() == false) aliases.push_back("SVN_" + Sat.svn()); + aliases.push_back("! global"); - //prepare all the aliases or whatever using _names + for (int i = 0; i < yamls.size(); i++) + { + auto& yaml = yamls[i]; - for (auto& alias : aliases) - for (int S = 0; S <= suffixes.size(); S++) - { - boost::trim_right(alias); + vector yamlAliases; + tryGetFromYaml( + yamlAliases, + {yaml, ""}, + {"3! satellite_options", Sat.id(), "@ aliases"}, + "Aliases for this satellite" + ); - vector estimationDescriptorVec = {estimation_parameters_str, "0! satellites", alias}; - vector optionsDescriptorVec = {"3! satellite_options", alias}; + for (auto& alias : yamlAliases) + { + aliases.push_back(alias); + } + } - string suffixName = (string) "_" + alias; + // add global and this id on either side of the aliases + aliases.push_back(Sat.sysName()); + if (Sat.blockType().empty() == false) + aliases.push_back(Sat.blockType()); + aliases.push_back(Sat.id()); + if (Sat.svn().empty() == false) + aliases.push_back("SVN_" + Sat.svn()); - for (int s = 0; s < S; s++) - { - estimationDescriptorVec .push_back(suffixes[s]); - optionsDescriptorVec .push_back(suffixes[s]); - suffixName += suffixes[s]; - } + // prepare all the aliases or whatever using _names - auto& suffixOpts = satOptsMap[suffixName]; + for (auto& alias : aliases) + for (int S = 0; S <= suffixes.size(); S++) + { + boost::trim_right(alias); - if (suffixOpts._initialised) - { - continue; - } + vector estimationDescriptorVec = { + estimation_parameters_str, + "0! satellites", + alias + }; + vector optionsDescriptorVec = {"3! satellite_options", alias}; - suffixOpts.id = suffixName; + string suffixName = (string) "_" + alias; - for (auto& yaml : yamls) - { - stringsToYamlObject({yaml, ""}, {"3! receiver_options"}, "Options to configure individual satellites, systems, or global configs"); + for (int s = 0; s < S; s++) + { + estimationDescriptorVec.push_back(suffixes[s]); + optionsDescriptorVec.push_back(suffixes[s]); + suffixName += suffixes[s]; + } - getKalmanFromYaml (suffixOpts, {yaml, ""}, estimationDescriptorVec); - getOptionsFromYaml (suffixOpts, {yaml, ""}, optionsDescriptorVec); - } + auto& suffixOpts = satOptsMap[suffixName]; - suffixOpts._initialised = true; - } + if (suffixOpts._initialised) + { + continue; + } - //add up all the aliases or whatever - for (int S = 0; S <= suffixes.size(); S++) - for (auto& alias : aliases) - { - string suffixName = (string) "_" + alias; + suffixOpts.id = suffixName; - for (int s = 0; s < S; s++) - { - suffixName += suffixes[s]; - } + for (auto& yaml : yamls) + { + stringsToYamlObject( + {yaml, ""}, + {"3! satellite_options"}, + "Options to configure individual satellites, systems, or global configs" + ); - auto& suffixOpts = satOptsMap[suffixName]; + getKalmanFromYaml(suffixOpts, {yaml, ""}, estimationDescriptorVec); + getOptionsFromYaml(suffixOpts, {yaml, ""}, optionsDescriptorVec); + } - if (suffixOpts.id != satOpts.id) - { - suffixOpts.inheritors[satOpts.id] = &satOpts; - } + suffixOpts._initialised = true; + } - BOOST_LOG_TRIVIAL(debug) << " Inheriting sat config from " << suffixName; + // add up all the aliases or whatever + for (int S = 0; S <= suffixes.size(); S++) + for (auto& alias : aliases) + { + string suffixName = (string) "_" + alias; + + for (int s = 0; s < S; s++) + { + suffixName += suffixes[s]; + } - satOpts += suffixOpts; - } + auto& suffixOpts = satOptsMap[suffixName]; - satOpts._initialised = true; - return satOpts; + if (suffixOpts.id != satOpts.id) + { + suffixOpts.inheritors[satOpts.id] = &satOpts; + } + + BOOST_LOG_TRIVIAL(debug) << " Inheriting sat config from " << suffixName; + + satOpts += suffixOpts; + } + + satOpts._initialised = true; + return satOpts; } - /** Set receiver options for a specific receiver using a hierarchy of sources -*/ + */ ReceiverOptions& ACSConfig::getRecOpts( - string id, ///< Receiver to search for options for - const vector& suffixes) ///< Optional suffix to get more specific versions + string id, ///< Receiver to search for options for + const vector& suffixes ///< Optional suffix to get more specific versions +) { - DOCS_REFERENCE(Aliases_And_Inheritance__); + DOCS_REFERENCE(Aliases_And_Inheritance__); - string fullId = id; + string fullId = id; - for (auto& suffix : suffixes) - { - fullId += "."; - fullId += suffix; - } + for (auto& suffix : suffixes) + { + fullId += "."; + fullId += suffix; + } - lock_guard guard(configMutex); + lock_guard guard(configMutex); - auto& recOpts = recOptsMap[fullId]; + auto& recOpts = recOptsMap[fullId]; - //return early if possible - if (recOpts._initialised) - return recOpts; + // return early if possible + if (recOpts._initialised) + return recOpts; - BOOST_LOG_TRIVIAL(debug) << "Getting rec config for " << fullId; + BOOST_LOG_TRIVIAL(debug) << "Getting rec config for " << fullId; - recOpts.id = fullId; + recOpts.id = fullId; - vector aliases; + vector aliases; - aliases.push_back("! global"); + aliases.push_back("! global"); - for (auto& alias : customAliasesMap[id]) - { - aliases.push_back(alias); - } + for (auto& alias : customAliasesMap[id]) + { + aliases.push_back(alias); + } - for (int i = 0; i < yamls.size(); i++) - { - auto& yaml = yamls[i]; + for (int i = 0; i < yamls.size(); i++) + { + auto& yaml = yamls[i]; - vector yamlAliases; - tryGetFromYaml(yamlAliases, {yaml, ""}, {"3! receiver_options", id, "@ aliases"}, "Aliases for this receiver"); + vector yamlAliases; + tryGetFromYaml( + yamlAliases, + {yaml, ""}, + {"3! receiver_options", id, "@ aliases"}, + "Aliases for this receiver" + ); - for (auto& alias : yamlAliases) - { - aliases.push_back(alias); - } - } + for (auto& alias : yamlAliases) + { + aliases.push_back(alias); + } + } - //add global and this id on either side of the aliases - aliases.push_back(id); + // add global and this id on either side of the aliases + aliases.push_back(id); - //prepare all the aliases or whatever using _names + // prepare all the aliases or whatever using _names - for (auto& alias : aliases) - for (int S = 0; S <= suffixes.size(); S++) - { - vector estimationDescriptorVec = {estimation_parameters_str, "0! receivers", alias}; - vector optionsDescriptorVec = {"3! receiver_options", alias}; + for (auto& alias : aliases) + for (int S = 0; S <= suffixes.size(); S++) + { + vector estimationDescriptorVec = { + estimation_parameters_str, + "0! receivers", + alias + }; + vector optionsDescriptorVec = {"3! receiver_options", alias}; - string suffixName = (string) "_" + alias; + string suffixName = (string) "_" + alias; - for (int s = 0; s < S; s++) - { - estimationDescriptorVec .push_back(suffixes[s]); - optionsDescriptorVec .push_back(suffixes[s]); - suffixName += suffixes[s]; - } + for (int s = 0; s < S; s++) + { + estimationDescriptorVec.push_back(suffixes[s]); + optionsDescriptorVec.push_back(suffixes[s]); + suffixName += suffixes[s]; + } - auto& suffixOpts = recOptsMap[suffixName]; + auto& suffixOpts = recOptsMap[suffixName]; - if (suffixOpts._initialised) - { - continue; - } + if (suffixOpts._initialised) + { + continue; + } - suffixOpts.id = suffixName; + suffixOpts.id = suffixName; - for (auto& yaml : yamls) - { - stringsToYamlObject({yaml, ""}, {"3! receiver_options"}, "Options to configure individual receivers or global configs"); + for (auto& yaml : yamls) + { + stringsToYamlObject( + {yaml, ""}, + {"3! receiver_options"}, + "Options to configure individual receivers or global configs" + ); - getKalmanFromYaml (suffixOpts, {yaml, ""}, estimationDescriptorVec); - getOptionsFromYaml (suffixOpts, {yaml, ""}, optionsDescriptorVec); - } + getKalmanFromYaml(suffixOpts, {yaml, ""}, estimationDescriptorVec); + getOptionsFromYaml(suffixOpts, {yaml, ""}, optionsDescriptorVec); + } - suffixOpts._initialised = true; - } + suffixOpts._initialised = true; + } - //add up all the aliases or whatever - for (int S = 0; S <= suffixes.size(); S++) - for (auto& alias : aliases) - { - string suffixName = (string) "_" + alias; + // add up all the aliases or whatever + for (int S = 0; S <= suffixes.size(); S++) + for (auto& alias : aliases) + { + string suffixName = (string) "_" + alias; - for (int s = 0; s < S; s++) - { - suffixName += suffixes[s]; - } + for (int s = 0; s < S; s++) + { + suffixName += suffixes[s]; + } - auto& suffixOpts = recOptsMap[suffixName]; + auto& suffixOpts = recOptsMap[suffixName]; - if (suffixOpts.id != recOpts.id) - { - suffixOpts.inheritors[recOpts.id] = &recOpts; - } + if (suffixOpts.id != recOpts.id) + { + suffixOpts.inheritors[recOpts.id] = &recOpts; + } + + BOOST_LOG_TRIVIAL(debug) << " Inheriting rec config from " << suffixName; - BOOST_LOG_TRIVIAL(debug) << " Inheriting rec config from " << suffixName; + recOpts += suffixOpts; + } - recOpts += suffixOpts; - } - - recOpts._initialised = true; - return recOpts; + recOpts._initialised = true; + return recOpts; } /** Set and scale a variable according to yaml options -*/ -template + */ +template void tryGetScaledFromYaml( - double& output, ///< Variable to output to - NodeStack node, ///< Yaml node to search within - const vector& number_parameter, ///< List of keys of the hierarchy to the value to be set - const vector& scale_parameter, ///< List of keys of the hierarchy to the scale to be applied - ENUM (&_from_string_nocase)(const char*), ///< Function to decode scale enum strings - const string& comment = "") ///< Description to use for documentation -{ - double number = output; - ENUM number_units = ENUM::_from_integral(1); - - tryGetFromYaml (number, node, number_parameter, comment); - tryGetEnumOpt (number_units, node, scale_parameter); - - number *= (int)number_units; - if (number != 0) - { - output = number; - } + double& output, ///< Variable to output to + NodeStack node, ///< Yaml node to search within + const vector& + number_parameter, ///< List of keys of the hierarchy to the value to be set + const vector& + scale_parameter, ///< List of keys of the hierarchy to the scale to be applied + const string& comment = "" ///< Description to use for documentation +) +{ + double number = output; + ENUM number_units = int_to_enum(1); + + tryGetFromYaml(number, node, number_parameter, comment); + tryGetEnumOpt(number_units, node, scale_parameter); + + number *= periodToSeconds(number_units); + if (number != 0) + { + output = number; + } } -void recurseLowerCase( - YAML::Node& node) -{ - for (auto it = node.begin(); it != node.end(); ++it) - try - { - it->first = boost::algorithm::to_lower_copy(it->first.as()); - if ( it->second.IsSequence() - ||it->second.IsMap()) - { - recurseLowerCase(it->second); - } - } - catch (...) - { - } +void recurseLowerCase(YAML::Node& node) +{ + for (auto it = node.begin(); it != node.end(); ++it) + try + { + it->first = boost::algorithm::to_lower_copy(it->first.as()); + if (it->second.IsSequence() || it->second.IsMap()) + { + recurseLowerCase(it->second); + } + } + catch (...) + { + } } void ACSConfig::recurseYaml( - const string& file, - YAML::Node node, - const string& stack, - const string& aliasStack) -{ - for (YAML::const_iterator it = node.begin(); it != node.end(); it++) - { - string key = it->first.as(); - -// std::cout << key << "\n"; - - string newStack = stack + key + ":"; - string newAliasStack = aliasStack + key + ":"; - - bool altered = false; - - auto& found = foundOptions[file][newStack]; - if (found) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Duplicate " << newStack << " entries found in config file: " << file; - } - found = true; - - if (availableOptions.find(newAliasStack) == availableOptions.end()) - { - //this yaml stack not found in the available options, check to see if it could have worked if it were an alias - - for (auto str : - { - "estimation_parameters:receivers:", - "estimation_parameters:satellites:", - "outputs:streams:", - "receiver_options:", - "satellite_options:", - }) - if (stack.find(str) != string::npos) - { - newAliasStack = str + (string)"global:"; - - altered = true; - - if (availableOptions.find(newAliasStack) == availableOptions.end()) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: " << newStack << " is not a valid yaml option"; - - continue; - } - break; - } - - if (altered == false) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: " << newStack << " is not a valid yaml option"; - - continue; - } - } - -// BOOST_LOG_TRIVIAL(debug) -// << newStack << " is a valid yaml option"; - - if (node[key].IsMap()) - { - recurseYaml(file, node[key], newStack, newAliasStack); - } - else if (node[key].IsNull()) - { - //dont complain about things that dont do anything - } - else - { - //is a final value, this must pass on its own - ie, dont let final leafs with dumb names get aliased away, only pass those that should - - if (altered) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: " << newStack << " is not a valid yaml option"; - - continue; - } - } - } + const string& file, + YAML::Node node, + const string& stack, + const string& aliasStack +) +{ + for (YAML::const_iterator it = node.begin(); it != node.end(); it++) + { + string key = it->first.as(); + + // std::cout << key << "\n"; + + string newStack = stack + key + ":"; + string newAliasStack = aliasStack + key + ":"; + + bool altered = false; + + auto& found = foundOptions[file][newStack]; + if (found) + { + BOOST_LOG_TRIVIAL(warning) + << "Duplicate " << newStack << " entries found in config file: " << file; + } + found = true; + + if (availableOptions.find(newAliasStack) == availableOptions.end()) + { + // this yaml stack not found in the available options, check to see if it could have + // worked if it were an alias + + for (auto str : { + "estimation_parameters:receivers:", + "estimation_parameters:satellites:", + "outputs:streams:", + "receiver_options:", + "satellite_options:", + }) + if (stack.find(str) != string::npos) + { + newAliasStack = str + (string) "global:"; + + altered = true; + + if (availableOptions.find(newAliasStack) == availableOptions.end()) + { + BOOST_LOG_TRIVIAL(warning) << newStack << " is not a valid yaml option"; + + continue; + } + break; + } + + if (altered == false) + { + BOOST_LOG_TRIVIAL(warning) << newStack << " is not a valid yaml option"; + + continue; + } + } + + // BOOST_LOG_TRIVIAL(debug) + // << newStack << " is a valid yaml option"; + + if (node[key].IsMap()) + { + recurseYaml(file, node[key], newStack, newAliasStack); + } + else if (node[key].IsNull()) + { + // dont complain about things that dont do anything + } + else + { + // is a final value, this must pass on its own - ie, dont let final leafs with dumb + // names get aliased away, only pass those that should + + if (altered) + { + BOOST_LOG_TRIVIAL(warning) << newStack << " is not a valid yaml option"; + + continue; + } + } + } } /** Prepare the configuration of the program -*/ + */ bool configure( - int argc, ///< Passthrough calling argument count - char **argv) ///< Passthrough calling argument list -{ - // Command line options - boost::program_options::options_description desc{"Options"}; - - // Do not set default values here, as this will overide the configuration file opitions!!! - desc.add_options() - - ("help,h", "Help") - ("quiet,q", "Less output") - ("very-quiet,Q", "Much less output") - ("verbose,v", "More output") - ("very-verbose,V", "Much more output") - ("interactive,I", "Use interactive terminal") - ("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") - ("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") - ("atx_files", boost::program_options::value>()->multitoken(), "ANTEX files") - ("nav_files", boost::program_options::value>()->multitoken(), "Navigation files") - ("snx_files", boost::program_options::value>()->multitoken(), "SINEX files") - ("sp3_files", boost::program_options::value>()->multitoken(), "Orbit (SP3) files") - ("clk_files", boost::program_options::value>()->multitoken(), "Clock (CLK) files") - ("obx_files", boost::program_options::value>()->multitoken(), "ORBEX (OBX) files") - ("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") - ("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") -// ("run_rts_only", boost::program_options::value(), "RTS filename (without _xxxxx suffix)") - ("dump-config-only", "Dump the configuration and exit") - ("walkthrough", "Run demonstration code interactively with commentary") - ("compare_clocks", "Compare clock files") - ("compare_orbits", "Compare sp3 files") - ("compare_attitudes", "Compare antex files") - ; - - boost::program_options::variables_map vm; - - boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); - - boost::program_options::notify(vm); - - if ( vm.count("help") - ||argc == 1) - { - BOOST_LOG_TRIVIAL(info) << desc; - BOOST_LOG_TRIVIAL(info) << "PEA finished"; - - exit(EXIT_SUCCESS); - } - - if (vm.count("walkthrough")) - { - walkthrough(); - exit(EXIT_SUCCESS); - } - - if (vm.count("very-verbose")) { boost::log::core::get()->set_filter (boost::log::trivial::severity >= boost::log::trivial::trace); acsSeverity = boost::log::trivial::trace;} - if (vm.count("verbose")) { boost::log::core::get()->set_filter (boost::log::trivial::severity >= boost::log::trivial::debug); acsSeverity = boost::log::trivial::debug;} - if (vm.count("quiet")) { boost::log::core::get()->set_filter (boost::log::trivial::severity >= boost::log::trivial::warning); acsSeverity = boost::log::trivial::warning;} - if (vm.count("very-quiet")) { boost::log::core::get()->set_filter (boost::log::trivial::severity >= boost::log::trivial::error); acsSeverity = boost::log::trivial::error;} - - if (vm.count("interactive")) { InteractiveTerminal::enable(); } - - if (vm.count("yaml-defaults")) - { - acsConfig.parse({""}, vm); - - exit(EXIT_SUCCESS); - } - - if (vm.count("config")) - { - vector configs = vm["config"].as>(); - - globber(configs); - - bool pass = acsConfig.parse(configs, vm); - - if (!pass) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Configuration aborted"; - - return false; - } - } - - if ( acsConfig.compare_clocks - ||vm.count("compare_clocks")) - { - std::cout << "\n" << "----- Clock Comparator -----" << "\n"; - tryGetFromOpts(acsConfig.clk_files, vm, {"clk_files"}); - compareClocks(acsConfig.clk_files); - std::cout << "\n" << "----- Clock Comparator -----" << "\n"; - exit(EXIT_SUCCESS); - } - - if ( acsConfig.compare_orbits - ||vm.count("compare_orbits")) - { - std::cout << "\n" << "----- Orbit Comparator -----" << "\n"; - tryGetFromOpts(acsConfig.sp3_files, vm, {"sp3_files"}); - compareOrbits(acsConfig.sp3_files); - std::cout << "\n" << "----- Orbit Comparator -----" << "\n"; - exit(EXIT_SUCCESS); - } - - if ( acsConfig.compare_attitudes - ||vm.count("compare_attitudes")) - { - std::cout << "\n" << "----- Attitude Comparator -----" << "\n"; - tryGetFromOpts(acsConfig.obx_files, vm, {"obx_files"}); - compareAttitudes(acsConfig.obx_files); - std::cout << "\n" << "----- Attitude Comparator -----" << "\n"; - exit(EXIT_SUCCESS); - } - - // Dump the configuration information - acsConfig.info(std::cout); - - // Check the configuration - bool valid = true; - valid &= checkValidFiles(acsConfig.snx_files, "sinex file (snx file)"); - valid &= checkValidFiles(acsConfig.nav_files, "navfiles"); - valid &= checkValidFiles(acsConfig.sp3_files, "orbit"); - valid &= checkValidFiles(acsConfig.clk_files, "clock file (CLK file)"); - valid &= checkValidFiles(acsConfig.obx_files, "orbex file (OBX file)"); - valid &= checkValidFiles(acsConfig.ocean_tide_loading_blq_files, "ocean loading information (Blq file)"); - valid &= checkValidFiles(acsConfig.atmos_tide_loading_blq_files, "atmospheric loading information (Blq file)"); - valid &= checkValidFiles(acsConfig.erp_files, "earth rotation parameter file (ERP file)"); - valid &= checkValidFiles(acsConfig.dcb_files, "code Biases file (DCB file)"); - valid &= checkValidFiles(acsConfig.bsx_files, "bias Sinex file (BSX file)"); - valid &= checkValidFiles(acsConfig.ion_files, "Ionosphere (IONEX file)"); - valid &= checkValidFiles(acsConfig.igrf_files, "geomagnetic field coefficients (IGRF file)"); - valid &= checkValidFiles(acsConfig.atx_files, "antenna information (ANTEX file)"); - valid &= checkValidFiles(acsConfig.egm_files, "Earth gravity model coefficients (egm file)"); - valid &= checkValidFiles(acsConfig.planetary_ephemeris_files, "Planetary and lunar ephemerides"); - valid &= checkValidFiles(acsConfig.ocean_tide_potential_files, "Ocean tide file (tide file)"); - valid &= checkValidFiles(acsConfig.atmos_tide_potential_files, "Atmospheric tide file (tide file)"); - valid &= checkValidFiles(acsConfig.cmc_files, "Center of Mass tide file corrections"); - valid &= checkValidFiles(acsConfig.hfeop_files, "Subdaily EOP variations"); - valid &= checkValidFiles(acsConfig.atmos_oceean_dealiasing_files, "Atmosphere and Ocean De-aliasing (AOD1B file)"); - valid &= checkValidFiles(acsConfig.ocean_pole_tide_potential_files, "Pole ocean tide file"); - valid &= checkValidFiles(acsConfig.sid_files, "satellite ID list"); - valid &= checkValidFiles(acsConfig.com_files, "centre-of-mass (com file)"); - valid &= checkValidFiles(acsConfig.crd_files, "SLR observation files (crd file)"); - valid &= checkValidFiles(acsConfig.gpt2grid_files, "grid"); - valid &= checkValidFiles(acsConfig.orography_files, "orography"); - - if (acsConfig.snx_files.empty()) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Invalid SINEX file "; - } - - if (vm.count("dump-config-only")) - { - BOOST_LOG_TRIVIAL(info) - << "PEA finished"; - - exit(EXIT_SUCCESS); - } - - return valid; + int argc, ///< Passthrough calling argument count + char** argv ///< Passthrough calling argument list +) +{ + // Command line options + boost::program_options::options_description desc{"Options"}; + + // Do not set default values here, as this will overide the configuration file opitions!!! + desc.add_options() + + ("help,h", "Help") + ("quiet,q", "Less output") + ("very-quiet,Q", "Much less output") + ("verbose,v", "More output") + ("very-verbose,V", "Much more output") + ("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") + ("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") + ("user_aliases,a", boost::program_options::value>()->multitoken(), "User definable aliases") + ("atx_files", boost::program_options::value>()->multitoken(), "ANTEX files") + ("nav_files", boost::program_options::value>()->multitoken(), "Navigation files") + ("ems_files", boost::program_options::value>()->multitoken(), "SBAS EMS files") + ("snx_files", boost::program_options::value>()->multitoken(), "SINEX files") + ("sp3_files", boost::program_options::value>()->multitoken(), "Orbit (SP3) files") + ("clk_files", boost::program_options::value>()->multitoken(), "Clock (CLK) files") + ("obx_files", boost::program_options::value>()->multitoken(), "ORBEX (OBX) files") + ("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") + ("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") + // ("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") + ; + + boost::program_options::variables_map vm; + + boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); + + boost::program_options::notify(vm); + + if (vm.count("help") || argc == 1) + { + BOOST_LOG_TRIVIAL(info) << desc; + BOOST_LOG_TRIVIAL(info) << "PEA finished"; + + exit(EXIT_SUCCESS); + } + + if (vm.count("very-verbose")) + { + boost::log::core::get()->set_filter( + boost::log::trivial::severity >= boost::log::trivial::trace + ); + acsSeverity = boost::log::trivial::trace; + } + if (vm.count("verbose")) + { + boost::log::core::get()->set_filter( + boost::log::trivial::severity >= boost::log::trivial::debug + ); + acsSeverity = boost::log::trivial::debug; + } + if (vm.count("quiet")) + { + boost::log::core::get()->set_filter( + boost::log::trivial::severity >= boost::log::trivial::warning + ); + acsSeverity = boost::log::trivial::warning; + } + if (vm.count("very-quiet")) + { + boost::log::core::get()->set_filter( + boost::log::trivial::severity >= boost::log::trivial::error + ); + acsSeverity = boost::log::trivial::error; + } + + if (vm.count("yaml-defaults")) + { + acsConfig.parse({""}, vm); + + exit(EXIT_SUCCESS); + } + + if (vm.count("config")) + { + vector configs = vm["config"].as>(); + + globber(configs); + + bool pass = acsConfig.parse(configs, vm); + + if (!pass) + { + BOOST_LOG_TRIVIAL(error) << "Configuration aborted"; + + return false; + } + } + + if (acsConfig.compare_clocks || vm.count("compare_clocks")) + { + std::cout << "\n" + << "----- Clock Comparator -----" << "\n"; + tryGetFromOpts(acsConfig.clk_files, vm, {"clk_files"}); + compareClocks(acsConfig.clk_files); + std::cout << "\n" + << "----- Clock Comparator -----" << "\n"; + exit(EXIT_SUCCESS); + } + + if (acsConfig.compare_orbits || vm.count("compare_orbits")) + { + std::cout << "\n" + << "----- Orbit Comparator -----" << "\n"; + tryGetFromOpts(acsConfig.sp3_files, vm, {"sp3_files"}); + compareOrbits(acsConfig.sp3_files); + std::cout << "\n" + << "----- Orbit Comparator -----" << "\n"; + exit(EXIT_SUCCESS); + } + + if (acsConfig.compare_attitudes || vm.count("compare_attitudes")) + { + std::cout << "\n" + << "----- Attitude Comparator -----" << "\n"; + tryGetFromOpts(acsConfig.obx_files, vm, {"obx_files"}); + compareAttitudes(acsConfig.obx_files); + std::cout << "\n" + << "----- Attitude Comparator -----" << "\n"; + exit(EXIT_SUCCESS); + } + + // Dump the configuration information + acsConfig.info(std::cout); + + // Check the configuration + bool valid = true; + valid &= checkValidFiles(acsConfig.snx_files, "sinex file (snx file)"); + valid &= checkValidFiles(acsConfig.nav_files, "navfiles"); + valid &= checkValidFiles(acsConfig.ems_files, "emsfiles"); + valid &= checkValidFiles(acsConfig.sp3_files, "orbit"); + valid &= checkValidFiles(acsConfig.clk_files, "clock file (CLK file)"); + valid &= checkValidFiles(acsConfig.obx_files, "orbex file (OBX file)"); + valid &= checkValidFiles( + acsConfig.ocean_tide_loading_blq_files, + "ocean loading information (Blq file)" + ); + valid &= checkValidFiles( + acsConfig.atmos_tide_loading_blq_files, + "atmospheric loading information (Blq file)" + ); + valid &= checkValidFiles(acsConfig.erp_files, "earth rotation parameter file (ERP file)"); + valid &= checkValidFiles(acsConfig.dcb_files, "code Biases file (DCB file)"); + valid &= checkValidFiles(acsConfig.bsx_files, "bias Sinex file (BSX file)"); + valid &= checkValidFiles(acsConfig.ion_files, "Ionosphere (IONEX file)"); + valid &= checkValidFiles(acsConfig.igrf_files, "geomagnetic field coefficients (IGRF file)"); + valid &= checkValidFiles(acsConfig.atx_files, "antenna information (ANTEX file)"); + valid &= checkValidFiles(acsConfig.egm_files, "Earth gravity model coefficients (egm file)"); + valid &= + checkValidFiles(acsConfig.planetary_ephemeris_files, "Planetary and lunar ephemerides"); + valid &= checkValidFiles(acsConfig.ocean_tide_potential_files, "Ocean tide file (tide file)"); + valid &= + checkValidFiles(acsConfig.atmos_tide_potential_files, "Atmospheric tide file (tide file)"); + valid &= checkValidFiles(acsConfig.cmc_files, "Center of Mass tide file corrections"); + valid &= checkValidFiles(acsConfig.hfeop_files, "Subdaily EOP variations"); + valid &= checkValidFiles(acsConfig.space_weather_files, "Space weather"); + valid &= checkValidFiles( + acsConfig.atmos_ocean_dealiasing_files, + "Atmosphere and Ocean De-aliasing (AOD1B file)" + ); + valid &= checkValidFiles(acsConfig.ocean_pole_tide_potential_files, "Pole ocean tide file"); + valid &= checkValidFiles(acsConfig.sid_files, "satellite ID list"); + valid &= checkValidFiles(acsConfig.com_files, "centre-of-mass (com file)"); + valid &= checkValidFiles(acsConfig.crd_files, "SLR observation files (crd file)"); + valid &= checkValidFiles(acsConfig.gpt2grid_files, "grid"); + valid &= checkValidFiles(acsConfig.orography_files, "orography"); + + if (acsConfig.snx_files.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "Invalid SINEX file "; + } + + if (vm.count("dump-config-only")) + { + BOOST_LOG_TRIVIAL(info) << "PEA finished"; + + exit(EXIT_SUCCESS); + } + + return valid; } void ACSConfig::sanityChecks() { - if (ambErrors.outage_reset_limit < epoch_interval) BOOST_LOG_TRIVIAL(warning) << "Warning: outage_reset_limit < epoch_interval, but it probably shouldnt be"; - if (ionErrors.outage_reset_limit < epoch_interval) BOOST_LOG_TRIVIAL(warning) << "Warning: outage_reset_limit < epoch_interval, but it probably shouldnt be"; + if (ionErrors.outage_reset_limit < epoch_interval) + BOOST_LOG_TRIVIAL(warning) << "ionospheric_components:outage_reset_limit < " + "epoch_interval, but it probably shouldnt be"; + + if (acsConfig.simulate_real_time == false) + { + for (E_Sys sys : magic_enum::enum_values()) + { + eph_time_delay[sys] = default_eph_time_delay[sys]; + } + } + + if (acsConfig.pppOpts.ionoOpts.use_if_combo) + { + for (auto& [id, recOpts] : recOptsMap) + { + if (recOpts.ionospheric_component2) + { + 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) + { + 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"; + } + } + } } bool ACSConfig::parse() { - return parse(configFilenames, commandOpts); + return parse(configFilenames, commandOpts); } /** Parse options to set acsConfig values. -* Command line options will override any values set in config files, which will themselves override any program default values. -*/ + * Command line options will override any values set in config files, which will themselves override + * any program default values. + */ bool ACSConfig::parse( - const vector& filenames, ///< Path to yaml based config file - boost::program_options::variables_map& newCommandOpts) ///< Variable map object of command line options -{ - DOCS_REFERENCE(Config__); - - configFilenames = filenames; - - bool modified = false; - - for (auto& filenameList : {filenames, includedFilenames}) - for (auto& filename : filenameList) - if (filename != "") - { - std::filesystem::path filePath(filename); - auto currentConfigModifyTime = std::filesystem::last_write_time(filePath); - - if (currentConfigModifyTime != configModifyTimeMap[filename]) - { - modified = true; - } - } - else - { - modified = true; - } - - if (modified == false) - { - return false; - } - - commandOpts = newCommandOpts; - - //clear old saved parameters - foundOptions .clear(); - satOptsMap .clear(); - recOptsMap .clear(); - defaultOutputOptions(); - - for (int i = E_Sys::GPS; i < E_Sys::SUPPORTED; i++) - { - E_Sys sys = E_Sys::_values()[i]; - - code_priorities[sys] = code_priorities_default; - } - - vector yamlList; - - yamls.resize(filenames.size()); - - for (int i = 0; i < filenames.size(); i++) - { - auto& filename = filenames [i]; - auto& yaml = yamls [i]; - - BOOST_LOG_TRIVIAL(info) - << "Checking configuration file " << filename; - - try - { - yaml.reset(); - yaml = YAML::LoadFile(filename); - } - catch (const YAML::BadFile &e) - { - if (commandOpts.count("yaml-defaults")) - { - //we expect to break, continue parsing - } - else - { - BOOST_LOG_TRIVIAL(error) << "Error: \nFailed to parse configuration file " << filename; - BOOST_LOG_TRIVIAL(error) << e.msg << "\n"; - return false; - } - } - catch (const YAML::ParserException& e) - { - BOOST_LOG_TRIVIAL(error) << "Error: \nFailed to parse configuration. Check for errors as described near the below:\n"; - BOOST_LOG_TRIVIAL(error) << e.what() << "\n" << "\n"; - return false; - } - - vector includes; - - auto inputs = stringsToYamlObject({yaml, ""}, {"0! inputs"}, docs["inputs"]); - - tryGetFromYaml(includes, inputs, {"1! include_yamls"}, "List of yaml files to include before this one"); - - for (auto& include : includes) - { - yamlList.push_back(include); - } - } - - for (auto& filename : filenames) - { - yamlList.push_back(filename); - } - - includedFilenames = yamlList; - - yamls.resize(yamlList.size()); - - defaultConfigs(); - - for (int i = 0; i < yamlList.size(); i++) - { - auto& filename = yamlList [i]; - auto& yaml = yamls [i]; - - BOOST_LOG_TRIVIAL(info) - << "Loading configuration from file " << filename; - - try - { - std::filesystem::path filePath(filename); - auto currentConfigModifyTime = std::filesystem::last_write_time(filePath); - - configModifyTimeMap[filename] = currentConfigModifyTime; - - yaml.reset(); - yaml = YAML::LoadFile(filename); - yaml["yaml_filename"] = filename; - yaml["yaml_number"] = i; - } - catch (const YAML::BadFile &e) - { - if (commandOpts.count("yaml-defaults")) - { - //we expect to break, continue parsing - } - else - { - BOOST_LOG_TRIVIAL(error) << "Error: \nFailed to parse configuration file " << filename; - BOOST_LOG_TRIVIAL(error) << e.msg << "\n"; - return false; - } - } - catch (const std::filesystem::filesystem_error &e) - { - if (commandOpts.count("yaml-defaults")) - { - //we expect to break, continue parsing - } - else - { - BOOST_LOG_TRIVIAL(error) << "Error: \nFailed to parse configuration file " << filename; - return false; - } - } - catch (const YAML::ParserException& e) - { - BOOST_LOG_TRIVIAL(error) << "Error: \nFailed to parse configuration. Check for errors as described near the below:\n"; - BOOST_LOG_TRIVIAL(error) << e.what() << "\n" << "\n"; - return false; - } - - recurseLowerCase(yaml); - -// outputs - { - auto outputs = stringsToYamlObject({yaml, ""}, {"1! outputs"}, docs["outputs"]); - - tryGetFromYaml(outputs_root, outputs, {"0! outputs_root" }, "Directory that outputs will be placed in"); - - { - auto metadata = stringsToYamlObject(outputs, {"2! metadata"}, "Options for setting metadata for inputs and outputs"); - - tryGetFromAny (config_description, commandOpts, metadata, {"1! config_description" }, "ID for this config, used to replace tags in other options"); - tryGetFromAny (stream_user, commandOpts, metadata, {"1! user" }, "Username for connecting to NTRIP casters"); - tryGetFromAny (stream_pass, commandOpts, metadata, {"1! pass" }, "Password for connecting to NTRIP casters"); - tryGetFromYaml(analysis_agency, metadata, {"@ analysis_agency" }, "Agency for output files headers"); - tryGetFromYaml(config_details, metadata, {"@ config_details" }, "Comments and details specific to the config"); - tryGetFromYaml(analysis_centre, metadata, {"@ analysis_centre" }, "Analysis centre for output files headers"); - tryGetFromYaml(ac_contact, metadata, {"@ ac_contact" }, "Contact person for output files headers"); - tryGetFromYaml(analysis_software, metadata, {"@ analysis_software" }, "Program for output files headers"); - tryGetFromYaml(analysis_software_version, metadata, {"@ analysis_software_version" }, "Version for output files headers"); - tryGetFromYaml(rinex_comment, metadata, {"@ rinex_comment" }, "Comment for output files headers"); - tryGetFromYaml(reference_system, metadata, {"@ reference_system" }, "Terrestrial Reference System Code"); - tryGetFromYaml(time_system, metadata, {"@ time_system" }, "Time system - e.g. \"G\", \"UTC\""); - tryGetFromYaml(ocean_tide_loading_model, metadata, {"@ ocean_tide_loading_model" }, "Ocean tide loading model applied"); - tryGetFromYaml(atmospheric_tide_loading_model, metadata, {"@ atmospheric_tide_loading_model" }, "Atmospheric tide loading model applied"); - tryGetFromYaml(geoid_model, metadata, {"@ geoid_model" }, "Geoid model name for undulation values"); - tryGetFromYaml(gradient_mapping_function, metadata, {"@ gradient_mapping_function" }, "Name of mapping function used for mapping horizontal troposphere gradients"); - } - - { - tryGetFromYaml(colourise_terminal, outputs, {"1! colourise_terminal" }, "Use ascii command codes to highlight warnings and errors"); - tryGetFromYaml(warn_once, outputs, {"1! warn_once" }, "Print warnings once only"); - } - - { - auto trace = stringsToYamlObject(outputs, {"2! trace"}, docs["trace"]); - - tryGetFromYaml(output_receiver_trace, trace, {"0! output_receivers" }, "Output trace files for individual receivers processing"); - tryGetFromYaml(output_network_trace, trace, {"0! output_network" }, "Output trace files for complete network of receivers, inclucing kalman filter results and statistics"); - tryGetFromYaml(output_ionosphere_trace, trace, {"0@ output_ionosphere" }, "Output trace files for ionosphere processing, inclucing kalman filter results and statistics"); - tryGetFromYaml(output_satellite_trace, trace, {"0@ output_satellites" }, "Output trace files for individual satellites processing"); - conditionalPrefix("", trace_directory, tryGetFromYaml(trace_directory, trace, {"! directory" }, "Directory to output trace files to")); - conditionalPrefix("", satellite_trace_filename, tryGetFromYaml(satellite_trace_filename, trace, {"1@ satellite_filename" }, "Template filename for satellite trace files")); - conditionalPrefix("", receiver_trace_filename, tryGetFromYaml(receiver_trace_filename, trace, {"1! receiver_filename" }, "Template filename for receiver trace files")); - conditionalPrefix("", ionosphere_trace_filename, tryGetFromYaml(ionosphere_trace_filename, trace, {"1@ ionosphere_filename" }, "Template filename for ionosphere trace files")); - conditionalPrefix("", network_trace_filename, tryGetFromYaml(network_trace_filename, trace, {"1! network_filename" }, "Template filename for network trace files")); - tryGetFromAny(trace_level, commandOpts, trace, {"! level" }, "Threshold level for printing messages (0-6). Increasing this increases the amount of data stored in all trace files"); - - traceLevel = acsConfig.trace_level; - - tryGetFromYaml(output_residual_chain, trace, {"! output_residual_chain" }, "Output component-wise details for measurement residuals"); - tryGetFromYaml(output_predicted_states, trace, {"! output_predicted_states" }, "Output states after state transition 1"); - tryGetFromYaml(output_initialised_states, trace, {"! output_initialised_states" }, "Output states after state transition 2"); - tryGetFromYaml(output_residuals, trace, {"! output_residuals" }, "Output measurements and residuals"); - tryGetFromYaml(output_config, trace, {"! output_config" }, "Output configuration files to top of trace files"); - tryGetFromYaml(output_json_trace, trace, {"@ output_json" }, "Output json formatted trace files"); - } - - { - auto output_rotation = stringsToYamlObject(outputs, {"2@ output_rotation"}, "Trace files can be rotated periodically by epoch interval. These options specify the period that applies to the template variables in filenames"); - - tryGetScaledFromYaml(rotate_period, output_rotation, {"@ period" }, {"@ period_units" }, E_Period::_from_string_nocase, "Period that times will be rounded by to generate template variables in filenames"); - } - - { - auto bias_sinex = stringsToYamlObject(outputs, {"3@ bias_sinex"}, "Rinex formatted bias sinex files"); - - tryGetFromYaml(output_bias_sinex, bias_sinex, {"0@ output" }, "Output bias sinex files"); - conditionalPrefix("", bias_sinex_directory, tryGetFromYaml(bias_sinex_directory, bias_sinex, {"@ directory" }, "Directory to output bias sinex files to")); - conditionalPrefix("", bias_sinex_filename, tryGetFromYaml(bias_sinex_filename, bias_sinex, {"@ filename" }, "Template filename for bias sinex files")); - tryGetFromYaml(bias_time_system, bias_sinex, {"@ bias_time_system" }, "Time system for bias SINEX \"G\", \"C\", \"R\", \"UTC\", \"TAI\""); - tryGetFromYaml(ambrOpts.code_output_interval, bias_sinex, {"@ code_output_interval" }, "Update interval for code biases"); - tryGetFromYaml(ambrOpts.phase_output_interval, bias_sinex, {"@ phase_output_interval" }, "Update interval for phase biases"); - tryGetFromYaml(ambrOpts.output_rec_bias, bias_sinex, {"@ output_rec_bias" }, "output receiver biases"); - } - - { - auto clocks = stringsToYamlObject(outputs, {"3! clocks"}, "Rinex formatted clock files"); - - tryGetFromYaml(output_clocks, clocks, {"0! output" }, "Output clock files"); - conditionalPrefix("", clocks_directory, tryGetFromYaml(clocks_directory, clocks, {"@ directory" }, "Directory to output clock files to")); - conditionalPrefix("", clocks_filename, tryGetFromYaml(clocks_filename, clocks, {"@ filename" }, "Template filename for clock files")); - tryGetEnumVec (clocks_receiver_sources, clocks, {"@ receiver_sources" }); - tryGetEnumVec (clocks_satellite_sources, clocks, {"@ satellite_sources" }); - tryGetFromYaml(clocks_output_interval, clocks, {"@ output_interval" }, "Update interval for clock records"); - } - - { - auto decoded_rtcm = stringsToYamlObject(outputs, {"6@ decoded_rtcm"}, "RTCM messages that are received may be recorded to human-readable json files"); - - tryGetFromYaml(output_decoded_rtcm_json, decoded_rtcm, {"0@ output" }, "Enable exporting decoded RTCM data to file"); - conditionalPrefix("", decoded_rtcm_json_directory, tryGetFromYaml(decoded_rtcm_json_directory, decoded_rtcm, {"@ directory" }, "Directory to export decoded RTCM data")); - conditionalPrefix("", decoded_rtcm_json_filename, tryGetFromYaml(decoded_rtcm_json_filename, decoded_rtcm, {"@ filename" }, "Decoded RTCM data filename")); - } - - { - auto encoded_rtcm = stringsToYamlObject(outputs, {"6@ encoded_rtcm"}, "RTCM messages that are encoded and transmitted may be recorded to human-readable json files"); - - tryGetFromYaml(output_encoded_rtcm_json, encoded_rtcm, {"0@ output" }, "Enable exporting encoded RTCM data to file"); - conditionalPrefix("", encoded_rtcm_json_directory, tryGetFromYaml(encoded_rtcm_json_directory, encoded_rtcm, {"@ directory" }, "Directory to export encoded RTCM data")); - conditionalPrefix("", encoded_rtcm_json_filename, tryGetFromYaml(encoded_rtcm_json_filename, encoded_rtcm, {"@ filename" }, "Encoded RTCM data filename")); - } - - { - auto erp = stringsToYamlObject(outputs, {"3@ erp"}, "Earth rotation parameters can be output to file"); - - tryGetFromYaml(output_erp, erp, {"0@ output" }, "Enable exporting of erp data"); - conditionalPrefix("", erp_directory, tryGetFromYaml(erp_directory, erp, {"@ directory" }, "Directory to export erp data files")); - conditionalPrefix("", erp_filename, tryGetFromYaml(erp_filename, erp, {"@ filename" }, "ERP data output filename")); - } - - { - auto ionex = stringsToYamlObject(outputs, {"4@ ionex"}, "IONEX formatted ionospheric mapping and modelling outputs"); - - tryGetFromYaml(output_ionex, ionex, {"0@ output" }, "Enable exporting ionospheric model data"); - conditionalPrefix("", ionex_directory, tryGetFromYaml(ionex_directory, ionex, {"@ directory" }, "Directory to export ionex data")); - conditionalPrefix("", ionex_filename, tryGetFromYaml(ionex_filename, ionex, {"@ filename" }, "Ionex data filename")); - tryGetFromYaml(ionexGrid.lat_centre, ionex, {"@ grid", "@ lat_centre" }, "Center lattitude for models"); - tryGetFromYaml(ionexGrid.lon_centre, ionex, {"@ grid", "@ lon_centre" }, "Center longitude for models"); - tryGetFromYaml(ionexGrid.lat_width, ionex, {"@ grid", "@ lat_width" }, "Total lattitudinal width of model"); - tryGetFromYaml(ionexGrid.lon_width, ionex, {"@ grid", "@ lon_width" }, "Total longitudinal width of model"); - tryGetFromYaml(ionexGrid.lat_res, ionex, {"@ grid", "@ lat_resolution" }, "Interval between lattitude outputs"); - tryGetFromYaml(ionexGrid.lon_res, ionex, {"@ grid", "@ lon_resolution" }, "Interval between longitude outputs"); - tryGetFromYaml(ionexGrid.time_res, ionex, {"@ grid", "@ time_resolution" }, "Interval between output epochs"); - } - - { - auto ionstec = stringsToYamlObject(outputs, {"4@ ionstec"}); - - tryGetFromYaml(output_ionstec, ionstec, {"0@ output" }); - conditionalPrefix("", ionstec_directory, tryGetFromYaml(ionstec_directory, ionstec, {"@ directory" })); - conditionalPrefix("", ionstec_filename, tryGetFromYaml(ionstec_filename, ionstec, {"@ filename" })); - } - - { - auto sbas_ems = stringsToYamlObject(outputs, {"4@ sbas_ems"}); - - tryGetFromYaml(output_sbas_ems, sbas_ems, {"0@ output" }); - conditionalPrefix("", ems_directory, tryGetFromYaml(ems_directory, sbas_ems, {"@ directory" })); - conditionalPrefix("", ems_filename, tryGetFromYaml(ems_filename, sbas_ems, {"@ filename" })); - } - - { - auto sinex = stringsToYamlObject(outputs, {"3@ sinex"}); - - tryGetFromYaml(output_sinex, sinex, {"0@ output" }); - conditionalPrefix("", sinex_directory, tryGetFromYaml(sinex_directory, sinex, {"@ directory" })); - conditionalPrefix("", sinex_filename, tryGetFromYaml(sinex_filename, sinex, {"@ filename" })); - } - - { - auto log = stringsToYamlObject(outputs, {"3! log"}, "Log files store console output in files"); - - tryGetFromYaml(output_log, log, {"0! output" }, "Enable console output logging"); - conditionalPrefix("", log_directory, tryGetFromYaml(log_directory, log, {"@ directory" }, "Log output directory")); - conditionalPrefix("", log_filename, tryGetFromYaml(log_filename, log, {"@ filename" }, "Log output filename")); - } - - { - auto gpx = stringsToYamlObject(outputs, {"3! gpx"}, "GPX files contain point data that may be easily viewed in GIS mapping software"); - - tryGetFromYaml(output_gpx, gpx, {"0! output" }); - conditionalPrefix("", gpx_directory, tryGetFromYaml(gpx_directory, gpx, {"@ directory" })); - conditionalPrefix("", gpx_filename, tryGetFromYaml(gpx_filename, gpx, {"@ filename" })); - } - { - auto pos = stringsToYamlObject(outputs, {"3! pos"}, "POS files contain point data that may be easily viewed in GIS mapping software"); - - tryGetFromYaml(output_pos, pos, {"0! output" }); - conditionalPrefix("", pos_directory, tryGetFromYaml(pos_directory, pos, {"@ directory" })); - conditionalPrefix("", pos_filename, tryGetFromYaml(pos_filename, pos, {"@ filename" })); - } - - { - auto ntrip_log = stringsToYamlObject(outputs, {"5@ ntrip_log"}); - - tryGetFromYaml(output_ntrip_log, ntrip_log, {"0@ output" }); - conditionalPrefix("", ntrip_log_directory, tryGetFromYaml(ntrip_log_directory, ntrip_log, {"@ directory" })); - conditionalPrefix("", ntrip_log_filename, tryGetFromYaml(ntrip_log_filename, ntrip_log, {"@ filename" })); - } - - { - auto network_statistics = stringsToYamlObject(outputs, {"5@ network_statistics"}); - - tryGetFromYaml(output_network_statistics_json, network_statistics, {"0@ output" }, "Enable exporting network statistics data to file"); - conditionalPrefix("", network_statistics_json_directory, tryGetFromYaml(network_statistics_json_directory, network_statistics, {"@ directory" }, "Directory to export network statistics data")); - conditionalPrefix("", network_statistics_json_filename, tryGetFromYaml(network_statistics_json_filename, network_statistics, {"@ filename" }, "Network statistics data filename")); - } - - { - auto sp3 = stringsToYamlObject(outputs, {"3@ sp3"}, "SP3 files contain orbital and clock data of satellites and receivers"); - - tryGetFromYaml(output_sp3, sp3, {"0@ output" }, "Enable SP3 file outputs"); - tryGetFromYaml(output_inertial_orbits, sp3, {"@ output_inertial" }, "Output the entries using inertial positions and velocities"); - tryGetFromYaml(output_sp3_velocities, sp3, {"@ output_velocities" }, "Output velocity data to SP3 file"); - conditionalPrefix("", sp3_directory, tryGetFromYaml(sp3_directory, sp3, {"@ directory" }, "Directory to store SP3 outputs")); - conditionalPrefix("", sp3_filename, tryGetFromYaml(sp3_filename, sp3, {"@ filename" }, "SP3 output filename")); - conditionalPrefix("", predicted_sp3_filename, tryGetFromYaml(predicted_sp3_filename, sp3, {"@ predicted_filename" }, "Filename for predicted SP3 outputs")); - tryGetEnumVec (sp3_clock_sources, sp3, {"@ clock_sources" }, "List of sources for clock data for SP3 outputs"); - tryGetEnumVec (sp3_orbit_sources, sp3, {"@ orbit_sources" }, "List of sources for orbit data for SP3 outputs"); - tryGetFromYaml(sp3_output_interval, sp3, {"@ output_interval" }, "Update interval for SP3 records"); - } - - { - auto orbit_ics = stringsToYamlObject(outputs, {"4@ orbit_ics"}, "Orbital parameters can be output in a yaml that Ginan can later use as an initial condition for futher processing."); - - tryGetFromYaml(output_orbit_ics, orbit_ics, {"@ output" }, "Output orbital initial condition file"); - conditionalPrefix("", orbit_ics_directory, tryGetFromYaml(orbit_ics_directory, orbit_ics, {"@ directory" }, "Output orbital initial condition directory")); - conditionalPrefix("", orbit_ics_filename, tryGetFromYaml(orbit_ics_filename, orbit_ics, {"@ filename" }, "Output orbital initial condition filename")); - } - - { - auto orbex = stringsToYamlObject(outputs, {"3@ orbex"}); - - tryGetFromYaml(output_orbex, orbex, {"0@ output" }, "Output orbex file"); - conditionalPrefix("", orbex_directory, tryGetFromYaml(orbex_directory, orbex, {"@ directory" }, "Output orbex directory")); - conditionalPrefix("", orbex_filename, tryGetFromYaml(orbex_filename, orbex, {"@ filename" }, "Output orbex filename")); - tryGetEnumVec (orbex_orbit_sources, orbex, {"@ orbit_sources" }, "Sources for orbex orbits"); - tryGetEnumVec (orbex_clock_sources, orbex, {"@ clock_sources" }, "Sources for orbex clocks"); - tryGetEnumVec (orbex_attitude_sources, orbex, {"@ attitude_sources" }, "Sources for orbex attitudes"); - tryGetEnumVec (orbex_record_types, orbex, {"@ record_types" }, "List of record types to output to orbex file"); - tryGetFromYaml(orbex_output_interval, orbex, {"@ output_interval" }, "Update interval for orbex records (irregular epoch interval is currently NOT supported)"); - } - - { - auto cost = stringsToYamlObject(outputs, {"3@ cost"}, docs["cost"]); - - tryGetFromYaml(output_cost, cost, {"0@ output" }, "Enable data exporting to troposphere COST file"); - tryGetEnumVec (cost_data_sources, cost, {"@ sources" }, "Source for troposphere delay data - KALMAN, etc."); - conditionalPrefix("", cost_directory, tryGetFromYaml(cost_directory, cost, {"@ directory" }, "Directory to export troposphere COST file")); - conditionalPrefix("", cost_filename, tryGetFromYaml(cost_filename, cost, {"@ filename" }, "Troposphere COST filename")); - tryGetFromYaml(cost_time_interval, cost, {"@ time_interval" }, "Time interval between entries in troposphere COST file (sec)"); - tryGetFromYaml(cost_format, cost, {"@ cost_format" }, "Format name & version number"); - tryGetFromYaml(cost_project, cost, {"@ cost_project" }, "Project name"); - tryGetFromYaml(cost_status, cost, {"@ cost_status" }, "File status"); - tryGetFromYaml(cost_centre, cost, {"@ cost_centre" }, "Processing centre"); - tryGetFromYaml(cost_method, cost, {"@ cost_method" }, "Processing method"); - tryGetFromYaml(cost_orbit_type, cost, {"@ cost_orbit_type" }, "Orbit type"); - tryGetFromYaml(cost_met_source, cost, {"@ cost_met_sources" }, "Source of met. data"); - } - - { - auto rinex_nav = stringsToYamlObject(outputs, {"5@ rinex_nav"}); - - tryGetFromYaml(output_rinex_nav, rinex_nav, {"0@ output" }); - conditionalPrefix("", rinex_nav_directory, tryGetFromYaml(rinex_nav_directory, rinex_nav, {"@ directory" })); - conditionalPrefix("", rinex_nav_filename, tryGetFromYaml(rinex_nav_filename, rinex_nav, {"@ filename" })); - tryGetFromYaml(rinex_nav_version, rinex_nav, {"@ version" }); - } - - { - auto rinex_obs = stringsToYamlObject(outputs, {"5@ rinex_obs"}); - - tryGetFromYaml(output_rinex_obs, rinex_obs, {"0@ output" }); - conditionalPrefix("", rinex_obs_directory, tryGetFromYaml(rinex_obs_directory, rinex_obs, {"@ directory" })); - conditionalPrefix("", rinex_obs_filename, tryGetFromYaml(rinex_obs_filename, rinex_obs, {"@ filename" })); - tryGetFromYaml(rinex_obs_print_C_code, rinex_obs, {"@ output_pseudorange" }); - tryGetFromYaml(rinex_obs_print_L_code, rinex_obs, {"@ output_phase_range" }); - tryGetFromYaml(rinex_obs_print_D_code, rinex_obs, {"@ output_doppler" }); - tryGetFromYaml(rinex_obs_print_S_code, rinex_obs, {"@ output_signal_to_noise" }); - tryGetFromYaml(rinex_obs_version, rinex_obs, {"@ version" }); - } - - { - auto rtcm_nav = stringsToYamlObject(outputs, {"5@ rtcm_nav"}); - - tryGetFromYaml(record_rtcm_nav, rtcm_nav, {"0@ output" }); - conditionalPrefix("", rtcm_nav_directory, tryGetFromYaml(rtcm_nav_directory, rtcm_nav, {"@ directory" })); - conditionalPrefix("", rtcm_nav_filename, tryGetFromYaml(rtcm_nav_filename, rtcm_nav, {"@ filename" })); - } - - { - auto rtcm_obs = stringsToYamlObject(outputs, {"5@ rtcm_obs"}); - - tryGetFromYaml(record_rtcm_obs, rtcm_obs, {"0@ output" }); - conditionalPrefix("", rtcm_obs_directory, tryGetFromYaml(rtcm_obs_directory, rtcm_obs, {"@ directory" })); - conditionalPrefix("", rtcm_obs_filename, tryGetFromYaml(rtcm_obs_filename, rtcm_obs, {"@ filename" })); - } - - { - auto raw_ubx = stringsToYamlObject(outputs, {"6@ raw_ubx"}); - - tryGetFromYaml(record_raw_ubx, raw_ubx, {"0 output" }); - conditionalPrefix("", raw_ubx_directory, tryGetFromYaml(raw_ubx_directory, raw_ubx, {"directory" })); - conditionalPrefix("", raw_ubx_filename, tryGetFromYaml(raw_ubx_filename, raw_ubx, {"filename" })); - } - - { - auto raw_custom = stringsToYamlObject(outputs, {"6@ raw_custom"}); - - tryGetFromYaml(record_raw_custom, raw_custom, {"0 output" }); - conditionalPrefix("", raw_custom_directory, tryGetFromYaml(raw_custom_directory, raw_custom, {"directory" })); - conditionalPrefix("", raw_custom_filename, tryGetFromYaml(raw_custom_filename, raw_custom, {"filename" })); - } - - { - auto slr_obs = stringsToYamlObject(outputs, {"7@ slr_obs"}, docs["slr_obs"]); - - tryGetFromYaml(output_slr_obs, slr_obs, {"0@ output" }, "Enable data exporting to tabular SLR obs file"); - conditionalPrefix("", slr_obs_directory, tryGetFromYaml(slr_obs_directory, slr_obs, {"@ directory" }, "Directory to export tabular SLR obs file")); - conditionalPrefix("", slr_obs_filename, tryGetFromYaml(slr_obs_filename, slr_obs, {"@ filename" }, "Tabular SLR obs filename")); - } - - { - auto trop_sinex = stringsToYamlObject(outputs, {"3@ trop_sinex"}, docs["trop_sinex"]); - - tryGetFromYaml(output_trop_sinex, trop_sinex, {"0@ output" }, "Enable data exporting to troposphere SINEX file"); - tryGetEnumVec (trop_sinex_data_sources, trop_sinex, {"@ sources" }, "Source for troposphere delay data - KALMAN, etc."); - conditionalPrefix("", trop_sinex_directory, tryGetFromYaml(trop_sinex_directory, trop_sinex, {"@ directory" }, "Directory to export troposphere SINEX file")); - conditionalPrefix("", trop_sinex_filename, tryGetFromYaml(trop_sinex_filename, trop_sinex, {"@ filename" }, "Troposphere SINEX filename")); - tryGetFromYaml(trop_sinex_sol_type, trop_sinex, {"@ sol_type" }, "Troposphere SINEX solution type"); - tryGetFromYaml(trop_sinex_obs_code, trop_sinex, {"@ obs_code" }, "Troposphere SINEX observation code"); - tryGetFromYaml(trop_sinex_const_code, trop_sinex, {"@ const_code" }, "Troposphere SINEX const code"); - tryGetFromYaml(trop_sinex_version, trop_sinex, {"@ version" }, "Troposphere SINEX version"); - } - - -// ssr_outputs - { - auto ssr_outputs = stringsToYamlObject(outputs, {"2@ ssr_outputs"}, docs["ssr_outputs"]); - - tryGetEnumVec (ssrOpts.ephemeris_sources, ssr_outputs, {"@ ephemeris_sources" }, "Sources for SSR ephemeris"); - tryGetEnumVec (ssrOpts.clock_sources, ssr_outputs, {"@ clock_sources" }, "Sources for SSR clocks"); - tryGetEnumVec (ssrOpts.code_bias_sources, ssr_outputs, {"2@ code_bias_sources" }, "Sources for SSR code biases"); - tryGetEnumVec (ssrOpts.phase_bias_sources, ssr_outputs, {"2@ phase_bias_sources" }, "Sources for SSR phase biases"); - tryGetFromYaml(ssrOpts.prediction_interval, ssr_outputs, {"@ prediction_interval" }); - tryGetFromYaml(ssrOpts.prediction_duration, ssr_outputs, {"@ prediction_duration" }); - tryGetFromYaml(ssrOpts.extrapolate_corrections, ssr_outputs, {"@ extrapolate_corrections" }); - tryGetFromYaml(ssrOpts.cmpssr_cell_mask, ssr_outputs, {"@ cmpssr_cell_mask" }); - tryGetFromYaml(ssrOpts.max_stec_sigma, ssr_outputs, {"@ max_stec_sigma" }); - -// atmospheric - { - auto atmospheric = stringsToYamlObject(ssr_outputs, {"@ atmospheric"}, docs["atmospheric"]); - - tryGetEnumVec (ssrOpts.atmosphere_sources, atmospheric, {"@ sources" }, "Sources for SSR ionosphere"); - tryGetFromYaml(ssrOpts.region_id, atmospheric, {"@ region_id" }, "Region ID for atmospheric corrections"); - tryGetFromYaml(ssrOpts.region_iod, atmospheric, {"@ region_iod" }, "Region IOD for atmospheric corrections (default: -1 for undefined)"); - tryGetFromYaml(ssrOpts.npoly_trop, atmospheric, {"@ npoly_trop" }); - tryGetFromYaml(ssrOpts.npoly_iono, atmospheric, {"@ npoly_iono" }); - tryGetFromYaml(ssrOpts.grid_type, atmospheric, {"@ grid_type" }, "Grid type for gridded atmospheric corrections"); - tryGetFromYaml(ssrOpts.use_grid_iono, atmospheric, {"@ use_grid_iono" }, "Grid type for gridded atmospheric corrections"); - tryGetFromYaml(ssrOpts.use_grid_trop, atmospheric, {"@ use_grid_trop" }, "Grid type for gridded atmospheric corrections"); - tryGetFromYaml(ssrOpts.lat_max, atmospheric, {"@ lat_max" }); - tryGetFromYaml(ssrOpts.lat_min, atmospheric, {"@ lat_min" }); - tryGetFromYaml(ssrOpts.lat_int, atmospheric, {"@ lat_int" }); - tryGetFromYaml(ssrOpts.lon_max, atmospheric, {"@ lon_max" }); - tryGetFromYaml(ssrOpts.lon_min, atmospheric, {"@ lon_min" }); - tryGetFromYaml(ssrOpts.lon_int, atmospheric, {"@ lon_int" }); - tryGetFromYaml(ssrOpts.cmpssr_stec_format, atmospheric, {"@ cmpssr_stec_format" }, "Format of STEC gridded corrections: 0:4bit(LSB=0.04) , 1:4bit(LSB=0.12), 2:5bit, 3:7bit, 4:16bit"); - tryGetFromYaml(ssrOpts.cmpssr_trop_format, atmospheric, {"@ cmpssr_trop_format" }, "Format of Trop. ZWD corrections: 0:8bit, 1:6bit"); - } - } - - { - auto streams = stringsToYamlObject(outputs, {"2@ streams"}); - - tryGetFromYaml(root_stream_url, streams, {"0@ root_url"}, "Root url to be prepended to all other streams specified in this section. If the streams used have individually specified root urls, usernames, or passwords, this should not be used."); - - SsrBroadcast dummyStreamData; - tryGetStreamFromYaml(dummyStreamData, streams, {"@ XMPL"}); - - auto [outStreamNode, outStreamString] = stringsToYamlObject(streams, {"1@ labels"}, "List of output stream is with further information to be found in its own section, as per XMPL below"); - - for (auto outLabelYaml : outStreamNode) - { - string outLabel = outLabelYaml.as(); - - tryGetStreamFromYaml(netOpts.uploadingStreamData[outLabel], streams, {outLabel}); - - conditionalPrefix("", netOpts.uploadingStreamData[outLabel].url); - - replaceTags(netOpts.uploadingStreamData[outLabel].url); - } - } - } - -// inputs - { - auto inputs = stringsToYamlObject({yaml, ""}, {"0! inputs" }, docs["inputs"]); - auto troposphere = stringsToYamlObject(inputs, {"2@ troposphere" }, "Files specifying tropospheric model inputs"); - auto tides = stringsToYamlObject(inputs, {"2@ tides" }, "Files specifying tidal loading and potential inputs"); - auto ionosphere = stringsToYamlObject(inputs, {"3@ ionosphere" }, "Files specifying ionospheric model inputs"); - - tryGetFromAny(inputs_root, commandOpts, inputs, {"0! inputs_root" }, "Root path to be added to all other input files (unless they are absolute)"); - - auto getAppendFiles = [&]( - vector& output, - NodeStack& nodeStack, - const string& descriptor, - const string& comment) - { - vector vec; - - tryGetFromAny(vec, commandOpts, nodeStack, {descriptor}, comment); - - conditionalPrefix("", vec); - - output.insert(output.end(), vec.begin(), vec.end()); - }; - - getAppendFiles(atx_files , inputs, {"4! atx_files" }, "List of atx files to use"); - getAppendFiles(snx_files , inputs, {"4@ snx_files" }, "List of snx files to use"); - getAppendFiles(erp_files , inputs, {"4! erp_files" }, "List of erp files to use"); - getAppendFiles(igrf_files , inputs, {"4@ igrf_files" }, "List of igrf files to use"); - getAppendFiles(egm_files , inputs, {"4@ egm_files" }, "List of egm files to use"); - getAppendFiles(planetary_ephemeris_files , inputs, {"4@ planetary_ephemeris_files" }, "List of jpl files to use"); - getAppendFiles(cmc_files , inputs, {"4@ cmc_files" }, "List of cmc files to use"); - getAppendFiles(hfeop_files , inputs, {"4@ hfeop_files" }, "List of hfeop files to use"); - getAppendFiles(atm_reg_definitions , ionosphere, {"@ atm_reg_definitions" }, "List of files to define regions for compact SSR"); - getAppendFiles(ion_files , ionosphere, {"@ ion_files" }, "List of IONEX files for VTEC input"); - getAppendFiles(vmf_files , troposphere, {"@ vmf_files" }, "List of vmf files to use"); - getAppendFiles(gpt2grid_files , troposphere, {"@ gpt2grid_files" }, "List of gpt2 grid files to use"); - getAppendFiles(orography_files , troposphere, {"@ orography_files" }, "List of orography files to use"); - getAppendFiles(ocean_tide_potential_files , tides, {"@ ocean_tide_potential_files" }, "List of tide files to use"); - getAppendFiles(atmos_tide_potential_files , tides, {"@ atmos_tide_potential_files" }, "List of tide files to use"); - getAppendFiles(ocean_tide_loading_blq_files , tides, {"@ ocean_tide_loading_blq_files" }, "List of otl blq files to use"); - getAppendFiles(atmos_tide_loading_blq_files , tides, {"@ atmos_tide_loading_blq_files" }, "List of atl blq files to use"); - getAppendFiles(ocean_pole_tide_loading_files , tides, {"@ ocean_pole_tide_loading_files" }, "List of opole files to use"); - getAppendFiles(atmos_oceean_dealiasing_files , tides, {"@ atmos_oceean_dealiasing_files" }, "List of tide files to use"); - getAppendFiles(ocean_pole_tide_potential_files , tides, {"@ ocean_pole_tide_potential_files"}, "List of tide files to use"); - - tryGetEnumVec (atl_blq_row_order , tides, {"@ atl_blq_row_order" }, "Row order for amplitude and phase components in ATL BLQ files"); - tryGetEnumVec (otl_blq_row_order , tides, {"@ otl_blq_row_order" }, "Row order for amplitude and phase components in OTL BLQ files"); - - tryGetEnumVec (atl_blq_col_order , tides, {"@ atl_blq_col_order" }, "Column order for amplitude and phase components in ATL BLQ files"); - tryGetEnumVec (otl_blq_col_order , tides, {"@ otl_blq_col_order" }, "Column order for amplitude and phase components in OTL BLQ files"); - - { - auto gnss_data = stringsToYamlObject(inputs, {"2! gnss_observations"}, "Signal observation data from gnss receivers to be used as measurements"); - - conditionalPrefix("", gnss_obs_root, tryGetFromAny(gnss_obs_root, commandOpts, gnss_data, {"0! gnss_observations_root" }, "Root path to be added to all other gnss data inputs (unless they are absolute)")); - - tryGetMappedList(rnx_inputs, commandOpts, gnss_data, {"1! rnx_inputs" }, "", "List of rinex inputs to use"); - tryGetMappedList(ubx_inputs, commandOpts, gnss_data, {"1# ubx_inputs" }, "", "List of ubxfiles inputs to use"); - tryGetMappedList(custom_inputs, commandOpts, gnss_data, {"1# custom_inputs" }, "", "List of customfiles inputs to use"); - tryGetMappedList(obs_rtcm_inputs, commandOpts, gnss_data, {"1! rtcm_inputs" }, "", "List of rtcmfiles inputs to use for observations"); - } - - { - auto pseudo_observation_data = stringsToYamlObject(inputs, {"2@ pseudo_observations"}, "Use data from pre-processed data products as observations. Useful for combining and comparing datasets"); - - conditionalPrefix("", pseudo_obs_root, tryGetFromYaml(pseudo_obs_root, pseudo_observation_data, {"0@ pseudo_observations_root" }, "Root path to be added to all other pseudo obs data files (unless they are absolute)")); - - tryGetMappedList(pseudo_sp3_inputs, commandOpts, pseudo_observation_data, {"@@ sp3_inputs" }, "", "List of sp3 inputs to use for pseudoobservations"); - tryGetMappedList(pseudo_snx_inputs, commandOpts, pseudo_observation_data, {"1@ snx_inputs" }, "", "List of snx inputs to use for pseudoobservations"); - conditionalPrefix("", pseudo_filter_files, tryGetFromAny(pseudo_filter_files, commandOpts, pseudo_observation_data, {"1# filter_files" }, "List of inputs to use for custom pseudoobservations")); - - tryGetFromYaml(eci_pseudoobs, pseudo_observation_data, {"@ eci_pseudoobs" }, "Pseudo observations are provided in eci frame rather than standard ECEF SP3 files"); - } - - { - auto satellite_data = stringsToYamlObject(inputs, {"2! satellite_data"}); - - conditionalPrefix("", sat_data_root, tryGetFromYaml(sat_data_root, satellite_data, {"0! satellite_data_root" }, "Root path to be added to all other satellite data files (unless they are absolute)")); - conditionalPrefix("", nav_files, tryGetFromAny(nav_files, commandOpts, satellite_data, {"1! nav_files" }, "List of ephemeris files to use")); - conditionalPrefix("", sp3_files, tryGetFromAny(sp3_files, commandOpts, satellite_data, {"1! sp3_files" }, "List of sp3 files to use")); - conditionalPrefix("", dcb_files, tryGetFromAny(dcb_files, commandOpts, satellite_data, {"1! dcb_files" }, "List of dcb files to use")); - conditionalPrefix("", bsx_files, tryGetFromAny(bsx_files, commandOpts, satellite_data, {"1! bsx_files" }, "List of biassinex files to use")); - conditionalPrefix("", clk_files, tryGetFromAny(clk_files, commandOpts, satellite_data, {"1! clk_files" }, "List of clock files to use")); - conditionalPrefix("", sid_files, tryGetFromAny(sid_files, commandOpts, satellite_data, {"2@ sid_files" }, "List of sat ID files to use - from https://cddis.nasa.gov/sp3c_satlist.html/")); - conditionalPrefix("", com_files, tryGetFromAny(com_files, commandOpts, satellite_data, {"2@ com_files" }, "List of com files to use - retroreflector offsets from centre-of-mass for spherical sats")); - conditionalPrefix("", crd_files, tryGetFromAny(crd_files, commandOpts, satellite_data, {"2@ crd_files" }, "List of crd files to use - SLR observation data")); - conditionalPrefix("", obx_files, tryGetFromAny(obx_files, commandOpts, satellite_data, {"1! obx_files" }, "List of orbex files to use")); - - // rtcm_inputs - { - auto rtcm_inputs = stringsToYamlObject(satellite_data, {"! rtcm_inputs"}, docs["rtcm_inputs"]); - - conditionalPrefix("", rtcm_inputs_root, tryGetFromYaml(rtcm_inputs_root, rtcm_inputs, {"0! rtcm_inputs_root" }, "Root path to be added to all other rtcm inputs (unless they are absolute)")); - - conditionalPrefix("", nav_rtcm_inputs, tryGetFromAny(nav_rtcm_inputs, commandOpts, rtcm_inputs, {"1! rtcm_inputs" }, "List of rtcm inputs to use for corrections")); - conditionalPrefix("", qzs_rtcm_inputs, tryGetFromAny(qzs_rtcm_inputs, commandOpts, rtcm_inputs, {"2@ qzl6_inputs" }, "List of qzss L6 inputs to use for corrections")); - - tryGetFromYaml(ssrInOpts.code_bias_valid_time, rtcm_inputs, {"@ code_bias_validity_time" }, "Valid time period of SSR code biases"); - tryGetFromYaml(ssrInOpts.phase_bias_valid_time, rtcm_inputs, {"@ phase_bias_validity_time" }, "Valid time period of SSR phase biases"); - tryGetFromYaml(ssrInOpts.one_freq_phase_bias, rtcm_inputs, {"@ one_freq_phase_bias" }, "Used stream have one SSR phase bias per frequency"); - tryGetFromYaml(ssrInOpts.global_vtec_valid_time,rtcm_inputs, {"@ global_vtec_valid_time" }, "Valid time period of global VTEC maps"); - tryGetFromYaml(ssrInOpts.local_stec_valid_time, rtcm_inputs, {"@ local_stec_valid_time" }, "Valid time period of local STEC corrections"); - tryGetFromYaml(ssrInOpts.local_trop_valid_time, rtcm_inputs, {"@ local_trop_valid_time" }, "Valid time period of local Troposphere corrections"); - tryGetFromYaml(validity_interval_factor, rtcm_inputs, {"@ validity_interval_factor" }); - tryGetEnumOpt(ssr_input_antenna_offset, rtcm_inputs, {"1! ssr_antenna_offset" }, "Ephemeris type that is provided in the listed SSR stream, i.e. satellite antenna-phase-centre (APC) or centre-of-mass (COM). This information is listed in the NTRIP Caster's sourcetable"); - } - - { - auto sbas_inputs = stringsToYamlObject(satellite_data, {"! sisnet_inputs"}, "Configuration for SiSNet stream input. SiSNet broadcast SBAS messages"); - - conditionalPrefix("", sisnet_inputs_root, tryGetFromYaml(sisnet_inputs_root, sbas_inputs, {"2@ sisnet_inputs_root" }, "Root path to be added to all other sisnet inputs (unless they are absolute)")); - - conditionalPrefix("", sisnet_inputs, tryGetFromAny(sisnet_inputs, commandOpts, sbas_inputs, {"2@ sisnet_inputs" }, "List of sisnet inputs to use for corrections")); - - tryGetFromYaml(sbsInOpts.prn, sbas_inputs, {"@ sbas_prn" }, "PRN for SBAS satelite"); - tryGetFromYaml(sbsInOpts.freq, sbas_inputs, {"@ sbas_carrier_frequency" }, "Carrier frequency of SBAS channel"); - } - } - } - -// processing_options - { - auto processing_options = stringsToYamlObject({ yaml, "" }, {processing_options_str}, "Various sections and parameters to specify how the observations are processed"); - -// process_modes - { - auto process_modes = stringsToYamlObject(processing_options, {"1! process_modes"}, "Aspects of the processing flow may be enabled and disabled according to desired type of solutions"); - - tryGetFromYaml(process_ionosphere, process_modes, {"@ ionosphere" }, "Compute Ionosphere models based on GNSS measurements"); - tryGetFromYaml(process_preprocessor, process_modes, {"! preprocessor" }, "Preprocessing and quality checks"); - tryGetFromYaml(process_spp, process_modes, {"! spp" }, "Perform SPP on receiver data"); - tryGetFromYaml(process_ppp, process_modes, {"! ppp" }, "Perform PPP network or end user mode"); - tryGetFromYaml(slrOpts.process_slr, process_modes, {"@ slr" }, "Process SLR observations"); - } - -// gnss_general - { - auto general = stringsToYamlObject(processing_options, {"0! gnss_general"}, "Options to specify the processing of gnss observations"); - - tryGetFromYaml (require_apriori_positions, general, {"@ require_apriori_positions" }, "Restrict processing to receivers that have apriori positions available"); - tryGetFromYaml (require_site_eccentricity, general, {"@ require_site_eccentricity" }, "Restrict processing to receivers that have site eccentricity information"); - tryGetFromYaml (require_sinex_data, general, {"@ require_sinex_data" }, "Restrict processing to receivers that have sinex data available"); - tryGetFromYaml (require_antenna_details, general, {"@ require_antenna_details" }, "Restrict processing to receivers that have antenna details"); - tryGetFromYaml (require_reflector_com, general, {"@ require_reflector_com" }, "Restrict processing to SLR observations that have center of mass to laser retroreflector array offsets"); - tryGetFromYaml (pivot_receiver, general, {"@ pivot_receiver" }, "Largely deprecated id of receiver to use for pivot constraints"); - tryGetFromYaml (pivot_satellite, general, {"@ pivot_satellite" }, "Largely deprecated id of satellite to use for pivot constraints"); - tryGetFromYaml (interpolate_rec_pco, general, {"@ interpolate_rec_pco" }, "Interpolate other known pco values to find pco for unknown frequencies"); - tryGetFromYaml (auto_fill_pco, general, {"@ auto_fill_pco" }, "Use similar PCOs when requested values are not found"); - tryGetFromYaml (pppOpts.equate_ionospheres, general, {"@ equate_ionospheres" }, "Use same STEC values for different receivers, useful for simulated rtk mode"); - tryGetFromYaml (pppOpts.equate_tropospheres, general, {"@ equate_tropospheres" }, "Use same troposphere values for different receivers, useful for simulated rtk mode"); - tryGetFromYaml (pppOpts.use_rtk_combo, general, {"@ use_rtk_combo" }, "Combine applicable observations to simulate an rtk solution"); - tryGetFromYaml (pppOpts.add_eop_component, general, {"@ add_eop_component" }, "Add eop adjustments as a component in residual chain (for adjusting frames to match ecef ephemeris)"); - tryGetFromYaml (delete_old_ephemerides, general, {"@ delete_old_ephemerides" }, "Remove old ephemerides that have accumulated over time from before far before the currently processing epoch"); - tryGetFromYaml (use_tgd_bias, general, {"@ use_tgd_bias" }, "Use TGD/BGD bias from ephemeris, DO NOT turn on unless using Klobuchar/NeQuick Ionospheres"); - tryGetFromYaml (common_sat_pco, general, {"@ common_sat_pco" }, "Use L1 satellite PCO values for all signals"); - tryGetFromYaml (common_rec_pco, general, {"@ common_rec_pco" }, "Use L1 receiver PCO values for all signals"); - tryGetFromYaml (leap_seconds, general, {"@ gpst_utc_leap_seconds" }, "Difference between gps time and utc in leap seconds"); - - tryGetFromYaml (process_meas[CODE], general, {"1@ code_measurements", "process" }, "Process code measurements"); - tryGetFromYaml (process_meas[PHAS], general, {"1@ phase_measurements", "process" }, "Process phase measurements"); - - tryGetFromYaml (fixed_phase_bias_var, general, {"@ fixed_phase_bias_var" }, "Variance of phase bias to be considered fixed/binded"); - tryGetFromYaml (adjust_rec_clocks_by_spp, general, {"@ adjust_rec_clocks_by_spp" }, "Adjust receiver clocks by spp values to minimise prefit residuals"); - tryGetFromYaml (adjust_clocks_for_jumps_only, general, {"@ adjust_clocks_for_jumps_only" }, "Round clock adjustments from SPP to half milliseconds"); - tryGetFromYaml (minimise_sat_clock_offsets, general, {"@ minimise_sat_clock_offsets" }, "Apply gauss-markov mu values to satellite clocks to minimise offsets with respect to broadcast values"); - tryGetFromYaml (minimise_sat_orbit_offsets, general, {"@ minimise_sat_orbit_offsets" }, "Apply gauss-markov mu values to satellite orbits to minimise offsets with respect to broadcast values"); - tryGetFromYaml (minimise_ionosphere_offsets, general, {"@ minimise_ionosphere_offsets" }, "Apply gauss-markov mu values to stec values to minimise offsets with respect to klobuchar values"); - - for (int i = E_Sys::GPS; i < E_Sys::SUPPORTED; i++) - { - E_Sys sys = E_Sys::_values()[i]; - - auto sys_options = stringsToYamlObject(general, {"1! sys_options", sys._to_string()}, (string)"Options for the " + sys._to_string() + " constellation"); - - tryGetFromYaml(process_sys [sys], sys_options, {"0! process" }, "Process this constellation"); - tryGetFromYaml(solve_amb_for [sys], sys_options, {"3! ambiguity_resolution" }, "Solve carrier phase ambiguities for this constellation"); - tryGetFromYaml(reject_eclipse [sys], sys_options, {"2@ reject_eclipse" }, "Exclude satellites that are in eclipsing region"); - tryGetFromYaml(receiver_amb_pivot [sys], sys_options, {"2@ receiver_amb_pivot" }, "Constrain: set of ambiguities, to eliminate receiver rank deficiencies"); - tryGetFromYaml(network_amb_pivot [sys], sys_options, {"2@ network_amb_pivot" }, "Constrain: set of ambiguities, to eliminate network rank deficiencies"); - tryGetFromYaml(use_for_iono_model [sys], sys_options, {"2@ use_for_iono_model" }, "Use this constellation as part of Ionospheric model"); - tryGetFromYaml(use_iono_corrections [sys], sys_options, {"2@ use_iono_corrections" }, "Use external ionosphere delay estimation for this constellation"); - tryGetEnumOpt( used_nav_types [sys], sys_options, {"2@ used_nav_type" }); - tryGetEnumVec (code_priorities [sys], sys_options, {"2! code_priorities" }, "List of observation codes to use in processing"); - } - } - -// epoch_control - { - auto epoch_control = stringsToYamlObject(processing_options, {"0! epoch_control"}, "Specifies the rate and duration of data processing"); - - int i = 0; - - string startStr; - string stopStr; - bool found = tryGetFromAny(epoch_interval, commandOpts, epoch_control, {"! epoch_interval" }, "Desired time step between each processing epoch"); - tryGetFromAny(epoch_tolerance, commandOpts, epoch_control, {"@ epoch_tolerance" }, "Tolerance of times to add to an epoch (usually half of the original data's sample rate)"); - tryGetFromAny(max_epochs, commandOpts, epoch_control, {"! max_epochs" }, "Maximum number of epochs to process"); - tryGetFromAny(startStr, commandOpts, epoch_control, {"! start_epoch" }, "(YYYY-MM-DD hh:mm:ss) The time of the first epoch to process (all observations before this will be skipped)"); - tryGetFromAny(stopStr, commandOpts, epoch_control, {"! end_epoch" }, "(YYYY-MM-DD hh:mm:ss) The time of the last epoch to process (all observations after this will be skipped)"); - - if (!startStr.empty()) start_epoch = boost::posix_time::time_from_string(startStr); - if (!stopStr .empty()) end_epoch = boost::posix_time::time_from_string(stopStr); - - if (found) - wait_next_epoch = epoch_interval + 0.05; - - tryGetFromYaml(sleep_milliseconds, epoch_control, {"# sleep_milliseconds" }, "Time to sleep before checking for new data - lower numbers are associated with high idle cpu usage"); - tryGetFromYaml(wait_next_epoch, epoch_control, {"@ wait_next_epoch" }, "Time to wait for next epochs data before skipping the epoch (will default to epoch_interval as an appropriate minimum value for realtime)"); - tryGetFromYaml(max_rec_latency, epoch_control, {"@ max_rec_latency" }, "Time to wait from the reception of the first data of an epoch before skipping receivers with data still unreceived"); - tryGetFromYaml(require_obs, epoch_control, {"@ require_obs" }, "Exit the program if no observation sources are available"); - tryGetFromYaml(assign_closest_epoch, epoch_control, {"@ assign_closest_epoch"}, "Assign observations to the closest epoch - don't skip observations that fall between epochs"); - tryGetFromAny(simulate_real_time, commandOpts, epoch_control, {"@ simulate_real_time" }, "For RTCM playback - delay processing to match original data rate"); - } - - -// model_error_handling - { - auto model_error_handling = stringsToYamlObject(processing_options, {"5! model_error_handling"}, "The kalman filter is capable of automatic statistical integrity modelling"); - - { - auto meas_deweighting = stringsToYamlObject(model_error_handling, {"0! meas_deweighting"}, "Measurements that are outside the expected confidence bounds may be deweighted so that outliers do not contaminate the filtered solution"); - - tryGetFromYaml(measErrors.enable, meas_deweighting, {"! enable" }, "Enable deweighting of all rejected measurement"); - tryGetFromYaml(measErrors.deweight_factor, meas_deweighting, {"! deweight_factor" }, "Factor to downweight the variance of measurements with statistically detected errors"); - } - - { - auto state_deweighting = stringsToYamlObject(model_error_handling, {"0! state_deweighting"}, "Any \"state\" errors cause deweighting of all measurements that reference the state"); - - tryGetFromYaml(stateErrors.enable, state_deweighting, {"! enable" }, "Enable deweighting of all referencing measurements"); - tryGetFromYaml(stateErrors.deweight_factor, state_deweighting, {"! deweight_factor" }, "Factor to downweight the variance of measurements with statistically detected errors"); - } - - { - auto error_accumulation = stringsToYamlObject(model_error_handling, {"0! error_accumulation"}, "Any receivers that are consistently getting many measurement rejections may be reinitialiased"); - - tryGetFromYaml(errorAccumulation.enable, error_accumulation, {"! enable" }, "Enable reinitialisation of receivers upon many rejections"); - tryGetFromYaml(errorAccumulation.receiver_error_count_threshold, error_accumulation, {"! receiver_error_count_threshold" }, "Number of errors for a receiver to be considered in error for a single epoch"); - tryGetFromYaml(errorAccumulation.receiver_error_epochs_threshold, error_accumulation, {"! receiver_error_epochs_threshold" }, "Number of consecutive epochs with receiver in error before it is removed and reinitialised"); - } - - - - { - auto orbit_errors = stringsToYamlObject(model_error_handling, {"2@ orbit_errors"}, "Orbital states that are not consistent with measurements may be reinitialised to allow for dynamic maneuvers"); - - tryGetFromYaml(orbErrors.enable, orbit_errors, {"@ enable" }, "Enable applying process noise impulses to orbits upon state errors"); - tryGetFromYaml(orbErrors.pos_proc_noise, orbit_errors, {"@ pos_process_noise" }, "Sigma to apply to orbital position states as reinitialisation"); - tryGetFromYaml(orbErrors.vel_proc_noise, orbit_errors, {"@ vel_process_noise" }, "Sigma to apply to orbital velocity states as reinitialisation"); - tryGetFromYaml(orbErrors.vel_proc_noise_trail, orbit_errors, {"@ vel_process_noise_trail" }, "Initial sigma for exponentially decaying noise to apply for subsequent epochs as soft reinitialisation"); - tryGetFromYaml(orbErrors.vel_proc_noise_trail_tau, orbit_errors, {"@ vel_process_noise_trail_tau" }, "Time constant for exponentially decauing noise"); - } - - { - auto ambiguities = stringsToYamlObject(model_error_handling, {"1! ambiguities"}, "Cycle slips in ambiguities are primary cause of incorrect gnss modelling and may be reinitialised"); - - tryGetFromYaml(ambErrors.outage_reset_limit, ambiguities, {"! outage_reset_limit" }, "Maximum number of seconds without phase measurements before the ambiguity associated with the measurement is reset."); - tryGetFromYaml(ambErrors.phase_reject_limit, ambiguities, {"! phase_reject_limit" }, "Maximum number of phase measurements to reject before the ambiguity associated with the measurement is reset."); - - tryGetFromYaml(ambErrors.resetOnSlip.LLI, ambiguities, {"@ reset_on", "@ lli" }, "Reset ambiguities if LLI test is detecting a slip"); - tryGetFromYaml(ambErrors.resetOnSlip.GF, ambiguities, {"@ reset_on", "@ gf" }, "Reset ambiguities if GF test is detecting a slip"); - tryGetFromYaml(ambErrors.resetOnSlip.MW, ambiguities, {"@ reset_on", "@ mw" }, "Reset ambiguities if MW test is detecting a slip"); - tryGetFromYaml(ambErrors.resetOnSlip.SCDIA, ambiguities, {"@ reset_on", "@ scdia" }, "Reset ambiguities if SCDIA test is detecting a slip"); - } - - { - auto ionospheric_components = stringsToYamlObject(model_error_handling, {"1! ionospheric_components"}); - - tryGetFromYaml(ionErrors.outage_reset_limit, ionospheric_components, {"! outage_reset_limit" }, "Maximum number of seconds without measurements before the ionosphere associated with the measurement is reset."); - } - - - { - auto exclusions = stringsToYamlObject(model_error_handling, {"1@ exclusions"}, "Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised"); - - tryGetFromYaml(exclude.bad_spp, exclusions, {"@ bad_spp" }, "Exclude measurements that were associated with failed SPP"); - tryGetFromYaml(exclude.config, exclusions, {"@ config" }, "Exclude measurements that are configured as exclusions"); - tryGetFromYaml(exclude.eclipse, exclusions, {"@ eclipse" }, "Exclude measurements that are in eclipse"); - tryGetFromYaml(exclude.elevation, exclusions, {"@ elevation" }, "Exclude measurements that fall below elevation mask"); - tryGetFromYaml(exclude.outlier, exclusions, {"@ outlier" }, "Exclude measurements that were rejected as SPP outliers"); - tryGetFromYaml(exclude.system, exclusions, {"@ system" }, "Exclude measurements that have been excluded by system configs"); - tryGetFromYaml(exclude.svh, exclusions, {"@ svh" }, "Exclude measurements that are not specified as healthy"); - tryGetFromYaml(exclude.LLI, exclusions, {"@ lli" }, "Exclude measurements that fail LLI slip test in preprocessor"); - tryGetFromYaml(exclude.GF, exclusions, {"@ gf" }, "Exclude measurements that fail GF slip test in preprocessor"); - tryGetFromYaml(exclude.MW, exclusions, {"@ mw" }, "Exclude measurements that fail MW slip test in preprocessor"); - tryGetFromYaml(exclude.SCDIA, exclusions, {"@ scdia" }, "Exclude measurements that fail SCDIA test in preprocessor"); - } - -// { -// auto clocks = stringsToYamlObject(model_error_handling, {"@ clocks"}, "Error responses specific to clock states"); -// -// tryGetFromYaml(reinit_on_clock_error, clocks, {"@ reinit_on_clock_error" }, "Any clock \"state\" errors cause removal and reinitialisation of the clocks and all associated ambiguities"); -// } - } - - auto getFilterOptions = [&]( - NodeStack& nodeStack, - FilterOptions& filterOpts) - { - auto outlier_screening = stringsToYamlObject(nodeStack, {"! outlier_screening"}, "Statistical checks allow for detection of outliers that exceed their confidence intervals."); - - if (std::get<1>(nodeStack).find("spp") == string::npos) - { - tryGetFromYaml(filterOpts.joseph_stabilisation, nodeStack, {"@ joseph_stabilisation" }); - 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.advanced_postfits, nodeStack, {"# advanced_postfits" }, "Use alternate calculation method to determine postfit residuals"); - } - - { - auto prefit = stringsToYamlObject(outlier_screening, {"! prefit"}); - - tryGetFromYaml(filterOpts.prefitOpts.max_iterations, prefit, {"! max_iterations" }, "Maximum number of measurements to exclude using prefit checks before attempting to filter"); - tryGetFromYaml(filterOpts.prefitOpts.sigma_check, prefit, {"@ sigma_check" }, "Enable sigma check"); - bool found = tryGetFromYaml(filterOpts.prefitOpts.state_sigma_threshold, prefit, {"@ sigma_threshold" }, "Sigma threshold"); - tryGetFromYaml(filterOpts.prefitOpts.meas_sigma_threshold, prefit, {"@ sigma_threshold" }, "Sigma threshold"); - tryGetFromYaml(filterOpts.prefitOpts.state_sigma_threshold, prefit, {"@ state_sigma_threshold" }, "Sigma threshold for states"); - tryGetFromYaml(filterOpts.prefitOpts.meas_sigma_threshold, prefit, {"@ meas_sigma_threshold" }, "Sigma threshold for measurements"); - tryGetFromYaml(filterOpts.prefitOpts.omega_test, prefit, {"@ omega_test" }, "Enable omega-test"); - - if (found) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: the yaml option 'prefit:sigma_threshold' is depreciated, better use 'prefit:state_sigma_threshold' and 'prefit:meas_sigma_threshold' instead"; - } - } - - { - auto postfit = stringsToYamlObject(outlier_screening, {"! postfit"}); - - tryGetFromYaml(filterOpts.postfitOpts.max_iterations, postfit, {"! max_iterations" }, "Maximum number of measurements to exclude using postfit checks while iterating filter"); - tryGetFromYaml(filterOpts.postfitOpts.sigma_check, postfit, {"@ sigma_check" }, "Enable sigma check"); - bool found = tryGetFromYaml(filterOpts.postfitOpts.state_sigma_threshold, postfit, {"@ sigma_threshold" }, "Sigma threshold"); - tryGetFromYaml(filterOpts.postfitOpts.meas_sigma_threshold, postfit, {"@ sigma_threshold" }, "Sigma threshold"); - tryGetFromYaml(filterOpts.postfitOpts.state_sigma_threshold, postfit, {"@ state_sigma_threshold" }, "Sigma threshold for states"); - tryGetFromYaml(filterOpts.postfitOpts.meas_sigma_threshold, postfit, {"@ meas_sigma_threshold" }, "Sigma threshold for measurements"); - tryGetFromYaml(filterOpts.chiSquareTest.sigma_threshold, postfit, {"@ sigma_threshold" }, "Sigma threshold"); - - if (found) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: the yaml option 'postfit:sigma_threshold' is depreciated, better use 'postfit:state_sigma_threshold' and 'postfit:meas_sigma_threshold' instead"; - } - } - - { - auto chi_sqaure = stringsToYamlObject(outlier_screening, {"! chi_square"}); - - tryGetFromYaml(filterOpts.chiSquareTest.enable, chi_sqaure, {"@ enable" }, "Enable Chi-square test"); - tryGetEnumOpt( filterOpts.chiSquareTest.mode, chi_sqaure, {"@ mode" }, "Chi-square test mode"); - tryGetFromYaml(filterOpts.chiSquareTest.sigma_threshold, chi_sqaure, {"@ sigma_threshold" }, "Chi-square test threshold in terms of 'times of sigma'"); - } - - if (std::get<1>(nodeStack).find("spp") == string::npos) - { - auto rts = stringsToYamlObject(nodeStack, {"@ rts"}, "RTS allows reverse smoothing of estimates such that early estimates can make use of later data."); - - tryGetFromYaml(process_rts, rts, {"0! enable" }, "Perform backward smoothing of states to improve precision of earlier states"); - tryGetFromYaml(filterOpts.rts_lag, rts, {"@ 1 lag" }, "Number of epochs to use in RTS smoothing. Negative numbers indicate full reverse smoothing."); - tryGetFromYaml(filterOpts.rts_interval, rts, {"# interval" }, "Number of seconds to use between fixed lag in RTS smoothing."); - conditionalPrefix("", pppOpts.rts_directory, tryGetFromYaml(filterOpts.rts_directory, rts, {"@ directory" }, "Directory for rts intermediate files")); - conditionalPrefix("", pppOpts.rts_filename, tryGetFromYaml(filterOpts.rts_filename, rts, {"@ filename" }, "Base filename for rts intermediate files")); - tryGetFromYaml(filterOpts.queue_rts_outputs, rts, {"@ queue_outputs" }, "Queue rts outputs so that processing is not limited by IO bandwidth"); - tryGetFromYaml(filterOpts.rts_smoothed_suffix, rts, {"@ suffix" }, "Suffix to be applied to smoothed versions of files"); - tryGetEnumOpt( filterOpts.rts_inverter, rts, {"@ inverter" }, "Inverter to be used within the rts processor, which may provide different performance outcomes in terms of processing time and accuracy and stability."); - tryGetFromYaml(filterOpts.output_intermediate_rts, rts, {"@ output_intermediates" }, "Output best available smoothed states when performing fixed-lag rts (slow, use only when needed)"); - } - }; - -// minimum_constraints - { - auto minimum_constraints = stringsToYamlObject(processing_options, {"5! minimum_constraints"}, "Receiver coodinates may be aligned to reference frames with minimal external constraints"); - - tryGetFromYaml(process_minimum_constraints, minimum_constraints, {"0! enable" }, "Transform states by minimal constraints to selected receiver coordinates"); - - tryGetKalmanFromYaml(minconOpts.delay, minimum_constraints, "1! delay", "Estimation and application of clock delay adjustment"); - tryGetKalmanFromYaml(minconOpts.scale, minimum_constraints, "1! scale", "Estimation and application of scaling factor"); - tryGetKalmanFromYaml(minconOpts.rotation, minimum_constraints, "1! rotation", "Estimation and application of angular offsets"); - tryGetKalmanFromYaml(minconOpts.translation, minimum_constraints, "1! translation", "Estimation and application of CoG offsets"); - - tryGetFromYaml(minconOpts.once_per_epoch, minimum_constraints, {"2@ once_per_epoch" }, "Perform minimum constraints on a temporary filter and output results once per epoch"); - tryGetFromYaml(minconOpts.full_vcv, minimum_constraints, {"2@ full_vcv" }, "! experimental ! Use full VCV for measurement noise in minimum constraints filter"); - tryGetFromYaml(minconOpts.constrain_orbits, minimum_constraints, {"2@ constrain_orbits" }, "Enforce rigid transformations of orbital states"); - tryGetEnumOpt( minconOpts.application_mode, minimum_constraints, {"2@ application_mode" }, "Method of transforming positions "); - tryGetFromYaml(minconOpts.transform_unweighted, minimum_constraints, {"2@ transform_unweighted" }, "Add design entries for transformation of positions without weighting"); - - getFilterOptions(minimum_constraints, minconOpts); - } - -// ppp_filter - { - auto ppp_filter = stringsToYamlObject(processing_options, {"4! ppp_filter"}, "Configurations for the kalman filter and its sub processes"); - - tryGetFromYaml (pppOpts.simulate_filter_only, ppp_filter, {"@ simulate_filter_only" }, "Residuals will be calculated, but no adjustments to state or covariances will be applied"); - tryGetFromYaml (pppOpts.assume_linearity, ppp_filter, {"@ assume_linearity" }, "Residuals will be adjusted during measurement combination rather than performing 2 seperate state transitions"); - - tryGetFromYaml (pppOpts.chunk_size, ppp_filter, {"@ chunking", "@ size" }); - tryGetFromYaml (pppOpts.receiver_chunking, ppp_filter, {"@ chunking", "@ by_receiver" }, "Split large filter and measurement matrices blockwise by receiver ID to improve processing speed"); - tryGetFromYaml (pppOpts.satellite_chunking, ppp_filter, {"@ chunking", "@ by_satellite" }, "Split large filter and measurement matrices blockwise by satellite ID to improve processing speed"); - - tryGetFromYaml (pppOpts.nuke_enable, ppp_filter, {"@ periodic_reset", "@ enable" }, "Enable periodic reset of filter states"); - tryGetFromYaml (pppOpts.nuke_interval, ppp_filter, {"@ periodic_reset", "@ interval" }, "Interval between reset of filter states"); - tryGetEnumVec (pppOpts.nuke_states, ppp_filter, {"@ periodic_reset", "@ states" }, "States to remove for periodic reset"); - -// ionospheric_component - { - auto ionospheric_components = stringsToYamlObject(ppp_filter, {"! ionospheric_components"}, "Slant ionospheric components"); - - tryGetEnumOpt( pppOpts.ionoOpts.corr_mode, ionospheric_components, {"@ corr_mode" }); - tryGetFromYaml(pppOpts.ionoOpts.common_ionosphere, ionospheric_components, {"! common_ionosphere" }, "Use the same ionosphere state for code and phase observations"); - tryGetFromYaml(pppOpts.ionoOpts.use_if_combo, ionospheric_components, {"! use_if_combo" }, "Combine 'uncombined' measurements to simulate an ionosphere-free solution"); - tryGetFromYaml(pppOpts.ionoOpts.use_gf_combo, ionospheric_components, {"! use_gf_combo" }, "Combine 'uncombined' measurements to simulate a geometry-free solution"); - } - - getFilterOptions(ppp_filter, pppOpts); - } - -// ion_filter - { - auto ion_filter = stringsToYamlObject(processing_options, {"5@ ion_filter"}, "Configurations for the ionospheric model kalman filter and its sub processes"); - - tryGetEnumOpt( ionModelOpts.model, ion_filter, {"@ model" }); - tryGetFromYaml(ionModelOpts.function_order, ion_filter, {"@ function_order" }, "Maximum order of Spherical harmonics for Ionospheric mapping"); - tryGetFromYaml(ionModelOpts.function_degree, ion_filter, {"@ function_degree" }, "Maximum degree of Spherical harmonics for Ionospheric mapping"); - tryGetFromYaml(ionModelOpts.estimate_sat_dcb, ion_filter, {"@ estimate_sat_dcb" }, "Estimate satellite dcb alongside Ionosphere models, should be false for local STEC"); - tryGetFromYaml(ionModelOpts.use_rotation_mtx, ion_filter, {"@ use_rotation_mtx" }, "Use 3D rotation matrix for spherical harmonics to maintain orientation toward the sun"); - tryGetFromYaml(ionModelOpts.basis_sigma_limit, ion_filter, {"@ model_sigma_limit" }, "Ionosphere states are removed when their sigma exceeds this value"); - - bool found = tryGetFromYaml(ionModelOpts.layer_heights, ion_filter, {"@ layer_heights" }, "List of heights of ionosphere layers to estimate"); - if (found) - for (auto& a : ionModelOpts.layer_heights) - { - a *= 1000; //km to m - } - - getFilterOptions(ion_filter, ionModelOpts); - } - - -// spp - { - auto spp = stringsToYamlObject(processing_options, {"1! spp"}, "Configurations for the kalman filter and its sub processes"); - - tryGetFromYaml(sppOpts.max_lsq_iterations, spp, {"! max_lsq_iterations" }, "Maximum number of iterations of least squares allowed for convergence"); - tryGetFromYaml(sppOpts.sigma_scaling, spp, {"! sigma_scaling" }, "Scale applied to measurement noise for spp"); - tryGetFromYaml(sppOpts.always_reinitialise, spp, {"@ always_reinitialise" }, "Reset SPP state to zero to avoid potential for lock-in of bad states"); - tryGetEnumOpt( sppOpts.iono_mode, spp, {"@ iono_mode" }); - - auto outlier_screening = stringsToYamlObject(spp, {"! outlier_screening"}, "Statistical checks allow for detection of outliers that exceed their confidence intervals."); - - tryGetFromYaml(sppOpts.max_gdop, outlier_screening, {"@ max_gdop" }, "Maximum dilution of precision before error is flagged"); - tryGetFromYaml(sppOpts.raim, outlier_screening, {"@ raim" }, "Enable Receiver Autonomous Integrity Monitoring. When SPP fails further SPP solutions are calculated with subsets of observations with the aim of eliminating a problem satellite"); - - getFilterOptions(spp, sppOpts); - } - -// preprocessor - { - auto preprocessor = stringsToYamlObject(processing_options, {"1@ preprocessor"}, "Configurations for the kalman filter and its sub processes"); - - tryGetFromYaml(preprocOpts.preprocess_all_data, preprocessor, {"@ preprocess_all_data" }); - { - auto cycle_slips = stringsToYamlObject(preprocessor, {"2@ cycle_slips"}, "Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised"); - - tryGetFromYaml(preprocOpts.slip_threshold, cycle_slips, {"@ slip_threshold" }, "Value used to determine when a slip has occurred"); - tryGetFromYaml(preprocOpts.mw_proc_noise, cycle_slips, {"@ mw_process_noise" }, "Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips"); - } - } - -// tryGetFromYaml(orbitOpts.degree_max, orbit_propagation, {"@ degree_max" }, "Maximum degree of spherical harmonics model"); -// ambiguity_resolution - { - auto ambiguity_resolution = stringsToYamlObject(processing_options, {"5@ ambiguity_resolution"}); - - tryGetFromYaml(ambrOpts.elevation_mask_deg, ambiguity_resolution, {"@ elevation_mask" }, "Minimum satellite elevation to perform ambiguity resolution"); - tryGetFromYaml(ambrOpts.lambda_set, ambiguity_resolution, {"@ lambda_set_size" }, "Maximum numer of candidate sets to be used in lambda_alt2 and lambda_bie modes"); - tryGetFromYaml(ambrOpts.AR_max_itr, ambiguity_resolution, {"@ max_rounding_iterations" }, "Maximum number of rounding iterations performed in iter_rnd and bootst modes"); - - tryGetEnumOpt( ambrOpts.mode, ambiguity_resolution, {"@ mode" }); - tryGetFromYaml(ambrOpts.succsThres, ambiguity_resolution, {"@ success_rate_threshold" }, "Thresold for integer validation, success rate test."); - tryGetFromYaml(ambrOpts.ratioThres, ambiguity_resolution, {"@ solution_ratio_threshold" }, "Thresold for integer validation, distance ratio test."); - - tryGetFromYaml(ambrOpts.once_per_epoch, ambiguity_resolution, {"@ once_per_epoch" }, "Perform ambiguity resolution on a temporary filter and output results once per epoch"); - tryGetFromYaml(ambrOpts.fix_and_hold, ambiguity_resolution, {"@ fix_and_hold" }, "Perform ambiguity resolution and commit results to the main processing filter"); - } - - -// predictions - { - auto predictions = stringsToYamlObject(processing_options, {"5@ predictions"}); - - tryGetScaledFromYaml(mongoOpts.prediction_offset, predictions, {"4@ offset" }, {"@ interval_units" }, E_Period::_from_string_nocase); - tryGetScaledFromYaml(mongoOpts.prediction_interval, predictions, {"4@ interval" }, {"@ interval_units" }, E_Period::_from_string_nocase); - tryGetScaledFromYaml(mongoOpts.forward_prediction_duration, predictions, {"4@ forward_duration" }, {"@ duration_units" }, E_Period::_from_string_nocase); - tryGetScaledFromYaml(mongoOpts.reverse_prediction_duration, predictions, {"4@ reverse_duration" }, {"@ duration_units" }, E_Period::_from_string_nocase); - } - - -// orbit_propagation - { - auto orbit_propagation = stringsToYamlObject(processing_options, {"5@ orbit_propagation"}); - - tryGetFromYaml(propagationOptions.integrator_time_step , orbit_propagation, {"@ integrator_time_step" }, "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"); - tryGetFromYaml(propagationOptions.egm_degree , orbit_propagation, {"@ egm_degree" }, "Degree of spherical harmonics gravity model"); - tryGetFromYaml(propagationOptions.indirect_J2 , orbit_propagation, {"@ indirect_J2" }, "J2 acceleration perturbation due to the Sun and Moon"); - tryGetFromYaml(propagationOptions.egm_field , orbit_propagation, {"@ egm_field" }, "Acceleration due to the high degree model of the Earth gravity model (exclude degree 0, made by central_force)"); - tryGetFromYaml(propagationOptions.solid_earth_tide , orbit_propagation, {"@ solid_earth_tide" }, "Model accelerations due to solid earth tides"); - tryGetFromYaml(propagationOptions.ocean_tide , orbit_propagation, {"@ ocean_tide" }, "Model accelerations due to ocean tides model"); - tryGetFromYaml(propagationOptions.atm_tide , orbit_propagation, {"@ atm_tide" }, "Model accelerations due to atmospheric tides model"); - tryGetFromYaml(propagationOptions.pole_tide_ocean , orbit_propagation, {"@ pole_tide_ocean" }, "Model accelerations due to ocean pole tide (degree 2 only)"); - tryGetFromYaml(propagationOptions.pole_tide_solid , orbit_propagation, {"@ pole_tide_solid" }, "Model accelerations due to solid pole tide (degree 2 only)"); - tryGetFromYaml(propagationOptions.aod , orbit_propagation, {"@ aod" }, "Model Atmospheric and Oceanic non tidal accelerations"); - tryGetFromYaml(propagationOptions.central_force , orbit_propagation, {"@ central_force" }, "Acceleration due to the central force"); - tryGetFromYaml(propagationOptions.general_relativity , orbit_propagation, {"@ general_relativity" }, "Model acceleration due general relativisty"); - } - } - -// estimation_parameters - { - auto estimation_parameters = stringsToYamlObject({yaml, ""}, {estimation_parameters_str}); - auto global_models = stringsToYamlObject(estimation_parameters, {"@ global_models"}); - - tryGetKalmanFromYaml(pppOpts.eop, global_models, "@ eop" ); - tryGetKalmanFromYaml(pppOpts.eop_rates, global_models, "@ eop_rates" ); - tryGetKalmanFromYaml(ionModelOpts.ion, global_models, "@ ion" ); - } - -// mongo - { - auto mongo = stringsToYamlObject({yaml, ""}, {"5! mongo"}, "Mongo is a database used to store results and intermediate values for later analysis and inter-process communication"); - - tryGetEnumOpt (mongoOpts.enable, mongo, {"0! enable" }, "Enable and connect to mongo database"); - tryGetEnumOpt (mongoOpts.output_measurements, mongo, {"1! output_measurements" }, "Output measurements and their residuals"); - tryGetEnumOpt (mongoOpts.output_components, mongo, {"1! output_components" }, "Output components of measurements"); - tryGetEnumOpt (mongoOpts.output_cumulative, mongo, {"1! output_cumulative" }, "Output cumulative residuals of components of measurements"); - tryGetEnumOpt (mongoOpts.output_states, mongo, {"1! output_states" }, "Output states"); - tryGetEnumOpt (mongoOpts.output_state_covars, mongo, {"1! output_state_covars" }, "Output covariance values of related states"); - tryGetEnumOpt (mongoOpts.output_config, mongo, {"2@ output_config" }, "Output config"); - tryGetEnumOpt (mongoOpts.output_trace, mongo, {"2@ output_trace" }, "Output trace"); - tryGetEnumOpt (mongoOpts.output_test_stats, mongo, {"2@ output_test_stats" }, "Output test statistics"); - tryGetEnumOpt (mongoOpts.output_logs, mongo, {"2@ output_logs" }, "Output console trace and warnings to mongo with timestamps and other metadata"); - tryGetEnumOpt (mongoOpts.output_ssr_precursors, mongo, {"2@ output_ssr_precursors" }, "Output orbits, clocks, and bias estimates to allow communication to ssr generating processes"); - tryGetEnumOpt (mongoOpts.delete_history, mongo, {"1! delete_history" }, "Drop the collection in the database at the beginning of the run to only show fresh data"); - tryGetEnumOpt (mongoOpts.cull_history, mongo, {"1@ cull_history" }, "Erase old database objects to limit the size and speed degredation over long runs"); - tryGetEnumOpt (mongoOpts.use_predictions, mongo, {"2@ use_predictions" }); - tryGetEnumOpt (mongoOpts.output_predictions, mongo, {"2@ output_predictions" }); - tryGetFromYaml(mongoOpts.queue_outputs, mongo, {"2@ queue_outputs" }, "Output data in a separate thread - may reduce latency"); - tryGetFromYaml(mongoOpts.min_cull_age, mongo, {"2@ min_cull_age" }, "Age of which to cull history"); - - tryGetEnumVec (mongoOpts.used_predictions, mongo, {"@ used_predictions" }, "Filter states to retrieve from mongo"); - tryGetEnumVec (mongoOpts.sent_predictions, mongo, {"@ sent_predictions" }, "Filter states to predict and send to mongo"); - - tryGetFromYaml(mongoOpts[E_Mongo::PRIMARY].suffix, mongo, {"3@ primary_suffix" }, "Suffix to append to database elements to make distinctions between runs for comparison"); - tryGetFromYaml(mongoOpts[E_Mongo::PRIMARY].database, mongo, {"3@ primary_database" }); - tryGetFromYaml(mongoOpts[E_Mongo::PRIMARY].uri, mongo, {"3@ primary_uri" }, "Location and port of the mongo database to connect to"); - - tryGetFromYaml(mongoOpts[E_Mongo::SECONDARY].suffix, mongo, {"3@ secondary_suffix" }, "Suffix to append to database elements to make distinctions between runs for comparison"); - tryGetFromYaml(mongoOpts[E_Mongo::SECONDARY].database, mongo, {"3@ secondary_database" }); - tryGetFromYaml(mongoOpts[E_Mongo::SECONDARY].uri, mongo, {"3@ secondary_uri" }, "Location and port of the mongo database to connect to"); - } - -// debug - { - auto debug = stringsToYamlObject({yaml, ""}, {"9@ debug"}, "Debug options are designed for developers and should probably not be used by normal users"); - - tryGetFromAny(fatal_level, commandOpts, debug, {"# fatal_message_level" }, "Threshold level for exiting the program early (0-2)"); - tryGetFromYaml(check_plumbing, debug, {"# check_plumbing" }, "Debugging option to show sizes of objects in memory to detect leaks"); - tryGetFromYaml(explain_measurements, debug, {"# explain_measurements" }, "Debugging option to show verbose measurement coefficients"); - tryGetFromYaml(retain_rts_files, debug, {"# retain_rts_files" }, "Debugging option to keep rts files for post processing"); - tryGetFromYaml(rts_only, debug, {"# rts_only" }, "Debugging option to only re-run rts from previous run"); - tryGetFromYaml(mincon_only, debug, {"# mincon_only" }, "Debugging option to re-run minimum constraints code"); - tryGetFromYaml(output_mincon, debug, {"# output_mincon" }, "Debugging option to only save pre-minimum constraints filter state"); - tryGetFromYaml(mincon_filename, debug, {"# mincon_filename" }, "Filename of pre-mincon filter state for backup/loading"); - tryGetFromYaml(check_broadcast_differences, debug, {"@ check_broadcast_differences" }); - tryGetFromAny (compare_orbits, commandOpts, debug, {"@ compare_orbits" }); - tryGetFromAny (compare_clocks, commandOpts, debug, {"@ compare_clocks" }); - tryGetFromAny (compare_attitudes, commandOpts, debug, {"@ compare_attitudes" }); - } - } - -// tryGetFromYaml(split_sys, outputs, { "split_sys" }); - - -// Try to change all filenames to replace etc with other values. - { - replaceTags(config_description); - replaceTags(gnss_obs_root); - replaceTags(pseudo_obs_root); - replaceTags(sat_data_root); - replaceTags(rtcm_inputs_root); - - replaceTags(vmf_files); globber(vmf_files); - replaceTags(atx_files); globber(atx_files); - replaceTags(snx_files); globber(snx_files); - replaceTags(erp_files); globber(erp_files); - replaceTags(ion_files); globber(ion_files); - replaceTags(nav_files); globber(nav_files); - replaceTags(sp3_files); globber(sp3_files); - replaceTags(dcb_files); globber(dcb_files); - replaceTags(bsx_files); globber(bsx_files); - replaceTags(clk_files); globber(clk_files); - replaceTags(obx_files); globber(obx_files); - replaceTags(sid_files); globber(sid_files); - replaceTags(cmc_files); globber(cmc_files); - replaceTags(com_files); globber(com_files); - replaceTags(crd_files); globber(crd_files); - replaceTags(egm_files); globber(egm_files); - replaceTags(igrf_files); globber(igrf_files); - replaceTags(hfeop_files); globber(hfeop_files); - replaceTags(gpt2grid_files); globber(gpt2grid_files); - replaceTags(orography_files); globber(orography_files); - replaceTags(atm_reg_definitions); globber(atm_reg_definitions); - replaceTags(pseudo_filter_files); globber(pseudo_filter_files); - replaceTags(planetary_ephemeris_files); globber(planetary_ephemeris_files); - replaceTags(ocean_tide_potential_files); globber(ocean_tide_potential_files); - replaceTags(atmos_tide_potential_files); globber(atmos_tide_potential_files); - replaceTags(ocean_tide_loading_blq_files); globber(ocean_tide_loading_blq_files); - replaceTags(atmos_tide_loading_blq_files); globber(atmos_tide_loading_blq_files); - replaceTags(ocean_pole_tide_loading_files); globber(ocean_pole_tide_loading_files); - replaceTags(atmos_oceean_dealiasing_files); globber(atmos_oceean_dealiasing_files); - replaceTags(ocean_pole_tide_potential_files); globber(ocean_pole_tide_potential_files); - - replaceTags(rnx_inputs); globber(rnx_inputs); - replaceTags(ubx_inputs); globber(ubx_inputs); - replaceTags(custom_inputs); globber(custom_inputs); - replaceTags(obs_rtcm_inputs); globber(obs_rtcm_inputs); - replaceTags(nav_rtcm_inputs); globber(nav_rtcm_inputs); - replaceTags(qzs_rtcm_inputs); globber(qzs_rtcm_inputs); - replaceTags(pseudo_sp3_inputs); globber(pseudo_sp3_inputs); - replaceTags(pseudo_snx_inputs); globber(pseudo_snx_inputs); - - - replaceTags(sp3_directory); replaceTags(sp3_filename); - replaceTags(erp_directory); replaceTags(erp_filename); - replaceTags(gpx_directory); replaceTags(gpx_filename); - replaceTags(pos_directory); replaceTags(pos_filename); - replaceTags(log_directory); replaceTags(log_filename); - replaceTags(cost_directory); replaceTags(cost_filename); - replaceTags(sinex_directory); replaceTags(sinex_filename); - replaceTags(ionex_directory); replaceTags(ionex_filename); - replaceTags(orbex_directory); replaceTags(orbex_filename); - replaceTags(clocks_directory); replaceTags(clocks_filename); - replaceTags(slr_obs_directory); replaceTags(slr_obs_filename); - replaceTags(ionstec_directory); replaceTags(ionstec_filename); - replaceTags(raw_ubx_directory); replaceTags(raw_ubx_filename); - replaceTags(rtcm_nav_directory); replaceTags(rtcm_nav_filename); - replaceTags(rtcm_obs_directory); replaceTags(rtcm_obs_filename); - replaceTags(orbit_ics_directory); replaceTags(orbit_ics_filename); - replaceTags(ntrip_log_directory); replaceTags(ntrip_log_filename); - replaceTags(rinex_obs_directory); replaceTags(rinex_obs_filename); - replaceTags(rinex_nav_directory); replaceTags(rinex_nav_filename); - replaceTags(raw_custom_directory); replaceTags(raw_custom_filename); - replaceTags(bias_sinex_directory); replaceTags(bias_sinex_filename); - replaceTags(trop_sinex_directory); replaceTags(trop_sinex_filename); - replaceTags(pppOpts.rts_directory); replaceTags(pppOpts.rts_filename); - replaceTags(sp3_directory); replaceTags(predicted_sp3_filename); - replaceTags(ems_directory); replaceTags(ems_filename); - replaceTags(trace_directory); replaceTags(receiver_trace_filename); - replaceTags(trace_directory); replaceTags(network_trace_filename); - replaceTags(trace_directory); replaceTags(satellite_trace_filename); - replaceTags(trace_directory); replaceTags(ionosphere_trace_filename); - replaceTags(decoded_rtcm_json_directory); replaceTags(decoded_rtcm_json_filename); - replaceTags(encoded_rtcm_json_directory); replaceTags(encoded_rtcm_json_filename); - replaceTags(network_statistics_json_directory); replaceTags(network_statistics_json_filename); - - replaceTags(mongoOpts[E_Mongo::PRIMARY] .uri); - replaceTags(mongoOpts[E_Mongo::PRIMARY] .suffix); - replaceTags(mongoOpts[E_Mongo::PRIMARY] .database); - replaceTags(mongoOpts[E_Mongo::SECONDARY] .uri); - replaceTags(mongoOpts[E_Mongo::SECONDARY] .suffix); - replaceTags(mongoOpts[E_Mongo::SECONDARY] .database); - } + const vector& filenames, ///< Path to yaml based config file + boost::program_options::variables_map& + newCommandOpts ///< Variable map object of command line options +) +{ + DOCS_REFERENCE(Config__); + + configFilenames = filenames; + + bool modified = false; + + for (auto& filenameList : {filenames, includedFilenames}) + for (auto& filename : filenameList) + if (filename != "") + { + std::filesystem::path filePath(filename); + auto currentConfigModifyTime = std::filesystem::last_write_time(filePath); + + if (currentConfigModifyTime != configModifyTimeMap[filename]) + { + modified = true; + } + } + else + { + modified = true; + } + + if (modified == false) + { + return false; + } + + commandOpts = newCommandOpts; + + // clear old saved parameters + foundOptions.clear(); + satOptsMap.clear(); + recOptsMap.clear(); + defaultOutputOptions(); + + for (E_Sys sys : magic_enum::enum_values()) + { + code_priorities[sys] = default_code_priorities; + eph_time_delay[sys] = default_eph_time_delay[sys]; + } + + vector yamlList; + + yamls.resize(filenames.size()); + + for (int i = 0; i < filenames.size(); i++) + { + auto& filename = filenames[i]; + auto& yaml = yamls[i]; + + BOOST_LOG_TRIVIAL(info) << "Checking configuration file " << filename; + + try + { + yaml.reset(); + yaml = YAML::LoadFile(filename); + } + catch (const YAML::BadFile& e) + { + if (commandOpts.count("yaml-defaults")) + { + // we expect to break, continue parsing + } + else + { + BOOST_LOG_TRIVIAL(error) << "Failed to parse configuration file " << filename; + BOOST_LOG_TRIVIAL(error) << e.msg << "\n"; + return false; + } + } + catch (const YAML::ParserException& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to parse configuration. Check for errors as " + "described near the below:"; + BOOST_LOG_TRIVIAL(error) << e.what() << "\n" + << "\n"; + return false; + } + + vector includes; + + auto inputs = stringsToYamlObject({yaml, ""}, {"0! inputs"}, docs["inputs"]); + + tryGetFromYaml( + includes, + inputs, + {"1! include_yamls"}, + "List of yaml files to include before this one" + ); + + for (auto& include : includes) + { + yamlList.push_back(include); + } + } + + for (auto& filename : filenames) + { + yamlList.push_back(filename); + } + + includedFilenames = yamlList; + + yamls.resize(yamlList.size()); + + defaultConfigs(); + + for (int i = 0; i < yamlList.size(); i++) + { + auto& filename = yamlList[i]; + auto& yaml = yamls[i]; + + BOOST_LOG_TRIVIAL(info) << "Loading configuration from file " << filename; + + try + { + std::filesystem::path filePath(filename); + auto currentConfigModifyTime = std::filesystem::last_write_time(filePath); + + configModifyTimeMap[filename] = currentConfigModifyTime; + + yaml.reset(); + yaml = YAML::LoadFile(filename); + yaml["yaml_filename"] = filename; + yaml["yaml_number"] = i; + } + catch (const YAML::BadFile& e) + { + if (commandOpts.count("yaml-defaults")) + { + // we expect to break, continue parsing + } + else + { + BOOST_LOG_TRIVIAL(error) << "Failed to parse configuration file " << filename; + BOOST_LOG_TRIVIAL(error) << e.msg << "\n"; + return false; + } + } + catch (const std::filesystem::filesystem_error& e) + { + if (commandOpts.count("yaml-defaults")) + { + // we expect to break, continue parsing + } + else + { + BOOST_LOG_TRIVIAL(error) << "Failed to parse configuration file " << filename; + return false; + } + } + catch (const YAML::ParserException& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to parse configuration. Check for errors as " + "described near the below:"; + BOOST_LOG_TRIVIAL(error) << e.what() << "\n" + << "\n"; + return false; + } + + recurseLowerCase(yaml); + + // outputs + { + auto outputs = stringsToYamlObject({yaml, ""}, {"1! outputs"}, docs["outputs"]); + + tryGetFromYaml( + outputs_root, + outputs, + {"0! outputs_root"}, + "Directory that outputs will be placed in" + ); + + { + auto metadata = stringsToYamlObject( + outputs, + {"2! metadata"}, + "Options for setting metadata for inputs and outputs" + ); + + tryGetFromAny( + config_description, + commandOpts, + metadata, + {"1! config_description"}, + "ID for this config, used to replace tags in other options" + ); + tryGetFromAny( + stream_user, + commandOpts, + metadata, + {"1! user"}, + "Username for connecting to NTRIP casters" + ); + tryGetFromAny( + stream_pass, + commandOpts, + metadata, + {"1! pass"}, + "Password for connecting to NTRIP casters" + ); + tryGetFromYaml( + analysis_agency, + metadata, + {"@ analysis_agency"}, + "Agency for output files headers" + ); + tryGetFromYaml( + config_details, + metadata, + {"@ config_details"}, + "Comments and details specific to the config" + ); + tryGetFromYaml( + analysis_centre, + metadata, + {"@ analysis_centre"}, + "Analysis centre for output files headers" + ); + tryGetFromYaml( + ac_contact, + metadata, + {"@ ac_contact"}, + "Contact person for output files headers" + ); + tryGetFromYaml( + analysis_software, + metadata, + {"@ analysis_software"}, + "Program for output files headers" + ); + tryGetFromYaml( + analysis_software_version, + metadata, + {"@ analysis_software_version"}, + "Version for output files headers" + ); + tryGetFromYaml( + rinex_comment, + metadata, + {"@ rinex_comment"}, + "Comment for output files headers" + ); + tryGetFromYaml( + reference_system, + metadata, + {"@ reference_system"}, + "Terrestrial Reference System Code" + ); + tryGetFromYaml( + time_system, + metadata, + {"@ time_system"}, + "Time system - e.g. \"G\", \"UTC\"" + ); + tryGetFromYaml( + ocean_tide_loading_model, + metadata, + {"@ ocean_tide_loading_model"}, + "Ocean tide loading model applied" + ); + tryGetFromYaml( + atmospheric_tide_loading_model, + metadata, + {"@ atmospheric_tide_loading_model"}, + "Atmospheric tide loading model applied" + ); + tryGetFromYaml( + geoid_model, + metadata, + {"@ geoid_model"}, + "Geoid model name for undulation values" + ); + tryGetFromYaml( + gradient_mapping_function, + metadata, + {"@ gradient_mapping_function"}, + "Name of mapping function used for mapping horizontal troposphere gradients" + ); + } + + { + tryGetFromYaml( + colourise_terminal, + outputs, + {"1! colourise_terminal"}, + "Use ascii command codes to highlight warnings and errors" + ); + tryGetFromYaml( + timestamp_console_logs, + outputs, + {"1! timestamp_console_logs"}, + "Add timestamps (GPST) to console logs" + ); + tryGetFromYaml(warn_once, outputs, {"1! warn_once"}, "Print warnings once only"); + } + + { + auto trace = stringsToYamlObject(outputs, {"2! trace"}, docs["trace"]); + + tryGetFromYaml( + output_receiver_trace, + trace, + {"0! output_receivers"}, + "Output trace files for individual receivers processing" + ); + tryGetFromYaml( + output_network_trace, + trace, + {"0! output_network"}, + "Output trace files for complete network of receivers, inclucing kalman filter " + "results and " + "statistics" + ); + tryGetFromYaml( + output_ionosphere_trace, + trace, + {"0@ output_ionosphere"}, + "Output trace files for ionosphere processing, inclucing kalman filter results " + "and statistics" + ); + tryGetFromYaml( + output_satellite_trace, + trace, + {"0@ output_satellites"}, + "Output trace files for individual satellites processing" + ); + tryGetFromYaml( + output_observations, + trace, + {"0@ output_observations"}, + "Output detailed observation data including CN0, elevation, azimuth, and " + "signal availability" + ); + conditionalPrefix( + "", + trace_directory, + tryGetFromYaml( + trace_directory, + trace, + {"! directory"}, + "Directory to output trace files to" + ) + ); + conditionalPrefix( + "", + satellite_trace_filename, + tryGetFromYaml( + satellite_trace_filename, + trace, + {"1@ satellite_filename"}, + "Template filename for satellite trace files" + ) + ); + conditionalPrefix( + "", + receiver_trace_filename, + tryGetFromYaml( + receiver_trace_filename, + trace, + {"1! receiver_filename"}, + "Template filename for receiver trace files" + ) + ); + conditionalPrefix( + "", + receiver_json_filename, + tryGetFromYaml( + receiver_json_filename, + trace, + {"1! json_filename"}, + "Template filename for receiver json files" + ) + ); + conditionalPrefix( + "", + ionosphere_trace_filename, + tryGetFromYaml( + ionosphere_trace_filename, + trace, + {"1@ ionosphere_filename"}, + "Template filename for ionosphere trace files" + ) + ); + conditionalPrefix( + "", + network_trace_filename, + tryGetFromYaml( + network_trace_filename, + trace, + {"1! network_filename"}, + "Template filename for network trace files" + ) + ); + tryGetFromAny( + trace_level, + commandOpts, + trace, + {"! level"}, + "Threshold level for printing messages (0-6). Increasing this increases the " + "amount of data stored " + "in all trace files" + ); + + traceLevel = acsConfig.trace_level; + + tryGetFromYaml( + output_residual_chain, + trace, + {"! output_residual_chain"}, + "Output component-wise details for measurement residuals" + ); + tryGetFromYaml( + output_predicted_states, + trace, + {"! output_predicted_states"}, + "Output states after state transition 1" + ); + tryGetFromYaml( + output_initialised_states, + trace, + {"! output_initialised_states"}, + "Output states after state transition 2" + ); + tryGetFromYaml( + output_residuals, + trace, + {"! output_residuals"}, + "Output measurements and residuals" + ); + tryGetFromYaml( + output_config, + trace, + {"! output_config"}, + "Output configuration files to top of trace files" + ); + tryGetFromYaml( + output_statistics, + trace, + {"! output_statistics"}, + "Output statistics accumulated each epoch" + ); + tryGetFromYaml( + output_summaries, + trace, + {"! output_summaries"}, + "Output summaries accumulated each epoch" + ); + tryGetFromYaml( + output_json_trace, + trace, + {"@ output_json"}, + "Output json formatted trace files" + ); + } + + { + auto output_rotation = stringsToYamlObject( + outputs, + {"2@ output_rotation"}, + "Trace files can be rotated periodically by epoch interval. These options " + "specify the period that " + "applies to the template variables in filenames" + ); + + tryGetScaledFromYaml( + rotate_period, + output_rotation, + {"@ period"}, + {"@ period_units"}, + "Period that times will be rounded by to generate template variables in " + "filenames" + ); + } + + { + auto bias_sinex = stringsToYamlObject( + outputs, + {"3@ bias_sinex"}, + "Rinex formatted bias sinex files" + ); + + tryGetFromYaml( + output_bias_sinex, + bias_sinex, + {"0@ output"}, + "Output bias sinex files" + ); + conditionalPrefix( + "", + bias_sinex_directory, + tryGetFromYaml( + bias_sinex_directory, + bias_sinex, + {"@ directory"}, + "Directory to output bias sinex files to" + ) + ); + conditionalPrefix( + "", + bias_sinex_filename, + tryGetFromYaml( + bias_sinex_filename, + bias_sinex, + {"@ filename"}, + "Template filename for bias sinex files" + ) + ); + tryGetFromYaml( + bias_time_system, + bias_sinex, + {"@ bias_time_system"}, + "Time system for bias SINEX \"G\", \"C\", \"R\", \"UTC\", \"TAI\"" + ); + tryGetFromYaml( + ambrOpts.code_output_interval, + bias_sinex, + {"@ code_output_interval"}, + "Update interval for code biases" + ); + tryGetFromYaml( + ambrOpts.phase_output_interval, + bias_sinex, + {"@ phase_output_interval"}, + "Update interval for phase biases" + ); + tryGetFromYaml( + ambrOpts.output_rec_bias, + bias_sinex, + {"@ output_rec_bias"}, + "output receiver biases" + ); + } + + { + auto clocks = + stringsToYamlObject(outputs, {"3! clocks"}, "Rinex formatted clock files"); + + tryGetFromYaml(output_clocks, clocks, {"0! output"}, "Output clock files"); + conditionalPrefix( + "", + clocks_directory, + tryGetFromYaml( + clocks_directory, + clocks, + {"@ directory"}, + "Directory to output clock files to" + ) + ); + conditionalPrefix( + "", + clocks_filename, + tryGetFromYaml( + clocks_filename, + clocks, + {"@ filename"}, + "Template filename for clock files" + ) + ); + tryGetEnumVec(clocks_receiver_sources, clocks, {"@ receiver_sources"}); + tryGetEnumVec(clocks_satellite_sources, clocks, {"@ satellite_sources"}); + tryGetFromYaml( + clocks_output_interval, + clocks, + {"@ output_interval"}, + "Update interval for clock records" + ); + } + + { + auto decoded_rtcm = stringsToYamlObject( + outputs, + {"6@ decoded_rtcm"}, + "RTCM messages that are received may be recorded to human-readable json files" + ); + + tryGetFromYaml( + output_decoded_rtcm_json, + decoded_rtcm, + {"0@ output"}, + "Enable exporting decoded RTCM data to file" + ); + conditionalPrefix( + "", + decoded_rtcm_json_directory, + tryGetFromYaml( + decoded_rtcm_json_directory, + decoded_rtcm, + {"@ directory"}, + "Directory to export decoded RTCM data" + ) + ); + conditionalPrefix( + "", + decoded_rtcm_json_filename, + tryGetFromYaml( + decoded_rtcm_json_filename, + decoded_rtcm, + {"@ filename"}, + "Decoded RTCM data filename" + ) + ); + } + + { + auto encoded_rtcm = stringsToYamlObject( + outputs, + {"6@ encoded_rtcm"}, + "RTCM messages that are encoded and transmitted may be recorded to " + "human-readable json files" + ); + + tryGetFromYaml( + output_encoded_rtcm_json, + encoded_rtcm, + {"0@ output"}, + "Enable exporting encoded RTCM data to file" + ); + conditionalPrefix( + "", + encoded_rtcm_json_directory, + tryGetFromYaml( + encoded_rtcm_json_directory, + encoded_rtcm, + {"@ directory"}, + "Directory to export encoded RTCM data" + ) + ); + conditionalPrefix( + "", + encoded_rtcm_json_filename, + tryGetFromYaml( + encoded_rtcm_json_filename, + encoded_rtcm, + {"@ filename"}, + "Encoded RTCM data filename" + ) + ); + } + + { + auto erp = stringsToYamlObject( + outputs, + {"3@ erp"}, + "Earth rotation parameters can be output to file" + ); + + tryGetFromYaml(output_erp, erp, {"0@ output"}, "Enable exporting of erp data"); + conditionalPrefix( + "", + erp_directory, + tryGetFromYaml( + erp_directory, + erp, + {"@ directory"}, + "Directory to export erp data files" + ) + ); + conditionalPrefix( + "", + erp_filename, + tryGetFromYaml(erp_filename, erp, {"@ filename"}, "ERP data output filename") + ); + } + + { + auto ionex = stringsToYamlObject( + outputs, + {"4@ ionex"}, + "IONEX formatted ionospheric mapping and modelling outputs" + ); + + tryGetFromYaml( + output_ionex, + ionex, + {"0@ output"}, + "Enable exporting ionospheric model data" + ); + conditionalPrefix( + "", + ionex_directory, + tryGetFromYaml( + ionex_directory, + ionex, + {"@ directory"}, + "Directory to export ionex data" + ) + ); + conditionalPrefix( + "", + ionex_filename, + tryGetFromYaml(ionex_filename, ionex, {"@ filename"}, "Ionex data filename") + ); + tryGetFromYaml( + ionexGrid.lat_centre, + ionex, + {"@ grid", "@ lat_centre"}, + "Center lattitude for models" + ); + tryGetFromYaml( + ionexGrid.lon_centre, + ionex, + {"@ grid", "@ lon_centre"}, + "Center longitude for models" + ); + tryGetFromYaml( + ionexGrid.lat_width, + ionex, + {"@ grid", "@ lat_width"}, + "Total lattitudinal width of model" + ); + tryGetFromYaml( + ionexGrid.lon_width, + ionex, + {"@ grid", "@ lon_width"}, + "Total longitudinal width of model" + ); + tryGetFromYaml( + ionexGrid.lat_res, + ionex, + {"@ grid", "@ lat_resolution"}, + "Interval between lattitude outputs" + ); + tryGetFromYaml( + ionexGrid.lon_res, + ionex, + {"@ grid", "@ lon_resolution"}, + "Interval between longitude outputs" + ); + tryGetFromYaml( + ionexGrid.time_res, + ionex, + {"@ grid", "@ time_resolution"}, + "Interval between output epochs" + ); + } + + { + auto ionstec = stringsToYamlObject(outputs, {"4@ ionstec"}); + + tryGetFromYaml(output_ionstec, ionstec, {"0@ output"}); + conditionalPrefix( + "", + ionstec_directory, + tryGetFromYaml(ionstec_directory, ionstec, {"@ directory"}) + ); + conditionalPrefix( + "", + ionstec_filename, + tryGetFromYaml(ionstec_filename, ionstec, {"@ filename"}) + ); + } + + { + auto sbas_ems = stringsToYamlObject(outputs, {"4@ sbas_ems"}); + + tryGetFromYaml(output_sbas_ems, sbas_ems, {"0@ output"}); + conditionalPrefix( + "", + ems_directory, + tryGetFromYaml(ems_directory, sbas_ems, {"@ directory"}) + ); + conditionalPrefix( + "", + ems_filename, + tryGetFromYaml(ems_filename, sbas_ems, {"@ filename"}) + ); + } + + { + auto sinex = stringsToYamlObject(outputs, {"3@ sinex"}); + + tryGetFromYaml(output_sinex, sinex, {"0@ output"}); + conditionalPrefix( + "", + sinex_directory, + tryGetFromYaml(sinex_directory, sinex, {"@ directory"}) + ); + conditionalPrefix( + "", + sinex_filename, + tryGetFromYaml(sinex_filename, sinex, {"@ filename"}) + ); + } + + { + auto log = stringsToYamlObject( + outputs, + {"3! log"}, + "Log files store console output in files" + ); + + tryGetFromYaml(output_log, log, {"0! output"}, "Enable console output logging"); + tryGetFromYaml(log_json, log, {"0! json"}, "Log with json metadata"); + conditionalPrefix( + "", + log_directory, + tryGetFromYaml(log_directory, log, {"@ directory"}, "Log output directory") + ); + conditionalPrefix( + "", + log_filename, + tryGetFromYaml(log_filename, log, {"@ filename"}, "Log output filename") + ); + } + + { + auto gpx = stringsToYamlObject( + outputs, + {"3! gpx"}, + "GPX files contain point data that may be easily viewed in GIS mapping software" + ); + + tryGetFromYaml(output_gpx, gpx, {"0! output"}); + conditionalPrefix( + "", + gpx_directory, + tryGetFromYaml(gpx_directory, gpx, {"@ directory"}) + ); + conditionalPrefix( + "", + gpx_filename, + tryGetFromYaml(gpx_filename, gpx, {"@ filename"}) + ); + } + { + auto pos = stringsToYamlObject( + outputs, + {"3! pos"}, + "POS files contain point data that may be easily viewed in GIS mapping software" + ); + + tryGetFromYaml(output_pos, pos, {"0! output"}); + conditionalPrefix( + "", + pos_directory, + tryGetFromYaml(pos_directory, pos, {"@ directory"}) + ); + conditionalPrefix( + "", + pos_filename, + tryGetFromYaml(pos_filename, pos, {"@ filename"}) + ); + } + + { + auto spp = stringsToYamlObject( + outputs, + {"3! spp"}, + "SPP output files contain point data from SPP and SBAS solutions, including " + "Protection levels" + ); + + tryGetFromYaml(output_spp, spp, {"0! output"}); + conditionalPrefix( + "", + spp_directory, + tryGetFromYaml(spp_directory, spp, {"@ directory"}) + ); + conditionalPrefix( + "", + spp_filename, + tryGetFromYaml(spp_filename, spp, {"@ filename"}) + ); + } + + { + auto ntrip_log = stringsToYamlObject(outputs, {"5@ ntrip_log"}); + + tryGetFromYaml(output_ntrip_log, ntrip_log, {"0@ output"}); + conditionalPrefix( + "", + ntrip_log_directory, + tryGetFromYaml(ntrip_log_directory, ntrip_log, {"@ directory"}) + ); + conditionalPrefix( + "", + ntrip_log_filename, + tryGetFromYaml(ntrip_log_filename, ntrip_log, {"@ filename"}) + ); + } + + { + auto network_statistics = stringsToYamlObject(outputs, {"5@ network_statistics"}); + + tryGetFromYaml( + output_network_statistics_json, + network_statistics, + {"0@ output"}, + "Enable exporting network statistics data to file" + ); + conditionalPrefix( + "", + network_statistics_json_directory, + tryGetFromYaml( + network_statistics_json_directory, + network_statistics, + {"@ directory"}, + "Directory to export network statistics data" + ) + ); + conditionalPrefix( + "", + network_statistics_json_filename, + tryGetFromYaml( + network_statistics_json_filename, + network_statistics, + {"@ filename"}, + "Network statistics data filename" + ) + ); + } + + { + auto sp3 = stringsToYamlObject( + outputs, + {"3@ sp3"}, + "SP3 files contain orbital and clock data of satellites and receivers" + ); + + tryGetFromYaml(output_sp3, sp3, {"0@ output"}, "Enable SP3 file outputs"); + tryGetFromYaml( + output_inertial_orbits, + sp3, + {"@ output_inertial"}, + "Output the entries using inertial positions and velocities" + ); + tryGetFromYaml( + output_sp3_velocities, + sp3, + {"@ output_velocities"}, + "Output velocity data to SP3 file" + ); + conditionalPrefix( + "", + sp3_directory, + tryGetFromYaml( + sp3_directory, + sp3, + {"@ directory"}, + "Directory to store SP3 outputs" + ) + ); + conditionalPrefix( + "", + sp3_filename, + tryGetFromYaml(sp3_filename, sp3, {"@ filename"}, "SP3 output filename") + ); + conditionalPrefix( + "", + predicted_sp3_filename, + tryGetFromYaml( + predicted_sp3_filename, + sp3, + {"@ predicted_filename"}, + "Filename for predicted SP3 outputs" + ) + ); + tryGetEnumVec( + sp3_clock_sources, + sp3, + {"@ clock_sources"}, + "List of sources for clock data for SP3 outputs" + ); + tryGetEnumVec( + sp3_orbit_sources, + sp3, + {"@ orbit_sources"}, + "List of sources for orbit data for SP3 outputs" + ); + tryGetFromYaml( + sp3_output_interval, + sp3, + {"@ output_interval"}, + "Update interval for SP3 records" + ); + } + + { + auto orbit_ics = stringsToYamlObject( + outputs, + {"4@ orbit_ics"}, + "Orbital parameters can be output in a yaml that Ginan can later use as an " + "initial condition for " + "futher processing." + ); + + tryGetFromYaml( + output_orbit_ics, + orbit_ics, + {"@ output"}, + "Output orbital initial condition file" + ); + conditionalPrefix( + "", + orbit_ics_directory, + tryGetFromYaml( + orbit_ics_directory, + orbit_ics, + {"@ directory"}, + "Output orbital initial condition directory" + ) + ); + conditionalPrefix( + "", + orbit_ics_filename, + tryGetFromYaml( + orbit_ics_filename, + orbit_ics, + {"@ filename"}, + "Output orbital initial condition filename" + ) + ); + } + + { + auto orbex = stringsToYamlObject(outputs, {"3@ orbex"}); + + tryGetFromYaml(output_orbex, orbex, {"0@ output"}, "Output orbex file"); + conditionalPrefix( + "", + orbex_directory, + tryGetFromYaml( + orbex_directory, + orbex, + {"@ directory"}, + "Output orbex directory" + ) + ); + conditionalPrefix( + "", + orbex_filename, + tryGetFromYaml(orbex_filename, orbex, {"@ filename"}, "Output orbex filename") + ); + tryGetEnumVec( + orbex_orbit_sources, + orbex, + {"@ orbit_sources"}, + "Sources for orbex orbits" + ); + tryGetEnumVec( + orbex_clock_sources, + orbex, + {"@ clock_sources"}, + "Sources for orbex clocks" + ); + tryGetEnumVec( + orbex_attitude_sources, + orbex, + {"@ attitude_sources"}, + "Sources for orbex attitudes" + ); + tryGetEnumVec( + orbex_record_types, + orbex, + {"@ record_types"}, + "List of record types to output to orbex file" + ); + tryGetFromYaml( + orbex_output_interval, + orbex, + {"@ output_interval"}, + "Update interval for orbex records (irregular epoch interval is currently NOT " + "supported)" + ); + } + + { + auto cost = stringsToYamlObject(outputs, {"3@ cost"}, docs["cost"]); + + tryGetFromYaml( + output_cost, + cost, + {"0@ output"}, + "Enable data exporting to troposphere COST file" + ); + tryGetEnumVec( + cost_data_sources, + cost, + {"@ sources"}, + "Source for troposphere delay data - KALMAN, etc." + ); + conditionalPrefix( + "", + cost_directory, + tryGetFromYaml( + cost_directory, + cost, + {"@ directory"}, + "Directory to export troposphere COST file" + ) + ); + conditionalPrefix( + "", + cost_filename, + tryGetFromYaml(cost_filename, cost, {"@ filename"}, "Troposphere COST filename") + ); + tryGetFromYaml( + cost_time_interval, + cost, + {"@ time_interval"}, + "Time interval between entries in troposphere COST file (sec)" + ); + tryGetFromYaml( + cost_format, + cost, + {"@ cost_format"}, + "Format name & version number" + ); + tryGetFromYaml(cost_project, cost, {"@ cost_project"}, "Project name"); + tryGetFromYaml(cost_status, cost, {"@ cost_status"}, "File status"); + tryGetFromYaml(cost_centre, cost, {"@ cost_centre"}, "Processing centre"); + tryGetFromYaml(cost_method, cost, {"@ cost_method"}, "Processing method"); + tryGetFromYaml(cost_orbit_type, cost, {"@ cost_orbit_type"}, "Orbit type"); + tryGetFromYaml( + cost_met_source, + cost, + {"@ cost_met_sources"}, + "Source of met. data" + ); + } + + { + auto rinex_nav = stringsToYamlObject(outputs, {"5@ rinex_nav"}); + + tryGetFromYaml(output_rinex_nav, rinex_nav, {"0@ output"}); + conditionalPrefix( + "", + rinex_nav_directory, + tryGetFromYaml(rinex_nav_directory, rinex_nav, {"@ directory"}) + ); + conditionalPrefix( + "", + rinex_nav_filename, + tryGetFromYaml(rinex_nav_filename, rinex_nav, {"@ filename"}) + ); + tryGetFromYaml(rinex_nav_version, rinex_nav, {"@ version"}); + } + + { + auto rinex_obs = stringsToYamlObject(outputs, {"5@ rinex_obs"}); + + tryGetFromYaml(output_rinex_obs, rinex_obs, {"0@ output"}); + conditionalPrefix( + "", + rinex_obs_directory, + tryGetFromYaml(rinex_obs_directory, rinex_obs, {"@ directory"}) + ); + conditionalPrefix( + "", + rinex_obs_filename, + tryGetFromYaml(rinex_obs_filename, rinex_obs, {"@ filename"}) + ); + tryGetFromYaml(rinex_obs_print_C_code, rinex_obs, {"@ output_pseudorange"}); + tryGetFromYaml(rinex_obs_print_L_code, rinex_obs, {"@ output_phase_range"}); + tryGetFromYaml(rinex_obs_print_D_code, rinex_obs, {"@ output_doppler"}); + tryGetFromYaml(rinex_obs_print_S_code, rinex_obs, {"@ output_signal_to_noise"}); + tryGetFromYaml(rinex_obs_version, rinex_obs, {"@ version"}); + } + + { + auto rtcm_nav = stringsToYamlObject(outputs, {"5@ rtcm_nav"}); + + tryGetFromYaml(record_rtcm_nav, rtcm_nav, {"0@ output"}); + conditionalPrefix( + "", + rtcm_nav_directory, + tryGetFromYaml(rtcm_nav_directory, rtcm_nav, {"@ directory"}) + ); + conditionalPrefix( + "", + rtcm_nav_filename, + tryGetFromYaml(rtcm_nav_filename, rtcm_nav, {"@ filename"}) + ); + } + + { + auto rtcm_obs = stringsToYamlObject(outputs, {"5@ rtcm_obs"}); + + tryGetFromYaml(record_rtcm_obs, rtcm_obs, {"0@ output"}); + conditionalPrefix( + "", + rtcm_obs_directory, + tryGetFromYaml(rtcm_obs_directory, rtcm_obs, {"@ directory"}) + ); + conditionalPrefix( + "", + rtcm_obs_filename, + tryGetFromYaml(rtcm_obs_filename, rtcm_obs, {"@ filename"}) + ); + } + + { + auto raw_ubx = stringsToYamlObject(outputs, {"6@ raw_ubx"}); + + tryGetFromYaml(record_raw_ubx, raw_ubx, {"0 output"}); + conditionalPrefix( + "", + raw_ubx_directory, + tryGetFromYaml(raw_ubx_directory, raw_ubx, {"directory"}) + ); + conditionalPrefix( + "", + raw_ubx_filename, + tryGetFromYaml(raw_ubx_filename, raw_ubx, {"filename"}) + ); + } + + { + auto raw_custom = stringsToYamlObject(outputs, {"6@ raw_custom"}); + + tryGetFromYaml(record_raw_custom, raw_custom, {"0 output"}); + conditionalPrefix( + "", + raw_custom_directory, + tryGetFromYaml(raw_custom_directory, raw_custom, {"directory"}) + ); + conditionalPrefix( + "", + raw_custom_filename, + tryGetFromYaml(raw_custom_filename, raw_custom, {"filename"}) + ); + } + + { + auto slr_obs = stringsToYamlObject(outputs, {"7@ slr_obs"}, docs["slr_obs"]); + + tryGetFromYaml( + output_slr_obs, + slr_obs, + {"0@ output"}, + "Enable data exporting to tabular SLR obs file" + ); + conditionalPrefix( + "", + slr_obs_directory, + tryGetFromYaml( + slr_obs_directory, + slr_obs, + {"@ directory"}, + "Directory to export tabular SLR obs file" + ) + ); + conditionalPrefix( + "", + slr_obs_filename, + tryGetFromYaml( + slr_obs_filename, + slr_obs, + {"@ filename"}, + "Tabular SLR obs filename" + ) + ); + } + + { + auto trop_sinex = + stringsToYamlObject(outputs, {"3@ trop_sinex"}, docs["trop_sinex"]); + + tryGetFromYaml( + output_trop_sinex, + trop_sinex, + {"0@ output"}, + "Enable data exporting to troposphere SINEX file" + ); + tryGetEnumVec( + trop_sinex_data_sources, + trop_sinex, + {"@ sources"}, + "Source for troposphere delay data - KALMAN, etc." + ); + conditionalPrefix( + "", + trop_sinex_directory, + tryGetFromYaml( + trop_sinex_directory, + trop_sinex, + {"@ directory"}, + "Directory to export troposphere SINEX file" + ) + ); + conditionalPrefix( + "", + trop_sinex_filename, + tryGetFromYaml( + trop_sinex_filename, + trop_sinex, + {"@ filename"}, + "Troposphere SINEX filename" + ) + ); + tryGetFromYaml( + trop_sinex_sol_type, + trop_sinex, + {"@ sol_type"}, + "Troposphere SINEX solution type" + ); + tryGetFromYaml( + trop_sinex_obs_code, + trop_sinex, + {"@ obs_code"}, + "Troposphere SINEX observation code" + ); + tryGetFromYaml( + trop_sinex_const_code, + trop_sinex, + {"@ const_code"}, + "Troposphere SINEX const code" + ); + tryGetFromYaml( + trop_sinex_version, + trop_sinex, + {"@ version"}, + "Troposphere SINEX version" + ); + } + + // ssr_outputs + { + auto ssr_outputs = + stringsToYamlObject(outputs, {"2@ ssr_outputs"}, docs["ssr_outputs"]); + + tryGetEnumVec( + ssrOpts.ephemeris_sources, + ssr_outputs, + {"@ ephemeris_sources"}, + "Sources for SSR ephemeris" + ); + tryGetEnumVec( + ssrOpts.clock_sources, + ssr_outputs, + {"@ clock_sources"}, + "Sources for SSR clocks" + ); + tryGetEnumVec( + ssrOpts.code_bias_sources, + ssr_outputs, + {"2@ code_bias_sources"}, + "Sources for SSR code biases" + ); + tryGetEnumVec( + ssrOpts.phase_bias_sources, + ssr_outputs, + {"2@ phase_bias_sources"}, + "Sources for SSR phase biases" + ); + tryGetFromYaml(ssrOpts.prediction_interval, ssr_outputs, {"@ prediction_interval"}); + tryGetFromYaml(ssrOpts.prediction_duration, ssr_outputs, {"@ prediction_duration"}); + tryGetFromYaml( + ssrOpts.extrapolate_corrections, + ssr_outputs, + {"@ extrapolate_corrections"} + ); + tryGetFromYaml(ssrOpts.cmpssr_cell_mask, ssr_outputs, {"@ cmpssr_cell_mask"}); + tryGetFromYaml(ssrOpts.max_stec_sigma, ssr_outputs, {"@ max_stec_sigma"}); + + // atmospheric + { + auto atmospheric = + stringsToYamlObject(ssr_outputs, {"@ atmospheric"}, docs["atmospheric"]); + + tryGetEnumVec( + ssrOpts.atmosphere_sources, + atmospheric, + {"@ sources"}, + "Sources for SSR ionosphere" + ); + tryGetFromYaml( + ssrOpts.region_id, + atmospheric, + {"@ region_id"}, + "Region ID for atmospheric corrections" + ); + tryGetFromYaml( + ssrOpts.region_iod, + atmospheric, + {"@ region_iod"}, + "Region IOD for atmospheric corrections (default: -1 for undefined)" + ); + tryGetFromYaml(ssrOpts.npoly_trop, atmospheric, {"@ npoly_trop"}); + tryGetFromYaml(ssrOpts.npoly_iono, atmospheric, {"@ npoly_iono"}); + tryGetFromYaml( + ssrOpts.grid_type, + atmospheric, + {"@ grid_type"}, + "Grid type for gridded atmospheric corrections" + ); + tryGetFromYaml( + ssrOpts.use_grid_iono, + atmospheric, + {"@ use_grid_iono"}, + "Grid type for gridded atmospheric corrections" + ); + tryGetFromYaml( + ssrOpts.use_grid_trop, + atmospheric, + {"@ use_grid_trop"}, + "Grid type for gridded atmospheric corrections" + ); + tryGetFromYaml(ssrOpts.lat_max, atmospheric, {"@ lat_max"}); + tryGetFromYaml(ssrOpts.lat_min, atmospheric, {"@ lat_min"}); + tryGetFromYaml(ssrOpts.lat_int, atmospheric, {"@ lat_int"}); + tryGetFromYaml(ssrOpts.lon_max, atmospheric, {"@ lon_max"}); + tryGetFromYaml(ssrOpts.lon_min, atmospheric, {"@ lon_min"}); + tryGetFromYaml(ssrOpts.lon_int, atmospheric, {"@ lon_int"}); + tryGetFromYaml( + ssrOpts.cmpssr_stec_format, + atmospheric, + {"@ cmpssr_stec_format"}, + "Format of STEC gridded corrections: 0:4bit(LSB=0.04) , 1:4bit(LSB=0.12), " + "2:5bit, 3:7bit, " + "4:16bit" + ); + tryGetFromYaml( + ssrOpts.cmpssr_trop_format, + atmospheric, + {"@ cmpssr_trop_format"}, + "Format of Trop. ZWD corrections: 0:8bit, 1:6bit" + ); + } + } + + { + auto streams = stringsToYamlObject(outputs, {"2@ streams"}); + + tryGetFromYaml( + root_stream_url, + streams, + {"0@ root_url"}, + "Root url to be prepended to all other streams specified in this section. If " + "the streams used have " + "individually specified root urls, usernames, or passwords, this should not be " + "used." + ); + + SsrBroadcast dummyStreamData; + tryGetStreamFromYaml(dummyStreamData, streams, {"@ XMPL"}); + + auto [outStreamNode, outStreamString] = stringsToYamlObject( + streams, + {"1@ labels"}, + "List of output stream is with further information to be found in its own " + "section, as per XMPL " + "below" + ); + + for (auto outLabelYaml : outStreamNode) + { + string outLabel = outLabelYaml.as(); + + tryGetStreamFromYaml( + netOpts.uploadingStreamData[outLabel], + streams, + {outLabel} + ); + + conditionalPrefix( + "", + netOpts.uploadingStreamData[outLabel].url + ); + + replaceTags(netOpts.uploadingStreamData[outLabel].url); + } + } + } + + // inputs + { + auto inputs = stringsToYamlObject({yaml, ""}, {"0! inputs"}, docs["inputs"]); + auto troposphere = stringsToYamlObject( + inputs, + {"2@ troposphere"}, + "Files specifying tropospheric model inputs" + ); + auto tides = stringsToYamlObject( + inputs, + {"2@ tides"}, + "Files specifying tidal loading and potential inputs" + ); + auto ionosphere = stringsToYamlObject( + inputs, + {"3@ ionosphere"}, + "Files specifying ionospheric model inputs" + ); + + tryGetFromAny( + inputs_root, + commandOpts, + inputs, + {"0! inputs_root"}, + "Root path to be added to all other input files (unless they are absolute)" + ); + + tryGetFromYaml( + allow_missing_inputs, + inputs, + {"@ allow_missing_inputs"}, + "Allow adding inpuut files which do not (yet) exist" + ); + + auto getAppendFiles = [&](vector& output, + NodeStack& nodeStack, + const string& descriptor, + const string& comment) + { + vector vec; + + tryGetFromAny(vec, commandOpts, nodeStack, {descriptor}, comment); + + conditionalPrefix("", vec); + + output.insert(output.end(), vec.begin(), vec.end()); + }; + + getAppendFiles(atx_files, inputs, {"4! atx_files"}, "List of atx files to use"); + getAppendFiles(snx_files, inputs, {"4@ snx_files"}, "List of snx files to use"); + getAppendFiles(erp_files, inputs, {"4! erp_files"}, "List of erp files to use"); + getAppendFiles(igrf_files, inputs, {"4@ igrf_files"}, "List of igrf files to use"); + getAppendFiles(egm_files, inputs, {"4@ egm_files"}, "List of egm files to use"); + getAppendFiles( + planetary_ephemeris_files, + inputs, + {"4@ planetary_ephemeris_files"}, + "List of jpl files to use" + ); + getAppendFiles(cmc_files, inputs, {"4@ cmc_files"}, "List of cmc files to use"); + getAppendFiles(hfeop_files, inputs, {"4@ hfeop_files"}, "List of hfeop files to use"); + getAppendFiles( + space_weather_files, + inputs, + {"4@ space_weather_files"}, + "List of space weather files to use" + ); + getAppendFiles( + atm_reg_definitions, + ionosphere, + {"@ atm_reg_definitions"}, + "List of files to define regions for compact SSR" + ); + getAppendFiles( + ion_files, + ionosphere, + {"@ ion_files"}, + "List of IONEX files for VTEC input" + ); + getAppendFiles(vmf_files, troposphere, {"@ vmf_files"}, "List of vmf files to use"); + getAppendFiles( + gpt2grid_files, + troposphere, + {"@ gpt2grid_files"}, + "List of gpt2 grid files to use" + ); + getAppendFiles( + orography_files, + troposphere, + {"@ orography_files"}, + "List of orography files to use" + ); + getAppendFiles( + ocean_tide_potential_files, + tides, + {"@ ocean_tide_potential_files"}, + "List of tide files to use" + ); + getAppendFiles( + atmos_tide_potential_files, + tides, + {"@ atmos_tide_potential_files"}, + "List of tide files to use" + ); + getAppendFiles( + ocean_tide_loading_blq_files, + tides, + {"@ ocean_tide_loading_blq_files"}, + "List of otl blq files to use" + ); + getAppendFiles( + atmos_tide_loading_blq_files, + tides, + {"@ atmos_tide_loading_blq_files"}, + "List of atl blq files to use" + ); + getAppendFiles( + ocean_pole_tide_loading_files, + tides, + {"@ ocean_pole_tide_loading_files"}, + "List of opole files to use" + ); + getAppendFiles( + atmos_ocean_dealiasing_files, + tides, + {"@ atmos_ocean_dealiasing_files"}, + "List of tide files to use" + ); + getAppendFiles( + ocean_pole_tide_potential_files, + tides, + {"@ ocean_pole_tide_potential_files"}, + "List of tide files to use" + ); + + tryGetEnumVec( + atl_blq_row_order, + tides, + {"@ atl_blq_row_order"}, + "Row order for amplitude and phase components in ATL BLQ files" + ); + tryGetEnumVec( + otl_blq_row_order, + tides, + {"@ otl_blq_row_order"}, + "Row order for amplitude and phase components in OTL BLQ files" + ); + + tryGetEnumVec( + atl_blq_col_order, + tides, + {"@ atl_blq_col_order"}, + "Column order for amplitude and phase components in ATL BLQ files" + ); + tryGetEnumVec( + otl_blq_col_order, + tides, + {"@ otl_blq_col_order"}, + "Column order for amplitude and phase components in OTL BLQ files" + ); + + { + auto gnss_data = stringsToYamlObject( + inputs, + {"2! gnss_observations"}, + "Signal observation data from gnss receivers to be used as measurements" + ); + + conditionalPrefix( + "", + gnss_obs_root, + tryGetFromAny( + gnss_obs_root, + commandOpts, + gnss_data, + {"0! gnss_observations_root"}, + "Root path to be added to all other gnss data inputs (unless they are " + "absolute)" + ) + ); + + tryGetMappedList( + rnx_inputs, + commandOpts, + gnss_data, + {"1! rnx_inputs"}, + "", + "List of rinex inputs to use" + ); + tryGetMappedList( + ubx_inputs, + commandOpts, + gnss_data, + {"1# ubx_inputs"}, + "", + "List of ubxfiles inputs to use" + ); + tryGetMappedList( + custom_inputs, + commandOpts, + gnss_data, + {"1# custom_inputs"}, + "", + "List of customfiles inputs to use" + ); + tryGetMappedList( + obs_rtcm_inputs, + commandOpts, + gnss_data, + {"1! rtcm_inputs"}, + "", + "List of rtcmfiles inputs to use for observations" + ); + } + + { + auto pseudo_observation_data = stringsToYamlObject( + inputs, + {"2@ pseudo_observations"}, + "Use data from pre-processed data products as observations. Useful for " + "combining and comparing " + "datasets" + ); + + conditionalPrefix( + "", + pseudo_obs_root, + tryGetFromYaml( + pseudo_obs_root, + pseudo_observation_data, + {"0@ pseudo_observations_root"}, + "Root path to be added to all other pseudo obs data files (unless they are " + "absolute)" + ) + ); + + tryGetMappedList( + pseudo_sp3_inputs, + commandOpts, + pseudo_observation_data, + {"@@ sp3_inputs"}, + "", + "List of sp3 inputs to use for pseudoobservations" + ); + tryGetMappedList( + pseudo_snx_inputs, + commandOpts, + pseudo_observation_data, + {"1@ snx_inputs"}, + "", + "List of snx inputs to use for pseudoobservations" + ); + conditionalPrefix( + "", + pseudo_filter_files, + tryGetFromAny( + pseudo_filter_files, + commandOpts, + pseudo_observation_data, + {"1# filter_files"}, + "List of inputs to use for custom pseudoobservations" + ) + ); + + tryGetFromYaml( + eci_pseudoobs, + pseudo_observation_data, + {"@ eci_pseudoobs"}, + "Pseudo observations are provided in eci frame rather than standard ECEF SP3 " + "files" + ); + } + + { + auto satellite_data = stringsToYamlObject(inputs, {"2! satellite_data"}); + + conditionalPrefix( + "", + sat_data_root, + tryGetFromYaml( + sat_data_root, + satellite_data, + {"0! satellite_data_root"}, + "Root path to be added to all other satellite data files (unless they are " + "absolute)" + ) + ); + conditionalPrefix( + "", + nav_files, + tryGetFromAny( + nav_files, + commandOpts, + satellite_data, + {"1! nav_files"}, + "List of ephemeris files to use" + ) + ); + conditionalPrefix( + "", + sp3_files, + tryGetFromAny( + sp3_files, + commandOpts, + satellite_data, + {"1! sp3_files"}, + "List of sp3 files to use" + ) + ); + conditionalPrefix( + "", + dcb_files, + tryGetFromAny( + dcb_files, + commandOpts, + satellite_data, + {"1! dcb_files"}, + "List of dcb files to use" + ) + ); + conditionalPrefix( + "", + bsx_files, + tryGetFromAny( + bsx_files, + commandOpts, + satellite_data, + {"1! bsx_files"}, + "List of biassinex files to use" + ) + ); + conditionalPrefix( + "", + clk_files, + tryGetFromAny( + clk_files, + commandOpts, + satellite_data, + {"1! clk_files"}, + "List of clock files to use" + ) + ); + conditionalPrefix( + "", + sid_files, + tryGetFromAny( + sid_files, + commandOpts, + satellite_data, + {"2@ sid_files"}, + "List of sat ID files to use - from " + "https://cddis.nasa.gov/sp3c_satlist.html/" + ) + ); + conditionalPrefix( + "", + com_files, + tryGetFromAny( + com_files, + commandOpts, + satellite_data, + {"2@ com_files"}, + "List of com files to use - retroreflector offsets from " + "centre-of-mass for spherical " + "sats" + ) + ); + conditionalPrefix( + "", + crd_files, + tryGetFromAny( + crd_files, + commandOpts, + satellite_data, + {"2@ crd_files"}, + "List of crd files to use - SLR observation data" + ) + ); + conditionalPrefix( + "", + obx_files, + tryGetFromAny( + obx_files, + commandOpts, + satellite_data, + {"1! obx_files"}, + "List of orbex files to use" + ) + ); + + // rtcm_inputs + { + auto rtcm_inputs = + stringsToYamlObject(satellite_data, {"! rtcm_inputs"}, docs["rtcm_inputs"]); + + conditionalPrefix( + "", + rtcm_inputs_root, + tryGetFromYaml( + rtcm_inputs_root, + rtcm_inputs, + {"0! rtcm_inputs_root"}, + "Root path to be added to all other rtcm inputs (unless they are " + "absolute)" + ) + ); + + conditionalPrefix( + "", + nav_rtcm_inputs, + tryGetFromAny( + nav_rtcm_inputs, + commandOpts, + rtcm_inputs, + {"1! rtcm_inputs"}, + "List of rtcm inputs to use for corrections" + ) + ); + conditionalPrefix( + "", + qzs_rtcm_inputs, + tryGetFromAny( + qzs_rtcm_inputs, + commandOpts, + rtcm_inputs, + {"2@ qzl6_inputs"}, + "List of qzss L6 inputs to use for corrections" + ) + ); + + tryGetFromYaml( + ssrInOpts.code_bias_valid_time, + rtcm_inputs, + {"@ code_bias_validity_time"}, + "Valid time period of SSR code biases" + ); + tryGetFromYaml( + ssrInOpts.phase_bias_valid_time, + rtcm_inputs, + {"@ phase_bias_validity_time"}, + "Valid time period of SSR phase biases" + ); + tryGetFromYaml( + ssrInOpts.one_freq_phase_bias, + rtcm_inputs, + {"@ one_freq_phase_bias"}, + "Used stream have one SSR phase bias per frequency" + ); + tryGetFromYaml( + ssrInOpts.global_vtec_valid_time, + rtcm_inputs, + {"@ global_vtec_valid_time"}, + "Valid time period of global VTEC maps" + ); + tryGetFromYaml( + ssrInOpts.local_stec_valid_time, + rtcm_inputs, + {"@ local_stec_valid_time"}, + "Valid time period of local STEC corrections" + ); + tryGetFromYaml( + ssrInOpts.local_trop_valid_time, + rtcm_inputs, + {"@ local_trop_valid_time"}, + "Valid time period of local Troposphere corrections" + ); + tryGetFromYaml( + validity_interval_factor, + rtcm_inputs, + {"@ validity_interval_factor"} + ); + tryGetEnumOpt( + ssr_input_antenna_offset, + rtcm_inputs, + {"1! ssr_antenna_offset"}, + "Ephemeris type that is provided in the listed SSR stream, i.e. satellite " + "antenna-phase-centre " + "(APC) or centre-of-mass (COM). This information is listed in the NTRIP " + "Caster's sourcetable" + ); + } + + { + auto sbas_inputs = stringsToYamlObject( + satellite_data, + {"! sbas_inputs"}, + "Configuration for SBAS related input. Including SiSNet streams and EMS " + "files" + ); + + conditionalPrefix( + "", + ems_files, + tryGetFromAny( + ems_files, + commandOpts, + sbas_inputs, + {"1! ems_files"}, + "List of SBAS EMS files to use" + ) + ); + conditionalPrefix( + "", + sisnet_inputs_root, + tryGetFromYaml( + sisnet_inputs_root, + sbas_inputs, + {"2@ sisnet_inputs_root"}, + "Root path to be added to all other sisnet inputs (unless they are " + "absolute)" + ) + ); + + conditionalPrefix( + "", + sisnet_inputs, + tryGetFromAny( + sisnet_inputs, + commandOpts, + sbas_inputs, + {"2@ sisnet_inputs"}, + "List of sisnet inputs to use for corrections" + ) + ); + + tryGetFromYaml( + sbsInOpts.prn, + sbas_inputs, + {"@ sbas_prn"}, + "PRN for SBAS satelite" + ); + tryGetFromYaml( + sbsInOpts.freq, + sbas_inputs, + {"@ sbas_frequency"}, + "Carrier frequency of SBAS channel" + ); + tryGetFromYaml( + sbs_time_delay, + sbas_inputs, + {"@ sbas_time_delay"}, + "Time delay for SBAS corrections when simulating real-time in post-process" + ); + tryGetFromYaml( + sbsInOpts.mt0, + sbas_inputs, + {"@ sbas_message_0"}, + "Message type replaced by MT0 (use 65 for SouthPAN L5)" + ); + tryGetFromYaml( + sbsInOpts.use_do259, + sbas_inputs, + {"@ use_do259"}, + "Use original standard DO-259, intead of DO-259A, for DFMC" + ); + tryGetFromYaml( + sbsInOpts.pvs_on_dfmc, + sbas_inputs, + {"@ pvs_on_dfmc"}, + "Interpret DFMC messages as PVS messages" + ); + tryGetFromYaml( + sbsInOpts.prec_aproach, + sbas_inputs, + {"@ prec_aproach"}, + "Limit SBAS solutions to precision approach (which limits maximum SBAS " + "correction age)" + ); + tryGetFromYaml( + sbsInOpts.dfmc_uire, + sbas_inputs, + {"@ iono_residual_dfmc"}, + "Ionosphere residual from IF combination (use with DFMC only)" + ); + tryGetFromYaml( + sbsInOpts.ems_year, + sbas_inputs, + {"@ ems_reference_year"}, + "Reference year for EMS files (should be within 50 year of real value)" + ); + tryGetFromYaml( + sbsInOpts.smth_win, + sbas_inputs, + {"@ smoothing_window"}, + "Smoothing window to be used by SBAS (100, 1 second samples are normally " + "used)" + ); + tryGetFromYaml( + sbsInOpts.smth_out, + sbas_inputs, + {"@ max_smooth_outage"}, + "Maximum outage to reset smoothing (10 seconds or 3 x obs_rate is " + "recommended)" + ); + } + } + } + + // processing_options + { + auto processing_options = stringsToYamlObject( + {yaml, ""}, + {processing_options_str}, + "Various sections and parameters to specify how the observations are processed" + ); + + // process_modes + { + auto process_modes = stringsToYamlObject( + processing_options, + {"1! process_modes"}, + "Aspects of the processing flow may be enabled and disabled according to " + "desired type of solutions" + ); + + tryGetFromYaml( + process_ionosphere, + process_modes, + {"@ ionosphere"}, + "Compute Ionosphere models based on GNSS measurements" + ); + tryGetFromYaml( + process_preprocessor, + process_modes, + {"! preprocessor"}, + "Preprocessing and quality checks" + ); + tryGetFromYaml( + process_spp, + process_modes, + {"! spp"}, + "Perform SPP on receiver data" + ); + tryGetFromYaml( + process_ppp, + process_modes, + {"! ppp"}, + "Perform PPP network or end user mode" + ); + tryGetFromYaml( + slrOpts.process_slr, + process_modes, + {"@ slr"}, + "Process SLR observations" + ); + } + + // gnss_general + { + auto general = stringsToYamlObject( + processing_options, + {"0! gnss_general"}, + "Options to specify the processing of gnss observations" + ); + + tryGetFromYaml( + require_apriori_positions, + general, + {"@ require_apriori_positions"}, + "Restrict processing to receivers that have apriori positions available" + ); + tryGetFromYaml( + require_site_eccentricity, + general, + {"@ require_site_eccentricity"}, + "Restrict processing to receivers that have site eccentricity information" + ); + tryGetFromYaml( + pppOpts.merge_correlated_states, + general, + {"@ merge_correlated_states"}, + "Combine correlated states to eliminate unestimable states" + ); + tryGetFromYaml( + require_sinex_data, + general, + {"@ require_sinex_data"}, + "Restrict processing to receivers that have sinex data available" + ); + tryGetFromYaml( + require_antenna_details, + general, + {"@ require_antenna_details"}, + "Restrict processing to receivers that have antenna details" + ); + tryGetFromYaml( + require_reflector_com, + general, + {"@ require_reflector_com"}, + "Restrict processing to SLR observations that have center of mass to laser " + "retroreflector array " + "offsets" + ); + tryGetFromYaml( + reference_clock, + general, + {"@ reference_clock"}, + "ID of sat/rec to use for reference clock in pivot calculations" + ); + tryGetFromYaml( + reference_bias, + general, + {"@ reference_bias"}, + "ID of sat/rec to use for reference bias in pivot calculations" + ); + tryGetFromYaml( + pivot_receiver, + general, + {"@ pivot_receiver"}, + "Largely deprecated option for iono" + ); + tryGetFromYaml( + interpolate_rec_pco, + general, + {"@ interpolate_rec_pco"}, + "Interpolate other known pco values to find pco for unknown frequencies" + ); + tryGetFromYaml( + auto_fill_pco, + general, + {"@ auto_fill_pco"}, + "Use similar PCOs when requested values are not found" + ); + tryGetFromYaml( + pppOpts.equate_ionospheres, + general, + {"@ equate_ionospheres"}, + "Use same STEC values for different receivers, useful for simulated rtk mode" + ); + tryGetFromYaml( + pppOpts.equate_tropospheres, + general, + {"@ equate_tropospheres"}, + "Use same troposphere values for different receivers, useful for simulated rtk " + "mode" + ); + tryGetFromYaml( + pppOpts.use_rtk_combo, + general, + {"@ use_rtk_combo"}, + "Combine applicable observations to simulate an rtk solution" + ); + tryGetFromYaml( + pppOpts.use_primary_signals, + general, + {"@ use_primary_signals"}, + "Limit processing to first signal of a frequency when multiple are available" + ); + tryGetFromYaml( + pppOpts.add_eop_component, + general, + {"@ add_eop_component"}, + "Add eop adjustments as a component in residual chain (for adjusting frames to " + "match ecef " + "ephemeris)" + ); + tryGetFromYaml( + delete_old_ephemerides, + general, + {"@ delete_old_ephemerides"}, + "Remove old ephemerides that have accumulated over time from before far before " + "the currently " + "processing epoch" + ); + tryGetFromYaml( + use_tgd_bias, + general, + {"@ use_tgd_bias"}, + "Use TGD/BGD bias from ephemeris, DO NOT turn on unless using " + "Klobuchar/NeQuick Ionospheres" + ); + tryGetFromYaml( + common_sat_pco, + general, + {"@ common_sat_pco"}, + "Use L1 satellite PCO values for all signals" + ); + tryGetFromYaml( + common_rec_pco, + general, + {"@ common_rec_pco"}, + "Use L1 receiver PCO values for all signals" + ); + tryGetFromYaml( + leap_seconds, + general, + {"@ gpst_utc_leap_seconds"}, + "Difference between gps time and utc in leap seconds" + ); + + tryGetFromYaml( + process_meas[CODE], + general, + {"1@ code_measurements", "process"}, + "Process code measurements" + ); + tryGetFromYaml( + process_meas[PHAS], + general, + {"1@ phase_measurements", "process"}, + "Process phase measurements" + ); + + tryGetFromYaml( + fixed_phase_bias_var, + general, + {"@ fixed_phase_bias_var"}, + "Variance of phase bias to be considered fixed/binded" + ); + tryGetFromYaml( + adjust_rec_clocks_by_spp, + general, + {"@ adjust_rec_clocks_by_spp"}, + "Adjust receiver clocks by SPP values to minimise prefit residuals" + ); + tryGetFromYaml( + adjust_clocks_for_jumps_only, + general, + {"@ adjust_clocks_for_jumps_only"}, + "Round clock adjustments from SPP to half milliseconds" + ); + // tryGetFromYaml (minimise_sat_clock_offsets, general, {"@ + // minimise_sat_clock_offsets" + // }, "Apply gauss-markov mu values to satellite clocks to minimise offsets with + // respect to broadcast values"); + tryGetFromYaml( + minimise_sat_orbit_offsets, + general, + {"@ minimise_sat_orbit_offsets"}, + "Apply gauss-markov mu values to satellite orbits to minimise offsets with " + "respect to broadcast " + "values" + ); + tryGetFromYaml( + minimise_ionosphere_offsets, + general, + {"@ minimise_ionosphere_offsets"}, + "Apply gauss-markov mu values to stec values to minimise offsets with respect " + "to klobuchar values" + ); + + auto clock_offset = stringsToYamlObject( + general, + {"@ minimise_sat_clock_offsets"}, + "Apply gauss-markov mu values to satellite clocks to minimise offsets with " + "respect to broadcast " + "values" + ); + tryGetFromYaml( + minimise_sat_clock_offsets.enable, + clock_offset, + {"@ enable"}, + "Enable gauss-markov mu values to satellite clocks to minimise offsets with " + "respect to broadcast " + "values" + ); + tryGetFromYaml( + minimise_sat_clock_offsets.max_offset, + clock_offset, + {"@ max_offset"}, + "Maximum satellite clock offset (meters) used in broadcast alignment" + ); + + for (E_Sys sys : magic_enum::enum_values()) + { + auto sys_options = stringsToYamlObject( + general, + {"1! sys_options", enum_to_string(sys)}, + (string) "Options for the " + enum_to_string(sys) + " constellation" + ); + + tryGetFromYaml( + process_sys[sys], + sys_options, + {"0! process"}, + "Process this constellation" + ); + tryGetFromYaml( + solve_amb_for[sys], + sys_options, + {"3! ambiguity_resolution"}, + "Solve carrier phase ambiguities for this constellation" + ); + tryGetFromYaml( + reject_eclipse[sys], + sys_options, + {"2@ reject_eclipse"}, + "Exclude satellites that are in eclipsing region" + ); + tryGetFromYaml( + receiver_amb_pivot[sys], + sys_options, + {"2@ receiver_amb_pivot"}, + "Constrain: set of ambiguities, to eliminate receiver rank deficiencies" + ); + tryGetFromYaml( + network_amb_pivot[sys], + sys_options, + {"2@ network_amb_pivot"}, + "Constrain: set of ambiguities, to eliminate network rank deficiencies" + ); + tryGetFromYaml( + use_for_iono_model[sys], + sys_options, + {"2@ use_for_iono_model"}, + "Use this constellation as part of Ionospheric model" + ); + tryGetFromYaml( + use_iono_corrections[sys], + sys_options, + {"2@ use_iono_corrections"}, + "Use external ionosphere delay estimation for this constellation" + ); + tryGetEnumOpt(used_nav_types[sys], sys_options, {"2@ used_nav_type"}); + tryGetEnumVec( + code_priorities[sys], + sys_options, + {"2! code_priorities"}, + "List of observation codes to use in processing" + ); + tryGetFromYaml( + constrain_best_ambiguity_integer[sys], + sys_options, + {"@ constrain_best_ambiguity_integer"}, + "Constrain the best ambiguity of a sys/code pair to an integer once" + ); + tryGetFromYaml( + constrain_clock[sys], + sys_options, + {"@ constrain_clock"}, + "ID of a sat/rec for constraining its clock" + ); + tryGetFromYaml( + constrain_phase_bias[sys], + sys_options, + {"@ constrain_phase_bias"}, + "ID of a sat/rec for constraining its phase bias" + ); + tryGetFromYaml( + eph_time_delay[sys], + sys_options, + {"@ eph_time_delay"}, + "Time delay for Broadcast Ephmeris when simulating real-time in " + "post-process" + ); + } + } + + // epoch_control + { + auto epoch_control = stringsToYamlObject( + processing_options, + {"0! epoch_control"}, + "Specifies the rate and duration of data processing" + ); + + int i = 0; + + string startStr; + string stopStr; + bool found = tryGetFromAny( + epoch_interval, + commandOpts, + epoch_control, + {"! epoch_interval"}, + "Desired time step between each processing epoch" + ); + tryGetFromAny( + epoch_tolerance, + commandOpts, + epoch_control, + {"@ epoch_tolerance"}, + "Tolerance of times to add to an epoch (usually half of the original data's " + "sample rate)" + ); + tryGetFromAny( + max_epochs, + commandOpts, + epoch_control, + {"! max_epochs"}, + "Maximum number of epochs to process" + ); + tryGetFromAny( + startStr, + commandOpts, + epoch_control, + {"! start_epoch"}, + "(YYYY-MM-DD hh:mm:ss) The time of the first epoch to process (all " + "observations before this will " + "be skipped)" + ); + tryGetFromAny( + stopStr, + commandOpts, + epoch_control, + {"! end_epoch"}, + "(YYYY-MM-DD hh:mm:ss) The time of the last epoch to process (all observations " + "after this will be " + "skipped)" + ); + + if (!startStr.empty()) + start_epoch = boost::posix_time::time_from_string(startStr); + if (!stopStr.empty()) + end_epoch = boost::posix_time::time_from_string(stopStr); + + if (found) + wait_next_epoch = epoch_interval + 0.05; + + tryGetFromYaml( + sleep_milliseconds, + epoch_control, + {"# sleep_milliseconds"}, + "Time to sleep before checking for new data - lower numbers are associated " + "with high idle cpu usage" + ); + tryGetFromYaml( + wait_next_epoch, + epoch_control, + {"@ wait_next_epoch"}, + "Maximum time for data being processed at an epoch over which PEA start " + "skipping next epoch (will default to epoch_interval+0.05 as an appropriate " + "minimum value for realtime)" + ); + tryGetFromYaml( + max_rec_latency, + epoch_control, + {"@ max_rec_latency"}, + "Maximum time to wait from the reception of the first data of an epoch before " + "skipping receivers with data still unreceived" + ); + tryGetFromYaml( + require_obs, + epoch_control, + {"@ require_obs"}, + "Exit the program if no observation sources are available" + ); + tryGetFromYaml( + assign_closest_epoch, + epoch_control, + {"@ assign_closest_epoch"}, + "Assign observations to the closest epoch - don't skip observations that fall " + "between epochs" + ); + tryGetFromAny( + simulate_real_time, + commandOpts, + epoch_control, + {"@ simulate_real_time"}, + "For RTCM playback - delay processing to match original data rate" + ); + } + + // model_error_handling + { + auto model_error_handling = stringsToYamlObject( + processing_options, + {"5! model_error_handling"}, + "The kalman filter is capable of automatic statistical integrity modelling" + ); + + { + auto meas_deweighting = stringsToYamlObject( + model_error_handling, + {"0! meas_deweighting"}, + "Measurements that are outside the expected confidence bounds may be " + "deweighted so that " + "outliers do not contaminate the filtered solution" + ); + + tryGetFromYaml( + measErrors.enable, + meas_deweighting, + {"! enable"}, + "Enable deweighting of all rejected measurement" + ); + tryGetFromYaml( + measErrors.deweight_factor, + meas_deweighting, + {"! deweight_factor"}, + "Factor to downweight the variance of measurements with statistically " + "detected errors" + ); + } + + { + auto state_deweighting = stringsToYamlObject( + model_error_handling, + {"0! state_deweighting"}, + "Any \"state\" errors cause deweighting of all measurements that reference " + "the state" + ); + + tryGetFromYaml( + stateErrors.enable, + state_deweighting, + {"! enable"}, + "Enable deweighting of all referencing measurements" + ); + tryGetFromYaml( + stateErrors.deweight_factor, + state_deweighting, + {"! deweight_factor"}, + "Factor to downweight the variance of measurements with statistically " + "detected errors" + ); + } + + { + auto error_accumulation = stringsToYamlObject( + model_error_handling, + {"0! error_accumulation"}, + "Any receivers or satellites that are consistently getting many " + "measurement rejections may be " + "reinitialiased" + ); + + tryGetFromYaml( + errorAccumulation.enable, + error_accumulation, + {"! enable"}, + "Enable reinitialisation of receivers, satellites, or individual states " + "upon many rejections" + ); + tryGetFromYaml( + errorAccumulation.receiver_error_count_threshold, + error_accumulation, + {"! receiver_error_count_threshold"}, + "Number of referencing measurement errors for a receiver to be considered " + "as a receiver error " + "for a single epoch" + ); + tryGetFromYaml( + errorAccumulation.receiver_error_epochs_threshold, + error_accumulation, + {"! receiver_error_epochs_threshold"}, + "Number of consecutive epochs with receiver in error before it is removed " + "and reinitialised" + ); + tryGetFromYaml( + errorAccumulation.satellite_error_count_threshold, + error_accumulation, + {"! satellite_error_count_threshold"}, + "Number of referencing measurement errors for a satellite to be considered " + "as a satellite " + "error for a single epoch" + ); + tryGetFromYaml( + errorAccumulation.satellite_error_epochs_threshold, + error_accumulation, + {"! satellite_error_epochs_threshold"}, + "Number of consecutive epochs with satellite in error before it is " + "reinitialised using the " + "orbit_errors configs" + ); + tryGetFromYaml( + errorAccumulation.state_error_count_threshold, + error_accumulation, + {"! state_error_count_threshold"}, + "Number of referencing measurement errors for a state to be considered as " + "a state error for a " + "single epoch" + ); + } + + { + auto satellite_errors = stringsToYamlObject( + model_error_handling, + {"2@ satellite_errors"}, + "Orbital states that are not consistent with measurements may be " + "reinitialised to allow for " + "dynamic maneuvers" + ); + + tryGetFromYaml( + satelliteErrors.enable, + satellite_errors, + {"@ enable"}, + "Enable applying process noise impulses to satellites upon state errors" + ); + tryGetFromYaml( + satelliteErrors.clk_proc_noise, + satellite_errors, + {"@ clk_process_noise"}, + "Sigma to apply to satellite clock states as reinitialisation" + ); + tryGetFromYaml( + satelliteErrors.pos_proc_noise, + satellite_errors, + {"@ pos_process_noise"}, + "Sigma to apply to orbital position states as reinitialisation" + ); + tryGetFromYaml( + satelliteErrors.vel_proc_noise, + satellite_errors, + {"@ vel_process_noise"}, + "Sigma to apply to orbital velocity states as reinitialisation" + ); + tryGetFromYaml( + satelliteErrors.vel_proc_noise_trail, + satellite_errors, + {"@ vel_process_noise_trail"}, + "Initial sigma for exponentially decaying noise to apply for subsequent " + "epochs as soft " + "reinitialisation" + ); + tryGetFromYaml( + satelliteErrors.vel_proc_noise_trail_tau, + satellite_errors, + {"@ vel_process_noise_trail_tau"}, + "Time constant for exponentially decaying noise" + ); + } + + { + auto ambiguities = stringsToYamlObject( + model_error_handling, + {"1! ambiguities"}, + "Cycle slips in ambiguities are primary cause of incorrect gnss modelling " + "and may be " + "reinitialised" + ); + + tryGetFromYaml( + ambErrors.phase_reject_limit, + ambiguities, + {"! phase_reject_limit"}, + "Maximum number of phase measurements to reject before the ambiguity " + "associated with the " + "measurement is reset." + ); + + tryGetFromYaml( + ambErrors.resetOnSlip.LLI, + ambiguities, + {"@ reset_on", "@ lli"}, + "Reset ambiguities if LLI test is detecting a slip" + ); + tryGetFromYaml( + ambErrors.resetOnSlip.retrack, + ambiguities, + {"@ reset_on", "@ retrack"}, + "Reset ambiguities on retrack test is detecting a slip" + ); + tryGetFromYaml( + ambErrors.resetOnSlip.GF, + ambiguities, + {"@ reset_on", "@ gf"}, + "Reset ambiguities if GF test is detecting a slip" + ); + tryGetFromYaml( + ambErrors.resetOnSlip.MW, + ambiguities, + {"@ reset_on", "@ mw"}, + "Reset ambiguities if MW test is detecting a slip" + ); + tryGetFromYaml( + ambErrors.resetOnSlip.SCDIA, + ambiguities, + {"@ reset_on", "@ scdia"}, + "Reset ambiguities if SCDIA test is detecting a slip" + ); + tryGetFromYaml( + ambErrors.resetOnSlip.single_freq, + ambiguities, + {"@ reset_on", "@ single_freq"}, + "Reset ambiguities on signle frequency detection" + ); + } + + { + auto ionospheric_components = + stringsToYamlObject(model_error_handling, {"1! ionospheric_components"}); + + tryGetFromYaml( + ionErrors.outage_reset_limit, + ionospheric_components, + {"! outage_reset_limit"}, + "Maximum number of seconds without measurements before the ionosphere " + "associated with the " + "measurement is reset." + ); + } + + { + auto exclusions = stringsToYamlObject( + model_error_handling, + {"1@ exclusions"}, + "Cycle slips may be detected by the preprocessor and measurements rejected " + "or ambiguities " + "reinitialised" + ); + + tryGetFromYaml( + exclude.bad_spp, + exclusions, + {"@ bad_spp"}, + "Exclude measurements that were associated with failed SPP" + ); + tryGetFromYaml( + exclude.config, + exclusions, + {"@ config"}, + "Exclude measurements that are configured as exclusions" + ); + tryGetFromYaml( + exclude.eclipse, + exclusions, + {"@ eclipse"}, + "Exclude measurements that are in eclipse" + ); + tryGetFromYaml( + exclude.elevation, + exclusions, + {"@ elevation"}, + "Exclude measurements that fall below elevation mask" + ); + tryGetFromYaml( + exclude.outlier, + exclusions, + {"@ outlier"}, + "Exclude measurements that were rejected as SPP outliers" + ); + tryGetFromYaml( + exclude.system, + exclusions, + {"@ system"}, + "Exclude measurements that have been excluded by system configs" + ); + tryGetFromYaml( + exclude.svh, + exclusions, + {"@ svh"}, + "Exclude measurements that are not specified as healthy" + ); + tryGetFromYaml( + exclude.LLI, + exclusions, + {"@ lli"}, + "Exclude measurements that fail LLI slip test in preprocessor" + ); + tryGetFromYaml( + exclude.retrack, + exclusions, + {"@ retrack"}, + "Exclude measurements that fail retrack slip test in preprocessor" + ); + tryGetFromYaml( + exclude.single_freq, + exclusions, + {"@ single_freq"}, + "Exclude measurements on signle frequency" + ); + tryGetFromYaml( + exclude.GF, + exclusions, + {"@ gf"}, + "Exclude measurements that fail GF slip test in preprocessor" + ); + tryGetFromYaml( + exclude.MW, + exclusions, + {"@ mw"}, + "Exclude measurements that fail MW slip test in preprocessor" + ); + tryGetFromYaml( + exclude.SCDIA, + exclusions, + {"@ scdia"}, + "Exclude measurements that fail SCDIA test in preprocessor" + ); + } + + // { + // auto clocks = stringsToYamlObject(model_error_handling, {"@ + // clocks"}, "Error responses specific to clock states"); + // + // tryGetFromYaml(reinit_on_clock_error, clocks, {"@ + // reinit_on_clock_error" }, "Any clock \"state\" errors cause removal and + // reinitialisation of the clocks and all associated ambiguities"); + // } + } + + auto getFilterOptions = [&](NodeStack& nodeStack, FilterOptions& filterOpts) + { + tryGetEnumOpt( + filterOpts.lsq_inverter, + nodeStack, + {"@ lsq_inverter"}, + "Inverter to be used within the least squares estimater, which may provide " + "different performance " + "outcomes in terms of processing time and accuracy and stability." + ); + + auto outlier_screening = stringsToYamlObject( + nodeStack, + {"! outlier_screening"}, + "Statistical checks allow for detection of outliers that exceed their " + "confidence intervals." + ); + + { + auto chi_sqaure = stringsToYamlObject(outlier_screening, {"! chi_square"}); + + tryGetFromYaml( + filterOpts.chiSquareTest.enable, + chi_sqaure, + {"@ enable"}, + "Enable Chi-square test" + ); + tryGetEnumOpt( + filterOpts.chiSquareTest.mode, + chi_sqaure, + {"@ mode"}, + "Chi-square test mode" + ); + tryGetFromYaml( + filterOpts.chiSquareTest.sigma_threshold, + chi_sqaure, + {"@ sigma_threshold"}, + "Chi-square test threshold in terms of 'times of sigma'" + ); + } + + { + auto leastSquare = stringsToYamlObject(outlier_screening, {"! least_square"}); + + tryGetFromYaml( + filterOpts.lsqOpts.max_iterations, + leastSquare, + {"! max_iterations"}, + "Maximum number of measurements to exclude using postfit checks in least " + "squares" + ); + tryGetFromYaml( + filterOpts.lsqOpts.sigma_check, + leastSquare, + {"@ sigma_check"}, + "Enable sigma check" + ); + tryGetFromYaml( + filterOpts.lsqOpts.omega_test, + leastSquare, + {"@ omega_test"}, + "Enable omega-test" + ); + bool found = tryGetFromYaml( + filterOpts.lsqOpts.meas_sigma_threshold, + leastSquare, + {"@ sigma_threshold"}, + "Sigma threshold" + ); + tryGetFromYaml( + filterOpts.lsqOpts.meas_sigma_threshold, + leastSquare, + {"@ meas_sigma_threshold"}, + "Sigma threshold for measurements" + ); + + if (found) + { + BOOST_LOG_TRIVIAL( + warning + ) << "The yaml option 'least_square:sigma_threshold' is " + "depreciated, better use 'least_square:meas_sigma_threshold' instead"; + } + } + + if (std::get<1>(nodeStack).find("spp") != + string::npos) // Skip setting all other filter options for SPP + { + return; + } + + { + 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, + {"@ joseph_stabilisation"} + ); + tryGetFromYaml( + filterOpts.advanced_postfits, + nodeStack, + {"# advanced_postfits"}, + "Use alternate calculation method to determine postfit residuals" + ); + } + + { + auto prefit = stringsToYamlObject(outlier_screening, {"! prefit"}); + + tryGetFromYaml( + filterOpts.prefitOpts.max_iterations, + prefit, + {"! max_iterations"}, + "Maximum number of measurements to exclude using prefit checks before " + "attempting to filter" + ); + tryGetFromYaml( + filterOpts.prefitOpts.sigma_check, + prefit, + {"@ sigma_check"}, + "Enable sigma check" + ); + tryGetFromYaml( + filterOpts.prefitOpts.omega_test, + prefit, + {"@ omega_test"}, + "Enable omega-test" + ); + bool found = tryGetFromYaml( + filterOpts.prefitOpts.state_sigma_threshold, + prefit, + {"@ sigma_threshold"}, + "Sigma threshold" + ); + tryGetFromYaml( + filterOpts.prefitOpts.meas_sigma_threshold, + prefit, + {"@ sigma_threshold"}, + "Sigma threshold" + ); + tryGetFromYaml( + filterOpts.prefitOpts.state_sigma_threshold, + prefit, + {"@ state_sigma_threshold"}, + "Sigma threshold for states" + ); + tryGetFromYaml( + filterOpts.prefitOpts.meas_sigma_threshold, + prefit, + {"@ meas_sigma_threshold"}, + "Sigma threshold for measurements" + ); + + if (found) + { + BOOST_LOG_TRIVIAL(warning) + << "The yaml option 'prefit:sigma_threshold' is depreciated, " + "better use " + "'prefit:state_sigma_threshold' and 'prefit:meas_sigma_threshold' " + "instead"; + } + } + + { + auto postfit = stringsToYamlObject(outlier_screening, {"! postfit"}); + + tryGetFromYaml( + filterOpts.postfitOpts.max_iterations, + postfit, + {"! max_iterations"}, + "Maximum number of measurements to exclude using postfit checks while " + "iterating filter" + ); + tryGetFromYaml( + filterOpts.postfitOpts.sigma_check, + postfit, + {"@ sigma_check"}, + "Enable sigma check" + ); + tryGetFromYaml( + filterOpts.postfitOpts.omega_test, + postfit, + {"@ omega_test"}, + "Enable omega-test" + ); + bool found = tryGetFromYaml( + filterOpts.postfitOpts.state_sigma_threshold, + postfit, + {"@ sigma_threshold"}, + "Sigma threshold" + ); + tryGetFromYaml( + filterOpts.postfitOpts.meas_sigma_threshold, + postfit, + {"@ sigma_threshold"}, + "Sigma threshold" + ); + tryGetFromYaml( + filterOpts.postfitOpts.state_sigma_threshold, + postfit, + {"@ state_sigma_threshold"}, + "Sigma threshold for states" + ); + tryGetFromYaml( + filterOpts.postfitOpts.meas_sigma_threshold, + postfit, + {"@ meas_sigma_threshold"}, + "Sigma threshold for measurements" + ); + tryGetFromYaml( + filterOpts.chiSquareTest.sigma_threshold, + postfit, + {"@ sigma_threshold"}, + "Sigma threshold" + ); + + if (found) + { + BOOST_LOG_TRIVIAL(warning) + << "The yaml option 'postfit:sigma_threshold' is depreciated, " + "better use " + "'postfit:state_sigma_threshold' and 'postfit:meas_sigma_threshold' " + "instead"; + } + } + + { + auto rts = stringsToYamlObject( + nodeStack, + {"@ rts"}, + "RTS allows reverse smoothing of estimates such that early estimates can " + "make use of later " + "data." + ); + + tryGetFromYaml( + process_rts, + rts, + {"0! enable"}, + "Perform backward smoothing of states to improve precision of earlier " + "states" + ); + tryGetFromYaml( + filterOpts.rts_lag, + rts, + {"@ 1 lag"}, + "Number of epochs to use in RTS smoothing. Negative numbers indicate full " + "reverse smoothing." + ); + tryGetFromYaml( + filterOpts.rts_interval, + rts, + {"# interval"}, + "Number of seconds to use between fixed lag in RTS smoothing." + ); + conditionalPrefix( + "", + pppOpts.rts_directory, + tryGetFromYaml( + filterOpts.rts_directory, + rts, + {"@ directory"}, + "Directory for rts intermediate files" + ) + ); + conditionalPrefix( + "", + pppOpts.rts_filename, + tryGetFromYaml( + filterOpts.rts_filename, + rts, + {"@ filename"}, + "Base filename for rts intermediate files" + ) + ); + tryGetFromYaml( + filterOpts.queue_rts_outputs, + rts, + {"@ queue_outputs"}, + "Queue rts outputs so that processing is not limited by IO bandwidth" + ); + tryGetFromYaml( + filterOpts.rts_smoothed_suffix, + rts, + {"@ suffix"}, + "Suffix to be applied to smoothed versions of files" + ); + tryGetFromYaml( + filterOpts.rts_regularisation, + rts, + {"@ regularisation"}, + "Regularisation term for RTS smoothing; has to be very very small (e.g. " + "1e-12)" + ); + } + }; + + // minimum_constraints + { + auto minimum_constraints = stringsToYamlObject( + processing_options, + {"5! minimum_constraints"}, + "Receiver coodinates may be aligned to reference frames with minimal external " + "constraints" + ); + + tryGetFromYaml( + process_minimum_constraints, + minimum_constraints, + {"0! enable"}, + "Transform states by minimal constraints to selected receiver coordinates" + ); + + tryGetKalmanFromYaml( + minconOpts.delay, + minimum_constraints, + "1! delay", + "Estimation and application of clock delay adjustment" + ); + tryGetKalmanFromYaml( + minconOpts.scale, + minimum_constraints, + "1! scale", + "Estimation and application of scaling factor" + ); + tryGetKalmanFromYaml( + minconOpts.rotation, + minimum_constraints, + "1! rotation", + "Estimation and application of angular offsets" + ); + tryGetKalmanFromYaml( + minconOpts.translation, + minimum_constraints, + "1! translation", + "Estimation and application of CoG offsets" + ); + + tryGetKalmanFromYaml( + minconOpts.delay_rate, + minimum_constraints, + "1! delay_rate", + "Estimation and application of clock delay adjustment rate" + ); + tryGetKalmanFromYaml( + minconOpts.scale_rate, + minimum_constraints, + "1! scale_rate", + "Estimation and application of scaling factor rate" + ); + tryGetKalmanFromYaml( + minconOpts.rotation_rate, + minimum_constraints, + "1! rotation_rate", + "Estimation and application of angular offsets rate" + ); + tryGetKalmanFromYaml( + minconOpts.translation_rate, + minimum_constraints, + "1! translation_rate", + "Estimation and application of CoG offsets rate" + ); + + tryGetFromYaml( + minconOpts.once_per_epoch, + minimum_constraints, + {"2@ once_per_epoch"}, + "Perform minimum constraints on a temporary filter and output results once per " + "epoch" + ); + tryGetFromYaml( + minconOpts.full_vcv, + minimum_constraints, + {"2@ full_vcv"}, + "! experimental ! Use full VCV for measurement noise in minimum constraints " + "filter" + ); + tryGetFromYaml( + minconOpts.constrain_orbits, + minimum_constraints, + {"2@ constrain_orbits"}, + "Enforce rigid transformations of orbital states" + ); + tryGetEnumOpt( + minconOpts.application_mode, + minimum_constraints, + {"2@ application_mode"}, + "Method of transforming positions " + ); + tryGetFromYaml( + minconOpts.transform_unweighted, + minimum_constraints, + {"2@ transform_unweighted"}, + "Add design entries for transformation of positions without weighting" + ); + + getFilterOptions(minimum_constraints, minconOpts); + } + + // ppp_filter + { + auto ppp_filter = stringsToYamlObject( + processing_options, + {"4! ppp_filter"}, + "Configurations for the kalman filter and its sub processes" + ); + + tryGetFromYaml( + pppOpts.simulate_filter_only, + ppp_filter, + {"@ simulate_filter_only"}, + "Residuals will be calculated, but no adjustments to state or covariances will " + "be applied" + ); + tryGetFromYaml( + pppOpts.assume_linearity, + ppp_filter, + {"@ assume_linearity"}, + "Residuals will be adjusted during measurement combination rather than " + "performing 2 seperate state " + "transitions" + ); + + tryGetFromYaml(pppOpts.chunk_size, ppp_filter, {"@ chunking", "@ size"}); + tryGetFromYaml( + pppOpts.receiver_chunking, + ppp_filter, + {"@ chunking", "@ by_receiver"}, + "Split large filter and measurement matrices blockwise by receiver ID to " + "improve processing speed" + ); + + tryGetFromYaml( + pppOpts.filter_reset_enable, + ppp_filter, + {"@ periodic_reset", "@ enable"}, + "Enable periodic reset of filter states" + ); + tryGetFromYaml( + pppOpts.reset_interval, + ppp_filter, + {"@ periodic_reset", "@ interval"}, + "Interval between reset of filter states" + ); + tryGetFromYaml( + pppOpts.reset_epochs, + ppp_filter, + {"@ periodic_reset", "@ times"}, + "Time to reset filter states in seconds of day [GPST]" + ); + tryGetEnumVec( + pppOpts.reset_states, + ppp_filter, + {"@ periodic_reset", "@ states"}, + "States to remove for periodic reset" + ); + + // ionospheric_component + { + auto ionospheric_components = stringsToYamlObject( + ppp_filter, + {"! ionospheric_components"}, + "Slant ionospheric components" + ); + + tryGetEnumOpt( + pppOpts.ionoOpts.corr_mode, + ionospheric_components, + {"@ corr_mode"} + ); + tryGetFromYaml( + pppOpts.ionoOpts.common_ionosphere, + ionospheric_components, + {"! common_ionosphere"}, + "Use the same ionosphere state for code and phase observations" + ); + tryGetFromYaml( + pppOpts.ionoOpts.use_if_combo, + ionospheric_components, + {"! use_if_combo"}, + "Combine 'uncombined' measurements to simulate an ionosphere-free solution" + ); + tryGetFromYaml( + pppOpts.ionoOpts.use_gf_combo, + ionospheric_components, + {"! use_gf_combo"}, + "Combine 'uncombined' measurements to simulate a geometry-free solution" + ); + } + + getFilterOptions(ppp_filter, pppOpts); + } + + // ion_filter + { + auto ion_filter = stringsToYamlObject( + processing_options, + {"5@ ion_filter"}, + "Configurations for the ionospheric model kalman filter and its sub processes" + ); + + tryGetEnumOpt(ionModelOpts.model, ion_filter, {"@ model"}); + tryGetFromYaml( + ionModelOpts.function_order, + ion_filter, + {"@ function_order"}, + "Maximum order of Spherical harmonics for Ionospheric mapping" + ); + tryGetFromYaml( + ionModelOpts.function_degree, + ion_filter, + {"@ function_degree"}, + "Maximum degree of Spherical harmonics for Ionospheric mapping" + ); + tryGetFromYaml( + ionModelOpts.estimate_sat_dcb, + ion_filter, + {"@ estimate_sat_dcb"}, + "Estimate satellite dcb alongside Ionosphere models, should be false for local " + "STEC" + ); + tryGetFromYaml( + ionModelOpts.use_rotation_mtx, + ion_filter, + {"@ use_rotation_mtx"}, + "Use 3D rotation matrix for spherical harmonics to maintain orientation toward " + "the sun" + ); + tryGetFromYaml( + ionModelOpts.basis_sigma_limit, + ion_filter, + {"@ model_sigma_limit"}, + "Ionosphere states are removed when their sigma exceeds this value" + ); + + bool found = tryGetFromYaml( + ionModelOpts.layer_heights, + ion_filter, + {"@ layer_heights"}, + "List of heights of ionosphere layers to estimate" + ); + if (found) + for (auto& a : ionModelOpts.layer_heights) + { + a *= 1000; // km to m + } + + getFilterOptions(ion_filter, ionModelOpts); + } + + // spp + { + auto spp = stringsToYamlObject( + processing_options, + {"1! spp"}, + "Configurations for the kalman filter and its sub processes" + ); + + tryGetFromYaml( + sppOpts.max_lsq_iterations, + spp, + {"! max_lsq_iterations"}, + "Maximum number of iterations of least squares allowed for convergence" + ); + tryGetFromYaml( + sppOpts.elevation_mask_deg, + spp, + {"! elevation_mask"}, + "Minimum elevation for satellites to be processed" + ); + tryGetFromYaml( + sppOpts.sigma_scaling, + spp, + {"! sigma_scaling"}, + "Scale applied to measurement noise for SPP" + ); + tryGetFromYaml( + sppOpts.always_reinitialise, + spp, + {"@ always_reinitialise"}, + "Reset SPP state to zero to avoid potential for lock-in of bad states" + ); + tryGetEnumOpt(sppOpts.iono_mode, spp, {"@ iono_mode"}); + tryGetEnumVec( + sppOpts.trop_models, + spp, + {"@ trop_models"}, + "List of models to use for troposphere" + ); + + auto outlier_screening = stringsToYamlObject( + spp, + {"! outlier_screening"}, + "Statistical checks allow for detection of outliers that exceed their " + "confidence intervals." + ); + + tryGetFromYaml( + sppOpts.max_gdop, + outlier_screening, + {"@ max_gdop"}, + "Maximum dilution of precision before error is flagged" + ); + + auto raim = stringsToYamlObject( + outlier_screening, + {"@ raim"}, + "Apply Receiver Autonomous Integrity Monitoring (RAIM). When SPP fails further " + "SPP solutions are calculated with subsets of observations with the aim of " + "eliminating a problem satellite" + ); + tryGetFromYaml(sppOpts.raim.enable, raim, {"@ enable"}, "Enable RAIM in SPP"); + tryGetFromYaml( + sppOpts.raim.max_iterations, + raim, + {"@ max_iterations"}, + "Maximum number of measurements to exclude using RAIM" + ); + + getFilterOptions(spp, sppOpts); + } + + // preprocessor + { + auto preprocessor = stringsToYamlObject( + processing_options, + {"1@ preprocessor"}, + "Configurations for the kalman filter and its sub processes" + ); + + tryGetFromYaml( + preprocOpts.preprocess_all_data, + preprocessor, + {"@ preprocess_all_data"} + ); + { + auto cycle_slips = stringsToYamlObject( + preprocessor, + {"2@ cycle_slips"}, + "Cycle slips may be detected by the preprocessor and measurements rejected " + "or ambiguities " + "reinitialised" + ); + + tryGetFromYaml( + preprocOpts.slip_threshold, + cycle_slips, + {"@ slip_threshold"}, + "Value used to determine when a slip has occurred" + ); + tryGetFromYaml( + preprocOpts.mw_proc_noise, + cycle_slips, + {"@ mw_process_noise"}, + "Process noise applied to filtered Melbourne-Wubenna measurements to " + "detect cycle slips" + ); + } + } + + // ambiguity_resolution + { + auto ambiguity_resolution = + stringsToYamlObject(processing_options, {"5@ ambiguity_resolution"}); + + tryGetFromYaml( + ambrOpts.elevation_mask_deg, + ambiguity_resolution, + {"@ elevation_mask"}, + "Minimum satellite elevation to perform ambiguity resolution" + ); + tryGetFromYaml( + ambrOpts.lambda_set, + ambiguity_resolution, + {"@ lambda_set_size"}, + "Maximum numer of candidate sets to be used in lambda_alt2 and lambda_bie modes" + ); + tryGetFromYaml( + ambrOpts.AR_max_itr, + ambiguity_resolution, + {"@ max_rounding_iterations"}, + "Maximum number of rounding iterations performed in iter_rnd and bootst modes" + ); + + tryGetEnumOpt(ambrOpts.mode, ambiguity_resolution, {"@ mode"}); + tryGetFromYaml( + ambrOpts.succsThres, + ambiguity_resolution, + {"@ success_rate_threshold"}, + "Thresold for integer validation, success rate test." + ); + tryGetFromYaml( + ambrOpts.ratioThres, + ambiguity_resolution, + {"@ solution_ratio_threshold"}, + "Thresold for integer validation, distance ratio test." + ); + + tryGetFromYaml( + ambrOpts.once_per_epoch, + ambiguity_resolution, + {"@ once_per_epoch"}, + "Perform ambiguity resolution on a temporary filter and output results once " + "per epoch" + ); + tryGetFromYaml( + ambrOpts.fix_and_hold, + ambiguity_resolution, + {"@ fix_and_hold"}, + "Perform ambiguity resolution and commit results to the main processing filter" + ); + } + + // predictions + { + auto predictions = stringsToYamlObject(processing_options, {"5@ predictions"}); + + tryGetScaledFromYaml( + mongoOpts.prediction_offset, + predictions, + {"4@ offset"}, + {"@ interval_units"} + ); + tryGetScaledFromYaml( + mongoOpts.prediction_interval, + predictions, + {"4@ interval"}, + {"@ interval_units"} + ); + tryGetScaledFromYaml( + mongoOpts.forward_prediction_duration, + predictions, + {"4@ forward_duration"}, + {"@ duration_units"} + ); + tryGetScaledFromYaml( + mongoOpts.reverse_prediction_duration, + predictions, + {"4@ reverse_duration"}, + {"@ duration_units"} + ); + } + + // orbit_propagation + { + auto orbit_propagation = + stringsToYamlObject(processing_options, {"5@ orbit_propagation"}); + + tryGetFromYaml( + propagationOptions.integrator_time_step, + orbit_propagation, + {"@ integrator_time_step"}, + "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" + ); + tryGetFromYaml( + propagationOptions.egm_degree, + orbit_propagation, + {"@ egm_degree"}, + "Degree of spherical harmonics gravity model" + ); + tryGetFromYaml( + propagationOptions.indirect_J2, + orbit_propagation, + {"@ indirect_J2"}, + "J2 acceleration perturbation due to the Sun and Moon" + ); + tryGetFromYaml( + propagationOptions.egm_field, + orbit_propagation, + {"@ egm_field"}, + "Acceleration due to the high degree model of the Earth gravity model (exclude " + "degree 0, made by " + "central_force)" + ); + tryGetFromYaml( + propagationOptions.solid_earth_tide, + orbit_propagation, + {"@ solid_earth_tide"}, + "Model accelerations due to solid earth tides" + ); + tryGetFromYaml( + propagationOptions.ocean_tide, + orbit_propagation, + {"@ ocean_tide"}, + "Model accelerations due to ocean tides model" + ); + tryGetFromYaml( + propagationOptions.atm_tide, + orbit_propagation, + {"@ atm_tide"}, + "Model accelerations due to atmospheric tides model" + ); + tryGetFromYaml( + propagationOptions.pole_tide_ocean, + orbit_propagation, + {"@ pole_tide_ocean"}, + "Model accelerations due to ocean pole tide (degree 2 only)" + ); + tryGetFromYaml( + propagationOptions.pole_tide_solid, + orbit_propagation, + {"@ pole_tide_solid"}, + "Model accelerations due to solid pole tide (degree 2 only)" + ); + tryGetFromYaml( + propagationOptions.aod, + orbit_propagation, + {"@ aod"}, + "Model Atmospheric and Oceanic non tidal accelerations" + ); + tryGetFromYaml( + propagationOptions.central_force, + orbit_propagation, + {"@ central_force"}, + "Acceleration due to the central force" + ); + tryGetFromYaml( + propagationOptions.general_relativity, + orbit_propagation, + {"@ general_relativity"}, + "Model acceleration due general relativisty" + ); + } + + // other + { + vector aliasPairs; + + tryGetFromAny( + aliasPairs, + commandOpts, + processing_options, + {"user_aliases"}, + "User definable alias pairs, eg 'myuser:mypass' (use quotes) will replace " + "instances of " + "with mypass in relevant filenames" + ); + + getUserAliases(aliasPairs); + } + } + + // estimation_parameters + { + auto estimation_parameters = + stringsToYamlObject({yaml, ""}, {estimation_parameters_str}); + auto global_models = stringsToYamlObject(estimation_parameters, {"@ global_models"}); + + tryGetKalmanFromYaml(pppOpts.eop, global_models, "@ eop"); + tryGetKalmanFromYaml(pppOpts.eop_rates, global_models, "@ eop_rates"); + tryGetKalmanFromYaml(ionModelOpts.ion, global_models, "@ ion"); + } + + // mongo + { + auto mongo = stringsToYamlObject( + {yaml, ""}, + {"5! mongo"}, + "Mongo is a database used to store results and intermediate values for later " + "analysis and " + "inter-process communication" + ); + + tryGetEnumOpt( + mongoOpts.enable, + mongo, + {"0! enable"}, + "Enable and connect to mongo database" + ); + tryGetEnumOpt( + mongoOpts.output_measurements, + mongo, + {"1! output_measurements"}, + "Output measurements and their residuals" + ); + tryGetEnumOpt( + mongoOpts.output_components, + mongo, + {"1! output_components"}, + "Output components of measurements" + ); + tryGetEnumOpt( + mongoOpts.output_cumulative, + mongo, + {"1! output_cumulative"}, + "Output cumulative residuals of components of measurements" + ); + tryGetEnumOpt(mongoOpts.output_states, mongo, {"1! output_states"}, "Output states"); + tryGetEnumOpt( + mongoOpts.output_state_covars, + mongo, + {"1! output_state_covars"}, + "Output covariance values of related states" + ); + tryGetEnumOpt(mongoOpts.output_config, mongo, {"2@ output_config"}, "Output config"); + tryGetEnumOpt(mongoOpts.output_trace, mongo, {"2@ output_trace"}, "Output trace"); + tryGetEnumOpt( + mongoOpts.output_test_stats, + mongo, + {"2@ output_test_stats"}, + "Output test statistics" + ); + tryGetEnumOpt( + mongoOpts.output_logs, + mongo, + {"2@ output_logs"}, + "Output console trace and warnings to mongo with timestamps and other metadata" + ); + tryGetEnumOpt( + mongoOpts.output_editing, + mongo, + {"2@ output_editing"}, + "Output additional information on data editing to mongo" + ); + tryGetEnumOpt( + mongoOpts.output_ssr_precursors, + mongo, + {"2@ output_ssr_precursors"}, + "Output orbits, clocks, and bias estimates to allow communication to ssr " + "generating processes" + ); + tryGetEnumOpt( + mongoOpts.delete_history, + mongo, + {"1! delete_history"}, + "Drop the collection in the database at the beginning of the run to only show " + "fresh data" + ); + tryGetEnumOpt( + mongoOpts.cull_history, + mongo, + {"1@ cull_history"}, + "Erase old database objects to limit the size and speed degredation over long runs" + ); + tryGetEnumOpt(mongoOpts.use_predictions, mongo, {"2@ use_predictions"}); + tryGetEnumOpt(mongoOpts.output_predictions, mongo, {"2@ output_predictions"}); + tryGetFromYaml( + mongoOpts.queue_outputs, + mongo, + {"2@ queue_outputs"}, + "Output data in a separate thread - may reduce latency" + ); + tryGetFromYaml( + mongoOpts.min_cull_age, + mongo, + {"2@ min_cull_age"}, + "Age of which to cull history" + ); + + tryGetEnumVec( + mongoOpts.used_predictions, + mongo, + {"@ used_predictions"}, + "Filter states to retrieve from mongo" + ); + tryGetEnumVec( + mongoOpts.sent_predictions, + mongo, + {"@ sent_predictions"}, + "Filter states to predict and send to mongo" + ); + + tryGetFromYaml( + mongoOpts[static_cast(E_Mongo::PRIMARY)].suffix, + mongo, + {"3@ primary_suffix"}, + "Suffix to append to database elements to make distinctions between runs for " + "comparison" + ); + tryGetFromYaml( + mongoOpts[static_cast(E_Mongo::PRIMARY)].database, + mongo, + {"3@ primary_database"} + ); + tryGetFromYaml( + mongoOpts[static_cast(E_Mongo::PRIMARY)].uri, + mongo, + {"3@ primary_uri"}, + "Location and port of the mongo database to connect to" + ); + + tryGetFromYaml( + mongoOpts[static_cast(E_Mongo::SECONDARY)].suffix, + mongo, + {"3@ secondary_suffix"}, + "Suffix to append to database elements to make distinctions between runs for " + "comparison" + ); + tryGetFromYaml( + mongoOpts[static_cast(E_Mongo::SECONDARY)].database, + mongo, + {"3@ secondary_database"} + ); + tryGetFromYaml( + mongoOpts[static_cast(E_Mongo::SECONDARY)].uri, + mongo, + {"3@ secondary_uri"}, + "Location and port of the mongo database to connect to" + ); + } + + // debug + { + auto debug = stringsToYamlObject( + {yaml, ""}, + {"9@ debug"}, + "Debug options are designed for developers and should probably not be used by " + "normal users" + ); + + tryGetFromAny( + fatal_level, + commandOpts, + debug, + {"# fatal_message_level"}, + "Threshold level for exiting the program early (0-2)" + ); + tryGetFromYaml( + check_plumbing, + debug, + {"# check_plumbing"}, + "Debugging option to show sizes of objects in memory to detect leaks" + ); + tryGetFromYaml( + explain_measurements, + debug, + {"# explain_measurements"}, + "Debugging option to show verbose measurement coefficients" + ); + tryGetFromYaml( + retain_rts_files, + debug, + {"# retain_rts_files"}, + "Debugging option to keep rts files for post processing" + ); + tryGetFromYaml( + rts_only, + debug, + {"# rts_only"}, + "Debugging option to only re-run rts from previous run" + ); + tryGetFromYaml( + mincon_only, + debug, + {"# mincon_only"}, + "Debugging option to re-run minimum constraints code" + ); + tryGetFromYaml( + output_mincon, + debug, + {"# output_mincon"}, + "Debugging option to only save pre-minimum constraints filter state" + ); + tryGetFromYaml( + mincon_filename, + debug, + {"# mincon_filename"}, + "Filename of pre-mincon filter state for backup/loading" + ); + tryGetFromYaml(check_broadcast_differences, debug, {"@ check_broadcast_differences"}); + tryGetFromAny(compare_orbits, commandOpts, debug, {"@ compare_orbits"}); + tryGetFromAny(compare_clocks, commandOpts, debug, {"@ compare_clocks"}); + tryGetFromAny(compare_attitudes, commandOpts, debug, {"@ compare_attitudes"}); + } + } + + // tryGetFromYaml(split_sys, outputs, { "split_sys" }); + + // Try to change all filenames to replace etc with other values. + { + replaceTags(config_description); + replaceTags(gnss_obs_root); + replaceTags(pseudo_obs_root); + replaceTags(sat_data_root); + replaceTags(rtcm_inputs_root); + + replaceTags(vmf_files); + globber(vmf_files); + replaceTags(atx_files); + globber(atx_files); + replaceTags(snx_files); + globber(snx_files); + replaceTags(erp_files); + globber(erp_files); + replaceTags(ion_files); + globber(ion_files); + replaceTags(nav_files); + globber(nav_files); + replaceTags(ems_files); + globber(ems_files); + replaceTags(sp3_files); + globber(sp3_files); + replaceTags(dcb_files); + globber(dcb_files); + replaceTags(bsx_files); + globber(bsx_files); + replaceTags(clk_files); + globber(clk_files); + replaceTags(obx_files); + globber(obx_files); + replaceTags(sid_files); + globber(sid_files); + replaceTags(cmc_files); + globber(cmc_files); + replaceTags(com_files); + globber(com_files); + replaceTags(crd_files); + globber(crd_files); + replaceTags(egm_files); + globber(egm_files); + replaceTags(igrf_files); + globber(igrf_files); + replaceTags(hfeop_files); + globber(hfeop_files); + replaceTags(gpt2grid_files); + globber(gpt2grid_files); + replaceTags(orography_files); + globber(orography_files); + replaceTags(atm_reg_definitions); + globber(atm_reg_definitions); + replaceTags(pseudo_filter_files); + globber(pseudo_filter_files); + replaceTags(space_weather_files); + globber(space_weather_files); + replaceTags(planetary_ephemeris_files); + globber(planetary_ephemeris_files); + replaceTags(ocean_tide_potential_files); + globber(ocean_tide_potential_files); + replaceTags(atmos_tide_potential_files); + globber(atmos_tide_potential_files); + replaceTags(ocean_tide_loading_blq_files); + globber(ocean_tide_loading_blq_files); + replaceTags(atmos_tide_loading_blq_files); + globber(atmos_tide_loading_blq_files); + replaceTags(ocean_pole_tide_loading_files); + globber(ocean_pole_tide_loading_files); + replaceTags(atmos_ocean_dealiasing_files); + globber(atmos_ocean_dealiasing_files); + replaceTags(ocean_pole_tide_potential_files); + globber(ocean_pole_tide_potential_files); + + replaceTags(rnx_inputs); + globber(rnx_inputs); + replaceTags(ubx_inputs); + globber(ubx_inputs); + replaceTags(custom_inputs); + globber(custom_inputs); + replaceTags(obs_rtcm_inputs); + globber(obs_rtcm_inputs); + replaceTags(nav_rtcm_inputs); + globber(nav_rtcm_inputs); + replaceTags(qzs_rtcm_inputs); + globber(qzs_rtcm_inputs); + replaceTags(pseudo_sp3_inputs); + globber(pseudo_sp3_inputs); + replaceTags(pseudo_snx_inputs); + globber(pseudo_snx_inputs); + + replaceTags(sp3_directory); + replaceTags(sp3_filename); + replaceTags(erp_directory); + replaceTags(erp_filename); + replaceTags(gpx_directory); + replaceTags(gpx_filename); + replaceTags(pos_directory); + replaceTags(pos_filename); + replaceTags(spp_directory); + replaceTags(spp_filename); + replaceTags(log_directory); + replaceTags(log_filename); + replaceTags(cost_directory); + replaceTags(cost_filename); + replaceTags(sinex_directory); + replaceTags(sinex_filename); + replaceTags(ionex_directory); + replaceTags(ionex_filename); + replaceTags(orbex_directory); + replaceTags(orbex_filename); + replaceTags(clocks_directory); + replaceTags(clocks_filename); + replaceTags(slr_obs_directory); + replaceTags(slr_obs_filename); + replaceTags(ionstec_directory); + replaceTags(ionstec_filename); + replaceTags(raw_ubx_directory); + replaceTags(raw_ubx_filename); + replaceTags(rtcm_nav_directory); + replaceTags(rtcm_nav_filename); + replaceTags(rtcm_obs_directory); + replaceTags(rtcm_obs_filename); + replaceTags(orbit_ics_directory); + replaceTags(orbit_ics_filename); + replaceTags(ntrip_log_directory); + replaceTags(ntrip_log_filename); + replaceTags(rinex_obs_directory); + replaceTags(rinex_obs_filename); + replaceTags(rinex_nav_directory); + replaceTags(rinex_nav_filename); + replaceTags(raw_custom_directory); + replaceTags(raw_custom_filename); + replaceTags(bias_sinex_directory); + replaceTags(bias_sinex_filename); + replaceTags(trop_sinex_directory); + replaceTags(trop_sinex_filename); + replaceTags(pppOpts.rts_directory); + replaceTags(pppOpts.rts_filename); + replaceTags(sp3_directory); + replaceTags(predicted_sp3_filename); + replaceTags(ems_directory); + replaceTags(ems_filename); + replaceTags(trace_directory); + replaceTags(receiver_json_filename); + replaceTags(trace_directory); + replaceTags(receiver_trace_filename); + replaceTags(trace_directory); + replaceTags(network_trace_filename); + replaceTags(trace_directory); + replaceTags(satellite_trace_filename); + replaceTags(trace_directory); + replaceTags(ionosphere_trace_filename); + replaceTags(decoded_rtcm_json_directory); + replaceTags(decoded_rtcm_json_filename); + replaceTags(encoded_rtcm_json_directory); + replaceTags(encoded_rtcm_json_filename); + replaceTags(network_statistics_json_directory); + replaceTags(network_statistics_json_filename); + + replaceTags(mongoOpts[static_cast(E_Mongo::PRIMARY)].uri); + replaceTags(mongoOpts[static_cast(E_Mongo::PRIMARY)].suffix); + replaceTags(mongoOpts[static_cast(E_Mongo::PRIMARY)].database); + replaceTags(mongoOpts[static_cast(E_Mongo::SECONDARY)].uri); + replaceTags(mongoOpts[static_cast(E_Mongo::SECONDARY)].suffix); + replaceTags(mongoOpts[static_cast(E_Mongo::SECONDARY)].database); + } + + // get template options + { + SatSys dummySat("G00"); + getSatOpts(dummySat, {"@ L1W"}); + getRecOpts("! global"); + getRecOpts("@ XMPL", {"@GPS", "@ L1W"}); + } + + for (auto& yaml : yamls) + { + string filename; + if (yaml["yaml_filename"]) + filename = yaml["yaml_filename"].as(); + + recurseYaml(filename, yaml); + } + + for (auto& [stack, defaults] : acsConfig.yamlDefaults) + { + if (defaults.comment.empty()) + { + string dummy; + string str = nonNumericStack(stack, dummy); + BOOST_LOG_TRIVIAL(debug) << "Dev: " << str << " has no documentation comment"; + } + } + + if (commandOpts.count("yaml-defaults")) + { + int level = commandOpts["yaml-defaults"].as(); + outputDefaultConfiguration(level); + } sanityChecks(); -// get template options - { - SatSys dummySat("G00"); - getSatOpts(dummySat, {"@ L1W"}); - getRecOpts("! global"); - getRecOpts("@ XMPL", {"@GPS", "@ L1W"}); - } - - for (auto& yaml : yamls) - { - string filename; - if (yaml["yaml_filename"]) - filename = yaml["yaml_filename"].as(); - - recurseYaml(filename, yaml); - } - - for (auto& [stack, defaults] : acsConfig.yamlDefaults) - { - if (defaults.comment.empty()) - { - string dummy; - string str = nonNumericStack(stack, dummy); - BOOST_LOG_TRIVIAL(debug) << "Dev: " << str << " has no documentation comment"; - } - } - - if (commandOpts.count("yaml-defaults")) - { - int level = commandOpts["yaml-defaults"].as(); - outputDefaultConfiguration(level); - } - - return true; + return true; } diff --git a/src/cpp/common/acsConfig.hpp b/src/cpp/common/acsConfig.hpp index 26c0609ac..cc206a036 100644 --- a/src/cpp/common/acsConfig.hpp +++ b/src/cpp/common/acsConfig.hpp @@ -1,1452 +1,1495 @@ - #pragma once +#include #include - -#include - -#include "eigenIncluder.hpp" - -#include -#include #include -#include +#include #include -#include -#include -#include -#include #include +#include +#include #include +#include +#include +#include +#include +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/satSys.hpp" +#include "common/trace.hpp" -using std::unordered_map; -using std::vector; -using std::string; -using std::tuple; -using std::mutex; using std::array; using std::map; +using std::mutex; using std::set; +using std::string; +using std::tuple; +using std::unordered_map; +using std::vector; -#include "satSys.hpp" -#include "trace.hpp" -#include "enums.h" - - -#define PI 3.141592653589793238462643383279502884197169399375105820974 -#define D2R (PI/180.0) ///< deg to rad -#define R2D (180.0/PI) ///< rad to deg - -template -bool isInited( - const BASE& base, - const COMP& comp) +template +bool isInited(const BASE& base, const COMP& comp) { - int offset = (char*)(&comp) - (char*)(&base); + int offset = (char*)(&comp) - (char*)(&base); - auto it = base.initialisedMap.find(offset); - if (it == base.initialisedMap.end()) - { - return false; - } + auto it = base.initialisedMap.find(offset); + if (it == base.initialisedMap.end()) + { + return false; + } - auto [dummy, inited] = *it; + auto [dummy, inited] = *it; - return inited; + return inited; } +struct SsrInputOptions +{ + double code_bias_valid_time = 3600; ///< Valid time period of SSR code biases + double phase_bias_valid_time = 300; ///< Valid time period of SSR phase biases + double global_vtec_valid_time = 300; ///< Valid time period of SSR global Ionospheres + double local_stec_valid_time = 120; ///< Valid time period of SSR local Ionospheres + double local_trop_valid_time = 120; ///< Valid time period of SSR local Tropospheres + bool one_freq_phase_bias = false; +}; + +struct SbsInputOptions +{ + string host; ///< hostname is passed as acsConfig.sisnet_inputs + string port; ///< port of SISNet steam + string user; ///< Username for SISnet stream access + string pass; ///< Password for SISnet stream access + int prn; ///< prn of SBAS satellite + int freq; ///< freq (L1 or L5) of SBAS channel + int mt0 = 0; ///< message that is replaced by MT0 (use 65 for SouthPAN L5) + int ems_year = 2059; ///< reference year for EMS files (2059 should work between 2009 and 2158) + bool use_do259 = false; ///< Use original standard DO-259, intead of DO-259A, for DFMC, Keep as + ///< 'false' unless using DFMC + bool pvs_on_dfmc = false; ///< Interpret DFMC messages as PVS messages + bool prec_aproach = true; ///< Limit SBAS solutions to precision approach (which limits maximum + ///< SBAS correction age) + bool dfmc_uire = false; ///< Ionosphere residual from IF combination (use with DFMC only) + int smth_win = + -1; ///< Smoothing window to be used by SBAS (100, 1 second samples are normally used) + double smth_out = 10; ///< Maximum outage to reset smoothing +}; + /** Input source filenames and directories -*/ + */ struct InputOptions { - string inputs_root = "."; - - string gnss_obs_root = ""; - string pseudo_obs_root = ""; - string custom_pseudo_obs_root = ""; - string sat_data_root = ""; - string rtcm_inputs_root = ""; - string sisnet_inputs_root = ""; - - vector atx_files; - vector snx_files; - vector nav_files; - vector sp3_files; - vector clk_files; - vector obx_files; - vector sid_files; - vector com_files; - vector crd_files; - vector vmf_files; - vector erp_files; - vector dcb_files; - vector bsx_files; - vector ion_files; - vector igrf_files; - vector egm_files; - vector cmc_files; - vector hfeop_files; - vector gpt2grid_files; - vector orography_files; - vector pseudo_filter_files; - vector atm_reg_definitions; - vector planetary_ephemeris_files; - vector ocean_tide_potential_files; - vector atmos_tide_potential_files; - vector ocean_tide_loading_blq_files; - vector atmos_tide_loading_blq_files; - vector ocean_pole_tide_loading_files; - vector atmos_oceean_dealiasing_files; - vector ocean_pole_tide_potential_files; - - vector sisnet_inputs; - vector nav_rtcm_inputs; - vector qzs_rtcm_inputs; - - map> rnx_inputs; - map> ubx_inputs; - map> custom_inputs; - map> obs_rtcm_inputs; - map> pseudo_sp3_inputs; - map> pseudo_snx_inputs; - - - vector atl_blq_row_order = {E_TidalComponent::UP, E_TidalComponent::EAST, E_TidalComponent::NORTH}; - vector otl_blq_row_order = {E_TidalComponent::UP, E_TidalComponent::WEST, E_TidalComponent::SOUTH}; - - vector atl_blq_col_order = - { - E_TidalConstituent::S1, - E_TidalConstituent::S2 - }; - - vector otl_blq_col_order = - { - E_TidalConstituent::M2, - E_TidalConstituent::S2, - E_TidalConstituent::N2, - E_TidalConstituent::K2, - E_TidalConstituent::K1, - E_TidalConstituent::O1, - E_TidalConstituent::P1, - E_TidalConstituent::Q1, - E_TidalConstituent::MF, - E_TidalConstituent::MM, - E_TidalConstituent::SSA - }; - - - bool eci_pseudoobs = false; - - string stream_user; - string stream_pass; + string inputs_root = "."; + + bool allow_missing_inputs = false; + + string gnss_obs_root = ""; + string pseudo_obs_root = ""; + string custom_pseudo_obs_root = ""; + string sat_data_root = ""; + string rtcm_inputs_root = ""; + string sisnet_inputs_root = ""; + + vector atx_files; + vector snx_files; + vector nav_files; + vector ems_files; + vector sp3_files; + vector clk_files; + vector obx_files; + vector sid_files; + vector com_files; + vector crd_files; + vector vmf_files; + vector erp_files; + vector dcb_files; + vector bsx_files; + vector ion_files; + vector igrf_files; + vector egm_files; + vector cmc_files; + vector hfeop_files; + vector gpt2grid_files; + vector orography_files; + vector pseudo_filter_files; + vector atm_reg_definitions; + vector space_weather_files; + vector planetary_ephemeris_files; + vector ocean_tide_potential_files; + vector atmos_tide_potential_files; + vector ocean_tide_loading_blq_files; + vector atmos_tide_loading_blq_files; + vector ocean_pole_tide_loading_files; + vector atmos_ocean_dealiasing_files; + vector ocean_pole_tide_potential_files; + + vector sisnet_inputs; + vector nav_rtcm_inputs; + vector qzs_rtcm_inputs; + + map> rnx_inputs; + map> ubx_inputs; + map> custom_inputs; + map> obs_rtcm_inputs; + map> pseudo_sp3_inputs; + map> pseudo_snx_inputs; + + vector atl_blq_row_order = { + E_TidalComponent::UP, + E_TidalComponent::EAST, + E_TidalComponent::NORTH + }; + vector otl_blq_row_order = { + E_TidalComponent::UP, + E_TidalComponent::WEST, + E_TidalComponent::SOUTH + }; + + vector atl_blq_col_order = {E_TidalConstituent::S1, E_TidalConstituent::S2}; + + vector otl_blq_col_order = { + E_TidalConstituent::M2, + E_TidalConstituent::S2, + E_TidalConstituent::N2, + E_TidalConstituent::K2, + E_TidalConstituent::K1, + E_TidalConstituent::O1, + E_TidalConstituent::P1, + E_TidalConstituent::Q1, + E_TidalConstituent::MF, + E_TidalConstituent::MM, + E_TidalConstituent::SSA + }; + + bool eci_pseudoobs = false; + + string stream_user; + string stream_pass; + + double sbs_time_delay = 0; + + SsrInputOptions ssrInOpts; + SbsInputOptions sbsInOpts; }; struct IonexOptions { - double lat_centre = 0; - double lon_centre = 0; - double lat_width = 90; - double lon_width = 90; - double lat_res = 10; - double lon_res = 10; - double time_res = 900; + double lat_centre = 0; + double lon_centre = 0; + double lat_width = 90; + double lon_width = 90; + double lat_res = 10; + double lon_res = 10; + double time_res = 900; }; /** Enabling and setting destiations of program outputs. -*/ + */ struct OutputOptions { - string outputs_root = "."; - - int fatal_level = 0; - double rotate_period = 60*60*24; - - int trace_level = 0; - bool output_receiver_trace = false; - bool output_network_trace = false; - bool output_ionosphere_trace = false; - bool output_satellite_trace = false; - bool output_json_trace = false; - string trace_directory = ""; - string receiver_trace_filename = "/-.trace"; - string network_trace_filename = "/-.trace"; - string ionosphere_trace_filename = "/-.trace"; - string satellite_trace_filename = "/-.trace"; - - bool record_raw_ubx = false; - string raw_ubx_directory = ""; - string raw_ubx_filename = "/--OBS.rtcm"; - - bool record_raw_custom = false; - string raw_custom_directory = ""; - string raw_custom_filename = "/--OBS.custom"; - - bool record_rtcm_obs = false; - bool record_rtcm_nav = false; - string rtcm_obs_directory = ""; - string rtcm_nav_directory = ""; - string rtcm_obs_filename = "/--OBS.rtcm"; - string rtcm_nav_filename = "/--NAV.rtcm"; - - bool output_log = false; - string log_directory = ""; - string log_filename = "/log-.json"; - - bool output_ntrip_log = false; - string ntrip_log_directory = ""; - string ntrip_log_filename = "/ntrip_log-.json"; - - bool output_gpx = false; - string gpx_directory = ""; - string gpx_filename = "/-.gpx"; - - bool output_pos = false; - string pos_directory = ""; - string pos_filename = "/-.pos"; - - string root_stream_url = ""; - - bool output_predicted_states = false; - bool output_initialised_states = false; - bool output_residuals = false; - bool output_residual_chain = true; - - bool output_config = false; - bool colourise_terminal = true; - bool warn_once = true; - - bool output_clocks = false; - vector clocks_receiver_sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; - vector clocks_satellite_sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; - double clocks_output_interval = 1; - string clocks_directory = ""; - string clocks_filename = "/-_.clk"; - - bool output_sp3 = false; - bool output_inertial_orbits = false; - bool output_sp3_velocities = false; - vector sp3_orbit_sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; - vector sp3_clock_sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; - double sp3_output_interval = 1; - string sp3_directory = ""; - string sp3_filename = "/-_-Filt.sp3"; - string predicted_sp3_filename = "/-_-Prop.sp3"; - - bool output_orbex = false; - vector orbex_orbit_sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; - vector orbex_clock_sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; - vector orbex_attitude_sources = {E_Source::NOMINAL}; - double orbex_output_interval = 1; - string orbex_directory = ""; - string orbex_filename = "/-_.obx"; - vector orbex_record_types = {E_OrbexRecord::ATT}; - - - bool split_sys = false; - - bool output_rinex_obs = false; - string rinex_obs_directory = ""; - string rinex_obs_filename = "/-_.O"; - double rinex_obs_version = 3.05; - bool rinex_obs_print_C_code = true; - bool rinex_obs_print_L_code = true; - bool rinex_obs_print_D_code = true; - bool rinex_obs_print_S_code = true; - - bool output_ionex = false; - string ionex_directory = ""; - string ionex_filename = "/-.INX"; - IonexOptions ionexGrid; - - bool output_rinex_nav = false; - string rinex_nav_directory = ""; - string rinex_nav_filename = "/-_nav_.rnx"; - double rinex_nav_version = 3.05; - - bool output_ionstec = false; - string ionstec_directory = ""; - string ionstec_filename = "/-.STEC"; - - bool output_erp = false; - string erp_directory = ""; - string erp_filename = "/-.ERP"; - - bool output_bias_sinex = false; - string bias_sinex_directory = ""; - string bias_sinex_filename = "/-.BIA"; - string bias_time_system = "G"; - - bool output_sinex = false; - string sinex_directory = ""; - string sinex_filename = "/-.snx"; - - bool output_trop_sinex = false; - vector trop_sinex_data_sources = {E_Source::KALMAN}; - string trop_sinex_directory = ""; - string trop_sinex_filename = "/-.tro"; - string trop_sinex_sol_type = "Solution parameters"; - char trop_sinex_obs_code = 'P'; - char trop_sinex_const_code = ' '; - double trop_sinex_version = 2.0; - - - bool output_cost = false; - vector cost_data_sources = {E_Source::KALMAN}; - string cost_directory = ""; - string cost_filename = "/cost_s_t___ga__.dat"; - int cost_time_interval = 900; - string cost_format = "COST-716 V2.2"; - string cost_project = "GA-NRT"; - string cost_status = "TEST"; - string cost_centre = "GA__ Geoscience Aus"; - string cost_method = "GINAN V2"; - string cost_orbit_type = "IGSPRE"; - string cost_met_source = "NONE"; - - bool output_slr_obs = false; - string slr_obs_directory = ""; - string slr_obs_filename = "/.slr_obs"; - - bool output_orbit_ics = false; - string orbit_ics_directory = ""; - string orbit_ics_filename = "/--orbits.yaml"; - - bool output_sbas_ems = false; - string ems_directory = ""; - string ems_filename = "/y/d/h.ems"; - - void defaultOutputOptions() - { - *this = OutputOptions(); - } - - bool output_decoded_rtcm_json = false; - string decoded_rtcm_json_directory = ""; - string decoded_rtcm_json_filename = "/-_rtcm_decoded.json"; - - bool output_encoded_rtcm_json = false; - string encoded_rtcm_json_directory = ""; - string encoded_rtcm_json_filename = "/-_rtcm_encoded.json"; - - bool output_network_statistics_json = false; - string network_statistics_json_directory = ""; - string network_statistics_json_filename = "/-_network_statistics.json"; + string outputs_root = "."; + + int fatal_level = 0; + double rotate_period = 60 * 60 * 24; + + int trace_level = 0; + bool output_receiver_trace = false; + bool output_network_trace = false; + bool output_ionosphere_trace = false; + bool output_satellite_trace = false; + bool output_observations = false; + bool output_json_trace = false; + string trace_directory = ""; + string receiver_trace_filename = "/-.trace"; + string receiver_json_filename = "/-.json"; + string network_trace_filename = "/-.trace"; + string ionosphere_trace_filename = "/-.trace"; + string satellite_trace_filename = "/-.trace"; + + bool record_raw_ubx = false; + string raw_ubx_directory = ""; + string raw_ubx_filename = "/--OBS.rtcm"; + + bool record_raw_custom = false; + string raw_custom_directory = ""; + string raw_custom_filename = "/--OBS.custom"; + + bool record_rtcm_obs = false; + bool record_rtcm_nav = false; + string rtcm_obs_directory = ""; + string rtcm_nav_directory = ""; + string rtcm_obs_filename = "/--OBS.rtcm"; + string rtcm_nav_filename = "/--NAV.rtcm"; + + bool output_log = false; + bool log_json = true; + string log_directory = ""; + string log_filename = "/log-.json"; + + bool output_ntrip_log = false; + string ntrip_log_directory = ""; + string ntrip_log_filename = "/ntrip_log-.json"; + + bool output_gpx = false; + string gpx_directory = ""; + string gpx_filename = "/-.gpx"; + + bool output_pos = false; + string pos_directory = ""; + string pos_filename = "/-.pos"; + + bool output_spp = false; + string spp_directory = ""; + string spp_filename = "/-
    .spp"; + + string root_stream_url = ""; + + bool output_predicted_states = false; + bool output_initialised_states = false; + bool output_residuals = false; + bool output_residual_chain = true; + + bool output_statistics = false; + bool output_summaries = false; + bool output_config = false; + bool colourise_terminal = true; + bool timestamp_console_logs = false; + bool warn_once = true; + + bool output_clocks = false; + vector clocks_receiver_sources = + {E_Source::KALMAN, E_Source::PRECISE, E_Source::SPP, E_Source::BROADCAST}; + vector clocks_satellite_sources = { + E_Source::KALMAN, + E_Source::PRECISE, + E_Source::BROADCAST + }; + double clocks_output_interval = 1; + string clocks_directory = ""; + string clocks_filename = "/-_.clk"; + + bool output_sp3 = false; + bool output_inertial_orbits = false; + bool output_sp3_velocities = false; + vector sp3_orbit_sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; + vector sp3_clock_sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; + double sp3_output_interval = 1; + string sp3_directory = ""; + string sp3_filename = "/-_-Filt.sp3"; + string predicted_sp3_filename = "/-_-Prop.sp3"; + + bool output_orbex = false; + vector orbex_orbit_sources = { + E_Source::KALMAN, + E_Source::PRECISE, + E_Source::BROADCAST + }; + vector orbex_clock_sources = { + E_Source::KALMAN, + E_Source::PRECISE, + E_Source::BROADCAST + }; + vector orbex_attitude_sources = { + E_Source::PRECISE, + E_Source::MODEL, + E_Source::NOMINAL + }; + double orbex_output_interval = 1; + string orbex_directory = ""; + string orbex_filename = "/-_.obx"; + vector orbex_record_types = {E_OrbexRecord::ATT}; + + bool split_sys = false; + + bool output_rinex_obs = false; + string rinex_obs_directory = ""; + string rinex_obs_filename = "/-_.O"; + double rinex_obs_version = 3.05; + bool rinex_obs_print_C_code = true; + bool rinex_obs_print_L_code = true; + bool rinex_obs_print_D_code = true; + bool rinex_obs_print_S_code = true; + + bool output_ionex = false; + string ionex_directory = ""; + string ionex_filename = "/-.INX"; + IonexOptions ionexGrid; + + bool output_rinex_nav = false; + string rinex_nav_directory = ""; + string rinex_nav_filename = "/-_nav_.rnx"; + double rinex_nav_version = 3.05; + + bool output_ionstec = false; + string ionstec_directory = ""; + string ionstec_filename = "/-.STEC"; + + bool output_erp = false; + string erp_directory = ""; + string erp_filename = "/-.ERP"; + + bool output_bias_sinex = false; + string bias_sinex_directory = ""; + string bias_sinex_filename = "/-.BIA"; + string bias_time_system = "G"; + + bool output_sinex = false; + string sinex_directory = ""; + string sinex_filename = "/-.snx"; + + bool output_trop_sinex = false; + vector trop_sinex_data_sources = {E_Source::KALMAN}; + string trop_sinex_directory = ""; + string trop_sinex_filename = "/-.tro"; + string trop_sinex_sol_type = "Solution parameters"; + char trop_sinex_obs_code = 'P'; + char trop_sinex_const_code = ' '; + double trop_sinex_version = 2.0; + + bool output_cost = false; + vector cost_data_sources = {E_Source::KALMAN}; + string cost_directory = ""; + string cost_filename = "/cost_s_t___ga__.dat"; + int cost_time_interval = 900; + string cost_format = "COST-716 V2.2"; + string cost_project = "GA-NRT"; + string cost_status = "TEST"; + string cost_centre = "GA__ Geoscience Aus"; + string cost_method = "GINAN V2"; + string cost_orbit_type = "IGSPRE"; + string cost_met_source = "NONE"; + + bool output_slr_obs = false; + string slr_obs_directory = ""; + string slr_obs_filename = "/.slr_obs"; + + bool output_orbit_ics = false; + string orbit_ics_directory = ""; + string orbit_ics_filename = "/--orbits.yaml"; + + bool output_sbas_ems = false; + string ems_directory = ""; + string ems_filename = "/y/d/h.ems"; + + void defaultOutputOptions() { *this = OutputOptions(); } + + bool output_decoded_rtcm_json = false; + string decoded_rtcm_json_directory = ""; + string decoded_rtcm_json_filename = + "/-_rtcm_decoded.json"; + + bool output_encoded_rtcm_json = false; + string encoded_rtcm_json_directory = ""; + string encoded_rtcm_json_filename = + "/-_rtcm_encoded.json"; + + bool output_network_statistics_json = false; + string network_statistics_json_directory = ""; + string network_statistics_json_filename = + "/-_network_statistics.json"; }; /** Options to be used only for debugging new features -*/ + */ struct DebugOptions { - bool mincon_only = false; - bool output_mincon = false; - string mincon_filename = "preMinconState.bin"; - bool check_plumbing = false; - bool retain_rts_files = false; - bool rts_only = false; - bool explain_measurements = false; - bool compare_orbits = false; - bool compare_clocks = false; - bool compare_attitudes = false; - - bool check_broadcast_differences = false; + bool mincon_only = false; + bool output_mincon = false; + string mincon_filename = "preMinconState.bin"; + bool check_plumbing = false; + bool retain_rts_files = false; + bool rts_only = false; + bool explain_measurements = false; + bool compare_orbits = false; + bool compare_clocks = false; + bool compare_attitudes = false; + + bool check_broadcast_differences = false; }; /** Options for processing SLR observations -*/ + */ struct SlrOptions { - bool process_slr = false; + bool process_slr = false; }; - - struct PreprocOptions { - double slip_threshold = 0.05; - double mw_proc_noise = 0; + double slip_threshold = 0.05; + double mw_proc_noise = 0; - bool preprocess_all_data = true; + bool preprocess_all_data = true; }; struct SlipOptions { - bool LLI = true; - bool GF = true; - bool MW = true; - bool SCDIA = true; + bool LLI = false; + bool GF = true; + bool MW = true; + bool SCDIA = true; + bool retrack = false; + bool single_freq = true; }; -struct ExcludeOptions : SlipOptions +struct ExcludeOptions { - bool bad_spp = true; - bool config = true; - bool eclipse = true; - bool elevation = true; - bool outlier = true; - bool system = true; - bool svh = true; + bool bad_spp = false; + bool config = true; + bool eclipse = true; + bool elevation = true; + bool outlier = true; + bool system = true; + bool svh = true; + // copy of the slip options + bool LLI = true; + bool GF = true; + bool MW = true; + bool SCDIA = true; + bool retrack = true; + bool single_freq = true; }; struct AmbiguityErrorHandler { - int phase_reject_limit = 10; - int outage_reset_limit = 300; + int phase_reject_limit = 10; - SlipOptions resetOnSlip; + SlipOptions resetOnSlip; }; struct IonoErrorHandler { - int outage_reset_limit = 300; + int outage_reset_limit = 300; }; -struct OrbitErrorHandler +struct SatelliteErrorHandler { - bool enable = false; - double pos_proc_noise = 10; - double vel_proc_noise = 5; - double vel_proc_noise_trail = 1; - double vel_proc_noise_trail_tau = 0.05; + bool enable = false; + double pos_proc_noise = 10; + double vel_proc_noise = 5; + double vel_proc_noise_trail = 1; + double vel_proc_noise_trail_tau = 0.05; + double clk_proc_noise = 1000; }; struct StateErrorHandler { - bool enable = true; - double deweight_factor = 1000; + bool enable = true; + double deweight_factor = 1000; }; struct MeasErrorHandler { - bool enable = true; - double deweight_factor = 1000; + bool enable = true; + double deweight_factor = 1000; }; struct ErrorAccumulationHandler { - bool enable = true; - int receiver_error_count_threshold = 4; - int receiver_error_epochs_threshold = 4; + 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; + int state_error_count_threshold = 4; }; - /** Options for the general operation of the software -*/ + */ struct GlobalOptions { - int sleep_milliseconds = 50; - double epoch_interval = 1; - double epoch_tolerance = 0.5; - int max_epochs = 0; - int leap_seconds = -1; - - boost::posix_time::ptime start_epoch { boost::posix_time::not_a_date_time }; - boost::posix_time::ptime end_epoch { boost::posix_time::not_a_date_time }; - - string config_description = "Pea"; - string config_details; - string analysis_agency = "GAA"; - string analysis_centre = "Geoscience Australia"; - string analysis_software = "Ginan"; - string analysis_software_version = "3.0"; - string ac_contact = "clientservices@ga.gov.au"; - string rinex_comment = "Daily 30-sec observations from IGS stations"; - string reference_system = "igb14"; - string time_system = "G"; - string ocean_tide_loading_model = "FES2004"; - string atmospheric_tide_loading_model = "---"; - string geoid_model = "EGM96"; - string gradient_mapping_function = "Chen & Herring, 1992"; - - bool simulate_real_time = false; - - bool process_preprocessor = true; - bool process_spp = true; - bool process_minimum_constraints = false; - bool process_ionosphere = false; - bool process_rts = false; - bool process_ppp = false; - bool process_orbits = false; - - map process_sys; - map solve_amb_for; - bool process_meas[NUM_MEAS_TYPES] = {true, true}; - map reject_eclipse; - - - string pivot_receiver = "NO_PIVOT"; - string pivot_satellite = "NO_PIVOT"; - - bool interpolate_rec_pco = true; - bool auto_fill_pco = true; - bool require_apriori_positions = false; - bool require_site_eccentricity = false; - bool require_sinex_data = false; - bool require_antenna_details = false; - bool require_reflector_com = false; - - - bool use_tgd_bias = false; - - double wait_next_epoch = 0; - double max_rec_latency = 0; - bool require_obs = true; - bool assign_closest_epoch = false; - - bool delete_old_ephemerides = true; - -// bool reinit_on_clock_error = false; - double validity_interval_factor = 10; - - E_OffsetType ssr_input_antenna_offset = E_OffsetType::UNSPECIFIED; - - map> code_priorities; - map used_nav_types = ///< Default observation codes on L1 for IF combination based satellite clocks - { - {E_Sys::GPS, E_NavMsgType::LNAV}, - {E_Sys::GLO, E_NavMsgType::FDMA}, - {E_Sys::GAL, E_NavMsgType::INAV}, - {E_Sys::BDS, E_NavMsgType::D1}, - {E_Sys::QZS, E_NavMsgType::LNAV} - }; - - vector code_priorities_default = - { - E_ObsCode::L1C, - E_ObsCode::L1P, - E_ObsCode::L1Y, - E_ObsCode::L1W, - E_ObsCode::L1M, - E_ObsCode::L1N, - E_ObsCode::L1S, - E_ObsCode::L1L, - E_ObsCode::L1X, - - E_ObsCode::L2W, - E_ObsCode::L2P, - E_ObsCode::L2Y, - E_ObsCode::L2C, - E_ObsCode::L2M, - E_ObsCode::L2N, - E_ObsCode::L2D, - E_ObsCode::L2S, - E_ObsCode::L2L, - E_ObsCode::L2X, - - E_ObsCode::L5I, - E_ObsCode::L5Q, - E_ObsCode::L5X - }; - - double fixed_phase_bias_var = 0.01; - - bool adjust_rec_clocks_by_spp = true; - bool adjust_clocks_for_jumps_only = false; - bool minimise_sat_clock_offsets = false; - bool minimise_sat_orbit_offsets = false; - bool minimise_ionosphere_offsets = false; - - map receiver_amb_pivot; ///< fix one ambiguity to eliminate rank deficiency - map network_amb_pivot; ///< fix ambiguities to eliminate network rank deficiencies - map use_for_iono_model; ///< use system for ionospheric modelling - map use_iono_corrections; ///< use system for ionospheric modelling - - bool common_sat_pco = false; - bool common_rec_pco = false; - bool use_trop_corrections = false; - - //to be removed? - - double predefined_fail = 0.001; /* pre-defined fail-rate (0.01,0.001) */ + int sleep_milliseconds = 50; + double epoch_interval = 1; + double epoch_tolerance = 0.5; + int max_epochs = 0; + int leap_seconds = -1; + + boost::posix_time::ptime start_epoch{boost::posix_time::not_a_date_time}; + boost::posix_time::ptime end_epoch{boost::posix_time::not_a_date_time}; + + string config_description = "Pea"; + string config_details; + string analysis_agency = "GAA"; + string analysis_centre = "Geoscience Australia"; + string analysis_software = "Ginan"; + string analysis_software_version = "3.0"; + string ac_contact = "clientservices@ga.gov.au"; + string rinex_comment = "Daily 30-sec observations from IGS stations"; + string reference_system = "igb14"; + string time_system = "G"; + string ocean_tide_loading_model = "FES2004"; + string atmospheric_tide_loading_model = "---"; + string geoid_model = "EGM96"; + string gradient_mapping_function = "Chen & Herring, 1992"; + + bool simulate_real_time = false; + + bool process_preprocessor = true; + bool process_spp = true; + bool process_minimum_constraints = false; + bool process_ionosphere = false; + bool process_rts = false; + bool process_ppp = false; + bool process_orbits = false; + + map process_sys; + map solve_amb_for; + bool process_meas[NUM_MEAS_TYPES] = {true, true}; + map reject_eclipse; + + string reference_clock = "NO_REFERENCE"; + string reference_bias = "NO_REFERENCE"; + string pivot_receiver = "NO_REFERENCE"; + + bool interpolate_rec_pco = true; + bool auto_fill_pco = true; + bool require_apriori_positions = false; + bool require_site_eccentricity = false; + bool require_sinex_data = false; + bool require_antenna_details = false; + bool require_reflector_com = false; + + bool use_tgd_bias = false; + + double wait_next_epoch = 0; + double max_rec_latency = 0; + bool require_obs = true; + bool assign_closest_epoch = false; + + bool delete_old_ephemerides = true; + + // bool reinit_on_clock_error = false; + double validity_interval_factor = 10; + + E_OffsetType ssr_input_antenna_offset = E_OffsetType::UNSPECIFIED; + + map> code_priorities; + map used_nav_types = ///< Default observation codes on L1 for IF + ///< combination based satellite clocks + {{E_Sys::GPS, E_NavMsgType::LNAV}, + {E_Sys::GLO, E_NavMsgType::FDMA}, + {E_Sys::GAL, E_NavMsgType::INAV}, + {E_Sys::BDS, E_NavMsgType::D1}, + {E_Sys::QZS, E_NavMsgType::LNAV}}; + + vector default_code_priorities = { + E_ObsCode::L1C, E_ObsCode::L1P, E_ObsCode::L1Y, E_ObsCode::L1W, E_ObsCode::L1M, + E_ObsCode::L1N, E_ObsCode::L1S, E_ObsCode::L1L, E_ObsCode::L1X, + + E_ObsCode::L2W, E_ObsCode::L2P, E_ObsCode::L2Y, E_ObsCode::L2C, E_ObsCode::L2M, + E_ObsCode::L2N, E_ObsCode::L2D, E_ObsCode::L2S, E_ObsCode::L2L, E_ObsCode::L2X, + + E_ObsCode::L5I, E_ObsCode::L5Q, E_ObsCode::L5X + }; + + double fixed_phase_bias_var = 0.01; + + bool adjust_rec_clocks_by_spp = true; + bool adjust_clocks_for_jumps_only = false; + // bool minimise_sat_clock_offsets = false; + struct + { + bool enable = false; + double max_offset = 10; + } minimise_sat_clock_offsets; + + bool minimise_sat_orbit_offsets = false; + bool minimise_ionosphere_offsets = false; + + map receiver_amb_pivot; ///< fix one ambiguity to eliminate rank deficiency + map network_amb_pivot; ///< fix ambiguities to eliminate network rank deficiencies + map use_for_iono_model; ///< use system for ionospheric modelling + map use_iono_corrections; ///< use system for ionospheric modelling + + map constrain_best_ambiguity_integer; + map constrain_clock; + map constrain_phase_bias; + + map eph_time_delay; + map default_eph_time_delay = { + {E_Sys::GPS, -7200.0}, + {E_Sys::GLO, 0.0}, + {E_Sys::GAL, 0.0}, + {E_Sys::QZS, 0.0}, + {E_Sys::BDS, 0.0}, + {E_Sys::LEO, 0.0}, + {E_Sys::SBS, 0.0} + }; + + bool common_sat_pco = false; + bool common_rec_pco = false; + bool use_trop_corrections = false; + + // to be removed? + + double predefined_fail = 0.001; /* pre-defined fail-rate (0.01,0.001) */ }; /** Options associated with kalman filter states -*/ + */ struct KalmanModel { - vector sigma = {-1}; //{0} is very necessary - vector apriori_value = {0}; - vector process_noise = {0}; - vector tau = {-1}; //tau<0 (inf): Random Walk model; tau>0: First Order Gauss Markov model - vector mu = {0}; - vector estimate = {false}; - vector use_remote_sigma = {false}; - vector comment = {""}; - - KalmanModel& operator+=( - const KalmanModel& rhs); - - map initialisedMap; + vector sigma = {-1}; //{0} is very necessary + vector sigma_limit = {0}; + vector outage_limit = {0}; + vector apriori_value = {0}; + vector process_noise = {0}; + vector tau = { + -1 + }; // tau<0 (inf): Random Walk model; tau>0: First Order Gauss Markov model + vector mu = {0}; + vector estimate = {false}; + vector use_remote_sigma = {false}; + vector comment = {""}; + + KalmanModel& operator+=(const KalmanModel& rhs); + + map initialisedMap; +}; + +struct LeastSquareOptions +{ + int max_iterations = 2; + bool sigma_check = false; + bool omega_test = true; + double meas_sigma_threshold = 4; }; struct PrefitOptions { - int max_iterations = 2; - bool sigma_check = true; - double state_sigma_threshold = 4; - double meas_sigma_threshold = 4; - bool omega_test = false; + int max_iterations = 2; + bool sigma_check = true; + bool omega_test = false; + double state_sigma_threshold = 4; + double meas_sigma_threshold = 4; }; struct PostfitOptions { - int max_iterations = 2; - bool sigma_check = true; - double state_sigma_threshold = 4; - double meas_sigma_threshold = 4; + int max_iterations = 2; + bool sigma_check = false; + bool omega_test = true; + double state_sigma_threshold = 4; + double meas_sigma_threshold = 4; }; struct ChiSquareOptions { - bool enable = false; - E_ChiSqMode mode = E_ChiSqMode::INNOVATION; - double sigma_threshold = 4; + bool enable = false; + E_ChiSqMode mode = E_ChiSqMode::INNOVATION; + double sigma_threshold = 4; }; struct RtsOptions { - string rts_filename = "Filter--.rts"; - string rts_directory = ""; - int rts_lag = -1; - int rts_interval = 0; - string rts_smoothed_suffix = "_smoothed"; - bool output_intermediate_rts = false; + string rts_filename = "/Filter-.rts"; + string rts_directory = ""; + int rts_lag = -1; + int rts_interval = 0; + string rts_smoothed_suffix = "_smoothed"; - bool queue_rts_outputs = false; + bool queue_rts_outputs = false; - E_Inverter rts_inverter = E_Inverter::LDLT; + double rts_regularisation = 1e-12; }; struct FilterOptions : RtsOptions { - bool simulate_filter_only = false; - bool assume_linearity = false; - bool advanced_postfits = false; + bool simulate_filter_only = false; + bool assume_linearity = false; + bool advanced_postfits = false; - bool joseph_stabilisation = false; + bool joseph_stabilisation = false; - E_Inverter inverter = E_Inverter::LDLT; + E_Inverter lsq_inverter = E_Inverter::INV; + E_Inverter inverter = E_Inverter::LDLT; - PrefitOptions prefitOpts; - PostfitOptions postfitOpts; - ChiSquareOptions chiSquareTest; + LeastSquareOptions lsqOpts; + PrefitOptions prefitOpts; + PostfitOptions postfitOpts; + ChiSquareOptions chiSquareTest; }; /** Options associated with the ionospheric modelling processing mode of operation -*/ + */ struct IonosphericOptions { - E_IonoMode corr_mode = E_IonoMode::BROADCAST; - - bool common_ionosphere = true; - bool use_if_combo = false; - bool use_gf_combo = false; + E_IonoMode corr_mode = E_IonoMode::BROADCAST; + bool common_ionosphere = true; + bool use_if_combo = false; + bool use_gf_combo = false; }; /** Options associated with the ppp processing mode of operation -*/ + */ struct PppOptions : FilterOptions { - KalmanModel eop; - KalmanModel eop_rates; + KalmanModel eop; + KalmanModel eop_rates; - IonosphericOptions ionoOpts; + IonosphericOptions ionoOpts; - bool equate_ionospheres = false; - bool equate_tropospheres = false; - bool use_rtk_combo = false; + bool equate_ionospheres = false; + bool equate_tropospheres = false; + bool use_rtk_combo = false; + bool merge_correlated_states = false; + bool use_primary_signals = false; - bool add_eop_component = false; + bool add_eop_component = false; - bool satellite_chunking = false; - bool receiver_chunking = false; - int chunk_size = 0; + bool receiver_chunking = false; + int chunk_size = 0; - bool nuke_enable = false; - int nuke_interval = 86400; - vector nuke_states = {KF::ALL}; + bool filter_reset_enable = false; + int reset_interval = 0; + vector reset_epochs = {}; + vector reset_states = {KF::ALL}; }; struct SppOptions : FilterOptions { - bool always_reinitialise = false; - int max_lsq_iterations = 12; - double max_gdop = 30; - double sigma_scaling = 1; - bool raim = true; - - E_IonoMode iono_mode = E_IonoMode::IONO_FREE_LINEAR_COMBO; + bool always_reinitialise = false; + int max_lsq_iterations = 12; + double elevation_mask_deg = 0; + double max_gdop = 30; + double sigma_scaling = 1; + struct + { + bool enable = true; + double max_iterations = 2; + } raim; + + E_IonoMode iono_mode = E_IonoMode::IONO_FREE_LINEAR_COMBO; + vector trop_models = {E_TropModel::STANDARD}; }; struct IonModelOptions : FilterOptions { - E_IonoModel model = E_IonoModel::NONE; - int numBasis; ///< not a directly configurable parameter - int function_order; - int function_degree; + E_IonoModel model = E_IonoModel::NONE; + int numBasis; ///< not a directly configurable parameter + int function_order; + int function_degree; - vector layer_heights; - bool estimate_sat_dcb = true; - bool use_rotation_mtx = false; - double basis_sigma_limit = 1000; + vector layer_heights; + bool estimate_sat_dcb = true; + bool use_rotation_mtx = false; + double basis_sigma_limit = 1000; - KalmanModel ion; + KalmanModel ion; }; struct AmbROptions { - E_ARmode mode = E_ARmode::OFF; - int lambda_set = 2; - int AR_max_itr = 1; + E_ARmode mode = E_ARmode::OFF; + int lambda_set = 2; + int AR_max_itr = 1; - double elevation_mask_deg = 15; + double elevation_mask_deg = 15; - double succsThres = 0.9999; ///< Thresholds for ambiguity validation: succsess rate NL - double ratioThres = 3; ///< Thresholds for ambiguity validation: succsess rate NL + double succsThres = 0.9999; ///< Thresholds for ambiguity validation: succsess rate NL + double ratioThres = 3; ///< Thresholds for ambiguity validation: succsess rate NL - double code_output_interval = 0; ///< Update interval for code biases, 0: no output - double phase_output_interval = 0; ///< Update interval for phase biases, 0: no output - bool output_rec_bias = false; ///< Output receivr bias + double code_output_interval = 0; ///< Update interval for code biases, 0: no output + double phase_output_interval = 0; ///< Update interval for phase biases, 0: no output + bool output_rec_bias = false; ///< Output receivr bias - bool once_per_epoch = true; - bool fix_and_hold = false; + bool once_per_epoch = true; + bool fix_and_hold = false; }; /** Rinex 2 conversions for individual receivers -*/ + */ struct Rinex23Conversion { - map codeConv = - { - {E_ObsCode2::P1, E_ObsCode::L1C}, - {E_ObsCode2::P2, E_ObsCode::L2W} - }; - - map phasConv = - { - {E_ObsCode2::L1, E_ObsCode::L1C}, - {E_ObsCode2::L2, E_ObsCode::L2W} - }; - - Rinex23Conversion& operator +=(const Rinex23Conversion& rhs) - { - for (auto& [code2, code3] : rhs.codeConv) { if (code3 != +E_ObsCode::NONE) codeConv[code2] = code3; } - for (auto& [code2, code3] : rhs.phasConv) { if (code3 != +E_ObsCode::NONE) phasConv[code2] = code3; } - - return *this; - } + map codeConv; + map> phasConv; + + Rinex23Conversion& operator+=(const Rinex23Conversion& rhs) + { + for (auto& [code2, code3] : rhs.codeConv) + { + if (code3 != E_ObsCode::NONE) + codeConv[code2] = code3; + } + for (auto& [code2, code3List] : rhs.phasConv) + { + if (!code3List.empty()) + phasConv[code2] = code3List; + } + + return *this; + } }; - - - - - - struct InertialKalmans { - KalmanModel accelerometer_scale; - KalmanModel accelerometer_bias; - KalmanModel orientation; - KalmanModel imu_offset; - KalmanModel gyro_scale; - KalmanModel gyro_bias; - - InertialKalmans& operator+=( - const InertialKalmans& rhs) - { - accelerometer_scale += rhs.accelerometer_scale; - accelerometer_bias += rhs.accelerometer_bias; - orientation += rhs.orientation; - imu_offset += rhs.imu_offset; - gyro_scale += rhs.gyro_scale; - gyro_bias += rhs.gyro_bias; - - return *this; - } + KalmanModel accelerometer_scale; + KalmanModel accelerometer_bias; + KalmanModel orientation; + KalmanModel imu_offset; + KalmanModel gyro_scale; + KalmanModel gyro_bias; + + InertialKalmans& operator+=(const InertialKalmans& rhs) + { + accelerometer_scale += rhs.accelerometer_scale; + accelerometer_bias += rhs.accelerometer_bias; + orientation += rhs.orientation; + imu_offset += rhs.imu_offset; + gyro_scale += rhs.gyro_scale; + gyro_bias += rhs.gyro_bias; + + return *this; + } }; struct EmpKalmans { - KalmanModel emp_d_0; - KalmanModel emp_d_1; - KalmanModel emp_d_2; - KalmanModel emp_d_3; - KalmanModel emp_d_4; - - KalmanModel emp_y_0; - KalmanModel emp_y_1; - KalmanModel emp_y_2; - KalmanModel emp_y_3; - KalmanModel emp_y_4; - - KalmanModel emp_b_0; - KalmanModel emp_b_1; - KalmanModel emp_b_2; - KalmanModel emp_b_3; - KalmanModel emp_b_4; - - KalmanModel emp_r_0; - KalmanModel emp_r_1; - KalmanModel emp_r_2; - KalmanModel emp_r_3; - KalmanModel emp_r_4; - - KalmanModel emp_t_0; - KalmanModel emp_t_1; - KalmanModel emp_t_2; - KalmanModel emp_t_3; - KalmanModel emp_t_4; - - KalmanModel emp_n_0; - KalmanModel emp_n_1; - KalmanModel emp_n_2; - KalmanModel emp_n_3; - KalmanModel emp_n_4; - - KalmanModel emp_p_0; - KalmanModel emp_p_1; - KalmanModel emp_p_2; - KalmanModel emp_p_3; - KalmanModel emp_p_4; - - KalmanModel emp_q_0; - KalmanModel emp_q_1; - KalmanModel emp_q_2; - KalmanModel emp_q_3; - KalmanModel emp_q_4; - - EmpKalmans& operator+=( - const EmpKalmans& rhs) - { - emp_d_0 += rhs.emp_d_0; - emp_d_1 += rhs.emp_d_1; - emp_d_2 += rhs.emp_d_2; - emp_d_3 += rhs.emp_d_3; - emp_d_4 += rhs.emp_d_4; - - emp_y_0 += rhs.emp_y_0; - emp_y_1 += rhs.emp_y_1; - emp_y_2 += rhs.emp_y_2; - emp_y_3 += rhs.emp_y_3; - emp_y_4 += rhs.emp_y_4; - - emp_b_0 += rhs.emp_b_0; - emp_b_1 += rhs.emp_b_1; - emp_b_2 += rhs.emp_b_2; - emp_b_3 += rhs.emp_b_3; - emp_b_4 += rhs.emp_b_4; - - emp_r_0 += rhs.emp_r_0; - emp_r_1 += rhs.emp_r_1; - emp_r_2 += rhs.emp_r_2; - emp_r_3 += rhs.emp_r_3; - emp_r_4 += rhs.emp_r_4; - - emp_t_0 += rhs.emp_t_0; - emp_t_1 += rhs.emp_t_1; - emp_t_2 += rhs.emp_t_2; - emp_t_3 += rhs.emp_t_3; - emp_t_4 += rhs.emp_t_4; - - emp_n_0 += rhs.emp_n_0; - emp_n_1 += rhs.emp_n_1; - emp_n_2 += rhs.emp_n_2; - emp_n_3 += rhs.emp_n_3; - emp_n_4 += rhs.emp_n_4; - - emp_p_0 += rhs.emp_p_0; - emp_p_1 += rhs.emp_p_1; - emp_p_2 += rhs.emp_p_2; - emp_p_3 += rhs.emp_p_3; - emp_p_4 += rhs.emp_p_4; - - emp_q_0 += rhs.emp_q_0; - emp_q_1 += rhs.emp_q_1; - emp_q_2 += rhs.emp_q_2; - emp_q_3 += rhs.emp_q_3; - emp_q_4 += rhs.emp_q_4; - - return *this; - } + KalmanModel emp_d_0; + KalmanModel emp_d_1; + KalmanModel emp_d_2; + KalmanModel emp_d_3; + KalmanModel emp_d_4; + + KalmanModel emp_y_0; + KalmanModel emp_y_1; + KalmanModel emp_y_2; + KalmanModel emp_y_3; + KalmanModel emp_y_4; + + KalmanModel emp_b_0; + KalmanModel emp_b_1; + KalmanModel emp_b_2; + KalmanModel emp_b_3; + KalmanModel emp_b_4; + + KalmanModel emp_r_0; + KalmanModel emp_r_1; + KalmanModel emp_r_2; + KalmanModel emp_r_3; + KalmanModel emp_r_4; + + KalmanModel emp_t_0; + KalmanModel emp_t_1; + KalmanModel emp_t_2; + KalmanModel emp_t_3; + KalmanModel emp_t_4; + + KalmanModel emp_n_0; + KalmanModel emp_n_1; + KalmanModel emp_n_2; + KalmanModel emp_n_3; + KalmanModel emp_n_4; + + KalmanModel emp_p_0; + KalmanModel emp_p_1; + KalmanModel emp_p_2; + KalmanModel emp_p_3; + KalmanModel emp_p_4; + + KalmanModel emp_q_0; + KalmanModel emp_q_1; + KalmanModel emp_q_2; + KalmanModel emp_q_3; + KalmanModel emp_q_4; + + EmpKalmans& operator+=(const EmpKalmans& rhs) + { + emp_d_0 += rhs.emp_d_0; + emp_d_1 += rhs.emp_d_1; + emp_d_2 += rhs.emp_d_2; + emp_d_3 += rhs.emp_d_3; + emp_d_4 += rhs.emp_d_4; + + emp_y_0 += rhs.emp_y_0; + emp_y_1 += rhs.emp_y_1; + emp_y_2 += rhs.emp_y_2; + emp_y_3 += rhs.emp_y_3; + emp_y_4 += rhs.emp_y_4; + + emp_b_0 += rhs.emp_b_0; + emp_b_1 += rhs.emp_b_1; + emp_b_2 += rhs.emp_b_2; + emp_b_3 += rhs.emp_b_3; + emp_b_4 += rhs.emp_b_4; + + emp_r_0 += rhs.emp_r_0; + emp_r_1 += rhs.emp_r_1; + emp_r_2 += rhs.emp_r_2; + emp_r_3 += rhs.emp_r_3; + emp_r_4 += rhs.emp_r_4; + + emp_t_0 += rhs.emp_t_0; + emp_t_1 += rhs.emp_t_1; + emp_t_2 += rhs.emp_t_2; + emp_t_3 += rhs.emp_t_3; + emp_t_4 += rhs.emp_t_4; + + emp_n_0 += rhs.emp_n_0; + emp_n_1 += rhs.emp_n_1; + emp_n_2 += rhs.emp_n_2; + emp_n_3 += rhs.emp_n_3; + emp_n_4 += rhs.emp_n_4; + + emp_p_0 += rhs.emp_p_0; + emp_p_1 += rhs.emp_p_1; + emp_p_2 += rhs.emp_p_2; + emp_p_3 += rhs.emp_p_3; + emp_p_4 += rhs.emp_p_4; + + emp_q_0 += rhs.emp_q_0; + emp_q_1 += rhs.emp_q_1; + emp_q_2 += rhs.emp_q_2; + emp_q_3 += rhs.emp_q_3; + emp_q_4 += rhs.emp_q_4; + + return *this; + } }; struct CommonKalmans { - KalmanModel pos; - KalmanModel pos_rate; - KalmanModel orbit; - KalmanModel clk; - KalmanModel clk_rate; - KalmanModel code_bias; - KalmanModel phase_bias; - KalmanModel pco; - KalmanModel ant_delta; - - CommonKalmans& operator+=( - const CommonKalmans& rhs) - { - pos += rhs.pos; - pos_rate += rhs.pos_rate; - orbit += rhs.orbit; - clk += rhs.clk; - clk_rate += rhs.clk_rate; - code_bias += rhs.code_bias; - phase_bias += rhs.phase_bias; - pco += rhs.pco; - ant_delta += rhs.ant_delta; - - return *this; - } + KalmanModel pos; + KalmanModel pos_rate; + KalmanModel orbit; + KalmanModel clk; + KalmanModel clk_rate; + KalmanModel code_bias; + KalmanModel phase_bias; + KalmanModel pco; + KalmanModel ant_delta; + KalmanModel cr; + KalmanModel cd; + CommonKalmans& operator+=(const CommonKalmans& rhs) + { + pos += rhs.pos; + pos_rate += rhs.pos_rate; + orbit += rhs.orbit; + clk += rhs.clk; + clk_rate += rhs.clk_rate; + code_bias += rhs.code_bias; + phase_bias += rhs.phase_bias; + pco += rhs.pco; + ant_delta += rhs.ant_delta; + cr += rhs.cr; + cd += rhs.cd; + + return *this; + } }; struct SatelliteKalmans : CommonKalmans, InertialKalmans, EmpKalmans { - //nothing here + // nothing here - SatelliteKalmans& operator+=( - const SatelliteKalmans& rhs) - { - CommonKalmans ::operator+=(rhs); - InertialKalmans ::operator+=(rhs); - EmpKalmans ::operator+=(rhs); + SatelliteKalmans& operator+=(const SatelliteKalmans& rhs) + { + CommonKalmans :: operator+=(rhs); + InertialKalmans ::operator+=(rhs); + EmpKalmans :: operator+=(rhs); - return *this; - } + return *this; + } }; struct ReceiverKalmans : CommonKalmans, InertialKalmans, EmpKalmans { - KalmanModel ambiguity; - KalmanModel strain_rate; - KalmanModel slr_range_bias; - KalmanModel slr_time_bias; - KalmanModel pcv; - KalmanModel ion_stec; - KalmanModel ion_model; - KalmanModel trop; - KalmanModel trop_grads; - KalmanModel trop_maps; - - ReceiverKalmans& operator+=( - const ReceiverKalmans& rhs) - { - CommonKalmans ::operator+=(rhs); - InertialKalmans ::operator+=(rhs); - EmpKalmans ::operator+=(rhs); - - ambiguity += rhs.ambiguity; - strain_rate += rhs.strain_rate; - slr_range_bias += rhs.slr_range_bias; - slr_time_bias += rhs.slr_time_bias; - pcv += rhs.pcv; - ion_stec += rhs.ion_stec; - ion_model += rhs.ion_model; - trop += rhs.trop; - trop_grads += rhs.trop_grads; - trop_maps += rhs.trop_maps; - - return *this; - } + KalmanModel ambiguity; + KalmanModel strain_rate; + KalmanModel slr_range_bias; + KalmanModel slr_time_bias; + KalmanModel pcv; + KalmanModel ion_stec; + KalmanModel ion_model; + KalmanModel trop; + KalmanModel trop_grads; + KalmanModel trop_maps; + + ReceiverKalmans& operator+=(const ReceiverKalmans& rhs) + { + CommonKalmans :: operator+=(rhs); + InertialKalmans ::operator+=(rhs); + EmpKalmans :: operator+=(rhs); + + ambiguity += rhs.ambiguity; + strain_rate += rhs.strain_rate; + slr_range_bias += rhs.slr_range_bias; + slr_time_bias += rhs.slr_time_bias; + pcv += rhs.pcv; + ion_stec += rhs.ion_stec; + ion_model += rhs.ion_model; + trop += rhs.trop; + trop_grads += rhs.trop_grads; + trop_maps += rhs.trop_maps; + + return *this; + } }; - - - - - - - - - - - struct CommonOptions { - bool exclude = false; - double pseudo_sigma = 100000; - double laser_sigma = 0.5; - - vector clock_codes = {}; - vector apriori_sigma_enu = {}; - double mincon_scale_apriori_sigma = 1; - double mincon_scale_filter_sigma = 0; - - Vector3d antenna_boresight = { 0, 0, +1}; - Vector3d antenna_azimuth = { 0, +1, 0}; - - double ellipse_propagation_time_tolerance = 30; - - struct - { - bool enable = true; - vector sources = {E_Source::KALMAN, E_Source::CONFIG, E_Source::PRECISE, E_Source::SPP, E_Source::BROADCAST}; - } posModel; - - struct - { - bool enable = true; - vector sources = {E_Source::KALMAN, E_Source::PRECISE, E_Source::BROADCAST}; - } clockModel; - - struct - { - bool enable = true; - vector sources = {E_Source::PRECISE, E_Source::MODEL, E_Source::NOMINAL}; - double model_dt = 1; - } attitudeModel; - - struct - { - bool enable = true; - double default_bias = 0; - double undefined_sigma = 0; - } codeBiasModel; - - struct - { - bool enable = false; - double default_bias = 0; - double undefined_sigma = 0; - } phaseBiasModel; - - struct - { - bool enable = true; - } pcoModel; - - struct - { - bool enable = true; - } pcvModel; - - struct - { - bool enable = true; - } phaseWindupModel; - - CommonOptions& operator+=( - const CommonOptions& rhs); - - map initialisedMap; + bool exclude = false; + double pseudo_sigma = 100000; + double laser_sigma = 0.5; + + vector clock_codes = {}; + vector apriori_sigma_enu = {}; + double mincon_scale_apriori_sigma = 1; + double mincon_scale_filter_sigma = 0; + + Vector3d antenna_boresight = {0, 0, +1}; + Vector3d antenna_azimuth = {0, +1, 0}; + + double ellipse_propagation_time_tolerance = 30; + + struct + { + bool enable = true; + vector sources = { + E_Source::KALMAN, + E_Source::CONFIG, + E_Source::PRECISE, + E_Source::SPP, + E_Source::BROADCAST + }; + } posModel; + + struct + { + bool enable = true; + vector sources = + {E_Source::KALMAN, E_Source::PRECISE, E_Source::SPP, E_Source::BROADCAST}; + } clockModel; + + struct + { + bool enable = true; + vector sources = {E_Source::PRECISE, E_Source::MODEL, E_Source::NOMINAL}; + double model_dt = 1; + } attitudeModel; + + struct + { + bool enable = true; + double default_bias = 0; + double undefined_sigma = 1; + } codeBiasModel; + + struct + { + bool enable = false; + double default_bias = 0; + double undefined_sigma = 0; + } phaseBiasModel; + + struct + { + bool enable = true; + } pcoModel; + + struct + { + bool enable = true; + } pcvModel; + + struct + { + bool enable = true; + } phaseWindupModel; + + CommonOptions& operator+=(const CommonOptions& rhs); + + map initialisedMap; }; - struct PropagationOptions { - int egm_degree = 12; - double integrator_time_step = 60; - bool egm_field = true; - bool solid_earth_tide = true; - bool pole_tide_ocean = true; - bool pole_tide_solid = true; - bool ocean_tide = true; - bool indirect_J2 = true; - bool aod = false; - bool atm_tide = false; - bool central_force = true; - bool general_relativity = true; + int egm_degree = 12; + double integrator_time_step = 60; + bool egm_field = true; + bool solid_earth_tide = true; + bool pole_tide_ocean = true; + bool pole_tide_solid = true; + bool ocean_tide = true; + bool indirect_J2 = true; + bool aod = false; + bool atm_tide = false; + bool central_force = true; + bool general_relativity = true; }; struct SurfaceDetails { - vector normal = {0,0,0}; - vector rotation_axis = {}; // Optional field - double shape = 0; - double area = 0; - double reflection_visible = 0; - double diffusion_visible = 0; - double absorption_visible = 0; - double thermal_reemission = 0; - double reflection_infrared = 0; - double diffusion_infrared = 0; - double absorption_infrared = 0; + vector normal = {0, 0, 0}; + vector rotation_axis = {}; // Optional field + double shape = 0; + double area = 0; + double reflection_visible = 0; + double diffusion_visible = 0; + double absorption_visible = 0; + double thermal_reemission = 0; + double reflection_infrared = 0; + double diffusion_infrared = 0; + double absorption_infrared = 0; }; /** Options associated with orbital force models -*/ + */ struct OrbitOptions { - double mass = 1000; - double area = 20; - double power = 20; - double srp_cr = 1.25; - - vector planetary_perturbations = {E_ThirdBody::SUN, E_ThirdBody::MOON, E_ThirdBody::JUPITER }; - bool empirical = true; - bool antenna_thrust = true; - E_SRPModel albedo = E_SRPModel::NONE; - E_SRPModel solar_radiation_pressure = E_SRPModel::NONE; - - vector empirical_dyb_eclipse = {true}; - vector empirical_rtn_eclipse = {false}; - - vector surface_details; - - struct - { - bool enable = false; - int interval = 1; - - double pos_proc_noise = 10; - double vel_proc_noise = 5; - } pseudoPulses; - - OrbitOptions& operator+=( - const OrbitOptions& rhs); - - map initialisedMap; + double mass = 1000; + double area = 20; + double power = 20; + double srp_cr = 1.25; + double drag_cd = 2.2; + + vector planetary_perturbations = { + E_ThirdBody::SUN, + E_ThirdBody::MOON, + E_ThirdBody::JUPITER + }; + bool empirical = true; + bool antenna_thrust = true; + E_SRPModel albedo = E_SRPModel::NONE; + E_SRPModel solar_radiation_pressure = E_SRPModel::NONE; + bool drag = false; + + vector empirical_dyb_eclipse = {true}; + vector empirical_rtn_eclipse = {false}; + + vector surface_details; + + struct + { + bool enable = false; + int interval = 0; + vector epochs = {}; + double pos_proc_noise = 10; + double vel_proc_noise = 5; + } pseudoPulses; + + OrbitOptions& operator+=(const OrbitOptions& rhs); + + map initialisedMap; }; - - - - - - /** Options to be applied to kalman filter states for individual satellites -*/ + */ struct SatelliteOptions : SatelliteKalmans, CommonOptions, OrbitOptions { - bool _initialised = false; - string id; + bool _initialised = false; + string id; - E_NoiseModel error_model = E_NoiseModel::UNIFORM; - double code_sigma = 0; - double phase_sigma = 0; + E_NoiseModel error_model = E_NoiseModel::UNIFORM; + double code_sigma = 0; + double phase_sigma = 0; - SatelliteOptions& operator+=( - const SatelliteOptions& rhs); + SatelliteOptions& operator+=(const SatelliteOptions& rhs); - void uninitialiseInheritors(); + void uninitialiseInheritors(); - map inheritors; - map inheritedFrom; + map inheritors; + map inheritedFrom; - map initialisedMap; + map initialisedMap; }; /** Options to be applied to kalman filter states for individual receivers -*/ + */ struct ReceiverOptions : ReceiverKalmans, CommonOptions { - bool _initialised = false; - string id; - - Rinex23Conversion rinex23Conv; - - bool kill = false; - vector zero_dcb_codes = {}; - Vector3d apriori_pos = Vector3d::Zero(); - string antenna_type ; - string receiver_type ; - string sat_id ; - double elevation_mask_deg = 10; - E_Sys receiver_reference_system = E_Sys::NONE; - - struct - { - bool enable = true; - Vector3d eccentricity = Vector3d::Zero(); - } eccentricityModel; - - struct - { - bool enable = true; - vector models = {E_TropModel::VMF3, E_TropModel::GPT2, E_TropModel::STANDARD}; - } tropModel; - - struct - { - bool enable = true; - bool solid = true; - bool otl = true; - bool atl = true; - bool spole = true; - bool opole = true; - } tideModels; - - bool range = true; - bool relativity = true; - bool relativity2 = true; - bool sagnac = true; - bool integer_ambiguity = true; - bool ionospheric_component = true; - bool ionospheric_component2 = false; - bool ionospheric_component3 = false; - bool ionospheric_model = false; - bool tropospheric_map = false; - bool eop = false; - - E_IonoMapFn mapping_function = E_IonoMapFn::MSLM; - double geomagnetic_field_height = 450; - double mapping_function_layer_height = 506.7; - double iono_sigma_limit = 1000; - - E_NoiseModel error_model = E_NoiseModel::ELEVATION_DEPENDENT; - double code_sigma = 1; - double phase_sigma = 0.0015; - - ReceiverOptions& operator+=( - const ReceiverOptions& rhs); - - map inheritors; - map inheritedFrom; - - map initialisedMap; + bool _initialised = false; + string id; + + 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 = 10; + E_Sys receiver_reference_system = E_Sys::NONE; + + struct + { + bool enable = true; + Vector3d eccentricity = Vector3d::Zero(); + } eccentricityModel; + + struct + { + bool enable = true; + vector models = {E_TropModel::VMF3, E_TropModel::GPT2, E_TropModel::STANDARD}; + } tropModel; + + struct + { + bool enable = true; + bool solid = true; + bool otl = true; + bool atl = true; + bool spole = true; + bool opole = true; + } tideModels; + + bool range = true; + bool relativity = true; + bool relativity2 = true; + bool sagnac = true; + bool integer_ambiguity = true; + bool ionospheric_component = true; + bool ionospheric_component2 = false; + bool ionospheric_component3 = false; + bool ionospheric_model = false; + bool tropospheric_map = false; + bool eop = false; + + E_IonoMapFn mapping_function = E_IonoMapFn::MSLM; + double geomagnetic_field_height = 450; + double mapping_function_layer_height = 506.7; + + E_NoiseModel error_model = E_NoiseModel::ELEVATION_DEPENDENT; + double code_sigma = 1; + double phase_sigma = 0.0015; + + ReceiverOptions& operator+=(const ReceiverOptions& rhs); + + map inheritors; + map inheritedFrom; + + map initialisedMap; }; - - - - - - - - - - /** Options associated with the minimum constraints mode of operation -*/ + */ struct MinimumConstraintOptions : FilterOptions { - KalmanModel delay; - KalmanModel scale; - KalmanModel rotation; - KalmanModel translation; - - bool once_per_epoch = false; - bool full_vcv = false; - bool constrain_orbits = true; - bool transform_unweighted = true; - - E_Mincon application_mode = E_Mincon::COVARIANCE_INVERSE; + KalmanModel delay; + KalmanModel scale; + KalmanModel rotation; + KalmanModel translation; + + KalmanModel delay_rate; + KalmanModel scale_rate; + KalmanModel rotation_rate; + KalmanModel translation_rate; + + bool once_per_epoch = false; + bool full_vcv = false; + bool constrain_orbits = true; + bool transform_unweighted = true; + + E_Mincon application_mode = E_Mincon::COVARIANCE_INVERSE; }; struct MongoInstanceOptions { - string uri = "mongodb://localhost:27017"; - string suffix; - string database = ""; + string uri = "mongodb://localhost:27017"; + string suffix; + string database = ""; }; struct MongoOptions : array { - E_Mongo enable = E_Mongo::NONE; - E_Mongo output_measurements = E_Mongo::NONE; - E_Mongo output_components = E_Mongo::NONE; - E_Mongo output_cumulative = E_Mongo::NONE; - E_Mongo output_states = E_Mongo::NONE; - E_Mongo output_state_covars = E_Mongo::NONE; - E_Mongo output_trace = E_Mongo::NONE; - E_Mongo output_config = E_Mongo::NONE; - E_Mongo output_test_stats = E_Mongo::NONE; - E_Mongo output_logs = E_Mongo::NONE; - E_Mongo output_ssr_precursors = E_Mongo::NONE; - E_Mongo delete_history = E_Mongo::NONE; - E_Mongo cull_history = E_Mongo::NONE; - E_Mongo use_predictions = E_Mongo::NONE; - E_Mongo output_predictions = E_Mongo::NONE; - - bool queue_outputs = false; - - vector used_predictions = {KF::ORBIT, KF::REC_POS, KF::SAT_CLOCK, KF::CODE_BIAS, KF::PHASE_BIAS, KF::EOP, KF::EOP_RATE}; - vector sent_predictions = {KF::ORBIT, KF::REC_POS, KF::SAT_CLOCK, KF::CODE_BIAS, KF::PHASE_BIAS, KF::EOP, KF::EOP_RATE}; - - double prediction_offset = 0; - double prediction_interval = 30; - double forward_prediction_duration = 300; - double reverse_prediction_duration = -1; - - double min_cull_age = 300; + E_Mongo enable = E_Mongo::NONE; + E_Mongo output_measurements = E_Mongo::NONE; + E_Mongo output_components = E_Mongo::NONE; + E_Mongo output_cumulative = E_Mongo::NONE; + E_Mongo output_states = E_Mongo::NONE; + E_Mongo output_state_covars = E_Mongo::NONE; + E_Mongo output_trace = E_Mongo::NONE; + E_Mongo output_config = E_Mongo::NONE; + E_Mongo output_test_stats = E_Mongo::NONE; + E_Mongo output_logs = E_Mongo::NONE; + E_Mongo output_ssr_precursors = E_Mongo::NONE; + E_Mongo delete_history = E_Mongo::NONE; + E_Mongo cull_history = E_Mongo::NONE; + E_Mongo use_predictions = E_Mongo::NONE; + E_Mongo output_predictions = E_Mongo::NONE; + E_Mongo output_editing = E_Mongo::NONE; + + bool queue_outputs = false; + + vector used_predictions = { + KF::ORBIT, + KF::REC_POS, + KF::SAT_CLOCK, + KF::CODE_BIAS, + KF::PHASE_BIAS, + KF::EOP, + KF::EOP_RATE + }; + vector sent_predictions = { + KF::ORBIT, + KF::REC_POS, + KF::SAT_CLOCK, + KF::CODE_BIAS, + KF::PHASE_BIAS, + KF::EOP, + KF::EOP_RATE + }; + + double prediction_offset = 0; + double prediction_interval = 30; + double forward_prediction_duration = 300; + double reverse_prediction_duration = -1; + + double min_cull_age = 300; }; /** Options associated with SSR corrections and exporting RTCM messages -*/ + */ struct SsrOptions { - bool extrapolate_corrections = false; - double prediction_interval = 30; - double prediction_duration = 0; - vector ephemeris_sources = {E_Source::PRECISE}; - vector clock_sources = {E_Source::KALMAN}; - vector code_bias_sources = {E_Source::PRECISE}; - vector phase_bias_sources = {E_Source::NONE}; - vector atmosphere_sources = {E_Source::NONE}; - bool cmpssr_cell_mask = false; - int cmpssr_stec_format = 3; - int cmpssr_trop_format = 1; - double max_stec_sigma = 1.0; - - int region_id = -1; - int region_iod = -1; - int npoly_trop = -1; - int npoly_iono = -1; - int grid_type = -1; - bool use_grid_iono = true; - bool use_grid_trop = true; - double lat_max = 0; - double lat_min = 0; - double lat_int = 0; - double lon_max = 0; - double lon_min = 0; - double lon_int = 0; - - int ngrid = 0; //not configs? - int nbasis = 0; //not configs? -}; - -struct SsrInOptions -{ - double code_bias_valid_time = 3600; ///< Valid time period of SSR code biases - double phase_bias_valid_time = 300; ///< Valid time period of SSR phase biases - double global_vtec_valid_time = 300; ///< Valid time period of SSR global Ionospheres - double local_stec_valid_time = 120; ///< Valid time period of SSR local Ionospheres - double local_trop_valid_time = 120; ///< Valid time period of SSR local Tropospheres - bool one_freq_phase_bias = false; -}; - -struct SbsInOptions -{ - string host; ///< hostname is passed as acsConfig.sisnet_inputs - string port; ///< port of SISNet steam - string user; ///< Username for SISnet stream access - string pass; ///< Password for SISnet stream access - int prn; ///< prn of SBAS satellite - int freq; ///< freq (L1 or L5) of SBAS channel + // todo Eugene: Use KALMAN source once RT POD done (same for code & phase) + bool extrapolate_corrections = false; + double prediction_interval = 30; + double prediction_duration = 0; + vector ephemeris_sources = {E_Source::PRECISE}; + vector clock_sources = {E_Source::KALMAN}; + vector code_bias_sources = {E_Source::PRECISE}; + vector phase_bias_sources = {E_Source::NONE}; + vector atmosphere_sources = {E_Source::NONE}; + bool cmpssr_cell_mask = false; + int cmpssr_stec_format = 3; + int cmpssr_trop_format = 1; + double max_stec_sigma = 1.0; + + int region_id = -1; + int region_iod = -1; + int npoly_trop = -1; + int npoly_iono = -1; + int grid_type = -1; + bool use_grid_iono = true; + bool use_grid_trop = true; + double lat_max = 0; + double lat_min = 0; + double lat_int = 0; + double lon_max = 0; + double lon_min = 0; + double lon_int = 0; + + int ngrid = 0; // not configs? + int nbasis = 0; // not configs? }; struct SSRMetaOpts { - bool itrf_datum = true; - int provider_id = 0; - int solution_id = 0; + bool itrf_datum = true; + int provider_id = 0; + int solution_id = 0; }; struct RtcmMsgTypeOpts { - int udi = 0; ///< Update interval (0 = don't upload message) + int udi = 0; ///< Update interval (0 = don't upload message) - map comp_udi; - map igs_udi; + map comp_udi; + map igs_udi; - // space for multiple UDI's here + // space for multiple UDI's here }; struct SsrBroadcast : SSRMetaOpts { - string url; + string url; - map rtcmMsgOptsMap; ///< RTCM message type options + map rtcmMsgOptsMap; ///< RTCM message type options }; struct NetworkOptions { - map uploadingStreamData; + map uploadingStreamData; }; - - - struct YamlDefault { - string defaultValue; - string comment; - string typeName; - bool found; - string foundValue; - string enumName; - int configLevel; //yet unused + string defaultValue; + string comment; + string typeName; + bool found; + string foundValue; + string enumName; + int configLevel; // yet unused }; /** General options object to be used throughout the software -*/ + */ struct ACSConfig : GlobalOptions, InputOptions, OutputOptions, DebugOptions { - vector yamls; - map yamlDefaults; - map availableOptions = { - {"yaml_filename:", true}, - {"yaml_number:", true}, - }; - map> foundOptions; + vector yamls; + map yamlDefaults; + map availableOptions = { + {"yaml_filename:", true}, + {"yaml_number:", true}, + }; + map> foundOptions; - mutex configMutex; + mutex configMutex; + map> customAliasesMap; - map> customAliasesMap; + vector configFilenames; + vector includedFilenames; + map configModifyTimeMap; + boost::program_options::variables_map commandOpts; - vector configFilenames; - vector includedFilenames; - map configModifyTimeMap; - boost::program_options::variables_map commandOpts; + static map docs; - static map docs; + void recurseYaml( + const string& file, + YAML::Node node, + const string& stack = "", + const string& aliasStack = "" + ); - void recurseYaml( - const string& file, - YAML::Node node, - const string& stack = "", - const string& aliasStack = ""); + bool parse(const vector& filenames, boost::program_options::variables_map& vm); - bool parse( - const vector& filenames, - boost::program_options::variables_map& vm); + bool parse(); - bool parse(); + void info(Trace& trace); - void info( - Trace& trace); + void sanityChecks(); - void sanityChecks(); + void outputDefaultConfiguration(int level); - void outputDefaultConfiguration( - int level); + SatelliteOptions& getSatOpts(SatSys Sat, const vector& suffixes = {}); + ReceiverOptions& getRecOpts(string id, const vector& suffixes = {}); + unordered_map satOptsMap; + unordered_map recOptsMap; - SatelliteOptions& getSatOpts(SatSys Sat, const vector& suffixes = {}); - ReceiverOptions& getRecOpts(string id, const vector& suffixes = {}); + PropagationOptions propagationOptions; + PreprocOptions preprocOpts; + MinimumConstraintOptions minconOpts; + AmbROptions ambrOpts; + SsrOptions ssrOpts; + PppOptions pppOpts; + SppOptions sppOpts; + SlrOptions slrOpts; + ExcludeOptions exclude; - unordered_map satOptsMap; - unordered_map recOptsMap; + StateErrorHandler stateErrors; + MeasErrorHandler measErrors; + AmbiguityErrorHandler ambErrors; + SatelliteErrorHandler satelliteErrors; + IonoErrorHandler ionErrors; + ErrorAccumulationHandler errorAccumulation; - PropagationOptions propagationOptions; - PreprocOptions preprocOpts; - MinimumConstraintOptions minconOpts; - SsrInOptions ssrInOpts; - SbsInOptions sbsInOpts; - AmbROptions ambrOpts; - SsrOptions ssrOpts; - PppOptions pppOpts; - SppOptions sppOpts; - SlrOptions slrOpts; - ExcludeOptions exclude; - - StateErrorHandler stateErrors; - MeasErrorHandler measErrors; - AmbiguityErrorHandler ambErrors; - OrbitErrorHandler orbErrors; - IonoErrorHandler ionErrors; - ErrorAccumulationHandler errorAccumulation; - - IonModelOptions ionModelOpts; - MongoOptions mongoOpts; - NetworkOptions netOpts; + IonModelOptions ionModelOpts; + MongoOptions mongoOpts; + NetworkOptions netOpts; }; -bool replaceString( - string& str, - string subStr, - string replacement, - bool warn = true); - -bool configure( - int argc, - char** argv); +bool replaceString(string& str, string subStr, string replacement, bool warn = true); -void dumpConfig( - Trace& trace); +bool configure(int argc, char** argv); -extern ACSConfig acsConfig; ///< Global variable housing all options to be used throughout the software +void dumpConfig(Trace& trace); +extern ACSConfig + acsConfig; ///< Global variable housing all options to be used throughout the software diff --git a/src/cpp/common/acsConfigDocs.cpp b/src/cpp/common/acsConfigDocs.cpp index dbe40219c..ac2dcd4bc 100644 --- a/src/cpp/common/acsConfigDocs.cpp +++ b/src/cpp/common/acsConfigDocs.cpp @@ -1,19 +1,18 @@ +#include "common/acsConfig.hpp" -#include "acsConfig.hpp" +map ACSConfig::docs = { - -map ACSConfig::docs = -{ - -{ "outputs", R"config(Specifies options to enable outputs and specify file locations. + {"outputs", R"config(Specifies options to enable outputs and specify file locations. Each section typically contains an option to `output` the filetype, and a `directory` to place the files named `filename`, along with any ancillary options. )config"}, -{ "inputs", R"config(This section of the yaml file specifies the lists of files to be used for general metadata inputs, and inputs of external product data. + {"inputs", + R"config(This section of the yaml file specifies the lists of files to be used for general metadata inputs, and inputs of external product data. )config"}, -{ "gnss_observations", R"config(This section specifies the sources of observation data to be used in positioning. + {"gnss_observations", + R"config(This section specifies the sources of observation data to be used in positioning. There are numerous ways that the `pea` can access GNSS observations to process. @@ -44,22 +43,34 @@ If multiple files are supplied with the same ID, they are all processed in seque )config"}, -{ "satellite_data", R"config(This section specifies sources of ephemerides and other satellite data.)config"}, - -{ "trace", "Trace files are used to document processing"}, - - -{ "ssr", "Values derived from applying received corrections to broadcast ephemeris"}, -{ "broadcast", "Values derived from broadcast ephemeris streams/files"}, -{ "precise", "Values derived from file-based products such as SP3/CLK/OBX"}, -{ "kalman", "Values estimated internally by the kalman filter"}, - - -{ "cost", "COST format files are used to export troposhere products, such as ZTD and delay gradients."}, -{ "trop_sinex", "Troposphere SINEX files are used to export troposhere products, such as ZTD and delay gradients."}, -{ "slr_obs", "SLR_OBS files are used as temporary files to arrange SLR observations by time. SLR observations are taken from CRD files, which are not strictly in time-order)."}, -{ "slr_options", "This section controls how Satellite Laser Ranging (SLR) observations are handled."}, -{ "ssr_outputs", "This section specifies how State State Representation (SSR) corrections are calculated before being published to an NTRIP caster."}, -{ "rtcm_inputs", "This section specifies how State State Representation (SSR) corrections are applied after they are downloaded from an NTRIP caster."}, + {"satellite_data", + R"config(This section specifies sources of ephemerides and other satellite data.)config"}, + + {"trace", "Trace files are used to document processing"}, + + {"ssr", "Values derived from applying received corrections to broadcast ephemeris"}, + {"broadcast", "Values derived from broadcast ephemeris streams/files"}, + {"precise", "Values derived from file-based products such as SP3/CLK/OBX"}, + {"kalman", "Values estimated internally by the kalman filter"}, + + {"cost", + "COST format files are used to export troposhere products, such as ZTD and delay gradients."}, + {"trop_sinex", + "Troposphere SINEX files are used to export troposhere products, such as ZTD and delay " + "gradients."}, + {"slr_obs", + "SLR_OBS files are used as temporary files to arrange SLR observations by time. SLR " + "observations are taken from " + "CRD files, which are not strictly in time-order)."}, + {"slr_options", + "This section controls how Satellite Laser Ranging (SLR) observations are handled."}, + {"ssr_outputs", + "This section specifies how State State Representation (SSR) corrections are calculated " + "before being published to " + "an NTRIP caster."}, + {"rtcm_inputs", + "This section specifies how State State Representation (SSR) corrections are applied after " + "they are downloaded " + "from an NTRIP caster."}, }; diff --git a/src/cpp/common/acsQC.cpp b/src/cpp/common/acsQC.cpp index f17746ee5..8c694b8ce 100644 --- a/src/cpp/common/acsQC.cpp +++ b/src/cpp/common/acsQC.cpp @@ -1,884 +1,1060 @@ - -// #pragma GCC optimize ("O0") - -#include -#include - -#include "observations.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "testUtils.hpp" -#include "constants.hpp" -#include "satStat.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "acsQC.hpp" -#include "trace.hpp" -#include "lambda.h" -#include "enums.h" - -#define THRES_MW_JUMP 10.0 -#define PDEGAP 60.0 -#define PDESLIPTHRESHOLD 0.5 -#define PROC_NOISE_IONO 0.001 - - -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... - ft1 = F1; - ft2 = F2; - ft3 = F5; - - if (acsConfig.code_priorities.find(sys) == acsConfig.code_priorities.end()) - return false; - - for (auto& code : acsConfig.code_priorities[sys]) - { - E_FType ft = code2Freq[sys][code]; - - if (ft1Ready == false) - { - ft1 = ft; ft1Ready = true; - continue; - } - - if (ft == ft1) - continue; - - if (ft2Ready == false) - { - ft2 = ft; ft2Ready = true; - continue; - } - - if (ft == ft2) - continue; - - { - ft3 = ft; - break; - } - } - - return true; -} -/** Detect cycle slip by reported loss of lock -*/ -void detslp_ll( - Trace& trace, ///< Trace to output to - ObsList& obsList) ///< List of observations to detect slips within -{ - tracepdeex(5, trace, "\n%s: n=%d", __FUNCTION__, obsList.size()); - -// auto begin_iter = boost::make_filter_iterator([] - - for (auto& obs : only(obsList)) - for (auto& [ft, sig] : obs.sigs) - { - if (obs.exclude) - { - continue; - } - - int f = ft; - if ( sig.L == 0 - || (sig.LLI & 0x03) == 0) - { - continue; - } - - tracepdeex(3, trace, "\n%s: slip detected sat=%s f=F%d\n", __FUNCTION__, obs.Sat.id().c_str(), ft); - - obs.satStat_ptr->sigStatMap[ft2string(ft)].slip. LLI = true; - obs.satStat_ptr->sigStatMap[ft2string(ft)].savedSlip. LLI = true; - } -} - -/** Detect cycle slip by geometry free phase jump -*/ -void detslp_gf( - Trace& trace, ///< Trace to output to - ObsList& obsList) ///< List of observations to detect slips within -{ - tracepdeex(5, trace, "\n%s: n=%d", __FUNCTION__, obsList.size()); - - for (auto& obs : only(obsList)) - { - if (obs.exclude) - { - continue; - } - - E_FType frq1; - E_FType frq2; - E_FType frq3; - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - if (pass == false) - continue; - - S_LC& lc = getLC(obs.satStat_ptr->lc_new, frq1, frq2); - - double gf1 = lc.GF_Phas_m; - if ( lc.valid == false - ||gf1 == 0) - { - continue; - } - - double gf0 = obs.satStat_ptr->gf; - obs.satStat_ptr->gf = gf1; - - if (gf0 == 0) - { - continue; - } - - tracepdeex(5, trace, "\n%s: sat=%s gf0=%f gf1=%f", __FUNCTION__, obs.Sat.id().c_str(), gf0, gf1); - - if (fabs(gf1 - gf0) > acsConfig.preprocOpts.slip_threshold) - { - tracepdeex(3, trace, "\n%s: slip detected: sat=%s gf0=%f gf1=%f", __FUNCTION__, 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; - } - } -} - -/** Detect slip by Melbourne-Wubbena linear combination jump -*/ -void detslp_mw( - Trace& trace, ///< Trace to output to - ObsList& obsList) ///< List of observations to detect slips within -{ - tracepdeex(5, trace, "\n%s: n=%d", __FUNCTION__, obsList.size()); - - for (auto& obs : only(obsList)) - { - if (obs.exclude) - { - continue; - } - - E_FType frq1; - E_FType frq2; - E_FType frq3; - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - if (pass == false) - continue; - - S_LC& lc = getLC(obs.satStat_ptr->lc_new, frq1, frq2); - - double mw1 = lc.MW_c; - if ( lc.valid == false - ||mw1 == 0) - { - continue; - } - - double mw0 = obs.satStat_ptr->mw; - obs.satStat_ptr->mw = mw1; - - if (mw0 == 0) - { - continue; - } - - tracepdeex(5, trace, "\n%s: sat=%s mw0=%f mw1=%f", __FUNCTION__, obs.Sat.id().c_str(), mw0, mw1); - - if (fabs(mw1 - mw0) > THRES_MW_JUMP) - { - tracepdeex(3, trace, "\n%s: slip detected: sat=%s mw0=%f mw1=%f", __FUNCTION__, 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; - } - } -} - -/** Melbourne-Wenbunna (MW) measurement noise (m) -*/ -double mwnoise( - double sigcode, ///< Code noise - double sigphase, ///< Phase noise - double lam1, ///< L1 wavelength - double lam2) ///< L2 wavelength -{ - double a = lam2 * lam2 / (lam2 + lam1) / (lam2 + lam1) + lam1 * lam1 / (lam2 + lam1) / (lam2 + lam1); - double b = lam2 * lam2 / (lam2 - lam1) / (lam2 - lam1) + lam1 * lam1 / (lam2 - lam1) / (lam2 - lam1); - return SQRT(a*SQR(sigcode) + b*SQR(sigphase)); -} - -/** Single channel detection–identification–adaptation (DIA) for integer cycle slips -*/ -void scdia( - Trace& trace, ///< Trace to output to - SatStat& satStat, ///< Persistant satellite status parameters - lc_t& lc, ///< Linear combinations - 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 -{ - 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) lc_pre_ptr = &satStat. lc_pre; - else lc_pre_ptr = &satStat.flt. lc_pre; - if (nf == 1) lc_pre_ptr = &satStat.flt. lc_pre; - - auto& lc_pre = *lc_pre_ptr; - - /* single frequency not supported in current PDE */ - if (nf == 1) - { - return; - } - - E_FType freqs[3] = {frq1, frq2, frq3}; - - /* m-rows measurements, n-cols unknowns */ - int m = 2 * nf + 1; - int n = 2 + nf; - VectorXd Z = VectorXd::Zero (m); - MatrixXd R = MatrixXd::Identity (m, m); - MatrixXd H = MatrixXd::Zero (m, n); - - double lam1 = lam[frq1]; - int i = 0; - - //phase and code - for (int f = 0; f < nf; f++) - { - E_FType frqX = freqs[f]; - double lamX = lam[frqX]; - - Z[i] = lc .L_m[frqX] - - lc_pre.L_m[frqX]; R(i,i) = 1 / (2 * SQR(sigmaPhase)); H(i,0) = 1; - H(i,1) = -SQR(lamX) / SQR(lam1); - H(i,2 + f) = lamX; i++; - - Z[i] = lc .P [frqX] - - lc_pre.P [frqX]; R(i,i) = 1 / (2 * SQR(sigmaCode)); H(i,0) = 1; - H(i,1) = +SQR(lamX) / SQR(lam1); i++; - } - - //ionosphere - { - Z[i] = satStat.dIono; R(i,i) = 1 / SQR(satStat.sigmaIono); H(i,1) = 1; i++; - } - - /* 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); - if (ind == 0) - { - return; - } - - satStat.sigStatMap[ft2string(frq1)].slip. SCDIA = true; - satStat.sigStatMap[ft2string(frq2)].slip. SCDIA = true; - if (nf == 3) satStat.sigStatMap[ft2string(frq3)].slip. SCDIA = true; - satStat.sigStatMap[ft2string(frq1)].savedSlip. SCDIA = true; - satStat.sigStatMap[ft2string(frq2)].savedSlip. SCDIA = true; - if (nf == 3) satStat.sigStatMap[ft2string(frq3)].savedSlip. SCDIA = true; - - VectorXd xp = VectorXd::Zero(n); - MatrixXd Pp = MatrixXd::Zero(n, n); - - if (filterMode == +E_FilterMode::LSQ) - { - MatrixXd N = MatrixXd::Zero (n, m); - VectorXd N1 = VectorXd::Zero (n); - matmul("TN", n, m, m, 1, H.data(), R.data(), 0, N.data()); /* H'*R */ - matmul("NN", n, n, m, 1, N.data(), H.data(), 0, Pp.data()); /* H'*R*H */ - matmul("NN", n, 1, m, 1, N.data(), Z.data(), 0, N1.data()); /* Nl=H'*R*Z */ - if (!matinv(Pp.data(), n)) - { - matmul("NN", n, 1, n, 1, Pp.data(), N1.data(), 0, xp.data()); - } - /* store float solution and vc matrix */ - matcpy(satStat.flt.a, xp.data() + 2, 1, nf); - - for (int i = 0; i < nf; i++) - for (int j = 0; j < nf; j++) - satStat.flt.Qa[i][j] = Pp.data()[(i + 2) * n + j + 2]; - } - else - { - satStat.flt.ne++; - if (satStat.flt.ne < 2) - { - satStat.flt.slip = 0; - satStat.flt.ne = 0; - - return; - } - - VectorXd x = VectorXd::Zero (n); - matcpy(x.data() + 2, satStat.flt.a, 1, nf); - - /* time update */ - MatrixXd Px = MatrixXd::Zero(n, n); - for (int i = 0; i < nf; i++) - for (int j = 0; j < nf; j++) - Px.data()[(i + 2) * n + j + 2] = satStat.flt.Qa[i][j]; - - Px.data()[0] = 1E6; - Px.data()[1 + n] = 1E6; - - /* measurement-prediction */ - matmul("NN", m, 1, n, -1, H.data(), x.data(), 1, Z.data()); - - /* transpose of desgin matrix */ - MatrixXd I = MatrixXd::Identity(m, m); - MatrixXd H1 = MatrixXd::Zero (n, m); - matmul("TN", n, m, m, +1, H.data(), I.data(), 0, H1.data()); - - /* measurement update */ - if (!matinv(R.data(), m)) - filter_(x.data(), Px.data(), H1.data(), Z.data(), R.data(), n, m, xp.data(), Pp.data()); - - matcpy(satStat.flt.a, xp.data() + 2, 1, nf); - - for (int i = 0; i < nf; i++) - for (int j = 0; j < nf; j++) - satStat.flt.Qa[i][j] = Pp.data()[(i + 2) * n + j + 2]; - } - - /* ambiguity vector and its variance */ - VectorXd a = VectorXd::Zero(nf); - matcpy(a.data(), xp.data() + n - nf, nf, 1); - - MatrixXd Qa = MatrixXd::Zero(nf, nf); - for (int i = 0; i < nf; i++) - for (int j = 0; j < nf; j++) - { - Qa.data()[i * nf + j] = Pp.data()[(n - nf + i) * n + j + n - nf]; - } - - /* integer cycle slip estimation */ - MatrixXd F = MatrixXd::Zero(nf, 2); - double s[2]; - lambda(trace, nf, 2, a.data(), Qa.data(), F.data(), s, acsConfig.predefined_fail, pass); - - if (filterMode == +E_FilterMode::LSQ) - { - /* least-squares */ - satStat.amb[0] = 0; - satStat.amb[1] = 0; - satStat.amb[2] = 0; - tracepdeex(2, trace, "(freq=%d) ", nf); - if (pass) - { - tracepdeex(2, trace, "fixed "); - for (int i = 0; i < 3; i++) - satStat.amb[i] = ROUND(F.data()[i]); - - for (auto& [key, sigStat] : satStat.sigStatMap) - { - sigStat.slip.SCDIA = true; - } - } - } - else - { - /* kalman filter */ - satStat.flt.amb[0] = 0; - satStat.flt.amb[1] = 0; - satStat.flt.amb[2] = 0; - if (pass) - { - memset(satStat.flt.a, 0, 3 * sizeof (double)); - memset(satStat.flt.Qa, 0, 9); //todo aaron, looks sketchy - satStat.flt.slip |= 2; - tracepdeex(1, trace, " ACC fixed "); - for (int i = 0; i < nf; i++) - { - satStat.flt.amb[i] = ROUND(F.data()[i]); - } - } - tracepdeex(1, trace, "ACC epoch used=%2d\n", satStat.flt.ne); - if (pass) - satStat.flt.ne = 0; - } -} - -/** Cycle slip detection for dual-frequency -*/ -void cycleslip2( - Trace& trace, ///< Trace to output to - SatStat& satStat, ///< Persistant satellite status parameters - lc_t& lcBase, ///< Linear combinations - GObs& obs) ///< Navigation object for this satellite -{ - GWeek week = lcBase.time; - GTow tow = lcBase.time; - - auto& recOpts = acsConfig.getRecOpts(obs.mount); - - double dt = (lcBase.time - satStat.lc_pre.time).to_double(); - - if ( dt < 20 - ||dt > PDEGAP) - { - // small interval or reset - - satStat.dIono = 0; - // approximation of ionosphere residual - - satStat.sigmaIono = PROC_NOISE_IONO * SQRT(dt); - } - else - { - // medium interval ~30s - - if (satStat.dIono == 0) - { - satStat.sigmaIono = PROC_NOISE_IONO * SQRT(dt); - } - } - - if (satStat.sigmaIono == 0) - { - 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) - { - return; - } - - auto& lam = obs.satNav_ptr->lamMap; - - double lam1 = lam[frq1]; - double lam2 = lam[frq2]; - - double lamw = lam1 * lam2 / (lam2 - lam1); //todo aaron, 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); - - double sigmaGF = 2 * sigmaPhase; - - S_LC lcNew = getLC(lcBase, frq1, frq2); - S_LC lcPre = getLC(satStat.lc_pre, frq1, frq2); - - double mwNoise = mwnoise(sigmaCode, sigmaPhase, lam1, lam2); - - /* averaged MW measurement and noise */ - double fNw; - if (acsConfig.preprocOpts.mw_proc_noise) { fNw = lcNew.MW_c - satStat.mwSlip.mean; } - else { fNw = lcNew.MW_c - lcPre.MW_c; } /* Eq (6) in TN */ - - /* clock jump */ - if (fabs(fNw * lamw) > 10e-3 * CLIGHT) - { - tracepdeex(1, trace, "Potential clock jump rather than cycle slip -cs2\n"); - } - - double deltaGF = lcNew.GF_Phas_m - - lcPre.GF_Phas_m; /* Eq (9) in TN */ - - tracepdeex(2, trace, "\nPDE-CS GPST DUAL %4d %8.1f %4s %5.2f %5.3f %8.4f %7.4f %8.4f ", - week, tow, lcBase.Sat.id().c_str(), satStat.el * R2D, lamw, deltaGF, fNw, sigmaGF); - - /* cycle slip detection */ - if (satStat.el >= recOpts.elevation_mask_deg * D2R) - { - scdia(trace, satStat, lcBase, lam, sigmaPhase, sigmaCode, 2, sys, E_FilterMode::LSQ); - } - - /* update TD ionosphere residual */ - if ( satStat.sigStatMap[ft2string(frq1)].slip.any == 0 - &&satStat.sigStatMap[ft2string(frq2)].slip.any == 0) - { - satStat.dIono = deltaGF / coef; - satStat.sigmaIono = sigmaGF / coef; - } -} - -/** Cycle slip detection and repair for triple-frequency -*/ -void cycleslip3( - Trace& trace, ///< Trace to output to - SatStat& satStat, ///< Persistant satellite status parameters - lc_t& lc, ///< Linear combinations - GObs& obs) ///< Navigation object for this satellite -{ - GWeek week = lc.time; - GTow tow = lc.time; - - auto& recOpts = acsConfig.getRecOpts(obs.mount); - - double dt = (lc.time - satStat.lc_pre.time).to_double(); - - /* small interval */ - if (dt < 20) - { - satStat.dIono = 0; - - /* approximation of ionosphere residual */ - satStat.sigmaIono = PROC_NOISE_IONO * SQRT(dt); - } - else - { - /* large interval */ - if (satStat.sigmaIono == 0) - { - satStat.sigmaIono = PROC_NOISE_IONO * SQRT(dt); - } - } - - if (satStat.sigmaIono == 0) - { - 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) - return; - - auto& lam = obs.satNav_ptr->lamMap; - double lam1 = lam[frq1]; - double lam2 = lam[frq2]; - double lam5 = lam[frq3]; - - /* TD MW noise (m) */ - double lamew = lam2 * lam5 / (lam5 - lam2); - if (lamew < 0) - lamew *= -1; - - /* elevation dependent noise */ - double sigmaCode = sqrt(obs.sigs.begin()->second.codeVar); - double sigmaPhase = sqrt(obs.sigs.begin()->second.phasVar); - - double mwNoise12 = mwnoise(sigmaCode, sigmaPhase, lam1, lam2); - double mwNoise15 = mwnoise(sigmaCode, sigmaPhase, lam1, lam5); - double mwNoise25 = mwnoise(sigmaCode, sigmaPhase, lam2, lam5); - - - double sigmaGF = 2 * sigmaPhase; /* TD GF noise */ - - S_LC lc25new = getLC(lc, frq2, frq3); - S_LC lc25pre = getLC(satStat.lc_pre, frq2, frq3); - - /* averaged EMW measurement and noise */ - double fNew; -// double sigmaEMW; - if (acsConfig.preprocOpts.mw_proc_noise) { fNew = lc25new.MW_c - satStat.emwSlip.mean; } - else { fNew = lc25new.MW_c - lc25pre.MW_c; } /* Eq (13) in TN */ - - double deltaGF25 = lc25new.GF_Phas_m - - lc25pre.GF_Phas_m; - - /* clock jump */ - if (fabs(fNew * lamew) > 10e-3 * CLIGHT) - { - fprintf(stdout, "Potential clock jump rather than cycle slip -cs3\n"); - return; - } - - /* ionosphere coefficient for L2 & L5 */ - double coef1 = SQR(CLIGHT / lam1) / SQR(CLIGHT / lam5) - SQR(CLIGHT / lam1) / SQR(CLIGHT / lam2); - if (coef1 < 0) - coef1 = -coef1; - - S_LC lcNew = getLC(lc, frq1, frq2); - S_LC lcPre = getLC(satStat.lc_pre, frq1, frq2); - - double lamw = lam1 * lam2 / (lam2 - lam1); - - double coef = SQR(lam2) / SQR(lam1) - 1; // ionosphere coefficient for L1 & LX - - /* averaged MW measurement and noise */ - double fNw; - if (acsConfig.preprocOpts.mw_proc_noise) { fNw = lcNew.MW_c - satStat.mwSlip.mean; } - else { fNw = lcNew.MW_c - lcPre.MW_c; } /* Eq (6) in TN */ - - double deltaGF = lcNew.GF_Phas_m - - lcPre.GF_Phas_m; - - tracepdeex(2, trace, "\nPDE-CS GPST TRIP %4d %8.1f %4s %5.2f %5.3f %8.4f %7.4f %8.4f %6.2f %8.4f %7.4f ", week, - tow, lc.Sat.id().c_str(), satStat.el * R2D, lamw, deltaGF, fNw, sigmaGF, lamew, deltaGF25, fNew); - - if (satStat.el >= recOpts.elevation_mask_deg * D2R) - { - scdia(trace, satStat, lc, lam, sigmaPhase, sigmaCode, 3, sys, E_FilterMode::LSQ); - } - - /* update TD ionosphere residual */ - if ( satStat.sigStatMap[ft2string(frq1)].slip.any == 0 - &&satStat.sigStatMap[ft2string(frq2)].slip.any == 0 - &&satStat.sigStatMap[ft2string(frq3)].slip.any == 0) - { - satStat.dIono = deltaGF / coef; - satStat.sigmaIono = sigmaGF / coef; - } -} - -/** Cycle slip detection and repair -*/ -void detectslip( - Trace& trace, ///< Trace to output to - SatStat& satStat, ///< Persistant satellite status parameters - lc_t& lc_new, ///< Linear combination for this epoch - lc_t& lc_old, ///< Linear combination from previous epoch - GObs& obs) ///< Navigation object for this satellite -{ - bool dualFreq = false; - E_Sys sys = lc_new.Sat.sys; - - char id[32]; - lc_new.Sat.getId(id); - - GWeek week = lc_new.time; - GTow tow = lc_new.time; - - auto& recOpts = acsConfig.getRecOpts(obs.mount); - - if (acsConfig.process_sys[sys] == false) - return; - - E_FType frq1; - E_FType frq2; - E_FType frq3; - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - if (pass == false) - { - return; - } - - /* first epoch or large gap or low elevation */ //todo aaron initialisation stuff, remove - if ( satStat.lc_pre.time.bigTime == 0 - || satStat.el < recOpts.elevation_mask_deg * D2R - || lc_new.time > lc_old.time + PDEGAP) - { - satStat.mwSlip = {}; - satStat.emwSlip = {}; - - if (lc_new.time > lc_old.time + PDEGAP) tracepdeex(1, trace, "\nPDE-CS GPST %4d %8.1f %4s %5.2f --time gap --", week, tow, id, satStat.el * R2D); - if (satStat.el < recOpts.elevation_mask_deg * D2R) tracepdeex(1, trace, "\nPDE-CS GPST %4d %8.1f %4s %5.2f --low_elevation --", week, tow, id, satStat.el * R2D); - else tracepdeex(1, trace, "\nPDE-CS GPST %4d %8.1f %4s %5.2f --satStat.lc_pre.time.time --", week, tow, id, satStat.el * R2D); - - return; - } - - if ( lc_new.L_m[frq1] != 0 - &&lc_new.L_m[frq2] != 0 - &&lc_new.L_m[frq3] == 0) - { - dualFreq = true; - } - - if ( dualFreq - &&lc_old.L_m[frq1] != 0 - &&lc_old.L_m[frq2] != 0) - { - cycleslip2(trace, satStat, lc_new, obs); - - /* update averaged MW noise when no cycle slip */ - if ( satStat.sigStatMap[ft2string(frq1)].slip.any == 0 - &&satStat.sigStatMap[ft2string(frq2)].slip.any == 0) - { - S_LC& lc12 = getLC(lc_new, frq1, frq2); - lowPassFilter(satStat.mwSlip, lc12.MW_c, acsConfig.preprocOpts.mw_proc_noise); - } - else - { - satStat.mwSlip = {}; - } - } - /* 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. - { - /* set slip flag for L5 (introduce new ambiguity for L5) */ - satStat.sigStatMap[ft2string(frq3)].slip.LLI = true; - cycleslip2(trace, satStat, lc_new, obs); - - /* update averaged MW noise when no cycle slip */ - if ( satStat.sigStatMap[ft2string(frq1)].slip.any == 0 - &&satStat.sigStatMap[ft2string(frq2)].slip.any == 0) - { - S_LC& lc12 = getLC(lc_new, frq1, frq2); - lowPassFilter(satStat.mwSlip, lc12.MW_c, acsConfig.preprocOpts.mw_proc_noise); - } - else - { - satStat.mwSlip = {}; - } - } - /* 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) - { - cycleslip3(trace, satStat, lc_new, obs); - - if (satStat.el * R2D > 30) - { - if ( satStat.sigStatMap[ft2string(frq1)].slip.any == 2 //todo aaron, check the 2 - &&satStat.amb[0] == 0 - &&satStat.amb[1] == 0 - &&satStat.amb[2] == 0) - { - satStat.sigStatMap[ft2string(frq1)].slip.any = 0; - satStat.sigStatMap[ft2string(frq2)].slip.any = 0; - satStat.sigStatMap[ft2string(frq3)].slip.any = 0; - } - } - - /*update averaged MW25 noise when no cycle slip */ - if ( satStat.sigStatMap[ft2string(frq1)].slip.any == 0 - &&satStat.sigStatMap[ft2string(frq2)].slip.any == 0 - &&satStat.sigStatMap[ft2string(frq3)].slip.any == 0) - { - S_LC& lc25 = getLC(lc_new, frq2, frq3); - lowPassFilter(satStat.emwSlip, lc25.MW_c, acsConfig.preprocOpts.mw_proc_noise); - } - else - { - satStat.emwSlip = {}; - } - } - /* track L1 or L2 again, new rising satellite */ - else if ( dualFreq - &&( lc_old.L_m[frq1] == 0 - ||lc_old.L_m[frq2] == 0)) - { - satStat.flt.slip = 0; - satStat.flt.ne = 0; - for (auto& [key, sigStat] : satStat.sigStatMap) - { - sigStat.slip.LLI = true; - } - - tracepdeex(1, trace, "\nPDE-CS GPST %4d %8.1f %4s %5.2f -- re-tracking --\n", week, tow, id, satStat.el * R2D); - } - else - { - satStat.flt.slip = 0; - satStat.flt.ne = 0; - for (auto& [key, sigStat] : satStat.sigStatMap) - { - sigStat.slip.LLI = true; - } - - tracepdeex(1, trace, "\nPDE-CS GPST %4d %8.1f %4s %5.2f --single frequency--\n", week, tow, id, satStat.el * R2D); - } -} - -void clearSlips( - ObsList& obsList) -{ - //clear non-persistent status values. - for (auto& obs : only(obsList)) - { - if (acsConfig.process_sys[obs.Sat.sys] == false) - { - continue; - } - - auto& satOpts = acsConfig.getSatOpts(obs.Sat); - - if (satOpts.exclude) - { - continue; - } - - for (auto& [sigKey, sigStat] : obs.satStat_ptr->sigStatMap) - { - SatStat& satStat = *(obs.satStat_ptr); - - satStat.slip = false; //todo aaron, is this used? - sigStat.slip.any = 0; - } - } -} - -/** Detect slips for multiple observations -*/ -void detectslips( - Trace& trace, ///< Trace to output to - ObsList& obsList) ///< List of observations to detect slips within -{ - tracepdeex(2, trace, "\n *-------- PDE cycle slip detection & repair --------*\n"); - - detslp_ll(trace, obsList); - detslp_gf(trace, obsList); - detslp_mw(trace, obsList); - - tracepdeex(2, trace, "\nPDE-CS GPST week sec prn el lamw gf12 mw12 siggf sigmw lamew gf25 mw25 LC N1 N2 N5\n"); - - for (auto& obs : only(obsList)) - { - if (obs.exclude) - { - continue; - } - - SatStat& satStat = *(obs.satStat_ptr); - - detectslip(trace, satStat, satStat.lc_new, satStat.lc_pre, obs); - - for (auto& [ft, sig] : obs.sigs) - { - auto& sigStat = obs.satStat_ptr->sigStatMap[ft2string(ft)]; - - if (sigStat.slip.any) - { - satStat.slip = true; - } - } - } -} +// #pragma GCC optimize ("O0") + +#include "common/acsQC.hpp" +#include +#include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/satStat.hpp" +#include "common/trace.hpp" +#include "rtklib/lambda.h" + +#define THRES_MW_JUMP 10.0 +#define PDEGAP 60.0 +#define PDESLIPTHRESHOLD 0.5 +#define PROC_NOISE_IONO 0.001 + +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; + + if (acsConfig.code_priorities.find(sys) == acsConfig.code_priorities.end()) + return false; + + for (auto& code : acsConfig.code_priorities[sys]) + { + E_FType ft = code2Freq[sys][code]; + + if (ft1Ready == false) + { + ft1 = ft; + ft1Ready = true; + continue; + } + + if (ft == ft1) + continue; + + if (ft2Ready == false) + { + ft2 = ft; + ft2Ready = true; + continue; + } + + if (ft == ft2) + continue; + + { + ft3 = ft; + break; + } + } + + return true; +} +/** Detect cycle slip by reported loss of lock + */ +void detslp_ll( + Trace& trace, ///< Trace to output to + ObsList& obsList ///< List of observations to detect slips within +) +{ + if (obsList.empty()) + { + tracepdeex(3, trace, "\n%s: epoch=? n=%zu (empty obsList)", __FUNCTION__, obsList.size()); + return; + } + + // Find first non-null element for the timestamp + std::string epoch = "?"; + for (const auto& sp : obsList) + { + if (sp) + { + epoch = sp->time.to_string(2); + break; + } + } + + tracepdeex(3, trace, "\n%s: epoch=%s n=%zu", __FUNCTION__, epoch.c_str(), obsList.size()); + + // auto begin_iter = boost::make_filter_iterator([] + + for (auto& obs : only(obsList)) + for (auto& [ft, sig] : obs.sigs) + { + if (obs.exclude) + { + continue; + } + + // removed unused variable 'f' + if (sig.L == 0 || (sig.LLI & 0x03) == 0) + { + continue; + } + + tracepdeex( + 3, + 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) + ); + + obs.satStat_ptr->sigStatMap[ft2string(ft)].slip.LLI = true; + obs.satStat_ptr->sigStatMap[ft2string(ft)].savedSlip.LLI = true; + } +} + +/** Detect cycle slip by geometry free phase jump + */ +void detslp_gf( + Trace& trace, ///< Trace to output to + ObsList& obsList ///< List of observations to detect slips within +) +{ + if (obsList.empty()) + { + tracepdeex(3, trace, "\n%s: epoch=? n=%zu (empty obsList)", __FUNCTION__, obsList.size()); + return; + } + + // Find first non-null element for the timestamp + std::string epoch = "?"; + for (const auto& sp : obsList) + { + if (sp) + { + epoch = sp->time.to_string(2); + break; + } + } + + tracepdeex(3, trace, "\n%s: epoch=%s n=%zu", __FUNCTION__, epoch.c_str(), obsList.size()); + + for (auto& obs : only(obsList)) + { + if (obs.exclude) + { + continue; + } + + E_FType frq1; + E_FType frq2; + E_FType frq3; + bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); + if (pass == false) + continue; + + S_LC& lc = getLC(obs.satStat_ptr->lc_new, frq1, frq2); + + double gf1 = lc.GF_Phas_m; + if (lc.valid == false || gf1 == 0) + { + continue; + } + + double gf0 = obs.satStat_ptr->gf; + obs.satStat_ptr->gf = gf1; + + if (gf0 == 0) + { + 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; + } + } +} + +/** Detect slip by Melbourne-Wubbena linear combination jump + */ +void detslp_mw( + Trace& trace, ///< Trace to output to + ObsList& obsList ///< List of observations to detect slips within +) +{ + if (obsList.empty()) + { + tracepdeex(3, trace, "\n%s: epoch=? n=%zu (empty obsList)", __FUNCTION__, obsList.size()); + return; + } + + // Find first non-null element for the timestamp + std::string epoch = "?"; + for (const auto& sp : obsList) + { + if (sp) + { + epoch = sp->time.to_string(2); + break; + } + } + + tracepdeex(3, trace, "\n%s: epoch=%s n=%zu", __FUNCTION__, epoch.c_str(), obsList.size()); + + for (auto& obs : only(obsList)) + { + if (obs.exclude) + { + continue; + } + + E_FType frq1; + E_FType frq2; + E_FType frq3; + bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); + if (pass == false) + continue; + + S_LC& lc = getLC(obs.satStat_ptr->lc_new, frq1, frq2); + + double mw1 = lc.MW_c; + if (lc.valid == false || mw1 == 0) + { + continue; + } + + double mw0 = obs.satStat_ptr->mw; + obs.satStat_ptr->mw = mw1; + + if (mw0 == 0) + { + 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; + } + } +} + +/** Melbourne-Wenbunna (MW) measurement noise (m) + */ +double mwnoise( + double sigcode, ///< Code noise + double sigphase, ///< Phase noise + double lam1, ///< L1 wavelength + double lam2 ///< L2 wavelength +) +{ + double a = + lam2 * lam2 / (lam2 + lam1) / (lam2 + lam1) + lam1 * lam1 / (lam2 + lam1) / (lam2 + lam1); + double b = + lam2 * lam2 / (lam2 - lam1) / (lam2 - lam1) + lam1 * lam1 / (lam2 - lam1) / (lam2 - lam1); + return SQRT(a * SQR(sigcode) + b * SQR(sigphase)); +} + +/** Single channel detection–identification–adaptation (DIA) for integer cycle slips + */ +void scdia( + Trace& trace, ///< Trace to output to + SatStat& satStat, ///< Persistant satellite status parameters + lc_t& lc, ///< Linear combinations + 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 +) +{ + 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) + lc_pre_ptr = &satStat.lc_pre; + else + lc_pre_ptr = &satStat.flt.lc_pre; + if (nf == 1) + lc_pre_ptr = &satStat.flt.lc_pre; + + auto& lc_pre = *lc_pre_ptr; + + /* single frequency not supported in current PDE */ + if (nf == 1) + { + return; + } + + E_FType freqs[3] = {frq1, frq2, frq3}; + + /* m-rows measurements, n-cols unknowns */ + int m = 2 * nf + 1; + int n = 2 + nf; + VectorXd Z = VectorXd::Zero(m); + MatrixXd R = MatrixXd::Identity(m, m); + MatrixXd H = MatrixXd::Zero(m, n); + + double lam1 = lam[frq1]; + int i = 0; + + // phase and code + for (int f = 0; f < nf; f++) + { + E_FType frqX = freqs[f]; + double lamX = lam[frqX]; + + Z[i] = lc.L_m[frqX] - lc_pre.L_m[frqX]; + R(i, i) = 1 / (2 * SQR(sigmaPhase)); + H(i, 0) = 1; + H(i, 1) = -SQR(lamX) / SQR(lam1); + H(i, 2 + f) = lamX; + i++; + + Z[i] = lc.P[frqX] - lc_pre.P[frqX]; + R(i, i) = 1 / (2 * SQR(sigmaCode)); + H(i, 0) = 1; + H(i, 1) = +SQR(lamX) / SQR(lam1); + i++; + } + + // ionosphere + { + Z[i] = satStat.dIono; + R(i, i) = 1 / SQR(satStat.sigmaIono); + H(i, 1) = 1; + i++; + } + + /* 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); + if (ind == 0) + { + return; + } + + satStat.sigStatMap[ft2string(frq1)].slip.SCDIA = true; + satStat.sigStatMap[ft2string(frq2)].slip.SCDIA = true; + if (nf == 3) + satStat.sigStatMap[ft2string(frq3)].slip.SCDIA = true; + satStat.sigStatMap[ft2string(frq1)].savedSlip.SCDIA = true; + satStat.sigStatMap[ft2string(frq2)].savedSlip.SCDIA = true; + if (nf == 3) + satStat.sigStatMap[ft2string(frq3)].savedSlip.SCDIA = true; + + VectorXd xp = VectorXd::Zero(n); + MatrixXd Pp = MatrixXd::Zero(n, n); + + if (filterMode == E_FilterMode::LSQ) + { + MatrixXd N = MatrixXd::Zero(n, m); + VectorXd N1 = VectorXd::Zero(n); + matmul("TN", n, m, m, 1, H.data(), R.data(), 0, N.data()); /* H'*R */ + matmul("NN", n, n, m, 1, N.data(), H.data(), 0, Pp.data()); /* H'*R*H */ + matmul("NN", n, 1, m, 1, N.data(), Z.data(), 0, N1.data()); /* Nl=H'*R*Z */ + if (!matinv(Pp.data(), n)) + { + matmul("NN", n, 1, n, 1, Pp.data(), N1.data(), 0, xp.data()); + } + /* store float solution and vc matrix */ + matcpy(satStat.flt.a, xp.data() + 2, 1, nf); + + for (int i = 0; i < nf; i++) + for (int j = 0; j < nf; j++) + satStat.flt.Qa[i][j] = Pp.data()[(i + 2) * n + j + 2]; + } + else + { + satStat.flt.ne++; + if (satStat.flt.ne < 2) + { + satStat.flt.slip = 0; + satStat.flt.ne = 0; + + return; + } + + VectorXd x = VectorXd::Zero(n); + matcpy(x.data() + 2, satStat.flt.a, 1, nf); + + /* time update */ + MatrixXd Px = MatrixXd::Zero(n, n); + for (int i = 0; i < nf; i++) + for (int j = 0; j < nf; j++) + Px.data()[(i + 2) * n + j + 2] = satStat.flt.Qa[i][j]; + + Px.data()[0] = 1E6; + Px.data()[1 + n] = 1E6; + + /* measurement-prediction */ + matmul("NN", m, 1, n, -1, H.data(), x.data(), 1, Z.data()); + + /* transpose of desgin matrix */ + MatrixXd I = MatrixXd::Identity(m, m); + MatrixXd H1 = MatrixXd::Zero(n, m); + matmul("TN", n, m, m, +1, H.data(), I.data(), 0, H1.data()); + + /* measurement update */ + if (!matinv(R.data(), m)) + filter_(x.data(), Px.data(), H1.data(), Z.data(), R.data(), n, m, xp.data(), Pp.data()); + + matcpy(satStat.flt.a, xp.data() + 2, 1, nf); + + for (int i = 0; i < nf; i++) + for (int j = 0; j < nf; j++) + satStat.flt.Qa[i][j] = Pp.data()[(i + 2) * n + j + 2]; + } + + /* ambiguity vector and its variance */ + VectorXd a = VectorXd::Zero(nf); + matcpy(a.data(), xp.data() + n - nf, nf, 1); + + MatrixXd Qa = MatrixXd::Zero(nf, nf); + for (int i = 0; i < nf; i++) + for (int j = 0; j < nf; j++) + { + Qa.data()[i * nf + j] = Pp.data()[(n - nf + i) * n + j + n - nf]; + } + + /* integer cycle slip estimation */ + MatrixXd F = MatrixXd::Zero(nf, 2); + double s[2]; + lambda(trace, nf, 2, a.data(), Qa.data(), F.data(), s, acsConfig.predefined_fail, pass); + + if (filterMode == E_FilterMode::LSQ) + { + /* least-squares */ + satStat.amb[0] = 0; + satStat.amb[1] = 0; + satStat.amb[2] = 0; + tracepdeex(2, trace, "(freq=%d) ", nf); + if (pass) + { + tracepdeex(2, trace, "fixed "); + for (int i = 0; i < 3; i++) + satStat.amb[i] = ROUND(F.data()[i]); + + for (auto& [key, sigStat] : satStat.sigStatMap) + { + sigStat.slip.SCDIA = true; + } + } + } + else + { + /* kalman filter */ + satStat.flt.amb[0] = 0; + satStat.flt.amb[1] = 0; + satStat.flt.amb[2] = 0; + if (pass) + { + memset(satStat.flt.a, 0, 3 * sizeof(double)); + memset(satStat.flt.Qa, 0, 9); // todo aaron, looks sketchy + satStat.flt.slip |= 2; + tracepdeex(1, trace, " ACC fixed "); + for (int i = 0; i < nf; i++) + { + satStat.flt.amb[i] = ROUND(F.data()[i]); + } + } + tracepdeex(1, trace, "ACC epoch used=%2d\n", satStat.flt.ne); + if (pass) + satStat.flt.ne = 0; + } +} + +/** Cycle slip detection for dual-frequency + */ +void cycleslip2( + Trace& trace, ///< Trace to output to + SatStat& satStat, ///< Persistant satellite status parameters + lc_t& lcBase, ///< Linear combinations + GObs& obs ///< Navigation object for this satellite +) +{ + string timeStr = lcBase.time.to_string(2); + + auto& recOpts = acsConfig.getRecOpts(obs.mount); + + double dt = (lcBase.time - satStat.lc_pre.time).to_double(); + + if (dt < 20 || dt > PDEGAP) + { + // small interval or reset + + satStat.dIono = 0; + // approximation of ionosphere residual + + satStat.sigmaIono = PROC_NOISE_IONO * SQRT(dt); + } + else + { + // medium interval ~30s + + if (satStat.dIono == 0) + { + satStat.sigmaIono = PROC_NOISE_IONO * SQRT(dt); + } + } + + if (satStat.sigmaIono == 0) + { + 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) + { + return; + } + + auto& lam = obs.satNav_ptr->lamMap; + + double lam1 = lam[frq1]; + double lam2 = lam[frq2]; + + double lamw = lam1 * lam2 / (lam2 - lam1); // todo aaron, 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); + + double sigmaGF = 2 * sigmaPhase; + + S_LC lcNew = getLC(lcBase, frq1, frq2); + S_LC lcPre = getLC(satStat.lc_pre, frq1, frq2); + + double mwNoise = mwnoise(sigmaCode, sigmaPhase, lam1, lam2); + + /* averaged MW measurement and noise */ + double fNw; + if (acsConfig.preprocOpts.mw_proc_noise) + { + fNw = lcNew.MW_c - satStat.mwSlip.mean; + } + else + { + fNw = lcNew.MW_c - lcPre.MW_c; + } /* Eq (6) in TN */ + + /* clock jump */ + if (fabs(fNw * lamw) > 10e-3 * CLIGHT) + { + tracepdeex(1, trace, "Potential clock jump rather than cycle slip -cs2\n"); + } + + double deltaGF = lcNew.GF_Phas_m - lcPre.GF_Phas_m; /* Eq (9) in TN */ + + tracepdeex( + 2, + trace, + "\nPDE-CS GPST DUAL %s %4s %5.2f %5.3f %8.4f %7.4f %8.4f " + " ", + timeStr.c_str(), + lcBase.Sat.id().c_str(), + satStat.el * R2D, + lamw, + deltaGF, + fNw, + sigmaGF + ); + + /* cycle slip detection */ + if (satStat.el >= recOpts.elevation_mask_deg * D2R) + { + scdia(trace, satStat, lcBase, lam, sigmaPhase, sigmaCode, 2, sys, E_FilterMode::LSQ); + } + + /* update TD ionosphere residual */ + if (satStat.sigStatMap[ft2string(frq1)].slip.any == 0 && + satStat.sigStatMap[ft2string(frq2)].slip.any == 0) + { + satStat.dIono = deltaGF / coef; + satStat.sigmaIono = sigmaGF / coef; + } +} + +/** Cycle slip detection and repair for triple-frequency + */ +void cycleslip3( + Trace& trace, ///< Trace to output to + SatStat& satStat, ///< Persistant satellite status parameters + lc_t& lc, ///< Linear combinations + GObs& obs ///< Navigation object for this satellite +) +{ + string timeStr = lc.time.to_string(2); + + auto& recOpts = acsConfig.getRecOpts(obs.mount); + + double dt = (lc.time - satStat.lc_pre.time).to_double(); + + /* small interval */ + if (dt < 20) + { + satStat.dIono = 0; + + /* approximation of ionosphere residual */ + satStat.sigmaIono = PROC_NOISE_IONO * SQRT(dt); + } + else + { + /* large interval */ + if (satStat.sigmaIono == 0) + { + satStat.sigmaIono = PROC_NOISE_IONO * SQRT(dt); + } + } + + if (satStat.sigmaIono == 0) + { + 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) + return; + + auto& lam = obs.satNav_ptr->lamMap; + double lam1 = lam[frq1]; + double lam2 = lam[frq2]; + double lam5 = lam[frq3]; + + /* TD MW noise (m) */ + double lamew = lam2 * lam5 / (lam5 - lam2); + if (lamew < 0) + lamew *= -1; + + /* elevation dependent noise */ + double sigmaCode = sqrt(obs.sigs.begin()->second.codeVar); + double sigmaPhase = sqrt(obs.sigs.begin()->second.phasVar); + + double mwNoise12 = mwnoise(sigmaCode, sigmaPhase, lam1, lam2); + double mwNoise15 = mwnoise(sigmaCode, sigmaPhase, lam1, lam5); + double mwNoise25 = mwnoise(sigmaCode, sigmaPhase, lam2, lam5); + + double sigmaGF = 2 * sigmaPhase; /* TD GF noise */ + + S_LC lc25new = getLC(lc, frq2, frq3); + S_LC lc25pre = getLC(satStat.lc_pre, frq2, frq3); + + /* averaged EMW measurement and noise */ + double fNew; + // double sigmaEMW; + if (acsConfig.preprocOpts.mw_proc_noise) + { + fNew = lc25new.MW_c - satStat.emwSlip.mean; + } + else + { + fNew = lc25new.MW_c - lc25pre.MW_c; + } /* Eq (13) in TN */ + + double deltaGF25 = lc25new.GF_Phas_m - lc25pre.GF_Phas_m; + + /* clock jump */ + if (fabs(fNew * lamew) > 10e-3 * CLIGHT) + { + fprintf(stdout, "Potential clock jump rather than cycle slip -cs3\n"); + return; + } + + /* ionosphere coefficient for L2 & L5 */ + double coef1 = + SQR(CLIGHT / lam1) / SQR(CLIGHT / lam5) - SQR(CLIGHT / lam1) / SQR(CLIGHT / lam2); + if (coef1 < 0) + coef1 = -coef1; + + S_LC lcNew = getLC(lc, frq1, frq2); + S_LC lcPre = getLC(satStat.lc_pre, frq1, frq2); + + double lamw = lam1 * lam2 / (lam2 - lam1); + + double coef = SQR(lam2) / SQR(lam1) - 1; // ionosphere coefficient for L1 & LX + + /* averaged MW measurement and noise */ + double fNw; + if (acsConfig.preprocOpts.mw_proc_noise) + { + fNw = lcNew.MW_c - satStat.mwSlip.mean; + } + else + { + fNw = lcNew.MW_c - lcPre.MW_c; + } /* Eq (6) in TN */ + + double deltaGF = lcNew.GF_Phas_m - lcPre.GF_Phas_m; + + tracepdeex( + 2, + trace, + "\nPDE-CS GPST TRIP %s %4s %5.2f %5.3f %8.4f %7.4f %8.4f %6.2f %8.4f %7.4f ", + timeStr.c_str(), + lc.Sat.id().c_str(), + satStat.el * R2D, + lamw, + deltaGF, + fNw, + sigmaGF, + lamew, + deltaGF25, + fNew + ); + + if (satStat.el >= recOpts.elevation_mask_deg * D2R) + { + scdia(trace, satStat, lc, lam, sigmaPhase, sigmaCode, 3, sys, E_FilterMode::LSQ); + } + + /* update TD ionosphere residual */ + if (satStat.sigStatMap[ft2string(frq1)].slip.any == 0 && + satStat.sigStatMap[ft2string(frq2)].slip.any == 0 && + satStat.sigStatMap[ft2string(frq3)].slip.any == 0) + { + satStat.dIono = deltaGF / coef; + satStat.sigmaIono = sigmaGF / coef; + } +} + +/** Cycle slip detection and repair + */ +void detectslip( + Trace& trace, ///< Trace to output to + SatStat& satStat, ///< Persistant satellite status parameters + lc_t& lc_new, ///< Linear combination for this epoch + lc_t& lc_old, ///< Linear combination from previous epoch + GObs& obs ///< Navigation object for this satellite +) +{ + bool dualFreq = false; + E_Sys sys = lc_new.Sat.sys; + + char id[32]; + lc_new.Sat.getId(id); + + string timeStr = lc_new.time.to_string(2); + + auto& recOpts = acsConfig.getRecOpts(obs.mount); + + if (acsConfig.process_sys[sys] == false) + return; + + E_FType frq1; + E_FType frq2; + E_FType frq3; + bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); + if (pass == false) + { + return; + } + + /* first epoch or large gap or low elevation */ // todo aaron initialisation stuff, remove + if (satStat.lc_pre.time.bigTime == 0 || satStat.el < recOpts.elevation_mask_deg * D2R || + lc_new.time > lc_old.time + PDEGAP) + { + satStat.mwSlip = {}; + satStat.emwSlip = {}; + + if (lc_new.time > lc_old.time + PDEGAP) + tracepdeex( + 1, + trace, + "\nPDE-CS GPST %s %4s %5.2f --time gap --", + timeStr.c_str(), + id, + satStat.el * R2D + ); + if (satStat.el < recOpts.elevation_mask_deg * D2R) + tracepdeex( + 1, + trace, + "\nPDE-CS GPST %s %4s %5.2f --low_elevation --", + timeStr.c_str(), + id, + satStat.el * R2D + ); + else + tracepdeex( + 1, + trace, + "\nPDE-CS GPST %s %4s %5.2f --satStat.lc_pre.time.time --", + timeStr.c_str(), + id, + satStat.el * R2D + ); + + return; + } + + if (lc_new.L_m[frq1] != 0 && lc_new.L_m[frq2] != 0 && lc_new.L_m[frq3] == 0) + { + dualFreq = true; + } + + if (dualFreq && lc_old.L_m[frq1] != 0 && lc_old.L_m[frq2] != 0) + { + cycleslip2(trace, satStat, lc_new, obs); + + /* update averaged MW noise when no cycle slip */ + if (satStat.sigStatMap[ft2string(frq1)].slip.any == 0 && + satStat.sigStatMap[ft2string(frq2)].slip.any == 0) + { + S_LC& lc12 = getLC(lc_new, frq1, frq2); + lowPassFilter(satStat.mwSlip, lc12.MW_c, acsConfig.preprocOpts.mw_proc_noise); + } + else + { + satStat.mwSlip = {}; + } + } + /* 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. + { + /* set slip flag for L5 (introduce new ambiguity for L5) */ + satStat.sigStatMap[ft2string(frq3)].slip.retrack = true; + satStat.sigStatMap[ft2string(frq3)].savedSlip.retrack = true; + cycleslip2(trace, satStat, lc_new, obs); + + /* update averaged MW noise when no cycle slip */ + if (satStat.sigStatMap[ft2string(frq1)].slip.any == 0 && + satStat.sigStatMap[ft2string(frq2)].slip.any == 0) + { + S_LC& lc12 = getLC(lc_new, frq1, frq2); + lowPassFilter(satStat.mwSlip, lc12.MW_c, acsConfig.preprocOpts.mw_proc_noise); + } + else + { + satStat.mwSlip = {}; + } + } + /* 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) + { + cycleslip3(trace, satStat, lc_new, obs); + + if (satStat.el * R2D > 30) + { + if (satStat.sigStatMap[ft2string(frq1)].slip.any == 2 // todo aaron, check the 2 + && satStat.amb[0] == 0 && satStat.amb[1] == 0 && satStat.amb[2] == 0) + { + satStat.sigStatMap[ft2string(frq1)].slip.any = 0; + satStat.sigStatMap[ft2string(frq2)].slip.any = 0; + satStat.sigStatMap[ft2string(frq3)].slip.any = 0; + } + } + + /*update averaged MW25 noise when no cycle slip */ + if (satStat.sigStatMap[ft2string(frq1)].slip.any == 0 && + satStat.sigStatMap[ft2string(frq2)].slip.any == 0 && + satStat.sigStatMap[ft2string(frq3)].slip.any == 0) + { + S_LC& lc25 = getLC(lc_new, frq2, frq3); + lowPassFilter(satStat.emwSlip, lc25.MW_c, acsConfig.preprocOpts.mw_proc_noise); + } + else + { + satStat.emwSlip = {}; + } + } + /* track L1 or L2 again, new rising satellite */ + else if (dualFreq && (lc_old.L_m[frq1] == 0 || lc_old.L_m[frq2] == 0)) + { + satStat.flt.slip = 0; + satStat.flt.ne = 0; + for (auto& [key, sigStat] : satStat.sigStatMap) + { + sigStat.slip.retrack = true; + sigStat.savedSlip.retrack = true; + } + + tracepdeex( + 1, + trace, + "\nPDE-CS GPST %s %4s %5.2f -- re-tracking --\n", + timeStr.c_str(), + id, + satStat.el * R2D + ); + } + else + { + satStat.flt.slip = 0; + satStat.flt.ne = 0; + for (auto& [key, sigStat] : satStat.sigStatMap) + { + sigStat.slip.singleFreq = true; + sigStat.savedSlip.singleFreq = true; + } + + tracepdeex( + 1, + trace, + "\nPDE-CS GPST %s %4s %5.2f --single frequency--\n", + timeStr.c_str(), + id, + satStat.el * R2D + ); + } +} + +void clearSlips(ObsList& obsList) +{ + // clear non-persistent status values. + for (auto& obs : only(obsList)) + { + if (acsConfig.process_sys[obs.Sat.sys] == false) + { + continue; + } + + auto& satOpts = acsConfig.getSatOpts(obs.Sat); + + if (satOpts.exclude) + { + continue; + } + + for (auto& [sigKey, sigStat] : obs.satStat_ptr->sigStatMap) + { + SatStat& satStat = *(obs.satStat_ptr); + + satStat.slip = false; // todo aaron, is this used? + sigStat.slip.any = 0; + } + } +} + +/** Detect slips for multiple observations + */ +void detectslips( + Trace& trace, ///< Trace to output to + ObsList& obsList ///< List of observations to detect slips within +) +{ + tracepdeex(2, trace, "\n *-------- PDE cycle slip detection & repair --------*\n"); + + detslp_ll(trace, obsList); + detslp_gf(trace, obsList); + detslp_mw(trace, obsList); + + tracepdeex( + 2, + trace, + "\nPDE-CS GPST epoch prn el lamw gf12 mw12 siggf " + "sigmw " + "lamew gf25 mw25 " + " LC N1 N2 N5\n" + ); + + for (auto& obs : only(obsList)) + { + if (obs.exclude) + { + continue; + } + + SatStat& satStat = *(obs.satStat_ptr); + + detectslip(trace, satStat, satStat.lc_new, satStat.lc_pre, obs); + + for (auto& [ft, sig] : obs.sigs) + { + auto& sigStat = obs.satStat_ptr->sigStatMap[ft2string(ft)]; + + if (sigStat.slip.any) + { + satStat.slip = true; + } + } + } +} diff --git a/src/cpp/common/acsQC.hpp b/src/cpp/common/acsQC.hpp index 94d3df09b..8f35011c3 100644 --- a/src/cpp/common/acsQC.hpp +++ b/src/cpp/common/acsQC.hpp @@ -1,46 +1,41 @@ - #pragma once -#include "trace.hpp" +#include "common/trace.hpp" struct ObsList; int lsqqc( - Trace& trace, - const double *H, - const double *P, - const double *Z, - double *v, - int m, - int n, - int ind, - int norb, - double *xo = nullptr, - double *Po = nullptr); + Trace& trace, + const double* H, + const double* P, + const double* Z, + double* v, + int m, + int n, + int ind, + int norb, + double* xo = nullptr, + double* Po = nullptr +); int chiqc( - Trace& trace, - const double *H, - const double *P, - const double *Z, - const double *xp, - double *v, - int m, - int n, - int ind); - -void clearSlips( - ObsList& obsList); - -void detectslips( - Trace& trace, - ObsList& obsList); - -void detslp_gf( - ObsList& obsList); - -void detslp_mw( - ObsList& obsList); - -void detslp_ll( - ObsList& obsList); + Trace& trace, + const double* H, + const double* P, + const double* Z, + const double* xp, + double* v, + int m, + int n, + int ind +); + +void clearSlips(ObsList& obsList); + +void detectslips(Trace& trace, ObsList& obsList); + +void detslp_gf(ObsList& obsList); + +void detslp_mw(ObsList& obsList); + +void detslp_ll(ObsList& obsList); diff --git a/src/cpp/common/algebra.cpp b/src/cpp/common/algebra.cpp index b2c74e432..92d703435 100644 --- a/src/cpp/common/algebra.cpp +++ b/src/cpp/common/algebra.cpp @@ -1,2265 +1,3868 @@ - +#include "common/algebra.hpp" +#include +#include +#include +#include #include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/algebraTrace.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" +#include "common/trace.hpp" + +using std::ostringstream; +using std::pair; /** Kalman Filter. * * This software uses specialised kalman filter classes to perform filtering. - * Using classes such as KFState, KFMeas, etc, prevents duplication of code, and ensures that many edge cases are taken care of - * without the need for the developer to consider them explicitly. + * Using classes such as KFState, KFMeas, etc, prevents duplication of code, and ensures that many + * edge cases are taken care of without the need for the developer to consider them explicitly. * * The basic workflow for using the filter is: * create filter object, - * create a list of measurements (only adding entries for required states, using KFKeys to reference the state element) - * combine measurements that in a list into a single matrix corresponding to the new state, - * and filtering - filtering internally saves the states for RTS code, using a single sequential file that has some headers added so that it can be traversed backwards + * create a list of measurements (only adding entries for required states, using KFKeys to reference + * the state element) combine measurements that in a list into a single matrix corresponding to the + * new state, and filtering - filtering internally saves the states for RTS code, using a single + * sequential file that has some headers added so that it can be traversed backwards * * The filter has some pre/post fit checks that remove measurements that are out of expected ranges, - * and there are functions provided for setting, resetting, and getting values, noises, and covariance values. + * and there are functions provided for setting, resetting, and getting values, noises, and + * covariance values. * - * KFKey objects are used to identify states. They may have a KF type, SatSys value, string (usually used for receiver id), and number associated with them, and can be set and read from filter objects as required. + * KFKey objects are used to identify states. They may have a KF type, SatSys value, string (usually + * used for receiver id), and number associated with them, and can be set and read from filter + * objects as required. * - * KFMeasEntry objects are used for an individual measurement, before being combined into KFMeas objects that contain all of the measurements for a filter iteration. + * KFMeasEntry objects are used for an individual measurement, before being combined into KFMeas + * objects that contain all of the measurements for a filter iteration. * - * Internally, the data is stored in maps and Eigen matrices/vectors, but the accessors should be used rather than the vectors themselves to ensure that states have been initialised and are in the expected order. + * Internally, the data is stored in maps and Eigen matrices/vectors, but the accessors should be + * used rather than the vectors themselves to ensure that states have been initialised and are in + * the expected order. * - * InitialState objects are created directly from yaml configurations, and contain the detailis about state transitions, including process noise, which are automatically added to the filter - * when a stateTransition() call is used, scaling any process noise according to the time gap since the last stateTransition. + * InitialState objects are created directly from yaml configurations, and contain the detailis + * about state transitions, including process noise, which are automatically added to the filter + * when a stateTransition() call is used, scaling any process noise according to the time gap since + * the last stateTransition. * * $$ K = HPH^\intercal + R $$ fgdfg */ ParallelArchitecture Kalman_Filter__() { - DOCS_REFERENCE(Binary_Archive__); + DOCS_REFERENCE(Binary_Archive__); } - - -#include -#include - -using std::ostringstream; -using std::pair; - -#include -#include - -#include "interactiveTerminal.hpp" -#include "eigenIncluder.hpp" -#include "algebraTrace.hpp" -#include "mongoWrite.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "mongo.hpp" -#include "trace.hpp" - // #pragma GCC optimize ("O0") const KFKey KFState::oneKey = {.type = KF::ONE}; - -bool KFKey::operator ==(const KFKey& b) const +bool KFKey::operator==(const KFKey& b) const { - if (str != b.str) return false; - if (Sat != b.Sat) return false; - if (type != b.type) return false; - if (num != b.num) return false; - else return true; + if (str != b.str) + return false; + if (Sat != b.Sat) + return false; + if (type != b.type) + return false; + if (num != b.num) + return false; + else + return true; } -bool KFKey::operator <(const KFKey& b) const +bool KFKey::operator!=(const KFKey& b) const { - if (str < b.str) return true; - if (str > b.str) return false; - - if (Sat < b.Sat) return true; - if (Sat > b.Sat) return false; - - if (type < b.type) return true; - if (type > b.type) return false; - - if (num < b.num) return true; - else return false; + return !(*this == b); } +bool KFKey::operator<(const KFKey& b) const +{ + if (str < b.str) + return true; + if (str > b.str) + return false; + + if (Sat < b.Sat) + return true; + if (Sat > b.Sat) + return false; + + if (type < b.type) + return true; + if (type > b.type) + return false; + + if (num < b.num) + return true; + else + return false; +} /** Finds the position in the noise vector of particular noise elements. -*/ -int KFMeas::getNoiseIndex( - const KFKey& key) ///< Key to search for in noise vector -const + */ +int KFMeas::getNoiseIndex(const KFKey& key) ///< Key to search for in noise vector + const { - auto index = noiseIndexMap.find(key); - if (index == noiseIndexMap.end()) - { - return -1; - } + auto index = noiseIndexMap.find(key); + if (index == noiseIndexMap.end()) + { + return -1; + } - return index->second; + return index->second; } /** Clears and initialises the state transition matrix to identity at the beginning of an epoch. -* Also clears any noise that was being added for the initialisation of a new state. -*/ -void KFState::initFilterEpoch() + * Also clears any noise that was being added for the initialisation of a new state. + */ +void KFState::initFilterEpoch(Trace& trace) +{ + initNoiseMap.clear(); + + for (auto& [key1, mapp] : stateTransitionMap) + { + if (key1 == oneKey) + { + continue; + } + + // remove initialisation elements for subsequent epochs + mapp.erase(oneKey); + } + + stateTransitionMap[oneKey][oneKey][0] = 1; + + // make a copy because iterators will be invalidated + auto sigmaMaxMapCopy = sigmaMaxMap; + + // remove any states that have exceeded their max variances + for (auto& [key, sigmaMax] : sigmaMaxMapCopy) + { + double sigma = 0; + getKFSigma(key, sigma); + + if (sigma > sigmaMax) + { + trace << "\n" + << "Removing '" << key << "' due to large sigma"; + removeState(key); + } + } + + // make a copy because iterators will be invalidated + auto outageLimitMapCopy = outageLimitMap; + + // remove any states that have exceeded their max outages + for (auto& [key, outageLimit] : outageLimitMapCopy) + { + double outage = (time - key.estimatedTime).to_double(); + + if (outage > outageLimit) + { + trace << "\n" + << "Removing '" << key << "' due to long outage"; + + outageLimitMap.erase(key); + + removeState(key); + } + } +} + +/** Finds the position in the KF state vector of particular states. + */ +int KFState::getKFIndex(const KFKey& key) ///< Key to search for in state + const +{ + auto index = kfIndexMap.find(key); + if (index == kfIndexMap.end()) + { + return -1; + } + return index->second; +} + +vector KFState::decomposedStateKeys(const KFKey& composedKey) const { - initNoiseMap.clear(); + auto it = pseudoStateMap.find(composedKey); + if (it == pseudoStateMap.end()) + { + return {composedKey}; + } - for (auto& [key1, mapp] : stateTransitionMap) - { - if (key1 == oneKey) - { - continue; - } + auto& [dummy, pseudoMap] = *it; - //remove initialisation elements for subsequent epochs - mapp.erase(oneKey); - } + vector decomposedKeys; + for (auto& [key, coeff] : pseudoMap) + { + decomposedKeys.push_back(key); + } - stateTransitionMap[oneKey][oneKey][0] = 1; + return decomposedKeys; } -/** Finds the position in the KF state vector of particular states. -*/ -int KFState::getKFIndex( - const KFKey& key) ///< Key to search for in state -const +/** Retrieve values from pseudo-states. + * Pseudo states are linear combinations of correlated states, and this function returns the value + * of the states assuming that the correlations from the point of creation are still valid. It + * returns the entirety of the state component for the primary state, with others returning 0. Later + * linear combinations of these should return the correct value for the combined state. + */ +E_Source KFState::getPseudoValue( + const KFKey& key, ///< Key to search for in state + double& value, ///< Output value + double* variance_ptr, ///< Optional variance output + double* adjustment_ptr ///< Optional adjustment output +) const { - auto index = kfIndexMap.find(key); - if (index == kfIndexMap.end()) - { - return -1; - } - return index->second; + lock_guard guard(kfStateMutex); + + auto it = pseudoParentMap.find(key); + if (it == pseudoParentMap.end()) + { + return E_Source::NONE; + } + + // this is a linear combination, make assumptions and return the values + + auto& [dummy, parent] = *it; + + auto& pseudoMap = pseudoStateMap.at(parent); + + auto& [primary, coeff] = *pseudoMap.begin(); + + if (key == primary) + { + // only the primary key returns values, assume all others 0 + + double pseudoValue; + double pseudoVariance; + double pseudoAdjustment; + getKFValue(parent, pseudoValue, &pseudoVariance, &pseudoAdjustment); + + double scalar = 1 / coeff; + + value = scalar * pseudoValue; + if (variance_ptr) + *variance_ptr = scalar * pseudoVariance * scalar; + if (adjustment_ptr) + *adjustment_ptr = scalar * pseudoAdjustment; + + return E_Source::PSEUDO; + } + + value = 0; + if (variance_ptr) + *variance_ptr = 0; + if (adjustment_ptr) + *adjustment_ptr = 0; + + return E_Source::PSEUDO; } /** Returns the value and variance of a state within the kalman filter object -*/ + */ E_Source KFState::getKFValue( - const KFKey& key, ///< Key to search for in state - double& value, ///< Output value - double* variance_ptr, ///< Optional variance output - double* adjustment_ptr, ///< Optional adjustment output - bool allowAlternate) ///< Optional flag to disable alternate filter -const + const KFKey& key, ///< Key to search for in state + double& value, ///< Output value + double* variance_ptr, ///< Optional variance output + double* adjustment_ptr, ///< Optional adjustment output + bool allowAlternate ///< Optional flag to disable alternate filter +) const { - auto a = kfIndexMap.find(key); - if (a == kfIndexMap.end()) - { - if ( allowAlternate == false - ||alternate_ptr == nullptr) - { - return E_Source::NONE; - } - - E_Source found = alternate_ptr->getKFValue(key, value, variance_ptr, adjustment_ptr); - if (found) - return E_Source::REMOTE; - - return E_Source::NONE; - } - - int index = a->second; - if (index >= x.size()) - { - return E_Source::NONE; - } - value = x(index); - - if (variance_ptr) - { - *variance_ptr = P(index,index); - } - - if (adjustment_ptr) - { - *adjustment_ptr = dx(index); - } - - return E_Source::KALMAN; + auto a = kfIndexMap.find(key); + if (a == kfIndexMap.end()) + { + E_Source found = getPseudoValue(key, value, variance_ptr, adjustment_ptr); + + if (found != E_Source::NONE) + return E_Source::PSEUDO; + + if (allowAlternate == false || alternate_ptr == nullptr) + { + return E_Source::NONE; + } + + found = alternate_ptr->getKFValue(key, value, variance_ptr, adjustment_ptr); + if (found != E_Source::NONE) + return E_Source::REMOTE; + + return E_Source::NONE; + } + + int index = a->second; + if (index >= x.size()) + { + return E_Source::NONE; + } + value = x(index); + + if (variance_ptr) + { + *variance_ptr = P(index, index); + } + + if (adjustment_ptr) + { + *adjustment_ptr = dx(index); + } + + return E_Source::KALMAN; } /** Returns the standard deviation of a state within the kalman filter object -*/ + */ bool KFState::getKFSigma( - const KFKey& key, ///< Key to search for in state - double& sigma) ///< Output value + const KFKey& key, ///< Key to search for in state + double& sigma ///< Output value +) { - auto a = kfIndexMap.find(key); - if (a == kfIndexMap.end()) - { - return false; - } - int index = a->second; - if (index >= x.size()) - { - return false; - } - - sigma = sqrt(P(index,index)); - return true; + auto a = kfIndexMap.find(key); + if (a == kfIndexMap.end()) + { + return false; + } + int index = a->second; + if (index >= x.size()) + { + return false; + } + + sigma = sqrt(P(index, index)); + return true; } void KFState::setAccelerator( - const KFKey& element, - const KFKey& dotElement, - const KFKey& dotDotElement, - const double value, - const InitialState& initialState) + const KFKey& element, + const KFKey& dotElement, + const KFKey& dotDotElement, + const double value, + const InitialState& initialState +) { - addKFState(dotDotElement, initialState); + addKFState(dotDotElement, initialState); - //t^2 term - stateTransitionMap[element] [dotDotElement][2] = value; + // t^2 term + stateTransitionMap[element][dotDotElement][2] = value; - //t terms - stateTransitionMap[dotElement] [dotDotElement][1] = value; + // t terms + stateTransitionMap[dotElement][dotDotElement][1] = value; } -/** Adds dynamics to a filter state by inserting off-diagonal, non-time dependent elements to transition matrix -*/ +/** Adds dynamics to a filter state by inserting off-diagonal, non-time dependent elements to + * transition matrix + */ void KFState::setKFTrans( - const KFKey& dest, ///< Key to search for in state to change in transition - const KFKey& source, ///< Key to search for in state as source - const double value, ///< Input value - const InitialState& initialState) ///< Initial state. + const KFKey& dest, ///< Key to search for in state to change in transition + const KFKey& source, ///< Key to search for in state as source + const double value, ///< Input value + const InitialState& initialState ///< Initial state. +) { - addKFState(dest, initialState); + lock_guard guard(kfStateMutex); + + addKFState(dest, initialState); - auto& transition = stateTransitionMap[dest][source][0]; + auto& transition = stateTransitionMap[dest][source][0]; - transition += value; + transition += value; } -/** Adds dynamics to a filter state by inserting off-diagonal, time dependent elements to transition matrix -*/ +/** Adds dynamics to a filter state by inserting off-diagonal, time dependent elements to transition + * matrix + */ void KFState::setKFTransRate( - const KFKey& integralKey, ///< Key to search for in state to change in transition - const KFKey& rateKey, ///< Key to search for in state as source - const double value, ///< Input value - const InitialState& initialRateState, ///< Initial state for rate state. - const InitialState& initialIntegralState) ///< Initial state for the thing that is modified by the rate + const KFKey& integralKey, ///< Key to search for in state to change in transition + const KFKey& rateKey, ///< Key to search for in state as source + const double value, ///< Input value + const InitialState& initialRateState, ///< Initial state for rate state. + const InitialState& + initialIntegralState ///< Initial state for the thing that is modified by the rate +) { - addKFState(rateKey, initialRateState); - addKFState(integralKey, initialIntegralState); + lock_guard guard(kfStateMutex); - stateTransitionMap[integralKey][rateKey][1] = value; + addKFState(rateKey, initialRateState); + addKFState(integralKey, initialIntegralState); + + stateTransitionMap[integralKey][rateKey][1] = value; } /** Remove a state from a kalman filter object. -*/ + */ void KFState::removeState( - const KFKey& kfKey) ///< Key to search for in state + const KFKey& kfKey, ///< Key to search for in state + bool allowDeleteParent +) { - stateTransitionMap. erase(kfKey); - procNoiseMap. erase(kfKey); - gaussMarkovTauMap. erase(kfKey); - gaussMarkovMuMap. erase(kfKey); - exponentialNoiseMap. erase(kfKey); + lock_guard guard(kfStateMutex); + + KFKey removeKey = kfKey; + + // for psuedo states this can get a bit recursive states are removed after they're combined, so + // it needs to be prevent specifically in that case + if (allowDeleteParent) + { + auto it = pseudoParentMap.find(kfKey); + if (it != pseudoParentMap.end()) + { + auto& [dummy, parent] = *it; + + removeKey = parent; + + traceTrivialTrace( + "Removing '%s' because of '%s'", + ((string)removeKey).c_str(), + ((string)kfKey).c_str() + ); + } + } + + traceTrivialTrace("Removing '%s'", ((string)removeKey).c_str()); + + stateTransitionMap.erase(removeKey); + sigmaMaxMap.erase(removeKey); + procNoiseMap.erase(removeKey); + gaussMarkovTauMap.erase(removeKey); + gaussMarkovMuMap.erase(removeKey); + exponentialNoiseMap.erase(removeKey); + errorCountMap.erase(removeKey); + + // do pseudo state removal after state transition is complete, + // outage limits should stay with the non-pseudo states, and shouldnt be deleted with the + // non-pseudo + // outageLimitMap. erase(removeKey); } /** Tries to add a state to the filter object. -* If it does not exist, it adds it to a list of states to be added. -* Call consolidateKFState() to apply the list to the filter object -*/ + * If it does not exist, it adds it to a list of states to be added. + * Call consolidateKFState() to apply the list to the filter object + */ bool KFState::addKFState( - const KFKey& kfKey, ///< The key to add to the state - const InitialState& initialState) ///< The initial conditions to add to the state + const KFKey& kfKey, ///< The key to add to the state + const InitialState& initialState ///< The initial conditions to add to the state +) { - auto iter = stateTransitionMap.find(kfKey); - if (iter != stateTransitionMap.end()) - { - //is an existing state, just update values - if (initialState.Q != 0) { procNoiseMap [kfKey] = initialState.Q; } - if (initialState.mu != 0) { gaussMarkovMuMap [kfKey] = initialState.mu; } - if (initialState.tau != 0) { gaussMarkovTauMap [kfKey] = initialState.tau; } - - return false; - } - - //this should be a new state, add to the state transition matrix to create a new state. - - //check if it exists in the state though, identity STM causes double ups if its reinitialised too quickly - double currentX = 0; - auto it = kfIndexMap.find(kfKey); - if (it != kfIndexMap.end()) - { - auto& [dummy, index] = *it; - currentX = x[index]; - } - - stateTransitionMap [kfKey][kfKey] [0] = 1; - stateTransitionMap [kfKey][oneKey] [0] = initialState.x - currentX; - initNoiseMap [kfKey] = initialState.P; - procNoiseMap [kfKey] = initialState.Q; - gaussMarkovTauMap [kfKey] = initialState.tau; - gaussMarkovMuMap [kfKey] = initialState.mu; - - if (initialState.P < 0) - { - //will be an uninitialised variable, do a least squares solution - lsqRequired = true; - } - - return true; + lock_guard guard(kfStateMutex); + + auto iter = stateTransitionMap.find(kfKey); + if (iter != stateTransitionMap.end()) + { + // is an existing state, just update values + if (initialState.Q != 0) + { + procNoiseMap[kfKey] = initialState.Q; + } + if (initialState.mu != 0) + { + gaussMarkovMuMap[kfKey] = initialState.mu; + } + if (initialState.tau != 0) + { + gaussMarkovTauMap[kfKey] = initialState.tau; + } + if (initialState.sigmaMax != 0) + { + sigmaMaxMap[kfKey] = initialState.sigmaMax; + } + if (initialState.outageLimit != 0) + { + outageLimitMap[kfKey] = initialState.outageLimit; + } + + return false; + } + + auto pseudoIt = pseudoParentMap.find(kfKey); + if (pseudoIt != pseudoParentMap.end()) + { + // this is an existing pseudostate, dont re-add a real state + return false; + } + + // this should be a new state, add to the state transition matrix to create a new state. + + // check if it exists in the state though, identity STM causes double ups if its reinitialised + // too quickly + double currentX = 0; + auto it = kfIndexMap.find(kfKey); + if (it != kfIndexMap.end()) + { + auto& [dummy, index] = *it; + currentX = x[index]; + } + + stateTransitionMap[kfKey][kfKey][0] = 1; + stateTransitionMap[kfKey][oneKey][0] = initialState.x - currentX; + if (initialState.P) + initNoiseMap[kfKey] = initialState.P; + if (initialState.Q) + procNoiseMap[kfKey] = initialState.Q; + if (initialState.mu) + gaussMarkovMuMap[kfKey] = initialState.mu; + if (initialState.tau) + gaussMarkovTauMap[kfKey] = initialState.tau; + if (initialState.sigmaMax) + sigmaMaxMap[kfKey] = initialState.sigmaMax; + if (initialState.outageLimit) + outageLimitMap[kfKey] = initialState.outageLimit; + + if (initialState.P < 0) + { + // will be an uninitialised variable, do a least squares solution + lsqRequired = true; + } + + return true; } -void KFState::setExponentialNoise( - const KFKey& kfKey, - const Exponential exponential) +/** Create a pseudo state that represents the linear combination of two or more perfectly correlated + * states. The configuration of the states that are combined are added according to the coefficients + * of correlation, which may not be appropriate for things like process noise or max sigmas, and + * should be configured accordingly. + */ +bool KFState::addPseudoState(const KFKey& kfKey, const map& coeffMap) { - exponentialNoiseMap[kfKey] = exponential; + lock_guard guard(kfStateMutex); + + InitialState init; + init.P = 0; + + for (auto& [key, coeff] : coeffMap) + { + auto& parentKey = pseudoParentMap[key]; + + if (parentKey.type != KF::NONE && parentKey != kfKey) + { + BOOST_LOG_TRIVIAL(warning) << "Pseudo state added with multiple parents. " << key + << " has parents '" << parentKey << "' and '" << kfKey; + + throw true; + } + } + + for (auto& [key, coeff] : coeffMap) + { + pseudoParentMap[key] = kfKey; + + // get initial values from other states + init.x += coeff * stateTransitionMap[key][oneKey][0]; + init.P += coeff * initNoiseMap[key] * coeff; + + // expect these to not be in the maps, use `at` rather than adding surplus entries or + // dealing with iterators + try + { + init.sigmaMax += coeff * sigmaMaxMap.at(key); + } + catch (...) + { + } + try + { + init.Q += coeff * procNoiseMap.at(key); + } + catch (...) + { + } + try + { + init.tau += gaussMarkovTauMap.at(key); + } + catch (...) + { + } + + removeState(key, false); + } + + pseudoStateMap[kfKey] = coeffMap; + + bool stateCreated = addKFState(kfKey, init); + + return stateCreated; +} + +void KFState::setExponentialNoise(const KFKey& kfKey, const Exponential exponential) +{ + lock_guard guard(kfStateMutex); + + exponentialNoiseMap[kfKey] = exponential; } /** Add process noise and dynamics to filter object manually. BEWARE! - * Not recommended for ordinary use, likely to break things. Dont touch unless you really know what you're doing. - * Hint - you dont really know what you're doing + * Not recommended for ordinary use, likely to break things. Dont touch unless you really know what + * you're doing. Hint - you dont really know what you're doing */ -void KFState::manualStateTransition( - Trace& trace, - GTime newTime, - MatrixXd& F, - MatrixXd& Q0) +void KFState::manualStateTransition(Trace& trace, GTime newTime, MatrixXd& F, MatrixXd& Q0) { - if (newTime != GTime::noTime()) - { - time = newTime; - } - - //output the state transition matrix to a trace file (used by RTS smoother) - if (rts_basename.empty() == false) - { - TransitionMatrixObject transitionMatrixObject = F; - - spitFilterToFile(transitionMatrixObject, E_SerialObject::TRANSITION_MATRIX, rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - } - - //compute the updated states and permutation and covariance matrices - VectorXd Fx = F * x; - if (simulate_filter_only == false) - { - dx = Fx - x; - x = (Fx ).eval(); - P = (F * P * F.transpose() + Q0 ).eval(); - } - - initFilterEpoch(); + if (newTime != GTime::noTime()) + { + time = newTime; + } + + // output the state transition matrix to a trace file (used by RTS smoother) + if (rts_basename.empty() == false) + { + TransitionMatrixObject transitionMatrixObject = F; + + spitFilterToFile( + transitionMatrixObject, + E_SerialObject::TRANSITION_MATRIX, + rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + } + + // compute the updated states and permutation and covariance matrices + VectorXd Fx = F * x; + if (simulate_filter_only == false) + { + dx = Fx - x; + x = (Fx).eval(); + P = (F * P * F.transpose() + Q0).eval(); + } + + initFilterEpoch(trace); } - /** Add process noise and dynamics to filter object according to time gap. - * This will also sort states according to their kfKey as a result of the way the state transition matrix is generated. + * This will also sort states according to their kfKey as a result of the way the state transition + * matrix is generated. */ void KFState::stateTransition( - Trace& trace, ///< Trace file for output - GTime newTime, ///< Time of update for process noise and dynamics (s) - MatrixXd* stm_ptr) ///< Optional pointer to output state transition matrix + Trace& trace, ///< Trace file for output + GTime newTime, ///< Time of update for process noise and dynamics (s) + MatrixXd* stm_ptr ///< Optional pointer to output state transition matrix +) { - double tgap = 0; - if ( newTime != GTime::noTime() - &&time != GTime::noTime()) - { - tgap = (newTime - time).to_double(); - } + double tgap = 0; + if (newTime != GTime::noTime() && time != GTime::noTime()) + { + tgap = (newTime - time).to_double(); + } + + if (newTime != GTime::noTime()) + { + time = newTime; + } + + int newStateCount = stateTransitionMap.size(); + if (newStateCount == 0) + { + std::cout << "THIS IS WEIRD" << "\n"; + return; + } + + // Initialise and populate a state transition and Z transition matrix + SparseMatrix F = SparseMatrix(newStateCount, x.rows()); + + // add transitions for any states (usually close to identity) + int row = 0; + map newKFIndexMap; + for (auto& [newStateKey, newStateMap] : stateTransitionMap) + { + newKFIndexMap[newStateKey] = row; + + for (auto& [sourceStateKey, values] : newStateMap) + { + int sourceIndex = getKFIndex(sourceStateKey); + + if ((sourceIndex < 0) || (sourceIndex >= F.cols())) + { + continue; + } + + for (auto& [tExp, value] : values) + { + double tau = -1; + + auto gmIter = gaussMarkovTauMap.find(sourceStateKey); + if (gmIter != gaussMarkovTauMap.end()) + { + auto& [dummy, sourceTau] = *gmIter; + + tau = sourceTau; + } + + double scalar = 1; + + if (tau < 0) + { + // Random Walk model (special case for First Order Gauss Markov model when tau + // == inf) + + for (int i = 0; i < tExp; i++) + { + scalar *= tgap / (i + 1); + } + + // F(row, sourceIndex) = value * scalar; + F.coeffRef(row, sourceIndex) += value * scalar; + + continue; + } + + // First Order Gauss Markov model, Ref: Carpenter and Lee (2008) - A Stable Clock + // Error Model Using Coupled First- and Second-Order Gauss-Markov Processes - + // https://ntrs.nasa.gov/api/citations/20080044877/downloads/20080044877.pdf + + double tempTerm = 1; + scalar = exp(-tgap / tau); + + for (int i = 0; i < tExp; i++) + { + scalar = tau * (tempTerm - scalar); // recursive formula derived according to + // Ref: Carpenter and Lee (2008) + tempTerm *= tgap / (i + 1); + } + + double transition = value * scalar; + + // F(row, sourceIndex) = transition; + F.coeffRef(row, sourceIndex) += transition; + + // Add state transitions to ONE element, to allow for tiedown to average value mu + // derived from integrating and distributing terms for v = (v0 - mu) * exp(-t/tau) + + // mu; tempTerm calculated above appears to be same as required for these terms too, + // (at least for tExp = 0,1) + + auto muIter = gaussMarkovMuMap.find(sourceStateKey); + if (muIter != gaussMarkovMuMap.end()) + { + auto& [dummy2, mu] = *muIter; + + // F(row, 0) = mu * (tempTerm - transition); + F.coeffRef(row, 0) += mu * (tempTerm - transition); + } + } + } + + row++; + } + + // scale and add process noise + MatrixXd Q0 = MatrixXd::Zero(newStateCount, newStateCount); + tgap = fabs(tgap); + + // add noise as 'process noise' as the method of initialising a state's variance + for (auto& [kfKey, value] : initNoiseMap) + { + auto iter = newKFIndexMap.find(kfKey); + if (iter == newKFIndexMap.end()) + { + // std::cout << kfKey << " broke" << "\n"; + continue; + } + int index = iter->second; + + if ((index < 0) || (index >= Q0.rows())) + { + continue; + } + + Q0(index, index) = value; + } + + // add time dependent exponential process noise) + if (tgap) + for (auto& [dest, exponential] : exponentialNoiseMap) + { + auto destIter = newKFIndexMap.find(dest); + if (destIter == newKFIndexMap.end()) + { + std::cout << dest << " broke" << "\n"; + continue; + } + + auto& expNoise = exponential.value; + + // if (expNoise > 0.01) + // { + trace << "\n" + << "Adding " << expNoise << " (per second) to process noise for " << dest << "\n"; + // } + + int destIndex = destIter->second; + + if ((destIndex < 0) || (destIndex >= Q0.rows())) + { + continue; + } + + Q0(destIndex, destIndex) += expNoise * tgap; + } + + // shrink time dependent exponential process noise + if (tgap) + for (auto& [dest, exponential] : exponentialNoiseMap) + { + auto& expNoise = exponential.value; + auto& expTau = exponential.tau; + + // shrink the exponential process noise the next time around + if (expTau) + expNoise *= exp(-tgap / expTau); + else + expNoise = 0; + } + + // add time dependent process noise + if (tgap) + for (auto& [dest, map] : stateTransitionMap) + for (auto& [source, vals] : map) + for (auto& [tExp, val] : vals) + { + auto initIter = initNoiseMap.find(dest); + if (initIter != initNoiseMap.end()) + { + // this was initialised this epoch + double init = initIter->second; + + if (init == 0) + { + // this was initialised with no noise, do lsq, dont add process noise + continue; + } + } + + auto destIter = newKFIndexMap.find(dest); + if (destIter == newKFIndexMap.end()) + { + std::cout << dest << " broke" << "\n"; + continue; + } + + int destIndex = destIter->second; + + if ((destIndex < 0) || (destIndex >= Q0.rows())) + { + continue; + } + + auto sourceIter = newKFIndexMap.find(source); + if (sourceIter == newKFIndexMap.end()) + { + std::cout << dest << " broKe" << "\n"; + continue; + } + + int sourceIndex = sourceIter->second; + + if ((sourceIndex < 0) || (sourceIndex >= Q0.rows())) + { + continue; + } + + auto iter2 = procNoiseMap.find(source); + if (iter2 == procNoiseMap.end()) + { + // std::cout << dest << " brOke" << "\n"; + continue; + } + + auto [dummy, sourceProcessNoise] = *iter2; + + auto gmIter = gaussMarkovTauMap.find(source); + if (gmIter != gaussMarkovTauMap.end()) + { + auto& [dummy, tau] = *gmIter; + + if (tau < 0) + { + // Random Walk model (special case for First Order Gauss Markov model + // when tau == inf) + + if (tExp == 0) + { + Q0(destIndex, destIndex) += sourceProcessNoise / 1 * tgap; + } + // else if (tExp == 1) { Q0(destIndex, destIndex) += + // sourceProcessNoise / 3 + // * tgap * tgap * tgap; Q0(sourceIndex, + // destIndex) += sourceProcessNoise / 2 * tgap * tgap; + // Q0(destIndex, sourceIndex) += sourceProcessNoise / 2 * tgap * tgap; + // } + // else if (tExp == 2) { Q0(destIndex, destIndex) += + // sourceProcessNoise / 20 * tgap * tgap * tgap * tgap * tgap;} + } + else + { + // First Order Gauss Markov model, Ref: Carpenter and Lee (2008) - A + // Stable Clock Error Model Using Coupled First- and Second-Order + // Gauss-Markov Processes - + // https://ntrs.nasa.gov/api/citations/20080044877/downloads/20080044877.pdf + + if (tExp == 0) + { + Q0(destIndex, destIndex) += + sourceProcessNoise / 2 * tau * (1 - exp(-2 * tgap / tau)); + } + else if (tExp == 1) + { + Q0(destIndex, destIndex) += + sourceProcessNoise / 2 * tau * tau * + (+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) + // Q0(sourceIndex, destIndex) += + // sourceProcessNoise / 2 + // * tau * tau * (1-exp(-tgap/tau)) * (1-exp(-tgap/tau)); + // Q0(destIndex, sourceIndex) += sourceProcessNoise / 2 * tau * tau + // * (1-exp(-tgap/tau)) + // * (1-exp(-tgap/tau)); + } + else if (tExp == 2) + { + std::cout << "FOGM model is not applied to acceleration term at " + "the moment" + << "\n"; + } + } + } + else + { + std::cout << "Tau value not found in filter: " << source << "\n"; + continue; + } + } + + // output the state transition matrix to a trace file (used by RTS smoother) + if (rts_basename.empty() == false) + { + TransitionMatrixObject transitionMatrixObject = F; + + spitFilterToFile( + transitionMatrixObject, + E_SerialObject::TRANSITION_MATRIX, + rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + } + + if (stm_ptr) + { + *stm_ptr = F; + } + + // std::cout << "x" << "\n" << x << "\n"; + // compute the updated states and permutation and covariance matrices + VectorXd Fx = F * x; + if (F.rows() == F.cols()) + { + dx = Fx - x; + } + else + { + dx = VectorXd::Zero(F.rows()); + } + + { + x = (Fx).eval(); + } + if (simulate_filter_only) + { + Q0.setZero(); + } + { + P = (F * P * F.transpose() + Q0).eval(); + } + // std::cout << "F" << "\n" << MatrixXd(F).format(heavyFmt) << "\n"; + // std::cout << "x1" << "\n" << MatrixXd(x).transpose().format(HeavyFmt) << "\n"; + // std::cout << "Q0" << "\n" << Q0 << "\n"; + // std::cout << "P" << "\n" << P << "\n"; + + // replace the index map with the updated version that corresponds to the updated state + kfIndexMap = std::move(newKFIndexMap); + + // remove any details about pseudo states that are no longer in the state vector + // do it here because it causes issues if maps dont exist in other places before state + // transitions + for (auto pseudoIt = pseudoStateMap.begin(); pseudoIt != pseudoStateMap.end();) + { + auto& [parent, childrenMap] = *pseudoIt; + + auto it = kfIndexMap.find(parent); + + if (it == kfIndexMap.end()) + { + // not in state any more, remove the pseudos + + // first erase the children pointing to this parent from the other map + for (auto& [child, coeff] : childrenMap) + { + pseudoParentMap.erase(child); + } + + // and erase it from this map pointing to them + pseudoIt = pseudoStateMap.erase(pseudoIt); + } + else + { + pseudoIt++; + } + } + + initFilterEpoch(trace); +} - if ( newTime != GTime::noTime()) - { - time = newTime; - } - - int newStateCount = stateTransitionMap.size(); - if (newStateCount == 0) - { - std::cout << "THIS IS WEIRD" << "\n"; - return; - } - - //Initialise and populate a state transition and Z transition matrix - SparseMatrix F = SparseMatrix (newStateCount, x.rows()); - - //add transitions for any states (usually close to identity) - int row = 0; - map newKFIndexMap; - for (auto& [newStateKey, newStateMap] : stateTransitionMap) - { - newKFIndexMap[newStateKey] = row; - - for (auto& [sourceStateKey, values] : newStateMap) - { - int sourceIndex = getKFIndex(sourceStateKey); - - if ( (sourceIndex < 0) - ||(sourceIndex >= F.cols())) - { - continue; - } - - for (auto& [tExp, value] : values) - { - double tau = -1; - - auto gmIter = gaussMarkovTauMap.find(sourceStateKey); - if (gmIter != gaussMarkovTauMap.end()) - { - auto& [dummy, sourceTau] = *gmIter; - - tau = sourceTau; - } - - double scalar = 1; - - if (tau < 0) - { - //Random Walk model (special case for First Order Gauss Markov model when tau == inf) - - for (int i = 0; i < tExp; i++) - { - scalar *= tgap / (i+1); - } - - // F(row, sourceIndex) = value * scalar; - F.coeffRef(row, sourceIndex) += value * scalar; - - continue; - } - - //First Order Gauss Markov model, Ref: Carpenter and Lee (2008) - A Stable Clock Error Model Using Coupled First- and Second-Order Gauss-Markov Processes - https://ntrs.nasa.gov/api/citations/20080044877/downloads/20080044877.pdf - - double tempTerm = 1; - scalar = exp(-tgap/tau); - - for (int i = 0; i < tExp; i++) - { - scalar = tau * (tempTerm - scalar); //recursive formula derived according to Ref: Carpenter and Lee (2008) - tempTerm *= tgap / (i+1); - } - - double transition = value * scalar; - - // F(row, sourceIndex) = transition; - F.coeffRef(row, sourceIndex) += transition; - - - //Add state transitions to ONE element, to allow for tiedown to average value mu - //derived from integrating and distributing terms for v = (v0 - mu) * exp(-t/tau) + mu; - //tempTerm calculated above appears to be same as required for these terms too, (at least for tExp = 0,1) - - auto muIter = gaussMarkovMuMap.find(sourceStateKey); - if (muIter != gaussMarkovMuMap.end()) - { - auto& [dummy2, mu] = *muIter; - - // F(row, 0) = mu * (tempTerm - transition); - F.coeffRef(row, 0) += mu * (tempTerm - transition); - } - } - } - - row++; - } - - //scale and add process noise - MatrixXd Q0 = MatrixXd::Zero(newStateCount, newStateCount); - tgap = fabs(tgap); - - //add noise as 'process noise' as the method of initialising a state's variance - for (auto& [kfKey, value] : initNoiseMap) - { - auto iter = newKFIndexMap.find(kfKey); - if (iter == newKFIndexMap.end()) - { -// std::cout << kfKey << " broke" << "\n"; - continue; - } - int index = iter->second; - - if ( (index < 0) - ||(index >= Q0.rows())) - { - continue; - } - - Q0(index, index) = value; - } - - //add time dependent exponential process noise) - if (tgap) - for (auto& [dest, exponential] : exponentialNoiseMap) - { - auto destIter = newKFIndexMap.find(dest); - if (destIter == newKFIndexMap.end()) - { - std::cout << dest << " broke" << "\n"; - continue; - } - - auto& expNoise = exponential.value; - - if (expNoise > 0.01) - { - trace - << "\n" << "Adding : " << expNoise << " to process noise for " << dest << " \n"; - } - - int destIndex = destIter->second; - - if ( (destIndex < 0) - ||(destIndex >= Q0.rows())) - { - continue; - } - - Q0(destIndex, destIndex) += expNoise * tgap; - } - - //shrink time dependent exponential process noise - if (tgap) - for (auto& [dest, exponential] : exponentialNoiseMap) - { - auto& expNoise = exponential.value; - auto& expTau = exponential.tau; - - //shrink the exponential process noise the next time around - if (expTau) expNoise *= exp(-tgap / expTau); - else expNoise = 0; - } - - //add time dependent process noise - if (tgap) - for (auto& [dest, map] : stateTransitionMap) - for (auto& [source, vals] : map) - for (auto& [tExp, val] : vals) - { - auto initIter = initNoiseMap.find(dest); - if (initIter != initNoiseMap.end()) - { - //this was initialised this epoch - double init = initIter->second; - - if (init == 0) - { - //this was initialised with no noise, do lsq, dont add process noise - continue; - } - } - - auto destIter = newKFIndexMap.find(dest); - if (destIter == newKFIndexMap.end()) - { - std::cout << dest << " broke" << "\n"; - continue; - } - - int destIndex = destIter->second; - - if ( (destIndex < 0) - ||(destIndex >= Q0.rows())) - { - continue; - } - - auto sourceIter = newKFIndexMap.find(source); - if (sourceIter == newKFIndexMap.end()) - { - std::cout << dest << " broKe" << "\n"; - continue; - } - - int sourceIndex = sourceIter->second; - - if ( (sourceIndex < 0) - ||(sourceIndex >= Q0.rows())) - { - continue; - } - - auto iter2 = procNoiseMap.find(source); - if (iter2 == procNoiseMap.end()) - { -// std::cout << dest << " brOke" << "\n"; - continue; - } - - auto [dummy, sourceProcessNoise] = *iter2; - - auto gmIter = gaussMarkovTauMap.find(source); - if (gmIter != gaussMarkovTauMap.end()) - { - auto& [dummy, tau] = *gmIter; - - if (tau < 0) - { - //Random Walk model (special case for First Order Gauss Markov model when tau == inf) - - if (tExp == 0) { Q0(destIndex, destIndex) += sourceProcessNoise / 1 * tgap;} -// else if (tExp == 1) { Q0(destIndex, destIndex) += sourceProcessNoise / 3 * tgap * tgap * tgap; - // Q0(sourceIndex, destIndex) += sourceProcessNoise / 2 * tgap * tgap; - // Q0(destIndex, sourceIndex) += sourceProcessNoise / 2 * tgap * tgap; -// } -// else if (tExp == 2) { Q0(destIndex, destIndex) += sourceProcessNoise / 20 * tgap * tgap * tgap * tgap * tgap;} - } - else - { - //First Order Gauss Markov model, Ref: Carpenter and Lee (2008) - A Stable Clock Error Model Using Coupled First- and Second-Order Gauss-Markov Processes - https://ntrs.nasa.gov/api/citations/20080044877/downloads/20080044877.pdf - - if (tExp == 0) { Q0(destIndex, destIndex) += sourceProcessNoise / 2 * tau * (1 - exp(-2*tgap/tau)); } - else if (tExp == 1) { Q0(destIndex, destIndex) += sourceProcessNoise / 2 * tau * tau * ( + 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) - // Q0(sourceIndex, destIndex) += sourceProcessNoise / 2 * tau * tau * (1-exp(-tgap/tau)) * (1-exp(-tgap/tau)); - // Q0(destIndex, sourceIndex) += sourceProcessNoise / 2 * tau * tau * (1-exp(-tgap/tau)) * (1-exp(-tgap/tau)); - } - else if (tExp == 2) { std::cout << "FOGM model is not applied to acceleration term at the moment" << "\n"; } - } - } - else - { - std::cout << "Tau value not found in filter: " << source << "\n"; - continue; - } - } - - //output the state transition matrix to a trace file (used by RTS smoother) - if (rts_basename.empty() == false) - { - TransitionMatrixObject transitionMatrixObject = F; - - spitFilterToFile(transitionMatrixObject, E_SerialObject::TRANSITION_MATRIX, rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - } - - if (stm_ptr) - { - *stm_ptr = F; - } - -// std::cout << "x" << "\n" << x << "\n"; - //compute the updated states and permutation and covariance matrices - VectorXd Fx = F * x; - if (F.rows() == F.cols()) - { - dx = Fx - x; - } - else - { - dx = VectorXd::Zero(F.rows()); - } - - { - x = (Fx ).eval(); - } - if (simulate_filter_only) - { - Q0.setZero(); - } - { - P = (F * P * F.transpose() + Q0 ).eval(); - } - // std::cout << "F" << "\n" << MatrixXd(F).format(heavyFmt) << "\n"; -// std::cout << "x1" << "\n" << MatrixXd(x).transpose().format(HeavyFmt) << "\n"; -// std::cout << "Q0" << "\n" << Q0 << "\n"; -// std::cout << "P" << "\n" << P << "\n"; - - //replace the index map with the updated version that corresponds to the updated state - kfIndexMap = std::move(newKFIndexMap); - - initFilterEpoch(); +/** Compare variances of measurements and estimated parameters to detect unreasonable values + * Ref: to be added + */ +void KFState::leastSquareSigmaChecks( + RejectCallbackDetails& callbackDetails, + MatrixXd& Pp, ///< Post-fit covariance of parameters + KFStatistics& statistics ///< Test statistics +) +{ + auto& kfMeas = callbackDetails.kfMeas; + auto& trace = callbackDetails.trace; + + auto& VV = kfMeas.VV; + auto& R = kfMeas.R; + auto& H = kfMeas.H; + + ArrayXd measRatios = ArrayXd::Zero(H.rows()); + ArrayXd measNumerator = ArrayXd::Zero(H.rows()); + ArrayXd measDenominator = ArrayXd::Zero(H.rows()); + + if (lsqOpts.sigma_check) + { + // use 'array' for component-wise calculations + measNumerator = VV.array(); + measDenominator = R.diagonal().array(); + } + else if (lsqOpts.omega_test) + { + MatrixXd HPH_ = H * Pp * H.transpose(); + + // use 'array' for component-wise calculations + measNumerator = VV.array(); + measDenominator = (R.diagonal() - HPH_.diagonal()).array(); + } + + measRatios = measNumerator / measDenominator.sqrt(); + measRatios = measRatios.isFinite().select( + measRatios, + 0 + ); // set ratio to 0 if corresponding variance is 0, e.g. ONE state, clk rate states + + kfMeas.postfitRatios = measRatios; + + statistics.sumOfSquares = measRatios.square().sum(); + statistics.averageRatio = measRatios.mean(); + + Eigen::ArrayXd::Index measIndex; + + double maxMeasRatio = measRatios.abs().maxCoeff(&measIndex); + + // if any are outside the expected values, flag an error + if (maxMeasRatio > lsqOpts.meas_sigma_threshold) + { + trace << "\n" + << time << "\tLARGE MEAS ERROR OF : " << maxMeasRatio << "\tAT " << measIndex + << " :\t" << kfMeas.obsKeys[measIndex]; + + callbackDetails.measIndex = measIndex; + } } /** Compare variances of measurements and pre-filtered states to detect unreasonable values -* Ref: Wang et al. (1997) - On Quality Control in Hydrographic GPS Surveying -* & Wieser et al. (2004) - Failure Scenarios to be Considered with Kinematic High Precision Relative GNSS Positioning - http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.573.9628&rep=rep1&type=pdf -*/ -void KFState::preFitSigmaCheck( - Trace& trace, ///< Trace to output to - KFMeas& kfMeas, ///< Measurements, noise, and design matrix - KFKey& badStateKey, ///< Key to the state that has worst ratio (only if worse than badMeasIndex) - int& badMeasIndex, ///< Index of the measurement that has the worst ratio - KFStatistics& statistics, ///< Test statistics - int begX, ///< Index of first state element to process - int numX, ///< Number of states elements to process - int begH, ///< Index of first measurement to process - int numH) ///< Number of measurements to process + * Ref: Wang et al. (1997) - On Quality Control in Hydrographic GPS Surveying + * & Wieser et al. (2004) - Failure Scenarios to be Considered with Kinematic High Precision + * Relative GNSS Positioning + * - http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.573.9628&rep=rep1&type=pdf + */ +void KFState::preFitSigmaChecks( + RejectCallbackDetails& callbackDetails, + KFStatistics& statistics, ///< Test statistics + int begX, ///< Index of first state element to process + int numX, ///< Number of states elements to process + int begH, ///< Index of first measurement to process + int numH ///< Number of measurements to process +) { - auto v = kfMeas.V.segment(begH, numH); - auto R = kfMeas.R.block(begH, begH, numH, numH); - auto H = kfMeas.H.block(begH, begX, numH, numX); - auto P = this-> P.block(begX, begX, numX, numX); - - ArrayXd measRatios = ArrayXd::Zero(numH); - ArrayXd stateRatios = ArrayXd::Zero(numX); - - if (prefitOpts.sigma_check) - { - //use 'array' for component-wise calculations - auto measVariations = v.array().square(); //delta squared - auto measVariances = ((H*P*H.transpose()).diagonal() + R.diagonal()).array(); - - measRatios = measVariations / measVariances; - measRatios = measRatios.isFinite() .select(measRatios, 0); - -// trace << "\n" << "DOING PRE SIGMA CHECK: "; - } - else if (prefitOpts.omega_test) - { - MatrixXd Qinv = (H*P*H.transpose() + R).inverse(); - MatrixXd H_Qinv = H.transpose() * Qinv; - - //use 'array' for component-wise calculations - auto measNumerator = (Qinv * v) .array().square(); //weighted residuals squared - auto stateNumerator = (H_Qinv * v) .array().square(); - - auto measDenominator = Qinv .diagonal().array(); //weights - auto stateDenominator = (H_Qinv * H) .diagonal().array(); - - measRatios = measNumerator / measDenominator; - measRatios = measRatios.isFinite() .select(measRatios, 0); //set ratio to 0 if corresponding variance is 0, e.g. ONE state, clk rate states - stateRatios = stateNumerator / stateDenominator; - stateRatios = stateRatios.isFinite().select(stateRatios, 0); - -// trace << "\n" << "DOING W-test: "; - } - - statistics.sumOfSquares = measRatios.sum(); - statistics.averageRatio = measRatios.mean(); - - //if any are outside the expected value, flag an error - - Eigen::ArrayXd::Index stateIndex; - Eigen::ArrayXd::Index measIndex; - - double maxStateRatio = stateRatios .maxCoeff(&stateIndex); - double maxMeasRatio = measRatios .maxCoeff(&measIndex); - - //if any are outside the expected values, flag an error - if ( maxStateRatio > maxMeasRatio * 0.95 - &&maxStateRatio > SQR(prefitOpts.state_sigma_threshold)) - { - int chunkIndex = stateIndex + begX; - - auto it = kfIndexMap.begin(); - std::advance(it, stateIndex); - - auto& [key, dummy] = *it; - - trace << "\n" << "LARGE STATE ERROR OF " << maxStateRatio << " AT " << chunkIndex << " : " << key; - - badStateKey = key; - } - - if (maxMeasRatio > SQR(prefitOpts.meas_sigma_threshold)) - { - int chunkIndex = measIndex + begH; - - trace << "\n" << "LARGE MEAS ERROR OF " << maxMeasRatio << " AT " << chunkIndex << " : " << kfMeas.obsKeys[chunkIndex]; - - badMeasIndex = measIndex + begH; - } + auto& trace = callbackDetails.trace; + auto& kfMeas = callbackDetails.kfMeas; + + auto V = kfMeas.V.segment(begH, numH); + auto R = kfMeas.R.block(begH, begH, numH, numH); + auto H = kfMeas.H.block(begH, begX, numH, numX); + auto P = this->P.block(begX, begX, numX, numX); + + ArrayXd measRatios = ArrayXd::Zero(numH); + ArrayXd stateRatios = ArrayXd::Zero(numX); + MatrixXd HPH_ = H * P * H.transpose(); + + if (prefitOpts.sigma_check) + { + // use 'array' for component-wise calculations + auto measVariations = V.array(); + auto measVariances = (HPH_.diagonal() + R.diagonal()).array(); + + measRatios = measVariations / measVariances.sqrt(); + } + else if (prefitOpts.omega_test) // Eugene: will be gone + { + MatrixXd Qinv = (HPH_ + R).inverse(); + MatrixXd H_Qinv = H.transpose() * Qinv; + + // use 'array' for component-wise calculations + auto measNumerator = (Qinv * V).array(); + auto stateNumerator = (H_Qinv * V).array(); + + auto measDenominator = Qinv.diagonal().array(); + auto stateDenominator = (H_Qinv * H).diagonal().array(); + + measRatios = measNumerator / measDenominator.sqrt(); + stateRatios = stateNumerator / stateDenominator.sqrt(); + } + + // Eugene: do abs() here + measRatios = measRatios.isFinite().select( + measRatios, + 0 + ); // set ratio to 0 if corresponding variance is 0, e.g. ONE state, clk rate states + stateRatios = stateRatios.isFinite().select(stateRatios, 0); + + kfMeas.prefitRatios.segment(begH, numH) = measRatios; + this->prefitRatios.segment(begX, numX) = stateRatios; + + statistics.sumOfSquares = measRatios.square().sum(); + statistics.averageRatio = measRatios.mean(); + + Eigen::ArrayXd::Index stateIndex; + Eigen::ArrayXd::Index measIndex; + + double maxStateRatio = stateRatios.abs().maxCoeff(&stateIndex); + double maxMeasRatio = measRatios.abs().maxCoeff(&measIndex); + + int stateChunkIndex = stateIndex + begX; + int measChunkIndex = measIndex + begH; + + auto it = kfIndexMap.begin(); + std::advance(it, stateChunkIndex); + + auto& [stateKey, dummy] = *it; + + // if any are outside the expected values, flag an error + if (maxStateRatio * sqrt(0.95) > maxMeasRatio && + maxStateRatio > prefitOpts.state_sigma_threshold) + { + trace << "\n" + << time << "\tLARGE STATE ERROR OF : " << maxStateRatio << "\tAT " + << stateChunkIndex << " :\t" << stateKey; + trace << "\n" + << time << "\tLargest meas error is : " << maxMeasRatio << "\tAT " << measChunkIndex + << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + + auto mask = (H.col(stateIndex).array() != 0); // Mask out referencing measurements, i.e. + // non-zero values in column stateIndex of H + measRatios.array() *= + mask.cast(); // Set measRatios of non-referencing measurements to 0 + + maxMeasRatio = measRatios.abs().maxCoeff(&measIndex); + measChunkIndex = measIndex + begH; + + trace << "\n" + << time << "\tLargest ref meas error is: " << maxMeasRatio << "\tAT " + << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + + callbackDetails.kfKey = stateKey; + callbackDetails.stateIndex = stateChunkIndex; + callbackDetails.measIndex = measChunkIndex; + + measRatios = (measRatios == 0).select(INFINITY, measRatios); + 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"; + } + else if (maxMeasRatio > prefitOpts.meas_sigma_threshold) + { + trace << "\n" + << time << "\tLARGE MEAS ERROR OF : " << maxMeasRatio << "\tAT " << measChunkIndex + << " :\t" << kfMeas.obsKeys[measChunkIndex]; + trace << "\n" + << time << "\tLargest state error is : " << maxStateRatio << "\tAT " + << stateChunkIndex << " :\t" << stateKey << "\n"; + + callbackDetails.measIndex = measChunkIndex; + } } void outputResiduals( - Trace& trace, ///< Trace file to output to - KFMeas& kfMeas, ///< Measurements, noise, and design matrix - int iteration, ///< Number of iterations prior to this check - string suffix, ///< Suffix to use in header - int begH, ///< Index of first measurement to process - int numH) ///< Number of measurements to process + Trace& trace, ///< Trace file to output to + KFMeas& kfMeas, ///< Measurements, noise, and design matrix + string suffix, ///< Suffix to use in header + int iteration, ///< Number of iterations prior to this check + int begH, ///< Index of first measurement to process + int numH ///< Number of measurements to process +) { - string name = "RESIDUALS"; - name += suffix; - Block block(trace, name); - - tracepdeex(0, trace, "#\t%2s\t%22s\t%10s\t%4s\t%4s\t%5s\t%13s\t%13s\t%16s\t %s\n", "It", "Time", "Type", "Sat", "Str", "Num", "Prefit Res", "Postfit Res", "Meas Sigma", "Comments"); - for (int i = begH; i < begH + numH; i++) - { - char var[32]; - - double sigma = sqrt(kfMeas.R(i,i)); - - if (sigma == 0 || (fabs(sigma) > 0.0001 && fabs(sigma) < 1e7)) snprintf(var, sizeof(var), "%16.7f", sigma); - else snprintf(var, sizeof(var), "%16.3e", sigma); - - tracepdeex(0, trace, "%%\t%2d\t%21s\t%20s\t%13.8f\t%13.8f\t%s\t %s\n", iteration, kfMeas.time.to_string(2).c_str(), ((string)kfMeas.obsKeys[i]).c_str(), kfMeas.V(i), kfMeas.VV(i), var, kfMeas.obsKeys[i].comment.c_str()); - } + tracepdeex(0, trace, "\n"); + + string name = "RESIDUALS"; + name += suffix; + + Block block(trace, name); + + tracepdeex( + 0, + trace, + "#\t%2s\t%22s\t%12s\t%4s\t%4s\t%7s\t%17s\t%17s\t%16s", + "It", + "Time", + "Type", + "Sat", + "Str", + "Code", + "Prefit Res", + "Postfit Res", + "Meas Sigma" + ); + tracepdeex(5, trace, "\t%16s", "Prefit Ratio"); + tracepdeex(5, trace, "\t%16s", "Postfit Ratio"); + tracepdeex(2, trace, "\t%s", "Comments"); + tracepdeex(0, trace, "\n"); + + int endH; + if (numH < 0) + endH = kfMeas.obsKeys.size(); + else + endH = begH + numH; + + for (int i = begH; i < endH; i++) + { + char preResStr[20]; + char postResStr[20]; + char sigmaStr[20]; + char preRatioStr[20]; + char postRatioStr[20]; + + double V = kfMeas.V(i); + + if (V == 0 || (fabs(V) > 0.0001 && fabs(V) < 1e7)) + snprintf(preResStr, sizeof(preResStr), "%17.8f", V); + else + snprintf(preResStr, sizeof(preResStr), "%17.4e", V); + + double VV = kfMeas.VV(i); + + if (VV == 0 || (fabs(VV) > 0.0001 && fabs(VV) < 1e7)) + snprintf(postResStr, sizeof(postResStr), "%17.8f", VV); + else + snprintf(postResStr, sizeof(postResStr), "%17.4e", VV); + + double sigma = sqrt(kfMeas.R(i, i)); + + if (sigma == 0 || (fabs(sigma) > 0.0001 && fabs(sigma) < 1e7)) + snprintf(sigmaStr, sizeof(sigmaStr), "%16.8f", sigma); + else + snprintf(sigmaStr, sizeof(sigmaStr), "%16.4e", sigma); + + double preRatio = 0; + if (i < kfMeas.prefitRatios.rows()) + preRatio = kfMeas.prefitRatios(i); + + if (preRatio == 0 || (fabs(preRatio) > 0.001 && fabs(preRatio) < 1e7)) + snprintf(preRatioStr, sizeof(preRatioStr), "%16.7f", preRatio); + else + snprintf(preRatioStr, sizeof(preRatioStr), "%16.3e", preRatio); + + double postRatio = 0; + if (i < kfMeas.postfitRatios.rows()) + postRatio = kfMeas.postfitRatios(i); + + if (postRatio == 0 || (fabs(postRatio) > 0.001 && fabs(postRatio) < 1e7)) + snprintf(postRatioStr, sizeof(postRatioStr), "%16.7f", postRatio); + else + snprintf(postRatioStr, sizeof(postRatioStr), "%16.3e", postRatio); + + tracepdeex( + 0, + trace, + "%%\t%2d\t%22s\t%30s\t%17.8f\t%17.8f\t%16s", + iteration, + kfMeas.time.to_string(2).c_str(), + ((string)kfMeas.obsKeys[i]).c_str(), + preResStr, + postResStr, + sigmaStr + ); + tracepdeex(5, trace, "\t%16s", preRatioStr); + tracepdeex(5, trace, "\t%16s", postRatioStr); + tracepdeex(2, trace, "\t%s", kfMeas.obsKeys[i].comment.c_str()); + tracepdeex(0, trace, "\n"); + } } /** Compare variances of measurements and filtered states to detect unreasonable values -*/ + */ void KFState::postFitSigmaChecks( - Trace& trace, ///< Trace file to output to - KFMeas& kfMeas, ///< Measurements, noise, and design matrix - VectorXd& dx, ///< The innovations from filtering to recalculate the deltas. - int iteration, ///< Number of iterations prior to this check - KFKey& badStateKey, ///< Key to the state that has worst ratio (only if worse than badMeasIndex) - int& badMeasIndex, ///< Index of the measurement that has the worst ratio - KFStatistics& statistics, ///< Test statistics - 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 + RejectCallbackDetails& callbackDetails, + VectorXd& dx, ///< The state innovations from filtering + MatrixXd& Qinv, ///< Inverse of innovation covariance matrix + MatrixXd& QinvH, ///< Qinv*H matrix for omega test + KFStatistics& statistics, ///< Test statistics + 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 +) { - auto H = kfMeas.H.block(begH, begX, numH, numX); - - //use 'array' for component-wise calculations - auto measVariations = kfMeas.VV .segment(begH, numH).array().square(); //delta squared - auto stateVariations = dx .segment(begX, numX).array().square(); - - auto measVariances = (kfMeas. R.block(begH, begH, numH, numH)).diagonal().array(); - auto stateVariances = P.block(begX, begX, numX, numX) .diagonal().array(); - - ArrayXd measRatios = measVariations / measVariances; - measRatios = measRatios.isFinite() .select(measRatios, 0); - ArrayXd stateRatios = stateVariations / stateVariances; - stateRatios = stateRatios.isFinite().select(stateRatios, 0); - -// trace << "\n" << "DOING SIGMACHECK: "; - - statistics.sumOfSquares = measRatios.sum(); - statistics.averageRatio = measRatios.mean(); - - //if any are outside the expected values, flag an error - - Eigen::ArrayXd::Index stateIndex; - Eigen::ArrayXd::Index measIndex; - -// std::cout << "\nStateRatios\n" << stateRatios; -// std::cout << "\nmeasRatios\n" << measRatios; - - double maxStateRatio = stateRatios .maxCoeff(&stateIndex); - double maxMeasRatio = measRatios .maxCoeff(&measIndex); - - //if any are outside the expected values, flag an error - if ( maxStateRatio > maxMeasRatio - &&maxStateRatio > SQR(postfitOpts.state_sigma_threshold)) - { - int chunkIndex = stateIndex + begX; - - auto it = kfIndexMap.begin(); - std::advance(it, stateIndex); - - auto& [key, dummy] = *it; - - trace << "\n" << "LARGE STATE ERROR OF " << maxStateRatio << " AT " << chunkIndex << " : " << key; - - badStateKey = key; - } - - if (maxMeasRatio > SQR(postfitOpts.meas_sigma_threshold)) - { - int chunkIndex = measIndex + begH; - - trace << "\n" << "LARGE MEAS ERROR OF " << maxMeasRatio << " AT " << chunkIndex << " : " << kfMeas.obsKeys[chunkIndex]; - - badMeasIndex = measIndex + begH; - -// std::cout << "\n" << "P" << "\n" << P.diagonal() << "\n"; -// std::cout << "\n" << "H" << "\n" << H << "\n"; -// std::cout << "\n" << "dx" << "\n" << dx << "\n"; -// std::cout << "\n" << "kfMeas.VV" << "\n" << kfMeas.VV << "\n"; -// std::cout << "\n" << stateRatios << "\n" << "\n" << measRatios << "\n"; - - } + auto& trace = callbackDetails.trace; + auto& kfMeas = callbackDetails.kfMeas; + + auto V = kfMeas.V.segment(begH, numH); + auto VV = kfMeas.VV.segment(begH, numH); + auto R = kfMeas.R.block(begH, begH, numH, numH); + auto H = kfMeas.H.block(begH, begX, numH, numX); + auto P = this->P.block(begX, begX, numX, numX); + + ArrayXd measRatios = ArrayXd::Zero(numH); + ArrayXd stateRatios = ArrayXd::Zero(numX); + + if (postfitOpts.sigma_check) + { + // use 'array' for component-wise calculations + auto measVariations = VV.array(); + auto stateVariations = dx.segment(begX, numX).array(); + + auto measVariances = R.diagonal().array(); + auto stateVariances = P.diagonal().array(); + + measRatios = measVariations / measVariances.sqrt(); + stateRatios = stateVariations / stateVariances.sqrt(); + } + else if (postfitOpts.omega_test) + { + // use 'array' for component-wise calculations + auto measNumerator = (Qinv * V).array(); + auto stateNumerator = (QinvH.transpose() * V).array(); + + auto measDenominator = Qinv.diagonal().array(); + auto stateDenominator = (QinvH.transpose() * H).diagonal().array(); + + measRatios = measNumerator / measDenominator.sqrt(); + stateRatios = stateNumerator / stateDenominator.sqrt(); + } + + // Eugene: do abs() here + measRatios = measRatios.isFinite().select( + measRatios, + 0 + ); // set ratio to 0 if corresponding variance is 0, e.g. ONE state, clk rate states + stateRatios = stateRatios.isFinite().select(stateRatios, 0); + + kfMeas.postfitRatios.segment(begH, numH) = measRatios; + this->postfitRatios.segment(begX, numX) = stateRatios; + + statistics.sumOfSquares = measRatios.square().sum(); + statistics.averageRatio = measRatios.mean(); + + Eigen::ArrayXd::Index stateIndex; + Eigen::ArrayXd::Index measIndex; + + double maxStateRatio = stateRatios.abs().maxCoeff(&stateIndex); + double maxMeasRatio = measRatios.abs().maxCoeff(&measIndex); + + int stateChunkIndex = stateIndex + begX; + int measChunkIndex = measIndex + begH; + + auto it = kfIndexMap.begin(); + std::advance(it, stateChunkIndex); + + auto& [stateKey, dummy] = *it; + + // if any are outside the expected values, flag an error + if (maxStateRatio * sqrt(0.95) > maxMeasRatio && + maxStateRatio > postfitOpts.state_sigma_threshold) + { + trace << "\n" + << time << "\tLARGE STATE ERROR OF : " << maxStateRatio << "\tAT " + << stateChunkIndex << " :\t" << stateKey; + trace << "\n" + << time << "\tLargest meas error is : " << maxMeasRatio << "\tAT " << measChunkIndex + << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + + auto mask = (H.col(stateIndex).array() != 0); // Mask out referencing measurements, i.e. + // non-zero values in column stateIndex of H + measRatios.array() *= + mask.cast(); // Set measRatios of non-referencing measurements to 0 + + maxMeasRatio = measRatios.abs().maxCoeff(&measIndex); + measChunkIndex = measIndex + begH; + + trace << "\n" + << time << "\tLargest ref meas error is: " << maxMeasRatio << "\tAT " + << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + + callbackDetails.kfKey = stateKey; + callbackDetails.stateIndex = stateChunkIndex; + callbackDetails.measIndex = measChunkIndex; + + measRatios = (measRatios == 0).select(INFINITY, measRatios); + 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"; + } + else if (maxMeasRatio > postfitOpts.meas_sigma_threshold) + { + trace << "\n" + << time << "\tLARGE MEAS ERROR OF : " << maxMeasRatio << "\tAT " << measChunkIndex + << " :\t" << kfMeas.obsKeys[measChunkIndex]; + trace << "\n" + << time << "\tLargest state error is : " << maxStateRatio << "\tAT " + << stateChunkIndex << " :\t" << stateKey << "\n"; + + callbackDetails.measIndex = measChunkIndex; + } } /** Compute Chi-square increment based on the change of fitting solution -*/ + */ double KFState::stateChiSquare( - Trace& trace, ///< Trace file to output to - MatrixXd& Pp, ///< Post-update covariance of states - VectorXd& dx, ///< The innovations from filtering to recalculate the deltas. - int begX, ///< Index of first state element to process - int numX, ///< Number of states elements to process - int begH, ///< Index of first measurement to process - int numH) ///< Number of measurements to process + Trace& trace, ///< Trace file to output to + MatrixXd& Pp, ///< Post-update covariance of states + VectorXd& dx, ///< The state innovations from filtering + int begX, ///< Index of first state element to process + int numX, ///< Number of states elements to process + int begH, ///< Index of first measurement to process + int numH ///< Number of measurements to process +) { - if (begX == 0) //exclude the One state - { - begX = 1; - numX -= 1; - } - - auto w = dx.segment(begX, numX); - MatrixXd P = this->P.block(begX, begX, numX, numX); - // MatrixXd dP = this->P.block(begX, begX, numX, numX) - Pp.block(begX, begX, numX, numX); //Ref: Li et al. (2020) - Robust Kalman Filtering Based on Chi-square Increment and Its Application - https://www.mdpi.com/2072-4292/12/4/732/pdf - - double chiSq = w.transpose() * P.inverse() * w; - // double chiSq = w.transpose() * dP.inverse() * w; //Ref: Li et al. (2020) - Robust Kalman Filtering Based on Chi-square Increment and Its Application - https://www.mdpi.com/2072-4292/12/4/732/pdf - //numerical instability problem exists for dP.inverse() - - trace << "\n" << "DOING STATE CHI-SQUARE TEST:"; - // for (int i = 0; i < numX; i++) trace << "dx: " << w(i) << "\tdP: " << dP(i, i) << "\n"; - - return chiSq; + if (begX == 0) // exclude the One state + { + begX = 1; + numX -= 1; + } + + auto w = dx.segment(begX, numX); + auto P = this->P.block(begX, begX, numX, numX); + // MatrixXd dP = this->P.block(begX, begX, numX, numX) - Pp.block(begX, begX, numX, numX); + // //Ref: Li et al. (2020) + // - Robust Kalman Filtering Based on Chi-square Increment and Its Application - + // https://www.mdpi.com/2072-4292/12/4/732/pdf + + double chiSq = w.transpose() * P.inverse() * w; + // double chiSq = w.transpose() * dP.inverse() * w; //Ref: Li et al. (2020) - Robust + // Kalman Filtering Based on Chi-square Increment and Its Application - + // https://www.mdpi.com/2072-4292/12/4/732/pdf numerical instability problem exists for + // dP.inverse() + + trace << "\n" + << "DOING STATE CHI-SQUARE TEST:"; + // for (int i = 0; i < numX; i++) trace << "dx: " << w(i) << "\tdP: " << dP(i, i) << "\n"; + + return chiSq; } /** Compute Chi-square increment based on post-fit residuals -*/ + */ double KFState::measChiSquare( - Trace& trace, ///< Trace file to output to - KFMeas& kfMeas, ///< Measurements, noise, and design matrix - VectorXd& dx, ///< The innovations from filtering to recalculate the deltas. - int begX, ///< Index of first state element to process - int numX, ///< Number of states elements to process - int begH, ///< Index of first measurement to process - int numH) ///< Number of measurements to process + Trace& trace, ///< Trace file to output to + KFMeas& kfMeas, ///< Measurements, noise, and design matrix + VectorXd& dx, ///< The state innovations from filtering + int begX, ///< Index of first state element to process + int numX, ///< Number of states elements to process + int begH, ///< Index of first measurement to process + int numH ///< Number of measurements to process +) { - auto w = dx.segment(begX, numX); - auto H = kfMeas.H.block(begH, begX, numH, numX); - VectorXd v = kfMeas.V.segment(begH, numH) - H * w; - auto R = kfMeas.R.block(begH, begH, numH, numH); + auto w = dx.segment(begX, numX); + auto H = kfMeas.H.block(begH, begX, numH, numX); + auto VV = kfMeas.VV.segment(begH, numH); + auto R = kfMeas.R.block(begH, begH, numH, numH); - double chiSq = (v.array().square() / R.diagonal().array()).sum(); + double chiSq = (VV.array().square() / R.diagonal().array()).sum(); - trace << "\n" << "DOING MEASUREMENT CHI-SQUARE TEST:"; - // for (int i = 0; i < numH; i++) trace << "v(+): " << v(i) << "\tR: " << R(i, i) << "\n"; + trace << "\n" + << "DOING MEASUREMENT CHI-SQUARE TEST:"; + // for (int i = 0; i < numH; i++) trace << "v(+): " << VV(i) << "\tR: " << R(i, i) << + // "\n"; - return chiSq; + return chiSq; } /** Compute Chi-square increment based on pre-fit residuals (innovations) -*/ + */ double KFState::innovChiSquare( - Trace& trace, ///< Trace to output to - KFMeas& kfMeas, ///< Measurements, noise, and design matrix - int begX, ///< Index of first state element to process - int numX, ///< Number of states elements to process - int begH, ///< Index of first measurement to process - int numH) ///< Number of measurements to process + Trace& trace, ///< Trace to output to + KFMeas& kfMeas, ///< Measurements, noise, and design matrix + int begX, ///< Index of first state element to process + int numX, ///< Number of states elements to process + int begH, ///< Index of first measurement to process + int numH ///< Number of measurements to process +) { - auto H = kfMeas.H.block(begH, begX, numH, numX); - auto v = kfMeas.V.segment(begH, numH); - auto R = kfMeas.R.block(begH, begH, numH, numH); - auto P = this->P.block(begX, begX, numX, numX); - MatrixXd Q = R + H * P * H.transpose(); + auto H = kfMeas.H.block(begH, begX, numH, numX); + auto V = kfMeas.V.segment(begH, numH); + auto R = kfMeas.R.block(begH, begH, numH, numH); + auto P = this->P.block(begX, begX, numX, numX); + MatrixXd Q = R + H * P * H.transpose(); - double chiSq = v.transpose() * Q.inverse() * v; + double chiSq = V.transpose() * Q.inverse() * V; - trace << "\n" << "DOING INNOVATION CHI-SQUARE TEST:"; - // for (int i = 0; i < numH; i++) trace << "v(-): " << v(i) << "\tS: " << Q(i, i) << "\n"; + trace << "\n" + << "DOING INNOVATION CHI-SQUARE TEST:"; + // for (int i = 0; i < numH; i++) trace << "v(-): " << v(i) << "\tS: " << Q(i, i) << + // "\n"; - return chiSq; + return chiSq; } /** Kalman filter. -*/ + */ bool KFState::kFilter( - Trace& trace, ///< Trace to output to - KFMeas& kfMeas, ///< Measurements, noise, and design matrices - VectorXd& xp, ///< Post-update state vector - MatrixXd& Pp, ///< Post-update covariance of states - VectorXd& dx, ///< Post-update state innovation - 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 + Trace& trace, ///< Trace to output to + KFMeas& kfMeas, ///< Measurements, noise, and design matrices + VectorXd& xp, ///< Post-update state vector + MatrixXd& Pp, ///< Post-update covariance of states + VectorXd& + dx, ///< Post-update state innovation // Eugene: change name to avoid interference + MatrixXd& Qinv, ///< Inverse of innovation covariance matrix + MatrixXd& QinvH, ///< Qinv*H matrix for omega test + 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 +) { - auto& R = kfMeas.R; - auto& v = kfMeas.V; - auto& H = kfMeas.H; - auto& H_star = kfMeas.H_star; - auto& noise = kfMeas.uncorrelatedNoise.asDiagonal(); - - auto subH = H.block(begH, begX, numH, numX); - - auto HRH_star = H_star * noise * H_star.transpose(); - auto HP = subH * P.block(begX, begX, numX, numX); - MatrixXd Q = HP * subH.transpose() - + R.block(begH, begH, numH, numH); - - MatrixXd K; - MatrixXd HRHQ_star; - - bool repeat = true; - while (repeat) - { - switch (inverter) - { - default: - { - BOOST_LOG_TRIVIAL(warning) << "Warning: kalman filter inverter type " << inverter << " not supported, reverting"; - inverter = E_Inverter::LDLT; - continue; - } - case E_Inverter::LDLT: - { - auto QQ = Q.triangularView().transpose(); - LDLT solver; - if ( ( solver.compute(QQ), solver.info() != Eigen::ComputationInfo::Success) - || (K = solver.solve(HP) .transpose(), solver.info() != Eigen::ComputationInfo::Success) - ||(advanced_postfits && (HRHQ_star = solver.solve(HRH_star) .transpose(), solver.info() != Eigen::ComputationInfo::Success))) - { - xp = x; - Pp = P; - dx = VectorXd::Zero(xp.rows()); - - BOOST_LOG_TRIVIAL(error) << "Error: Failed to calculate kalman gain, see trace file for matrices"; - - trace << "\n" << "Kalman Filter Error1"; - trace << "\n" << "Q:" << "\n" << Q; - trace << "\n" << "H:" << "\n" << H; - trace << "\n" << "R:" << "\n" << R; - trace << "\n" << "P:" << "\n" << P; - - return false; - } - - break; - } - case E_Inverter::LLT: - { - auto QQ = Q.triangularView().transpose(); - LLT solver; - if ( ( solver.compute(QQ), solver.info() != Eigen::ComputationInfo::Success) - || (K = solver.solve(HP) .transpose(), solver.info() != Eigen::ComputationInfo::Success) - ||(advanced_postfits && (HRHQ_star = solver.solve(HRH_star) .transpose(), solver.info() != Eigen::ComputationInfo::Success))) - { - inverter = E_Inverter::LDLT; - continue; - } - - break; - } - case E_Inverter::INV: - { - MatrixXd Qinv = Q.inverse(); - K = P * H.transpose() * Qinv; - - if (advanced_postfits) - { - HRHQ_star = HRH_star * Qinv; - } - break; - } - } - repeat = false; - } - - if (advanced_postfits) - { - kfMeas.VV = HRHQ_star * v; - } - - dx.segment(begX, numX) = K * v.segment(begH, numH); - xp.segment(begX, numX) = x. segment(begX, numX) - + dx.segment(begX, numX); - -// trace << "\n" << "h " << "\n" << subH; -// trace << "\n" << "hp " << "\n" << HP; -// trace << "\n" << "Q " << "\n" << Q; -// trace << "\n" << "K " << "\n" << K; -// trace << "\n" << "X " << "\n" << x. segment(begX, numX); -// trace << "\n" << "DX" << "\n" << dx.segment(begX, numX); -// trace << "\n" << "xp" << "\n" << xp.segment(begX, numX); - - if (joseph_stabilisation) - { - MatrixXd IKH = MatrixXd::Identity(P.rows(), P.cols()) - K * H; - Pp = IKH * P * IKH.transpose() + K * R * K.transpose(); - } - else - { - Pp.block(begX, begX, numX, numX) = P.block(begX, begX, numX, numX) - K * HP; - - Pp.block(begX, begX, numX, numX) = ( Pp.block(begX, begX, numX, numX) - + Pp.block(begX, begX, numX, numX).transpose() ).eval() / 2; - } - - bool error = xp.segment(begX, numX).array().isNaN().any(); - if (error) - { - std::cout << "\n" << "x:" << "\n" << x << "\n"; - std::cout << "\n" << "xp:" << "\n" << xp << "\n"; - std::cout << "\n" << "R :" << "\n" << R << "\n"; - std::cout << "\n" << "K :" << "\n" << K << "\n"; - std::cout << "\n" << "P :" << "\n" << P << "\n"; - std::cout << "\n" << "v :" << "\n" << v << "\n"; - std::cout << "\n"; - std::cout << "NAN found. Exiting..."; - std::cout << "\n"; - - exit(0); - } - - return true; + auto& R = kfMeas.R; + auto& V = kfMeas.V; + auto& H = kfMeas.H; + auto& H_star = kfMeas.H_star; + + auto noise = kfMeas.uncorrelatedNoise.asDiagonal(); // todo Eugene: check chunking indices + + // Get pointers to block data (no copying!) + const double* H_ptr = H.data() + begH + begX * H.rows(); // H block starting point + const double* P_ptr = P.data() + begX + begX * P.rows(); // P block starting point + const double* R_ptr = R.data() + begH + begH * R.rows(); // R block starting point + const double* V_ptr = V.data() + begH; // V segment starting point + + int ldH = H.rows(); // Leading dimension of full H matrix + int ldP = P.rows(); // Leading dimension of full P matrix + int ldR = R.rows(); // Leading dimension of full R matrix + + MatrixXd I = MatrixXd::Identity(numH, numH); + auto subH_star = H_star.middleRows(begH, numH); + MatrixXd HRH_star = subH_star * noise * subH_star.transpose(); + + // Compute HP = H * P using BLAS directly on blocks (no copy!) + MatrixXd HP(numH, numX); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numH, + numX, + numX, + 1.0, + H_ptr, + ldH, + P_ptr, + ldP, + 0.0, + HP.data(), + numH + ); + + // Compute Q = HP * H' + R using BLAS directly + MatrixXd Q(numH, numH); + // First: Q = R (copy R block) + for (int j = 0; j < numH; j++) + { + LapackWrapper::dcopy(numH, R_ptr + j * ldR, 1, Q.data() + j * numH, 1); + } + // Then: Q = HP * H' + Q + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasTrans, + numH, + numH, + numX, + 1.0, + HP.data(), + numH, + H_ptr, + ldH, + 1.0, + Q.data(), + numH + ); + + MatrixXd K; + MatrixXd HRHQ_star; + + // Sequential solver fallback chain using separate factorization and solve + // This allows us to factorize Q once and reuse it for multiple solves + // Order: dpotrf/dpotrs -> dsytrf/dsytrs -> dgetrf/dgetrs + + int info; + std::vector ipiv(numH); + MatrixXd Q_work = Q; // Working copy for factorization + char uplo = 'U'; + int solver_used = 0; // 1=Cholesky, 2=LDLT, 3=LU + + // Try 1: Cholesky factorization (dpotrf) - fastest, for symmetric positive definite + info = LapackWrapper::dpotrf(LapackWrapper::COL_MAJOR, uplo, numH, Q_work.data(), numH); + + if (info == 0) + { + solver_used = 1; + } + else + { + BOOST_LOG_TRIVIAL(warning) << "dpotrf (Cholesky factorization) failed with info = " << info + << ", trying dsytrf (symmetric indefinite)"; + + // Cholesky failed, restore Q and try symmetric indefinite + Q_work = Q; + + // Try 2: Symmetric indefinite factorization (dsytrf) + info = LapackWrapper::dsytrf( + LapackWrapper::COL_MAJOR, + uplo, + numH, + Q_work.data(), + numH, + ipiv.data() + ); + + if (info == 0) + { + solver_used = 2; + } + else + { + BOOST_LOG_TRIVIAL(warning) + << "dsytrf (symmetric indefinite factorization) failed with info = " << info + << ", trying dgetrf (general LU)"; + + // Both symmetric solvers failed, try general LU + Q_work = Q; + + // Try 3: General LU factorization (dgetrf) + info = LapackWrapper::dgetrf( + LapackWrapper::COL_MAJOR, + numH, + numH, + Q_work.data(), + numH, + ipiv.data() + ); + + if (info == 0) + { + solver_used = 3; + } + else + { + // All factorizations failed + BOOST_LOG_TRIVIAL(error) + << "dgetrf (general LU factorization) failed with info = " << info + << " - all factorization methods exhausted"; + + 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; + + return false; + } + } + } + + // Now Q_work contains the factorization, solve multiple systems using the same factorization + + // System 1: Compute Kalman gain: Solve Q * K' = HP for K' + MatrixXd KT = HP; // Will be overwritten with solution + + if (solver_used == 1) + { + // Cholesky solve + info = LapackWrapper::dpotrs( + LapackWrapper::COL_MAJOR, + uplo, + numH, + numX, + Q_work.data(), + numH, + KT.data(), + numH + ); + } + else if (solver_used == 2) + { + // Symmetric indefinite solve + info = LapackWrapper::dsytrs( + LapackWrapper::COL_MAJOR, + uplo, + numH, + numX, + Q_work.data(), + numH, + ipiv.data(), + KT.data(), + numH + ); + } + else // solver_used == 3 + { + // General LU solve + info = LapackWrapper::dgetrs( + LapackWrapper::COL_MAJOR, + 'N', + numH, + numX, + Q_work.data(), + numH, + ipiv.data(), + KT.data(), + numH + ); + } + + if (info != 0) + { + BOOST_LOG_TRIVIAL(error) << "Solve failed for Kalman gain with info = " << info; + + xp = x; + Pp = P; + dx = VectorXd::Zero(xp.rows()); + return false; + } + + // Transpose to get K = (K')' + K = KT.transpose(); + + // System 3: Compute HRHQ_star for advanced postfits (do this FIRST - no copy needed) + // Reuse the same factorization from Q_work directly + if (advanced_postfits) + { + HRHQ_star = HRH_star; + + if (solver_used == 1) + { + // Cholesky solve: Q * HRHQ_star = HRH_star + info = LapackWrapper::dpotrs( + LapackWrapper::COL_MAJOR, + uplo, + numH, + numH, + Q_work.data(), + numH, + HRHQ_star.data(), + numH + ); + } + else if (solver_used == 2) + { + // Symmetric indefinite solve: Q * HRHQ_star = HRH_star + info = LapackWrapper::dsytrs( + LapackWrapper::COL_MAJOR, + uplo, + numH, + numH, + Q_work.data(), + numH, + ipiv.data(), + HRHQ_star.data(), + numH + ); + } + else // solver_used == 3 + { + // General LU solve: Q * HRHQ_star = HRH_star + info = LapackWrapper::dgetrs( + LapackWrapper::COL_MAJOR, + 'N', + numH, + numH, + Q_work.data(), + numH, + ipiv.data(), + HRHQ_star.data(), + numH + ); + } + + if (info == 0) + { + HRHQ_star.transposeInPlace(); + } + else + { + BOOST_LOG_TRIVIAL(warning) + << "Failed to compute HRHQ_star for advanced postfits (info = " << info << ")"; + } + } + + // System 2: Compute Qinv if needed for omega test (do this LAST) + // Use direct matrix inversion (dpotri/dsytri/dgetri) working directly on Q_work + // Since System 3 is already done, Q_work is no longer needed + if (postfitOpts.omega_test) + { + // Work directly on Q_work (will be overwritten with inverse, no copy needed!) + if (solver_used == 1) + { + // Cholesky: Compute inverse directly from L*L' factorization + info = LapackWrapper::dpotri(LapackWrapper::COL_MAJOR, uplo, numH, Q_work.data(), numH); + + if (info == 0) + { + // dpotri only fills upper triangle (uplo='U'), copy to lower using pointer access + double* Q_ptr = Q_work.data(); + for (int j = 0; j < numH; j++) + { + for (int i = j + 1; i < numH; i++) + { + Q_ptr[j * numH + i] = + Q_ptr[i * numH + j]; // Column-major: copy upper to lower + } + } + } + } + else if (solver_used == 2) + { + // Symmetric indefinite: Compute inverse directly from LDLT factorization + info = LapackWrapper::dsytri( + LapackWrapper::COL_MAJOR, + uplo, + numH, + Q_work.data(), + numH, + ipiv.data() + ); + + if (info == 0) + { + // dsytri only fills upper triangle (uplo='U'), copy to lower using pointer access + double* Q_ptr = Q_work.data(); + for (int j = 0; j < numH; j++) + { + for (int i = j + 1; i < numH; i++) + { + Q_ptr[j * numH + i] = + Q_ptr[i * numH + j]; // Column-major: copy upper to lower + } + } + } + } + else // solver_used == 3 + { + // General LU: Compute inverse directly from PLU factorization + info = LapackWrapper::dgetri( + LapackWrapper::COL_MAJOR, + numH, + Q_work.data(), + numH, + ipiv.data() + ); + // dgetri fills the full matrix, no symmetrization needed + } + + if (info == 0) + { + // Quick validation - sample diagonal elements for NaN/Inf (faster than checking all n²) + bool is_valid = true; + const int sample_stride = std::max(1, numH / 10); // Sample ~10 elements + for (int i = 0; i < numH; i += sample_stride) + { + if (!std::isfinite(Q_work(i, i))) // Check diagonal + { + is_valid = false; + BOOST_LOG_TRIVIAL(error) + << "Matrix inversion produced invalid diagonal value (NaN/Inf) at row " << i + << ", solver_used=" << solver_used; + break; + } + } + + if (is_valid) + { + // Assign inverted Q_work to Qinv (move semantics, no copy!) + Qinv = std::move(Q_work); + + // Compute QinvH = Qinv * H + QinvH.resize(numH, numX); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numH, + numX, + numH, + 1.0, + Qinv.data(), + numH, + H_ptr, + ldH, + 0.0, + QinvH.data(), + numH + ); + } + else + { + // Fallback: compute Qinv by solving if inversion produced invalid values + BOOST_LOG_TRIVIAL(warning) + << "Falling back to solving Q*Qinv=I due to invalid inversion result"; + + Qinv = MatrixXd::Identity(numH, numH); + + // Re-copy factorization (it was overwritten) + LapackWrapper::dcopy(numH * numH, Q_work.data(), 1, Qinv.data(), 1); + + if (solver_used == 1) + { + LapackWrapper::dpotrs( + LapackWrapper::COL_MAJOR, + uplo, + numH, + numH, + Q_work.data(), + numH, + Qinv.data(), + numH + ); + } + else if (solver_used == 2) + { + LapackWrapper::dsytrs( + LapackWrapper::COL_MAJOR, + uplo, + numH, + numH, + Q_work.data(), + numH, + ipiv.data(), + Qinv.data(), + numH + ); + } + else + { + LapackWrapper::dgetrs( + LapackWrapper::COL_MAJOR, + 'N', + numH, + numH, + Q_work.data(), + numH, + ipiv.data(), + Qinv.data(), + numH + ); + } + + QinvH.resize(numH, numX); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numH, + numX, + numH, + 1.0, + Qinv.data(), + numH, + H_ptr, + ldH, + 0.0, + QinvH.data(), + numH + ); + } + } + else + { + BOOST_LOG_TRIVIAL(error) << "Matrix inversion failed with info = " << info + << ", solver_used=" << solver_used; + } + } + + if (advanced_postfits) + { + kfMeas.VV.segment(begH, numH) = HRHQ_star * VectorXd::Map(V_ptr, numH); + } + + // Use BLAS-optimized state update: dx = K * v + LapackWrapper::dgemv( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + numX, + numH, + 1.0, + K.data(), + numX, + V_ptr, + 1, + 0.0, + dx.data() + begX, + 1 + ); + + // xp = x + dx + LapackWrapper::dcopy(numX, x.data() + begX, 1, xp.data() + begX, 1); + LapackWrapper::daxpy(numX, 1.0, dx.data() + begX, 1, xp.data() + begX, 1); + + // trace << "\n" << "H " << "\n" << subH; + // trace << "\n" << "Hp " << "\n" << HP; + // trace << "\n" << "Q " << "\n" << Q; + // trace << "\n" << "K " << "\n" << K; + // trace << "\n" << "x " << "\n" << x. segment(begX, numX); + // trace << "\n" << "dx" << "\n" << dx.segment(begX, numX); + // trace << "\n" << "xp" << "\n" << xp.segment(begX, numX); + + // Use BLAS-optimized covariance update + MatrixXd subPp(numX, numX); + if (joseph_stabilisation) + { + // Joseph form: Pp = (I-K*H)*P*(I-K*H)' + K*R*K' + + // Step 1: IKH = I - K*H + MatrixXd IKH = MatrixXd::Identity(numX, numX); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numX, + numX, + numH, + -1.0, + K.data(), + numX, + H_ptr, + ldH, + 1.0, + IKH.data(), + numX + ); + + // Step 2: temp = IKH * P + MatrixXd temp(numX, numX); + double* P_block = const_cast(P_ptr); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numX, + numX, + numX, + 1.0, + IKH.data(), + numX, + P_block, + ldP, + 0.0, + temp.data(), + numX + ); + + // Step 3: subPp = temp * IKH' + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasTrans, + numX, + numX, + numX, + 1.0, + temp.data(), + numX, + IKH.data(), + numX, + 0.0, + subPp.data(), + numX + ); + + // Step 4: temp2 = K * R + MatrixXd temp2(numX, numH); + double* R_block = const_cast(R_ptr); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numX, + numH, + numH, + 1.0, + K.data(), + numX, + R_block, + ldR, + 0.0, + temp2.data(), + numX + ); + + // Step 5: subPp += temp2 * K' + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasTrans, + numX, + numX, + numH, + 1.0, + temp2.data(), + numX, + K.data(), + numX, + 1.0, + subPp.data(), + numX + ); + } + else + { + // Standard form: Pp = P - K*HP + + // Copy P block into subPp + double* P_block = const_cast(P_ptr); + for (int j = 0; j < numX; j++) + { + LapackWrapper::dcopy(numX, P_block + j * ldP, 1, subPp.data() + j * numX, 1); + } + + // Compute KHP = K * HP + MatrixXd KHP(numX, numX); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numX, + numX, + numH, + 1.0, + K.data(), + numX, + HP.data(), + numH, + 0.0, + KHP.data(), + numX + ); + + // subPp = subPp - KHP + LapackWrapper::daxpy(numX * numX, -1.0, KHP.data(), 1, subPp.data(), 1); + } + + // Symmetrize for numerical stability + for (int i = 0; i < numX; i++) + { + for (int j = i + 1; j < numX; j++) + { + double avg = (subPp(i, j) + subPp(j, i)) / 2.0; + subPp(i, j) = avg; + subPp(j, i) = avg; + } + } + + Pp.block(begX, begX, numX, numX) = (subPp + subPp.transpose()).eval() / 2; + + bool error = xp.segment(begX, numX).array().isNaN().any(); + if (error) + { + std::cout << "\n" + << "xp:" << "\n" + << xp.segment(begX, numX); + std::cout << "\n" + << "x :" << "\n" + << x.segment(begX, numX); + std::cout << "\n" + << "P block:" << "\n" + << P.block(begX, begX, numX, numX); + std::cout << "\n" + << "R block:" << "\n" + << R.block(begH, begH, numH, numH); + std::cout << "\n" + << "K :" << "\n" + << K; + std::cout << "\n" + << "V segment:" << "\n" + << V.segment(begH, numH); + std::cout << "\n"; + std::cout << "NAN found. Exiting..."; + std::cout << "\n"; + + exit(0); + } + + return true; } -/** Perform chi squared quality control. -*/ -bool KFState::chiQC( - Trace& trace, ///< Trace to output to - KFMeas& kfMeas, ///< Measurements, noise, and design matrix - VectorXd& xp) ///< Post filtered state vector +/** Least squares estimator. + */ +bool KFState::leastSquare( + Trace& trace, ///< Trace to output to + KFMeas& kfMeas, ///< Measurements, noise, and design matrices + VectorXd& xp, ///< Post-fit parameter vector + MatrixXd& Pp ///< Post-fit covariance of parameters +) { - auto& H = kfMeas.H; - auto W = kfMeas.W(all, 0); - - VectorXd& y = kfMeas.Y; - VectorXd v = y - H * xp; - double v_Wv = v.transpose() * W.asDiagonal() * v; + // invert measurement noise matrix to get a weight matrix + ArrayXd weights = 1 / kfMeas.R.diagonal().array(); // Eugene to check if R is diagonal + weights = + weights.isFinite().select(weights, 0); // Set weight to 0 if corresponding variance is 0 + kfMeas.W = weights.matrix(); + + auto& H = kfMeas.H; + auto& V = kfMeas.V; + + int numX = H.cols(); + int numH = H.rows(); + + if (numX == 0 || numH == 0) + { + trace << "\n" + << "EMPTY DESIGN MATRIX DURING LEAST SQUARES"; + return false; + } + + if (numH < numX) + { + trace << "\n" + << "Insufficient measurements for least squares " << numH << " < " << numX; + return false; + } + + // calculate least squares solution + MatrixXd I = MatrixXd::Identity(numX, numX); + MatrixXd W = kfMeas.W.asDiagonal(); + MatrixXd H_W = H.transpose() * W; + MatrixXd N = H_W * H; + + bool repeat = true; + while (repeat) + { + switch (lsq_inverter) + { + default: + { + BOOST_LOG_TRIVIAL(warning) << "Least squares inverter type " << lsq_inverter + << " not supported, reverting"; + + lsq_inverter = E_Inverter::LDLT; + continue; + } + case E_Inverter::LDLT: + { + auto NN = N.triangularView().transpose(); + LDLT solver; + if ((solver.compute(NN), solver.info() != Eigen::ComputationInfo::Success) || + (Pp = solver.solve(I), solver.info() != Eigen::ComputationInfo::Success)) + { + BOOST_LOG_TRIVIAL(error) + << "Failed to solve normal equation, see trace file for matrices"; + + trace << "\n" + << "Least Squares Error"; + trace << "\n" + << "N: " << "\n" + << N; + trace << "\n" + << "H: " << "\n" + << H; + trace << "\n" + << "W: " << "\n" + << W; + + return false; + } + + break; + } + case E_Inverter::LLT: + { + auto NN = N.triangularView().transpose(); + LLT solver; + if ((solver.compute(NN), solver.info() != Eigen::ComputationInfo::Success) || + (Pp = solver.solve(I), solver.info() != Eigen::ComputationInfo::Success)) + { + BOOST_LOG_TRIVIAL(warning) << "Normal matrix not invertible with LLT " + "inverter, trying LDLT inverter instead"; + + lsq_inverter = E_Inverter::LDLT; + continue; + } + + break; + } + case E_Inverter::INV: + { + Pp = N.inverse(); + + if (Pp.array().isNaN().any() || Pp.array().isInf().any()) + { + BOOST_LOG_TRIVIAL(warning) << "Normal matrix not invertible with INV " + "inverter, trying LDLT inverter instead"; + + lsq_inverter = E_Inverter::LDLT; + continue; + } + + break; + } + } + + repeat = false; + } + + xp = Pp * H_W * V; + + // std::cout << "N : " << "\n" << N; + bool error = xp.array().isNaN().any(); + if (error) + { + std::cout << "\n" + << "xp:" << "\n" + << xp << "\n"; + std::cout << "\n" + << "P :" << "\n" + << P << "\n"; + std::cout << "\n" + << "W :" << "\n" + << W << "\n"; + std::cout << "\n" + << "H :" << "\n" + << H << "\n"; + std::cout << "\n"; + std::cout << "NAN found. Exiting...."; + std::cout << "\n"; + + exit(-1); + } + + return true; +} - //trace << "\n" << "chiqcV" << v.rows() << "\n"; +/** Perform chi squared quality control. + */ +void KFState::chiQC( + Trace& trace, ///< Trace to output to + KFMeas& kfMeas ///< Measurements, noise, and design matrix +) +{ + auto& VV = kfMeas.VV; + auto W = kfMeas.W.asDiagonal(); - dof = v.rows() - (x.rows() - 1); //ignore KF::ONE element -> -1 - chi = v_Wv; + dof = VV.rows() - (x.rows() - 1); // ignore KF::ONE element -> -1 + if (dof < 1) + { + trace << "Error with Chi-square test: dof=" << dof; - if (dof < 1) - { - chiQCPass = true; - return true; - } + chiQCPass = false; + chi2 = 0; + chi2PerDof = NAN; + qc = 0; - boost::math::normal normDist; + return; + } - double alpha = cdf(complement(normDist, chiSquareTest.sigma_threshold)) * 2; //two-tailed + chi2 = VV.transpose() * W * VV; + chi2PerDof = chi2 / dof; - boost::math::chi_squared chiSqDist(dof); + boost::math::normal normDist; - double thres = quantile(complement(chiSqDist, alpha)); + double alpha = cdf(complement(normDist, chiSquareTest.sigma_threshold)) * 2; // two-tailed - /* chi-square validation */ - if (chi > thres) - { - tracepdeex(5, trace, "\nChiSquare error detected: dof:%d chi:%f thres:%f", dof, chi, thres); + boost::math::chi_squared chiSqDist(dof); -// auto variations = v.array().square(); -// Eigen::MatrixXf::Index index; -// trace << " -> LARGEe ERROR OF " << sqrt(variations.maxCoeff(&index)) << " AT " << index; + qc = quantile(complement(chiSqDist, alpha)); - chiQCPass = false; - return false; - } + /* chi-square validation */ + if (chi2 > qc) + { + chiQCPass = false; + return; + } - chiQCPass = true; - return true; + chiQCPass = true; + return; } /** Combine a list of KFMeasEntrys into a single KFMeas object for used in the filter -*/ + */ KFMeas::KFMeas( - KFState& kfState, ///< Filter state to correspond to - KFMeasEntryList& kfEntryList, ///< List of input measurements as lists of entries - GTime measTime, ///< Time to use for measurements and hence state transitions - MatrixXd* noiseMatrix_ptr) ///< Optional pointer to use custom noise matrix + KFState& kfState, ///< Filter state to correspond to + KFMeasEntryList& kfEntryList, ///< List of input measurements as lists of entries + GTime measTime, ///< Time to use for measurements and hence state transitions + MatrixXd* noiseMatrix_ptr ///< Optional pointer to use custom noise matrix +) { - int numMeas = kfEntryList.size(); - - if (measTime == GTime::noTime()) - { - measTime = time; - } - - time = measTime; - - V .resize(numMeas); - VV .resize(numMeas); - Y .resize(numMeas); - - //merge all individual noise elements into the a new map and then vector - { - map noiseElementMap; - - for (auto& entry : kfEntryList) - for (auto& [kfKey, value] : entry.noiseElementMap) - { - noiseElementMap[kfKey] = value; - } - - uncorrelatedNoise = VectorXd::Zero(noiseElementMap.size()); - - int noises = 0; - for (auto& [key, value] : noiseElementMap) - { - uncorrelatedNoise(noises) = value; - - noiseIndexMap[key] = noises; - noises++; - } - } - - R = MatrixXd::Zero(numMeas, numMeas); - H = MatrixXd::Zero(numMeas, kfState.x .rows()); - H_star = MatrixXd::Zero(numMeas, uncorrelatedNoise .rows()); - - obsKeys .resize(numMeas); - metaDataMaps .resize(numMeas); - componentsMaps .resize(numMeas); - - bool error = false; -# ifdef ENABLE_PARALLELISATION - Eigen::setNbThreads(1); -# pragma omp parallel for -# endif - for (int meas = 0; meas < kfEntryList.size(); meas++) - { - auto it = kfEntryList.begin(); - std::advance(it, meas); - - auto& entry = *it; - - R(meas, meas) = entry.noise; - - auto& value = Y(meas); - auto& innov = V(meas); - - value = entry.value; - innov = entry.innov; - - for (auto& [kfKey, coeff] : entry.designEntryMap) - { - if (coeff == 0) - { - continue; - } - - int index = kfState.getKFIndex(kfKey); - if (index < 0) - { - std::cout << "Code error: Trying to create measurement for undefined key, check stateTransition() is called first: " << kfKey << "\n"; - error = true; - } - H(meas, index) = coeff; - - if (kfState.assume_linearity) - { - double xVal = kfState.x[index]; - double uVal = entry.usedValueMap[kfKey]; - - double deltaX = xVal - uVal; - if (deltaX) - { - BOOST_LOG_TRIVIAL(debug) << std::fixed - << "Adjusting meas '" << entry.obsKey << "' as '" << kfKey << "' changed " << deltaX - << "\tfrom " << uVal - << "\tto " << xVal - << "\t : " << innov - << "\t-> " << innov - deltaX * coeff; - - value -= deltaX * coeff; - innov -= deltaX * coeff; - } - } - } - - for (auto& [kfKey, coeff] : entry.noiseEntryMap) - { - int index = getNoiseIndex(kfKey); - if (index < 0) - { - std::cout << "Code error: Trying to create measurement for undefined key :" << kfKey << "\n"; - error = true; - } - H_star(meas, index) = coeff; - } - - obsKeys [meas] = std::move(entry.obsKey); - metaDataMaps [meas] = std::move(entry.metaDataMap); - componentsMaps [meas] = std::move(entry.componentsMap); - } - Eigen::setNbThreads(0); - - if (error) - { - return; - } - - if (noiseMatrix_ptr) - { - R = *noiseMatrix_ptr; - } - - if (uncorrelatedNoise.rows() != 0) - { - SparseMatrix R_A = SparseMatrix(numMeas, uncorrelatedNoise.rows()); - - int meas = 0; - for (auto& entry: kfEntryList) - { - for (auto& [kfKey, value] : entry.noiseEntryMap) - { - int noiseIndex = noiseIndexMap[kfKey]; - - R_A.insert(meas, noiseIndex) = value; - } - - meas++; - } - - R = R_A * uncorrelatedNoise.asDiagonal() * R_A.transpose(); - } + int numMeas = kfEntryList.size(); + + if (measTime == GTime::noTime()) + { + measTime = time; + } + + time = measTime; + + V.resize(numMeas); + VV.resize(numMeas); + Y.resize(numMeas); + + // merge all individual noise elements into the a new map and then vector + { + map noiseElementMap; + + for (auto& entry : kfEntryList) + for (auto& [kfKey, value] : entry.noiseElementMap) + { + noiseElementMap[kfKey] = value; + } + + uncorrelatedNoise = VectorXd::Zero(noiseElementMap.size()); + + int noises = 0; + for (auto& [key, value] : noiseElementMap) + { + uncorrelatedNoise(noises) = value; + + noiseIndexMap[key] = noises; + noises++; + } + } + + R = MatrixXd::Zero(numMeas, numMeas); + H = MatrixXd::Zero(numMeas, kfState.x.rows()); + H_star = MatrixXd::Zero(numMeas, uncorrelatedNoise.rows()); + + prefitRatios = VectorXd::Zero(numMeas); + postfitRatios = VectorXd::Zero(numMeas); + + obsKeys.resize(numMeas); + metaDataMaps.resize(numMeas); + componentsMaps.resize(numMeas); + + bool error = false; +#ifdef ENABLE_PARALLELISATION + Eigen::setNbThreads(1); +#pragma omp parallel for +#endif + for (int meas = 0; meas < kfEntryList.size(); meas++) + { + auto it = kfEntryList.begin(); + std::advance(it, meas); + + auto& entry = *it; + + R(meas, meas) = entry.noise; + + auto& value = Y(meas); + auto& innov = V(meas); + + value = entry.value; + innov = entry.innov; + + for (auto& [kfKey, coeff] : entry.designEntryMap) + { + if (coeff == 0) + { + continue; + } + + int index = kfState.getKFIndex(kfKey); + if (index < 0) + { + std::cout << "Code error: Trying to create measurement for undefined key, check " + "stateTransition() is " + "called first: " + << kfKey << "\n"; + error = true; + } + H(meas, index) = coeff; + + if (kfState.assume_linearity) + { + double xVal = kfState.x[index]; + double uVal = entry.usedValueMap[kfKey]; + + double deltaX = xVal - uVal; + if (deltaX) + { + BOOST_LOG_TRIVIAL(debug) + << std::fixed << "Adjusting meas '" << entry.obsKey << "' as '" << kfKey + << "' changed " << deltaX << "\tfrom " << uVal << "\tto " << xVal + << "\t : " << innov << "\t-> " << innov - deltaX * coeff; + + value -= deltaX * coeff; + innov -= deltaX * coeff; + } + } + } + + for (auto& [kfKey, coeff] : entry.noiseEntryMap) + { + int index = getNoiseIndex(kfKey); + if (index < 0) + { + std::cout << "Code error: Trying to create measurement for undefined key :" << kfKey + << "\n"; + error = true; + } + H_star(meas, index) = coeff; + } + + obsKeys[meas] = std::move(entry.obsKey); + metaDataMaps[meas] = std::move(entry.metaDataMap); + componentsMaps[meas] = std::move(entry.componentsMap); + } + Eigen::setNbThreads(0); + + if (error) + { + return; + } + + if (noiseMatrix_ptr) + { + R = *noiseMatrix_ptr; + } + + if (uncorrelatedNoise.rows() != 0) + { + SparseMatrix R_A = SparseMatrix(numMeas, uncorrelatedNoise.rows()); + + int meas = 0; + for (auto& entry : kfEntryList) + { + for (auto& [kfKey, value] : entry.noiseEntryMap) + { + int noiseIndex = noiseIndexMap[kfKey]; + + R_A.insert(meas, noiseIndex) = value; + } + + meas++; + } + + R = R_A * uncorrelatedNoise.asDiagonal() * R_A.transpose(); + } } -bool KFState::doStateRejectCallbacks( - Trace& trace, ///< Trace file for output - KFMeas& kfMeas, ///< Measurements that were passed to the filter - KFKey& badKey, ///< Key in state that was unsatisfactory - bool postFit) ///< Rejection occured during post-filtering checks +bool KFState::doStateRejectCallbacks(RejectCallbackDetails rejectDetails) { - for (auto& callback : stateRejectCallbacks) - { - bool keepGoing = callback(trace, *this, kfMeas, badKey, postFit); + auto& trace = rejectDetails.trace; - if (keepGoing == false) - { - return false; - } - } + for (auto& callback : stateRejectCallbacks) + { + bool keepGoing = callback(rejectDetails); - return true; + if (keepGoing == false) + { + trace << "\n"; + + return false; + } + } + + trace << "\n"; + + return true; } -bool KFState::doMeasRejectCallbacks( - Trace& trace, ///< Trace file for output - KFMeas& kfMeas, ///< Measurements that were passed to the filter - int badIndex, ///< Index in measurement list that was unsatisfactory - bool postFit) ///< Rejection occured during post-filtering checks +bool KFState::doMeasRejectCallbacks(RejectCallbackDetails rejectDetails) { - for (auto& callback : measRejectCallbacks) - { - bool keepGoing = callback(trace, *this, kfMeas, badIndex, postFit); + auto& trace = rejectDetails.trace; - if (keepGoing == false) - { - return false; - } - } + for (auto& callback : measRejectCallbacks) + { + bool keepGoing = callback(rejectDetails); - return true; -} + if (keepGoing == false) + { + trace << "\n"; + + return false; + } + } + trace << "\n"; + + return true; +} /** Kalman filter operation -*/ + */ void KFState::filterKalman( - Trace& trace, ///< Trace file for output - KFMeas& kfMeas, ///< Measurement object - const string& suffix, ///< Suffix to append to residuals block - bool innovReady, ///< Innovation already constructed - map* filterChunkMap_ptr) ///< Optional map of chunks for parallel processing of sub filters + Trace& trace, ///< Trace file for output + KFMeas& kfMeas, ///< Measurement object + const string& suffix, ///< Suffix to append to residuals block + bool innovReady, ///< Innovation already constructed + map* + filterChunkMap_ptr ///< Optional map of chunks for parallel processing of sub filters +) { - DOCS_REFERENCE(Kalman_Filter__); - - if (kfMeas.time != GTime::noTime()) - { - time = kfMeas.time; - } - - map dummyFilterChunkMap; - if (filterChunkMap_ptr == nullptr) - { - filterChunkMap_ptr = &dummyFilterChunkMap; - } - - filterChunkMap = *filterChunkMap_ptr; - - if (filterChunkMap.empty()) - { - FilterChunk filterChunk; - - filterChunk.trace_ptr = &trace; - filterChunk.numX = x.rows(); - - filterChunkMap[""] = filterChunk; - } - - auto returnEarlyPrep = [&]() - { - if (rts_basename.empty() == false) - { - spitFilterToFile(*this, E_SerialObject::FILTER_MINUS, rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - spitFilterToFile(*this, E_SerialObject::FILTER_PLUS, rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - spitFilterToFile(kfMeas, E_SerialObject::MEASUREMENT, rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - } - }; - - if (kfMeas.H.rows() == 0) - { - //nothing to be done, clean up and return early - returnEarlyPrep(); - return; - } - - /* kalman filter measurement update */ - if (innovReady == false) - { - kfMeas.V = kfMeas.Y - kfMeas.H * x; - kfMeas.VV = kfMeas.V; - } - - TestStatistics testStatistics; - - for (auto& [id, filterChunk] : filterChunkMap) - { - if (filterChunk.numH == 0) - { - continue; - } - - if (filterChunk.numX < 0) filterChunk.numX = x.rows(); - if (filterChunk.numH < 0) filterChunk.numH = kfMeas.H.rows(); - - KFStatistics statistics; - for (int i = 0; i < prefitOpts.max_iterations; i++) - { - auto& chunkTrace = *filterChunk.trace_ptr; - - if ( prefitOpts.sigma_check == false - && prefitOpts.omega_test == false) - { - continue; - } - - KFKey badState; - int badMeasIndex = -1; - - preFitSigmaCheck(chunkTrace, kfMeas, badState, badMeasIndex, statistics, filterChunk.begX, filterChunk.numX, filterChunk.begH, filterChunk.numH); - - if (badState.type) { chunkTrace << "\n" << "Prefit check failed state test"; bool keepGoing = doStateRejectCallbacks (chunkTrace, kfMeas, badState, false); /*continue;*/ } //always fallthrough - if (badMeasIndex >= 0) { chunkTrace << "\n" << "Prefit check failed measurement test"; bool keepGoing = doMeasRejectCallbacks (chunkTrace, kfMeas, badMeasIndex, false); continue; } //retry next iteration - else { chunkTrace << "\n" << "Prefit check passed"; break; } - } - - testStatistics.sumOfSquaresPre += statistics.sumOfSquares; - testStatistics.averageRatioPre += statistics.averageRatio / filterChunkMap.size(); - } - - if ( prefitOpts.sigma_check - || prefitOpts.omega_test) - { - trace << "\n" << "Sum-of-squared test statistics (prefit): " << testStatistics.sumOfSquaresPre << "\n"; - } - - VectorXd xp = x; - MatrixXd Pp = P; - dx = VectorXd::Zero(x.rows()); - - - statisticsMap["States"] = x.rows(); - - for (auto& [id, fc] : filterChunkMap) - { - if (fc.numH == 0) - { - continue; - } - - if (fc.id.empty() == false) - { - BOOST_LOG_TRIVIAL(info) << " ------- FILTERING CHUNK " << fc.id << " --------\n"; - } - - statisticsMap["Observations"] += fc.numH; - - KFStatistics statistics; - for (int i = 0; i < postfitOpts.max_iterations; i++) - { - auto& chunkTrace = *fc.trace_ptr; - - bool pass = kFilter(chunkTrace, kfMeas, xp, Pp, dx, fc.begX, fc.numX, fc.begH, fc.numH); - - if (pass == false) - { - chunkTrace << "FILTER FAILED" << "\n"; - returnEarlyPrep(); - return; - } - - if (advanced_postfits == false) - { - kfMeas.VV.segment(fc.begH, fc.numH) = kfMeas.V.segment(fc.begH,fc.numH) - - kfMeas.H.block(fc.begH, fc.begX, fc.numH, fc.numX) * dx.segment(fc.begX, fc.numX); - } - - if (output_residuals) - { - InteractiveTerminal ss("Residuals" + suffix, trace); - - outputResiduals(ss, kfMeas, i, suffix, fc.begH, fc.numH); - } - - if (postfitOpts.sigma_check == false) - { - break; - } - - KFKey badState; - int badMeasIndex = -1; - - postFitSigmaChecks(chunkTrace, kfMeas, dx, i, badState, badMeasIndex, statistics, fc.begX, fc.numX, fc.begH, fc.numH); - bool stopIterating = true; - if (badState.type) { chunkTrace << "\n" << "Postfit check failed state test"; bool keepGoing = doStateRejectCallbacks (chunkTrace, kfMeas, badState, true); stopIterating = false; } - if (badMeasIndex >= 0) { chunkTrace << "\n" << "Postfit check failed measurement test"; bool keepGoing = doMeasRejectCallbacks (chunkTrace, kfMeas, badMeasIndex, true); stopIterating = false; } - - if (stopIterating) { chunkTrace << "\n" << "Postfit check passed"; } - - if ( stopIterating - ||i == postfitOpts.max_iterations - 1) - { - statisticsMap["Filter iterations " + std::to_string(i+1)]++; - - break; - } - } - - if (outputMongoMeasurements) - { - mongoMeasResiduals(kfMeas.time, kfMeas, acsConfig.mongoOpts.queue_outputs, suffix, fc.begH, fc.numH); - } - - testStatistics.sumOfSquaresPost += statistics.sumOfSquares; - testStatistics.averageRatioPost += statistics.averageRatio / filterChunkMap.size(); - } - - if (postfitOpts.sigma_check) - trace << "\n" << "Sum-of-squared test statistics (postfit): " << testStatistics.sumOfSquaresPost << "\n"; - - if (chiSquareTest.enable) - { - for (auto& [id, fc] : filterChunkMap) - { - if (fc.numH == 0) - { - continue; - } - - 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 - { - case E_ChiSqMode::INNOVATION: { testStatistics.chiSq += innovChiSquare (chunkTrace, kfMeas, fc.begX, fc.numX, fc.begH, fc.numH); break; } - case E_ChiSqMode::MEASUREMENT: { testStatistics.chiSq += measChiSquare (chunkTrace, kfMeas, dx, fc.begX, fc.numX, fc.begH, fc.numH); break; } - case E_ChiSqMode::STATE: { testStatistics.chiSq += stateChiSquare (chunkTrace, Pp, dx, fc.begX, fc.numX, fc.begH, fc.numH); break; } - default: { BOOST_LOG_TRIVIAL(error) << "Error: Unknown Chi-square test mode"; break; } - } - } - - if (chiSquareTest.mode == +E_ChiSqMode::STATE) testStatistics.dof = x.rows() - 1; - else testStatistics.dof = kfMeas. H.rows(); // todo Eugene: revisit DOF in the future for MEASUREMENT mode - - testStatistics.chiSqPerDof = testStatistics.chiSq / testStatistics.dof; - - // check against threshold - boost::math::normal normDist; - double alpha = cdf(complement(normDist, chiSquareTest.sigma_threshold)) * 2; //two-tailed - - boost::math::chi_squared chiSqDist(testStatistics.dof); - testStatistics.qc = quantile(complement(chiSqDist, alpha)); - if (testStatistics.chiSq <= testStatistics.qc) trace << "\n" << "Chi-square test passed"; - else trace << "\n" << "Chi-square test failed"; - - trace << "\n" - << "Chi-square increment: " << testStatistics.chiSq - << "\tThreshold: " << testStatistics.qc - << "\tNumber of measurements:" << kfMeas.H.rows() - << "\tNumber of states:" << x.rows() - 1 - << "\tDegree of freedom: " << testStatistics.dof - << "\tChi-square per DOF: " << testStatistics.chiSqPerDof << "\n"; - } - - if (acsConfig.mongoOpts.output_test_stats) - { - mongoTestStat(*this, testStatistics); - } - - if (rts_basename.empty() == false) - { - spitFilterToFile(*this, E_SerialObject::FILTER_MINUS, rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - } - - if (simulate_filter_only) - { - dx = VectorXd::Zero(x.rows()); - } - else - { - x = std::move(xp); - P = std::move(Pp); - } - - if (rts_basename.empty() == false) - { - spitFilterToFile(*this, E_SerialObject::FILTER_PLUS, rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - spitFilterToFile(kfMeas, E_SerialObject::MEASUREMENT, rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - } - - initFilterEpoch(); + DOCS_REFERENCE(Kalman_Filter__); + + if (kfMeas.time != GTime::noTime()) + { + time = kfMeas.time; + } + + map dummyFilterChunkMap; + if (filterChunkMap_ptr == nullptr) + { + filterChunkMap_ptr = &dummyFilterChunkMap; + } + + filterChunkMap = *filterChunkMap_ptr; + + if (filterChunkMap.empty()) + { + FilterChunk filterChunk; + + filterChunk.numX = x.rows(); + + filterChunkMap[""] = filterChunk; + } + + auto returnEarlyPrep = [&]() + { + if (rts_basename.empty() == false) + { + spitFilterToFile( + *this, + E_SerialObject::FILTER_MINUS, + rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + spitFilterToFile( + *this, + E_SerialObject::FILTER_PLUS, + rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + spitFilterToFile( + kfMeas, + E_SerialObject::MEASUREMENT, + rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + } + }; + + if (kfMeas.H.rows() == 0) + { + // nothing to be done, clean up and return early + returnEarlyPrep(); + return; + } + + /* kalman filter measurement update */ + if (innovReady == false) + { + kfMeas.V = kfMeas.Y - kfMeas.H * x; + kfMeas.VV = kfMeas.V; + } + + TestStatistics testStatistics; + + prefitRatios = VectorXd::Zero(x.rows()); + + for (auto& [id, fc] : filterChunkMap) + { + if (fc.numH == 0) + { + continue; + } + + if (fc.numX < 0) + fc.numX = x.rows(); + if (fc.numH < 0) + fc.numH = kfMeas.H.rows(); + + KFStatistics statistics; + for (int i = 0; i < prefitOpts.max_iterations; i++) + { + if (prefitOpts.sigma_check == false && prefitOpts.omega_test == false) + { + continue; + } + + std::stringstream stringBuffer; + + RejectCallbackDetails rejectCallbackDetails(stringBuffer, *this, kfMeas); + rejectCallbackDetails.stage = E_FilterStage::PREFIT; + + preFitSigmaChecks( + rejectCallbackDetails, + statistics, + fc.begX, + fc.numX, + fc.begH, + fc.numH + ); + + bool stopIterating = true; + if (rejectCallbackDetails.kfKey.type != int_to_enum(0)) + { + stringBuffer << "\n" + << "Prefit check failed state test" << "\n"; + doStateRejectCallbacks(rejectCallbackDetails); + stopIterating = false; + } + else if (rejectCallbackDetails.measIndex >= 0) + { + stringBuffer << "\n" + << "Prefit check failed measurement test" << "\n"; + doMeasRejectCallbacks(rejectCallbackDetails); + stopIterating = false; + } + + if (stopIterating) + { + stringBuffer << "\n" + << "Prefit check passed" << "\n"; + } + else + { + if (i == prefitOpts.max_iterations - 1) + { + BOOST_LOG_TRIVIAL(warning) + << "Max pre-fit filter iterations limit reached at " << time << " in " + << suffix << ", limit is " << prefitOpts.max_iterations; + stringBuffer << "\n" + << "Warning: Max pre-fit filter iterations limit reached at " + << time << " in " << suffix << ", limit is " + << prefitOpts.max_iterations << "\n"; + + stopIterating = true; + } + } + + trace << stringBuffer.str(); + if (fc.trace_ptr) + *fc.trace_ptr << stringBuffer.str(); + + if (stopIterating) + { + break; + } + } + + testStatistics.sumOfSquaresPre += statistics.sumOfSquares; + testStatistics.averageRatioPre += statistics.averageRatio / filterChunkMap.size(); + } + + if (prefitOpts.sigma_check || prefitOpts.omega_test) + { + trace << "\n" + << "Sum-of-squared test statistics (prefit): " << testStatistics.sumOfSquaresPre + << "\n"; + } + + VectorXd xp = x; + MatrixXd Pp = P; + dx = VectorXd::Zero(x.rows()); + + postfitRatios = VectorXd::Zero(x.rows()); + + statisticsMap["States"] = x.rows(); + BOOST_LOG_TRIVIAL(info) << " ------- FILTERING BY CHUNK " << filterChunkMap.size() + << " --------\n"; + for (auto& [id, fc] : filterChunkMap) + { + if (fc.numH == 0) + { + continue; + } + + if (fc.id.empty() == false) + { + BOOST_LOG_TRIVIAL(info) + << " ------- FILTERING CHUNK " << fc.id << " --------\n"; + } + + MatrixXd Qinv = MatrixXd::Identity(fc.numH, fc.numH); + MatrixXd QinvH = MatrixXd::Ones(fc.numH, fc.numX); + + statisticsMap["Observations"] += fc.numH; + + 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); + + if (pass == false) + { + trace << "FILTER FAILED" << "\n"; + returnEarlyPrep(); + return; + } + + if (advanced_postfits == false) + { + kfMeas.VV.segment(fc.begH, fc.numH) = + kfMeas.V.segment(fc.begH, fc.numH) - + kfMeas.H.block(fc.begH, fc.begX, fc.numH, fc.numX) * + dx.segment(fc.begX, fc.numX); + } + + bool stopIterating = true; + + if (postfitOpts.sigma_check || postfitOpts.omega_test) + { + std::stringstream stringBuffer; + + RejectCallbackDetails rejectCallbackDetails(stringBuffer, *this, kfMeas); + rejectCallbackDetails.stage = E_FilterStage::POSTFIT; + + postFitSigmaChecks( + rejectCallbackDetails, + dx, + Qinv, + QinvH, + statistics, + fc.begX, + fc.numX, + fc.begH, + fc.numH + ); + + if (rejectCallbackDetails.kfKey.type != int_to_enum(0)) + { + stringBuffer << "\n" + << "Postfit check failed state test" << "\n"; + doStateRejectCallbacks(rejectCallbackDetails); + stopIterating = false; + } + else if (rejectCallbackDetails.measIndex >= 0) + { + stringBuffer << "\n" + << "Postfit check failed measurement test" << "\n"; + doMeasRejectCallbacks(rejectCallbackDetails); + stopIterating = false; + } + + if (stopIterating) + { + stringBuffer << "\n" + << "Postfit check passed" << "\n"; + } + else + { + if (i == postfitOpts.max_iterations - 1) + { + BOOST_LOG_TRIVIAL(warning) + << "Max post-fit filter iterations limit reached at " << time << " in " + << suffix << ", limit is " << postfitOpts.max_iterations; + stringBuffer << "\n" + << "Warning: Max post-fit filter iterations limit reached at " + << time << " in " << suffix << ", limit is " + << postfitOpts.max_iterations << "\n"; + + stopIterating = true; + } + } + + trace << stringBuffer.str(); + if (fc.trace_ptr) + *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)]++; + + break; + } + } + + if (outputMongoMeasurements) + { + mongoMeasResiduals( + kfMeas.time, + kfMeas, + acsConfig.mongoOpts.queue_outputs, + suffix, + fc.begH, + fc.numH + ); + } + + testStatistics.sumOfSquaresPost += statistics.sumOfSquares; + testStatistics.averageRatioPost += statistics.averageRatio / filterChunkMap.size(); + } + + if (postfitOpts.sigma_check || postfitOpts.omega_test) + trace << "\n" + << "Sum-of-squared test statistics (postfit): " << testStatistics.sumOfSquaresPost + << "\n"; + + if (chiSquareTest.enable) + { + for (auto& [id, fc] : filterChunkMap) + { + if (fc.numH == 0) + { + continue; + } + + 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 + { + case E_ChiSqMode::INNOVATION: + { + testStatistics.chiSq += + innovChiSquare(chunkTrace, kfMeas, fc.begX, fc.numX, fc.begH, fc.numH); + break; + } + case E_ChiSqMode::MEASUREMENT: + { + testStatistics.chiSq += + measChiSquare(chunkTrace, kfMeas, dx, fc.begX, fc.numX, fc.begH, fc.numH); + break; + } + case E_ChiSqMode::STATE: + { + testStatistics.chiSq += + stateChiSquare(chunkTrace, Pp, dx, fc.begX, fc.numX, fc.begH, fc.numH); + break; + } + default: + { + BOOST_LOG_TRIVIAL(error) << "Unknown Chi-square test mode"; + break; + } + } + } + + if (chiSquareTest.mode == E_ChiSqMode::STATE) + testStatistics.dof = x.rows() - 1; + else + testStatistics.dof = + kfMeas.H.rows(); // todo Eugene: revisit DOF in the future for MEASUREMENT mode + + testStatistics.chiSqPerDof = testStatistics.chiSq / testStatistics.dof; + + // check against threshold + boost::math::normal normDist; + double alpha = cdf(complement(normDist, chiSquareTest.sigma_threshold)) * 2; // two-tailed + + boost::math::chi_squared chiSqDist(testStatistics.dof); + testStatistics.qc = quantile(complement(chiSqDist, alpha)); + if (testStatistics.dof > 0 && testStatistics.chiSq <= testStatistics.qc) + trace << "\n" + << "Chi-square test passed"; + else + trace << "\n" + << "Chi-square test failed"; + + trace << "\n" + << "Chi-square increment: " << testStatistics.chiSq + << "\tThreshold: " << testStatistics.qc + << "\tNumber of measurements:" << kfMeas.H.rows() + << "\tNumber of states:" << x.rows() - 1 + << "\tDegree of freedom: " << testStatistics.dof + << "\tChi-square per DOF: " << testStatistics.chiSqPerDof << "\n"; + } + + if (acsConfig.mongoOpts.output_test_stats != E_Mongo::NONE) + { + mongoTestStat(*this, testStatistics); + } + + if (rts_basename.empty() == false) + { + spitFilterToFile( + *this, + E_SerialObject::FILTER_MINUS, + rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + } + + if (simulate_filter_only) + { + dx = VectorXd::Zero(x.rows()); + } + else + { + x = std::move(xp); + P = std::move(Pp); + } + + if (rts_basename.empty() == false) + { + spitFilterToFile( + *this, + E_SerialObject::FILTER_PLUS, + rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + spitFilterToFile( + kfMeas, + E_SerialObject::MEASUREMENT, + rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + } + + initFilterEpoch(trace); } /** Least squares estimator for new kalman filter states. -* If new states have been added that do not contain variance values, the filter will assume that these states values and covariances should be -* estimated using least squares. -* -* This function will extract the minimum required states from the existing state vector, -* and the minimum required measurements in order to perform least squares for the uninitialised states. -*/ -void KFState::leastSquareInitStates( - Trace& trace, ///< Trace file for output - KFMeas& kfMeas, ///< Measurement object - bool initCovars, ///< Option to also initialise off-diagonal covariance values - VectorXd* dx, ///< Optional output of state deltas - bool innovReady) ///< Perform conversion between V & Y + * If new states have been added that do not contain variance values, the filter will assume that + * these states values and covariances should be estimated using least squares. + * + * This function will extract the minimum required states from the existing state vector, + * and the minimum required measurements in order to perform least squares for the uninitialised + * states. + */ +bool KFState::leastSquareInitStates( + Trace& trace, ///< Trace file for output + KFMeas& kfMeas, ///< Measurement object + const string& suffix, ///< Suffix to append to residuals block + bool initCovars, ///< Option to also initialise off-diagonal covariance values + bool innovReady, ///< Apriori states available and residuals already calculated + bool skipLsqCheck ///< Skip outlier screening in case not converged or within SPP RAIM +) { - lsqRequired = false; - - chiQCPass = false; - - if (innovReady) - { - kfMeas.Y = kfMeas.V; - } - - vector newStateIndicies; - - //find all the states that aren't initialised, they need least squaring. - for (auto& [key, i] : kfIndexMap) - { - if ( (key.type != KF::ONE) - &&(P(i,i) < 0)) - { - //this is a new state and needs to be evaluated using least squares - newStateIndicies.push_back(i); - } - } - - //get the subset of the measurement matrix that applies to the uninitialised states - auto subsetA = kfMeas.H(all, newStateIndicies); - - //find the subset of measurements that are required for the initialisation - auto usedMeas = subsetA.rowwise().any(); - - map pseudoMeasStates; - vector leastSquareMeasIndicies; - - for (int meas = 0; meas < usedMeas.rows(); meas++) - { - //if not used, dont worry about it - if (usedMeas(meas) == 0) - { - continue; - } - - //this measurement is used to calculate a new state. - //copy it to a new design matrix - leastSquareMeasIndicies.push_back(meas); - - //remember make a pseudo measurement of anything it references that is already set - for (int state = 0; state < kfMeas.H.cols(); state++) - { - if ( (kfMeas.H(meas, state) != 0) - &&(P(state,state) >= 0)) - { - pseudoMeasStates[state] = true; - } - } - } - - int newMeasCount = leastSquareMeasIndicies.size() + pseudoMeasStates.size(); - - //Create new measurement objects with larger size, (using all states for now) - KFMeas leastSquareMeas; - - leastSquareMeas.Y = VectorXd::Zero(newMeasCount); - leastSquareMeas.R = MatrixXd::Zero(newMeasCount, newMeasCount); - leastSquareMeas.H = MatrixXd::Zero(newMeasCount, kfMeas.H.cols()); - //VV - //V - - int measCount = leastSquareMeasIndicies.size(); - - //copy in the required measurements from the old set - leastSquareMeas.Y.head (measCount) = kfMeas.Y(leastSquareMeasIndicies); - leastSquareMeas.R.topLeftCorner (measCount, measCount) = kfMeas.R(leastSquareMeasIndicies, leastSquareMeasIndicies); - leastSquareMeas.H.topRows (measCount) = kfMeas.H(leastSquareMeasIndicies, all); - - //append any new pseudo measurements to the end - for (auto& [state, boool] : pseudoMeasStates) - { - leastSquareMeas.Y(measCount) = x(state); - leastSquareMeas.R(measCount, measCount) = P(state, state); - leastSquareMeas.H(measCount, state) = 1; - measCount++; - } - - //find the subset of states required for these measurements - vector usedCols; - auto usedStates = leastSquareMeas.H.colwise().any(); - for (int i = 0; i < usedStates.cols(); i++) - { - if (usedStates(i) != 0) - { - usedCols.push_back(i); - } - } - - //create a new meaurement object using only the required states. - KFMeas leastSquareMeasSubs; - leastSquareMeasSubs.Y = leastSquareMeas.Y; - leastSquareMeasSubs.R = leastSquareMeas.R; - leastSquareMeasSubs.H = leastSquareMeas.H(all, usedCols); - - //invert measurement noise matrix to get a weight matrix - leastSquareMeasSubs.W = (1 / leastSquareMeasSubs.R.diagonal().array()).matrix(); - - VectorXd w = (1 / leastSquareMeasSubs.R.diagonal().array()).matrix().col(0); - - for (int i = 0; i < w.rows(); i++) - { - if (std::isinf(w(i))) - { - w(i) = 0; - } - } - - if ( leastSquareMeasSubs.H.cols() == 0 - ||leastSquareMeasSubs.H.rows() == 0) - { - trace << "\n" << "EMPTY DESIGN MATRIX DURING LEAST SQUARES"; - return; - } - - if (leastSquareMeasSubs.R.rows() < leastSquareMeasSubs.H.cols()) - { - trace << "\n" << "Insufficient measurements for least squares " << leastSquareMeasSubs.R.rows() << " " << x.rows(); - return; - } - auto& H = leastSquareMeasSubs.H; - auto& Y = leastSquareMeasSubs.Y; - -// std::cout << "\nkfmeasY\n" << kfMeas.Y << "\n"; -// std::cout << "\nkfmeasV\n" << kfMeas.V << "\n"; -// std::cout << "\nY\n" << Y << "\n"; - - //calculate least squares solution - MatrixXd W = w.asDiagonal(); - MatrixXd H_W = H.transpose() * W; - MatrixXd Q = H_W * H; - - MatrixXd Qinv = Q.inverse(); - VectorXd x1 = Qinv * H_W * Y; - -// std::cout << "Q : " << "\n" << Q; - bool error = x1.array().isNaN().any(); - if (error) - { - std::cout << "\n" << "P :" << "\n" << P << "\n"; - std::cout << "\n" << "x1:" << "\n" << x1 << "\n"; - std::cout << "\n" << "w :" << "\n" << w << "\n"; - std::cout << "\n" << "H :" << "\n" << H << "\n"; - std::cout << "\n"; - std::cout << "NAN found. Exiting...."; - std::cout << "\n"; - - exit(-1); - } - - if (chiSquareTest.enable) - chiQC(trace, leastSquareMeasSubs, x1); - - if (dx) - { - (*dx) = x1; - } - - for (int i = 0; i < usedCols.size(); i++) - { - int stateRowIndex = usedCols[i]; - - if (P(stateRowIndex, stateRowIndex) >= 0) - { - continue; - } - - double newStateVal = x1(i); - double newStateCov = Qinv(i,i); - - if (dx) - { - x(stateRowIndex) += newStateVal; - P(stateRowIndex,stateRowIndex) = newStateCov; - kfMeas.VV = kfMeas.Y - H * *dx; - } - else - { - x(stateRowIndex) = newStateVal; - P(stateRowIndex,stateRowIndex) = newStateCov; - } - - if (initCovars) - { - for (int j = 0; j < i; j++) - { - int stateColIndex = usedCols[j]; - - newStateCov = Qinv(i,j); - - P(stateRowIndex,stateColIndex) = newStateCov; - P(stateColIndex,stateRowIndex) = newStateCov; - } - } - } + lsqRequired = false; + + sigmaPass = false; // Eugene: can also do this for filterKalman + chiQCPass = false; // Eugene: can also do this for filterKalman + + if (innovReady == false) + { + kfMeas.V = kfMeas.Y; // x == 0 in this case + } + + // find all the states that aren't initialised, they need least squaring. + vector newStateIndicies; + for (auto& [key, index] : kfIndexMap) + { + if ((key.type != KF::ONE) && (P(index, index) < 0)) + { + // this is a new state and needs to be estimated using least squares + newStateIndicies.push_back(index); + } + } + + // find the subset of measurements that are required for the initialisation + auto usedMeasMask = kfMeas.H(all, newStateIndicies).rowwise().any(); + + vector leastSquareMeasIndicies; + vector pseudoMeasStateIndicies; + + for (int measIndex = 0; measIndex < usedMeasMask.rows(); measIndex++) + { + // if not used, dont worry about it + if (usedMeasMask(measIndex) == 0) + { + continue; + } + + // this measurement is used to calculate a new state. + // copy it to a new design matrix + leastSquareMeasIndicies.push_back(measIndex); + + // remember make a pseudo measurement of anything it references that is already set + for (int stateIndex = 0; stateIndex < kfMeas.H.cols(); stateIndex++) + { + if ((kfMeas.H(measIndex, stateIndex) != 0) && (P(stateIndex, stateIndex) >= 0)) + { + pseudoMeasStateIndicies.push_back(stateIndex); + } + } + } + + int lsqMeasCount = leastSquareMeasIndicies.size(); + int pseudoMeasCount = pseudoMeasStateIndicies.size(); + int totalMeasCount = lsqMeasCount + pseudoMeasCount; + + // Create new measurement objects with larger size, (using all states for now) + KFMeas leastSquareMeas; + + leastSquareMeas.V = VectorXd::Zero(totalMeasCount); + leastSquareMeas.R = MatrixXd::Zero(totalMeasCount, totalMeasCount); + leastSquareMeas.H = MatrixXd::Zero(totalMeasCount, kfMeas.H.cols()); + + // copy in the required measurements from the old set + leastSquareMeas.V.head(lsqMeasCount) = kfMeas.V(leastSquareMeasIndicies); + leastSquareMeas.R.topLeftCorner(lsqMeasCount, lsqMeasCount) = + kfMeas.R(leastSquareMeasIndicies, leastSquareMeasIndicies); + leastSquareMeas.H.topRows(lsqMeasCount) = kfMeas.H(leastSquareMeasIndicies, all); + + // append any new pseudo measurements to the end + for (int i = 0; i < pseudoMeasCount; i++) + { + int measIndex = lsqMeasCount + i; + int stateIndex = pseudoMeasStateIndicies[i]; + + if (innovReady) + { + leastSquareMeas.V(measIndex) = 0; // take x as apriori state + } + else + { + leastSquareMeas.V(measIndex) = x(stateIndex); // take 0 as apriori state + } + leastSquareMeas.R(measIndex, measIndex) = + P(stateIndex, stateIndex); // todo Eugene: check equivalence w/ back-subsitution - + // pseudo var should be 0 instead of P, or doesn't matter? + leastSquareMeas.H(measIndex, stateIndex) = 1; + } + + // find the subset of states required for these measurements + vector usedStateIndicies; + auto usedStateMask = leastSquareMeas.H.colwise().any(); + for (int i = 0; i < usedStateMask.cols(); i++) + { + if (usedStateMask(i) != 0) + { + usedStateIndicies.push_back(i); + } + } + + // create a new meaurement object using only the required states. + KFMeas leastSquareMeasSubs; + leastSquareMeasSubs.time = kfMeas.time; + leastSquareMeasSubs.V = leastSquareMeas.V; + leastSquareMeasSubs.R = leastSquareMeas.R; + leastSquareMeasSubs.H = leastSquareMeas.H(all, usedStateIndicies); + + for (int i = 0; i < lsqMeasCount; i++) + { + int measIndex = leastSquareMeasIndicies[i]; + leastSquareMeasSubs.obsKeys.push_back(kfMeas.obsKeys[measIndex]); + leastSquareMeasSubs.metaDataMaps.push_back(kfMeas.metaDataMaps[measIndex]); + leastSquareMeasSubs.componentsMaps.push_back(kfMeas.componentsMaps[measIndex]); + } + + int usedStateCount = usedStateIndicies.size(); + VectorXd xp = VectorXd::Zero(usedStateCount); + MatrixXd Pp = MatrixXd::Identity(usedStateCount, usedStateCount); + + TestStatistics testStatistics; + KFStatistics statistics; + for (int i = 0; i < lsqOpts.max_iterations; i++) + { + bool pass = leastSquare(trace, leastSquareMeasSubs, xp, Pp); + + if (pass == false) + { + trace << "LSQ FAILED" << "\n"; + return false; + } + + leastSquareMeasSubs.VV = leastSquareMeasSubs.V - leastSquareMeasSubs.H * xp; + + if (chiSquareTest.enable) + { + chiQC(trace, leastSquareMeasSubs); + + if (chiQCPass) + trace << "\nChi-square test passed: "; + else + trace << "\nChi-square test failed: "; + + trace << "dof=" << dof << "\tchi^2=" << chi2 << "\tthres=" << qc + << "\tsigma0=" << sqrt(chi2PerDof); + } + + bool stopIterating = true; + + if ((lsqOpts.sigma_check || lsqOpts.omega_test) && skipLsqCheck == false) + { + std::stringstream stringBuffer; + + RejectCallbackDetails rejectCallbackDetails(stringBuffer, *this, leastSquareMeasSubs); + rejectCallbackDetails.stage = E_FilterStage::LSQ; + + leastSquareSigmaChecks(rejectCallbackDetails, Pp, statistics); + + if (rejectCallbackDetails.measIndex >= 0) + { + stringBuffer << "\n" + << "Least squares check failed"; + doMeasRejectCallbacks(rejectCallbackDetails); + stopIterating = false; + } + + if (stopIterating) + { + stringBuffer << "\n" + << "Least squares check passed"; + sigmaPass = true; + } + else + { + if (i == lsqOpts.max_iterations - 1) + { + BOOST_LOG_TRIVIAL(debug) + << "Max least squares iterations limit reached at " << time << " in " + << suffix << ", limit is " << lsqOpts.max_iterations; + stringBuffer << "\n" + << "Warning: Max least squares iterations limit reached at " + << time << " in " << suffix << ", limit is " + << lsqOpts.max_iterations; + + stopIterating = true; + sigmaPass = false; + } + } + + 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)]++; + + break; + } + } + + testStatistics.sumOfSquaresLsq = statistics.sumOfSquares; + testStatistics.averageRatioLsq = statistics.averageRatio; + + if ((lsqOpts.sigma_check || lsqOpts.omega_test) && skipLsqCheck == false) + trace << "\n" + << "Sum-of-squared test statistics (least squares): " + << testStatistics.sumOfSquaresLsq << "\n"; + + for (int i = 0; i < usedStateCount; i++) + { + int stateRowIndex = usedStateIndicies[i]; + + if (P(stateRowIndex, stateRowIndex) >= 0) + { + continue; + } + + double newStateVal = xp(i); + double newStateCov = Pp(i, i); + + dx(stateRowIndex) = newStateVal; + + if (innovReady) + { + x(stateRowIndex) += newStateVal; + } + else + { + x(stateRowIndex) = newStateVal; + } + + P(stateRowIndex, stateRowIndex) = newStateCov; + + if (initCovars) + { + for (int j = 0; j < i; j++) + { + int stateColIndex = usedStateIndicies[j]; + + newStateCov = Pp(i, j); + + P(stateRowIndex, stateColIndex) = newStateCov; + P(stateColIndex, stateRowIndex) = newStateCov; + } + } + } + + kfMeas.VV = kfMeas.V - kfMeas.H * dx; + kfMeas.R(leastSquareMeasIndicies, leastSquareMeasIndicies) = + leastSquareMeasSubs.R.topLeftCorner(lsqMeasCount, lsqMeasCount); + if (leastSquareMeasSubs.postfitRatios.rows() >= lsqMeasCount) + { + kfMeas.postfitRatios(leastSquareMeasIndicies) = + leastSquareMeasSubs.postfitRatios.head(lsqMeasCount); + } + + return true; } /** Get a portion of the state vector by passing a list of keys -*/ + */ VectorXd KFState::getSubState( - map& kfKeyMap, ///< List of keys to return within substate - MatrixXd* covarMat_ptr, ///< Optional pointer to a matrix for output of covariance submatrix - VectorXd* adjustVec_ptr) ///< Optional pointer to a vector for output of last adjustments -const + map& kfKeyMap, ///< List of keys to return within substate + MatrixXd* covarMat_ptr, ///< Optional pointer to a matrix for output of covariance submatrix + VectorXd* adjustVec_ptr ///< Optional pointer to a vector for output of last adjustments +) const { - vector indices; - indices.resize(kfKeyMap.size()); - - for (auto& [kfKey, mapIndex] : kfKeyMap) - { - int stateIndex = getKFIndex(kfKey); - if (stateIndex >= 0) - { - indices[mapIndex] = stateIndex; - } - } - - VectorXd subState = x (indices); - if (covarMat_ptr) { *covarMat_ptr = P (indices, indices); } - if (adjustVec_ptr) { *adjustVec_ptr = dx(indices); } - - return subState; + vector indices; + indices.resize(kfKeyMap.size()); + + for (auto& [kfKey, mapIndex] : kfKeyMap) + { + int stateIndex = getKFIndex(kfKey); + if (stateIndex >= 0) + { + indices[mapIndex] = stateIndex; + } + } + + VectorXd subState = x(indices); + if (covarMat_ptr) + { + *covarMat_ptr = P(indices, indices); + } + if (adjustVec_ptr) + { + *adjustVec_ptr = dx(indices); + } + + return subState; } /** Get a portion of a state by passing in a list of keys. * Only gets some aspects, as most aren't required */ void KFState::getSubState( - map& kfKeyMap, ///< List of keys to return within substate - KFState& subState) ///< Output state -const + map& kfKeyMap, ///< List of keys to return within substate + KFState& subState ///< Output state +) const { - vector indices; - indices.resize(kfKeyMap.size()); - - subState.kfIndexMap.clear(); - for (auto& [kfKey, mapIndex] : kfKeyMap) - { - int stateIndex = getKFIndex(kfKey); - if (stateIndex >= 0) - { - indices[mapIndex] = stateIndex; - } - - subState.kfIndexMap[kfKey] = mapIndex; - } - - subState.time = time; - subState.x = x (indices); - subState.dx = dx(indices); - subState.P = P (indices, indices); - - subState.stateTransitionMap.clear(); - - for (auto& [keyA, stmMap] : stateTransitionMap) - { - auto itA = kfKeyMap.find(keyA); - if (itA == kfKeyMap.end()) - { - continue; - } - - for (auto& [keyB, st] : stmMap) - { - auto itB = kfKeyMap.find(keyB); - if (itB == kfKeyMap.end()) - { - continue; - } - - subState.stateTransitionMap[keyA][keyB] = st; - } - } + vector indices; + indices.resize(kfKeyMap.size()); + + subState.kfIndexMap.clear(); + for (auto& [kfKey, mapIndex] : kfKeyMap) + { + int stateIndex = getKFIndex(kfKey); + if (stateIndex >= 0) + { + indices[mapIndex] = stateIndex; + } + + subState.kfIndexMap[kfKey] = mapIndex; + } + + subState.time = time; + subState.x = x(indices); + subState.dx = dx(indices); + subState.P = P(indices, indices); + + subState.stateTransitionMap.clear(); + + for (auto& [keyA, stmMap] : stateTransitionMap) + { + auto itA = kfKeyMap.find(keyA); + if (itA == kfKeyMap.end()) + { + continue; + } + + for (auto& [keyB, st] : stmMap) + { + auto itB = kfKeyMap.find(keyB); + if (itB == kfKeyMap.end()) + { + continue; + } + + subState.stateTransitionMap[keyA][keyB] = st; + } + } } -KFState KFState::getSubState( - vector types) -const +KFState KFState::getSubState(vector types, KFMeas* meas_ptr) const { - if (std::find(types.begin(), types.end(), +KF::ALL) != types.end()) - { - return *this; - } - - KFState subState; - - vector indices; - - int index = 0; - for (auto& [kfKey, mapIndex] : kfIndexMap) - { - if (std::find(types.begin(), types.end(), kfKey.type) == types.end()) - { - continue; - } - - indices.push_back(mapIndex); - subState.kfIndexMap[kfKey] = index; - - index++; - } - - subState.time = time; - subState.x = x (indices); - subState.dx = dx(indices); - subState.P = P (indices, indices); - - // for (auto& [kfKey, thing] : stateTransitionMap) - // for (auto& [other, entry] : thing) try { subState.kfIndexMap.at(kfKey); - // subState.kfIndexMap.at(other); subState.stateTransitionMap [kfKey][other] = entry; } catch (...){} - // for (auto& [kfKey, entry] : gaussMarkovTauMap) try { subState.kfIndexMap.at(kfKey); subState.gaussMarkovTauMap [kfKey] = entry; } catch (...){} - // for (auto& [kfKey, entry] : gaussMarkovMuMap) try { subState.kfIndexMap.at(kfKey); subState.gaussMarkovMuMap [kfKey] = entry; } catch (...){} - // for (auto& [kfKey, entry] : procNoiseMap) try { subState.kfIndexMap.at(kfKey); subState.procNoiseMap [kfKey] = entry; } catch (...){} - // for (auto& [kfKey, entry] : exponentialNoiseMap) try { subState.kfIndexMap.at(kfKey); subState.exponentialNoiseMap[kfKey] = entry; } catch (...){} - - return subState; + if (std::find(types.begin(), types.end(), KF::ALL) != types.end()) + { + return *this; + } + + KFState subState; + + vector indices; + + int index = 0; + for (auto& [kfKey, mapIndex] : kfIndexMap) + { + if (std::find(types.begin(), types.end(), kfKey.type) == types.end()) + { + continue; + } + + indices.push_back(mapIndex); + subState.kfIndexMap[kfKey] = index; + + index++; + } + + subState.time = time; + subState.x = x(indices); + subState.dx = dx(indices); + subState.P = P(indices, indices); + + if (meas_ptr) + { + auto& meas = *meas_ptr; + + meas.H = meas.H(all, indices); + } + + return subState; } /** Output keys and states in human readable format -*/ + */ void KFState::outputStates( - Trace& output, ///< Trace to output to - string suffix, ///< Suffix to append to state block info tag in trace files - int begX, ///< Index of first state element to process - int numX) ///< Number of state elements to process + Trace& trace, ///< Trace to output to + string suffix, ///< Suffix to append to state block info tag in trace files + int iteration, ///< Number of iterations prior to this check + int begX, ///< Index of first state element to process + int numX ///< Number of state elements to process +) { - tracepdeex(2, output, "\n\n"); - - string name = "STATES"; - name += suffix; - - InteractiveTerminal trace(name, output); - Block block(trace, name); - - tracepdeex(1, trace, "#\t%20s\t%20s\t%5s\t%3s\t%7s\t%17s\t%17s\t%15s", "Time", "Type", "Str", "Sat", "Num", "State", "Sigma", "Adjust"); - tracepdeex(5, trace, "\t%17s", "Mu"); - tracepdeex(2, trace, "\t%s", "Comments"); - tracepdeex(1, trace, "\n"); - - int endX; - if (numX < 0) endX = x.rows(); - else endX = begX + numX; - - bool noAdjust = dx.isZero(); - - for (auto& [key, index] : kfIndexMap) - { - if (index >= x.rows()) - { - continue; - } - if ( index < begX - ||index >= endX) - { - continue; - } - - double _x = x(index); - double _dx = 0; - if (index < dx.rows()) - _dx = dx(index); - double _sigma = sqrt(P(index, index)); - string type = KF::_from_integral(key.type)._to_string(); - - char dStr[20]; - char xStr[20]; - char pStr[20]; - char muStr[20]; - if (noAdjust) snprintf(dStr, sizeof(dStr), "%15.0s", ""); - else if (_dx == 0 || (fabs(_dx) > 0.0001 && fabs(_dx) < 1e5)) snprintf(dStr, sizeof(dStr), "%15.8f", _dx); - else snprintf(dStr, sizeof(dStr), "%15.4e", _dx); - - if (_x == 0 || (fabs(_x) > 0.0001 && fabs(_x) < 1e8)) snprintf(xStr, sizeof(xStr), "%17.7f", _x); - else snprintf(xStr, sizeof(xStr), "%17.3e", _x); - - if (_sigma == 0 || (fabs(_sigma)> 0.0001 && fabs(_sigma) < 1e8)) snprintf(pStr, sizeof(pStr), "%17.8f", _sigma); - else snprintf(pStr, sizeof(pStr), "%17.4e", _sigma); - - double mu = 0; - auto it = gaussMarkovMuMap.find(key); - if (it != gaussMarkovMuMap.end()) - mu = it->second; - - if (mu == 0) snprintf(muStr, sizeof(muStr), ""); - else if (fabs(mu)> 0.0001 && fabs(mu) < 1e8) snprintf(muStr, sizeof(muStr), "%17.8f", mu); - else snprintf(muStr, sizeof(muStr), "%17.4e", mu); - - - tracepdeex(1, trace, "*\t%20s\t%20s\t%5s\t%3s\t%7d\t%s\t%s\t%s", - time.to_string(0).c_str(), - type.c_str(), - key.str.c_str(), - key.Sat.id().c_str(), - key.num, - xStr, - pStr, - dStr); - tracepdeex(5, trace, "\t%17s", muStr); - tracepdeex(6, trace, "\t%x", key.rec_ptr); - tracepdeex(2, trace, "\t%-40s", key.comment.c_str()); - tracepdeex(1, trace, "\n"); - } + tracepdeex(1, trace, "\n"); + + string name = "STATES"; + name += suffix; + + Block block(trace, name); + + tracepdeex( + 1, + trace, + "#\t%2s\t%22s\t%12s\t%4s\t%4s\t%7s\t%17s\t%17s\t%16s", + "It", + "Time", + "Type", + "Sat", + "Str", + "Code", + "State", + "Sigma", + "Adjust" + ); + tracepdeex(5, trace, "\t%16s", "Prefit Ratio"); + tracepdeex(5, trace, "\t%16s", "Postfit Ratio"); + tracepdeex(5, trace, "\t%17s", "Mu"); + tracepdeex(2, trace, "\t%s", "Comments"); + tracepdeex(1, trace, "\n"); + + int endX; + if (numX < 0) + endX = x.rows(); + else + endX = begX + numX; + + bool noAdjust = dx.isZero(); + + for (auto& [key, index] : kfIndexMap) + { + if (index >= x.rows()) + { + continue; + } + if (index < begX || index >= endX) + { + continue; + } + + char xStr[20]; + char sigmaStr[20]; + char dxStr[20]; + char preRatioStr[20]; + char postRatioStr[20]; + char muStr[20]; + + double _x = x(index); + + if (_x == 0 || (fabs(_x) > 0.001 && fabs(_x) < 1e8)) + snprintf(xStr, sizeof(xStr), "%17.7f", _x); + else + snprintf(xStr, sizeof(xStr), "%17.3e", _x); + + double _sigma = sqrt(P(index, index)); + + if (_sigma == 0 || (fabs(_sigma) > 0.0001 && fabs(_sigma) < 1e8)) + snprintf(sigmaStr, sizeof(sigmaStr), "%17.8f", _sigma); + else + snprintf(sigmaStr, sizeof(sigmaStr), "%17.4e", _sigma); + + double _dx = 0; + if (index < dx.rows()) + _dx = dx(index); + + if (noAdjust) + snprintf(dxStr, sizeof(dxStr), "%16.0s", ""); + else if (_dx == 0 || (fabs(_dx) > 0.0001 && fabs(_dx) < 1e6)) + snprintf(dxStr, sizeof(dxStr), "%16.8f", _dx); + else + snprintf(dxStr, sizeof(dxStr), "%16.4e", _dx); + + double preRatio = 0; + if (index < prefitRatios.rows()) + preRatio = prefitRatios(index); + + if (preRatio == 0 || (fabs(preRatio) > 0.001 && fabs(preRatio) < 1e7)) + snprintf(preRatioStr, sizeof(preRatioStr), "%16.7f", preRatio); + else + snprintf(preRatioStr, sizeof(preRatioStr), "%16.3e", preRatio); + + double postRatio = 0; + if (index < postfitRatios.rows()) + postRatio = postfitRatios(index); + + if (postRatio == 0 || (fabs(postRatio) > 0.001 && fabs(postRatio) < 1e7)) + snprintf(postRatioStr, sizeof(postRatioStr), "%16.7f", postRatio); + else + snprintf(postRatioStr, sizeof(postRatioStr), "%16.3e", postRatio); + + double mu = 0; + auto it = gaussMarkovMuMap.find(key); + if (it != gaussMarkovMuMap.end()) + mu = it->second; + + if (mu == 0) + snprintf(muStr, sizeof(muStr), ""); + else if (fabs(mu) > 0.001 && fabs(mu) < 1e8) + snprintf(muStr, sizeof(muStr), "%17.7f", mu); + else + snprintf(muStr, sizeof(muStr), "%17.3e", mu); + + tracepdeex( + 1, + trace, + "*\t%2d\t%22s\t%30s\t%17s\t%17s\t%16s", + iteration, + time.to_string(2).c_str(), + ((string)key).c_str(), + xStr, + sigmaStr, + dxStr + ); + tracepdeex(5, trace, "\t%16s", preRatioStr); + tracepdeex(5, trace, "\t%16s", postRatioStr); + tracepdeex(5, trace, "\t%17s", muStr); + tracepdeex(6, trace, "\t%x", key.rec_ptr); + tracepdeex(2, trace, "\t%s", key.comment.c_str()); + tracepdeex(1, trace, "\n"); + } } -MatrixXi correlationMatrix( - MatrixXd& P) +MatrixXi correlationMatrix(MatrixXd& P) { - MatrixXi correlations = MatrixXi(P.rows(), P.cols()); + MatrixXi correlations = MatrixXi(P.rows(), P.cols()); - for (int i = 0; i < P.rows(); i++) - for (int j = 0; j <= i; j++) - { - double v1 = P(i, i); - double v2 = P(j, j); - double v12 = P(i, j); + for (int i = 0; i < P.rows(); i++) + for (int j = 0; j <= i; j++) + { + double v1 = P(i, i); + double v2 = P(j, j); + double v12 = P(i, j); - double correlation = v12 / sqrt(v1 * v2) * 100; - correlations(i, j) = correlation; - correlations(j, i) = correlation; - } + double correlation = v12 / sqrt(v1 * v2) * 100; + correlations(i, j) = correlation; + correlations(j, i) = correlation; + } - return correlations; + return correlations; } -void KFState::outputConditionNumber( - Trace& trace) +void KFState::outputConditionNumber(Trace& trace) { - Eigen::JacobiSVD svd(P.bottomRightCorner(P.rows()-1, P.cols()-1)); - double conditionNumber = svd.singularValues()(0) / svd.singularValues()(svd.singularValues().size()-1); + Eigen::JacobiSVD svd(P.bottomRightCorner(P.rows() - 1, P.cols() - 1)); + double conditionNumber = + svd.singularValues()(0) / svd.singularValues()(svd.singularValues().size() - 1); - tracepdeex(0, trace, "\n\n Condition number: %f", conditionNumber); + tracepdeex(0, trace, "\n\n Condition number: %f", conditionNumber); } -void KFState::outputCorrelations( - Trace& trace) +void KFState::outputCorrelations(Trace& trace) { - tracepdeex(2, trace, "\n\n"); - - Block block(trace, "CORRELATIONS"); - - int skip = 0; - int total = kfIndexMap.size(); - for (auto& [key, index] : kfIndexMap) - { - if (key.type == KF::ONE) - { - continue; - } - - tracepdeex(2, trace, "%s ", KFKey::emptyString().c_str()); - for (int i = 0; i < skip; i++) - { - tracepdeex(2, trace, "| "); - } - - trace << "> "; - - for (int i = 0; i < total - skip; i++) - { - tracepdeex(2, trace, "-----"); - } - - trace << key << "\n"; - - skip++; - } - - MatrixXi correlations = correlationMatrix(P); - - for (auto& [key, index] : kfIndexMap) - { - if (key.type == KF::ONE) - { - continue; - } - - trace << key << " : "; - - for (auto& [key2, index2] : kfIndexMap) - { - if (key2.type == KF::ONE) - { - continue; - } - - int correlation = correlations(index, index2); - - if (index == index2) tracepdeex(2, trace, "%4s ", "100"); - else if (fabs(correlation) > 100) tracepdeex(2, trace, "%4s ", "----"); - else if (fabs(correlation) < 1) tracepdeex(2, trace, "%4s ", ""); - else tracepdeex(2, trace, "%4.0f ", correlation); - } - - trace << "\n"; - } + tracepdeex(2, trace, "\n"); + + Block block(trace, "CORRELATIONS"); + + int skip = 0; + int total = kfIndexMap.size(); + for (auto& [key, index] : kfIndexMap) + { + if (key.type == KF::ONE) + { + continue; + } + + tracepdeex(2, trace, "%s ", KFKey::emptyString().c_str()); + for (int i = 0; i < skip; i++) + { + tracepdeex(2, trace, "| "); + } + + trace << "> "; + + for (int i = 0; i < total - skip; i++) + { + tracepdeex(2, trace, "-----"); + } + + trace << key << "\n"; + + skip++; + } + + MatrixXi correlations = correlationMatrix(P); + + for (auto& [key, index] : kfIndexMap) + { + if (key.type == KF::ONE) + { + continue; + } + + trace << key << " : "; + + for (auto& [key2, index2] : kfIndexMap) + { + if (key2.type == KF::ONE) + { + continue; + } + + int correlation = correlations(index, index2); + + if (index == index2) + tracepdeex(2, trace, "%4s ", "100"); + else if (fabs(correlation) > 100) + tracepdeex(2, trace, "%4s ", "----"); + else if (fabs(correlation) < 1) + tracepdeex(2, trace, "%4s ", ""); + else + tracepdeex(2, trace, "%4.0f ", correlation); + } + + trace << "\n"; + } } -void KFState::outputMeasurements( - Trace& trace, - KFMeas& meas) +void KFState::outputMeasurements(Trace& trace, KFMeas& meas) { - tracepdeex(2, trace, "\n\n"); + tracepdeex(2, trace, "\n"); - Block block(trace, "MEASUREMENTS"); + Block block(trace, "MEASUREMENTS"); - int total = kfIndexMap.size(); - int skip = 0; - for (auto& [key, index] : kfIndexMap) - { - if (key.type == KF::ONE) - { - continue; - } + int total = kfIndexMap.size(); + int skip = 0; + for (auto& [key, index] : kfIndexMap) + { + if (key.type == KF::ONE) + { + continue; + } - tracepdeex(0, trace, "%s ", KFKey::emptyString().c_str()); + tracepdeex(0, trace, "%s ", KFKey::emptyString().c_str()); - for (int i = 0; i < skip; i++) - { - tracepdeex(2, trace, "| "); - } + for (int i = 0; i < skip; i++) + { + tracepdeex(2, trace, "| "); + } - trace << "> "; + trace << "> "; - for (int i = 0; i < total - skip; i++) - { - tracepdeex(2, trace, "-------"); - } + for (int i = 0; i < total - skip; i++) + { + tracepdeex(2, trace, "-------"); + } - trace << key << "\n"; + trace << key << "\n"; - skip++; - } + skip++; + } - for (int i = 0; i < meas.obsKeys.size(); i++) - { - auto& key = meas.obsKeys[i]; + for (int i = 0; i < meas.obsKeys.size(); i++) + { + auto& key = meas.obsKeys[i]; - trace << key << " : "; + trace << key << " : "; - for (int j = 1; j < meas.H.cols(); j++) - { - double a = meas.H(i,j); + for (int j = 1; j < meas.H.cols(); j++) + { + double a = meas.H(i, j); - if (fabs(a) > 0.001) tracepdeex(2, trace, "%6.2f ", a); - else tracepdeex(2, trace, "%6.2s ", ""); - } - tracepdeex(2, trace, "\t : %16.4f\n", meas.V(i)); - } + if (fabs(a) > 0.001) + tracepdeex(2, trace, "%6.2f ", a); + else + tracepdeex(2, trace, "%6.2s ", ""); + } + tracepdeex(2, trace, "\t : %16.4f\n", meas.V(i)); + } } -InitialState initialStateFromConfig( - const KalmanModel& kalmanModel, - int index) +InitialState initialStateFromConfig(const KalmanModel& kalmanModel, int index) { - InitialState init = {}; - - if (index < kalmanModel.estimate .size()) init.estimate = kalmanModel.estimate [index]; - else init.estimate = kalmanModel.estimate .back(); - if (index < kalmanModel.use_remote_sigma.size()) init.use_remote_sigma = kalmanModel.use_remote_sigma[index]; - else init.use_remote_sigma = kalmanModel.use_remote_sigma.back(); - if (index < kalmanModel.apriori_value .size()) init.x = kalmanModel.apriori_value [index]; - else init.x = kalmanModel.apriori_value .back(); - if (index < kalmanModel.sigma .size()) init.P = SQR( kalmanModel.sigma [index]) * SGN(kalmanModel.sigma [index]); - else init.P = SQR( kalmanModel.sigma .back()) * SGN(kalmanModel.sigma .back()); - if (index < kalmanModel.tau .size()) init.tau = kalmanModel.tau [index]; - else init.tau = kalmanModel.tau .back(); - if (index < kalmanModel.mu .size()) init.mu = kalmanModel.mu [index]; - else init.mu = kalmanModel.mu .back(); - if (index < kalmanModel.process_noise .size()) init.Q = SQR( kalmanModel.process_noise [index]) * SGN(kalmanModel.process_noise[index]); - else init.Q = SQR( kalmanModel.process_noise .back()) * SGN(kalmanModel.process_noise.back()); - if (index < kalmanModel.comment .size()) init.comment = kalmanModel.comment [index]; - else init.comment = kalmanModel.comment .back(); - - return init; + InitialState init = {}; + + if (index < kalmanModel.estimate.size()) + init.estimate = kalmanModel.estimate[index]; + else + init.estimate = kalmanModel.estimate.back(); + if (index < kalmanModel.use_remote_sigma.size()) + init.use_remote_sigma = kalmanModel.use_remote_sigma[index]; + else + init.use_remote_sigma = kalmanModel.use_remote_sigma.back(); + if (index < kalmanModel.apriori_value.size()) + init.x = kalmanModel.apriori_value[index]; + else + init.x = kalmanModel.apriori_value.back(); + if (index < kalmanModel.sigma.size()) + init.P = SQR(kalmanModel.sigma[index]) * SGN(kalmanModel.sigma[index]); + else + init.P = SQR(kalmanModel.sigma.back()) * SGN(kalmanModel.sigma.back()); + if (index < kalmanModel.sigma_limit.size()) + init.sigmaMax = kalmanModel.sigma_limit[index]; + else + init.sigmaMax = kalmanModel.sigma_limit.back(); + if (index < kalmanModel.outage_limit.size()) + init.outageLimit = kalmanModel.outage_limit[index]; + else + init.outageLimit = kalmanModel.outage_limit.back(); + if (index < kalmanModel.tau.size()) + init.tau = kalmanModel.tau[index]; + else + init.tau = kalmanModel.tau.back(); + if (index < kalmanModel.mu.size()) + init.mu = kalmanModel.mu[index]; + else + init.mu = kalmanModel.mu.back(); + if (index < kalmanModel.process_noise.size()) + init.Q = SQR(kalmanModel.process_noise[index]) * SGN(kalmanModel.process_noise[index]); + else + init.Q = SQR(kalmanModel.process_noise.back()) * SGN(kalmanModel.process_noise.back()); + if (index < kalmanModel.comment.size()) + init.comment = kalmanModel.comment[index]; + else + init.comment = kalmanModel.comment.back(); + + return init; } -KFState mergeFilters( - const vector& kfStatePointerList, - const vector& stateList) +KFState mergeFilters(const vector& kfStatePointerList, const vector& stateList) { - map stateValueMap; - map> stateCovarMap; - - for (auto& statePointer : kfStatePointerList) - { - KFState& kfState = *statePointer; - - for (auto& [key1, index1] : kfState.kfIndexMap) - for (auto state1 : stateList) - { - if (key1.type == +state1) - { - stateValueMap[key1] = kfState.x(index1); - - for (auto& [key2, index2] : kfState.kfIndexMap) - for (auto state2 : stateList) - { - if (key2.type == +state2) - { - double val = kfState.P(index1, index2); - if (val != 0) - { - stateCovarMap[key1][key2] = val; - } - - break; - } - } - break; - } - } - } - - KFState mergedKFState; - - mergedKFState.x = VectorXd::Zero(stateValueMap.size()); - mergedKFState.dx = VectorXd::Zero(stateValueMap.size()); - mergedKFState.P = MatrixXd::Zero(stateValueMap.size(), stateValueMap.size()); - - int i = 0; - for (auto& [key, value] : stateValueMap) - { - mergedKFState.kfIndexMap[key] = i; - mergedKFState.x(i) = value; - - i++; - } - - for (auto& [key1, map2] : stateCovarMap) - for (auto& [key2, value] : map2) - { - int index1 = mergedKFState.kfIndexMap[key1]; - int index2 = mergedKFState.kfIndexMap[key2]; - - mergedKFState.P(index1, index2) = value; - } - - return mergedKFState; + map stateValueMap; + map> stateCovarMap; + + for (auto& statePointer : kfStatePointerList) + { + KFState& kfState = *statePointer; + + for (auto& [key1, index1] : kfState.kfIndexMap) + for (auto state1 : stateList) + { + if (key1.type == state1) + { + stateValueMap[key1] = kfState.x(index1); + + for (auto& [key2, index2] : kfState.kfIndexMap) + for (auto state2 : stateList) + { + if (key2.type == state2) + { + double val = kfState.P(index1, index2); + if (val != 0) + { + stateCovarMap[key1][key2] = val; + } + + break; + } + } + break; + } + } + } + + KFState mergedKFState; + + mergedKFState.x = VectorXd::Zero(stateValueMap.size()); + mergedKFState.dx = VectorXd::Zero(stateValueMap.size()); + mergedKFState.P = MatrixXd::Zero(stateValueMap.size(), stateValueMap.size()); + + int i = 0; + for (auto& [key, value] : stateValueMap) + { + mergedKFState.kfIndexMap[key] = i; + mergedKFState.x(i) = value; + + i++; + } + + for (auto& [key1, map2] : stateCovarMap) + for (auto& [key2, value] : map2) + { + int index1 = mergedKFState.kfIndexMap[key1]; + int index2 = mergedKFState.kfIndexMap[key2]; + + mergedKFState.P(index1, index2) = value; + } + + return mergedKFState; } -bool isPositiveSemiDefinite( - MatrixXd& mat) +bool isPositiveSemiDefinite(MatrixXd& mat) { - for (int i = 0; i < mat.rows(); i++) - for (int j = 0; j < i; j++) - { - double a = mat(i, i); - double ab = mat(i, j); - double b = mat(j, j); - - if (ab * ab > a * b) - { -// std::cout << "large off diagonals " << "\n"; -// return false; - if (ab > 0) ab = +sqrt(0.99 * a * b); - else ab = -sqrt(0.99 * a * b); - mat(i, j) = ab; - mat(j, i) = ab; - } - } - return true; + for (int i = 0; i < mat.rows(); i++) + for (int j = 0; j < i; j++) + { + double a = mat(i, i); + double ab = mat(i, j); + double b = mat(j, j); + + if (ab * ab > a * b) + { + // std::cout << "large off diagonals " << "\n"; + // return false; + if (ab > 0) + ab = +sqrt(0.99 * a * b); + else + ab = -sqrt(0.99 * a * b); + mat(i, j) = ab; + mat(j, i) = ab; + } + } + return true; } diff --git a/src/cpp/common/algebra.hpp b/src/cpp/common/algebra.hpp index bc31ec6e9..b21a1b0d9 100644 --- a/src/cpp/common/algebra.hpp +++ b/src/cpp/common/algebra.hpp @@ -1,854 +1,915 @@ - #pragma once -#include "eigenIncluder.hpp" #include - #include -#include -#include #include +#include #include #include +#include #include -#include +#include +#include "common/acsConfig.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/satSys.hpp" +#include "common/trace.hpp" using boost::algorithm::to_lower; +using boost::algorithm::to_upper; +using std::hash; using std::lock_guard; +using std::map; +using std::pair; +using std::recursive_mutex; using std::string; -using std::vector; -using std::mutex; using std::tuple; -using std::hash; -using std::pair; -using std::map; - -#include "acsConfig.hpp" -#include "satSys.hpp" -#include "gTime.hpp" -#include "trace.hpp" - +using std::vector; -//forward declaration +// forward declaration struct KFMeasEntryList; struct Receiver; struct KFState; /** Keys used to interface with Kalman filter objects. -* These have parameters to separate states of different 'type', for different 'Sat's, with different receiver id 'str's and may have a different 'num' (eg xyz->0,1,2) -* -* Keys should be used rather than indices for accessing kalman filter state parameters. -*/ + * These have parameters to separate states of different 'type', for different 'Sat's, with + * different receiver id 'str's and may have a different 'num' (eg xyz->0,1,2) + * + * Keys should be used rather than indices for accessing kalman filter state parameters. + */ struct KFKey { - short int type = 0; ///< Key type (From enum) - SatSys Sat = {}; ///< Satellite - string str; ///< String (receiver ID) - int num = 0; ///< Subkey number (eg xyz => 0,1,2) - string comment; ///< Optional comment - Receiver* rec_ptr = 0; ///< Pointer to station object for dereferencing - - bool operator == (const KFKey& b) const; - bool operator < (const KFKey& b) const; - - /** Create a string with the same spacing as ordinary outputs - */ - static string emptyString() - { - KFKey key; - string str = key; - for (auto& c : str) - { - if (c != '\t') - c = ' '; - } - - return str; - } - - operator string() const - { - char buff[100]; - snprintf(buff, sizeof(buff), "%10s\t%4s\t%4s\t%5d", KF::_from_integral(type)._to_string(), Sat.id().c_str(), str.c_str(), num); - string str = buff; - - return str; - } - - string commaString() const - { - char buff[100]; - snprintf(buff, sizeof(buff), "%s,%s,%s,%d", KF::_from_integral(type)._to_string(), Sat.id().c_str(), str.c_str(), num); - string str = buff; - to_lower(str); - - return str; - } - - friend ostream& operator<<(ostream& os, const KFKey& kfKey) - { - string str = kfKey; - os << str; - - return os; - } - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - ar & Sat; - ar & str; - ar & num; - ar & type; - ar & comment; - } - + KF type = KF::NONE; ///< Key type (From enum) + SatSys Sat = {}; ///< Satellite + string str; ///< String (receiver ID) + int num = 0; ///< Subkey number (eg xyz => 0,1,2) + string comment; ///< Optional comment + Receiver* rec_ptr = 0; ///< Pointer to station object for dereferencing + + mutable GTime estimatedTime; + + bool operator!=(const KFKey& b) const; + bool operator==(const KFKey& b) const; + bool operator<(const KFKey& b) const; + + string code() const + { + string code; + + // Measurements or per-measurement states + if (type == KF::CODE_MEAS || type == KF::PHAS_MEAS || type == KF::AMBIGUITY || + type == KF::Z_AMB || type == KF::CODE_BIAS || type == KF::PHASE_BIAS) + { + // IFLC combined + if (num > 100) + { + int num1 = num / 100; + int num2 = num % 100; + + string code1 = enum_to_string(static_cast(num1)); + string code2 = enum_to_string(static_cast(num2)); + code = code1 + "-" + code2; + } + + // Uncombined + else + { + code = enum_to_string(static_cast(num)); + } + + return code; + } + + // STEC + if (type == KF::IONO_STEC && acsConfig.pppOpts.ionoOpts.common_ionosphere == false) + { + code = (num == CODE) ? "CODE" : "PHASE"; + + return code; + } + + int component = 0; + + // Empirical force coefficients + if (type >= KF::EMP_D_0 && type <= KF::EMP_Q_4 && + (static_cast(type) - static_cast(KF::EMP_D_0)) % 5 != 0) + { + // component = E_TrigType::COS + num; + + code = enum_to_string(enum_add(E_TrigType::COS, num)); + + return code; + } + + // Cartesian coordinates + if ((type >= KF::REC_POS && type <= KF::ACC) || + (type > KF::BEGIN_INERTIAL_STATES && type < KF::END_INERTIAL_STATES) || + type == KF::ORBIT || type == KF::ORBIT_MEAS || type == KF::XFORM_XLATE || + type == KF::XFORM_RTATE || type == KF::XFORM_XLATE_RATE || type == KF::XFORM_RTATE_RATE) + { + component = enum_to_int(enum_add(E_StateComponent::X, num)); + } + + // Quaternions + else if (type == KF::ORIENTATION) + { + component = enum_to_int(enum_add(E_StateComponent::W, num)); + } + + // Local tangental coordinates + else if (type == KF::TROP_GRAD || type == KF::ANT_DELTA) + { + component = enum_to_int(enum_add(E_StateComponent::E, num)); + } + + // EOP parameters + else if (type == KF::EOP || type == KF::EOP_RATE) + { + component = enum_to_int(enum_add(E_StateComponent::XP, num)); + } + + code = string(magic_enum::enum_name(static_cast(component))); + std::replace(code.begin(), code.end(), '_', '-'); + + return code; + } + + /** Create a string with the same spacing as ordinary outputs + */ + static string emptyString() + { + KFKey key; + string keyStr = key; + for (auto& c : keyStr) + { + if (c != '\t') + c = ' '; + } + + return keyStr; + } + + operator string() const + { + char buff[100]; + + snprintf( + buff, + sizeof(buff), + "%10s\t%4s\t%4s\t%7s", + enum_to_string(type).c_str(), + Sat.id().c_str(), + str.c_str(), + this->code().c_str() + ); + + return string(buff); + } + + string commaString() const + { + char buff[100]; + snprintf( + buff, + sizeof(buff), + "%s,%s,%s,%s", + enum_to_string(type).c_str(), + Sat.id().c_str(), + str.c_str(), + this->code().c_str() + ); + string keyStr = buff; + + return keyStr; + } + + friend ostream& operator<<(ostream& os, const KFKey& kfKey) + { + string keyStr = kfKey; + os << keyStr; + + return os; + } + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + ar & Sat; + ar & str; + ar & num; + ar & type; + ar & comment; + } }; struct FilterChunk { - string id; - Trace* trace_ptr = nullptr; - int begX = 0; - int numX = 0; - int begH = 0; - int numH = -1; - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - ar & id; - ar & begX; - ar & numX; - } + string id; + Trace* trace_ptr = nullptr; + int begX = 0; + int numX = 0; + int begH = 0; + int numH = -1; + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + ar & id; + ar & begX; + ar & numX; + } }; struct ComponentsDetails { - double value = 0; - string eq; //not valid after combinations - double var = 0; - - - ComponentsDetails& operator+=(const ComponentsDetails& rhs) - { - value += rhs.value; - var += rhs.var; - return *this; - } - - ComponentsDetails operator*(double rhs) - { - ComponentsDetails newDetails = *this; - newDetails.value *= rhs; - newDetails.var *= rhs; - newDetails.var *= rhs; - return newDetails; - } + double value = 0; + string eq; // not valid after combinations + double var = 0; + + ComponentsDetails& operator+=(const ComponentsDetails& rhs) + { + value += rhs.value; + var += rhs.var; + return *this; + } + + ComponentsDetails operator*(double rhs) + { + ComponentsDetails newDetails = *this; + newDetails.value *= rhs; + newDetails.var *= rhs; + newDetails.var *= rhs; + return newDetails; + } }; /** Object to hold measurements, design matrices, and residuals for multiple observations -*/ + */ struct KFMeas { - GTime time = GTime::noTime(); ///< Epoch these measurements were recorded - VectorXd Y; ///< Value of the observations (for linear systems) - VectorXd V; ///< Prefit Residual of the observations (for non-linear systems) - VectorXd VV; ///< Postfit Residual of the observations (for non-linear systems) - VectorXd W; ///< Weight (inverse of noise) used in least squares - MatrixXd R; ///< Measurement noise for these observations - MatrixXd H; ///< Design matrix between measurements and state - MatrixXd H_star; ///< Design matrix between measurements and noise states - VectorXd uncorrelatedNoise; ///< Uncorellated noise for measurements - - map noiseIndexMap; ///< Map from key to indexes of parameters in the noise vector - vector obsKeys; ///< Vector of optional labels for reporting when measurements are removed etc. - vector> metaDataMaps; - vector> componentsMaps; - - KFMeas() - { - - }; - - KFMeas( - KFMeas& kfMeas, ///< Measurement to form linear combination from - vector>&& triplets, ///< Linear combination triplets - vector&& obsKeys, ///< New obs key vector - vector>&& metaDataMaps) ///< Optional new metadata vector - : obsKeys {obsKeys}, - metaDataMaps {metaDataMaps} - { - auto F = SparseMatrix(obsKeys.size(), kfMeas.obsKeys.size()); - - F.setFromTriplets(triplets.begin(), triplets.end()); - - time = kfMeas.time; -// Y = F * kfMeas.Y; - V = F * kfMeas.V; - VV = V; -// W = F * kfMeas.W; - R = F * kfMeas.R * F.transpose(); - H = F * kfMeas.H; - H_star = F * kfMeas.H_star; - uncorrelatedNoise = kfMeas.uncorrelatedNoise; - - componentsMaps.resize(obsKeys.size()); - for (auto& triplet : triplets) - { - auto newIndex = triplet.row(); - auto oldIndex = triplet.col(); - double scalar = triplet.value(); - - for (auto& [component, details] : kfMeas.componentsMaps[oldIndex]) - { - componentsMaps[newIndex][component] += details * scalar; - } - } - } - - KFMeas( - KFState& kfState, - KFMeasEntryList& kfEntryList, - GTime measTime = GTime::noTime(), - MatrixXd* noiseMatrix_ptr = nullptr); - - int getNoiseIndex( - const KFKey& key) - const; - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - int rows = H.rows(); - int cols = H.cols(); - ar & rows; - ar & cols; - - if (ARCHIVE::is_saving::value) - { - //just wrote this, we are writing - map, double> H2; - - ar & obsKeys; - ar & time; - ar & VV; - - for (int i = 0; i < rows; i++) - for (int j = 0; j < cols; j++) - { - double value = H(i,j); - if (value) - { - H2[{i,j}] = value; - } - } - - ar & H2; - } - else - { - //we're reading - map, double> H2; - - ar & obsKeys; - ar & time; - ar & VV; - ar & H2; - - H = MatrixXd::Zero(rows,cols); - R = MatrixXd::Zero(rows,rows); - V = VectorXd::Zero(rows); - - for (auto & [index, value] : H2) - { - H(index.first, index.second) = value; - } - } - } + GTime time = GTime::noTime(); ///< Epoch these measurements were recorded + VectorXd Y; ///< Value of the observations (for linear systems) + VectorXd V; ///< Prefit Residual of the observations (for non-linear systems) + VectorXd VV; ///< Postfit Residual of the observations (for non-linear systems) + VectorXd W; ///< Weight (inverse of noise) used in least squares + MatrixXd R; ///< Measurement noise for these observations + MatrixXd H; ///< Design matrix between measurements and state + MatrixXd H_star; ///< Design matrix between measurements and noise states + VectorXd uncorrelatedNoise; ///< Uncorellated noise for measurements + VectorXd prefitRatios; ///< Prefit sigma check or omega test ratios of measurements + VectorXd postfitRatios; ///< Postfit sigma check or omega test ratios of measurements + + map noiseIndexMap; ///< Map from key to indexes of parameters in the noise vector + vector + obsKeys; ///< Vector of optional labels for reporting when measurements are removed etc. + vector> metaDataMaps; + vector> componentsMaps; + + KFMeas() { + + }; + + KFMeas( + KFMeas& kfMeas, ///< Measurement to form linear combination from + vector>&& triplets, ///< Linear combination triplets + vector&& obsKeys, ///< New obs key vector + vector>&& metaDataMaps ///< Optional new metadata vector + ) + : obsKeys{obsKeys}, metaDataMaps{metaDataMaps} + { + auto F = SparseMatrix(obsKeys.size(), kfMeas.obsKeys.size()); + + F.setFromTriplets(triplets.begin(), triplets.end()); + + time = kfMeas.time; + // Y = F * kfMeas.Y; + V = F * kfMeas.V; + VV = V; + // W = F * kfMeas.W; + R = F * kfMeas.R * F.transpose(); + H = F * kfMeas.H; + H_star = F * kfMeas.H_star; + uncorrelatedNoise = kfMeas.uncorrelatedNoise; + + componentsMaps.resize(obsKeys.size()); + for (auto& triplet : triplets) + { + auto newIndex = triplet.row(); + auto oldIndex = triplet.col(); + double scalar = triplet.value(); + + for (auto& [component, details] : kfMeas.componentsMaps[oldIndex]) + { + componentsMaps[newIndex][component] += details * scalar; + } + } + } + + KFMeas( + KFState& kfState, + KFMeasEntryList& kfEntryList, + GTime measTime = GTime::noTime(), + MatrixXd* noiseMatrix_ptr = nullptr + ); + + int getNoiseIndex(const KFKey& key) const; + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + int rows = H.rows(); + int cols = H.cols(); + ar & rows; + ar & cols; + + if (ARCHIVE::is_saving::value) + { + // just wrote this, we are writing + map, double> H2; + + ar & obsKeys; + ar & time; + ar & VV; + + for (int i = 0; i < rows; i++) + for (int j = 0; j < cols; j++) + { + double value = H(i, j); + if (value) + { + H2[{i, j}] = value; + } + } + + ar & H2; + } + else + { + // we're reading + map, double> H2; + + ar & obsKeys; + ar & time; + ar & VV; + ar & H2; + + H = MatrixXd::Zero(rows, cols); + R = MatrixXd::Zero(rows, rows); + V = VectorXd::Zero(rows); + + for (auto& [index, value] : H2) + { + H(index.first, index.second) = value; + } + } + } }; /** Object to hold the values used to initialise new states when adding to the kalman filter object -*/ + */ struct InitialState { - bool estimate = false; - bool use_remote_sigma = false; - double x = 0; ///< State value - double P = -1; ///< State Covariance - double Q = 0; ///< Process Noise, -ve indicates infinite (throw away state) - double tau = -1; ///< Correlation Time, default to -1 (inf) (Random Walk) - double mu = 0; ///< Desired Mean Value - string comment; + bool estimate = false; + bool use_remote_sigma = false; + double x = 0; ///< State value + double P = -1; ///< State Covariance + double sigmaMax = 0; ///< Sigma limit + double outageLimit = 0; ///< Maxiumum time without state estimation + double Q = 0; ///< Process Noise, -ve indicates infinite (throw away state) + double tau = -1; ///< Correlation Time, default to -1 (inf) (Random Walk) + double mu = 0; ///< Desired Mean Value + string comment; }; struct KFStatistics { - double averageRatio = 0; - double sumOfSquares = 0; + double averageRatio = 0; + double sumOfSquares = 0; }; struct KFState; struct KalmanModel; struct KFMeasEntry; -InitialState initialStateFromConfig( - const KalmanModel& kalmanModel, - int index = 0); +InitialState initialStateFromConfig(const KalmanModel& kalmanModel, int index = 0); -typedef std::ostream Trace; +typedef std::ostream Trace; struct KFMeasList : vector { - }; struct KFMeasEntryList : vector { +}; +struct RejectCallbackDetails +{ + Trace& trace; ///< Trace to output to + KFState& kfState; + KFMeas& kfMeas; ///< Measurements, noise, and design matrix + + RejectCallbackDetails(Trace& trace, KFState& kfState, KFMeas& kfMeas) + : trace{trace}, kfState{kfState}, kfMeas{kfMeas} + { + } + + KFKey kfKey; ///< Key to the state that is flagged as an error + int stateIndex = -1; ///< Index of the state that is flagged as an error + int measIndex = -1; ///< Index of the measurement that is flagged as an outlier + E_FilterStage stage = E_FilterStage::LSQ; ///< prefit, postfit, least squares }; -typedef bool (*StateRejectCallback) (Trace& trace, KFState& kfState, KFMeas& meas, const KFKey& key, bool postFit); -typedef bool (*MeasRejectCallback) (Trace& trace, KFState& kfState, KFMeas& meas, int index, bool postFit); +typedef bool (*StateRejectCallback)(RejectCallbackDetails rejectCallbackDetails); +typedef bool (*MeasRejectCallback)(RejectCallbackDetails rejectCallbackDetails); struct Exponential { - double value = 0; - double tau = 0; + double value = 0; + double tau = 0; }; /** Kalman filter object. -* -* Contains most persistent parameters and values of state. Includes state vector, covariance, and process noise. -* -* This object performs all operations on the kalman filter to ensure that edge cases are included and state kept in a valid configuration. -*/ + * + * Contains most persistent parameters and values of state. Includes state vector, covariance, and + * process noise. + * + * This object performs all operations on the kalman filter to ensure that edge cases are included + * and state kept in a valid configuration. + */ struct KFState_ : FilterOptions { - bool lsqRequired = false; ///< Uninitialised parameters require least squares calculation + bool lsqRequired = false; ///< Uninitialised parameters require least squares calculation - GTime time = {}; - VectorXd x; ///< State - MatrixXd P; ///< State Covariance - VectorXd dx; ///< Last filter update + GTime time = {}; + VectorXd x; ///< State + MatrixXd P; ///< State Covariance + VectorXd dx; ///< Last filter update + VectorXd prefitRatios; ///< Prefit sigma check or omega test ratios of states + VectorXd postfitRatios; ///< Postfit sigma check or omega test ratios of states - map kfIndexMap; ///< Map from key to indexes of parameters in the state vector + map kfIndexMap; ///< Map from key to indexes of parameters in the state vector - map>> stateTransitionMap; - map gaussMarkovTauMap; - map gaussMarkovMuMap; - map procNoiseMap; - map initNoiseMap; - map exponentialNoiseMap; + map>> stateTransitionMap; + map gaussMarkovTauMap; + map gaussMarkovMuMap; + map procNoiseMap; + map initNoiseMap; + map sigmaMaxMap; + map outageLimitMap; + map exponentialNoiseMap; - vector stateRejectCallbacks; - vector measRejectCallbacks; + map> + pseudoStateMap; ///< Map of pseudo states, and a further map of their coefficients + map + pseudoParentMap; ///< Map from ordinary states to their combined pseudo state parent. - map filterChunkMap; + map errorCountMap; - map metaDataMap; + vector stateRejectCallbacks; + vector measRejectCallbacks; - bool chiQCPass = false; - double chi = 0; - int dof = 0; + map filterChunkMap; - string id = "KFState"; + map metaDataMap; - string rts_basename = ""; + bool sigmaPass = false; - bool output_residuals = false; - bool outputMongoMeasurements = false; + bool chiQCPass = false; + double chi2 = 0; + int dof = 0; + double chi2PerDof = INFINITY; + double qc = 0; - KFState* alternate_ptr = nullptr; + string id = "KFState"; - map statisticsMap; - map statisticsMapSum; -}; + string rts_basename = ""; + bool output_residuals = false; + bool outputMongoMeasurements = false; -/** Wrapper to protect main KFState_ structure from multithreading issues. - * The main purpose of this structure is to allow the use of the `const` attribute, signifying whether the object is safe to be modified without multithreading locks. - * - * When a KFState is passed to multithreading code, it should be passed as a `const` reference, preventing ordinary modification of its members, which are likely to collide during parallel calculations. - * - * Wrapper functions cast the object so it is as-if it were const and then call the ordinary functions after obtaining the object's mutex + KFState* alternate_ptr = nullptr; + + map statisticsMap; + map statisticsMapSum; +}; + +/** Wrapper to simplify copying with default copy but overriding slightly. */ struct KFState : KFState_ { - mutex kfStateMutex; - - static const KFKey oneKey; ///< KFStates generally contain a ONE state as the first element, used for converting matrix additions to matrix multiplications. - - KFState( - const KFState &kfState) - : KFState_ (kfState), - kfStateMutex () - { - //dont use same rts file unless explicitly copied - rts_basename.clear(); - } - - 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); - - kfIndexMap[oneKey] = 0; - - initFilterEpoch(); - } - - KFState& operator=( - const KFState& kfState) - { - KFState_* thisKfState_ = (KFState_*)this; - KFState_* thatKfState_ = (KFState_*)&kfState; - - *thisKfState_ = *thatKfState_; - - //dont use same rts file unless explicitly copied - rts_basename.clear(); - - return *this; - } - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - ar & kfIndexMap; - ar & time; - ar & x; - ar & dx; - ar & filterChunkMap; - - double num; - int rows = P.rows(); - ar & rows; - - if (ARCHIVE::is_saving::value) - { - for (int i = 0; i < P.rows(); i++) - for (int j = 0; j <= i; j++) - { - num = P(i,j); - - ar & num; - } - } - else - { - P = MatrixXd(rows,rows); - - for (int i = 0; i < P.rows(); i++) - for (int j = 0; j <= i; j++) - { - ar & num; - - P(i,j) = num; - P(j,i) = num; - } - } - } - - void initFilterEpoch(); - - int getKFIndex( - const KFKey& key) - const; - - E_Source getKFValue( - const KFKey& key, - double& value, - double* variance = nullptr, - double* adjustment_ptr = nullptr, - bool allowAlternate = true) - const; - - bool getKFSigma( - const KFKey& key, - double& sigma); - - bool addKFState( - const KFKey& kfKey, - const InitialState& initialState = {}); - - void setExponentialNoise( - const KFKey& kfKey, - const Exponential exponential); - - void setAccelerator( - const KFKey& element, - const KFKey& dotElement, - const KFKey& dotDotElement, - const double value, - const InitialState& initialState = {}); - - void setKFTrans( - const KFKey& dest, - const KFKey& source, - const double value, - const InitialState& initialState = {}); - - void setKFTransRate( - const KFKey& integral, - const KFKey& rate, - const double value, - const InitialState& initialRateState = {}, - const InitialState& initialIntegralState = {}); - - void addNoiseElement( - const KFKey& obsKey, - const double variance); - - void removeState( - const KFKey& kfKey); - - void stateTransition( - Trace& trace, - GTime newTime, - MatrixXd* stm_ptr = nullptr); - - void manualStateTransition( - Trace& trace, - GTime newTime, - MatrixXd& stm, - MatrixXd& procNoise); - - void preFitSigmaCheck( - Trace& trace, - KFMeas& kfMeas, - KFKey& badStateKey, - int& badMeasIndex, - KFStatistics& statistics, - int begX, - int numX, - int begH, - int numH); - - void postFitSigmaChecks( - Trace& trace, - KFMeas& kfMeas, - VectorXd& dx, - int iteration, - KFKey& badStateKey, - int& badMeasIndex, - KFStatistics& statistics, - int begX, - int numX, - int begH, - int numH); - - double stateChiSquare( - Trace& trace, - MatrixXd& Pp, - VectorXd& dx, - int begX, - int numX, - int begH, - int numH); - - double measChiSquare( - Trace& trace, - KFMeas& kfMeas, - VectorXd& dx, - int begX, - int numX, - int begH, - int numH); - - double innovChiSquare( - Trace& trace, - KFMeas& kfMeas, - int begX, - int numX, - int begH, - int numH); - - bool kFilter( - Trace& trace, - KFMeas& kfMeas, - VectorXd& xp, - MatrixXd& Pp, - VectorXd& dx, - int begX = 0, - int numX = -1, - int begH = 0, - int numH = -1); - - bool chiQC( - Trace& trace, - KFMeas& kfMeas, - VectorXd& xp); - - void outputStates( - Trace& trace, - string suffix = "", - int begX = 0, - int numX = -1); - - void outputConditionNumber( - Trace& trace); - - void outputCorrelations( - Trace& trace); - - void outputMeasurements( - Trace& trace, - KFMeas& meas); - - bool doStateRejectCallbacks( - Trace& trace, - KFMeas& kfMeas, - KFKey& badKey, - bool postFit); - - bool doMeasRejectCallbacks( - Trace& trace, - KFMeas& kfMeas, - int badIndex, - bool postFit); - - void filterKalman( - Trace& trace, - KFMeas& kfMeas, - const string& suffix = "", - bool innovReady = false, - map* filterChunkMap_ptr = nullptr); - - void leastSquareInitStates( - Trace& trace, - KFMeas& kfMeas, - bool initCovars = false, - VectorXd* dx = nullptr, - bool innovReady = false); - - VectorXd getSubState( - map& kfKeyMap, - MatrixXd* covarMat_ptr = nullptr, - VectorXd* adjustVec_ptr = nullptr) - const; - - void getSubState( - map& kfKeyMap, - KFState& kfState) - const; - - KFState getSubState( - vector) - const; - - void removeState( - const KFKey& kfKey) - const - { - auto& kfState = *const_cast(this); lock_guard guard(kfState.kfStateMutex); kfState.removeState (kfKey); - } - - void setExponentialNoise( - const KFKey& kfKey, - const Exponential exponential) - const - { - auto& kfState = *const_cast(this); lock_guard guard(kfState.kfStateMutex); kfState.setExponentialNoise (kfKey, exponential); - } - - bool addKFState( - const KFKey& kfKey, - const InitialState& initialState = {}) - const - { - auto& kfState = *const_cast(this); lock_guard guard(kfState.kfStateMutex); return kfState.addKFState (kfKey, initialState); - } - - void setKFTrans( - const KFKey& dest, - const KFKey& source, - const double value, - const InitialState& initialState = {}) - const - { - auto& kfState = *const_cast(this); lock_guard guard(kfState.kfStateMutex); kfState.setKFTrans (dest, source, value, initialState); - } - - void setKFTransRate( - const KFKey& integral, - const KFKey& rate, - const double value, - const InitialState& initialRateState = {}, - const InitialState& initialIntegralState = {}) - const - { - auto& kfState = *const_cast(this); lock_guard guard(kfState.kfStateMutex); kfState.setKFTransRate (integral, rate, value, initialRateState, initialIntegralState); - } + mutable recursive_mutex kfStateMutex; + + static const KFKey oneKey; ///< KFStates generally contain a ONE state as the first element, + ///< used for converting matrix additions to matrix multiplications. + + KFState(const KFState& kfState) : KFState_(kfState), kfStateMutex() + { + // dont use same rts file unless explicitly copied + rts_basename.clear(); + } + + 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); + + kfIndexMap[oneKey] = 0; + + initFilterEpoch(nullStream); + } + + KFState& operator=(const KFState& kfState) + { + KFState_* thisKfState_ = (KFState_*)this; + KFState_* thatKfState_ = (KFState_*)&kfState; + + *thisKfState_ = *thatKfState_; + + // dont use same rts file unless explicitly copied + rts_basename.clear(); + + return *this; + } + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + ar & kfIndexMap; + ar & time; + ar & x; + ar & dx; + ar & filterChunkMap; + + double num; + int rows = P.rows(); + ar & rows; + + if (ARCHIVE::is_saving::value) + { + for (int i = 0; i < P.rows(); i++) + for (int j = 0; j <= i; j++) + { + num = P(i, j); + + ar & num; + } + } + else + { + P = MatrixXd(rows, rows); + + for (int i = 0; i < P.rows(); i++) + for (int j = 0; j <= i; j++) + { + ar & num; + + P(i, j) = num; + P(j, i) = num; + } + } + } + + void initFilterEpoch(Trace& trace); + + int getKFIndex(const KFKey& key) const; + + E_Source getKFValue( + const KFKey& key, + double& value, + double* variance = nullptr, + double* adjustment_ptr = nullptr, + bool allowAlternate = true + ) const; + + E_Source getPseudoValue( + const KFKey& key, + double& value, + double* variance = nullptr, + double* adjustment_ptr = nullptr + ) const; + + bool getKFSigma(const KFKey& key, double& sigma); + + bool addKFState(const KFKey& kfKey, const InitialState& initialState = {}); + + bool addPseudoState(const KFKey& kfKey, const map& coeffMap); + + void setExponentialNoise(const KFKey& kfKey, const Exponential exponential); + + void setAccelerator( + const KFKey& element, + const KFKey& dotElement, + const KFKey& dotDotElement, + const double value, + const InitialState& initialState = {} + ); + + void setKFTrans( + const KFKey& dest, + const KFKey& source, + const double value, + const InitialState& initialState = {} + ); + + void setKFTransRate( + const KFKey& integral, + const KFKey& rate, + const double value, + const InitialState& initialRateState = {}, + const InitialState& initialIntegralState = {} + ); + + void addNoiseElement(const KFKey& obsKey, const double variance); + + void removeState(const KFKey& kfKey, bool allowDeleteParent = true); + + void stateTransition(Trace& trace, GTime newTime, MatrixXd* stm_ptr = nullptr); + + void manualStateTransition(Trace& trace, GTime newTime, MatrixXd& stm, MatrixXd& procNoise); + + void leastSquareSigmaChecks( + RejectCallbackDetails& callbackDetails, + MatrixXd& Pp, + KFStatistics& statistics + ); + + void preFitSigmaChecks( + RejectCallbackDetails& callbackDetails, + KFStatistics& statistics, + int begX, + int numX, + int begH, + int numH + ); + + void postFitSigmaChecks( + RejectCallbackDetails& callbackDetails, + VectorXd& dx, + MatrixXd& Qinv, + MatrixXd& QinvH, + KFStatistics& statistics, + int begX, + int numX, + int begH, + int numH + ); + + double stateChiSquare( + Trace& trace, + MatrixXd& Pp, + VectorXd& dx, + int begX, + int numX, + int begH, + int numH + ); + + double measChiSquare( + Trace& trace, + KFMeas& kfMeas, + VectorXd& dx, + int begX, + int numX, + int begH, + int numH + ); + + double innovChiSquare(Trace& trace, KFMeas& kfMeas, int begX, int numX, int begH, int numH); + + bool kFilter( + Trace& trace, + KFMeas& kfMeas, + VectorXd& xp, + MatrixXd& Pp, + VectorXd& dx, + MatrixXd& Qinv, + MatrixXd& QinvH, + int begX = 0, + int numX = -1, + int begH = 0, + int numH = -1 + ); + + bool leastSquare(Trace& trace, KFMeas& kfMeas, VectorXd& xp, MatrixXd& Pp); + + void chiQC(Trace& trace, KFMeas& kfMeas); + + void + outputStates(Trace& trace, string suffix = "", int iteration = -1, int begX = 0, int numX = -1); + + void outputConditionNumber(Trace& trace); + + void outputCorrelations(Trace& trace); + + void outputMeasurements(Trace& trace, KFMeas& meas); + + bool doStateRejectCallbacks(RejectCallbackDetails rejectDetails); + + bool doMeasRejectCallbacks(RejectCallbackDetails rejectDetails); + + void filterKalman( + Trace& trace, + KFMeas& kfMeas, + const string& suffix = "", + bool innovReady = false, + map* filterChunkMap_ptr = nullptr + ); + + bool leastSquareInitStates( + Trace& trace, + KFMeas& kfMeas, + const string& suffix, + bool initCovars = false, + bool innovReady = false, + bool skipLsqCheck = false + ); + + VectorXd getSubState( + map& kfKeyMap, + MatrixXd* covarMat_ptr = nullptr, + VectorXd* adjustVec_ptr = nullptr + ) const; + + void getSubState(map& kfKeyMap, KFState& kfState) const; + + KFState getSubState(vector types, KFMeas* meas_ptr = nullptr) const; + + vector decomposedStateKeys(const KFKey& composedKey) const; }; /** Object to hold an individual measurement. -* Includes the measurement itself, (or its innovation) and design matrix entries -* Adding design matrix entries for states that do not yet exist will create and add new states to the measurement's kalman filter object. -*/ + * Includes the measurement itself, (or its innovation) and design matrix entries + * Adding design matrix entries for states that do not yet exist will create and add new states to + * the measurement's kalman filter object. + */ struct KFMeasEntry { - KFState* kfState_ptr = nullptr; ///< Pointer to filter object that measurements are referencing - const KFState* constKfState_ptr = nullptr; ///< Pointer to filter object that measurements are referencing - - double valid = true; ///< Optional parameter to invalidate a measurement (to avoid needing to delete it and reshuffle a vector) - double value = 0; ///< Value of measurement (for linear systems) - double noise = 0; ///< Noise of measurement - double innov = 0; ///< Innovation of measurement (for non-linear systems) - KFKey obsKey = {}; ///< Optional labels to be used in output traces - - map componentsMap; - - map noiseElementMap; - map designEntryMap; - map usedValueMap; - map noiseEntryMap; - map metaDataMap; - - KFMeasEntry( - KFState* kfState_ptr, - KFKey obsKey = {}) - : kfState_ptr (kfState_ptr), - obsKey (obsKey) - { - - } - - KFMeasEntry( - const KFState* constKfState_ptr, - KFKey obsKey = {}) - : constKfState_ptr (constKfState_ptr), - obsKey (obsKey) - { - - } - - KFMeasEntry() - { - - } - - /** Adds a noise element for this measurement - */ - void addNoiseEntry( - const KFKey kfKey, ///< Key to determine the origin of the noise - const double value, ///< Noise entry matrix entry value - const double variance) ///< Variance of noise element - { - if ( value == 0 - ||variance <= 0) - { - return; - } - - noiseElementMap [kfKey] = variance; - noiseEntryMap [kfKey] += value; - } - - /** Adds a design matrix entry for this measurement - */ - void addDsgnEntry( - const KFKey& kfKey, ///< Key to determine which state parameter is affected - const double value, ///< Design matrix entry value - const InitialState& initialState = {}) ///< Initial conditions for new states - { - if (value == 0) - { - return; - } - - if (initialState.Q < 0) - { - addNoiseEntry(kfKey, value, initialState.P); - return; - } - - bool retval = false; - if (kfState_ptr) { kfState_ptr ->addKFState(kfKey, initialState); } - if (constKfState_ptr) { constKfState_ptr->addKFState(kfKey, initialState); } - - usedValueMap [kfKey] = initialState.x; - designEntryMap [kfKey] += value; - } - - /** Adds the measurement noise entry for this measurement - */ - void setNoise( - const double value) ///< Measurement noise matrix entry value - { - if (value == 0) - { - std::cout << "Zero noise encountered" << "\n"; -// return; - } - if (std::isinf(value)) - { - std::cout << "Inf noise encountered" << "\n"; - return; - } - else if (std::isnan(value)) - { - std::cout << "Nan noise encountered" << "\n"; - return; - } - - this->noise = value; - } - - /** Adds the actual measurement value for this measurement - */ - void setValue( - const double value) ///< Actual measurement entry value - { - this->value = value; - } - - /** Adds the innovation value for this measurement - */ - void setInnov( - const double value) ///< Innovation entry value - { - this->innov = value; - } + KFState* kfState_ptr = nullptr; ///< Pointer to filter object that measurements are referencing + + double valid = true; ///< Optional parameter to invalidate a measurement (to avoid needing to + ///< delete it and reshuffle a vector) + double value = 0; ///< Value of measurement (for linear systems) + double noise = 0; ///< Noise of measurement + double innov = 0; ///< Innovation of measurement (for non-linear systems) + KFKey obsKey = {}; ///< Optional labels to be used in output traces + + map componentsMap; + + map noiseElementMap; + map designEntryMap; + map usedValueMap; + map noiseEntryMap; + map metaDataMap; + + KFMeasEntry(KFState* kfState_ptr, KFKey obsKey = {}) : kfState_ptr(kfState_ptr), obsKey(obsKey) + { + } + + KFMeasEntry() {} + + /** Adds a noise element for this measurement + */ + void addNoiseEntry( + const KFKey kfKey, ///< Key to determine the origin of the noise + const double value, ///< Noise entry matrix entry value + const double variance ///< Variance of noise element + ) + { + if (value == 0 || variance <= 0) + { + return; + } + + noiseElementMap[kfKey] = variance; + noiseEntryMap[kfKey] += value; + } + + /** Adds a design matrix entry for this measurement + */ + void addDsgnEntry( + const KFKey& kfKey, ///< Key to determine which state parameter is affected + const double value, ///< Design matrix entry value + const InitialState& initialState = {} ///< Initial conditions for new states + ) + { + if (value == + 0) // Eugene: Design entry value can be 0 in theory but still needs intial state? + { + return; + } + + if (initialState.Q < 0) + { + addNoiseEntry(kfKey, value, initialState.P); + return; + } + + if (kfState_ptr) + { + auto& kfState = *kfState_ptr; + + kfState.addKFState(kfKey, initialState); + + auto it = kfState.outageLimitMap.find(kfKey); + if (it != kfState.outageLimitMap.end()) + { + auto& [editKey, dummy] = *it; + + editKey.estimatedTime = kfState.time; + } + } + + usedValueMap[kfKey] = initialState.x; + designEntryMap[kfKey] += value; + } + + /** Adds the measurement noise entry for this measurement + */ + void setNoise(const double value) ///< Measurement noise matrix entry value + { + if (value == 0) + { + std::cout << "Zero noise encountered" << "\n"; + // return; + } + if (std::isinf(value)) + { + std::cout << "Inf noise encountered" << "\n"; + return; + } + else if (std::isnan(value)) + { + std::cout << "Nan noise encountered" << "\n"; + return; + } + + this->noise = value; + } + + /** Adds the actual measurement value for this measurement + */ + void setValue(const double value) ///< Actual measurement entry value + { + this->value = value; + } + + /** Adds the innovation value for this measurement + */ + void setInnov(const double value) ///< Innovation entry value + { + this->innov = value; + } }; +KFState mergeFilters(const vector& kfStatePointerList, const vector& stateList); -KFState mergeFilters( - const vector& kfStatePointerList, - const vector& stateList); - -MatrixXi correlationMatrix( - MatrixXd& P); +MatrixXi correlationMatrix(MatrixXd& P); void outputResiduals( - Trace& trace, - KFMeas& kfMeas, - int iteration, - string suffix, - int begH, - int numH); - - -bool isPositiveSemiDefinite( - MatrixXd& mat); - -int filter_(const double *x, const double *P, const double *H, - const double *v, const double *R, int n, int m, - double *xp, double *Pp); + Trace& trace, + KFMeas& kfMeas, + string suffix = "", + int iteration = -1, + int begH = 0, + int numH = -1 +); + +bool isPositiveSemiDefinite(MatrixXd& mat); + +int filter_( + const double* x, + const double* P, + const double* H, + const double* v, + const double* R, + int n, + int m, + double* xp, + double* Pp +); // matrix and vector functions -double *mat (int n, int m); -int *imat (int n, int m); -double *zeros(int n, int m); -double *eye (int n); -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); -int matinv(double *A, int n); -int solve (const char *tr, const double *A, const double *Y, int n, - int m, double *X); - - +double* mat(int n, int m); +int* imat(int n, int m); +double* zeros(int n, int m); +double* eye(int n); +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 + ); +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 ae656f6ab..8b1328b3e 100644 --- a/src/cpp/common/algebraTrace.cpp +++ b/src/cpp/common/algebraTrace.cpp @@ -1,205 +1,242 @@ - -#include -#include -#include -#include - -using std::map; - -#include "architectureDocs.hpp" -#include "eigenIncluder.hpp" -#include "algebraTrace.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "algebra.hpp" - +#include "common/algebraTrace.hpp" #include #include +#include #include #include #include -#include +#include +#include +#include +#include +#include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/receiver.hpp" +using std::map; /** Save complex variables to files. * * This allows for complex variables including full filter states to be stored to file. * * When binary archives are written, first some metadata about the next segment of data is stored. - * An enumerated variable type is written, so that a variable of that type may be prepared later for reading the data into. + * An enumerated variable type is written, so that a variable of that type may be prepared later for + * reading the data into. * * After the binary data, an entry with the position of the metadata block for that data is written. - * This allows for the file to be read in reverse order, seeking backward from the length metadata to the type metadata, before reading the subsequent actual data. - * This is of fundamental importance for RTS_Smoothing__(), and the primary use-case of the binary archive in Ginan. + * This allows for the file to be read in reverse order, seeking backward from the length metadata + * to the type metadata, before reading the subsequent actual data. This is of fundamental + * importance for RTS_Smoothing__(), and the primary use-case of the binary archive in Ginan. * - * The binary archive is also used as storage for debugging functions such as mincon_only, but without the necessity for reverse reading + * The binary archive is also used as storage for debugging functions such as mincon_only, but + * without the necessity for reverse reading */ -Architecture Binary_Archive__() -{ - -} - +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 + long int& startPos, ///< Position of object + string filename ///< Path to archive file +) { - std::fstream fileStream(filename, std::ifstream::binary | std::ifstream::in); - - if (!fileStream) - { - return E_SerialObject::NONE; - } - - binary_iarchive serial(fileStream, 1); //no header - - long int itemDelta; - - fileStream.seekg (0, fileStream.end); - long int fileSize = fileStream.tellg(); - - if (startPos < 0) { fileStream.seekg( -sizeof(itemDelta), fileStream.end); } - else { fileStream.seekg(startPos -sizeof(itemDelta), fileStream.beg); } - - long int currentPosition = fileStream.tellg(); - if ( (currentPosition >= fileSize) - ||(currentPosition < 0)) - { - return E_SerialObject::NONE; - } - - serial & itemDelta; - - long int itemPosition = currentPosition - itemDelta; - - fileStream.seekg(itemPosition, fileStream.beg); - - int typeInt; - serial & typeInt; - E_SerialObject type = E_SerialObject::_from_integral(typeInt); - - return type; + std::fstream fileStream(filename, std::ios::binary | std::ios::in); + + if (!fileStream) + { + return E_SerialObject::NONE; + } + + binary_iarchive serial(fileStream, 1); // no header + + long int itemDelta; + + fileStream.seekg(0, fileStream.end); + std::streamoff fileSize = fileStream.tellg(); + + // Check if tellg() failed (returns -1 on error) + if (fileSize < 0) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get file size for " << filename + << " (tellg() returned " << fileSize << ")"; + return E_SerialObject::NONE; + } + + // Log if file is larger than 2GB (potential issue on 32-bit systems) + if (fileSize > 2147483647LL) + { + BOOST_LOG_TRIVIAL(warning) << "RTS file size (" << fileSize << " bytes / " + << (fileSize / (1024.0 * 1024.0 * 1024.0)) + << " GB) exceeds 2GB - ensure 64-bit file I/O is supported"; + } + + if (startPos < 0) + { + fileStream.seekg(-sizeof(itemDelta), fileStream.end); + } + else + { + fileStream.seekg(startPos - sizeof(itemDelta), fileStream.beg); + } + + std::streamoff currentPosition = fileStream.tellg(); + if ((currentPosition >= fileSize) || (currentPosition < 0)) + { + BOOST_LOG_TRIVIAL(error) << "Invalid position after seek: " << currentPosition + << " (fileSize=" << fileSize << ")"; + return E_SerialObject::NONE; + } + + serial & itemDelta; + + std::streamoff itemPosition = currentPosition - itemDelta; + + fileStream.seekg(itemPosition, fileStream.beg); + + int typeInt; + serial & typeInt; + E_SerialObject type = int_to_enum(typeInt); + + return type; } -void tryPrepareFilterPointers( - KFState& kfState, - ReceiverMap& receiverMap) +void tryPrepareFilterPointers(KFState& kfState, ReceiverMap& receiverMap) { - map replacementKFIndexMap; - for (auto& [key, index] : kfState.kfIndexMap) - { - KFKey kfKey = key; - - if (kfKey.type == +KF::REC_POS) - { - //make sure all rec pos are associated with receivers - receiverMap[kfKey.str].id = kfKey.str; - } - - if ( kfKey.rec_ptr == nullptr - && kfKey.str.empty() == false) - { - auto it = receiverMap.find(kfKey.str); - if (it != receiverMap.end()) - { - auto& [id, station] = *it; - - kfKey.rec_ptr = &station; - } - } - - replacementKFIndexMap[kfKey] = index; - } - - kfState.kfIndexMap = replacementKFIndexMap; + map replacementKFIndexMap; + for (auto& [key, index] : kfState.kfIndexMap) + { + KFKey kfKey = key; + + if (kfKey.type == KF::REC_POS) + { + // make sure all rec pos are associated with receivers + receiverMap[kfKey.str].id = kfKey.str; + } + + if (kfKey.rec_ptr == nullptr && kfKey.str.empty() == false) + { + auto it = receiverMap.find(kfKey.str); + if (it != receiverMap.end()) + { + auto& [id, rec] = *it; + + kfKey.rec_ptr = &rec; + } + } + + replacementKFIndexMap[kfKey] = index; + } + + kfState.kfIndexMap = replacementKFIndexMap; } struct QueuedSpit { - shared_ptr ptr; - E_SerialObject type; - string filename; - bool valid = false; - bool available = false; + shared_ptr ptr; + E_SerialObject type; + string filename; + bool valid = false; + bool available = false; }; - -void spitQueuedToFile( - QueuedSpit& spit) +void spitQueuedToFile(QueuedSpit& spit) { - switch (spit.type) - { - default: std::cout << "ERROR: missing queued type " << spit.type; break; - case E_SerialObject::FILTER_MINUS: //fallthrough - case E_SerialObject::FILTER_PLUS: //fallthrough - case E_SerialObject::FILTER_SMOOTHED: { auto& kfState = *std::static_pointer_cast (spit.ptr); spitFilterToFile(kfState, spit.type, spit.filename); break; } - case E_SerialObject::TRANSITION_MATRIX: { auto& transitionObject = *std::static_pointer_cast (spit.ptr); spitFilterToFile(transitionObject, spit.type, spit.filename); break; } - case E_SerialObject::MEASUREMENT: { auto& kfMeas = *std::static_pointer_cast (spit.ptr); spitFilterToFile(kfMeas, spit.type, spit.filename); break; } - case E_SerialObject::METADATA: { auto& metatdata = *std::static_pointer_cast> (spit.ptr); spitFilterToFile(metatdata, spit.type, spit.filename); break; } - } + switch (spit.type) + { + default: + std::cout << "ERROR: missing queued type " << spit.type; + break; + case E_SerialObject::FILTER_MINUS: // fallthrough + case E_SerialObject::FILTER_PLUS: // fallthrough + case E_SerialObject::FILTER_SMOOTHED: + { + auto& kfState = *std::static_pointer_cast(spit.ptr); + spitFilterToFile(kfState, spit.type, spit.filename); + break; + } + case E_SerialObject::TRANSITION_MATRIX: + { + auto& transitionObject = *std::static_pointer_cast(spit.ptr); + spitFilterToFile(transitionObject, spit.type, spit.filename); + break; + } + case E_SerialObject::MEASUREMENT: + { + auto& kfMeas = *std::static_pointer_cast(spit.ptr); + spitFilterToFile(kfMeas, spit.type, spit.filename); + break; + } + case E_SerialObject::METADATA: + { + auto& metatdata = *std::static_pointer_cast>(spit.ptr); + spitFilterToFile(metatdata, spit.type, spit.filename); + break; + } + } } -list spitQueue; -std::mutex spitQueueMutex; -bool spitQueueRunning = false; +list spitQueue; +std::mutex spitQueueMutex; +bool spitQueueRunning = false; void spitQueueRun() { - BOOST_LOG_TRIVIAL(debug) << "Running rts queue thread"; + BOOST_LOG_TRIVIAL(debug) << "Running rts queue thread"; - while (1) - { - QueuedSpit* spit_ptr; + while (1) + { + QueuedSpit* spit_ptr; - //use pointer and braces to limit guard scope - { - lock_guard guard(spitQueueMutex); + // use pointer and braces to limit guard scope + { + lock_guard guard(spitQueueMutex); - if (spitQueue.empty()) - { - break; - } + if (spitQueue.empty()) + { + break; + } - BOOST_LOG_TRIVIAL(debug) << "RTS queue has " << spitQueue.size() << " entries to go"; + BOOST_LOG_TRIVIAL(debug) << "RTS queue has " << spitQueue.size() << " entries to go"; - spit_ptr = &spitQueue.front(); - } + spit_ptr = &spitQueue.front(); + } - spitQueuedToFile(*spit_ptr); + spitQueuedToFile(*spit_ptr); - { - lock_guard guard(spitQueueMutex); + { + lock_guard guard(spitQueueMutex); - spitQueue.pop_front(); - } - } + spitQueue.pop_front(); + } + } - lock_guard guard(spitQueueMutex); - spitQueueRunning = false; + lock_guard guard(spitQueueMutex); + spitQueueRunning = false; } void spitFilterToFileQueued( - shared_ptr& object_ptr, ///< Object to output - E_SerialObject type, ///< Type of object - string filename) ///< Path to file to output to + shared_ptr& object_ptr, ///< Object to output + E_SerialObject type, ///< Type of object + string filename ///< Path to file to output to +) { - QueuedSpit spit; + QueuedSpit spit; - spit.ptr = object_ptr; - spit.type = type; - spit.filename = filename; + spit.ptr = object_ptr; + spit.type = type; + spit.filename = filename; - lock_guard guard(spitQueueMutex); + lock_guard guard(spitQueueMutex); - spitQueue.push_back(std::move(spit)); + spitQueue.push_back(std::move(spit)); - if (spitQueueRunning == false) - { - spitQueueRunning = true; + if (spitQueueRunning == false) + { + spitQueueRunning = true; - std::thread(spitQueueRun).detach(); - } + std::thread(spitQueueRun).detach(); + } } diff --git a/src/cpp/common/algebraTrace.hpp b/src/cpp/common/algebraTrace.hpp index 9f1642993..225b54e18 100644 --- a/src/cpp/common/algebraTrace.hpp +++ b/src/cpp/common/algebraTrace.hpp @@ -1,242 +1,248 @@ - #pragma once -#include -#include -#include -#include -#include -#include -#include - -using std::make_shared; -using std::shared_ptr; -using std::vector; -using std::string; -using std::pair; -using std::map; - -#include #include #include +#include #include #include #include #include - -#include "enums.h" - +#include +#include +#include +#include +#include +#include +#include #include "architectureDocs.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" + +using boost::archive::binary_iarchive; +using boost::archive::binary_oarchive; +using boost::serialization::serialize; +using std::make_shared; +using std::map; +using std::pair; +using std::shared_ptr; +using std::string; +using std::vector; struct ReceiverMap; struct KFState; /** Types of objects that are stored in kalman filter binary archives -*/ -BETTER_ENUM(E_SerialObject, int, - NONE, - FILTER_MINUS, - FILTER_PLUS, - FILTER_SMOOTHED, - TRANSITION_MATRIX, - NAVIGATION_DATA, - STRING, - MEASUREMENT, - METADATA -) + */ +enum class E_SerialObject : int +{ + NONE, + FILTER_MINUS, + FILTER_PLUS, + FILTER_SMOOTHED, + TRANSITION_MATRIX, + NAVIGATION_DATA, + STRING, + MEASUREMENT, + METADATA +}; struct TransitionMatrixObject { - map, double> forwardTransitionMap; - int rows; - int cols; - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - ar & forwardTransitionMap; - ar & rows; - ar & cols; - } - - TransitionMatrixObject() - { - - } - - TransitionMatrixObject( - const MatrixXd& rhs) - { - forwardTransitionMap.clear(); - rows = rhs.rows(); - cols = rhs.cols(); - - for (int row = 0; row < rhs.rows(); row++) - for (int col = 0; col < rhs.cols(); col++) - { - double transition = rhs(row,col); - - if (transition == 0) - { - continue; - } - - forwardTransitionMap[{row, col}] = transition; - } - } - - TransitionMatrixObject( - const SparseMatrix& rhs) - { - forwardTransitionMap.clear(); - rows = rhs.rows(); - cols = rhs.cols(); - - for (int k = 0; k < rhs.outerSize(); ++k) - for (Eigen::SparseMatrix::InnerIterator it(rhs, k); it; ++it) - { - double transition = it.value(); - - if (transition == 0) - { - continue; - } - - forwardTransitionMap[{it.row(), it.col()}] = transition; - } - } - - MatrixXd asMatrix() - { - MatrixXd transition = MatrixXd::Zero(rows, cols); - - for (auto& [keyPair, value] : forwardTransitionMap) - { - transition(keyPair.first, keyPair.second) = value; - } - - return transition; - } + map, double> forwardTransitionMap; + int rows; + int cols; + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + ar & forwardTransitionMap; + ar & rows; + ar & cols; + } + + TransitionMatrixObject() {} + + TransitionMatrixObject(const MatrixXd& rhs) + { + forwardTransitionMap.clear(); + rows = rhs.rows(); + cols = rhs.cols(); + + for (int row = 0; row < rhs.rows(); row++) + for (int col = 0; col < rhs.cols(); col++) + { + double transition = rhs(row, col); + + if (transition == 0) + { + continue; + } + + forwardTransitionMap[{row, col}] = transition; + } + } + + TransitionMatrixObject(const SparseMatrix& rhs) + { + forwardTransitionMap.clear(); + rows = rhs.rows(); + cols = rhs.cols(); + + for (int k = 0; k < rhs.outerSize(); ++k) + for (Eigen::SparseMatrix::InnerIterator it(rhs, k); it; ++it) + { + double transition = it.value(); + + if (transition == 0) + { + continue; + } + + forwardTransitionMap[{it.row(), it.col()}] = transition; + } + } + + MatrixXd asMatrix() + { + MatrixXd transition = MatrixXd::Zero(rows, cols); + + for (auto& [keyPair, value] : forwardTransitionMap) + { + transition(keyPair.first, keyPair.second) = value; + } + + return transition; + } }; -using boost::serialization::serialize; -using boost::archive::binary_oarchive; -using boost::archive::binary_iarchive; - - -void spitFilterToFileQueued( - shared_ptr& object_ptr, - E_SerialObject type, - string filename); +void spitFilterToFileQueued(shared_ptr& object_ptr, E_SerialObject type, string filename); /** Output filter state to a file for later reading. - * Uses a binary archive which requires all of the relevant class members to have serialization functions written. - * Output format is TypeId, ObjectData, NumBytes - this allows seeking backward from the end of the file to the beginning of each object. -*/ -template + * Uses a binary archive which requires all of the relevant class members to have serialization + * functions written. Output format is TypeId, ObjectData, NumBytes - this allows seeking backward + * from the end of the file to the beginning of each object. + */ +template void spitFilterToFile( - TYPE& object, ///< Object to output - E_SerialObject type, ///< Type of object - string filename, ///< Path to file to output to - bool queue = false) ///< Optionally queue outputs in a separate thread + TYPE& object, ///< Object to output + E_SerialObject type, ///< Type of object + string filename, ///< Path to file to output to + bool queue = false ///< Optionally queue outputs in a separate thread +) { - DOCS_REFERENCE(Binary_Archive__); + DOCS_REFERENCE(Binary_Archive__); - if (queue) - { - shared_ptr copy_ptr = make_shared(object); + if (queue) + { + shared_ptr copy_ptr = make_shared(object); - spitFilterToFileQueued(copy_ptr, type, filename); + spitFilterToFileQueued(copy_ptr, type, filename); - return; - } + return; + } - try - { - std::fstream fileStream(filename, std::ifstream::binary | std::ifstream::out | std::ifstream::app); + try + { + std::fstream fileStream(filename, std::ios::binary | std::ios::out | std::ios::app); - if (!fileStream) - { - std::cout << "\n" << "Error opening algebra file '" << filename << "' for writing"; - return; - } + if (!fileStream) + { + std::cout << "\n" + << "Error opening algebra file '" << filename << "' for writing"; + return; + } - // std::cout << "RTS - writing " << type._to_string() << " to file " << filename << "\n"; + // std::cout << "RTS - writing " << enum_to_string(type) << " to file " << filename << + // "\n"; - binary_oarchive serial(fileStream, 1); //no header + binary_oarchive serial(fileStream, 1); // no header - long int pos = fileStream.tellp(); + // 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(); - int type_int = type; - serial & type_int; - serial & object; + int type_int = static_cast(type); + serial & type_int; + serial & object; - long int end = fileStream.tellp(); - long int delta = end - pos; - serial & delta; - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) << "Error: Writing to " << filename << " failed, drive may be full"; - } + long int end = fileStream.tellp(); + long int delta = end - pos; + serial & delta; + } + catch (...) + { + BOOST_LOG_TRIVIAL(error) << "Writing to " << filename << " failed, drive may be full"; + } } /* Retrieve an object from an archive -*/ -template + */ +template 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 + 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::fstream fileStream(filename, std::ifstream::binary | std::ifstream::in); + std::fstream fileStream(filename, std::ios::binary | std::ios::in); - if (!fileStream) - { - std::cout << "\n" << "Error opening algebra file " << filename << "for reading"; - return false; - } + if (!fileStream) + { + std::cout << "\n" + << "Error opening algebra file " << filename << "for reading"; + return false; + } - binary_iarchive serial(fileStream, 1); //no header + binary_iarchive serial(fileStream, 1); // no header - long int itemDelta; + long int itemDelta; - if (startPos < 0) { fileStream.seekg( -sizeof(itemDelta), fileStream.end); } - else { fileStream.seekg(startPos -sizeof(itemDelta), fileStream.beg); } + if (startPos < 0) + { + fileStream.seekg(-sizeof(itemDelta), fileStream.end); + } + else + { + fileStream.seekg(startPos - sizeof(itemDelta), fileStream.beg); + } - long int currentPosition = fileStream.tellg(); + std::streamoff currentPosition = fileStream.tellg(); - serial & itemDelta; + if (currentPosition < 0) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get position in file " << filename + << " (tellg() returned " << currentPosition << ")"; + return false; + } - long int itemPosition = currentPosition - itemDelta; + serial & itemDelta; - fileStream.seekg(itemPosition, fileStream.beg); + std::streamoff itemPosition = currentPosition - itemDelta; - int typeInt; - serial & typeInt; + fileStream.seekg(itemPosition, fileStream.beg); - E_SerialObject type = E_SerialObject::_from_integral(typeInt); - if (type != expectedType) - { - std::cout << "\n" << "Error: Unexpected algebra file object type"; - return false; - } + int typeInt; + serial & typeInt; - serial & object; + E_SerialObject type = int_to_enum(typeInt); + if (type != expectedType) + { + std::cout << "\n" + << "Error: Unexpected algebra file object type"; + return false; + } - startPos = itemPosition; + serial & object; - return true; -} + startPos = itemPosition; -E_SerialObject getFilterTypeFromFile( - long int& startPos, - string filename); + return true; +} +E_SerialObject getFilterTypeFromFile(long int& startPos, string filename); -void tryPrepareFilterPointers( - KFState& kfState, - ReceiverMap& receiverMap); +void tryPrepareFilterPointers(KFState& kfState, ReceiverMap& receiverMap); extern bool spitQueueRunning; diff --git a/src/cpp/common/algebra_old.cpp b/src/cpp/common/algebra_old.cpp index 582c81dce..f0ef5a056 100644 --- a/src/cpp/common/algebra_old.cpp +++ b/src/cpp/common/algebra_old.cpp @@ -1,425 +1,543 @@ - - #include #include +#include "common/acsQC.hpp" +#include "common/algebra.hpp" +#include "common/algebraTrace.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/trace.hpp" using std::pair; - -#include "eigenIncluder.hpp" -#include "algebraTrace.hpp" -#include "testUtils.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "acsQC.hpp" -#include "trace.hpp" - [[deprecated]] -double *mat(int n, int m) +double* mat(int n, int m) { - double *p; - - if (n<=0||m<=0) return NULL; - if (!(p=(double *)malloc(sizeof(double)*n*m))) { - } - return p; + double* p; + + if (n <= 0 || m <= 0) + return NULL; + if (!(p = (double*)malloc(sizeof(double) * n * m))) + { + } + return p; } [[deprecated]] -int *imat(int n, int m) +int* imat(int n, int m) { - int *p; - - if (n<=0||m<=0) return NULL; - if (!(p=(int *)malloc(sizeof(int)*n*m))) { - } - return p; + int* p; + + if (n <= 0 || m <= 0) + return NULL; + if (!(p = (int*)malloc(sizeof(int) * n * m))) + { + } + return p; } [[deprecated]] -double *zeros(int n, int m) +double* zeros(int n, int m) { - double *p; - - if (n<=0||m<=0) return NULL; - if (!(p=(double *)calloc(sizeof(double),n*m))) { - } - return p; + double* p; + + if (n <= 0 || m <= 0) + return NULL; + if (!(p = (double*)calloc(sizeof(double), n * m))) + { + } + return p; } [[deprecated]] -double *eye(int n) +double* eye(int n) { - double *p; - int i; + double* p; + int i; - if ((p=zeros(n,n))) for (i=0;i=0) c+=a[n]*b[n]; - return c; + while (--n >= 0) + c += a[n] * b[n]; + return c; } [[deprecated]] -double norm(const double *a, int n) +double norm(const double* a, int n) { - return sqrt(dot(a,a,n)); + return sqrt(dot(a, a, n)); } [[deprecated]] -void matcpy(double *A, const double *B, int n, int m) +void matcpy(double* A, const double* B, int n, int m) { - memcpy(A,B,sizeof(double)*n*m); + memcpy(A, B, sizeof(double) * n * m); } -#ifdef LAPACK +#ifdef LAPACK [[deprecated]] -void matmul(const char *tr, int n, int k, int m, double alpha, - const double *A, const double *B, double beta, double *C) +void matmul( + const char* tr, + int n, + int k, + int m, + double alpha, + const double* A, + const double* B, + double beta, + double* C +) { - int lda=tr[0]=='T'?m:n,ldb=tr[1]=='T'?k:m; - - dgemm_((char *)tr,(char *)tr+1,&n,&k,&m,&alpha,(double *)A,&lda,(double *)B, - &ldb,&beta,C,&n); + int lda = tr[0] == 'T' ? m : n, ldb = tr[1] == 'T' ? k : m; + + dgemm_( + (char*)tr, + (char*)tr + 1, + &n, + &k, + &m, + &alpha, + (double*)A, + &lda, + (double*)B, + &ldb, + &beta, + C, + &n + ); } [[deprecated]] -int matinv(double *A, int n) +int matinv(double* A, int n) { - double *work; - int info,lwork=n*16,*ipiv=imat(n,1); - - work=mat(lwork,1); - dgetrf_(&n,&n,A,&n,ipiv,&info); - if (!info) dgetri_(&n,A,&n,ipiv,work,&lwork,&info); - free(ipiv); free(work); - return info; + double* work; + int info, lwork = n * 16, *ipiv = imat(n, 1); + + work = mat(lwork, 1); + dgetrf_(&n, &n, A, &n, ipiv, &info); + if (!info) + dgetri_(&n, A, &n, ipiv, work, &lwork, &info); + free(ipiv); + free(work); + return info; } /* solve linear equation ------------------------------------------------------- -* solve linear equation (X=A\Y or X=A'\Y) -* args : char *tr I transpose flag ("N":normal,"T":transpose) -* double *A I input matrix A (n x n) -* double *Y I input matrix Y (n x m) -* int n,m I size of matrix A,Y -* double *X O X=A\Y or X=A'\Y (n x m) -* return : status (0:ok,0>:error) -* notes : matirix stored by column-major order (fortran convention) -* X can be same as Y -*-----------------------------------------------------------------------------*/ + * solve linear equation (X=A\Y or X=A'\Y) + * args : char *tr I transpose flag ("N":normal,"T":transpose) + * double *A I input matrix A (n x n) + * double *Y I input matrix Y (n x m) + * int n,m I size of matrix A,Y + * double *X O X=A\Y or X=A'\Y (n x m) + * return : status (0:ok,0>:error) + * notes : matirix stored by column-major order (fortran convention) + * X can be same as Y + *-----------------------------------------------------------------------------*/ [[deprecated]] -int solve(const char *tr, const double *A, const double *Y, int n, - int m, double *X) +int solve(const char* tr, const double* A, const double* Y, int n, int m, double* X) { - double *B=mat(n,n); - int info,*ipiv=imat(n,1); - - matcpy(B,A,n,n); - matcpy(X,Y,n,m); - dgetrf_(&n,&n,B,&n,ipiv,&info); - if (!info) dgetrs_((char *)tr,&n,&m,B,&n,ipiv,X,&n,&info); - free(ipiv); free(B); - return info; + double* B = mat(n, n); + int info, *ipiv = imat(n, 1); + + matcpy(B, A, n, n); + matcpy(X, Y, n, m); + dgetrf_(&n, &n, B, &n, ipiv, &info); + if (!info) + dgetrs_((char*)tr, &n, &m, B, &n, ipiv, X, &n, &info); + free(ipiv); + free(B); + return info; } #else /* without LAPACK/BLAS or MKL */ /* multiply matrix -----------------------------------------------------------*/ [[deprecated]] -void matmul(const char *tr, int n, int k, int m, double alpha, - const double *A, const double *B, double beta, double *C) +void matmul( + const char* tr, + int n, + int k, + int m, + double alpha, + const double* A, + const double* B, + double beta, + double* C +) { - double d; - int i,j,x,f=tr[0]=='N'?(tr[1]=='N'?1:2):(tr[1]=='N'?3:4); - - for (i=0;ibig) big=tmp; - if (big>0.0) vv[i]=1.0/big; else {free(vv); return -1;} - } - for (j=0;j=big) {big=tmp; imax=i;} - } - if (j!=imax) { - for (k=0;k big) + big = tmp; + if (big > 0.0) + vv[i] = 1.0 / big; + else + { + free(vv); + return -1; + } + } + for (j = 0; j < n; j++) + { + for (i = 0; i < j; i++) + { + s = A[i + j * n]; + for (k = 0; k < i; k++) + s -= A[i + k * n] * A[k + j * n]; + A[i + j * n] = s; + } + big = 0.0; + for (i = j; i < n; i++) + { + s = A[i + j * n]; + for (k = 0; k < j; k++) + s -= A[i + k * n] * A[k + j * n]; + A[i + j * n] = s; + if ((tmp = vv[i] * fabs(s)) >= big) + { + big = tmp; + imax = i; + } + } + if (j != imax) + { + for (k = 0; k < n; k++) + { + tmp = A[imax + k * n]; + A[imax + k * n] = A[j + k * n]; + A[j + k * n] = tmp; + } + *d = -(*d); + vv[imax] = vv[j]; + } + indx[j] = imax; + if (A[j + j * n] == 0.0) + { + free(vv); + return -1; + } + if (j != n - 1) + { + tmp = 1.0 / A[j + j * n]; + for (i = j + 1; i < n; i++) + A[i + j * n] *= tmp; + } + } + free(vv); + return 0; } [[deprecated]] -void lubksb(const double *A, int n, const int *indx, double *b) +void lubksb(const double* A, int n, const int* indx, double* b) { - double s; - int i,ii=-1,ip,j; - - for (i=0;i=0) for (j=ii;j=0;i--) { - s=b[i]; for (j=i+1;j= 0) + for (j = ii; j < i; j++) + s -= A[i + j * n] * b[j]; + else if (s) + ii = i; + b[i] = s; + } + for (i = n - 1; i >= 0; i--) + { + s = b[i]; + for (j = i + 1; j < n; j++) + s -= A[i + j * n] * b[j]; + b[i] = s / A[i + i * n]; + } } [[deprecated]] -int matinv(double *A, int n) +int matinv(double* A, int n) { - double d,*B; - int i,j,*indx; - - indx=imat(n,1); B=mat(n,n); matcpy(B,A,n,n); - if (ludcmp(B,n,indx,&d)) {free(indx); free(B); return -1;} - for (j=0;j 0) - { - for (int i = 0; i < norb; i++) - { - Pp[i + i * n] += 1E6; - } - } - - matcpy(L, Pp, n, n); - - if (!matinv(Pp, n)) - { - matmul("NN", n, 1, n, 1.0, Pp, N1, 0.0, xp); - - /* chi-square testing */ - info = chiqc(trace, H, P, Z, xp, v, m, n, ind); - - /* for output */ - if (xo) - matcpy(xo, xp, n, 1); - if (Po) - matcpy(Po, Pp, n, n); - } - else - { - info = 1; - tracepdeex(1, trace, "vtpv= Warning: least-squares estimation error\n"); - } - - free(xp); - free(N); - free(N1); - free(Pp); - free(vtp); - free(L); - free(g); - free(S); - return info; + double* xp = mat(n, 1); + double* N = mat(n, m); + double* N1 = mat(n, 1); + double* Pp = mat(n, n); + double* vtp = mat(1, m); + double* L = mat(n, n); + double* g = mat(n, 1); + double* S = zeros(n, n); + int info = 0; + + /* least-squares */ + matmul("TN", n, m, m, 1, H, P, 0, N); /* H'*P */ + matmul("NN", n, n, m, 1, N, H, 0, Pp); /* H'*P*H */ + matmul("NN", n, 1, m, 1, N, Z, 0, N1); /* Nl=H'*P*Z */ + + // TODO build constraint matrix about here + /* constrain the 1st epoch LS orbit estimation, to be refined */ + if (norb > 0) + { + for (int i = 0; i < norb; i++) + { + Pp[i + i * n] += 1E6; + } + } + + matcpy(L, Pp, n, n); + + if (!matinv(Pp, n)) + { + matmul("NN", n, 1, n, 1.0, Pp, N1, 0.0, xp); + + /* chi-square testing */ + info = chiqc(trace, H, P, Z, xp, v, m, n, ind); + + /* for output */ + if (xo) + matcpy(xo, xp, n, 1); + if (Po) + matcpy(Po, Pp, n, n); + } + else + { + info = 1; + tracepdeex(1, trace, "vtpv= Warning: least-squares estimation error\n"); + } + + free(xp); + free(N); + free(N1); + free(Pp); + free(vtp); + free(L); + free(g); + free(S); + return info; } /* quality control using chi-square test --------------------------------------- -* args : File *fp I output file -* const double *H I design matrix (mxn) -* const double *P I weight matrix (mxm) -* const double *Z I observed - computed (mx1) -* const double *xp I estimated parameters (nx1) -* double *v O post-fit residual (mx1) -* int m I number of observations -* int n I number of unknowns -* -* return : 0 - no outlier, 1 - outlier detected -* ---------------------------------------------------------------------------*/ + * args : File *fp I output file + * const double *H I design matrix (mxn) + * const double *P I weight matrix (mxm) + * const double *Z I observed - computed (mx1) + * const double *xp I estimated parameters (nx1) + * double *v O post-fit residual (mx1) + * int m I number of observations + * int n I number of unknowns + * + * return : 0 - no outlier, 1 - outlier detected + * ---------------------------------------------------------------------------*/ int chiqc( - Trace& trace, - const double *H, - const double *P, - const double *Z, - const double *xp, - double *v, - int m, - int n, - int ind) + Trace& trace, + const double* H, + const double* P, + const double* Z, + const double* xp, + double* v, + int m, + int n, + int ind +) { - int info = 0; - double* vtp = mat(1, m); - double vtpv = 0; - double val; - double thres; - - matcpy(v, Z, m, 1); - - /* calculate vtpv for chi-square testing */ - matmul("NN", m, 1, n, 1, H, xp, -1, v); /* v = H*xp-v */ - matmul("TN", 1, m, m, 1, v, P, 0, vtp); /* vtpv */ - matmul("NN", 1, 1, m, 1, vtp, v, 0, &vtpv); - - const double chisqr_arr[100] = - { - /* chi-sqr(n) (alpha=0.001) */ - 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 (ind == 0) - { - val = vtpv / (m - n); + int info = 0; + double* vtp = mat(1, m); + double vtpv = 0; + double val; + double thres; + + matcpy(v, Z, m, 1); + + /* calculate vtpv for chi-square testing */ + matmul("NN", m, 1, n, 1, H, xp, -1, v); /* v = H*xp-v */ + matmul("TN", 1, m, m, 1, v, P, 0, vtp); /* vtpv */ + matmul("NN", 1, 1, m, 1, vtp, v, 0, &vtpv); + + const double chisqr_arr[100] = { + /* chi-sqr(n) (alpha=0.001) */ + 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 (ind == 0) + { + val = vtpv / (m - n); #if (1) - thres = chisqr_arr[m - n - 1] / (m - n); + thres = chisqr_arr[m - n - 1] / (m - n); #else - thres = 3; + thres = 3; #endif - } - else - { - val = vtpv / m; - thres = 35; - } - - tracepdeex(2, trace, " vtpv=%8.1f val=%8.1f thres=%6.2f %4d %4d", vtpv, val, thres, m, n); - /* chi-square validation */ - if (val > thres) - { - tracepdeex(1, trace, " detected by LOM"); //Local Overall Model - info = 1; - } - - free(vtp); - - return info; + } + else + { + val = vtpv / m; + thres = 35; + } + + tracepdeex(2, trace, " vtpv=%8.1f val=%8.1f thres=%6.2f %4d %4d", vtpv, val, thres, m, n); + /* chi-square validation */ + if (val > thres) + { + tracepdeex(1, trace, " detected by LOM"); // Local Overall Model + info = 1; + } + + free(vtp); + + return info; } - diff --git a/src/cpp/common/antenna.cpp b/src/cpp/common/antenna.cpp index 39d5a8a78..b100284aa 100644 --- a/src/cpp/common/antenna.cpp +++ b/src/cpp/common/antenna.cpp @@ -1,432 +1,458 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -/** - */ -FileType ATX__() -{ - -} - +#include "common/antenna.hpp" #include #include #include +#include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/navigation.hpp" +#include "orbprop/coordinates.hpp" using std::ifstream; -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "antenna.hpp" -#include "common.hpp" -#include "enums.h" - - -map roughFrequency = -{ - {F1, FREQ1}, - {F2, FREQ1}, - {F5, FREQ5}, - {F6, FREQ6}, - {F7, FREQ7}, - {F8, FREQ8}, - {G1, FREQ1_GLO}, - {G2, FREQ2_GLO}, - {G3, FREQ3_GLO}, - {G4, FREQ4_GLO}, - {G6, FREQ6_GLO}, - {B1, FREQ1_CMP}, - {B3, FREQ3_CMP}, - {I9, FREQ9_IRN} +/** + */ +FileType ATX__() {} + +map roughFrequency = { + {F1, FREQ1}, + {F2, FREQ1}, + {F5, FREQ5}, + {F6, FREQ6}, + {F7, FREQ7}, + {F8, FREQ8}, + {G1, FREQ1_GLO}, + {G2, FREQ2_GLO}, + {G3, FREQ3_GLO}, + {G4, FREQ4_GLO}, + {G6, FREQ6_GLO}, + {B1, FREQ1_CMP}, + {B3, FREQ3_CMP}, + {I9, FREQ9_IRN} }; /* decode antenna field */ -int decodef(char *p, int n, double *v) +int decodef(char* p, int n, double* v) { - int i; - for (i = 0; i < n; i++) - v[i] = 0; - - for (i = 0, p = strtok(p," "); p && i < n; p = strtok(nullptr, " ")) - { - v[i] = atof(p) * 1E-3; - i++; - } - return i; + int i; + for (i = 0; i < n; i++) + v[i] = 0; + + for (i = 0, p = strtok(p, " "); p && i < n; p = strtok(nullptr, " ")) + { + v[i] = atof(p) * 1E-3; + i++; + } + return i; } bool findAntenna( - string code, - E_Sys sys, - GTime time, - Navigation& nav, - E_FType ft, - PhaseCenterData** pcd_ptr_ptr) + string code, + E_Sys sys, + GTime time, + Navigation& nav, + E_FType ft, + PhaseCenterData** pcd_ptr_ptr +) { -// BOOST_LOG_TRIVIAL(debug) -// << "Searching for " << type << ", " << code; + // BOOST_LOG_TRIVIAL(debug) + // << "Searching for " << type << ", " << code; - auto it0 = nav.pcvMap.find(code); - if (it0 == nav.pcvMap.end()) - { - return false; - } + auto it0 = nav.pcvMap.find(code); + if (it0 == nav.pcvMap.end()) + { + return false; + } - auto& [dummyCode, pcvSysFreqMap] = *it0; + auto& [dummyCode, pcvSysFreqMap] = *it0; - auto it1 = pcvSysFreqMap.find(sys); - if (it1 == pcvSysFreqMap.end()) - { - return false; - } + auto it1 = pcvSysFreqMap.find(sys); + if (it1 == pcvSysFreqMap.end()) + { + return false; + } - auto& [dummySys, pcvFreqMap] = *it1; + auto& [dummySys, pcvFreqMap] = *it1; - auto it2 = pcvFreqMap.find(ft); - if (it2 == pcvFreqMap.end()) - { - return false; - } + auto it2 = pcvFreqMap.find(ft); + if (it2 == pcvFreqMap.end()) + { + return false; + } - auto& [dummy2, pcvTimeMap] = *it2; + auto& [dummy2, pcvTimeMap] = *it2; - auto it3 = pcvTimeMap.lower_bound(time); - if (it3 == pcvTimeMap.end()) - { - //just use the first chronologically, (last when sorted as they are) instead - auto it4 = pcvTimeMap.rbegin(); + auto it3 = pcvTimeMap.lower_bound(time); + if (it3 == pcvTimeMap.end()) + { + // just use the first chronologically, (last when sorted as they are) instead + auto it4 = pcvTimeMap.rbegin(); - auto& [dummyTime, pcd] = *it4; + auto& [dummyTime, pcd] = *it4; - if (pcd_ptr_ptr) - *pcd_ptr_ptr = &pcd; + if (pcd_ptr_ptr) + *pcd_ptr_ptr = &pcd; - return true; - } + return true; + } - auto& [dummyTime, pcd] = *it3; + auto& [dummyTime, pcd] = *it3; - if (pcd_ptr_ptr) - *pcd_ptr_ptr = &pcd; + if (pcd_ptr_ptr) + *pcd_ptr_ptr = &pcd; - return true; + return true; } /** linearly interpolate */ -template +template TYPE interp(double x1, double x2, TYPE y1, TYPE y2, double x) { - return y2-(y2-y1)*(x2-x)/(x2-x1); + return y2 - (y2 - y1) * (x2 - x) / (x2 - x1); } - -Vector3d makeAntPco( - string id, - E_Sys sys, - E_FType ftx, - GTime time, - double& var, - E_Radio radio) +Vector3d makeAntPco(string id, E_Sys sys, E_FType ftx, GTime time, double& var, E_Radio radio) { - auto& pcoFreqMap = nav.pcoMap[id][sys]; - - if (pcoFreqMap.empty()) - return Vector3d::Zero(); - - if (roughFrequency.find(ftx) == roughFrequency.end()) - return Vector3d::Zero(); - - double lamX = CLIGHT / roughFrequency[ftx]; - - Vector3d pco1 = Vector3d::Zero(); - Vector3d pco2 = Vector3d::Zero(); - double lam1 = 0; - double lam2 = 0; - - for (auto& [ft, pcoFreq] : pcoFreqMap) - { - if (roughFrequency.find(ft) == roughFrequency.end()) - continue; - - double lam = CLIGHT / roughFrequency[ft]; - - double var; - Vector3d pco = antPco(id, sys, ft, time, var, radio); - - if (pco.isZero()) - continue; - - if (lam1 == 0) { lam1 = lam; pco1 = pco; } - else if (lam2 == 0) { lam2 = lam; pco2 = pco; } - else - { - double close1 = fabs(lam1 - lamX) - fabs(lam - lamX); - double close2 = fabs(lam2 - lamX) - fabs(lam - lamX); - - if (close1 > close2 && close1 > 0) { lam1 = lam; pco1 = pco; } - if (close2 > close1 && close2 > 0) { lam2 = lam; pco2 = pco; } - } - } - - if (lam1 == 0) - return Vector3d::Zero(); - - var = 0; - - if ( lam2 == 0 - || lam1 == lam2) - { - return pco1; - } - - double k32 = (lamX-lam2)/(lam1-lam2); - double k31 = (lamX-lam1)/(lam1-lam2); - - Vector3d pco3 = k32 * pco1 - - k31 * pco2; - return pco3; + auto& pcoFreqMap = nav.pcoMap[id][sys]; + + if (pcoFreqMap.empty()) + return Vector3d::Zero(); + + if (roughFrequency.find(ftx) == roughFrequency.end()) + return Vector3d::Zero(); + + double lamX = CLIGHT / roughFrequency[ftx]; + + Vector3d pco1 = Vector3d::Zero(); + Vector3d pco2 = Vector3d::Zero(); + double lam1 = 0; + double lam2 = 0; + + for (auto& [ft, pcoFreq] : pcoFreqMap) + { + if (roughFrequency.find(ft) == roughFrequency.end()) + continue; + + double lam = CLIGHT / roughFrequency[ft]; + + double var; + Vector3d pco = antPco(id, sys, ft, time, var, radio); + + if (pco.isZero()) + continue; + + if (lam1 == 0) + { + lam1 = lam; + pco1 = pco; + } + else if (lam2 == 0) + { + lam2 = lam; + pco2 = pco; + } + else + { + double close1 = fabs(lam1 - lamX) - fabs(lam - lamX); + double close2 = fabs(lam2 - lamX) - fabs(lam - lamX); + + if (close1 > close2 && close1 > 0) + { + lam1 = lam; + pco1 = pco; + } + if (close2 > close1 && close2 > 0) + { + lam2 = lam; + pco2 = pco; + } + } + } + + if (lam1 == 0) + return Vector3d::Zero(); + + var = 0; + + if (lam2 == 0 || lam1 == lam2) + { + return pco1; + } + + double k32 = (lamX - lam2) / (lam1 - lam2); + double k31 = (lamX - lam1) / (lam1 - lam2); + + Vector3d pco3 = k32 * pco1 - k31 * pco2; + return pco3; } /** fetch pco */ -Vector3d antPco( - string id, - E_Sys sys, - E_FType ft, - GTime time, - double& var, - E_Radio radio, - bool interp) +Vector3d +antPco(string id, E_Sys sys, E_FType ft, GTime time, double& var, E_Radio radio, bool interp) { - auto it0 = nav.pcoMap.find(id); - if (it0 == nav.pcoMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No PCO found for '" << id << "'"; - - return Vector3d::Zero(); - } - - auto& [dummy0, pcoSysFreqMap] = *it0; - - vector testSyss = {sys}; - vector testFts = {ft}; - - if (acsConfig.auto_fill_pco) - { - testSyss.push_back(E_Sys::GPS); - testFts .push_back(F2); - } - - bool found = false; - - E_Sys foundTestSys; - for (auto testSys : testSyss) - { - auto it1 = pcoSysFreqMap.find(testSys); - if (it1 == pcoSysFreqMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No PCO found for " << id << " for " << testSys; - - continue; - } - - foundTestSys = testSys; - found = true; - break; - } - - if (found == false) - { - return Vector3d::Zero(); - } - - auto& pcoFreqMap = pcoSysFreqMap[foundTestSys]; - - found = false; - - E_FType foundTestFt; - for (auto testFt : testFts) - { - auto it2 = pcoFreqMap.find(testFt); - if (it2 == pcoFreqMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No PCO found for " << id << " for " << foundTestSys << " L" << testFt; - - if (interp == false) - continue; - - Vector3d madePCO = makeAntPco(id, foundTestSys, testFt, time, var, radio); - - if (madePCO.isZero() == false) - { - return madePCO; - } - - continue; - } - - foundTestFt = testFt; - found = true; - break; - } - - if (found == false) - { - return Vector3d::Zero(); - } - - auto& pcoTimeMap = pcoFreqMap[foundTestFt]; - - auto it3 = pcoTimeMap.lower_bound(time); - if (it3 == pcoTimeMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No PCO found for " << id << " for " << foundTestSys << " L" << foundTestFt << " at " << time; - return Vector3d::Zero(); - } - - auto& [dummy3, pco] = *it3; - - var = 0; - - if (radio == +E_Radio::TRANSMITTER) return pco.satPco; - else return pco.recPco; + if (id.empty()) + { + return Vector3d::Zero(); + } + + auto it0 = nav.pcoMap.find(id); + if (it0 == nav.pcoMap.end()) + { + BOOST_LOG_TRIVIAL(warning) << "No PCO found for '" << id << "'"; + + return Vector3d::Zero(); + } + + auto& [dummy0, pcoSysFreqMap] = *it0; + + vector testSyss = {sys}; + vector testFts = {ft}; + + if (acsConfig.auto_fill_pco) + { + testSyss.push_back(E_Sys::GPS); + testFts.push_back(F2); + } + + bool found = false; + + E_Sys foundTestSys; + for (auto testSys : testSyss) + { + auto it1 = pcoSysFreqMap.find(testSys); + if (it1 == pcoSysFreqMap.end()) + { + BOOST_LOG_TRIVIAL(warning) << "No PCO found for " << id << " for " << testSys; + + continue; + } + + foundTestSys = testSys; + found = true; + break; + } + + if (found == false) + { + return Vector3d::Zero(); + } + + auto& pcoFreqMap = pcoSysFreqMap[foundTestSys]; + + found = false; + + E_FType foundTestFt; + for (auto testFt : testFts) + { + auto it2 = pcoFreqMap.find(testFt); + if (it2 == pcoFreqMap.end()) + { + BOOST_LOG_TRIVIAL(warning) + << "No PCO found for " << id << " for " << foundTestSys << " L" << testFt; + + if (interp == false) + continue; + + Vector3d madePCO = makeAntPco(id, foundTestSys, testFt, time, var, radio); + + if (madePCO.isZero() == false) + { + return madePCO; + } + + continue; + } + + foundTestFt = testFt; + found = true; + break; + } + + if (found == false) + { + return Vector3d::Zero(); + } + + auto& pcoTimeMap = pcoFreqMap[foundTestFt]; + + auto it3 = pcoTimeMap.lower_bound(time); + if (it3 == pcoTimeMap.end()) + { + BOOST_LOG_TRIVIAL(warning) << "No PCO found for " << id << " for " << foundTestSys << " L" + << foundTestFt << " at " << time; + return Vector3d::Zero(); + } + + auto& [dummy3, pco] = *it3; + + if (pco.validUntil != GTime::noTime() && time > pco.validUntil) + { + BOOST_LOG_TRIVIAL(warning) << "No PCO found for " << id << " for " << foundTestSys << " L" + << foundTestFt << " at " << time; + return Vector3d::Zero(); + } + + var = 0; + + if (radio == E_Radio::TRANSMITTER) + return pco.satPco; + else + return pco.recPco; } /** find and interpolate antenna pcv -*/ + */ double antPcv( - string id, ///< antenna id - E_Sys sys, ///< satellite system - E_FType ft, ///< frequency - GTime time, ///< time - AttStatus& attStatus, ///< Orientation of antenna - VectorEcef e, ///< Line of sight vector - double* az_ptr, ///< Optional pointer to output antenna frame azimuth in degrees - double* zen_ptr) ///< Optional pointer to output antenna frame zenith in degrees + string id, ///< antenna id + E_Sys sys, ///< satellite system + E_FType ft, ///< frequency + GTime time, ///< time + AttStatus& attStatus, ///< Orientation of antenna + VectorEcef e, ///< Line of sight vector + double* az_ptr, ///< Optional pointer to output antenna frame azimuth in degrees + double* zen_ptr ///< Optional pointer to output antenna frame zenith in degrees +) { - // Rotate relative look vector into local frame - Matrix3d ant2Ecef = rotBasisMat(attStatus.eXAnt, attStatus.eYAnt, attStatus.eZAnt); - - Vector3d localLook = ant2Ecef.transpose() * e; - - double az = atan2(localLook(0), localLook(1)); - double zen = acos(localLook.z()) * R2D; - - wrap2Pi(az); - - az *= R2D; - - if (az_ptr) *az_ptr = az; - if (zen_ptr) *zen_ptr = zen; - - auto it0 = nav.pcvMap.find(id); - if (it0 == nav.pcvMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No PCV found for '" << id << "'"; - return 0; - } - - auto& [dummy0, pcvSysFreqMap] = *it0; - - auto it1 = pcvSysFreqMap.find(sys); - if (it1 == pcvSysFreqMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No PCV found for " << id << " for " << sys; - return 0; - } - - auto& [dummy1, pcvFreqMap] = *it1; - - auto it2 = pcvFreqMap.find(ft); - if (it2 == pcvFreqMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No PCV found for " << id << " for " << sys << " L" << ft; - return 0; - } - - auto& [dummy2, pcvTimeMap] = *it2; - - auto it3 = pcvTimeMap.lower_bound(time); - if (it3 == pcvTimeMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No PCV found for " << id << " for " << sys << " L" << ft << " at " << time; - return 0; - } - - auto& [dummy3, pcd] = *it3; - - auto& pcvMap1D = pcd.elMap; - auto& pcvMap2D = pcd.azElMap; - - int nz = pcd.nz; - int naz = pcd.naz; - double zen1 = pcd.zenStart; - double dzen = pcd.zenDelta; - double dazi = pcd.aziDelta; - - /* select zenith angle range */ - int zen_n; - for (zen_n = 1; zen_n < nz - 1; zen_n++) - { - if ((zen1 + dzen * zen_n) >= zen) - { - break; - } - } - - double xz1 = zen1 + dzen * (zen_n - 1); - double xz2 = zen1 + dzen * (zen_n); - - - double pcv; - - if ( naz == 0 - ||az == 0) - { - // linear interpolate receiver pcv - non azimuth-dependent - - double yz1 = pcvMap1D[zen_n - 1]; // lower bound - double yz2 = pcvMap1D[zen_n]; // upper bound - pcv = interp(xz1, xz2, yz1, yz2, zen); - } - else - { - // bilinear interpolate receiver pcv - azimuth-dependent - - // select azimuth angle range */ - int az_n; - for (az_n = 1; az_n < naz; az_n++) - { - if ((dazi * az_n) >= az) - { - break; - } - } - if (az_n == naz) - { - az_n = 0; - } - - double xa1 = dazi * (az_n -1); - double xa2 = dazi * (az_n); - - double yz3 = pcvMap2D[az_n-1] [zen_n-1]; double yz1 = pcvMap2D[az_n-1] [zen_n]; - double yz4 = pcvMap2D[az_n] [zen_n-1]; double yz2 = pcvMap2D[az_n] [zen_n]; - - // linear interpolation along zenith angle - double ya1 = interp(xz1, xz2, yz3, yz1, zen); - double ya2 = interp(xz1, xz2, yz4, yz2, zen); - - // linear interpolation along azimuth angle - pcv = interp(xa1, xa2, ya1, ya2, az); - } - - return pcv; + if (id.empty()) + { + return 0; + } + + // Rotate relative look vector into local frame + Matrix3d ant2Ecef = rotBasisMat(attStatus.eXAnt, attStatus.eYAnt, attStatus.eZAnt); + + Vector3d localLook = ant2Ecef.transpose() * e; + + double az = atan2(localLook(0), localLook(1)); + double zen = acos(localLook.z()) * R2D; + + wrap2Pi(az); + + az *= R2D; + + if (az_ptr) + *az_ptr = az; + if (zen_ptr) + *zen_ptr = zen; + + auto it0 = nav.pcvMap.find(id); + if (it0 == nav.pcvMap.end()) + { + BOOST_LOG_TRIVIAL(warning) << "No PCV found for '" << id << "'"; + return 0; + } + + auto& [dummy0, pcvSysFreqMap] = *it0; + + auto it1 = pcvSysFreqMap.find(sys); + if (it1 == pcvSysFreqMap.end()) + { + BOOST_LOG_TRIVIAL(warning) << "No PCV found for " << id << " for " << sys; + return 0; + } + + auto& [dummy1, pcvFreqMap] = *it1; + + auto it2 = pcvFreqMap.find(ft); + if (it2 == pcvFreqMap.end()) + { + BOOST_LOG_TRIVIAL(warning) << "No PCV found for " << id << " for " << sys << " L" << ft; + return 0; + } + + auto& [dummy2, pcvTimeMap] = *it2; + + auto it3 = pcvTimeMap.lower_bound(time); + if (it3 == pcvTimeMap.end()) + { + BOOST_LOG_TRIVIAL(warning) + << "No PCV found for " << id << " for " << sys << " L" << ft << " at " << time; + return 0; + } + + auto& [dummy3, pcd] = *it3; + + if (pcd.validUntil != GTime::noTime() && time > pcd.validUntil) + { + BOOST_LOG_TRIVIAL(warning) + << "No PCV found for " << id << " for " << sys << " L" << ft << " at " << time; + return 0; + } + + auto& pcvMap1D = pcd.elMap; + auto& pcvMap2D = pcd.azElMap; + + int nz = pcd.nz; + int naz = pcd.naz; + double zen1 = pcd.zenStart; + double dzen = pcd.zenDelta; + double dazi = pcd.aziDelta; + + /* select zenith angle range */ + int zen_n; + for (zen_n = 1; zen_n < nz - 1; zen_n++) + { + if ((zen1 + dzen * zen_n) >= zen) + { + break; + } + } + + double xz1 = zen1 + dzen * (zen_n - 1); + double xz2 = zen1 + dzen * (zen_n); + + double pcv; + + if (naz == 0 || az == 0) + { + // linear interpolate receiver pcv - non azimuth-dependent + + double yz1 = pcvMap1D[zen_n - 1]; // lower bound + double yz2 = pcvMap1D[zen_n]; // upper bound + pcv = interp(xz1, xz2, yz1, yz2, zen); + } + else + { + // bilinear interpolate receiver pcv - azimuth-dependent + + // select azimuth angle range */ + int az_n; + for (az_n = 1; az_n < naz; az_n++) + { + if ((dazi * az_n) >= az) + { + break; + } + } + if (az_n == naz) + { + az_n = 0; + } + + double xa1 = dazi * (az_n - 1); + double xa2 = dazi * (az_n); + + double yz3 = pcvMap2D[az_n - 1][zen_n - 1]; + double yz1 = pcvMap2D[az_n - 1][zen_n]; + double yz4 = pcvMap2D[az_n][zen_n - 1]; + double yz2 = pcvMap2D[az_n][zen_n]; + + // linear interpolation along zenith angle + double ya1 = interp(xz1, xz2, yz3, yz1, zen); + double ya2 = interp(xz1, xz2, yz4, yz2, zen); + + // linear interpolation along azimuth angle + pcv = interp(xa1, xa2, ya1, ya2, az); + } + + return pcv; } /**Change the last four characters of antenna type to NONE @@ -434,321 +460,354 @@ double antPcv( * This function is useful for when searching for an antenna model in ANTEX * The IGS convention is to default to NONE for the radome if the calibration value is not available */ -void radome2none( - string& antenna_type) +void radome2none(string& antenna_type) { - size_t length = antenna_type.size(); - if (length != 20) - { - printf("\n*** ERROR radome2none(): string length is less then 20 characters received %ld characters\n",length); - return; - } - - antenna_type.replace(length - 4, 4, "NONE"); + size_t length = antenna_type.size(); + if (length != 20) + { + printf( + "\n*** ERROR radome2none(): string length is less then 20 characters received %ld " + "characters\n", + length + ); + return; + } + + antenna_type.replace(length - 4, 4, "NONE"); } -map antexCodes = -{ - {"G01", F1 }, - {"G02", F2 }, - {"G05", F5 }, - {"R01", G1 }, - {"R02", G2 }, - {"R04", G4 }, - {"R06", G6 }, - {"E01", F1 }, - {"E05", F5 }, - {"E06", F6 }, - {"E07", F7 }, - {"E08", F8 }, - {"C01", F1 }, - {"C02", B1 }, - {"C05", F5 }, - {"C06", B3 }, - {"C07", F7 }, - {"C08", F8 }, - {"J01", F1 }, - {"J02", F2 }, - {"J05", F5 }, - {"J06", F6 }, - {"S01", F1 }, - {"S05", F5 }, - {"I05", F5 }, - {"I09", I9 } -}; +map antexCodes = {{"G01", F1}, {"G02", F2}, {"G05", F5}, {"R01", G1}, {"R02", G2}, + {"R04", G4}, {"R06", G6}, {"E01", F1}, {"E05", F5}, {"E06", F6}, + {"E07", F7}, {"E08", F8}, {"C01", F1}, {"C02", B1}, {"C05", F5}, + {"C06", B3}, {"C07", F7}, {"C08", F8}, {"J01", F1}, {"J02", F2}, + {"J05", F5}, {"J06", F6}, {"S01", F1}, {"S05", F5}, {"I05", F5}, + {"I09", I9}}; /** Read antex file */ -void readantexf( - string filepath, - Navigation& nav) +void readantexf(string filepath, Navigation& nav) { - DOCS_REFERENCE(ATX__); - - bool noazi_flag = false; - int num_azi_rd = 0; - int irms = 0; - - ifstream fileStream(filepath); - if (!fileStream) - { - BOOST_LOG_TRIVIAL(error) - << "Error opening antex file" << filepath << "\n"; - return; - } - - const PhaseCenterData pcv0 = {}; - PhaseCenterData recPcv; - PhaseCenterData freqPcv; - VectorEnu recPco; - Vector3d satPco = Vector3d::Zero(); - string id; - GTime time; - - E_FType ft = FTYPE_NONE; - E_Sys sys = E_Sys::NONE; - - while (fileStream) - { - string line; - - getline(fileStream, line); - - char* buff = &line[0]; - - char* comment = buff + 60; - - if (irms) - continue; - - // Read in the ANTEX header information - - if (strlen(buff) < 60 ) { continue; } - if (strstr(comment, "ANTEX VERSION / SYST")) { continue; } - if (strstr(comment, "PCV TYPE / REFANT")) { continue; } - if (strstr(comment, "COMMENT")) { continue; } - if (strstr(comment, "END OF HEADER")) { continue; } - - // Read in specific Antenna information now - - if (strstr(comment, "START OF ANTENNA")) - { - recPcv = pcv0; - freqPcv = pcv0; - recPco = Vector3d::Zero(); - satPco = Vector3d::Zero(); - id = ""; - time = GTime::noTime(); - - continue; - } -// if (strstr(comment, "END OF ANTENNA")) -// { -// GTime time = epoch2time(recPcv.tf); -// -// continue; -// } - - - if (strstr(comment, "METH / BY / # / DATE")) - { -// int num_calibrated; -// char cal_method[20]; -// char cal_agency[20]; -// char cal_date[10]; -// strncpy(cal_method, buff, 20);/* Should be CHAMBER or FIELD or ROBOT or COPIED ot CONVERTED */ -// cal_method[19] = '\0'; -// strncpy(cal_agency, buff + 20, 20); -// cal_agency[19] = '\0'; -// strncpy(tmp, buff + 40, 10); -// num_calibrated = atoi(tmp); -// strncpy(cal_date, buff + 50, 10); -// cal_date[9] = '\0'; - - continue; - } - - if (strstr(comment, "DAZI")) - { - char tmp[10]; - strncpy(tmp,buff ,8); tmp[8] = '\0'; - recPcv.aziDelta = atof(tmp); - - if (recPcv.aziDelta < 0.0001) recPcv.naz = 0; - else recPcv.naz = (360 / recPcv.aziDelta) + 1; - - continue; - } - - if (strstr(comment, "SINEX CODE")) - { - recPcv.calibModel .assign(buff, 10); - continue; - } - - if (strstr(comment, "TYPE / SERIAL NO")) - { - recPcv.type .assign(buff, 20); - recPcv.code .assign(buff+20, 20); - recPcv.svn .assign(buff+40, 4); - recPcv.cospar .assign(buff+50, 10); - - // stack antenna pco and pcv - string satId = recPcv.code; - if (satId.find_first_not_of(' ') == satId.npos) { id = recPcv.type; } - else { id = recPcv.code; } - - boost::trim_right(id); - boost::trim_right(recPcv.type); - - continue; - } - - if (strstr(comment, "ZEN1 / ZEN2 / DZEN")) - { - char tmp[10]; - strncpy(tmp, buff, 8); tmp[8] = '\0'; recPcv.zenStart = atof(tmp); - strncpy(tmp, buff+8, 7); tmp[8] = '\0'; recPcv.zenStop = atof(tmp); - strncpy(tmp, buff+16, 7); tmp[8] = '\0'; recPcv.zenDelta = atof(tmp); - - recPcv.nz = (recPcv.zenStop - recPcv.zenStart) / recPcv.zenDelta + 1 ; - - continue; - } - - if (strstr(comment, "# OF FREQUENCIES")) - { -// strncpy(tmp, buff, 8); -// tmp[8] = '\0'; -// pcv.nf = atoi(tmp); - - continue; - } - - if (strstr(comment, "VALID FROM")) - { - /* if (!str2time(buff,0,43,pcv.ts)) continue;*/ - char valid_from[44]; - strncpy(valid_from, buff, 43); valid_from[43] = '\0'; - char* p = strtok(valid_from, " "); - int j = 0; - while (p != nullptr) - { - recPcv.tf[j] = (double) atoi(p); - p = strtok(nullptr, " "); - j++; - } - - time = epoch2time(recPcv.tf); - - continue; - } - - if (strstr(comment, "VALID UNTIL")) - { - /* if (!str2time(buff,0,43,pcv.te)) continue;*/ - char valid_until[44]; - strncpy(valid_until, buff ,43); valid_until[43] = '\0'; - char* p = strtok(valid_until, " "); - int j = 0; - while (p != nullptr) - { - recPcv.tu[j] = (double) atoi(p); - p = strtok(nullptr, " "); - j++; - } - - continue; - } - - if (strstr(comment, "NORTH / EAST / UP")) // "NORTH / EAST / UP" for receiver and "X / Y / Z" for satellite - { - double neu[3]; - if (decodef(buff, 3, neu) < 3) - { - continue; - } - - recPco.n() = neu[0]; - recPco.e() = neu[1]; - recPco.u() = neu[2]; - - satPco.x() = neu[0]; - satPco.y() = neu[1]; - satPco.z() = neu[2]; - - continue; - } - - if (strstr(comment, "START OF FREQUENCY")) - { - num_azi_rd = 0; - noazi_flag = false; - - string antexFCode; - antexFCode.assign(&buff[3], 3); - - sys = SatSys::sysFromChar(antexFCode[0]); - ft = antexCodes[antexFCode]; - - freqPcv = recPcv; - - continue; - } - - if (strstr(comment, "END OF FREQUENCY")) - { - noazi_flag = false; - - nav.pcvMap[id][sys][ft][time] = freqPcv; - nav.pcoMap[id][sys][ft][time].recPco = recPco; - nav.pcoMap[id][sys][ft][time].satPco = satPco; - - if (id.size() <= 3) // filters out non-PRNS e.g. "3S-02-TSADM NONE" - { - nav.svnMap[SatSys(id.c_str())][time] = recPcv.svn; - nav.blocktypeMap[recPcv.svn] = recPcv.type; - } - - continue; - } - - if (strstr(comment, "START OF FREQ RMS")) { irms = 1; continue; } - if (strstr(comment, "END OF FREQ RMS")) { irms = 0; continue; } - - if ( irms == 0 - && strstr(buff, "NOAZI")) - { - for (int i = 0; i < recPcv.nz; i++) - { - int offset = i * 8 + 8; - char tmp[10]; - strncpy(tmp, buff + offset, 8); tmp[8]='\0'; - double pcv_val = atof(tmp); - freqPcv.elMap.push_back(pcv_val * 1e-3); - } - - noazi_flag = true; - - continue; - } - - if ( irms == 0 - && noazi_flag) - { - char tmp[10]; - strncpy(tmp, buff, 8); tmp[8]='\0'; - - for (int i = 0; i < recPcv.nz; i++) - { - int offset = i * 8 + 8; - strncpy(tmp, buff + offset, 8); tmp[8]='\0'; - double pcv_val = atof(tmp); - freqPcv.azElMap[num_azi_rd].push_back(pcv_val * 1e-3); - } - num_azi_rd++; - - continue; - } - } + DOCS_REFERENCE(ATX__); + + bool noAziLineRead = false; + int numAziLinesRead = 0; + int irms = 0; + + ifstream fileStream(filepath); + if (!fileStream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening antex file" << filepath << "\n"; + return; + } + + const PhaseCenterData pcv0 = {}; + PhaseCenterData recPcv; + PhaseCenterData freqPcv; + VectorEnu recPco; + Vector3d satPco = Vector3d::Zero(); + string id; + GTime validFrom; + GTime validUntil; + + E_FType ft = NONE; + E_Sys sys = E_Sys::NONE; + + while (fileStream) + { + string line; + + getline(fileStream, line); + + char* buff = &line[0]; + + char* comment = buff + 60; + + if (irms) + continue; + + // Read in the ANTEX header information + + if (strlen(buff) < 60) + { + continue; + } + if (strstr(comment, "ANTEX VERSION / SYST")) + { + continue; + } + if (strstr(comment, "PCV TYPE / REFANT")) + { + continue; + } + if (strstr(comment, "COMMENT")) + { + continue; + } + if (strstr(comment, "END OF HEADER")) + { + continue; + } + + // Read in specific Antenna information now + + if (strstr(comment, "START OF ANTENNA")) + { + recPcv = pcv0; + freqPcv = pcv0; + recPco = Vector3d::Zero(); + satPco = Vector3d::Zero(); + id = ""; + validFrom = GTime::noTime(); + validUntil = GTime::noTime(); + + continue; + } + // if (strstr(comment, "END OF ANTENNA")) + // { + // GTime time = epoch2time(recPcv.tf); + // + // continue; + // } + + if (strstr(comment, "METH / BY / # / DATE")) + { + // int num_calibrated; + // char cal_method[20]; + // char cal_agency[20]; + // char cal_date[10]; + // strncpy(cal_method, buff, 20);/* Should be CHAMBER or FIELD or ROBOT + // or COPIED ot CONVERTED */ cal_method[19] = '\0'; + // strncpy(cal_agency, buff + 20, 20); cal_agency[19] = '\0'; strncpy(tmp, + // buff + 40, 10); num_calibrated = atoi(tmp); + // strncpy(cal_date, buff + 50, 10); cal_date[9] = '\0'; + + continue; + } + + if (strstr(comment, "DAZI")) + { + char tmp[10]; + strncpy(tmp, buff, 8); + tmp[8] = '\0'; + recPcv.aziDelta = atof(tmp); + + if (recPcv.aziDelta < 0.0001) + recPcv.naz = 0; + else + recPcv.naz = (360 / recPcv.aziDelta) + 1; + + continue; + } + + if (strstr(comment, "SINEX CODE")) + { + recPcv.calibModel.assign(buff, 10); + continue; + } + + if (strstr(comment, "TYPE / SERIAL NO")) + { + recPcv.type.assign(buff, 20); + recPcv.code.assign(buff + 20, 20); + recPcv.svn.assign(buff + 40, 4); + recPcv.cospar.assign(buff + 50, 10); + + // stack antenna pco and pcv + string satId = recPcv.code; + if (satId.find_first_not_of(' ') == satId.npos) + { + id = recPcv.type; + } + else + { + id = recPcv.code; + } + + boost::trim_right(id); + boost::trim_right(recPcv.type); + + continue; + } + + if (strstr(comment, "ZEN1 / ZEN2 / DZEN")) + { + char tmp[10]; + strncpy(tmp, buff, 8); + tmp[8] = '\0'; + recPcv.zenStart = atof(tmp); + strncpy(tmp, buff + 8, 7); + tmp[8] = '\0'; + recPcv.zenStop = atof(tmp); + strncpy(tmp, buff + 16, 7); + tmp[8] = '\0'; + recPcv.zenDelta = atof(tmp); + + recPcv.nz = (recPcv.zenStop - recPcv.zenStart) / recPcv.zenDelta + 1; + + continue; + } + + if (strstr(comment, "# OF FREQUENCIES")) + { + // strncpy(tmp, buff, 8); + // tmp[8] = '\0'; + // pcv.nf = atoi(tmp); + + continue; + } + + if (strstr(comment, "VALID FROM")) + { + /* if (!str2time(buff,0,43,pcv.ts)) continue;*/ + char valid_from[44]; + strncpy(valid_from, buff, 43); + valid_from[43] = '\0'; + char* p = strtok(valid_from, " "); + int j = 0; + + GEpoch ep; + while (p != nullptr) + { + ep[j] = (double)atoi(p); + p = strtok(nullptr, " "); + j++; + } + + validFrom = ep; + + continue; + } + + if (strstr(comment, "VALID UNTIL")) + { + /* if (!str2time(buff,0,43,pcv.te)) continue;*/ + char valid_until[44]; + strncpy(valid_until, buff, 43); + valid_until[43] = '\0'; + char* p = strtok(valid_until, " "); + + int j = 0; + + GEpoch ep; + + while (p != nullptr) + { + ep[j] = (double)atoi(p); + p = strtok(nullptr, " "); + j++; + } + + validUntil = ep; + + continue; + } + + if (strstr( + comment, + "NORTH / EAST / UP" + )) // "NORTH / EAST / UP" for receiver and "X / Y / Z" for satellite + { + double neu[3]; + if (decodef(buff, 3, neu) < 3) + { + continue; + } + + recPco.n() = neu[0]; + recPco.e() = neu[1]; + recPco.u() = neu[2]; + + satPco.x() = neu[0]; + satPco.y() = neu[1]; + satPco.z() = neu[2]; + + continue; + } + + if (strstr(comment, "START OF FREQUENCY")) + { + numAziLinesRead = 0; + noAziLineRead = false; + + string antexFCode; + antexFCode.assign(&buff[3], 3); + + sys = SatSys::sysFromChar(antexFCode[0]); + ft = antexCodes[antexFCode]; + + freqPcv = recPcv; + + continue; + } + + if (strstr(comment, "END OF FREQUENCY")) + { + noAziLineRead = false; + + auto& pcv = nav.pcvMap[id][sys][ft][validFrom]; + auto& pco = nav.pcoMap[id][sys][ft][validFrom]; + + pcv = freqPcv; + pco.recPco = recPco; + pco.satPco = satPco; + + pcv.validUntil = validUntil; + pco.validUntil = validUntil; + + if (id.size() <= 3) // filters out non-PRNS e.g. "3S-02-TSADM NONE" + { + nav.svnMap[SatSys(id.c_str())][validFrom] = recPcv.svn; + nav.blocktypeMap[recPcv.svn] = recPcv.type; + } + + continue; + } + + if (strstr(comment, "START OF FREQ RMS")) + { + irms = 1; + continue; + } + if (strstr(comment, "END OF FREQ RMS")) + { + irms = 0; + continue; + } + + if (irms == 0 && strstr(buff, "NOAZI")) + { + char tmp[10]; + + for (int i = 0; i < recPcv.nz; i++) + { + int offset = i * 8 + 8; + strncpy(tmp, buff + offset, 8); + tmp[8] = '\0'; + + double pcv_val = atof(tmp); + freqPcv.elMap.push_back(pcv_val * 1e-3); + } + + noAziLineRead = true; + + continue; + } + + if (irms == 0 && noAziLineRead) + { + char tmp[10]; + + for (int i = 0; i < recPcv.nz; i++) + { + int offset = i * 8 + 8; + strncpy(tmp, buff + offset, 8); + tmp[8] = '\0'; + double pcv_val = atof(tmp); + freqPcv.azElMap[numAziLinesRead].push_back(pcv_val * 1e-3); + } + numAziLinesRead++; + + continue; + } + } } diff --git a/src/cpp/common/antenna.hpp b/src/cpp/common/antenna.hpp index 3442b76ee..d17ca6932 100644 --- a/src/cpp/common/antenna.hpp +++ b/src/cpp/common/antenna.hpp @@ -1,85 +1,79 @@ - #pragma once - +#include #include #include -#include +#include "common/azElMapData.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/satStat.hpp" +#include "common/trace.hpp" +using std::map; using std::string; using std::vector; -using std::map; - -#include "eigenIncluder.hpp" -#include "azElMapData.hpp" -#include "satStat.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" struct PhaseCenterData : AzElMapData { - /* antenna parameter type */ - E_FType ft; - string type; ///< antenna type - string code; ///< serial number or satellite code - string svn; ///< SVN in satellites - string cospar; ///< Cospar code satellites - string calibModel; ///< name of the antenna calibration model - - double tf[6]; ///< valid from YMDHMS - double tu[6]; ///< valid until YMDHMS + E_FType ft; + string type; ///< antenna type + string code; ///< serial number or satellite code + string svn; ///< SVN in satellites + string cospar; ///< Cospar code satellites + string calibModel; ///< name of the antenna calibration model + + GTime validFrom; + GTime validUntil; }; struct PhaseCenterOffset { - Vector3d satPco = Vector3d::Zero(); - Vector3d recPco = Vector3d::Zero(); + Vector3d satPco = Vector3d::Zero(); + Vector3d recPco = Vector3d::Zero(); + + GTime validFrom; + GTime validUntil; }; -//forward declaration for pointer below +// forward declaration for pointer below struct SatSys; struct AttStatus; struct Navigation; -VectorEcef satAntOff( - Trace& trace, - GTime time, - AttStatus& attStatus, - SatSys& Sat, - map& lamMap); +VectorEcef +satAntOff(Trace& trace, GTime time, AttStatus& attStatus, SatSys& Sat, map& lamMap); Vector3d antPco( - string id, - E_Sys sys, - E_FType ft, - GTime time, - double& var, - E_Radio radio, - bool interp = false); + string id, + E_Sys sys, + E_FType ft, + GTime time, + double& var, + E_Radio radio, + bool interp = false +); double antPcv( - string id, - E_Sys sys, - E_FType ft, - GTime time, - AttStatus& attStatus, - VectorEcef e, - double* az_ptr = nullptr, - double* zen_ptr = nullptr); + string id, + E_Sys sys, + E_FType ft, + GTime time, + AttStatus& attStatus, + VectorEcef e, + double* az_ptr = nullptr, + double* zen_ptr = nullptr +); bool findAntenna( - string code, - E_Sys sys, - GTime time, - Navigation& nav, - E_FType ft, - PhaseCenterData** pcd_ptr_ptr = nullptr); - -void readantexf( - string file, - Navigation& nav); + string code, + E_Sys sys, + GTime time, + Navigation& nav, + E_FType ft, + PhaseCenterData** pcd_ptr_ptr = nullptr +); -void radome2none( - string& antenna_type); +void readantexf(string file, Navigation& nav); +void radome2none(string& antenna_type); diff --git a/src/cpp/common/api.cpp b/src/cpp/common/api.cpp index a06574e8a..7b0acc28e 100644 --- a/src/cpp/common/api.cpp +++ b/src/cpp/common/api.cpp @@ -1,16 +1,13 @@ - - // #pragma GCC optimize ("O0") -#include "api.hpp" - +#include "common/api.hpp" vector oncePerEpochCallbacks; void callbacksOncePerEpoch() { - for (auto& callback : oncePerEpochCallbacks) - { - callback(); - } + for (auto& callback : oncePerEpochCallbacks) + { + callback(); + } } diff --git a/src/cpp/common/api.hpp b/src/cpp/common/api.hpp index ce3552e13..02ba022e9 100644 --- a/src/cpp/common/api.hpp +++ b/src/cpp/common/api.hpp @@ -1,13 +1,11 @@ - #pragma once #include using std::vector; -typedef void (*apiCallback) (); +typedef void (*apiCallback)(); extern vector oncePerEpochCallbacks; void callbacksOncePerEpoch(); - diff --git a/src/cpp/common/attitude.cpp b/src/cpp/common/attitude.cpp index f5e3966e6..49e0a8808 100644 --- a/src/cpp/common/attitude.cpp +++ b/src/cpp/common/attitude.cpp @@ -1,294 +1,302 @@ - // #pragma GCC optimize ("O0") - /** \file -* ###References: -* -* 1. D.D.McCarthy, IERS Technical Note 21, IERS Conventions 1996, July 1996 -* 2. D.D.McCarthy and G.Petit, IERS Technical Note 32, IERS Conventions 2003, November 2003 -* 3. D.A.Vallado, Fundamentals of Astrodynamics and Applications 2nd ed, Space Technology Library, 2004 -* 4. J.Kouba, A Guide to using International GNSS Service (IGS) products, May 2009 -* 5. RTCM Paper, April 12, 2010, Proposed SSR Messages for SV Orbit Clock, Code Biases, URA -* 6. MacMillan et al., Atmospheric gradients and the VLBI terrestrial and celestial reference frames, Geophys. Res. Let., 1997 -* 7. G.Petit and B.Luzum (eds), IERS Technical Note No. 36, IERS Conventions (2010), 2010 -* 8. J.Kouba, A simplified yaw-attitude model for eclipsing GPS satellites, GPS Solutions, 13:1-12, 2009 -* 9. F.Dilssner, GPS IIF-1 satellite antenna phase center and attitude modeling, InsideGNSS, September, 2010 -* 10. F.Dilssner, The GLONASS-M satellite yaw-attitude model, Advances in Space Research, 2010 -* 11. IGS MGEX (http://igs.org/mgex) -*/ - + * ###References: + * + * 1. D.D.McCarthy, IERS Technical Note 21, IERS Conventions 1996, July 1996 + * 2. D.D.McCarthy and G.Petit, IERS Technical Note 32, IERS Conventions 2003, November 2003 + * 3. D.A.Vallado, Fundamentals of Astrodynamics and Applications 2nd ed, Space Technology Library, + * 2004 + * 4. J.Kouba, A Guide to using International GNSS Service (IGS) products, May 2009 + * 5. RTCM Paper, April 12, 2010, Proposed SSR Messages for SV Orbit Clock, Code Biases, URA + * 6. MacMillan et al., Atmospheric gradients and the VLBI terrestrial and celestial reference + * frames, Geophys. Res. Let., 1997 + * 7. G.Petit and B.Luzum (eds), IERS Technical Note No. 36, IERS Conventions (2010), 2010 + * 8. J.Kouba, A simplified yaw-attitude model for eclipsing GPS satellites, GPS Solutions, + * 13:1-12, 2009 + * 9. F.Dilssner, GPS IIF-1 satellite antenna phase center and attitude modeling, InsideGNSS, + * September, 2010 + * 10. F.Dilssner, The GLONASS-M satellite yaw-attitude model, Advances in Space Research, 2010 + * 11. IGS MGEX (http://igs.org/mgex) + */ #include - #include +#include "common/acsConfig.hpp" +#include "common/antenna.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/ephPrecise.hpp" +#include "common/ephemeris.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/satStat.hpp" +#include "common/sinex.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/planets.hpp" +#include "pea/ppp.hpp" using std::vector; - -#include "eigenIncluder.hpp" -#include "observations.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "ephPrecise.hpp" -#include "ephemeris.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "receiver.hpp" -#include "planets.hpp" -#include "satStat.hpp" -#include "antenna.hpp" -#include "common.hpp" -#include "sinex.hpp" -#include "trace.hpp" -#include "enums.h" -#include "ppp.hpp" - - /** exclude meas of eclipsing satellite (block IIA) -*/ -void testEclipse( - ObsList& obsList) + */ +void testEclipse(ObsList& obsList) { - /* unit vector of sun direction (ecef) */ - VectorEcef rsun; - planetPosEcef(obsList.front()->time, E_ThirdBody::SUN, rsun); + if (obsList.empty()) + return; - Vector3d esun = rsun.normalized(); + /* unit vector of sun direction (ecef) */ + VectorEcef rsun; + planetPosEcef(obsList.front()->time, E_ThirdBody::SUN, rsun); - for (auto& obs : only(obsList)) - { - if (acsConfig.reject_eclipse[obs.Sat.sys] == false) - continue; + Vector3d esun = rsun.normalized(); - if (obs.exclude) - { - continue; - } + for (auto& obs : only(obsList)) + { + if (acsConfig.reject_eclipse[obs.Sat.sys] == false) + continue; - double r = obs.rSatCom.norm(); - if (r <= 0) - continue; + if (obs.exclude) + { + continue; + } - /* only block IIA */ -// if (obs.satNav_ptr->pcv.type == "BLOCK IIA") //todo take from the satSys object -// continue; + double r = obs.rSatCom.norm(); + if (r <= 0) + continue; - /* sun-earth-satellite angle */ - double cosa = obs.rSatCom.dot(esun) / r; + /* only block IIA */ + // if (obs.satNav_ptr->pcv.type == "BLOCK IIA") //todo take from the satSys + // object continue; - if (cosa < -1) cosa = -1; - if (cosa > +1) cosa = +1; + /* sun-earth-satellite angle */ + double cosa = obs.rSatCom.dot(esun) / r; - double ang = acos(cosa); + if (cosa < -1) + cosa = -1; + if (cosa > +1) + cosa = +1; - /* test eclipse */ - if ( ang < PI / 2 - || r * sin(ang) > RE_WGS84) - continue; + double ang = acos(cosa); -// trace(3, "eclipsing sat excluded %s sat=%s\n", obs.time.to_string(0).c_str(), obs.Sat.id().c_str()); + /* test eclipse */ + if (ang < PI / 2 || r * sin(ang) > RE_WGS84) + continue; - obs.excludeEclipse = true; - } -} + // trace(3, "eclipsing sat excluded %s sat=%s\n", obs.time.to_string(0).c_str(), + // obs.Sat.id().c_str()); + obs.excludeEclipse = true; + } +} struct SatGeom { - SatSys Sat; ///< Satellite - VectorEcef rSat; ///< Satellite position (ECEF) - VectorEcef vSat; ///< Satellite velocity (ECEF) - VectorEcef vSatPrime; ///< Satellite velocity (ECEF + Earth rotation component) - VectorEcef rSun; ///< Sun position (ECEF) - VectorEcef rMoon; ///< Moon position (ECEF) - VectorEcef eNorm; ///< Normalised orbit normal vector (ECEF) - double beta = 0; ///< Sun elevation angle with respect to the orbital plane - double betaRate = 0; ///< dBeta/dt - double mu = 0; ///< Angle of sat from 'midnight' (when sat is at the furthest point from Sun in its orbit) - double muRate = 0; ///< dMu/dt + SatSys Sat; ///< Satellite + VectorEcef rSat; ///< Satellite position (ECEF) + VectorEcef vSat; ///< Satellite velocity (ECEF) + VectorEcef vSatPrime; ///< Satellite velocity (ECEF + Earth rotation component) + VectorEcef rSun; ///< Sun position (ECEF) + VectorEcef rMoon; ///< Moon position (ECEF) + VectorEcef eNorm; ///< Normalised orbit normal vector (ECEF) + double beta = 0; ///< Sun elevation angle with respect to the orbital plane + double betaRate = 0; ///< dBeta/dt + double mu = 0; ///< Angle of sat from 'midnight' (when sat is at the furthest point from Sun in + ///< its orbit) + double muRate = 0; ///< dMu/dt }; /** Calculates satellite orbit geometry - for use in calculating modelled yaw */ -SatGeom satOrbitGeometry( - SatPos& satPos) ///< Observation +SatGeom satOrbitGeometry(SatPos& satPos) ///< Observation { - SatGeom satGeom; - auto& rSat = satGeom.rSat; - auto& vSat = satGeom.vSat; - auto& vSatPrime = satGeom.vSatPrime; - auto& rSun = satGeom.rSun; - auto& rMoon = satGeom.rMoon; - auto& eNorm = satGeom.eNorm; - auto& time = satPos.posTime; + SatGeom satGeom; + auto& rSat = satGeom.rSat; + auto& vSat = satGeom.vSat; + auto& vSatPrime = satGeom.vSatPrime; + auto& rSun = satGeom.rSun; + auto& rMoon = satGeom.rMoon; + auto& eNorm = satGeom.eNorm; + auto& time = satPos.posTime; - rSat = satPos.rSatCom; - vSat = satPos.satVel; + rSat = satPos.rSatCom; + vSat = satPos.satVel; - ERPValues erpv = getErp(nav.erp, time); + ERPValues erpv = getErp(nav.erp, time); - FrameSwapper frameSwapper(time, erpv); + FrameSwapper frameSwapper(time, erpv); - VectorEci vSatEci; - VectorEci rSatEci = frameSwapper(rSat, &vSat, &vSatEci); + VectorEci vSatEci; + VectorEci rSatEci = frameSwapper(rSat, &vSat, &vSatEci); - double beta[2]; - double mu [2]; - int DT = 1; + double beta[2]; + double mu[2]; + int DT = 1; - //do dt = 1, 0, so that the final results end up in the right place - for (int dt : {DT, 0}) - { - SatPos satPosDt; - satPosDt.Sat = satPos.Sat; + // do dt = 1, 0, so that the final results end up in the right place + for (int dt : {DT, 0}) + { + SatPos satPosDt; + satPosDt.Sat = satPos.Sat; - propagateEllipse(nullStream, time, dt, rSatEci, vSatEci, satPosDt); + propagateEllipse(nullStream, time, dt, rSatEci, vSatEci, satPosDt); - rSat = satPosDt.rSatCom; - vSat = satPosDt.satVel; + rSat = satPosDt.rSatCom; + vSat = satPosDt.satVel; - VectorEcef vSatPrime = vSat; - vSatPrime[0] -= OMGE * rSat[1]; - vSatPrime[1] += OMGE * rSat[0]; + VectorEcef vSatPrime = vSat; + vSatPrime[0] -= OMGE * rSat[1]; + vSatPrime[1] += OMGE * rSat[0]; - planetPosEcef(time + dt, E_ThirdBody::MOON, rMoon); - planetPosEcef(time + dt, E_ThirdBody::SUN, rSun); + planetPosEcef(time + dt, E_ThirdBody::MOON, rMoon); + planetPosEcef(time + dt, E_ThirdBody::SUN, rSun); - Vector3d n = rSat.cross(vSatPrime); //orbit-axis - Vector3d p = rSun.cross(n); //ascension? + Vector3d n = rSat.cross(vSatPrime); // orbit-axis + Vector3d p = rSun.cross(n); // ascension? - Vector3d eSat = rSat. normalized(); - Vector3d eSun = rSun. normalized(); - eNorm = n. normalized(); //orbit-axis - Vector3d ep = p. normalized(); //ascension? + Vector3d eSat = rSat.normalized(); + Vector3d eSun = rSun.normalized(); + eNorm = n.normalized(); // orbit-axis + Vector3d ep = p.normalized(); // ascension? - beta[dt] = asin(eNorm.dot(eSun)); //angle between sun and orbital plane + beta[dt] = asin(eNorm.dot(eSun)); // angle between sun and orbital plane - double E = acos(eSat .dot(ep)); //angle between sat and ascension node? + double E = acos(eSat.dot(ep)); // angle between sat and ascension node? - if (eSat.dot(eSun) <= 0) mu[dt] = PI / 2 - E; //sat on dark side - else mu[dt] = PI / 2 + E; //sat on noon side + if (eSat.dot(eSun) <= 0) + mu[dt] = PI / 2 - E; // sat on dark side + else + mu[dt] = PI / 2 + E; // sat on noon side - wrapPlusMinusPi(beta[dt]); - wrapPlusMinusPi(mu [dt]); - } + wrapPlusMinusPi(beta[dt]); + wrapPlusMinusPi(mu[dt]); + } - double dBeta = beta [1] - beta [0]; - double dMu = mu [1] - mu [0]; + double dBeta = beta[1] - beta[0]; + double dMu = mu[1] - mu[0]; - wrapPlusMinusPi(dBeta); - wrapPlusMinusPi(dMu); + wrapPlusMinusPi(dBeta); + wrapPlusMinusPi(dMu); - satGeom.beta = beta [0]; - satGeom.mu = mu [0]; - satGeom.betaRate = dBeta / DT; - satGeom.muRate = dMu / DT; - satGeom.Sat = satPos.Sat; + satGeom.beta = beta[0]; + satGeom.mu = mu[0]; + satGeom.betaRate = dBeta / DT; + satGeom.muRate = dMu / DT; + satGeom.Sat = satPos.Sat; - return satGeom; + return satGeom; } /** Calculate nominal (ideal) sat yaw for GPS sats * Returns result between (-PI, PI] -*/ + */ double nominalYawGps( - double beta, ///< Sun elevation angle with respect to the orbital plane - double mu) ///< Angle of sat from 'midnight' (when sat is at the furthest point from Sun in its orbit) + double beta, ///< Sun elevation angle with respect to the orbital plane + double mu ///< Angle of sat from 'midnight' (when sat is at the furthest point from Sun in its + ///< orbit) +) { - if ( abs(beta) < 1E-12 - &&abs(mu) < 1E-12) - return PI; + if (abs(beta) < 1E-12 && abs(mu) < 1E-12) + return PI; - double yaw = atan2(-tan(beta), sin(mu)) + PI; - wrapPlusMinusPi(yaw); - return yaw; + double yaw = atan2(-tan(beta), sin(mu)) + PI; + wrapPlusMinusPi(yaw); + return yaw; } /** Calculate nominal sat yaw for GPS sats at a given time * Returns result between (-PI, PI] -*/ + */ double nominalYawGpsAtTime( - SatGeom& satGeom, ///< Satellite geometry - GTime time, ///< Time of satGeom - GTime reqTime) ///< Requested time + SatGeom& satGeom, ///< Satellite geometry + GTime time, ///< Time of satGeom + GTime reqTime ///< Requested time +) { - double dt = (reqTime - time).to_double(); - return nominalYawGps(satGeom.beta + satGeom.betaRate * dt, satGeom.mu + satGeom.muRate * dt); + double dt = (reqTime - time).to_double(); + return nominalYawGps(satGeom.beta + satGeom.betaRate * dt, satGeom.mu + satGeom.muRate * dt); } /** Calculates nominal yaw rate -*/ -double nominalYawRate( - SatGeom& satGeom) ///< Satellite geometry + */ +double nominalYawRate(SatGeom& satGeom) ///< Satellite geometry { - double nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); - double dt = 1E-3; - GTime time; - time.bigTime = 100; // Any positive value - double nominalYaw2 = nominalYawGpsAtTime(satGeom, time, time + dt); - double dYaw = nominalYaw2 - nominalYaw; - wrapPlusMinusPi(dYaw); - return dYaw / dt; + double nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); + double dt = 1E-3; + GTime time; + time.bigTime = 100; // Any positive value + double nominalYaw2 = nominalYawGpsAtTime(satGeom, time, time + dt); + double dYaw = nominalYaw2 - nominalYaw; + wrapPlusMinusPi(dYaw); + return dYaw / dt; } /** Finds time when max yaw-rate catch-up started, previous to this point in time -*/ + */ bool findCatchupStart( - GTime earliestTime, ///< Search backwards until this time - GTime time, ///< Solution time - SatGeom satGeom, ///< Satellite geometry (copy) - double maxYawRate, ///< Maximum yaw rate (rad/s) - GTime& catchupTime, ///< Time of catchup start - double dt = -1) ///< Time step to search backwards by (sec) + GTime earliestTime, ///< Search backwards until this time + GTime time, ///< Solution time + SatGeom satGeom, ///< Satellite geometry (copy) + double maxYawRate, ///< Maximum yaw rate (rad/s) + GTime& catchupTime, ///< Time of catchup start + double dt = -1 ///< Time step to search backwards by (sec) +) { - bool catchupExists = false; - while (time > earliestTime) - { - if (abs(nominalYawRate(satGeom)) > maxYawRate) - catchupExists = true; - else if (catchupExists) - { - catchupTime = time; - break; - } - - time += dt; - satGeom.beta += satGeom.betaRate * dt; - satGeom.mu += satGeom.muRate * dt; - } - return catchupExists; + bool catchupExists = false; + while (time > earliestTime) + { + if (abs(nominalYawRate(satGeom)) > maxYawRate) + catchupExists = true; + else if (catchupExists) + { + catchupTime = time; + break; + } + + time += dt; + satGeom.beta += satGeom.betaRate * dt; + satGeom.mu += satGeom.muRate * dt; + } + return catchupExists; } /** Calculates proportion of circle 1 visible with overlapping circle 2 */ double circleAreaVisible( - double r1, ///< radius of circle 1 - double r2, ///< radius of circle 2 - double d) ///< distance between circle centres + double r1, ///< radius of circle 1 + double r2, ///< radius of circle 2 + double d ///< distance between circle centres +) { - double intersection = 0; - if (d >= r1 + r2) // no overlap - { - intersection = 0; - } - else if (abs(r1 - r2) >= d) // full overlap - { - double smallerR = std::min(r1, r2); - intersection = PI * SQR(smallerR); - } - else // partial overlap - { - double area1 = PI * SQR(r1); - double area2 = PI * SQR(r2); - - double d1 = (SQR(r1) - SQR(r2) + SQR(d)) / (2 * d); - double d2 = (SQR(r2) - SQR(r1) + SQR(d)) / (2 * d); - intersection = SQR(r1) * acos(d1 / r1) - d1 * sqrt(SQR(r1) - SQR(d1)) - + SQR(r2) * acos(d2 / r2) - d2 * sqrt(SQR(r2) - SQR(d2)); // Ref: https://diego.assencio.com/?index=8d6ca3d82151bad815f78addf9b5c1c6 - } - - double area1 = PI * SQR(r1); - return (area1 - intersection) / area1; + double intersection = 0; + if (d >= r1 + r2) // no overlap + { + intersection = 0; + } + else if (abs(r1 - r2) >= d) // full overlap + { + double smallerR = std::min(r1, r2); + intersection = PI * SQR(smallerR); + } + else // partial overlap + { + double area1 = PI * SQR(r1); + double area2 = PI * SQR(r2); + + double d1 = (SQR(r1) - SQR(r2) + SQR(d)) / (2 * d); + double d2 = (SQR(r2) - SQR(r1) + SQR(d)) / (2 * d); + intersection = + SQR(r1) * acos(d1 / r1) - d1 * sqrt(SQR(r1) - SQR(d1)) + SQR(r2) * acos(d2 / r2) - + d2 * sqrt( + SQR(r2) - SQR(d2) + ); // Ref: https://diego.assencio.com/?index=8d6ca3d82151bad815f78addf9b5c1c6 + } + + double area1 = PI * SQR(r1); + return (area1 - intersection) / area1; } /** Calculates fraction of Sun's disk visible by spacecraft @@ -297,231 +305,242 @@ double circleAreaVisible( * 1 = no eclipse (outside penumbra) */ double sunVisibility( - Vector3d& rSat, ///< Satellite position (ECEF) - Vector3d& rSun, ///< Sun position (ECEF) - Vector3d& rMoon) ///< Moon position (ECEF) + Vector3d& rSat, ///< Satellite position (ECEF) + Vector3d& rSun, ///< Sun position (ECEF) + Vector3d& rMoon ///< Moon position (ECEF) +) { - struct PosRadius - { - Vector3d pos = Vector3d::Zero(); - double radius = 0; - }; - Vector3d rEarth = Vector3d::Zero(); - - PosRadius earthPosRadius = {rEarth, RE_WGS84}; - PosRadius moonPosRadius = {rMoon, MoonRadius}; - - for (auto& bodyPosRadius : {earthPosRadius, moonPosRadius}) - { - Vector3d rBody = bodyPosRadius.pos; - double bodyRadius = bodyPosRadius.radius; - - Vector3d sunToSat = -rSun + rSat; - Vector3d sunToBody = -rSun + rBody; - Vector3d bodyToSat = -rBody + rSat; - - if (sunToSat.norm() < sunToBody.norm()) - continue; - - Vector3d unitSunToSat = sunToSat.normalized(); - double bodySatAlongSunSat = bodyToSat.dot( unitSunToSat); - Vector3d centreSeparation = bodyToSat.cross( unitSunToSat); - - double sunRadiusAngularSize = SunRadius / sunToSat.norm(); - double bodyRadiusAngularSize = bodyRadius / bodySatAlongSunSat; - double separationAngularSize = centreSeparation.norm() / bodySatAlongSunSat; - - double fraction = circleAreaVisible(sunRadiusAngularSize, bodyRadiusAngularSize, separationAngularSize); - if (fraction < 1) - return fraction; - } - return 1; // no eclipse from Earth or Moon + struct PosRadius + { + Vector3d pos = Vector3d::Zero(); + double radius = 0; + }; + Vector3d rEarth = Vector3d::Zero(); + + PosRadius earthPosRadius = {rEarth, RE_WGS84}; + PosRadius moonPosRadius = {rMoon, MoonRadius}; + + for (auto& bodyPosRadius : {earthPosRadius, moonPosRadius}) + { + Vector3d rBody = bodyPosRadius.pos; + double bodyRadius = bodyPosRadius.radius; + + Vector3d sunToSat = -rSun + rSat; + Vector3d sunToBody = -rSun + rBody; + Vector3d bodyToSat = -rBody + rSat; + + if (sunToSat.norm() < sunToBody.norm()) + continue; + + Vector3d unitSunToSat = sunToSat.normalized(); + double bodySatAlongSunSat = bodyToSat.dot(unitSunToSat); + Vector3d centreSeparation = bodyToSat.cross(unitSunToSat); + + double sunRadiusAngularSize = SunRadius / sunToSat.norm(); + double bodyRadiusAngularSize = bodyRadius / bodySatAlongSunSat; + double separationAngularSize = centreSeparation.norm() / bodySatAlongSunSat; + + double fraction = + circleAreaVisible(sunRadiusAngularSize, bodyRadiusAngularSize, separationAngularSize); + if (fraction < 1) + return fraction; + } + return 1; // no eclipse from Earth or Moon } /** Returns true if satellite is in eclipse (shadow umbra); else false -*/ + */ bool inEclipse( - Vector3d& rSat, ///< Satellite position (ECEF) - Vector3d& rSun, ///< Sun position (ECEF) - Vector3d& rMoon) ///< Moon position (ECEF) + Vector3d& rSat, ///< Satellite position (ECEF) + Vector3d& rSun, ///< Sun position (ECEF) + Vector3d& rMoon ///< Moon position (ECEF) +) { - double visibility = sunVisibility(rSat, rSun, rMoon); + double visibility = sunVisibility(rSat, rSun, rMoon); - if (visibility == 0) return true; - else return false; + if (visibility == 0) + return true; + else + return false; } void sunMoonPos( - GTime time, ///< Time of - VectorEcef& rSun, ///< Sun position (ECEF) - VectorEcef& rMoon) ///< Moon position (ECEF) + GTime time, ///< Time of + VectorEcef& rSun, ///< Sun position (ECEF) + VectorEcef& rMoon ///< Moon position (ECEF) +) { - planetPosEcef(time, E_ThirdBody::SUN, rSun); - planetPosEcef(time, E_ThirdBody::MOON, rMoon); + planetPosEcef(time, E_ThirdBody::SUN, rSun); + planetPosEcef(time, E_ThirdBody::MOON, rMoon); } /** Finds time when eclipse started, prior (default) to this point in time * For finding eclipses forward in time, set dt to some positive value -*/ + */ GTime findEclipseBoundaries( - GTime time, ///< Solution time - SatGeom& satGeom, ///< Satellite geometry - bool searchForward) ///< Search forward in time + GTime time, ///< Solution time + SatGeom& satGeom, ///< Satellite geometry + bool searchForward ///< Search forward in time +) { - double precision = 1; + double precision = 1; - VectorEcef rSat = satGeom.rSat; - VectorEcef vSat = satGeom.vSat; - VectorEcef rSun = satGeom.rSun; - VectorEcef rMoon = satGeom.rMoon; + VectorEcef rSat = satGeom.rSat; + VectorEcef vSat = satGeom.vSat; + VectorEcef rSun = satGeom.rSun; + VectorEcef rMoon = satGeom.rMoon; - ERPValues erpv = getErp(nav.erp, time); + ERPValues erpv = getErp(nav.erp, time); - FrameSwapper frameSwapper(time, erpv); + FrameSwapper frameSwapper(time, erpv); - VectorEci rSatEci0; - VectorEci vSatEci0; - rSatEci0 = frameSwapper(rSat, &vSat, &vSatEci0); + VectorEci rSatEci0; + VectorEci vSatEci0; + rSatEci0 = frameSwapper(rSat, &vSat, &vSatEci0); - double dt = 0; - if (searchForward) - { - double start = 0; - double interval = 0.5 * 60 * 60; // Eclipses are ~0.8-1.5hrs in length - double end = interval; + double dt = 0; + if (searchForward) + { + double start = 0; + double interval = 0.5 * 60 * 60; // Eclipses are ~0.8-1.5hrs in length + double end = interval; - // Find rough 1/2hr interval that eclipse falls within - while (inEclipse(rSat, rSun, rMoon)) - { - start = end; - end += interval; + // Find rough 1/2hr interval that eclipse falls within + while (inEclipse(rSat, rSun, rMoon)) + { + start = end; + end += interval; - SatPos satPos; - satPos.Sat = satGeom.Sat; + SatPos satPos; + satPos.Sat = satGeom.Sat; - propagateEllipse(nullStream, time, end, rSatEci0, vSatEci0, satPos); + propagateEllipse(nullStream, time, end, rSatEci0, vSatEci0, satPos); - rSat = satPos.rSatCom; + rSat = satPos.rSatCom; - sunMoonPos(time + end, rSun, rMoon); - } + sunMoonPos(time + end, rSun, rMoon); + } - // Binary search to nearest second - while (start <= end) - { - double mid = start + (end - start) / 2; + // Binary search to nearest second + while (start <= end) + { + double mid = start + (end - start) / 2; - SatPos satPos; - satPos.Sat = satGeom.Sat; + SatPos satPos; + satPos.Sat = satGeom.Sat; - propagateEllipse(nullStream, time, mid, rSatEci0, vSatEci0, satPos); + propagateEllipse(nullStream, time, mid, rSatEci0, vSatEci0, satPos); - rSat = satPos.rSatCom; + rSat = satPos.rSatCom; - sunMoonPos(time + mid, rSun, rMoon); + sunMoonPos(time + mid, rSun, rMoon); - if (inEclipse(rSat, rSun, rMoon)) start = mid + precision; - else end = mid - precision; - } - dt = end; - } - else // findEclipseBoundaries() usually gets called v. near to the start of the eclipse - { - while (inEclipse(rSat, rSun, rMoon)) - { - dt -= precision; + if (inEclipse(rSat, rSun, rMoon)) + start = mid + precision; + else + end = mid - precision; + } + dt = end; + } + else // findEclipseBoundaries() usually gets called v. near to the start of the eclipse + { + while (inEclipse(rSat, rSun, rMoon)) + { + dt -= precision; - SatPos satPos; - satPos.Sat = satGeom.Sat; + SatPos satPos; + satPos.Sat = satGeom.Sat; - propagateEllipse(nullStream, time, dt, rSatEci0, vSatEci0, satPos); + propagateEllipse(nullStream, time, dt, rSatEci0, vSatEci0, satPos); - rSat = satPos.rSatCom; + rSat = satPos.rSatCom; - sunMoonPos(time + dt, rSun, rMoon); - } - } + sunMoonPos(time + dt, rSun, rMoon); + } + } - return time + dt; + return time + dt; } /** Yaw model for GPS-IIR sats * Ref: http://acc.igs.org/orbits/EclipseReadMe.pdf * Ref: https://igsac-cnes.cls.fr/documents/meeting/2021_04_28_Strasser_et_al_EGU21.pdf - * Ref: https://www.researchgate.net/publication/306924379_Observed_features_of_GPS_Block_IIF_satellite_yaw_maneuvers_and_corresponding_modeling + * Ref: + * https://www.researchgate.net/publication/306924379_Observed_features_of_GPS_Block_IIF_satellite_yaw_maneuvers_and_corresponding_modeling * Returns false if no modelled yaw available -*/ + */ bool satYawGpsIIR( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom, ///< Satellite geometry - double betaBias = 0) ///< Beta angle bias + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom, ///< Satellite geometry + double betaBias = 0 ///< Beta angle bias +) { - auto& startTime = attStatus.startTime; - auto& startSign = attStatus.startSign; - auto& startYaw = attStatus.startYaw; - auto& startYawRate = attStatus.startYawRate; - auto& nominalYaw = attStatus.nominalYaw; - auto& modelYaw = attStatus.modelYaw; - auto& modelYawTime = attStatus.modelYawTime; - - // Midnight / noon turns - Catch-up yaw steering - double maxYawRate = 0.2 * D2R; - bool maxYawRateFound = getSnxSatMaxYawRate(Sat.svn(), time, maxYawRate); - if (maxYawRateFound == false) - BOOST_LOG_TRIVIAL(warning) << "Warning: Max yaw rate not found for " << Sat.svn() << " in " << __FUNCTION__ << ", check sinex files for '+SATELLITE/YAW_BIAS_RATE' block"; - - nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); - double dYaw = nominalYaw - modelYaw; - wrapPlusMinusPi(dYaw); - if ( maxYawRateFound == false - ||modelYawTime == GTime::noTime()) - { - modelYaw = nominalYaw; - return true; - } - - double dt = 1; - GTime currTime = modelYawTime; - while (currTime < time) - { - double dtFromTime = (currTime - time).to_double(); - double currNominalYaw = nominalYawGpsAtTime(satGeom, time, currTime); - dYaw = currNominalYaw - modelYaw; - wrapPlusMinusPi(dYaw); - if ( abs(dYaw) / dt > maxYawRate - &&startTime == GTime::noTime()) // Not exiting eclipse - { - if (startSign == 0) // Entering catchup - { - double currBeta = satGeom.beta + satGeom.betaRate * dtFromTime; - startSign = SGN(dYaw); - if ( abs(currBeta) < abs(betaBias) - &&SGN(currBeta) == SGN(betaBias)) - { - startSign *= -1; // beta angles between 0 & betaBias result in opposite direction catch-up steering - } - } - - modelYaw += startSign * maxYawRate * dt; - wrapPlusMinusPi(modelYaw); - } - else - { - modelYaw = currNominalYaw; - startTime = GTime::noTime(); - startSign = 0; - startYaw = 0; - startYawRate = 0; - } - currTime += dt; - } - - if (time < attStatus.excludeTime) - return false; - - return true; + auto& startTime = attStatus.startTime; + auto& startSign = attStatus.startSign; + auto& startYaw = attStatus.startYaw; + auto& startYawRate = attStatus.startYawRate; + auto& nominalYaw = attStatus.nominalYaw; + auto& modelYaw = attStatus.modelYaw; + auto& modelYawTime = attStatus.modelYawTime; + + // Midnight / noon turns - Catch-up yaw steering + double maxYawRate = 0.2 * D2R; + bool maxYawRateFound = getSnxSatMaxYawRate(Sat.svn(), time, maxYawRate); + if (maxYawRateFound == false) + BOOST_LOG_TRIVIAL(warning) + << "Max yaw rate not found for " << Sat.svn() << " in " << __FUNCTION__ + << ", check sinex files for '+SATELLITE/YAW_BIAS_RATE' block"; + + nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); + double dYaw = nominalYaw - modelYaw; + wrapPlusMinusPi(dYaw); + if (maxYawRateFound == false || modelYawTime == GTime::noTime()) + { + modelYaw = nominalYaw; + return true; + } + + double dt = 1; + GTime currTime = modelYawTime; + while (currTime < time) + { + double dtFromTime = (currTime - time).to_double(); + double currNominalYaw = nominalYawGpsAtTime(satGeom, time, currTime); + dYaw = currNominalYaw - modelYaw; + wrapPlusMinusPi(dYaw); + if (abs(dYaw) / dt > maxYawRate && startTime == GTime::noTime()) // Not exiting eclipse + { + if (startSign == 0) // Entering catchup + { + double currBeta = satGeom.beta + satGeom.betaRate * dtFromTime; + startSign = SGN(dYaw); + if (abs(currBeta) < abs(betaBias) && SGN(currBeta) == SGN(betaBias)) + { + startSign *= -1; // beta angles between 0 & betaBias result in opposite + // direction catch-up steering + } + } + + modelYaw += startSign * maxYawRate * dt; + wrapPlusMinusPi(modelYaw); + } + else + { + modelYaw = currNominalYaw; + startTime = GTime::noTime(); + startSign = 0; + startYaw = 0; + startYawRate = 0; + } + currTime += dt; + } + + if (time < attStatus.excludeTime) + return false; + + return true; } /** Yaw model for GPS-IIA sats @@ -529,774 +548,905 @@ bool satYawGpsIIR( * Ref: https://igsac-cnes.cls.fr/documents/meeting/2021_04_28_Strasser_et_al_EGU21.pdf * Ref: https://tda.jpl.nasa.gov/progress_report/42-123/123B.pdf * Returns false if no modelled yaw available -*/ + */ bool satYawGpsIIA( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom) ///< Satellite geometry + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom ///< Satellite geometry +) { - auto& startTime = attStatus.startTime; - auto& startSign = attStatus.startSign; - auto& startYaw = attStatus.startYaw; - auto& startYawRate = attStatus.startYawRate; - - attStatus.nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); - double yawBias = 0.5 * D2R; - - if (inEclipse(satGeom.rSat, satGeom.rSun, satGeom.rMoon) == false) - return satYawGpsIIR(Sat, attStatus, time, satGeom, yawBias); - - // Midnight turning - Shadow max yaw steering - double maxYawRate = 0.12 * D2R; - bool maxYawRateFound = getSnxSatMaxYawRate(Sat.svn(), time, maxYawRate); - if (maxYawRateFound == false) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Max yaw rate not found for " << Sat.svn() << " in " << __FUNCTION__ - << ", check sinex files for '+SATELLITE/YAW_BIAS_RATE' block"; - - return false; - } - - if (startTime == GTime::noTime()) // Start of eclipse - { - startTime = findEclipseBoundaries(time, satGeom, false); - startYaw = nominalYawGpsAtTime(satGeom, time, startTime); - double startBeta = satGeom.beta + satGeom.betaRate * (startTime - time).to_double(); - startSign = SGN(yawBias); - startYawRate = maxYawRate; - } - - attStatus.modelYaw = startYaw + startSign * startYawRate * (time - startTime).to_double(); - wrapPlusMinusPi(attStatus.modelYaw); - attStatus.excludeTime = time + 30 * 60; // Recommended to exclude up to 30min after exiting eclipse - return true; + auto& startTime = attStatus.startTime; + auto& startSign = attStatus.startSign; + auto& startYaw = attStatus.startYaw; + auto& startYawRate = attStatus.startYawRate; + + attStatus.nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); + double yawBias = 0.5 * D2R; + + if (inEclipse(satGeom.rSat, satGeom.rSun, satGeom.rMoon) == false) + return satYawGpsIIR(Sat, attStatus, time, satGeom, yawBias); + + // Midnight turning - Shadow max yaw steering + double maxYawRate = 0.12 * D2R; + bool maxYawRateFound = getSnxSatMaxYawRate(Sat.svn(), time, maxYawRate); + if (maxYawRateFound == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Max yaw rate not found for " << Sat.svn() << " in " << __FUNCTION__ + << ", check sinex files for '+SATELLITE/YAW_BIAS_RATE' block"; + + return false; + } + + if (startTime == GTime::noTime()) // Start of eclipse + { + startTime = findEclipseBoundaries(time, satGeom, false); + startYaw = nominalYawGpsAtTime(satGeom, time, startTime); + double startBeta = satGeom.beta + satGeom.betaRate * (startTime - time).to_double(); + startSign = SGN(yawBias); + startYawRate = maxYawRate; + } + + attStatus.modelYaw = startYaw + startSign * startYawRate * (time - startTime).to_double(); + wrapPlusMinusPi(attStatus.modelYaw); + attStatus.excludeTime = + time + 30 * 60; // Recommended to exclude up to 30min after exiting eclipse + return true; } /** Yaw model for GPS-IIF sats - * Ref: https://www.researchgate.net/publication/306924379_Observed_features_of_GPS_Block_IIF_satellite_yaw_maneuvers_and_corresponding_modeling + * Ref: + * https://www.researchgate.net/publication/306924379_Observed_features_of_GPS_Block_IIF_satellite_yaw_maneuvers_and_corresponding_modeling * Returns false if no modelled yaw available -*/ + */ bool satYawGpsIIF( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom) ///< Satellite geometry + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom ///< Satellite geometry +) { - auto& startTime = attStatus.startTime; - auto& startSign = attStatus.startSign; - auto& startYaw = attStatus.startYaw; - auto& startYawRate = attStatus.startYawRate; - - attStatus.nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); - - if (inEclipse(satGeom.rSat, satGeom.rSun, satGeom.rMoon) == false) - return satYawGpsIIR(Sat, attStatus, time, satGeom, -0.7 * D2R); - - // Midnight turning - Shadow constant yaw steering - if (startTime == GTime::noTime()) // Start of eclipse - { - startTime = findEclipseBoundaries(time, satGeom, false); - startYaw = nominalYawGpsAtTime(satGeom, time, startTime); - - GTime endTime = findEclipseBoundaries(time, satGeom, true); - double endYaw = nominalYawGpsAtTime(satGeom, time, endTime); - - double dYaw = endYaw - startYaw; - wrapPlusMinusPi(dYaw); - startYawRate = abs(dYaw) / (endTime - startTime).to_double(); - startSign = SGN(dYaw); - } - attStatus.modelYaw = startYaw + startSign * startYawRate * (time - startTime).to_double(); - wrapPlusMinusPi(attStatus.modelYaw); - return true; + auto& startTime = attStatus.startTime; + auto& startSign = attStatus.startSign; + auto& startYaw = attStatus.startYaw; + auto& startYawRate = attStatus.startYawRate; + + attStatus.nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); + + if (inEclipse(satGeom.rSat, satGeom.rSun, satGeom.rMoon) == false) + return satYawGpsIIR(Sat, attStatus, time, satGeom, -0.7 * D2R); + + // Midnight turning - Shadow constant yaw steering + if (startTime == GTime::noTime()) // Start of eclipse + { + startTime = findEclipseBoundaries(time, satGeom, false); + startYaw = nominalYawGpsAtTime(satGeom, time, startTime); + + GTime endTime = findEclipseBoundaries(time, satGeom, true); + double endYaw = nominalYawGpsAtTime(satGeom, time, endTime); + + double dYaw = endYaw - startYaw; + wrapPlusMinusPi(dYaw); + startYawRate = abs(dYaw) / (endTime - startTime).to_double(); + startSign = SGN(dYaw); + } + attStatus.modelYaw = startYaw + startSign * startYawRate * (time - startTime).to_double(); + wrapPlusMinusPi(attStatus.modelYaw); + return true; } /** Yaw model for GPS-III sats - * Ref: https://www.gpsworld.com/new-type-on-the-block-generating-high-precision-orbits-for-gps-iii-satellites/ -*/ + * Ref: + * https://www.gpsworld.com/new-type-on-the-block-generating-high-precision-orbits-for-gps-iii-satellites/ + */ bool satYawGpsIII( - AttStatus& attStatus, ///< Satellite att status - SatGeom& satGeom) ///< Satellite geometry + AttStatus& attStatus, ///< Satellite att status + SatGeom& satGeom ///< Satellite geometry +) { - auto& beta = satGeom.beta; - double betaModified = beta; - double betaThreshold = 4.78 * D2R; - if (abs(betaModified) < betaThreshold) - betaModified = beta + (SGN(beta) * betaThreshold - beta) / (1 + 13000 * pow(sin(satGeom.mu), 4)); - - attStatus.nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); - attStatus.modelYaw = nominalYawGps(betaModified, satGeom.mu); - return true; + auto& beta = satGeom.beta; + double betaModified = beta; + double betaThreshold = 4.78 * D2R; + if (abs(betaModified) < betaThreshold) + betaModified = + beta + (SGN(beta) * betaThreshold - beta) / (1 + 13000 * pow(sin(satGeom.mu), 4)); + + attStatus.nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); + attStatus.modelYaw = nominalYawGps(betaModified, satGeom.mu); + return true; } /** Calculate nominal (ideal) sat yaw for GAL-IOV sats. Follows IGS convention. * Roughly equal to nominalYawGps() * Ref: https://www.gsc-europa.eu/support-to-developers/galileo-satellite-metadata#3.1 -*/ -double nominalYawGalIov( - Vector3d& eSunOrf) ///< Unit vector to Sun in Orbital reference frame {A,-C,-R} + */ +double +nominalYawGalIov(Vector3d& eSunOrf) ///< Unit vector to Sun in Orbital reference frame {A,-C,-R} { - return atan2( -eSunOrf(1) / sqrt(1 - SQR(eSunOrf(2))), - -eSunOrf(0) / sqrt(1 - SQR(eSunOrf(2)))); + return atan2(-eSunOrf(1) / sqrt(1 - SQR(eSunOrf(2))), -eSunOrf(0) / sqrt(1 - SQR(eSunOrf(2)))); } /** Yaw model for GAL IOV sats. Follows original GAL convention. * Returns false if no modelled yaw available. * Aux Sun vector is not provided in [1], so is taken from [2] * Ref: https://www.gsc-europa.eu/support-to-developers/galileo-satellite-metadata#3.1 - * Ref: https://github.com/groops-devs/groops/blob/main/source/programs/simulation/simulateStarCameraGnss.cpp -*/ + * Ref: + * https://github.com/groops-devs/groops/blob/main/source/programs/simulation/simulateStarCameraGnss.cpp + */ bool satYawGalIov( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom) ///< Satellite geometry + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom ///< Satellite geometry +) { - auto& rSat = satGeom.rSat; - auto& vSatPrime = satGeom.vSatPrime; - auto& rSun = satGeom.rSun; - auto& beta = satGeom.beta; - auto& mu = satGeom.mu; - auto& startSign = attStatus.startSign; - - Vector3d eSunRac = ecef2rac(rSat, vSatPrime) * (rSun - rSat); - eSunRac.normalize(); - Vector3d eSunOrf; // Orbital reference frame - eSunOrf << eSunRac(1), -eSunRac(2), -eSunRac(0); - attStatus.nominalYaw = nominalYawGalIov(eSunOrf); wrapPlusMinusPi(attStatus.nominalYaw); - attStatus.modelYaw = attStatus.nominalYaw; - - // Midnight/noon turns - double sinBetaX = sin(15 * D2R); - double sinBetaY = sin( 2 * D2R); - if ( abs(eSunOrf(0)) < sinBetaX - &&abs(eSunOrf(1)) < sinBetaY) - { - if (attStatus.modelYawTime == GTime::noTime()) // Check sat not mid-way through eclipse on startup - return false; - - if (startSign == 0) // i.e. start of switchover period - startSign = SGN(eSunOrf(1)); - - Vector3d eSunAux; - eSunAux(0) = eSunOrf(0); - eSunAux(1) = 0.5 * (sinBetaY * startSign + eSunOrf(1)) - +0.5 * (sinBetaY * startSign - eSunOrf(1)) * cos(PI * abs(eSunOrf(0))/sinBetaX); - eSunAux(2) = sqrt(1 - SQR(eSunOrf(0)) - SQR(eSunAux(1))) * SGN(eSunOrf(2)); - attStatus.modelYaw = nominalYawGalIov(eSunAux); - } - else - { - startSign = 0; // reset when exit eclipse switchover region - } - return true; + auto& rSat = satGeom.rSat; + auto& vSatPrime = satGeom.vSatPrime; + auto& rSun = satGeom.rSun; + auto& beta = satGeom.beta; + auto& mu = satGeom.mu; + auto& startSign = attStatus.startSign; + + Vector3d eSunRac = ecef2rac(rSat, vSatPrime) * (rSun - rSat); + eSunRac.normalize(); + Vector3d eSunOrf; // Orbital reference frame + eSunOrf << eSunRac(1), -eSunRac(2), -eSunRac(0); + attStatus.nominalYaw = nominalYawGalIov(eSunOrf); + wrapPlusMinusPi(attStatus.nominalYaw); + attStatus.modelYaw = attStatus.nominalYaw; + + // Midnight/noon turns + double sinBetaX = sin(15 * D2R); + double sinBetaY = sin(2 * D2R); + if (abs(eSunOrf(0)) < sinBetaX && abs(eSunOrf(1)) < sinBetaY) + { + if (attStatus.modelYawTime == + GTime::noTime()) // Check sat not mid-way through eclipse on startup + return false; + + if (startSign == 0) // i.e. start of switchover period + startSign = SGN(eSunOrf(1)); + + Vector3d eSunAux; + eSunAux(0) = eSunOrf(0); + eSunAux(1) = + 0.5 * (sinBetaY * startSign + eSunOrf(1)) + + 0.5 * (sinBetaY * startSign - eSunOrf(1)) * cos(PI * abs(eSunOrf(0)) / sinBetaX); + eSunAux(2) = sqrt(1 - SQR(eSunOrf(0)) - SQR(eSunAux(1))) * SGN(eSunOrf(2)); + attStatus.modelYaw = nominalYawGalIov(eSunAux); + } + else + { + startSign = 0; // reset when exit eclipse switchover region + } + return true; } /** Smoothed yaw steering for GAL & BDS -*/ + */ double smoothedYaw( - double startYaw, ///< Satellite yaw at start of modified yaw steering - GTime startTime, ///< Start time of modified yaw steering (due to noon/midnight turn) - GTime time, ///< Solution time - double tMax) ///< Maximum mnvr time + double startYaw, ///< Satellite yaw at start of modified yaw steering + GTime startTime, ///< Start time of modified yaw steering (due to noon/midnight turn) + GTime time, ///< Solution time + double tMax ///< Maximum mnvr time +) { - double sign = SGN(startYaw); - double dtSinceStart = (time - startTime).to_double(); - return PI / 2 * sign + (startYaw - PI / 2 * sign) * cos(2 * PI / tMax * dtSinceStart); + double sign = SGN(startYaw); + double dtSinceStart = (time - startTime).to_double(); + return PI / 2 * sign + (startYaw - PI / 2 * sign) * cos(2 * PI / tMax * dtSinceStart); } /** Calculates colinear angle - the scalar angle between midnight or noon, whichever is closest -*/ -double colinearAngle( - double mu) ///< Mu angle (rad) + */ +double colinearAngle(double mu) ///< Mu angle (rad) { - double colinearAngle = abs(mu); - if (abs(mu) > PI / 2) - colinearAngle = PI - abs(mu); - return colinearAngle; + double colinearAngle = abs(mu); + if (abs(mu) > PI / 2) + colinearAngle = PI - abs(mu); + return colinearAngle; } /** Yaw model for GAL FOC sats. Follows IGS convention. * Returns false if no modelled yaw available * Ref: https://www.gsc-europa.eu/support-to-developers/galileo-satellite-metadata#3.1 -*/ + */ bool satYawGalFoc( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite yaw status - GTime time, ///< Solution time - SatGeom& satGeom) ///< Satellite geometry + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite yaw status + GTime time, ///< Solution time + SatGeom& satGeom ///< Satellite geometry +) { - auto& beta = satGeom.beta; - auto& betaRate = satGeom.betaRate; - auto& mu = satGeom.mu; - auto& muRate = satGeom.muRate; - auto& nominalYaw = attStatus.nominalYaw; - auto& modelYaw = attStatus.modelYaw; - auto& modelYawTime = attStatus.modelYawTime; - auto& startTime = attStatus.startTime; - auto& startYaw = attStatus.startYaw; - - nominalYaw = nominalYawGps(beta, mu); wrapPlusMinusPi(nominalYaw); - double betaThresh = 4.1 * D2R; - double colAngThresh = 10 * D2R; - if ( abs(beta) < betaThresh - &&colinearAngle(mu) < colAngThresh) - { - if (startTime == GTime::noTime()) // Find start of modified-steering period - { - GTime currTime = time; - double currMu = mu; - double dt = -1; - while (colinearAngle(currMu) < colAngThresh) // Ignore beta when finding start of modified-steering period - { - currTime += dt; - currMu = mu + muRate * (currTime - time).to_double(); - wrapPlusMinusPi(currMu); - } - if (startTime == GTime::noTime()) - startTime = currTime; - - startYaw = nominalYawGpsAtTime(satGeom, time, currTime); - wrapPlusMinusPi(startYaw); - } - modelYaw = smoothedYaw(startYaw, startTime, time, 5656); - } - else - { - modelYaw = nominalYaw; - startTime = GTime::noTime(); - startYaw = 0; - } - - return true; + auto& beta = satGeom.beta; + auto& betaRate = satGeom.betaRate; + auto& mu = satGeom.mu; + auto& muRate = satGeom.muRate; + auto& nominalYaw = attStatus.nominalYaw; + auto& modelYaw = attStatus.modelYaw; + auto& modelYawTime = attStatus.modelYawTime; + auto& startTime = attStatus.startTime; + auto& startYaw = attStatus.startYaw; + + nominalYaw = nominalYawGps(beta, mu); + wrapPlusMinusPi(nominalYaw); + double betaThresh = 4.1 * D2R; + double colAngThresh = 10 * D2R; + if (abs(beta) < betaThresh && colinearAngle(mu) < colAngThresh) + { + if (startTime == GTime::noTime()) // Find start of modified-steering period + { + GTime currTime = time; + double currMu = mu; + double dt = -1; + while (colinearAngle(currMu) < colAngThresh + ) // Ignore beta when finding start of modified-steering period + { + currTime += dt; + currMu = mu + muRate * (currTime - time).to_double(); + wrapPlusMinusPi(currMu); + } + if (startTime == GTime::noTime()) + startTime = currTime; + + startYaw = nominalYawGpsAtTime(satGeom, time, currTime); + wrapPlusMinusPi(startYaw); + } + modelYaw = smoothedYaw(startYaw, startTime, time, 5656); + } + else + { + modelYaw = nominalYaw; + startTime = GTime::noTime(); + startYaw = 0; + } + + return true; } /** Finds time when midnight/noon-centred max yaw started, previous to this point in time -*/ + */ void findCentredYawStart( - GTime earliestTime, ///< Search backwards until this time - GTime time, ///< Solution time - SatGeom& satGeom, ///< Satellite geometry - double maxYawRate, ///< Maximum yaw rate (rad/s) - GTime& startTime, ///< Time at max yaw start - double& startYaw, ///< Yaw at time of max yaw start - double dt = -1) ///< Time step to search backwards by (sec) + GTime earliestTime, ///< Search backwards until this time + GTime time, ///< Solution time + SatGeom& satGeom, ///< Satellite geometry + double maxYawRate, ///< Maximum yaw rate (rad/s) + GTime& startTime, ///< Time at max yaw start + double& startYaw, ///< Yaw at time of max yaw start + double dt = -1 ///< Time step to search backwards by (sec) +) { - auto& mu = satGeom.mu; - auto& muRate = satGeom.muRate; - - GTime currTime = time; - double currNominalYaw = 0; - while (time > earliestTime) - { - currTime += -1; - currNominalYaw = nominalYawGpsAtTime(satGeom, time, currTime); - double currMu = mu + muRate * (currTime - time).to_double(); - double yawFromMid = abs(PI / 2 - abs(currNominalYaw)); wrapPlusMinusPi(yawFromMid); - double angleFromNoon= PI - currMu; wrapPlusMinusPi(angleFromNoon); - double angleFromMid = std::min(abs(currMu), abs(angleFromNoon)); wrapPlusMinusPi(angleFromMid); // Orbital angle - double timeTillMid = angleFromMid / muRate; - double midYawRate = yawFromMid / timeTillMid; - if (abs(midYawRate) < maxYawRate) - break; - } - startTime = currTime; - startYaw = currNominalYaw; + auto& mu = satGeom.mu; + auto& muRate = satGeom.muRate; + + GTime currTime = time; + double currNominalYaw = 0; + while (time > earliestTime) + { + currTime += -1; + currNominalYaw = nominalYawGpsAtTime(satGeom, time, currTime); + double currMu = mu + muRate * (currTime - time).to_double(); + double yawFromMid = abs(PI / 2 - abs(currNominalYaw)); + wrapPlusMinusPi(yawFromMid); + double angleFromNoon = PI - currMu; + wrapPlusMinusPi(angleFromNoon); + double angleFromMid = std::min(abs(currMu), abs(angleFromNoon)); + wrapPlusMinusPi(angleFromMid); // Orbital angle + double timeTillMid = angleFromMid / muRate; + double midYawRate = yawFromMid / timeTillMid; + if (abs(midYawRate) < maxYawRate) + break; + } + startTime = currTime; + startYaw = currNominalYaw; } /** Yaw model for GLONASS sats * Returns false if no modelled yaw available * Ref: https://igsac-cnes.cls.fr/documents/meeting/2021_04_28_Strasser_et_al_EGU21.pdf -*/ + */ bool satYawGlo( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom) ///< Satellite geometry + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom ///< Satellite geometry +) { - auto& rSat = satGeom.rSat; - auto& vSatPrime = satGeom.vSatPrime; - auto& rSun = satGeom.rSun; - auto& rMoon = satGeom.rMoon; - auto& beta = satGeom.beta; - auto& mu = satGeom.mu; - auto& muRate = satGeom.muRate; - auto& nominalYaw = attStatus.nominalYaw; - auto& modelYaw = attStatus.modelYaw; - auto& startTime = attStatus.startTime; - auto& startSign = attStatus.startSign; - auto& startYaw = attStatus.startYaw; - - // Nominal behaviour - nominalYaw = nominalYawGps(beta, mu); - - double maxYawRate = 0.25 * D2R; - bool maxYawRateFound = getSnxSatMaxYawRate(Sat.svn(), time, maxYawRate); - if (maxYawRateFound == false) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Max yaw rate not found for " << Sat.svn() << " in " << __FUNCTION__ - << ", check sinex files for '+SATELLITE/YAW_BIAS_RATE' block"; - - return false; - } - - // Eclipse turn - Shadow max yaw steering and stop - if (inEclipse(rSat, rSun, rMoon)) - { - if (startTime == GTime::noTime()) // Start of eclipse - { - startTime = findEclipseBoundaries(time, satGeom, false); - startYaw = nominalYawGpsAtTime(satGeom, time, startTime); - - startSign = -SGN(abs(startYaw) - PI / 2); - if (startYaw < 0) - startSign *= -1; - } - double endYaw = startSign * (PI - abs(startYaw)); - double dYaw = endYaw - startYaw; - wrapPlusMinusPi(dYaw); - GTime endTime = startTime + abs(dYaw) / maxYawRate; - if (time < endTime) - { - modelYaw = startYaw + startSign * maxYawRate * (time - startTime).to_double(); - wrapPlusMinusPi(modelYaw); - } - else - { - modelYaw = endYaw; - } - return true; - } - - double yawFromNoon = abs(PI / 2 - abs(nominalYaw)); wrapPlusMinusPi(yawFromNoon); // from current position to noon - double angleFromNoon = PI - mu; wrapPlusMinusPi(angleFromNoon); - double timeTillNoon = angleFromNoon / muRate; - double noonYawRate = yawFromNoon / timeTillNoon; - if (abs(noonYawRate) > maxYawRate) - { - if (startTime == GTime::noTime()) - { - findCentredYawStart(attStatus.modelYawTime, time, satGeom, maxYawRate, startTime, startYaw); - startSign = -SGN(abs(startYaw) - PI / 2); - if (startYaw < 0) - startSign *= -1; - } - modelYaw = startYaw + startSign * maxYawRate * (time - startTime).to_double(); - } - else - { - modelYaw = nominalYaw; - startTime = GTime::noTime(); - startSign = 0; - startYaw = 0; - } - return true; + auto& rSat = satGeom.rSat; + auto& vSatPrime = satGeom.vSatPrime; + auto& rSun = satGeom.rSun; + auto& rMoon = satGeom.rMoon; + auto& beta = satGeom.beta; + auto& mu = satGeom.mu; + auto& muRate = satGeom.muRate; + auto& nominalYaw = attStatus.nominalYaw; + auto& modelYaw = attStatus.modelYaw; + auto& startTime = attStatus.startTime; + auto& startSign = attStatus.startSign; + auto& startYaw = attStatus.startYaw; + + // Nominal behaviour + nominalYaw = nominalYawGps(beta, mu); + + double maxYawRate = 0.25 * D2R; + bool maxYawRateFound = getSnxSatMaxYawRate(Sat.svn(), time, maxYawRate); + if (maxYawRateFound == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Max yaw rate not found for " << Sat.svn() << " in " << __FUNCTION__ + << ", check sinex files for '+SATELLITE/YAW_BIAS_RATE' block"; + + return false; + } + + // Eclipse turn - Shadow max yaw steering and stop + if (inEclipse(rSat, rSun, rMoon)) + { + if (startTime == GTime::noTime()) // Start of eclipse + { + startTime = findEclipseBoundaries(time, satGeom, false); + startYaw = nominalYawGpsAtTime(satGeom, time, startTime); + + startSign = -SGN(abs(startYaw) - PI / 2); + if (startYaw < 0) + startSign *= -1; + } + double endYaw = startSign * (PI - abs(startYaw)); + double dYaw = endYaw - startYaw; + wrapPlusMinusPi(dYaw); + GTime endTime = startTime + abs(dYaw) / maxYawRate; + if (time < endTime) + { + modelYaw = startYaw + startSign * maxYawRate * (time - startTime).to_double(); + wrapPlusMinusPi(modelYaw); + } + else + { + modelYaw = endYaw; + } + return true; + } + + double yawFromNoon = abs(PI / 2 - abs(nominalYaw)); + wrapPlusMinusPi(yawFromNoon); // from current position to noon + double angleFromNoon = PI - mu; + wrapPlusMinusPi(angleFromNoon); + double timeTillNoon = angleFromNoon / muRate; + double noonYawRate = yawFromNoon / timeTillNoon; + if (abs(noonYawRate) > maxYawRate) + { + if (startTime == GTime::noTime()) + { + findCentredYawStart( + attStatus.modelYawTime, + time, + satGeom, + maxYawRate, + startTime, + startYaw + ); + startSign = -SGN(abs(startYaw) - PI / 2); + if (startYaw < 0) + startSign *= -1; + } + modelYaw = startYaw + startSign * maxYawRate * (time - startTime).to_double(); + } + else + { + modelYaw = nominalYaw; + startTime = GTime::noTime(); + startSign = 0; + startYaw = 0; + } + return true; } /** Yaw model for GLONASS Block K sats * Note: no yaw model exists for GLONASS-K yet -*/ + */ bool satYawGloK( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom) ///< Satellite geometry + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom ///< Satellite geometry +) { - auto& beta = satGeom.beta; - auto& mu = satGeom.mu; - auto& nominalYaw = attStatus.nominalYaw; - auto& modelYaw = attStatus.modelYaw; - - // Nominal behaviour - nominalYaw = nominalYawGps(beta, mu); - modelYaw = nominalYaw; - return true; + auto& beta = satGeom.beta; + auto& mu = satGeom.mu; + auto& nominalYaw = attStatus.nominalYaw; + auto& modelYaw = attStatus.modelYaw; + + // Nominal behaviour + nominalYaw = nominalYawGps(beta, mu); + modelYaw = nominalYaw; + return true; } /** Orbit-normal mode yaw (always zero) -*/ + */ double orbitNormalYaw() { - return 0; + return 0; } /** Orbit normal yaw model - * Will set nominalYaw & modelYaw to the orbit-normal yaw (0 or PI) closest to the given nominal yaw. -*/ + * Will set nominalYaw & modelYaw to the orbit-normal yaw (0 or PI) closest to the given nominal + * yaw. + */ bool satYawOrbNor( - AttStatus& attStatus, ///< Satellite att status - double nominalYaw = 0) ///< Nominal yaw + AttStatus& attStatus, ///< Satellite att status + double nominalYaw = 0 ///< Nominal yaw +) { - wrapPlusMinusPi(nominalYaw); - double offset = 0; - if (abs(nominalYaw) > PI / 2) - offset = PI; - attStatus.nominalYaw = orbitNormalYaw() + offset; wrapPlusMinusPi(attStatus.modelYaw); - attStatus.modelYaw = orbitNormalYaw() + offset; wrapPlusMinusPi(attStatus.modelYaw); - return true; + wrapPlusMinusPi(nominalYaw); + double offset = 0; + if (abs(nominalYaw) > PI / 2) + offset = PI; + attStatus.nominalYaw = orbitNormalYaw() + offset; + wrapPlusMinusPi(attStatus.modelYaw); + attStatus.modelYaw = orbitNormalYaw() + offset; + wrapPlusMinusPi(attStatus.modelYaw); + return true; } /** Yaw model for QZSS-1 satellites - * Requires qzss_yaw_modes.snx - generated by scripts/qzss_ohi_merge.py and ohi-qzs*.txt's from https://qzss.go.jp/en/technical/qzssinfo/index.html - * Ref: https://qzss.go.jp/en/technical/qzssinfo/index.html -*/ + * Requires qzss_yaw_modes.snx - generated by scripts/qzss_ohi_merge.py and ohi-qzs*.txt's from + * https://qzss.go.jp/en/technical/qzssinfo/index.html Ref: + * https://qzss.go.jp/en/technical/qzssinfo/index.html + */ bool satYawQzs1( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom, ///< Satellite geometry - bool* orbitNormal = nullptr) ///< Returns true if satellite is in ON mode + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom, ///< Satellite geometry + bool* orbitNormal = nullptr ///< Returns true if satellite is in ON mode +) { - attStatus.nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); - attStatus.modelYaw = attStatus.nominalYaw; - if (orbitNormal) - *orbitNormal = false; - - string attMode; - bool attModeFound = getSnxSatAttMode(Sat.svn(), time, attMode); - if ( attModeFound - &&attMode.substr(0,2) == "ON") - { - satYawOrbNor(attStatus); - if (orbitNormal) - *orbitNormal = true; - } - - return attModeFound; + attStatus.nominalYaw = nominalYawGps(satGeom.beta, satGeom.mu); + attStatus.modelYaw = attStatus.nominalYaw; + if (orbitNormal) + *orbitNormal = false; + + string attMode; + bool attModeFound = getSnxSatAttMode(Sat.svn(), time, attMode); + if (attModeFound && attMode.substr(0, 2) == "ON") + { + satYawOrbNor(attStatus); + if (orbitNormal) + *orbitNormal = true; + } + + return attModeFound; } /** Yaw model for QZSS-2I & QZSS-2A satellites - * Requires qzss_yaw_modes.snx - generated by scripts/qzss_ohi_merge.py and ohi-qzs*.txt's from https://qzss.go.jp/en/technical/qzssinfo/index.html - * Ref: https://qzss.go.jp/en/technical/qzssinfo/index.html -*/ + * Requires qzss_yaw_modes.snx - generated by scripts/qzss_ohi_merge.py and ohi-qzs*.txt's from + * https://qzss.go.jp/en/technical/qzssinfo/index.html Ref: + * https://qzss.go.jp/en/technical/qzssinfo/index.html + */ bool satYawQzs2I( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom) ///< Satellite geometry + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom ///< Satellite geometry +) { - auto& mu = satGeom.mu; - auto& muRate = satGeom.muRate; - auto& startTime = attStatus.startTime; - auto& startSign = attStatus.startSign; - auto& startYaw = attStatus.startYaw; - auto& modelYaw = attStatus.modelYaw; - auto& nominalYaw = attStatus.nominalYaw; - - // Check for Orbit-Normal mode - double temp = modelYaw; - bool onMode; - bool modelYawValid = satYawQzs1(Sat, attStatus, time, satGeom, &onMode); - if (onMode) - return true; - modelYaw = temp; - - - // Centered max yaw steering around noon/midnight - double maxYawRate = 0.055 * D2R; // Ref: https://qzss.go.jp/en/technical/qzssinfo/khp0mf0000000wuf-att/spi-qzs2_c.pdf - double yawFromMid = abs(PI / 2 - abs(attStatus.nominalYaw)); wrapPlusMinusPi(yawFromMid); - double angleFromNoon = PI - mu; wrapPlusMinusPi(angleFromNoon); - double angleFromMid = std::min(abs(mu), abs(angleFromNoon)); wrapPlusMinusPi(angleFromMid); - double timeTillMid = angleFromMid / muRate; - double midYawRate = yawFromMid / timeTillMid; - if (abs(midYawRate) > maxYawRate) - { - if (startTime == GTime::noTime()) - { - findCentredYawStart(attStatus.modelYawTime, time, satGeom, maxYawRate, startTime, startYaw); - startSign = -SGN(abs(startYaw) - PI / 2); - if (startYaw < 0) - startSign *= -1; - } - modelYaw = startYaw + startSign * maxYawRate * (time - startTime).to_double(); - } - else - { - modelYaw = nominalYaw; - startTime = GTime::noTime(); - startSign = 0; - startYaw = 0; - } - return modelYawValid; + auto& mu = satGeom.mu; + auto& muRate = satGeom.muRate; + auto& startTime = attStatus.startTime; + auto& startSign = attStatus.startSign; + auto& startYaw = attStatus.startYaw; + auto& modelYaw = attStatus.modelYaw; + auto& nominalYaw = attStatus.nominalYaw; + + // Check for Orbit-Normal mode + double temp = modelYaw; + bool onMode; + bool modelYawValid = satYawQzs1(Sat, attStatus, time, satGeom, &onMode); + if (onMode) + return true; + modelYaw = temp; + + // Centered max yaw steering around noon/midnight + double maxYawRate = + 0.055 * + D2R; // Ref: https://qzss.go.jp/en/technical/qzssinfo/khp0mf0000000wuf-att/spi-qzs2_c.pdf + double yawFromMid = abs(PI / 2 - abs(attStatus.nominalYaw)); + wrapPlusMinusPi(yawFromMid); + double angleFromNoon = PI - mu; + wrapPlusMinusPi(angleFromNoon); + double angleFromMid = std::min(abs(mu), abs(angleFromNoon)); + wrapPlusMinusPi(angleFromMid); + double timeTillMid = angleFromMid / muRate; + double midYawRate = yawFromMid / timeTillMid; + if (abs(midYawRate) > maxYawRate) + { + if (startTime == GTime::noTime()) + { + findCentredYawStart( + attStatus.modelYawTime, + time, + satGeom, + maxYawRate, + startTime, + startYaw + ); + startSign = -SGN(abs(startYaw) - PI / 2); + if (startYaw < 0) + startSign *= -1; + } + modelYaw = startYaw + startSign * maxYawRate * (time - startTime).to_double(); + } + else + { + modelYaw = nominalYaw; + startTime = GTime::noTime(); + startSign = 0; + startYaw = 0; + } + return modelYawValid; } /** Yaw model for BDS-3I/3M satellites * Ref: https://doi.org/10.1007/s10291-018-0783-1 https://doi.org/10.1017/S0373463318000103 -*/ + */ bool satYawBds3( - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom, ///< Satellite geometry - double tMax) ///< Maximum mnvr time + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom, ///< Satellite geometry + double tMax ///< Maximum mnvr time +) { - auto& beta = satGeom.beta; - auto& betaRate = satGeom.betaRate; - auto& mu = satGeom.mu; - auto& muRate = satGeom.muRate; - attStatus.nominalYaw = nominalYawGps(beta, mu); - attStatus.modelYaw = attStatus.nominalYaw; - double alpha = PI - mu; wrapPlusMinusPi(alpha); - double beta0 = 3 * D2R; - double mu0 = 6 * D2R; - - if ( ( abs(beta) <= beta0) - &&( abs(alpha) <= mu0 - ||abs(mu) <= mu0)) - { - if ( alpha == 0 - ||beta == 0) - { - attStatus.modelYaw = nominalYawGps(SGN(beta) * beta0, mu); - return true; - } - - if (attStatus.startTime == GTime::noTime()) // start of switchover period - { - double currBeta = beta; - double currAlpha = alpha; - double currMu = mu; - GTime currTime = time; - while ( ( abs(currBeta) <= beta0) - &&( abs(currAlpha) <= mu0 - ||abs(currMu) <= mu0)) - { - currTime += -1; - currBeta = beta + betaRate * (currTime - time).to_double(); wrapPlusMinusPi(currBeta); - currMu = mu + muRate * (currTime - time).to_double(); wrapPlusMinusPi(currMu); - currAlpha = PI - currMu; wrapPlusMinusPi(currAlpha); - } - attStatus.startTime = currTime; - attStatus.startYaw = nominalYawGpsAtTime(satGeom, time, currTime); - } - - attStatus.modelYaw = smoothedYaw(attStatus.startYaw, attStatus.startTime, time, tMax); - } - else - { - attStatus.startTime = GTime::noTime(); - attStatus.startYaw = 0; - } - - return true; + auto& beta = satGeom.beta; + auto& betaRate = satGeom.betaRate; + auto& mu = satGeom.mu; + auto& muRate = satGeom.muRate; + attStatus.nominalYaw = nominalYawGps(beta, mu); + attStatus.modelYaw = attStatus.nominalYaw; + double alpha = PI - mu; + wrapPlusMinusPi(alpha); + double beta0 = 3 * D2R; + double mu0 = 6 * D2R; + + if ((abs(beta) <= beta0) && (abs(alpha) <= mu0 || abs(mu) <= mu0)) + { + if (alpha == 0 || beta == 0) + { + attStatus.modelYaw = nominalYawGps(SGN(beta) * beta0, mu); + return true; + } + + if (attStatus.startTime == GTime::noTime()) // start of switchover period + { + double currBeta = beta; + double currAlpha = alpha; + double currMu = mu; + GTime currTime = time; + while ((abs(currBeta) <= beta0) && (abs(currAlpha) <= mu0 || abs(currMu) <= mu0)) + { + currTime += -1; + currBeta = beta + betaRate * (currTime - time).to_double(); + wrapPlusMinusPi(currBeta); + currMu = mu + muRate * (currTime - time).to_double(); + wrapPlusMinusPi(currMu); + currAlpha = PI - currMu; + wrapPlusMinusPi(currAlpha); + } + attStatus.startTime = currTime; + attStatus.startYaw = nominalYawGpsAtTime(satGeom, time, currTime); + } + + attStatus.modelYaw = smoothedYaw(attStatus.startYaw, attStatus.startTime, time, tMax); + } + else + { + attStatus.startTime = GTime::noTime(); + attStatus.startYaw = 0; + } + + return true; } /** Yaw model for BDS-2I/2M satellites * Requires bds_yaw_modes.snx * Ref: https://doi.org/10.1007/s10291-018-0783-1 https://doi.org/10.1017/S0373463318000103 -*/ + */ bool satYawBds2( - SatSys& Sat, ///< Satellite ID - AttStatus& attStatus, ///< Satellite att status - GTime time, ///< Solution time - SatGeom& satGeom, ///< Satellite geometry - double tMax) ///< Maximum mnvr time + SatSys& Sat, ///< Satellite ID + AttStatus& attStatus, ///< Satellite att status + GTime time, ///< Solution time + SatGeom& satGeom, ///< Satellite geometry + double tMax ///< Maximum mnvr time +) { - auto& beta = satGeom.beta; - auto& betaRate = satGeom.betaRate; - auto& mu = satGeom.mu; - auto& muRate = satGeom.muRate; - auto& startTime = attStatus.startTime; - attStatus.nominalYaw = nominalYawGps(beta, mu); - - // Beta-dependent smoothed yaw - string attMode; - bool attModeFound = getSnxSatAttMode(Sat.svn(), time, attMode); - if ( attModeFound - &&attMode.substr(0,7) == "BETA_SY") - { - return satYawBds3(attStatus, time, satGeom, tMax); - } - - // Beta-dependent orbit-normal yaw - bool orbitNormalMode = false; - double betaThresh = 4 * D2R; - if (abs(beta) <= betaThresh) - { - if (PI - abs(attStatus.nominalYaw) < 5 * D2R) - { - orbitNormalMode = true; - startTime = time; - } - else - { - if (startTime != GTime::noTime()) - { - // Check if next yaw cycle has a qualifying yaw < 5deg & beta < 4deg - Vector3d& rSat = satGeom.rSat; - Vector3d& vSatPrime = satGeom.vSatPrime; - - double muAtPeak = PI / 2; // mu when yaw is closest to PI or -PI - if (muAtPeak < mu) - muAtPeak += 2 * PI; - - double muRange = muAtPeak - mu; - double timeTillPeak = muRange / muRate; - double betaAtPeak = beta + betaRate * timeTillPeak; - if (abs(betaAtPeak) <= betaThresh) - orbitNormalMode = true; - else - startTime = GTime::noTime(); - } - } - } - - if (orbitNormalMode) satYawOrbNor(attStatus, PI); - else attStatus.modelYaw = attStatus.nominalYaw; - - return true; + auto& beta = satGeom.beta; + auto& betaRate = satGeom.betaRate; + auto& mu = satGeom.mu; + auto& muRate = satGeom.muRate; + auto& startTime = attStatus.startTime; + attStatus.nominalYaw = nominalYawGps(beta, mu); + + // Beta-dependent smoothed yaw + string attMode; + bool attModeFound = getSnxSatAttMode(Sat.svn(), time, attMode); + if (attModeFound && attMode.substr(0, 7) == "BETA_SY") + { + return satYawBds3(attStatus, time, satGeom, tMax); + } + + // Beta-dependent orbit-normal yaw + bool orbitNormalMode = false; + double betaThresh = 4 * D2R; + if (abs(beta) <= betaThresh) + { + if (PI - abs(attStatus.nominalYaw) < 5 * D2R) + { + orbitNormalMode = true; + startTime = time; + } + else + { + if (startTime != GTime::noTime()) + { + // Check if next yaw cycle has a qualifying yaw < 5deg & beta < 4deg + Vector3d& rSat = satGeom.rSat; + Vector3d& vSatPrime = satGeom.vSatPrime; + + double muAtPeak = PI / 2; // mu when yaw is closest to PI or -PI + if (muAtPeak < mu) + muAtPeak += 2 * PI; + + double muRange = muAtPeak - mu; + double timeTillPeak = muRange / muRate; + double betaAtPeak = beta + betaRate * timeTillPeak; + if (abs(betaAtPeak) <= betaThresh) + orbitNormalMode = true; + else + startTime = GTime::noTime(); + } + } + } + + if (orbitNormalMode) + satYawOrbNor(attStatus, PI); + else + attStatus.modelYaw = attStatus.nominalYaw; + + return true; } /** Yaw model for BDS-3M-SECM satellites * Ref: https://doi.org/10.48550/arXiv.2112.13252 -*/ + */ bool satYawBds3Secm( - AttStatus& attStatus, ///< Satellite att status - SatGeom& satGeom) ///< Satellite geometry + AttStatus& attStatus, ///< Satellite att status + SatGeom& satGeom ///< Satellite geometry +) { - auto& beta = satGeom.beta; - auto& mu = satGeom.mu; - auto& startSign = attStatus.startSign; - attStatus.nominalYaw = nominalYawGps(beta, mu); - attStatus.modelYaw = attStatus.nominalYaw; - double beta0 = 3 * D2R; - if (abs(beta) <= beta0) - { - double absYaw = abs(nominalYawGps(beta0, mu)); - if ( ( startSign == 0) - ||( startSign != SGN(beta) - &&absYaw < 5 * D2R)) - { - startSign = SGN(beta); - } - attStatus.modelYaw = nominalYawGps(startSign * beta0, mu); - } - return true; + auto& beta = satGeom.beta; + auto& mu = satGeom.mu; + auto& startSign = attStatus.startSign; + attStatus.nominalYaw = nominalYawGps(beta, mu); + attStatus.modelYaw = attStatus.nominalYaw; + double beta0 = 3 * D2R; + if (abs(beta) <= beta0) + { + double absYaw = abs(nominalYawGps(beta0, mu)); + if ((startSign == 0) || (startSign != SGN(beta) && absYaw < 5 * D2R)) + { + startSign = SGN(beta); + } + attStatus.modelYaw = nominalYawGps(startSign * beta0, mu); + } + return true; } -/** Calculates unit vectors of satellite-fixed coordinates (ECEF) given yaw (assuming Z+ is facing toward Earth) +/** Calculates unit vectors of satellite-fixed coordinates (ECEF) given yaw (assuming Z+ is facing + * toward Earth) */ void yawToAttVecs( - Vector3d& rSat, ///< Sat position (ECEF) - Vector3d& satVel, ///< Sat velocity (ECEF) - double yaw, ///< Yaw (rad) - Vector3d& eXSat, ///< X+ unit vector of satellite-fixed coordinates (ECEF) - Vector3d& eYSat, ///< Y+ unit vector of satellite-fixed coordinates (ECEF) - Vector3d& eZSat) ///< Z+ unit vector of satellite-fixed coordinates (ECEF) + Vector3d& rSat, ///< Sat position (ECEF) + Vector3d& satVel, ///< Sat velocity (ECEF) + double yaw, ///< Yaw (rad) + Vector3d& eXSat, ///< X+ unit vector of satellite-fixed coordinates (ECEF) + Vector3d& eYSat, ///< Y+ unit vector of satellite-fixed coordinates (ECEF) + Vector3d& eZSat ///< Z+ unit vector of satellite-fixed coordinates (ECEF) +) { - Vector3d vSatPrime = satVel; - vSatPrime[0] -= OMGE * rSat[1]; - vSatPrime[1] += OMGE * rSat[0]; - Vector3d n = rSat.cross(vSatPrime); //orbit-axis - Vector3d en = n. normalized(); - Vector3d eSat = rSat. normalized(); - Vector3d ex = en.cross(eSat); - - double cosy = cos(yaw); - double siny = sin(yaw); - eXSat = siny * en - cosy * ex; - eYSat = cosy * en + siny * ex; - eZSat = eXSat.cross(eYSat); + Vector3d vSatPrime = satVel; + vSatPrime[0] -= OMGE * rSat[1]; + vSatPrime[1] += OMGE * rSat[0]; + Vector3d n = rSat.cross(vSatPrime); // orbit-axis + Vector3d en = n.normalized(); + Vector3d eSat = rSat.normalized(); + Vector3d ex = en.cross(eSat); + + double cosy = cos(yaw); + double siny = sin(yaw); + eXSat = siny * en - cosy * ex; + eYSat = cosy * en + siny * ex; + eZSat = eXSat.cross(eYSat); } /** Calculates nominal & model yaw -*/ + */ void updateSatYaw( - SatPos& satPos, ///< Observation - AttStatus& attStatus) ///< Satellite att status. Use a disposable copy if calling inside multithreaded code + SatPos& satPos, ///< Observation + AttStatus& attStatus ///< Satellite att status. Use a disposable copy if calling inside + ///< multithreaded code +) { - if (satPos.posTime <= attStatus.modelYawTime) - return; // The requested time is in the past, all the models are designed to propagate forwards, so we're out of luck. - - - auto& modelYawValid = attStatus.modelYawValid; - auto& Sat = satPos.Sat; - auto& time = satPos.posTime; - SatGeom satGeom = satOrbitGeometry(satPos); - - string blockTypeStr = Sat.blockType(); - std::replace(blockTypeStr.begin(), blockTypeStr.end(), '-', '_'); - std::replace(blockTypeStr.begin(), blockTypeStr.end(), '+', 'P'); - E_Block blockType = E_Block::UNKNOWN; - if (E_Block::_is_valid(blockTypeStr.c_str())) - blockType = E_Block::_from_string_nocase(blockTypeStr.c_str()); - - switch (blockType) - { - case E_Block::GPS_I: // Unmodelled - case E_Block::GPS_II: - case E_Block::GPS_IIA: { modelYawValid = satYawGpsIIA (Sat, attStatus, time, satGeom); break; } - case E_Block::GPS_IIR_A: - case E_Block::GPS_IIR_B: - case E_Block::GPS_IIR_M: { modelYawValid = satYawGpsIIR (Sat, attStatus, time, satGeom); break; } - case E_Block::GPS_IIF: { modelYawValid = satYawGpsIIF (Sat, attStatus, time, satGeom); break; } - case E_Block::GPS_IIIA: { modelYawValid = satYawGpsIII ( attStatus, satGeom); break; } - case E_Block::GLO_M: - case E_Block::GLO_MP: - case E_Block::GLO: { modelYawValid = satYawGlo (Sat, attStatus, time, satGeom); break; } - case E_Block::GLO_K1A: - case E_Block::GLO_K1B: - case E_Block::GLO_K2: { modelYawValid = satYawGloK (Sat, attStatus, time, satGeom); break; } - case E_Block::GAL_0A: // Unmodelled - case E_Block::GAL_0B: // Unmodelled - case E_Block::GAL_1: { modelYawValid = satYawGalIov (Sat, attStatus, time, satGeom); break; } - case E_Block::GAL_2: { modelYawValid = satYawGalFoc (Sat, attStatus, time, satGeom); break; } - case E_Block::BDS_2M: { modelYawValid = satYawBds2 (Sat, attStatus, time, satGeom, 3090); break; } - case E_Block::BDS_2G: { modelYawValid = satYawOrbNor ( attStatus, PI); break; } - case E_Block::BDS_2I: { 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_3I: { modelYawValid = satYawBds3 ( attStatus, time, satGeom, 5740); break; } - case E_Block::BDS_3M_CAST: { modelYawValid = satYawBds3 ( attStatus, time, satGeom, 3090); break; } - case E_Block::BDS_3M_SECM_A:{ modelYawValid = satYawBds3Secm ( attStatus, satGeom); break; } - case E_Block::BDS_3G: { modelYawValid = satYawOrbNor ( attStatus, PI); break; } - case E_Block::BDS_3M_SECM_B:{ modelYawValid = satYawBds3Secm ( attStatus, satGeom); break; } - case E_Block::QZS_1: { modelYawValid = satYawQzs1 (Sat, attStatus, time, satGeom); break; } - case E_Block::QZS_2G: { modelYawValid = satYawOrbNor ( attStatus, PI); break; } - case E_Block::QZS_2A: - case E_Block::QZS_2I: { modelYawValid = satYawQzs2I (Sat, attStatus, time, satGeom); break; } - case E_Block::IRS_1I: // Unmodelled - case E_Block::IRS_1G: // Unmodelled - case E_Block::IRS_2G: // Unmodelled - default: { - modelYawValid = false; - satYawGpsIIR (Sat, attStatus, time, satGeom); - BOOST_LOG_TRIVIAL(warning) << "Warning: Attitude model not implemented for " << Sat.blockType() << " in " << __FUNCTION__ << "; using GPS-IIR model instead."; - } - } - - if (modelYawValid) - attStatus.modelYawTime = time; + if (satPos.posTime <= attStatus.modelYawTime) + return; // The requested time is in the past, all the models are designed to propagate + // forwards, so we're out of luck. + + auto& modelYawValid = attStatus.modelYawValid; + auto& Sat = satPos.Sat; + auto& time = satPos.posTime; + SatGeom satGeom = satOrbitGeometry(satPos); + + string blockTypeStr = Sat.blockType(); + std::replace(blockTypeStr.begin(), blockTypeStr.end(), '-', '_'); + std::replace(blockTypeStr.begin(), blockTypeStr.end(), '+', 'P'); + E_Block blockType = E_Block::UNKNOWN; + if (is_valid_enum_string(blockTypeStr.c_str())) + blockType = string_to_enum_nocase_throw(blockTypeStr.c_str()); + + switch (blockType) + { + case E_Block::GPS_I: // Unmodelled + case E_Block::GPS_II: + case E_Block::GPS_IIA: + { + modelYawValid = satYawGpsIIA(Sat, attStatus, time, satGeom); + break; + } + case E_Block::GPS_IIR_A: + case E_Block::GPS_IIR_B: + case E_Block::GPS_IIR_M: + { + modelYawValid = satYawGpsIIR(Sat, attStatus, time, satGeom); + break; + } + case E_Block::GPS_IIF: + { + modelYawValid = satYawGpsIIF(Sat, attStatus, time, satGeom); + break; + } + case E_Block::GPS_IIIA: + { + modelYawValid = satYawGpsIII(attStatus, satGeom); + break; + } + case E_Block::GLO_M: + case E_Block::GLO_MP: + case E_Block::GLO: + { + modelYawValid = satYawGlo(Sat, attStatus, time, satGeom); + break; + } + case E_Block::GLO_K1A: + case E_Block::GLO_K1B: + case E_Block::GLO_K2: + { + modelYawValid = satYawGloK(Sat, attStatus, time, satGeom); + break; + } + case E_Block::GAL_0A: // Unmodelled + case E_Block::GAL_0B: // Unmodelled + case E_Block::GAL_1: + { + modelYawValid = satYawGalIov(Sat, attStatus, time, satGeom); + break; + } + case E_Block::GAL_2: + { + modelYawValid = satYawGalFoc(Sat, attStatus, time, satGeom); + break; + } + case E_Block::BDS_2M: + { + modelYawValid = satYawBds2(Sat, attStatus, time, satGeom, 3090); + break; + } + case E_Block::BDS_2G: + { + modelYawValid = satYawOrbNor(attStatus, PI); + break; + } + case E_Block::BDS_2I: + { + 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_3I: + { + modelYawValid = satYawBds3(attStatus, time, satGeom, 5740); + break; + } + case E_Block::BDS_3M_CAST: + { + modelYawValid = satYawBds3(attStatus, time, satGeom, 3090); + break; + } + case E_Block::BDS_3M_SECM_A: + { + modelYawValid = satYawBds3Secm(attStatus, satGeom); + break; + } + case E_Block::BDS_3G: + { + modelYawValid = satYawOrbNor(attStatus, PI); + break; + } + case E_Block::BDS_3M_SECM_B: + { + modelYawValid = satYawBds3Secm(attStatus, satGeom); + break; + } + case E_Block::QZS_1: + { + modelYawValid = satYawQzs1(Sat, attStatus, time, satGeom); + break; + } + case E_Block::QZS_2G: + { + modelYawValid = satYawOrbNor(attStatus, PI); + break; + } + case E_Block::QZS_2A: + case E_Block::QZS_2I: + { + modelYawValid = satYawQzs2I(Sat, attStatus, time, satGeom); + break; + } + case E_Block::IRS_1I: // Unmodelled + case E_Block::IRS_1G: // Unmodelled + case E_Block::IRS_2G: // Unmodelled + default: + { + modelYawValid = false; + satYawGpsIIR(Sat, attStatus, time, satGeom); + BOOST_LOG_TRIVIAL(warning) + << "Attitude model not implemented for " << Sat.blockType() << " in " + << __FUNCTION__ << "; using GPS-IIR model instead."; + } + } + + if (modelYawValid) + attStatus.modelYawTime = time; } /** Recalls satellite nominal/model attitude * Returns false if attitude is invalid -*/ + */ bool satAttModel( - Vector3d& rSat, ///< Satellite position (ECEF) - Vector3d& vSat, ///< Satellite velocity (ECEF) - AttStatus& attStatus, ///< Attitude status for orientation - E_Source source) ///< Type of attitude model to return + Vector3d& rSat, ///< Satellite position (ECEF) + Vector3d& vSat, ///< Satellite velocity (ECEF) + AttStatus& attStatus, ///< Attitude status for orientation + E_Source source ///< Type of attitude model to return +) { - double yaw; - if (source == +E_Source::NOMINAL) yaw = attStatus.nominalYaw; //Nominal yaw only - no advanced noon/midnight turns - else yaw = attStatus.modelYaw; + double yaw; + if (source == E_Source::NOMINAL) + yaw = attStatus.nominalYaw; // Nominal yaw only - no advanced noon/midnight turns + else + yaw = attStatus.modelYaw; - bool pass = false; - if ( attStatus.modelYawValid - ||source == +E_Source::NOMINAL) - { - pass = true; - } + bool pass = false; + if (attStatus.modelYawValid || source == E_Source::NOMINAL) + { + pass = true; + } - yawToAttVecs(rSat, vSat, yaw, attStatus.eXBody, attStatus.eYBody, attStatus.eZBody); + yawToAttVecs(rSat, vSat, yaw, attStatus.eXBody, attStatus.eYBody, attStatus.eZBody); - return pass; + return pass; } /** Converts coords of frame A (expressed in frame G) into transformation matrix from A to G @@ -1304,255 +1454,293 @@ bool satAttModel( * transform from body frame to ECEF frame: ecef = rot * body. */ Matrix3d rotBasisMat( - Vector3d& eX, ///< X+ unit vector of new coordinates - Vector3d& eY, ///< Y+ unit vector of new coordinates - Vector3d& eZ) ///< Z+ unit vector of new coordinates + Vector3d& eX, ///< X+ unit vector of new coordinates + Vector3d& eY, ///< Y+ unit vector of new coordinates + Vector3d& eZ ///< Z+ unit vector of new coordinates +) { - Matrix3d rot; - rot.col(0) = eX; - rot.col(1) = eY; - rot.col(2) = eZ; + Matrix3d rot; + rot.col(0) = eX; + rot.col(1) = eY; + rot.col(2) = eZ; - return rot; + return rot; } /** Calculates antenna attitude - unit vectors of antenna-fixed coordinates (ECEF) -*/ + */ void updateAntAtt( - Vector3d& bore, ///< Sensor boresight vector (body frame) - Vector3d& azim, ///< Sensor azimuth vector (body frame) - AttStatus& attStatus) ///< Attitude status + Vector3d& bore, ///< Sensor boresight vector (body frame) + Vector3d& azim, ///< Sensor azimuth vector (body frame) + AttStatus& attStatus ///< Attitude status +) { - Vector3d eE = azim.cross(bore); - Vector3d eN = azim; - Vector3d eU = bore; + Vector3d eE = azim.cross(bore); + Vector3d eN = azim; + Vector3d eU = bore; - Matrix3d ant2Body = rotBasisMat(eE, eN, eU); - Matrix3d body2Ecef = rotBasisMat(attStatus.eXBody, attStatus.eYBody, attStatus.eZBody); - Matrix3d ant2Ecef = body2Ecef * ant2Body; + Matrix3d ant2Body = rotBasisMat(eE, eN, eU); + Matrix3d body2Ecef = rotBasisMat(attStatus.eXBody, attStatus.eYBody, attStatus.eZBody); + Matrix3d ant2Ecef = body2Ecef * ant2Body; - attStatus.eXAnt = ant2Ecef.col(0); - attStatus.eYAnt = ant2Ecef.col(1); - attStatus.eZAnt = ant2Ecef.col(2); + attStatus.eXAnt = ant2Ecef.col(0); + attStatus.eYAnt = ant2Ecef.col(1); + attStatus.eZAnt = ant2Ecef.col(2); } /** Retrieves precise attitude (from file) - unit vectors of satellite-fixed coordinates (ECEF) - * Returns false if no attitude available (e.g. not found in file, not enough data to interpolate, etc.) -*/ + * Returns false if no attitude available (e.g. not found in file, not enough data to interpolate, + * etc.) + */ bool preciseAttitude( - string id, ///< Satellite/receiver ID - GTime time, ///< Solution time - AttStatus& attStatus) ///< Attitude status + string id, ///< Satellite/receiver ID + GTime time, ///< Solution time + AttStatus& attStatus ///< Attitude status +) { - auto attMapItr = nav.attMapMap.find(id); if (attMapItr == nav.attMapMap.end()) return false; - auto& [dummy, attMap] = *attMapItr; - auto entryItr2 = attMap.lower_bound(time); if (entryItr2 == attMap .end()) return false; - - auto entryItr1 = entryItr2; - if (entryItr1 != attMap.begin()) entryItr1--; - else if (entryItr2 != attMap.end()) entryItr2++; - else return false; - - auto& [dummy1, entry1] = *entryItr1; - auto& [dummy2, entry2] = *entryItr2; - Quaterniond quat1 = entry1.q; - Quaterniond quat2 = entry2.q; - GTime t1 = entry1.time; - GTime t2 = entry2.time; - - double frac = (time - t1).to_double() - / (t2 - t1).to_double(); - if ( frac < 0 - ||frac > 1) //note: Quaterniond::slerp only accepts frac = [0,1] - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Insufficient precise attitude data to perform slerp in " << __FUNCTION__; - return false; - } - - if ( t1 < time - 6.0 * 60 * 60 // 6hrs away (180deg behind orbitwise) is too far to slerp reliably - ||t2 > time + 6.0 * 60 * 60) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: No nearby precise attitude data to perform slerp in " << __FUNCTION__; - return false; - } - - Quaterniond quatNow = quat1.slerp(frac, quat2); - - Matrix3d body2Ecef = Matrix3d::Identity(); - switch (entry1.frame) - { - case E_ObxFrame::ECI: - { - Matrix3d eci2Body = quatNow.toRotationMatrix(); - - Matrix3d body2Eci = eci2Body.transpose(); - - ERPValues erpv = getErp(nav.erp, time); - - Matrix3d i2tMatrix; - eci2ecef(time, erpv, i2tMatrix); - - body2Ecef = i2tMatrix * body2Eci; - - break; - } - case E_ObxFrame::ECEF: - { - Matrix3d ecef2Body = quatNow.toRotationMatrix(); - - body2Ecef = ecef2Body.transpose(); - - break; - } - case E_ObxFrame::BCRS: - { - - - break; - } - default: - { - BOOST_LOG_TRIVIAL(error) - << "Error: Unknown frame type in " << __FUNCTION__ << ": " << entry1.frame._to_string(); - return false; - } - } - - attStatus.eXBody = body2Ecef.col(0); - attStatus.eYBody = body2Ecef.col(1); - attStatus.eZBody = body2Ecef.col(2); - - return true; + auto attMapItr = nav.attMapMap.find(id); + if (attMapItr == nav.attMapMap.end()) + return false; + auto& [dummy, attMap] = *attMapItr; + auto entryItr2 = attMap.lower_bound(time); + if (entryItr2 == attMap.end()) + return false; + + auto entryItr1 = entryItr2; + if (entryItr1 != attMap.begin()) + entryItr1--; + else if (entryItr2 != attMap.end()) + entryItr2++; + else + return false; + + auto& [dummy1, entry1] = *entryItr1; + auto& [dummy2, entry2] = *entryItr2; + Quaterniond quat1 = entry1.q; + Quaterniond quat2 = entry2.q; + GTime t1 = entry1.time; + GTime t2 = entry2.time; + + double frac = (time - t1).to_double() / (t2 - t1).to_double(); + if (frac < 0 || frac > 1) // note: Quaterniond::slerp only accepts frac = [0,1] + { + BOOST_LOG_TRIVIAL(warning) + << "Insufficient precise attitude data to perform slerp in " << __FUNCTION__; + return false; + } + + if (t1 < time - + 6.0 * 60 * 60 // 6hrs away (180deg behind orbitwise) is too far to slerp reliably + || t2 > time + 6.0 * 60 * 60) + { + BOOST_LOG_TRIVIAL(warning) + << "No nearby precise attitude data to perform slerp in " << __FUNCTION__; + return false; + } + + Quaterniond quatNow = quat1.slerp(frac, quat2); + + Matrix3d body2Ecef = Matrix3d::Identity(); + switch (entry1.frame) + { + case E_ObxFrame::ECI: + { + Matrix3d eci2Body = quatNow.toRotationMatrix(); + + Matrix3d body2Eci = eci2Body.transpose(); + + ERPValues erpv = getErp(nav.erp, time); + + Matrix3d i2tMatrix; + eci2ecef(time, erpv, i2tMatrix); + + body2Ecef = i2tMatrix * body2Eci; + + break; + } + case E_ObxFrame::ECEF: + { + Matrix3d ecef2Body = quatNow.toRotationMatrix(); + + body2Ecef = ecef2Body.transpose(); + + break; + } + case E_ObxFrame::BCRS: + { + break; + } + default: + { + BOOST_LOG_TRIVIAL(error) + << "Unknown frame type in " << __FUNCTION__ << ": " << enum_to_string(entry1.frame); + return false; + } + } + + attStatus.eXBody = body2Ecef.col(0); + attStatus.eYBody = body2Ecef.col(1); + attStatus.eZBody = body2Ecef.col(2); + + return true; } bool kalmanAttitude( - string id, ///< Satellite/receiver ID - GTime time, ///< Solution time - AttStatus& attStatus, ///< Attitude status - const KFState* kfState_ptr) + string id, ///< Satellite/receiver ID + GTime time, ///< Solution time + AttStatus& attStatus, ///< Attitude status + const KFState* kfState_ptr +) { - if (kfState_ptr == nullptr) - { - return false; - } - - auto& kfState = *kfState_ptr; - - bool found = true; - - Quaterniond quat; - - for (int i = 0; i < 4; i++) - { - KFKey kfKey; - kfKey.type = KF::ORIENTATION; - kfKey.str = id; - kfKey.num = i; - - if (i == 0) { found &= kfState.getKFValue(kfKey, quat.w()); } - if (i == 1) { found &= kfState.getKFValue(kfKey, quat.x()); } - if (i == 2) { found &= kfState.getKFValue(kfKey, quat.y()); } - if (i == 3) { found &= kfState.getKFValue(kfKey, quat.z()); } - } - - if (found == false) - { - return false; - } - - quat.normalize(); - - Matrix3d body2Ecef = quat.toRotationMatrix().transpose(); - - attStatus.eXBody = body2Ecef.col(0); - attStatus.eYBody = body2Ecef.col(1); - attStatus.eZBody = body2Ecef.col(2); - - return true; + if (kfState_ptr == nullptr) + { + return false; + } + + auto& kfState = *kfState_ptr; + + bool found = true; + + Quaterniond quat; + + for (int i = 0; i < 4; i++) + { + KFKey kfKey; + kfKey.type = KF::ORIENTATION; + kfKey.str = id; + kfKey.num = i; + + if (i == 0) + { + found = found && (kfState.getKFValue(kfKey, quat.w()) != E_Source::NONE); + } + if (i == 1) + { + found = found && (kfState.getKFValue(kfKey, quat.x()) != E_Source::NONE); + } + if (i == 2) + { + found = found && (kfState.getKFValue(kfKey, quat.y()) != E_Source::NONE); + } + if (i == 3) + { + found = found && (kfState.getKFValue(kfKey, quat.z()) != E_Source::NONE); + } + } + + if (found == false) + { + return false; + } + + quat.normalize(); + + Matrix3d body2Ecef = quat.toRotationMatrix().transpose(); + + attStatus.eXBody = body2Ecef.col(0); + attStatus.eYBody = body2Ecef.col(1); + attStatus.eZBody = body2Ecef.col(2); + + return true; } /** Calculates satellite attitude - unit vectors of satellite-fixed coordinates (ECEF) * Returns false if no attitude available (e.g. due to eclipse, not found in file, etc.) -*/ + */ bool updateSatAtt( - SatSys& Sat, ///< Satellite ID - GTime time, ///< Solution time - VectorEcef& rSat, ///< Satellite position (ECEF) - VectorEcef& vSat, ///< Satellite velocity (ECEF) - vector attitudeTypes, ///< Attitude type - AttStatus& attStatus) ///< Attitude status + SatSys& Sat, ///< Satellite ID + GTime time, ///< Solution time + VectorEcef& rSat, ///< Satellite position (ECEF) + VectorEcef& vSat, ///< Satellite velocity (ECEF) + vector attitudeTypes, ///< Attitude type + AttStatus& attStatus ///< Attitude status +) { - bool valid = false; - for (auto attitudeType : attitudeTypes) - { - switch (attitudeType) - { - default: BOOST_LOG_TRIVIAL(error) << "Unknown attitudeType in " << __FUNCTION__ << ": " << attitudeType._to_string(); - case E_Source::NOMINAL: { valid = satAttModel ( rSat, vSat, attStatus, E_Source::NOMINAL); break; } - case E_Source::MODEL: { valid = satAttModel ( rSat, vSat, attStatus, E_Source::MODEL); break; } - case E_Source::PRECISE: { valid = preciseAttitude (Sat, time, attStatus); break; } - } - - if (valid) - { - break; - } - } - return valid; + bool valid = false; + for (auto attitudeType : attitudeTypes) + { + switch (attitudeType) + { + default: + BOOST_LOG_TRIVIAL(error) << "Unknown attitudeType in " << __FUNCTION__ << ": " + << enum_to_string(attitudeType); + case E_Source::NOMINAL: + { + valid = satAttModel(rSat, vSat, attStatus, E_Source::NOMINAL); + break; + } + case E_Source::MODEL: + { + valid = satAttModel(rSat, vSat, attStatus, E_Source::MODEL); + break; + } + case E_Source::PRECISE: + { + valid = preciseAttitude(Sat, time, attStatus); + break; + } + } + + if (valid) + { + break; + } + } + return valid; } /** Satellite attitude - calculates unit vectors of satellite-fixed coordinates (ECEF) * Returns false if no attitude available (usually due to eclipse) -*/ + */ bool updateSatAtt( - SatPos& satPos, ///< satellite position data - vector attitudeTypes, ///< Attitude type - AttStatus& attStatus) ///< Attitude status + SatPos& satPos, ///< satellite position data + vector attitudeTypes, ///< Attitude type + AttStatus& attStatus ///< Attitude status +) { - return updateSatAtt( - satPos.Sat, - satPos.posTime, - satPos.rSatCom, - satPos.satVel, - attitudeTypes, - attStatus); + return updateSatAtt( + satPos.Sat, + satPos.posTime, + satPos.rSatCom, + satPos.satVel, + attitudeTypes, + attStatus + ); } /** Satellite attitude - calculates attitude of satellite as a quaternion (ECEF) -* Also transforms coordinates in body frame into ECEF -* Returns false if no attitude available (usually due to eclipse) -*/ + * Also transforms coordinates in body frame into ECEF + * Returns false if no attitude available (usually due to eclipse) + */ bool satQuat( - SatPos& satPos, ///< observation - vector attitudeTypes, ///< Attitude type - Quaterniond& quat) ///< Rotation of satellite from ECEF + SatPos& satPos, ///< observation + vector attitudeTypes, ///< Attitude type + Quaterniond& quat ///< Rotation of satellite from ECEF +) { - auto& attStatus = satPos.satNav_ptr->attStatus; + auto& attStatus = satPos.satNav_ptr->attStatus; - bool pass = updateSatAtt(satPos, attitudeTypes, attStatus); + bool pass = updateSatAtt(satPos, attitudeTypes, attStatus); - Matrix3d body2Ecef = rotBasisMat(attStatus.eXBody, attStatus.eYBody, attStatus.eZBody); + Matrix3d body2Ecef = rotBasisMat(attStatus.eXBody, attStatus.eYBody, attStatus.eZBody); - quat = Quaterniond(body2Ecef); + quat = Quaterniond(body2Ecef); - return pass; + return pass; } /** Update sat nominal/model yaws. * Call outside of multithreading code that may reference the same satellite in different threads */ -void updateSatAtts( - SatPos& satPos) ///< observation +void updateSatAtts(SatPos& satPos) ///< observation { - auto& satNav = *satPos.satNav_ptr; - auto& attStatus = satNav.attStatus; - auto& satOpts = acsConfig.getSatOpts(satPos.Sat); + auto& satNav = *satPos.satNav_ptr; + auto& attStatus = satNav.attStatus; + auto& satOpts = acsConfig.getSatOpts(satPos.Sat); - updateSatYaw(satPos, attStatus); - updateSatAtt(satPos, satOpts.attitudeModel.sources, attStatus); - updateAntAtt(satNav.antBoresight, satNav.antAzimuth, attStatus); + updateSatYaw(satPos, attStatus); + updateSatAtt(satPos, satOpts.attitudeModel.sources, attStatus); + updateAntAtt(satNav.antBoresight, satNav.antAzimuth, attStatus); } /** Nominal receiver attitude - unit vectors of receiver-fixed coordinates (ECEF) @@ -1562,107 +1750,129 @@ void updateSatAtts( * z -> up */ bool basicRecAttitude( - Receiver& rec, ///< Receiver position (ECEF) - AttStatus& attStatus) ///< Attitude status + Receiver& rec, ///< Receiver position (ECEF) + AttStatus& attStatus ///< Attitude status +) { - VectorPos pos = ecef2pos(rec.aprioriPos); + VectorPos pos = ecef2pos(rec.aprioriPos); - Matrix3d E; - pos2enu(pos, E.data()); + Matrix3d E; + pos2enu(pos, E.data()); - attStatus.eXBody = E.row(0); // x = east - attStatus.eYBody = E.row(1); // y = north - attStatus.eZBody = E.row(2); // z = up + attStatus.eXBody = E.row(0); // x = east + attStatus.eYBody = E.row(1); // y = north + attStatus.eZBody = E.row(2); // z = up - return true; + return true; } /** Attitude of receiver */ void recAtt( - Receiver& rec, ///< Receiver - GTime time, ///< Time - vector attitudeTypes, ///< Attitude type - const KFState* kfState_ptr, - const KFState* remote_ptr) + Receiver& rec, ///< Receiver + GTime time, ///< Time + vector attitudeTypes, ///< Attitude type + const KFState* kfState_ptr, + const KFState* remote_ptr +) { - auto& attStatus = rec.attStatus; - - bool valid = false; - for (auto& attitudeType : attitudeTypes) - { - switch (attitudeType) - { - default: BOOST_LOG_TRIVIAL(error) << "Unknown attitudeType in " << __FUNCTION__ << ": " << attitudeType._to_string(); - case E_Source::MODEL: //fallthrough - case E_Source::NOMINAL: { valid = basicRecAttitude(rec, attStatus); break; } - case E_Source::PRECISE: { valid = preciseAttitude (rec.id, time, attStatus); break; } - case E_Source::KALMAN: { valid = kalmanAttitude (rec.id, time, attStatus, kfState_ptr); break; } - case E_Source::REMOTE: { valid = kalmanAttitude (rec.id, time, attStatus, remote_ptr); break; } - } - - if (valid) - { - break; - } - } - - updateAntAtt(rec.antBoresight, rec.antAzimuth, attStatus); - -// SatSys Sat(rec.id.c_str()); //todo aaron, this should be the recSatId thing instead -// if (Sat.prn) -// { -// nav.satNavMap[Sat].attStatus = attStatus; -// } + auto& attStatus = rec.attStatus; + + bool valid = false; + for (auto& attitudeType : attitudeTypes) + { + switch (attitudeType) + { + default: + BOOST_LOG_TRIVIAL(error) << "Unknown attitudeType in " << __FUNCTION__ << ": " + << enum_to_string(attitudeType); + case E_Source::MODEL: // fallthrough + case E_Source::NOMINAL: + { + valid = basicRecAttitude(rec, attStatus); + break; + } + case E_Source::PRECISE: + { + valid = preciseAttitude(rec.id, time, attStatus); + break; + } + case E_Source::KALMAN: + { + valid = kalmanAttitude(rec.id, time, attStatus, kfState_ptr); + break; + } + case E_Source::REMOTE: + { + valid = kalmanAttitude(rec.id, time, attStatus, remote_ptr); + break; + } + } + + if (valid) + { + break; + } + } + + updateAntAtt(rec.antBoresight, rec.antAzimuth, attStatus); + + // SatSys Sat(rec.id.c_str()); //todo aaron, this should be the recSatId thing instead + // if (Sat.prn) + // { + // nav.satNavMap[Sat].attStatus = attStatus; + // } } /** phase windup model -*/ + */ void phaseWindup( - GObs& obs, ///< Observation detailing the satellite to apply model to - Receiver& rec, ///< Position of receiver (ECEF) - double& phw) ///< Output of phase windup result + GObs& obs, ///< Observation detailing the satellite to apply model to + Receiver& rec, ///< Position of receiver (ECEF) + double& phw ///< Output of phase windup result +) { - auto& attStatus = obs.satNav_ptr->attStatus; - Vector3d& eXSat = attStatus.eXAnt; - Vector3d& eYSat = attStatus.eYAnt; - Vector3d& eZSat = attStatus.eZAnt; + auto& attStatus = obs.satNav_ptr->attStatus; + Vector3d& eXSat = attStatus.eXAnt; + Vector3d& eYSat = attStatus.eYAnt; + Vector3d& eZSat = attStatus.eZAnt; - Vector3d& eXRec = rec.attStatus.eXAnt; - Vector3d& eYRec = rec.attStatus.eYAnt; - Vector3d& eZRec = rec.attStatus.eZAnt; + Vector3d& eXRec = rec.attStatus.eXAnt; + Vector3d& eYRec = rec.attStatus.eYAnt; + Vector3d& eZRec = rec.attStatus.eZAnt; - /* unit vector satellite to receiver */ - Vector3d look = obs.satStat_ptr->e; + /* unit vector satellite to receiver */ + Vector3d look = obs.satStat_ptr->e; - //Get axis of rotation between antenna boresight and look vector - Vector3d recAxis = +1 * eZRec .cross(look) .normalized(); - Vector3d satAxis = -1 * eZSat .cross(look) .normalized(); + // Get axis of rotation between antenna boresight and look vector + Vector3d recAxis = +1 * eZRec.cross(look).normalized(); + Vector3d satAxis = -1 * eZSat.cross(look).normalized(); - //We dont need 1,2 for the first axis, because they are common + // We dont need 1,2 for the first axis, because they are common - //Get another unit vector to finish the boresight, axis, coordinate set - Vector3d recQuad1 = +1 * eZRec .cross(recAxis) .normalized(); - Vector3d satQuad1 = -1 * eZSat .cross(satAxis) .normalized(); + // Get another unit vector to finish the boresight, axis, coordinate set + Vector3d recQuad1 = +1 * eZRec.cross(recAxis).normalized(); + Vector3d satQuad1 = -1 * eZSat.cross(satAxis).normalized(); - //Get another unit vector to finish the look vector, axis, coordinate set - Vector3d recQuad2 = look .cross(recAxis) .normalized(); - Vector3d satQuad2 = look .cross(satAxis) .normalized(); + // Get another unit vector to finish the look vector, axis, coordinate set + Vector3d recQuad2 = look.cross(recAxis).normalized(); + Vector3d satQuad2 = look.cross(satAxis).normalized(); - //Apply a zero-twist rotation to the antenna to align it with the look vector. - //Get projection of x axis on coordinate set1, then apply to coordinate set 2 - Vector3d recXnew = eXRec.dot(recAxis) * recAxis + eXRec.dot(recQuad1) * recQuad2; - Vector3d recYnew = eYRec.dot(recAxis) * recAxis + eYRec.dot(recQuad1) * recQuad2; + // Apply a zero-twist rotation to the antenna to align it with the look vector. + // Get projection of x axis on coordinate set1, then apply to coordinate set 2 + Vector3d recXnew = eXRec.dot(recAxis) * recAxis + eXRec.dot(recQuad1) * recQuad2; + Vector3d recYnew = eYRec.dot(recAxis) * recAxis + eYRec.dot(recQuad1) * recQuad2; - Vector3d satXnew = eXSat.dot(satAxis) * satAxis + eXSat.dot(satQuad1) * satQuad2; - Vector3d satYnew = eYSat.dot(satAxis) * satAxis + eYSat.dot(satQuad1) * satQuad2; + Vector3d satXnew = eXSat.dot(satAxis) * satAxis + eXSat.dot(satQuad1) * satQuad2; + Vector3d satYnew = eYSat.dot(satAxis) * satAxis + eYSat.dot(satQuad1) * satQuad2; - //Get angle offset by looking at receiver's new components alignment with satellite's new x component - double angleOffset = atan2(satXnew.dot(recYnew), -satYnew.dot(recYnew)); + // Get angle offset by looking at receiver's new components alignment with satellite's new x + // component + double angleOffset = atan2(satXnew.dot(recYnew), -satYnew.dot(recYnew)); - //Convert to a fraction of cycles (apply an offset to match old code) - double phaseFraction = angleOffset / (2 * PI) - 0.25; + // Convert to a fraction of cycles (apply an offset to match old code) + double phaseFraction = angleOffset / (2 * PI) - 0.25; - //keep phase windup continuous from previous result across cycles - phw = phaseFraction + floor(phw - phaseFraction + 0.5); + // keep phase windup continuous from previous result across cycles + phw = phaseFraction + floor(phw - phaseFraction + 0.5); } diff --git a/src/cpp/common/attitude.hpp b/src/cpp/common/attitude.hpp index 303be32d0..73f409e04 100644 --- a/src/cpp/common/attitude.hpp +++ b/src/cpp/common/attitude.hpp @@ -1,33 +1,34 @@ - #pragma once -#include "eigenIncluder.hpp" -#include "trace.hpp" -#include "gTime.hpp" -#include "enums.h" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/trace.hpp" /** Persistent data for yaw model -*/ + */ 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 excludeTime = GTime::noTime(); ///< Time to skip yaw modelling until, due to unknown yaw behaviour - - double nominalYaw = 0; ///< Latest nominal yaw - double modelYaw = 0; ///< Latest modelled yaw (i.e. considering noon/midnight turns) - GTime modelYawTime = GTime::noTime(); ///< Time of modelYaw (and nominalYaw) - bool modelYawValid = false; ///< Model yaw was calculated sucessfully - - VectorEcef eXBody; ///< X+ unit vector of body-fixed coordinates (ECEF) - VectorEcef eYBody; ///< Y+ unit vector of body-fixed coordinates (ECEF) - VectorEcef eZBody; ///< Z+ unit vector of body-fixed coordinates (ECEF) - - VectorEcef eXAnt; ///< X+ unit vector of antenna-fixed coordinates (ECEF) - VectorEcef eYAnt; ///< Y+ unit vector of antenna-fixed coordinates (ECEF) - VectorEcef eZAnt; ///< Z+ unit vector of antenna-fixed coordinates (ECEF) + 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 + + double nominalYaw = 0; ///< Latest nominal yaw + double modelYaw = 0; ///< Latest modelled yaw (i.e. considering noon/midnight turns) + GTime modelYawTime = GTime::noTime(); ///< Time of modelYaw (and nominalYaw) + bool modelYawValid = false; ///< Model yaw was calculated sucessfully + + VectorEcef eXBody; ///< X+ unit vector of body-fixed coordinates (ECEF) + VectorEcef eYBody; ///< Y+ unit vector of body-fixed coordinates (ECEF) + VectorEcef eZBody; ///< Z+ unit vector of body-fixed coordinates (ECEF) + + VectorEcef eXAnt; ///< X+ unit vector of antenna-fixed coordinates (ECEF) + VectorEcef eYAnt; ///< Y+ unit vector of antenna-fixed coordinates (ECEF) + VectorEcef eZAnt; ///< Z+ unit vector of antenna-fixed coordinates (ECEF) }; struct Receiver; @@ -35,15 +36,13 @@ struct KFState; struct SatPos; void recAtt( - Receiver& rec, - GTime time, - vector attitudeTypes, - const KFState* kfState_ptr = nullptr, - const KFState* remote_ptr = nullptr); - -void updateSatAtts( - SatPos& satPos); - -void updateSatYaw( - SatPos& satPos, - AttStatus& attStatus); + Receiver& rec, + GTime time, + vector attitudeTypes, + const KFState* kfState_ptr = nullptr, + const KFState* remote_ptr = nullptr +); + +void updateSatAtts(SatPos& satPos); + +void updateSatYaw(SatPos& satPos, AttStatus& attStatus); diff --git a/src/cpp/common/azElMapData.hpp b/src/cpp/common/azElMapData.hpp index 8b738bcf1..e572870a9 100644 --- a/src/cpp/common/azElMapData.hpp +++ b/src/cpp/common/azElMapData.hpp @@ -1,22 +1,21 @@ - #pragma once -#include #include +#include -using std::vector; using std::map; +using std::vector; -template +template struct AzElMapData { - double aziDelta; ///< azimuth increment (degree) - double zenStart; - double zenStop; - double zenDelta; - int nz; ///< number of zenith intervals - int naz; ///< number of non-azimuth dependent intervals - - vector elMap; - map> azElMap; + double aziDelta; ///< azimuth increment (degree) + double zenStart; + double zenStop; + double zenDelta; + int nz; ///< number of zenith intervals + int naz; ///< number of non-azimuth dependent intervals + + vector elMap; + map> azElMap; }; diff --git a/src/cpp/common/biasSINEXread.cpp b/src/cpp/common/biasSINEXread.cpp index e56e49d8c..ff4910746 100644 --- a/src/cpp/common/biasSINEXread.cpp +++ b/src/cpp/common/biasSINEXread.cpp @@ -1,361 +1,359 @@ - // #pragma GCC optimize ("O0") #include - -#include "constants.hpp" -#include "acsConfig.hpp" -#include "biases.hpp" -#include "enums.h" +#include "common/acsConfig.hpp" +#include "common/biases.hpp" +#include "common/constants.hpp" +#include "common/enums.h" /** Convert observation code string to enum code -*/ + */ E_ObsCode str2code( - string& input, ///< The input observation code string - E_MeasType& measType) ///< Measurement type of this observation - CODE/PHAS - as output + string& input, ///< The input observation code string + E_MeasType& measType ///< Measurement type of this observation - CODE/PHAS - as output +) { - char cods[] = "Lxx"; - cods[1] = input[1]; - cods[2] = input[2]; - - E_ObsCode code = E_ObsCode::NONE; - - if (input[0] == 'L') measType = PHAS; - else if (input[0] == 'C') measType = CODE; - else - { - measType = CODE; - code = E_ObsCode::NONE; - } - - try - { - code = E_ObsCode::_from_string(cods); - } - catch (...) - { - code = E_ObsCode::NONE; - } - - return code; + char cods[] = "Lxx"; + cods[1] = input[1]; + cods[2] = input[2]; + + E_ObsCode code = E_ObsCode::NONE; + + if (input[0] == 'L') + measType = PHAS; + else if (input[0] == 'C') + measType = CODE; + else + { + measType = CODE; + code = E_ObsCode::NONE; + } + + try + { + code = string_to_enum(cods); + } + catch (...) + { + code = E_ObsCode::NONE; + } + + return code; } /** Convert time string in bias SINEX to gtime struct -*/ + */ GTime sinex_time_text( - string& line, ///< line to read - E_TimeSys tsys) ///< time system + string& line, ///< line to read + E_TimeSys tsys ///< time system +) { - double yds[3]; - GTime time = {}; - if (sscanf(line.c_str(), "%lf:%lf:%lf", &yds[0], &yds[1], &yds[2]) == 3) - { - time = yds2time(yds, tsys); - } - - return time; + double yds[3]; + GTime time = {}; + if (sscanf(line.c_str(), "%lf:%lf:%lf", &yds[0], &yds[1], &yds[2]) == 3) + { + time = yds2time(yds, tsys); + } + + return time; } /** Read header line in bias SINEX file -*/ -void read_biasSINEX_head( - const char* buff) ///< Line to read + */ +void read_biasSINEX_head(const char* buff) ///< Line to read { - /* This is the function to read the header, but data in header is of no use at the moment */ - return; + /* This is the function to read the header, but data in header is of no use at the moment */ + return; } /** Read data line in bias SINEX file -*/ + */ bool read_biasSINEX_line( - char* buff, ///< Line to read - E_TimeSys tsys) ///< time system "UTC", "TAI", etc. + char* buff, ///< Line to read + E_TimeSys tsys ///< time system "UTC", "TAI", etc. +) { - int size = strlen(buff); - - if (size < 91) - { - BOOST_LOG_TRIVIAL(error) << "Error: Short bias line in SINEX file (" << size << "): " << buff; - return false; - } - - if (tsys == +E_TimeSys::NONE) - { - BOOST_LOG_TRIVIAL(error) << "Unkown time system for bias SINEX file: " << tsys._to_string(); - return false; - } - - BiasEntry entry; - entry.source = "bsx"; - - string type (buff + 1, 4); - string svn (buff + 6, 4); - string sat (buff + 11, 3); - string name (buff + 15, 4); - string cod1str (buff + 25, 3); - string cod2str (buff + 30, 3); - string startTime(buff + 35, 14); - string endTime (buff + 50, 14); - string units (buff + 65, 3); - string biasStr (buff + 70, 21); - - if ( type != "DSB " - &&type != "OSB ") - { - return false; - } - - SatSys Sat(sat.c_str()); - if (acsConfig.process_sys[Sat.sys] == false) - { - return false; - } - - string id; - if (name != " ") - { - //this seems to be a receiver, but may have satellite dependency for glonass - entry.Sat = Sat; - entry.name = name; - id = name; - } - else if (sat != " ") - { - //this should be a satellite, but check its valid //todo aaron, system for receiver dcbs - - if ( Sat.prn == 0 - ||Sat.sys == +E_Sys::NONE) - { - return false; - } - - entry.Sat = Sat; - entry.name = ""; - id = sat; - } - else - { - //no valid identifier - return false; - } - - E_MeasType dummy; - entry.cod1 = str2code(cod1str, entry.measType); - entry.cod2 = str2code(cod2str, dummy); - - - SatSys lamSat = Sat; - if (lamSat.prn == 0) - { - lamSat.prn++; - } - - int ft1 = code2Freq[Sat.sys][entry.cod1]; - double lam1 = nav.satNavMap[lamSat].lamMap[ft1]; - - /* decoding start/end times */ - entry.tini = sinex_time_text(startTime, tsys); - entry.tfin = sinex_time_text(endTime, tsys); - - if (entry.tini != GTime::noTime() && entry.tfin != GTime::noTime()) entry.refTime = entry.tini + (entry.tfin - entry.tini).to_double() / 2; - else if (entry.tini != GTime::noTime() && entry.tfin == GTime::noTime()) entry.refTime = entry.tini; - else if (entry.tini == GTime::noTime() && entry.tfin != GTime::noTime()) entry.refTime = entry.tfin; - else - { - BOOST_LOG_TRIVIAL(error) << "Error: Invalid interval for bias in SINEX file: " << buff; - return false; - } - - /* decoding units */ - double fact = 0; - - if (units == "ns ") fact = CLIGHT / 1e9; - else if (units == "cyc" && entry.measType == PHAS && lam1 > 0) fact = lam1; - else - { - return false; - } - - /* decoding bias */ - try - { - entry.bias = stod(biasStr) * fact; - } - catch (const std::invalid_argument& ia) - { - BOOST_LOG_TRIVIAL(error) << "Error: Invalid bias in SINEX file: " << buff; - return false; - } - - /* reading/decoding standard deviation */ - if (strlen(buff) >= 103) - { - string stdstr(buff + 92, 11); - - try - { - double stdv = stod(stdstr) * fact; - entry.var = SQR(stdv); - } - catch (const std::invalid_argument& ia) - { - entry.var = 0; - } - } - - if (strlen(buff) >= 125) - { - string slopStr (buff + 104, 21); - - try - { - entry.slop = stod(slopStr) * fact; - } - catch (const std::invalid_argument& ia) - { - entry.slop = 0; - } - } - - if (strlen(buff) >= 137) - { - string stdstr (buff + 126, 11); - - try - { - double stdv = stod(stdstr) * fact; - entry.slpv = SQR(stdv); - } - catch (const std::invalid_argument& ia) - { - entry.slpv = 0; - } - } - - if ( Sat.sys == +E_Sys::GLO - &&Sat.prn == 0) - { - // this seems to be a receiver - // for ambiguous GLO receiver bias id (i.e. PRN not specified), duplicate bias entry for each satellite - for (int prn = 1; prn <= NSATGLO; prn++) - { - Sat.prn = prn; - id = entry.name + ":" + Sat.id(); - // entry.Sat = Sat; - pushBiasEntry(id, entry); - } - } - else if ( Sat.sys == +E_Sys::GLO - &&Sat.prn != 0) - { - // this can be a receiver or satellite - id = id + ":" + Sat.id(); - pushBiasEntry(id, entry); - } - else - { - // this can be a receiver or satellite - id = id + ":" + Sat.sysChar(); - pushBiasEntry(id, entry); - } - - return true; + int size = strlen(buff); + + if (size < 91) + { + BOOST_LOG_TRIVIAL(error) << "Short bias line in SINEX file (" << size << "): " << buff; + return false; + } + + if (tsys == E_TimeSys::NONE) + { + BOOST_LOG_TRIVIAL(error) << "Unkown time system for bias SINEX file: " + << enum_to_string(tsys); + return false; + } + + BiasEntry entry; + entry.source = "bsx"; + + string type(buff + 1, 4); + string svn(buff + 6, 4); + string sat(buff + 11, 3); + string name(buff + 15, 4); + string cod1str(buff + 25, 3); + string cod2str(buff + 30, 3); + string startTime(buff + 35, 14); + string endTime(buff + 50, 14); + string units(buff + 65, 3); + string biasStr(buff + 70, 21); + + if (type != "DSB " && type != "OSB ") + { + return false; + } + + SatSys Sat(sat.c_str()); + if (acsConfig.process_sys[Sat.sys] == false) + { + return false; + } + + string id; + if (name != " ") + { + // this seems to be a receiver, but may have satellite dependency for glonass + entry.Sat = Sat; + entry.name = name; + id = name; + } + else if (sat != " ") + { + // this should be a satellite, but check its valid //todo aaron, system for receiver + // dcbs + + if (Sat.prn == 0 || Sat.sys == E_Sys::NONE) + { + return false; + } + + entry.Sat = Sat; + entry.name = ""; + id = sat; + } + else + { + // no valid identifier + return false; + } + + E_MeasType dummy; + entry.cod1 = str2code(cod1str, entry.measType); + entry.cod2 = str2code(cod2str, dummy); + + SatSys lamSat = Sat; + if (lamSat.prn == 0) + { + lamSat.prn++; + } + + int ft1 = code2Freq[Sat.sys][entry.cod1]; + double lam1 = nav.satNavMap[lamSat].lamMap[ft1]; + + /* decoding start/end times */ + entry.tini = sinex_time_text(startTime, tsys); + entry.tfin = sinex_time_text(endTime, tsys); + + /* decoding units */ + double fact = 0; + + if (units == "ns ") + fact = CLIGHT / 1e9; + else if (units == "cyc" && entry.measType == PHAS && lam1 > 0) + fact = lam1; + else + { + return false; + } + + /* decoding bias */ + try + { + entry.bias = stod(biasStr) * fact; + } + catch (const std::invalid_argument& ia) + { + BOOST_LOG_TRIVIAL(error) << "Invalid bias in SINEX file: " << buff; + return false; + } + + /* reading/decoding standard deviation */ + if (strlen(buff) >= 103) + { + string stdstr(buff + 92, 11); + + try + { + double stdv = stod(stdstr) * fact; + entry.var = SQR(stdv); + } + catch (const std::invalid_argument& ia) + { + entry.var = 0; + } + } + + if (strlen(buff) >= 125) + { + string slopStr(buff + 104, 21); + + try + { + entry.slop = stod(slopStr) * fact; + } + catch (const std::invalid_argument& ia) + { + entry.slop = 0; + } + } + + if (strlen(buff) >= 137) + { + string stdstr(buff + 126, 11); + + try + { + double stdv = stod(stdstr) * fact; + entry.slpv = SQR(stdv); + } + catch (const std::invalid_argument& ia) + { + entry.slpv = 0; + } + } + + updateRefTime(entry); + + if (Sat.sys == E_Sys::GLO && Sat.prn == 0) + { + // this seems to be a receiver + // for ambiguous GLO receiver bias id (i.e. PRN not specified), duplicate bias entry for + // each satellite + for (int prn = 1; prn <= NSATGLO; prn++) + { + Sat.prn = prn; + id = entry.name + ":" + Sat.id(); + // entry.Sat = Sat; + pushBiasEntry(id, entry); + } + } + else if (Sat.sys == E_Sys::GLO && Sat.prn != 0) + { + // this can be a receiver or satellite + id = id + ":" + Sat.id(); + pushBiasEntry(id, entry); + } + else + { + // this can be a receiver or satellite + id = id + ":" + Sat.sysChar(); + pushBiasEntry(id, entry); + } + + return true; } /** Read single bias SINEX file -*/ -bool readBiasSinex( - string& filename) ///< File to read + */ +bool readBiasSinex(string& filename) ///< File to read { - int nbia = 0; - bool datasect = false; - E_TimeSys tsys = E_TimeSys::NONE; - - std::ifstream inputStream(filename); - if (!inputStream) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Could not find bias SINEX file " << filename.c_str(); - return false; - } - - string line; - while (std::getline(inputStream, line)) - { - char* buff = &line[0]; - - if (buff[0] == '*') - continue; /* comment line */ - - if (strstr(buff, "TIME_SYSTEM")) - { - string timeSystem(buff + 41, 1); - if (timeSystem != " ") - { - if (timeSystem == "G") tsys = E_TimeSys::GPST; - else if (timeSystem == "C") tsys = E_TimeSys::BDT; - else if (timeSystem == "R") tsys = E_TimeSys::GLONASST; - else if (timeSystem == "U") tsys = E_TimeSys::UTC; - else if (timeSystem == "T") tsys = E_TimeSys::TAI; - else - { - BOOST_LOG_TRIVIAL(warning) << "Warning: unsupported time system: " << timeSystem; - return false; - } - } - } - - if (strstr(buff, "%=BIA")) - { - read_biasSINEX_head(buff); - continue; - } - - if (strstr(buff, "%=ENDBIA")) - { - //fprintf(stdout,"\n ... %d biases read\n",nbia); - return true; - } - - if (strstr(buff, "%=")) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: erroneous bias SINEX file " << filename.c_str(); - return false; - } - - if (strstr(buff, "+FILE/REFERENCE")) - { - /* read bias description here... */ - continue; - } - - if (strstr(buff, "+BIAS/DESCRIPTION")) - { - /* read bias description here... */ - continue; - } - - if (strstr(buff, "+BIAS/SOLUTION")) - { - datasect = true; - continue; - } - - if (strstr(buff, "-BIAS/SOLUTION")) - { - datasect = false; - continue; - } - - if (!datasect) - continue; - - if ( strstr(buff, " DSB ") - || strstr(buff, " OSB ")) - { - if (read_biasSINEX_line(buff, tsys)) - nbia++; - } - } - - return true; + int nbia = 0; + bool datasect = false; + E_TimeSys tsys = E_TimeSys::NONE; + + std::ifstream inputStream(filename); + if (!inputStream) + { + BOOST_LOG_TRIVIAL(warning) << "Could not find bias SINEX file " << filename.c_str(); + return false; + } + + string line; + while (std::getline(inputStream, line)) + { + char* buff = &line[0]; + + if (buff[0] == '*') + continue; /* comment line */ + + if (strstr(buff, "TIME_SYSTEM")) + { + string timeSystem(buff + 41, 1); + if (timeSystem != " ") + { + if (timeSystem == "G") + tsys = E_TimeSys::GPST; + else if (timeSystem == "C") + tsys = E_TimeSys::BDT; + else if (timeSystem == "R") + tsys = E_TimeSys::GLONASST; + else if (timeSystem == "U") + tsys = E_TimeSys::UTC; + else if (timeSystem == "T") + tsys = E_TimeSys::TAI; + else + { + BOOST_LOG_TRIVIAL(warning) << "Unsupported time system: " << timeSystem; + return false; + } + } + } + + if (strstr(buff, "%=BIA")) + { + read_biasSINEX_head(buff); + continue; + } + + if (strstr(buff, "%=ENDBIA")) + { + // fprintf(stdout,"\n ... %d biases read\n",nbia); + return true; + } + + if (strstr(buff, "%=")) + { + BOOST_LOG_TRIVIAL(warning) << "Erroneous bias SINEX file " << filename.c_str(); + return false; + } + + if (strstr(buff, "+FILE/REFERENCE")) + { + /* read bias description here... */ + continue; + } + + if (strstr(buff, "+BIAS/DESCRIPTION")) + { + /* read bias description here... */ + continue; + } + + if (strstr(buff, "+BIAS/SOLUTION")) + { + datasect = true; + continue; + } + + if (strstr(buff, "-BIAS/SOLUTION")) + { + datasect = false; + continue; + } + + if (!datasect) + continue; + + if (strstr(buff, " DSB ") || strstr(buff, " OSB ")) + { + if (read_biasSINEX_line(buff, tsys)) + nbia++; + } + } + + return true; } diff --git a/src/cpp/common/biasSINEXwrite.cpp b/src/cpp/common/biasSINEXwrite.cpp index 744d1cafe..4ce1ba187 100644 --- a/src/cpp/common/biasSINEXwrite.cpp +++ b/src/cpp/common/biasSINEXwrite.cpp @@ -1,525 +1,695 @@ - -// #pragma GCC optimize ("O0") - -#include "GNSSambres.hpp" -#include "constants.hpp" -#include "ionoModel.hpp" -#include "biases.hpp" -#include "ppp.hpp" - -map> sinexBiases_out; -long int bottomOfFile = 0; -double startTimeofFile[3]; -string lastBiasSINEXFile; - -/** Convert enum observation code to code string -*/ -string code2str( - E_ObsCode code, ///< The input enum observation code - E_MeasType measType) ///< Measurement type of this observation - CODE/PHAS -{ - if (code == +E_ObsCode::NONE) - return ""; - - string outstr = code._to_string(); - - char head; - if (measType == PHAS) head='L'; - if (measType == CODE) head='C'; - - outstr[0] = head; - - return outstr; -} - -/** Determine if the bias is an OSB or DSB given observation codes -*/ -string biasType( - E_ObsCode code1, ///< Base code of observation for the bias - E_ObsCode code2) ///< Secondary code of observation for the bias -{ - if (code1 != +E_ObsCode::NONE && code2 != +E_ObsCode::NONE) return "DSB"; - else if (code1 != +E_ObsCode::NONE && code2 == +E_ObsCode::NONE) return "OSB"; - else if (code1 == +E_ObsCode::NONE && code2 != +E_ObsCode::NONE) return "OSB"; - else return "NONE"; -} - -/** Update bias SINEX first line -*/ -void updateFirstLine( - GTime time, ///< Time of bias to write - E_TimeSys tsys, ///< Time system - Trace& trace, ///< Trace to output to - int numbias) ///< Number of biases to be written -{ - GTime now = timeGet(); - - double ydsNow[3]; - time2yds(now, ydsNow, tsys); - - double ydsEnd[3]; - time2yds(time, ydsEnd, tsys); - - trace.seekp(0); - tracepdeex(0, trace, "%%=BIA 1.00 %3s %4d:%03d:%05d ", acsConfig.analysis_agency.c_str(), (int)ydsNow[0], (int)ydsNow[1], (int)ydsNow[2]); - tracepdeex(0, trace, "%3s %4d:%03d:%05d ",acsConfig.analysis_agency.c_str(), (int)startTimeofFile[0], (int)startTimeofFile[1], (int)startTimeofFile[2]); - tracepdeex(0, trace, "%4d:%03d:%05d A %8d\n", (int)ydsEnd[0], (int)ydsEnd[1], (int)ydsEnd[2], numbias); -} - -/** Write bias SINEX head for the first time -*/ -void writeBSINEXHeader( - GTime time, ///< Time of bias to write - E_TimeSys tsys, ///< Time system - Trace& trace, ///< Trace to output to - double updateInterval) ///< Bias Update Interval (seconds) -{ - time2yds(time, startTimeofFile, tsys); - updateFirstLine(time, tsys, trace, 0); - - tracepdeex(0, trace, "*-------------------------------------------------------------------------------\n"); - tracepdeex(0, trace, "+FILE/REFERENCE\n"); - tracepdeex(0, trace, " DESCRIPTION %s, %s\n",acsConfig.analysis_agency.c_str(), acsConfig.analysis_centre.c_str()); - tracepdeex(0, trace, " OUTPUT OSB estimates for day %3d, %4d\n", startTimeofFile[1], startTimeofFile[0]); - tracepdeex(0, trace, " CONTACT %s\n", acsConfig.ac_contact.c_str()); - tracepdeex(0, trace, " SOFTWARE %s\n", acsConfig.analysis_software.c_str()); - tracepdeex(0, trace, " INPUT %s\n", acsConfig.rinex_comment.c_str()); - tracepdeex(0, trace, "-FILE/REFERENCE\n"); - - - tracepdeex(0, trace, "*-------------------------------------------------------------------------------\n"); - tracepdeex(0, trace, "+BIAS/DESCRIPTION\n"); - tracepdeex(0, trace, " OBSERVATION_SAMPLING %12.0f\n", acsConfig.epoch_interval); - tracepdeex(0, trace, " PARAMETER_SPACING %12.0f\n", updateInterval); - tracepdeex(0, trace, " DETERMINATION_METHOD COMBINED_ANALYSIS\n"); - tracepdeex(0, trace, " BIAS_MODE ABSOLUTE\n"); - tracepdeex(0, trace, " TIME_SYSTEM %s \n", acsConfig.bias_time_system.c_str()); - - auto& recOpts = acsConfig.getRecOpts("global"); - E_Sys refConst = recOpts.receiver_reference_system; - tracepdeex(0, trace, " RECEIVER_CLOCK_REFERENCE_GNSS %c\n", refConst._to_string()[0]); - - for (auto& [sys, solve] : acsConfig.solve_amb_for) - { - if (solve == false) - continue; - - char sysChar; - switch (sys) - { - case E_Sys::GPS: sysChar = 'G'; break; - case E_Sys::GLO: sysChar = 'R'; break; - case E_Sys::GAL: sysChar = 'E'; break; - case E_Sys::QZS: sysChar = 'J'; break; - case E_Sys::BDS: sysChar = 'C'; break; - default: continue; - } - -// E_ObsCode code1= acsConfig.clock_codesL1[sys]; -// E_ObsCode code2= acsConfig.clock_codesL2[sys]; -// tracepdeex(0, trace, " SATELLITE_CLOCK_REFERENCE_OBSERVABLES %c %s %s\n", sysChar,code1._to_string(),code2._to_string()); - } - tracepdeex(0, trace, "-BIAS/DESCRIPTION\n"); - - - tracepdeex(0, trace, "*-------------------------------------------------------------------------------\n"); - tracepdeex(0, trace, "+BIAS/SOLUTION\n"); - tracepdeex(0, trace, "*BIAS SVN_ PRN STATION__ OBS1 OBS2 BIAS_START____ BIAS_END______ UNIT __ESTIMATED_VALUE____ _STD_DEV___\n"); - - bottomOfFile = trace.tellp(); -} - -/** print bias SINEX data line -*/ -int writeBSINEXLine( - GTime time, ///< Time of bias to write - E_TimeSys tsys, ///< Time system - BiasEntry& bias, ///< Bias entry to write - Trace& trace) ///< Stream to output to -{ - string typstr = biasType(bias.cod1, bias.cod2); - - if (typstr != "OSB") - return 0; - - - string sat = " "; - string svn = " "; - if (bias.Sat.prn == 0) - { - char sysChar = bias.Sat.sysChar(); - sat[0] = sysChar; - svn[0] = sysChar; - } - else - { - sat = bias.Sat.id(); - svn = bias.Sat.svn(); - } - - string cod1 = code2str(bias.cod1, bias.measType); - string cod2 = code2str(bias.cod2, bias.measType); - - bool newbottom = false; - if (bias.posInOutFile < 0) - { - trace.seekp(bottomOfFile); - - bias.posInOutFile = bottomOfFile; - newbottom = true; - } - else - { - trace.seekp(bias.posInOutFile); - } - - tracepdeex(0, trace, " %-4s %4s %3s %-9s %-4s %-4s", - typstr.c_str(), - svn.c_str(), - sat.c_str(), - bias.name.c_str(), - cod1.c_str(), - cod2.c_str()); - - double tini[3]; - double tend[3]; - time2yds(bias.tini, tini, tsys); - time2yds(bias.tfin, tend, tsys); - - tracepdeex(0, trace, " %4d:%03d:%05d %4d:%03d:%05d %-4s", - (int)tini[0], - (int)tini[1], - (int)tini[2], - (int)tend[0], - (int)tend[1], - (int)tend[2], - "ns"); - - tracepdeex(0, trace, " %21.5f", bias.bias * (1E9/CLIGHT)); - tracepdeex(0, trace, " %11.6f", sqrt(bias.var) * (1E9/CLIGHT)); - - tracepdeex(0, trace, "\n"); - - if (newbottom) - bottomOfFile = trace.tellp(); - - return 1; -} - -/** Adding/updating new bias entry - */ -int addBiasEntry( - Trace& trace, - GTime tini, - GTime tfin, - KFKey kfKey, - E_MeasType measType, - double bias, - double var) -{ - auto& biasMap = sinexBiases_out[kfKey]; - int found = -1; - - for (auto& [ind, bias] : biasMap) - { - if (bias.tini != tini) continue; - if (bias.measType != measType) continue; - - found = ind; - break; - } - - tracepdeex(3,trace,"\n Searched %s bias for %s %2d %s: ",(measType==CODE)?"CODE ":"PHASE", kfKey.Sat.id().c_str(), kfKey.num, tini.to_string().c_str()); - - if (found >= 0) - { - biasMap[found].bias = bias; - - if (var < 1e-12) - var = 1e-12; - - biasMap[found].var = var; - tracepdeex(4,trace," found at index %d: %.4f %.4f", found, bias, var); - } - else - { - found = biasMap.size(); - BiasEntry entry; - entry.name = kfKey.str; - entry.Sat = kfKey.Sat; - entry.cod1 = E_ObsCode::_from_integral(kfKey.num); - entry.cod2 = E_ObsCode::NONE; - - entry.measType = measType; - entry.tini = tini; - entry.tfin = tfin; - - entry.bias = bias; - entry.var = var; - entry.slop = 0; - entry.slpv = 0; - entry.posInOutFile = -1; - biasMap[found] = entry; - - tracepdeex(4,trace," not found, stored at index %d: %.4f %.4f", found, bias, var); - } - return found; -} - -/** Store bias output to write into bias SINEX files -*/ -void updateBiasOutput( - Trace& trace, ///< Trace to output to - GTime time, ///< Time of bias update - KFState& kfState, ///< Filter state to take biases from - KFState& ionState, ///< Filter state to take biases from - ReceiverMap& receiverMap, ///< stations for which to output receiver biases - E_MeasType measType) ///< Type of measurement to find bias for -{ - int nstore = 0; - double bias; - double bvar; - - double updateRate = 0; - if (measType == E_MeasType::CODE) updateRate = acsConfig.ambrOpts.code_output_interval; - if (measType == E_MeasType::PHAS) updateRate = acsConfig.ambrOpts.phase_output_interval; - - if (updateRate <= 0) - return; - - if (updateRate < 30) - updateRate = 30; - - GWeek week = time; - GTow tow = time; - - tow = updateRate * floor(tow / updateRate); - GTime tini = gpst2time (week, tow); - GTime tfin = tini + updateRate; - - KFKey key; - if (measType == E_MeasType::CODE) key.type = KF::CODE_BIAS; - if (measType == E_MeasType::PHAS) key.type = KF::PHASE_BIAS; - - for (E_Sys sys : E_Sys::_values()) - { - if (acsConfig.process_sys[sys] == false) - continue; - - auto sats = getSysSats(sys); - - for (auto& Sat : sats) - for (auto& obsCode : acsConfig.code_priorities[sys]) - if (queryBiasOutput(trace, time, kfState, ionState, Sat, "", obsCode, bias, bvar, measType)) - { - key.Sat = Sat; - key.str = ""; - key.num = obsCode; - - if (bias != 0) - addBiasEntry( trace, tini, tfin, key, measType, bias, bvar); -// else if (key.type == KF::CODE_BIAS -// && (obsCode == acsConfig.clock_codesL1[Sat.sys] -// || obsCode == acsConfig.clock_codesL2[Sat.sys])) -// { -// bvar = 1e-12; -// addBiasEntry( trace, tini, tfin, key, measType, bias, bvar); -// } - - } - - if (acsConfig.ambrOpts.output_rec_bias == false) - continue; - - SatSys sat0; - sat0.sys = sys; - sat0.prn = 0; - - for (auto& [id, rec] : receiverMap) - for (auto& obsCode : acsConfig.code_priorities[sys]) - if (queryBiasOutput(trace, time, kfState, ionState, sat0, id, obsCode, bias, bvar, measType)) - { - key.Sat = sat0; - key.str = id; - key.num = obsCode; - - if (bias != 0) - addBiasEntry( trace, tini, tfin, key, measType, bias, bvar); - } - - } -} - -/** Write stored bias output to SINEX file - * Return number of written biases (-1 if file not found) -*/ -void writeBiasSinex( - Trace& trace, ///< Trace to output to - GTime time, ///< Time of bias to write - KFState& kfState, ///< Filter state to take biases from - KFState& ionState, ///< Filter state to take biases from - string biasfile, ///< File to write - ReceiverMap& receiverMap) ///< stations for which to output receiver biases -{ - tracepdeex(3,trace,"Writing bias SINEX into: %s %s\n", biasfile.c_str(), time.to_string().c_str()); - - std::ofstream outputStream(biasfile, std::fstream::in | std::fstream::out); - if (!outputStream) - { - tracepdeex(2, trace, "ERROR: cannot open bias SINEX output: %s\n", biasfile.c_str()); - return; - } - - if ( acsConfig.ambrOpts.code_output_interval <= 0 - && acsConfig.ambrOpts.phase_output_interval <= 0) - { - return; - } - - E_TimeSys tsys = E_TimeSys::NONE; - if (acsConfig.bias_time_system == "G") tsys = E_TimeSys::GPST; - else if (acsConfig.bias_time_system == "C") tsys = E_TimeSys::BDT; - else if (acsConfig.bias_time_system == "R") tsys = E_TimeSys::GLONASST; - else if (acsConfig.bias_time_system == "UTC") tsys = E_TimeSys::UTC; - else if (acsConfig.bias_time_system == "TAI") tsys = E_TimeSys::TAI; - - if (biasfile != lastBiasSINEXFile) - { - tracepdeex(3, trace, "\nStarting new bias SINEX file: %s\n", biasfile.c_str()); - - double updt1; - double updt2; - - if (acsConfig.ambrOpts.code_output_interval > acsConfig.ambrOpts.phase_output_interval) - { - updt1 = acsConfig.ambrOpts.code_output_interval; - updt2 = acsConfig.ambrOpts.phase_output_interval; - } - else - { - updt1 = acsConfig.ambrOpts.phase_output_interval; - updt2 = acsConfig.ambrOpts.code_output_interval; - } - - if (updt2==0) - updt2 = updt1; - - // int week; - // double tow = time2gpst(time, &week); - // tow = updt1 * floor (tow / updt1); - // GTime time0 = gpst2time (week, tow); - - GTime time0 = time.floorTime((int)updt1); - - sinexBiases_out.clear(); - - writeBSINEXHeader(time0, tsys, outputStream, updt2); - - lastBiasSINEXFile = biasfile; - } - - if (acsConfig.ambrOpts.code_output_interval > 0) updateBiasOutput(trace, time, kfState, ionState, receiverMap, CODE); - if (acsConfig.ambrOpts.phase_output_interval > 0) updateBiasOutput(trace, time, kfState, ionState, receiverMap, PHAS); - - int numbias=0; - for (auto& [key, biasMap] : sinexBiases_out) - for (auto& [ind, bias] : biasMap) - { - if (biasType(bias.cod1, bias.cod2) != "OSB") - continue; - - numbias++; - } - - updateFirstLine(time, tsys, outputStream, numbias); - - for (auto& [Key, biasMap] : sinexBiases_out) - for (auto& [ind, bias] : biasMap) - { - if ((time-bias.tfin) > DTTOL) - continue; - - if (bias.measType!=CODE) - continue; - - tracepdeex(5,trace,"\n CODE bias for %s %2d %s ... at pos %d", bias.Sat.id().c_str(), bias.cod1._to_string(), ind, bias.posInOutFile); - - writeBSINEXLine(time, tsys, bias, outputStream); - } - - for (auto& [Key, biasMap] : sinexBiases_out) - for (auto& [ind, bias] : biasMap) - { - if ((time-bias.tfin) > DTTOL) - continue; - - if (bias.measType==CODE) - continue; - - tracepdeex(5,trace,"\n PHASE bias for %s %2d %s ... at pos %d", bias.Sat.id().c_str(), bias.cod1._to_string(), ind, bias.posInOutFile); - - writeBSINEXLine(time, tsys, bias, outputStream); - } - - outputStream.seekp(bottomOfFile); - - tracepdeex(0, outputStream, "-BIAS/SOLUTION\n%%=ENDBIA"); - - return; -} - -/** Find and combine biases from multiple sources: -bias inputs, UC biases and DCB from ionosphere modules -*/ -bool queryBiasOutput( - Trace& trace, - GTime time, - KFState& kfState, - KFState& ionState, - SatSys Sat, - string Rec, - E_ObsCode obsCode, - double& bias, - double& variance, - E_MeasType type) -{ - bias = 0; - variance = 0; - - tracepdeex(3,trace,"\n Searching %s bias for %s %s %s: ",(type==CODE)?"CODE ":"PHASE", Sat.id().c_str(), obsCode._to_string(), time.to_string().c_str()); - - bool found = false; - - if (getBias(trace, time, Rec, Sat, obsCode, type, bias, variance)) - { - tracepdeex(4,trace, "found input %.4f %.4e", bias, variance); - found = true; - } - - if (acsConfig.process_ppp) - if (queryBiasUC(trace, time, kfState, Sat, Rec, obsCode, bias, variance, type)) - { - tracepdeex(4,trace, "found UC %.4f %.4e", bias, variance); - found = true; - } - - /* Ionosphere DCB */ - if (acsConfig.process_ionosphere) - { - double dcbBias = 0; - double dcbVar = 0; - - if (queryBiasDCB(trace, ionState, Sat, Rec, obsCode, dcbBias, dcbVar)) - { - found = true; - tracepdeex(4,trace, "found DCB"); - double sign = -1; - if (type == CODE) - sign = 1; - bias += sign*dcbBias; - variance += dcbVar; - } - } - - return true; -} +// #pragma GCC optimize ("O0") + +#include "ambres/GNSSambres.hpp" +#include "common/biases.hpp" +#include "common/constants.hpp" +#include "iono/ionoModel.hpp" +#include "pea/ppp.hpp" + +map> sinexBiases_out; +long int bottomOfFile = 0; +double startTimeofFile[3]; +string lastBiasSINEXFile; + +/** Convert enum observation code to code string + */ +string code2str( + E_ObsCode code, ///< The input enum observation code + E_MeasType measType ///< Measurement type of this observation - CODE/PHAS +) +{ + if (code == E_ObsCode::NONE) + return ""; + + string outstr = enum_to_string(code); + + char head; + if (measType == PHAS) + head = 'L'; + if (measType == CODE) + head = 'C'; + + outstr[0] = head; + + return outstr; +} + +/** Determine if the bias is an OSB or DSB given observation codes + */ +string biasType( + E_ObsCode code1, ///< Base code of observation for the bias + E_ObsCode code2 ///< Secondary code of observation for the bias +) +{ + if (code1 != E_ObsCode::NONE && code2 != E_ObsCode::NONE) + return "DSB"; + else if (code1 != E_ObsCode::NONE && code2 == E_ObsCode::NONE) + return "OSB"; + else if (code1 == E_ObsCode::NONE && code2 != E_ObsCode::NONE) + return "OSB"; + else + return "NONE"; +} + +/** Update bias SINEX first line + */ +void updateFirstLine( + GTime time, ///< Time of bias to write + E_TimeSys tsys, ///< Time system + Trace& trace, ///< Trace to output to + int numbias ///< Number of biases to be written +) +{ + GTime now = timeGet(); + + double ydsNow[3]; + time2yds(now, ydsNow, tsys); + + double ydsEnd[3]; + time2yds(time, ydsEnd, tsys); + + trace.seekp(0); + tracepdeex( + 0, + trace, + "%%=BIA 1.00 %3s %4d:%03d:%05d ", + acsConfig.analysis_agency.c_str(), + (int)ydsNow[0], + (int)ydsNow[1], + (int)ydsNow[2] + ); + tracepdeex( + 0, + trace, + "%3s %4d:%03d:%05d ", + acsConfig.analysis_agency.c_str(), + (int)startTimeofFile[0], + (int)startTimeofFile[1], + (int)startTimeofFile[2] + ); + tracepdeex( + 0, + trace, + "%4d:%03d:%05d A %8d\n", + (int)ydsEnd[0], + (int)ydsEnd[1], + (int)ydsEnd[2], + numbias + ); +} + +/** Write bias SINEX head for the first time + */ +void writeBSINEXHeader( + GTime time, ///< Time of bias to write + E_TimeSys tsys, ///< Time system + Trace& trace, ///< Trace to output to + double updateInterval ///< Bias Update Interval (seconds) +) +{ + time2yds(time, startTimeofFile, tsys); + updateFirstLine(time, tsys, trace, 0); + + tracepdeex( + 0, + trace, + "*-------------------------------------------------------------------------------\n" + ); + tracepdeex(0, trace, "+FILE/REFERENCE\n"); + tracepdeex( + 0, + trace, + " DESCRIPTION %s, %s\n", + acsConfig.analysis_agency.c_str(), + acsConfig.analysis_centre.c_str() + ); + tracepdeex( + 0, + trace, + " OUTPUT OSB estimates for day %3d, %4d\n", + startTimeofFile[1], + startTimeofFile[0] + ); + tracepdeex(0, trace, " CONTACT %s\n", acsConfig.ac_contact.c_str()); + tracepdeex(0, trace, " SOFTWARE %s\n", acsConfig.analysis_software.c_str()); + tracepdeex(0, trace, " INPUT %s\n", acsConfig.rinex_comment.c_str()); + tracepdeex(0, trace, "-FILE/REFERENCE\n"); + + tracepdeex( + 0, + trace, + "*-------------------------------------------------------------------------------\n" + ); + tracepdeex(0, trace, "+BIAS/DESCRIPTION\n"); + tracepdeex( + 0, + trace, + " OBSERVATION_SAMPLING %12.0f\n", + acsConfig.epoch_interval + ); + tracepdeex(0, trace, " PARAMETER_SPACING %12.0f\n", updateInterval); + tracepdeex(0, trace, " DETERMINATION_METHOD COMBINED_ANALYSIS\n"); + tracepdeex(0, trace, " BIAS_MODE ABSOLUTE\n"); + tracepdeex( + 0, + trace, + " TIME_SYSTEM %s \n", + acsConfig.bias_time_system.c_str() + ); + + auto& recOpts = acsConfig.getRecOpts("global"); + E_Sys refConst = recOpts.receiver_reference_system; + tracepdeex( + 0, + trace, + " RECEIVER_CLOCK_REFERENCE_GNSS %c\n", + enum_to_string(refConst)[0] + ); + + for (auto& [sys, solve] : acsConfig.solve_amb_for) + { + if (solve == false) + continue; + + char sysChar; + switch (sys) + { + case E_Sys::GPS: + sysChar = 'G'; + break; + case E_Sys::GLO: + sysChar = 'R'; + break; + case E_Sys::GAL: + sysChar = 'E'; + break; + case E_Sys::QZS: + sysChar = 'J'; + break; + case E_Sys::BDS: + sysChar = 'C'; + break; + default: + continue; + } + + // E_ObsCode code1= acsConfig.clock_codesL1[sys]; + // E_ObsCode code2= acsConfig.clock_codesL2[sys]; + // tracepdeex(0, trace, " SATELLITE_CLOCK_REFERENCE_OBSERVABLES %c %s %s\n", + // sysChar,enum_to_string(code1),enum_to_string(code2)); + } + tracepdeex(0, trace, "-BIAS/DESCRIPTION\n"); + + tracepdeex( + 0, + trace, + "*-------------------------------------------------------------------------------\n" + ); + tracepdeex(0, trace, "+BIAS/SOLUTION\n"); + tracepdeex( + 0, + trace, + "*BIAS SVN_ PRN STATION__ OBS1 OBS2 BIAS_START____ BIAS_END______ UNIT " + "__ESTIMATED_VALUE____ _STD_DEV___\n" + ); + + bottomOfFile = trace.tellp(); +} + +/** print bias SINEX data line + */ +int writeBSINEXLine( + GTime time, ///< Time of bias to write + E_TimeSys tsys, ///< Time system + BiasEntry& bias, ///< Bias entry to write + Trace& trace ///< Stream to output to +) +{ + string typstr = biasType(bias.cod1, bias.cod2); + + if (typstr != "OSB") + return 0; + + string sat = " "; + string svn = " "; + if (bias.Sat.prn == 0) + { + char sysChar = bias.Sat.sysChar(); + sat[0] = sysChar; + svn[0] = sysChar; + } + else + { + sat = bias.Sat.id(); + svn = bias.Sat.svn(); + } + + string cod1 = code2str(bias.cod1, bias.measType); + string cod2 = code2str(bias.cod2, bias.measType); + + bool newbottom = false; + if (bias.posInOutFile < 0) + { + trace.seekp(bottomOfFile); + + bias.posInOutFile = bottomOfFile; + newbottom = true; + } + else + { + trace.seekp(bias.posInOutFile); + } + + tracepdeex( + 0, + trace, + " %-4s %4s %3s %-9s %-4s %-4s", + typstr.c_str(), + svn.c_str(), + sat.c_str(), + bias.name.c_str(), + cod1.c_str(), + cod2.c_str() + ); + + double tini[3]; + double tend[3]; + time2yds(bias.tini, tini, tsys); + time2yds(bias.tfin, tend, tsys); + + tracepdeex( + 0, + trace, + " %4d:%03d:%05d %4d:%03d:%05d %-4s", + (int)tini[0], + (int)tini[1], + (int)tini[2], + (int)tend[0], + (int)tend[1], + (int)tend[2], + "ns" + ); + + tracepdeex(0, trace, " %21.5f", bias.bias * (1E9 / CLIGHT)); + tracepdeex(0, trace, " %11.6f", sqrt(bias.var) * (1E9 / CLIGHT)); + + tracepdeex(0, trace, "\n"); + + if (newbottom) + bottomOfFile = trace.tellp(); + + return 1; +} + +/** Adding/updating new bias entry + */ +int addBiasEntry( + Trace& trace, + GTime tini, + GTime tfin, + KFKey kfKey, + E_MeasType measType, + double bias, + double var +) +{ + auto& biasMap = sinexBiases_out[kfKey]; + int found = -1; + + for (auto& [ind, bias] : biasMap) + { + if (bias.tini != tini) + continue; + if (bias.measType != measType) + continue; + + found = ind; + break; + } + + tracepdeex( + 3, + trace, + "\n Searched %s bias for %s %3s %s: ", + (measType == CODE) ? "CODE " : "PHASE", + kfKey.Sat.id().c_str(), + kfKey.code().c_str(), + tini.to_string().c_str() + ); + + if (found >= 0) + { + biasMap[found].bias = bias; + + if (var < 1e-12) + var = 1e-12; + + biasMap[found].var = var; + tracepdeex(4, trace, " found at index %d: %.4f %.4f", found, bias, var); + } + else + { + found = biasMap.size(); + BiasEntry entry; + entry.name = kfKey.str; + entry.Sat = kfKey.Sat; + entry.cod1 = int_to_enum(kfKey.num); + entry.cod2 = E_ObsCode::NONE; + + entry.measType = measType; + entry.tini = tini; + entry.tfin = tfin; + + entry.bias = bias; + entry.var = var; + entry.slop = 0; + entry.slpv = 0; + entry.posInOutFile = -1; + biasMap[found] = entry; + + tracepdeex(4, trace, " not found, stored at index %d: %.4f %.4f", found, bias, var); + } + return found; +} + +/** Store bias output to write into bias SINEX files + */ +void updateBiasOutput( + Trace& trace, ///< Trace to output to + GTime time, ///< Time of bias update + KFState& kfState, ///< Filter state to take biases from + KFState& ionState, ///< Filter state to take biases from + ReceiverMap& receiverMap, ///< Receivers for which to output biases + E_MeasType measType ///< Type of measurement to find bias for +) +{ + int nstore = 0; + double bias; + double bvar; + + double updateRate = 0; + if (measType == E_MeasType::CODE) + updateRate = acsConfig.ambrOpts.code_output_interval; + if (measType == E_MeasType::PHAS) + updateRate = acsConfig.ambrOpts.phase_output_interval; + + if (updateRate <= 0) + return; + + if (updateRate < 30) + updateRate = 30; + + GWeek week = time; + GTow tow = time; + + tow = updateRate * floor(tow / updateRate); + GTime tini = gpst2time(week, tow); + GTime tfin = tini + updateRate; + + KFKey key; + if (measType == E_MeasType::CODE) + key.type = KF::CODE_BIAS; + if (measType == E_MeasType::PHAS) + key.type = KF::PHASE_BIAS; + + for (E_Sys sys : magic_enum::enum_values()) + { + if (acsConfig.process_sys[sys] == false) + continue; + + auto sats = getSysSats(sys); + + for (auto& Sat : sats) + for (auto& obsCode : acsConfig.code_priorities[sys]) + if (queryBiasOutput( + trace, + time, + kfState, + ionState, + Sat, + "", + obsCode, + bias, + bvar, + measType + )) + { + key.Sat = Sat; + key.str = ""; + key.num = static_cast(obsCode); + + if (bias != 0) + addBiasEntry(trace, tini, tfin, key, measType, bias, bvar); + // else if (key.type == KF::CODE_BIAS + // && (obsCode == acsConfig.clock_codesL1[Sat.sys] + // || obsCode == acsConfig.clock_codesL2[Sat.sys])) + // { + // bvar = 1e-12; + // addBiasEntry( trace, tini, tfin, key, measType, bias, bvar); + // } + } + + if (acsConfig.ambrOpts.output_rec_bias == false) + continue; + + SatSys sat0; + sat0.sys = sys; + sat0.prn = 0; + + for (auto& [id, rec] : receiverMap) + for (auto& obsCode : acsConfig.code_priorities[sys]) + if (queryBiasOutput( + trace, + time, + kfState, + ionState, + sat0, + id, + obsCode, + bias, + bvar, + measType + )) + { + key.Sat = sat0; + key.str = id; + key.num = static_cast(obsCode); + + if (bias != 0) + addBiasEntry(trace, tini, tfin, key, measType, bias, bvar); + } + } +} + +/** Write stored bias output to SINEX file + * Return number of written biases (-1 if file not found) + */ +void writeBiasSinex( + Trace& trace, ///< Trace to output to + string biasfile, ///< File to write + GTime time, ///< Time of bias to write + KFState& kfState, ///< Filter state to take biases from + KFState& ionState, ///< Filter state to take biases from + ReceiverMap& receiverMap ///< Receivers for which to output biases +) +{ + tracepdeex( + 3, + trace, + "Writing bias SINEX into: %s %s\n", + biasfile.c_str(), + time.to_string().c_str() + ); + + std::ofstream outputStream(biasfile, std::fstream::in | std::fstream::out); + if (!outputStream) + { + tracepdeex(2, trace, "ERROR: cannot open bias SINEX output: %s\n", biasfile.c_str()); + return; + } + + if (acsConfig.ambrOpts.code_output_interval <= 0 && + acsConfig.ambrOpts.phase_output_interval <= 0) + { + return; + } + + E_TimeSys tsys = E_TimeSys::NONE; + if (acsConfig.bias_time_system == "G") + tsys = E_TimeSys::GPST; + else if (acsConfig.bias_time_system == "C") + tsys = E_TimeSys::BDT; + else if (acsConfig.bias_time_system == "R") + tsys = E_TimeSys::GLONASST; + else if (acsConfig.bias_time_system == "UTC") + tsys = E_TimeSys::UTC; + else if (acsConfig.bias_time_system == "TAI") + tsys = E_TimeSys::TAI; + + if (biasfile != lastBiasSINEXFile) + { + tracepdeex(3, trace, "\nStarting new bias SINEX file: %s\n", biasfile.c_str()); + + double updt1; + double updt2; + + if (acsConfig.ambrOpts.code_output_interval > acsConfig.ambrOpts.phase_output_interval) + { + updt1 = acsConfig.ambrOpts.code_output_interval; + updt2 = acsConfig.ambrOpts.phase_output_interval; + } + else + { + updt1 = acsConfig.ambrOpts.phase_output_interval; + updt2 = acsConfig.ambrOpts.code_output_interval; + } + + if (updt2 == 0) + updt2 = updt1; + + // int week; + // double tow = time2gpst(time, &week); + // tow = updt1 * floor (tow / updt1); + // GTime time0 = gpst2time (week, tow); + + GTime time0 = time.floorTime((int)updt1); + + sinexBiases_out.clear(); + + writeBSINEXHeader(time0, tsys, outputStream, updt2); + + lastBiasSINEXFile = biasfile; + } + + if (acsConfig.ambrOpts.code_output_interval > 0) + updateBiasOutput(trace, time, kfState, ionState, receiverMap, CODE); + if (acsConfig.ambrOpts.phase_output_interval > 0) + updateBiasOutput(trace, time, kfState, ionState, receiverMap, PHAS); + + int numbias = 0; + for (auto& [key, biasMap] : sinexBiases_out) + for (auto& [ind, bias] : biasMap) + { + if (biasType(bias.cod1, bias.cod2) != "OSB") + continue; + + numbias++; + } + + updateFirstLine(time, tsys, outputStream, numbias); + + for (auto& [Key, biasMap] : sinexBiases_out) + for (auto& [ind, bias] : biasMap) + { + if ((time - bias.tfin) > DTTOL) + continue; + + if (bias.measType != CODE) + continue; + + tracepdeex( + 5, + trace, + "\n CODE bias for %s %s %2d ... at pos %d", + bias.Sat.id().c_str(), + enum_to_string(bias.cod1).c_str(), + ind, + bias.posInOutFile + ); + + writeBSINEXLine(time, tsys, bias, outputStream); + } + + for (auto& [Key, biasMap] : sinexBiases_out) + for (auto& [ind, bias] : biasMap) + { + if ((time - bias.tfin) > DTTOL) + continue; + + if (bias.measType == CODE) + continue; + + tracepdeex( + 5, + trace, + "\n PHASE bias for %s %2d %s ... at pos %d", + bias.Sat.id().c_str(), + enum_to_string(bias.cod1).c_str(), + ind, + bias.posInOutFile + ); + + writeBSINEXLine(time, tsys, bias, outputStream); + } + + outputStream.seekp(bottomOfFile); + + tracepdeex(0, outputStream, "-BIAS/SOLUTION\n%%=ENDBIA"); + + return; +} + +/** Find and combine biases from multiple sources: +bias inputs, UC biases and DCB from ionosphere modules +*/ +bool queryBiasOutput( + Trace& trace, + GTime time, + KFState& kfState, + KFState& ionState, + SatSys Sat, + string Rec, + E_ObsCode obsCode, + double& bias, + double& variance, + E_MeasType type +) +{ + bias = 0; + variance = 0; + + tracepdeex( + 3, + trace, + "\n Searching %s bias for %s %s %s: ", + (type == CODE) ? "CODE " : "PHASE", + Sat.id().c_str(), + enum_to_string(obsCode), + time.to_string().c_str() + ); + + bool found = false; + + if (getBias(trace, time, Rec, Sat, obsCode, type, bias, variance)) + { + tracepdeex(4, trace, "found input %.4f %.4e", bias, variance); + found = true; + } + + if (acsConfig.process_ppp) + if (queryBiasUC(trace, time, kfState, Sat, Rec, obsCode, bias, variance, type)) + { + tracepdeex(4, trace, "found UC %.4f %.4e", bias, variance); + found = true; + } + + /* Ionosphere DCB */ + if (acsConfig.process_ionosphere) + { + double dcbBias = 0; + double dcbVar = 0; + + if (queryBiasDCB(trace, ionState, Sat, Rec, obsCode, dcbBias, dcbVar)) + { + found = true; + tracepdeex(4, trace, "found DCB"); + double sign = -1; + if (type == CODE) + sign = 1; + bias += sign * dcbBias; + variance += dcbVar; + } + } + + return true; +} diff --git a/src/cpp/common/biases.cpp b/src/cpp/common/biases.cpp index 0968ea17e..b4d5da733 100644 --- a/src/cpp/common/biases.cpp +++ b/src/cpp/common/biases.cpp @@ -1,596 +1,642 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -FileType BSX__() -{ - -} - +#include "common/biases.hpp" #include +#include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/constants.hpp" +#include "common/enums.h" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "biases.hpp" -#include "enums.h" - +FileType BSX__() {} -BiasMap biasMaps; ///< Multi dimensional map, as biasMaps[measType][id][code1][code2][time] +BiasMap biasMaps; ///< Multi dimensional map, as biasMaps[measType][id][code1][code2][time] +void updateRefTime(BiasEntry& entry) +{ + if (entry.tini != GTime::noTime() && entry.tfin != GTime::noTime()) + entry.refTime = entry.tini + (entry.tfin - entry.tini).to_double() / 2; + else if (entry.tini != GTime::noTime() && entry.tfin == GTime::noTime()) + entry.refTime = entry.tini; + else if (entry.tini == GTime::noTime() && entry.tfin != GTime::noTime()) + entry.refTime = entry.tfin; + else + { + BOOST_LOG_TRIVIAL(error) << "Invalid interval for " << std::to_string(entry.measType) + << " bias: " << entry.name << ":" << entry.Sat.id() << ":" + << enum_to_string(entry.cod1) << "-" << enum_to_string(entry.cod2); + } +} /** Initialise satellite DSBs between default signals, e.g. P1-P2 DCBs, with 0 values -*/ + */ void initialiseBias() { - BiasEntry entry; - entry.bias = 0; - entry.var = 0; - entry.measType = CODE; - entry.name = ""; - entry.source = "init"; - - for (E_Sys sys : E_Sys::_values()) - { - auto sats = getSysSats(sys); - if (acsConfig.process_sys[sys]) - for (auto Sat : sats) - { - string id = Sat.id() + ":" + string(1,Sat.sysChar()); - entry.Sat = Sat; -// entry.cod1 = acsConfig.clock_codesL1[sys]; -// entry.cod2 = acsConfig.clock_codesL2[sys]; - - pushBiasEntry(id, entry); - } - } + BiasEntry entry; + entry.bias = 0; + entry.var = 0; + entry.measType = CODE; + entry.name = ""; + entry.source = "init"; + + for (E_Sys sys : magic_enum::enum_values()) + { + auto sats = getSysSats(sys); + if (acsConfig.process_sys[sys]) + for (auto Sat : sats) + { + string id = Sat.id() + ":" + string(1, Sat.sysChar()); + entry.Sat = Sat; + // entry.cod1 = acsConfig.clock_codesL1[sys]; + // entry.cod2 = acsConfig.clock_codesL2[sys]; + + pushBiasEntry(id, entry); + } + } } /** Add default 0 values to DSBs between default signals, e.g. P1-P2 DCBs, for each bias ID -*/ + */ void addDefaultBias() { - BiasEntry entry; - entry.bias = 0; - entry.var = 0; - entry.measType = CODE; - - for (auto& [id, obsObsBiasMap] : biasMaps[entry.measType]) - { - // get Sat and receiver name from the first entry in the CODE bias map - auto obsBiasMap = obsObsBiasMap .begin()->second; - auto biasMap = obsBiasMap .begin()->second; - auto bias = biasMap .begin()->second; - - entry.Sat = bias.Sat; - entry.name = bias.name; -// entry.cod1 = acsConfig.clock_codesL1[bias.Sat.sys]; -// entry.cod2 = acsConfig.clock_codesL2[bias.Sat.sys]; - entry.source = "def1"; - -// pushBiasEntry(id, entry); //todo aaron, disabled - } - - for (auto& Sat : getSysSats(E_Sys::GPS)) - { - entry.cod1 = E_ObsCode::L1W; - entry.cod2 = E_ObsCode::L1C; - entry.Sat = Sat; - entry.source = "def2"; - - string id = Sat.id() + ":" + Sat.sysChar(); - - pushBiasEntry(id, entry); - } + BiasEntry entry; + entry.bias = 0; + entry.var = 0; + entry.measType = CODE; + + for (auto& [id, obsObsBiasMap] : biasMaps[entry.measType]) + { + // get Sat and receiver name from the first entry in the CODE bias map + auto obsBiasMap = obsObsBiasMap.begin()->second; + auto biasMap = obsBiasMap.begin()->second; + auto bias = biasMap.begin()->second; + + entry.Sat = bias.Sat; + entry.name = bias.name; + // entry.cod1 = acsConfig.clock_codesL1[bias.Sat.sys]; + // entry.cod2 = acsConfig.clock_codesL2[bias.Sat.sys]; + entry.source = "def1"; + + // pushBiasEntry(id, entry); //todo aaron, disabled + } + + for (auto& Sat : getSysSats(E_Sys::GPS)) + { + entry.cod1 = E_ObsCode::L1W; + entry.cod2 = E_ObsCode::L1C; + entry.Sat = Sat; + entry.source = "def2"; + + string id = Sat.id() + ":" + Sat.sysChar(); + + pushBiasEntry(id, entry); + } } -void loadStateBiases( //todo aaron this probably needs to be called to write biases from filter to files - KFState& kfState) +void loadStateBiases( // todo aaron this probably needs to be called to write biases from filter to + // files + KFState& kfState +) { - for (auto& [kfKey, index] : kfState.kfIndexMap) - { - if ( kfKey.type != +KF::CODE_BIAS - &&kfKey.type != +KF::PHASE_BIAS) - { - continue; - } - - BiasEntry entry; - if (kfKey.type == +KF::CODE_BIAS) entry.measType = CODE; - if (kfKey.type == +KF::PHASE_BIAS) entry.measType = PHAS; - - entry.tini = kfState.time; - entry.cod1 = E_ObsCode::_from_integral(kfKey.num); - entry.bias = kfState.x(index); - entry.var = kfState.P(index,index); - entry.name = kfKey.str; - entry.Sat = kfKey.Sat; - entry.source = "KALMAN"; - - string id; - if (entry.name.empty() == false) id = entry.name; - else id = entry.Sat.id(); - id.push_back(':'); - id.push_back(entry.Sat.sysChar()); - - pushBiasEntry(id, entry); - } + for (auto& [kfKey, index] : kfState.kfIndexMap) + { + if (kfKey.type != KF::CODE_BIAS && kfKey.type != KF::PHASE_BIAS) + { + continue; + } + + BiasEntry entry; + if (kfKey.type == KF::CODE_BIAS) + entry.measType = CODE; + if (kfKey.type == KF::PHASE_BIAS) + entry.measType = PHAS; + + entry.tini = kfState.time; + entry.cod1 = int_to_enum(kfKey.num); + entry.bias = kfState.x(index); + entry.var = kfState.P(index, index); + entry.name = kfKey.str; + entry.Sat = kfKey.Sat; + entry.source = "KALMAN"; + + string id; + if (entry.name.empty() == false) + id = entry.name; + else + id = entry.Sat.id(); + id.push_back(':'); + id.push_back(entry.Sat.sysChar()); + + updateRefTime(entry); + pushBiasEntry(id, entry); + } } /** Push forward and reverse bias entry into biasMaps -*/ + */ void pushBiasEntry( - string id, ///< Device ID - BiasEntry entry) ///< Bias entry to push into biasMaps + string id, ///< Device ID + BiasEntry entry ///< Bias entry to push into biasMaps +) { - //add forward bias to maps - biasMaps [entry.measType] - [id] - [entry.cod1] - [entry.cod2] - [entry.tini] = entry; - - decomposeDSBBias(id, entry); - - //create reverse bias and add to maps - entry.bias *= -1; - entry.slop *= -1; - - E_ObsCode swap = entry.cod1; - entry.cod1 = entry.cod2; - entry.cod2 = swap; - - biasMaps [entry.measType] - [id] - [entry.cod1] - [entry.cod2] - [entry.tini] = entry; - - decomposeDSBBias(id, entry); + // add forward bias to maps + biasMaps[entry.measType][id][entry.cod1][entry.cod2][entry.tini] = entry; + + decomposeDSBBias(id, entry); + + // create reverse bias and add to maps + entry.bias *= -1; + entry.slop *= -1; + + E_ObsCode swap = entry.cod1; + entry.cod1 = entry.cod2; + entry.cod2 = swap; + + biasMaps[entry.measType][id][entry.cod1][entry.cod2][entry.tini] = entry; + + decomposeDSBBias(id, entry); } -void cullOldBiases( - GTime time) +void cullOldBiases(GTime time) { - for (auto& typeBiasMap : biasMaps) - for (auto& [dummy1, idBiasMap] : typeBiasMap) - for (auto& [dummy2, code1BiasMap] : idBiasMap) - for (auto& [dummy3, code2BiasMap] : code1BiasMap) - { - auto foundIt = code2BiasMap.lower_bound(time); - - if (foundIt == code2BiasMap.end()) - { - continue; - } - - foundIt++; - - //delete all before this found one (after since its reversed ordering) - code2BiasMap.erase(foundIt, code2BiasMap.end()); - } + for (auto& typeBiasMap : biasMaps) + for (auto& [dummy1, idBiasMap] : typeBiasMap) + for (auto& [dummy2, code1BiasMap] : idBiasMap) + for (auto& [dummy3, code2BiasMap] : code1BiasMap) + { + auto foundIt = code2BiasMap.lower_bound(time); + + if (foundIt == code2BiasMap.end()) + { + continue; + } + + foundIt++; // Always reserve the latest entry when culling + + // delete all before this found one (after since its reversed ordering) + code2BiasMap.erase(foundIt, code2BiasMap.end()); + } } /** Decompose DSB or DCB into OSBs -*/ + */ bool decomposeDSBBias( - string id, ///< ID of the bias - BiasEntry& DSB) ///< DSB to be decomposed + string id, ///< ID of the bias + BiasEntry& DSB ///< DSB to be decomposed +) { - auto& Sat = DSB.Sat; + auto& Sat = DSB.Sat; -// if ( DSB.cod1 != acsConfig.clock_codesL1[Sat.sys] -// ||DSB.cod2 != acsConfig.clock_codesL2[Sat.sys]) -// { -// return false; -// } + // if ( DSB.cod1 != acsConfig.clock_codesL1[Sat.sys] + // ||DSB.cod2 != acsConfig.clock_codesL2[Sat.sys]) + // { + // return false; + // } - E_FType ft1 = code2Freq[Sat.sys][DSB.cod1]; - E_FType ft2 = code2Freq[Sat.sys][DSB.cod2]; - double lam1 = genericWavelength[ft1]; - double lam2 = genericWavelength[ft2]; + E_FType ft1 = code2Freq[Sat.sys][DSB.cod1]; + E_FType ft2 = code2Freq[Sat.sys][DSB.cod2]; + double lam1 = genericWavelength[ft1]; + double lam2 = genericWavelength[ft2]; - if ( lam1 == 0 - || lam2 == 0) - { - return false; - } + if (lam1 == 0 || lam2 == 0) + { + return false; + } - if (lam1 == lam2) - { - return false; - } + if (lam1 == lam2) + { + return false; + } - double c2 = -SQR(lam1) / (SQR(lam2) - SQR(lam1)); - double c1 = c2 - 1; + double c2 = -SQR(lam1) / (SQR(lam2) - SQR(lam1)); + double c1 = c2 - 1; - BiasEntry entry = DSB; - entry.cod2 = E_ObsCode::NONE; - entry.source += "*"; + BiasEntry entry = DSB; + entry.cod2 = E_ObsCode::NONE; + entry.source += "*"; - entry.cod1 = DSB.cod1; - entry.bias = c2 * DSB.bias; - entry.var = SQR(c2) * DSB.var; + entry.cod1 = DSB.cod1; + entry.bias = c2 * DSB.bias; + entry.var = SQR(c2) * DSB.var; - pushBiasEntry(id, entry); + pushBiasEntry(id, entry); - entry.cod1 = DSB.cod2; - entry.bias = c1 * DSB.bias; - entry.var = SQR(c1) * DSB.var; + entry.cod1 = DSB.cod2; + entry.bias = c1 * DSB.bias; + entry.var = SQR(c1) * DSB.var; - pushBiasEntry(id, entry); + pushBiasEntry(id, entry); - return true; + return true; } /** Convert GPS/QZS TGD into OSBs & DSB -*/ + */ bool decomposeTGDBias( - SatSys Sat, ///< The satellite to decompose the bias of - double tgd) ///< GPS or QZS TGD to be decomposed + SatSys Sat, ///< The satellite to decompose the bias of + double tgd ///< GPS or QZS TGD to be decomposed +) { - auto sys = Sat.sys; - - E_ObsCode cod1 = E_ObsCode::NONE; - E_ObsCode cod2 = E_ObsCode::NONE; - if (sys == +E_Sys::GPS) { cod1 = E_ObsCode::L1W; cod2 = E_ObsCode::L2W; } - else if (sys == +E_Sys::QZS) { cod1 = E_ObsCode::L1C; cod2 = E_ObsCode::L2L; } - else return false; - - string id = Sat.id() + ":" + Sat.sysChar(); - double bias = tgd * CLIGHT; - double gamma = SQR(FREQ1) / SQR(FREQ2); - - BiasEntry entry; - entry.tini.bigTime = 1; - entry.measType = CODE; - entry.Sat = Sat; - entry.name = Sat.id(); - - //covert TGD to P1-P2 DCB - entry.cod1 = cod1; - entry.cod2 = cod2; - entry.bias = bias * (1 - gamma); - entry.var = 0; - entry.source = "tgd"; - - pushBiasEntry(id, entry); - - return true; + auto sys = Sat.sys; + + E_ObsCode cod1 = E_ObsCode::NONE; + E_ObsCode cod2 = E_ObsCode::NONE; + if (sys == E_Sys::GPS) + { + cod1 = E_ObsCode::L1W; + cod2 = E_ObsCode::L2W; + } + else if (sys == E_Sys::QZS) + { + cod1 = E_ObsCode::L1C; + cod2 = E_ObsCode::L2L; + } + else + return false; + + string id = Sat.id() + ":" + Sat.sysChar(); + double bias = tgd * CLIGHT; + double gamma = SQR(FREQ1) / SQR(FREQ2); + + BiasEntry entry; + entry.tini.bigTime = 1; + entry.measType = CODE; + entry.Sat = Sat; + entry.name = Sat.id(); + + // covert TGD to P1-P2 DCB + entry.cod1 = cod1; + entry.cod2 = cod2; + entry.bias = bias * (1 - gamma); + entry.var = 0; + entry.source = "tgd"; + + updateRefTime(entry); + pushBiasEntry(id, entry); + + return true; } /** Convert GAL BGDs into OSBs & DSB -*/ + */ bool decomposeBGDBias( - SatSys Sat, ///< The satellite to decompose the bias of - double bgd1, ///< BGD E5a/E1 to be decomposed - double bgd2) ///< BGD E5b/E1 to be decomposed + SatSys Sat, ///< The satellite to decompose the bias of + double bgd1, ///< BGD E5a/E1 to be decomposed + double bgd2 ///< BGD E5b/E1 to be decomposed +) { - E_ObsCode cod1 = E_ObsCode::L1C; - E_ObsCode cod2 = E_ObsCode::L5Q; - E_ObsCode cod3 = E_ObsCode::L7Q; - - string id = Sat.id() + ":" + Sat.sysChar(); - double bgdE1E5a = bgd1 * CLIGHT; - double bgdE1E5b = bgd2 * CLIGHT; - double gammaE1E5a = SQR(FREQ1) / SQR(FREQ5); - double gammaE1E5b = SQR(FREQ1) / SQR(FREQ7); - - BiasEntry entry; - entry.tini.bigTime = 1; - entry.measType = CODE; - entry.Sat = Sat; - entry.name = ""; - entry.source = "bgd"; - - //store BGD E5b/E1 as C1C-IF OSB - entry.cod1 = cod1; - entry.cod2 = E_ObsCode::NONE; - entry.bias = bgdE1E5a; - entry.var = 0; - - pushBiasEntry(id, entry); //todo aaron, check which of these match the clock_codes and only create those. - - //covert BGD E5a/E1 to C5Q-IF OSB - entry.cod1 = cod2; - entry.cod2 = E_ObsCode::NONE; - entry.bias = bgdE1E5a * gammaE1E5a; - entry.var = 0; - - pushBiasEntry(id, entry); - - //covert BGD E5b/E1 to C7Q-IF OSB - entry.cod1 = cod3; - entry.cod2 = E_ObsCode::NONE; - entry.bias = bgdE1E5a - bgdE1E5b * (1 - gammaE1E5b); - entry.var = 0; - - pushBiasEntry(id, entry); - - //covert BGD E5b/E1 to C1C-C7Q DSB - entry.cod1 = cod1; - entry.cod2 = cod3; - entry.bias = bgdE1E5b * (1 - gammaE1E5b); - entry.var = 0; - - pushBiasEntry(id, entry); - - return true; + E_ObsCode cod1 = E_ObsCode::L1C; + E_ObsCode cod2 = E_ObsCode::L5Q; + E_ObsCode cod3 = E_ObsCode::L7Q; + + string id = Sat.id() + ":" + Sat.sysChar(); + double bgdE1E5a = bgd1 * CLIGHT; + double bgdE1E5b = bgd2 * CLIGHT; + double gammaE1E5a = SQR(FREQ1) / SQR(FREQ5); + double gammaE1E5b = SQR(FREQ1) / SQR(FREQ7); + + BiasEntry entry; + entry.tini.bigTime = 1; + entry.measType = CODE; + entry.Sat = Sat; + entry.name = ""; + entry.source = "bgd"; + + // store BGD E5b/E1 as C1C-IF OSB + entry.cod1 = cod1; + entry.cod2 = E_ObsCode::NONE; + entry.bias = bgdE1E5a; + entry.var = 0; + + updateRefTime(entry); + pushBiasEntry( + id, + entry + ); // todo aaron, check which of these match the clock_codes and only create those. + + // covert BGD E5a/E1 to C5Q-IF OSB + entry.cod1 = cod2; + entry.cod2 = E_ObsCode::NONE; + entry.bias = bgdE1E5a * gammaE1E5a; + entry.var = 0; + + pushBiasEntry(id, entry); + + // covert BGD E5b/E1 to C7Q-IF OSB + entry.cod1 = cod3; + entry.cod2 = E_ObsCode::NONE; + entry.bias = bgdE1E5a - bgdE1E5b * (1 - gammaE1E5b); + entry.var = 0; + + pushBiasEntry(id, entry); + + // covert BGD E5b/E1 to C1C-C7Q DSB + entry.cod1 = cod1; + entry.cod2 = cod3; + entry.bias = bgdE1E5b * (1 - gammaE1E5b); + entry.var = 0; + + pushBiasEntry(id, entry); + + return true; } - BiasEntry interpolateBias( - GTime& time, ///< Time of bias to interpolate - const BiasEntry& bias1, ///< First bias entry - const BiasEntry& bias2) ///< Second bias entry + GTime& time, ///< Time of bias to interpolate + const BiasEntry& bias1, ///< First bias entry + const BiasEntry& bias2 ///< Second bias entry +) { - BiasEntry output = bias1; - - double dt1 = (time - bias1.refTime).to_double(); - double dt2 = (time - bias2.refTime).to_double(); - double dT = (bias2.refTime - bias1.refTime).to_double(); - - if ( time < bias2.tini // bias1.tini < time < bias2.tini, do interpolation - &&output.slop == 0) // calculate bias slope (as to interpolate linearly) if not available - { - double coeff1 = -dt2 / dT; - double coeff2 = -dt1 / dT; - output.bias = bias1.bias * coeff1 - bias2.bias * coeff2; - output.var = bias1.var * SQR(coeff1) + bias2.var * SQR(coeff2); - output.tini = bias1.refTime; - output.tfin = bias2.refTime; - } - else // otherwise use existing bias slope value (and tini and tfin) whether it is 0 or not - { - output.bias += output.slop * dt1; - output.var = bias1.var + bias1.slpv * SQR(dt1); - } - - return output; + double dt1 = (time - bias1.refTime).to_double(); + double dt2 = (time - bias2.refTime).to_double(); + double dT = (bias2.refTime - bias1.refTime).to_double(); + + BiasEntry output = bias1; + if (dT == 0 || + output.slop != 0) // Use bias1 to extrapolate when not interpolatable or slop is not 0 + { + output.bias += output.slop * dt1; + output.var += output.slpv * SQR(dt1); + } + else // Otherwise do interpolation + { + double coeff1 = -dt2 / dT; + double coeff2 = -dt1 / dT; + output.bias = bias1.bias * coeff1 - bias2.bias * coeff2; + output.var = bias1.var * SQR(coeff1) + bias2.var * SQR(coeff2); + output.tini = time; // Always reference to current time + // output.tfin = GTime::noTime(); + // output.slop = (bias2.bias - bias1.bias) / dT; + // output.slpv = (bias2.var + bias1.var) / SQR(dT); + } + + return output; } bool calculateBias( - GTime& time, ///< Time of bias to look up - BiasEntry& output, ///< The bias entry retrieved - const TimeBiasMap& timeBiasMap) ///< Bias map for given measrement type, device & observation codes, as timeBiasMap[time] + GTime& time, ///< Time of bias to look up + BiasEntry& output, ///< The bias entry retrieved + const TimeBiasMap& timeBiasMap ///< Bias map for given measrement type, device & observation + ///< codes, as timeBiasMap[time] +) { - //find the last bias in that map that comes before the desired time - auto biasIt = timeBiasMap.lower_bound(time); - if (biasIt == timeBiasMap.end()) - { - return false; - } - - //a valid time entry was found, use it - auto& [dummy1, bias1] = *biasIt; - - //get the first bias in that map that comes after the desired time, if no entry comes after, use the earlier one (same to bias1) for extrapolation purpose - if (biasIt != timeBiasMap.begin()) - { - biasIt--; - } - else - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No suitable data for bias interpolation, extrapolated bias in use."; - } - - auto& [dummy2, bias2] = *biasIt; - - GTime tfin = bias2.tfin; - if (tfin == GTime::noTime()) - { - tfin = bias2.tini + S_IN_DAY; - } - - if (time <= tfin) // Only calculate bias when requested epoch time is within the valid time period - { - output = interpolateBias(time, bias1, bias2); - return true; - } - - return false; + // find the last bias in that map that comes no later than the desired time + auto biasIt = timeBiasMap.lower_bound(time); + if (biasIt == timeBiasMap.end()) + { + return false; + } + + // a valid time entry was found, use it + auto& [dummy1, bias1] = *biasIt; + + // get the first bias in that map that comes after the desired time, if no entry comes after, + // use the earlier one (same to bias1) for extrapolation purpose + if (biasIt != timeBiasMap.begin()) + { + biasIt--; + } + else + { + BOOST_LOG_TRIVIAL(warning) + << "No suitable data for bias interpolation, extrapolated bias in use."; + } + + auto& [dummy2, bias2] = *biasIt; + + output = interpolateBias(time, bias1, bias2); + + if (isnan(output.bias) == false) + { + return true; + } + + return false; } void setRestrictiveStartTime( - GTime& current, ///< Current start time of bias - GTime& potential) ///< Potential start time of bias + GTime& current, ///< Current start time of bias + GTime& potential ///< Potential start time of bias +) { - if (current < potential) - { - current = potential; - } + if (current < potential) + { + current = potential; + } } void setRestrictiveEndTime( - GTime& current, ///< Current end time of bias - GTime& potential) ///< Potential end time of bias + GTime& current, ///< Current end time of bias + GTime& potential ///< Potential end time of bias +) { - if ( current == GTime::noTime() - ||(current > potential - && potential != GTime::noTime())) - { - current = potential; - } + if (current == GTime::noTime() || (current > potential && potential != GTime::noTime())) + { + current = potential; + } } /** Recurser of bias chaining, i.e. searching the path between base code and secondary code -*/ + */ bool biasRecurser( - Trace& trace, ///< Trace to output to - GTime& time, ///< Time of bias to look up - BiasEntry& output, ///< The bias entry retrieved - const E_ObsCode& obsCode1, ///< Base code of observation to find biases for - const E_ObsCode& obsCode2, ///< Secondary code of observation to find biases for - const ObsObsBiasMap& obsObsBiasMap, ///< Bias map for given measrement type & device, as obsObsBiasMap[code1][code2][time] - set& checkedObscodes) ///< A list of all checked observation codes + Trace& trace, ///< Trace to output to + GTime& time, ///< Time of bias to look up + BiasEntry& output, ///< The bias entry retrieved + const E_ObsCode& obsCode1, ///< Base code of observation to find biases for + const E_ObsCode& obsCode2, ///< Secondary code of observation to find biases for + const ObsObsBiasMap& obsObsBiasMap, ///< Bias map for given measrement type & device, as + ///< obsObsBiasMap[code1][code2][time] + set& checkedObscodes ///< A list of all checked observation codes +) { - checkedObscodes.insert(obsCode1); - - //try to find the base key in the big map - auto it1 = obsObsBiasMap.find(obsCode1); - if (it1 == obsObsBiasMap.end()) - { - //the obscode was not found, we have no hope - return false; - } - auto& [dummy, obsBiasMap] = *it1; - - //try to find the secondary key in the sub map - auto it2 = obsBiasMap.find(obsCode2); - if (it2 != obsBiasMap.end()) - { - //the obscode was found, use it - - auto& [dummy, timeBiasMap] = *it2; - - bool pass = calculateBias(time, output, timeBiasMap); - if (pass) - return true; - } - - //we didnt find what we were looking for with this set of obscodes, - //try to find a different path to the destination - //use the base key we found, and check all of it's siblings - for (auto& [secondaryKey, primarySecondaryTimeBiasMap] : obsBiasMap) - { - if (checkedObscodes.count(secondaryKey)) - { - //already checked - continue; - } - - BiasEntry pathB; - bool pass = biasRecurser(trace, time, pathB, secondaryKey, obsCode2, obsObsBiasMap, checkedObscodes); - if (pass == false) - continue; - - //a valid secondary path was found, now get the first half at the correct time - - BiasEntry pathA; - pass = calculateBias(time, pathA, primarySecondaryTimeBiasMap); - if (pass == false) - continue; - - output = pathA; - - output.bias += pathB.bias; - output.var += pathB.var; - output.slop += pathB.slop; - output.slpv += pathB.slpv; - output.source += "," + pathB.source; - output.cod2 = pathB.cod2; - -// printf("\nTraversing %s %s %s %f %f %f", -// pathA.cod1._to_string(), -// pathA.cod2._to_string(), -// pathB.cod2._to_string(), -// pathA.bias, -// pathB.bias, -// output.bias); - - setRestrictiveStartTime (output.tini, pathB.tini); - setRestrictiveEndTime (output.tfin, pathB.tfin); - - return true; - } - - return false; + checkedObscodes.insert(obsCode1); + + // try to find the base key in the big map + auto it1 = obsObsBiasMap.find(obsCode1); + if (it1 == obsObsBiasMap.end()) + { + // the obscode was not found, we have no hope + return false; + } + auto& [dummy, obsBiasMap] = *it1; + + // try to find the secondary key in the sub map + auto it2 = obsBiasMap.find(obsCode2); + if (it2 != obsBiasMap.end()) + { + // the obscode was found, use it + + auto& [dummy, timeBiasMap] = *it2; + + bool pass = calculateBias(time, output, timeBiasMap); + if (pass) + return true; + } + + // we didnt find what we were looking for with this set of obscodes, + // try to find a different path to the destination + // use the base key we found, and check all of it's siblings + for (auto& [secondaryKey, primarySecondaryTimeBiasMap] : obsBiasMap) + { + if (checkedObscodes.count(secondaryKey)) + { + // already checked + continue; + } + + BiasEntry pathB; + bool pass = biasRecurser( + trace, + time, + pathB, + secondaryKey, + obsCode2, + obsObsBiasMap, + checkedObscodes + ); + if (pass == false) + continue; + + // a valid secondary path was found, now get the first half at the correct time + + BiasEntry pathA; + pass = calculateBias(time, pathA, primarySecondaryTimeBiasMap); + if (pass == false) + continue; + + output = pathA; + + output.bias += pathB.bias; + output.var += pathB.var; + output.slop += pathB.slop; + output.slpv += pathB.slpv; + output.source += "," + pathB.source; + output.cod2 = pathB.cod2; + + // printf("\nTraversing %s %s %s %f %f %f", + // pathA.enum_to_string(cod1), + // pathA.enum_to_string(cod2), + // pathB.enum_to_string(cod2), + // pathA.bias, + // pathB.bias, + // output.bias); + + setRestrictiveStartTime(output.tini, pathB.tini); + setRestrictiveEndTime(output.tfin, pathB.tfin); + + return true; + } + + return false; } /** Search for hardware bias for given measurmenet type -*/ + */ bool getBiasEntry( - Trace& trace, ///< Trace to output to - GTime time, ///< Time of bias to look up - const string& id, ///< The id of the device to retrieve the bias of - E_MeasType measType, ///< The measurement type to retrieve the bias of - BiasEntry& output, ///< The bias entry retrieved - E_ObsCode obsCode1, ///< Base code of observation to find biases for - E_ObsCode obsCode2) ///< Secondary code of observation to find biases for + Trace& trace, ///< Trace to output to + GTime time, ///< Time of bias to look up + const string& id, ///< The id of the device to retrieve the bias of + E_MeasType measType, ///< The measurement type to retrieve the bias of + BiasEntry& output, ///< The bias entry retrieved + E_ObsCode obsCode1, ///< Base code of observation to find biases for + E_ObsCode obsCode2 ///< Secondary code of observation to find biases for +) { - //get the basic map of biases for this ID and measurmenet type - try - { - auto& biasMap = biasMaps[measType].at(id); + // get the basic map of biases for this ID and measurmenet type + try + { + auto& biasMap = biasMaps[measType].at(id); - set checkedObscodes; + set checkedObscodes; - bool pass = biasRecurser(trace, time, output, obsCode1, obsCode2, biasMap, checkedObscodes); + bool pass = biasRecurser(trace, time, output, obsCode1, obsCode2, biasMap, checkedObscodes); - return pass; - } - catch (...) - { - return false; - } + return pass; + } + catch (...) + { + return false; + } } /** Search for hardware biases in phase and code -*/ + */ bool getBias( - Trace& trace, ///< Trace to output to - GTime time, ///< Time of bias to look up - string id, ///< The id of the device to retrieve the bias of - SatSys Sat, ///< The satellite to retrieve the bias of - E_ObsCode obsCode1, ///< Base code of observation to find biases for - E_MeasType measType, ///< Type of bias to retreive (code/phase) - double& bias, ///< Hardware bias - double& var, ///< Hardware bias variance - KFState* kfState_ptr) ///< Optional filter to search for biases + Trace& trace, ///< Trace to output to + GTime time, ///< Time of bias to look up + string id, ///< The id of the device to retrieve the bias of + SatSys Sat, ///< The satellite to retrieve the bias of + E_ObsCode obsCode1, ///< Base code of observation to find biases for + E_MeasType measType, ///< Type of bias to retreive (code/phase) + double& bias, ///< Hardware bias + double& var, ///< Hardware bias variance + KFState* kfState_ptr ///< Optional filter to search for biases +) { - E_ObsCode obsCode2 = E_ObsCode::NONE; - - if ( acsConfig.ssrInOpts.one_freq_phase_bias - && measType == PHAS) - { - E_FType freq = code2Freq[Sat.sys][obsCode1]; - - obsCode1 = freq2CodeHax(Sat.sys, freq); - } - - if (kfState_ptr) - { - auto& kfState = *kfState_ptr; - - KFKey kfKey; - if (measType == CODE) kfKey.type = KF::CODE_BIAS; - if (measType == PHAS) kfKey.type = KF::PHASE_BIAS; - -// kfKey.str = id; -// kfKey.Sat = SatSys(Sat.sys); - kfKey.Sat = Sat; - kfKey.num = obsCode1; - - bool found = kfState.getKFValue(kfKey, bias, &var); - - if (found) - { - return true; - } - } - - if (Sat.sys == +E_Sys::GLO) id = id + ":" + Sat.id(); - else id = id + ":" + Sat.sysChar(); - - string type; - if (measType == CODE) type = "CODE"; - if (measType == PHAS) type = "PHAS"; - - tracepdeex(3, trace, "\nReading %s bias for %6s, %4s-%4s ...", type.c_str(), id.c_str(), obsCode1._to_string(), obsCode2._to_string()); - - BiasEntry foundBias; - bool pass = getBiasEntry(trace, time, id, measType, foundBias, obsCode1, obsCode2); - if (pass == false) - { - tracepdeex(3, trace, " Not found, var: %5.1f", var); - return false; - } - - bias = foundBias.bias; - var = foundBias.var; - tracepdeex(3, trace, " Found: %11.4f, var: %5.1f from %s", bias, var, foundBias.source.c_str()); - - return true; + E_ObsCode obsCode2 = E_ObsCode::NONE; + + if (acsConfig.ssrInOpts.one_freq_phase_bias && measType == PHAS) + { + E_FType freq = code2Freq[Sat.sys][obsCode1]; + + obsCode1 = freq2CodeHax(Sat.sys, freq); + } + + if (kfState_ptr) + { + auto& kfState = *kfState_ptr; + + KFKey kfKey; + if (measType == CODE) + kfKey.type = KF::CODE_BIAS; + if (measType == PHAS) + kfKey.type = KF::PHASE_BIAS; + + // kfKey.str = id; + // kfKey.Sat = SatSys(Sat.sys); + kfKey.Sat = Sat; + kfKey.num = static_cast(obsCode1); + + bool found = (kfState.getKFValue(kfKey, bias, &var) != E_Source::NONE); + + if (found) + { + return true; + } + } + + if (Sat.sys == E_Sys::GLO) + id = id + ":" + Sat.id(); + else + id = id + ":" + Sat.sysChar(); + + string type; + if (measType == CODE) + type = "CODE"; + if (measType == PHAS) + type = "PHAS"; + + tracepdeex( + 3, + trace, + "\nReading %s bias for %6s, %4s-%4s ...", + type.c_str(), + id.c_str(), + enum_to_string(obsCode1), + enum_to_string(obsCode2) + ); + + BiasEntry foundBias; + bool pass = getBiasEntry(trace, time, id, measType, foundBias, obsCode1, obsCode2); + if (pass == false || isnan(foundBias.bias)) + { + tracepdeex(3, trace, " Not found..."); + return false; + } + + bias = foundBias.bias; + var = (isnan(foundBias.var) ? 0 : foundBias.var); + + tracepdeex(3, trace, " Found: %11.4f, var: %5.1f from %s", bias, var, foundBias.source.c_str()); + + return true; } - - diff --git a/src/cpp/common/biases.hpp b/src/cpp/common/biases.hpp index 7350128be..184eafe6f 100644 --- a/src/cpp/common/biases.hpp +++ b/src/cpp/common/biases.hpp @@ -1,124 +1,109 @@ - -#pragma once - -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "algebra.hpp" -#include "satSys.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "enums.h" - -#include -#include -#include -#include - -using std::string; -using std::array; -using std::map; -using std::set; - -struct ReceiverMap; - -struct BiasEntry -{ - GTime tini; ///< start time - GTime tfin; ///< end time - GTime refTime; ///< reference time of bias value - E_MeasType measType = CODE; ///< Measurement type - E_ObsCode cod1 = E_ObsCode::NONE; ///< Measurement code 1 - E_ObsCode cod2 = E_ObsCode::NONE; ///< Measurement code 2 - double bias = 0; ///< hardware bias in meters - double slop = 0; ///< hardware bias slope in meters/second - double var = 0; ///< hardware bias variance in meters^2 - double slpv = 0; ///< hardware bias slope variance in (meters/second)^2 - string name; ///< receiver name for receiver bias - SatSys Sat; ///< satellite prn for satellite bias / satellite system for receiver bias - string source = "X"; - - long int posInOutFile =-1; ///< Position this entry is written in biasSINEX file -}; - - -struct TimeBiasMap : map> -{ - -}; - -struct ObsObsBiasMap : map> -{ - -}; - -struct BiasMap : array, NUM_MEAS_TYPES> -{ - -}; - -E_ObsCode str2code( - string& input, - E_MeasType& measType); - -void pushBiasEntry( - string id, - BiasEntry entry); - -void initialiseBias(); - -void addDefaultBias(); - -void cullOldBiases( - GTime time); - -bool decomposeDSBBias( - string id, - BiasEntry& DSB); - -bool decomposeTGDBias( - SatSys Sat, - double tgd); - -bool decomposeBGDBias( - SatSys Sat, - double bgd1, - double bgd2); - -bool readBiasSinex( - string& file); - -bool getBias( - Trace& trace, - GTime time, - string id, - SatSys Sat, - E_ObsCode obsCode1, - E_MeasType measType, - double& bias, - double& var, - KFState* kfState_ptr = nullptr); - -void writeBiasSinex( - Trace& trace, - GTime time, - KFState& kfState, - KFState& ionState, - string biasfile, - ReceiverMap& receiverMap); - -bool queryBiasOutput( - Trace& trace, - GTime time, - KFState& kfState, - KFState& ionState, - SatSys Sat, - string Rec, - E_ObsCode obsCode, - double& bias_out, - double& variance, - E_MeasType type); - -void loadStateBiases( - KFState& kfState); - -extern BiasMap biasMaps; +#pragma once +// #include "acsConfig.hpp" + +#include +#include +#include +#include +#include "algebra.hpp" +#include "common.hpp" +#include "enums.h" +#include "gTime.hpp" +#include "navigation.hpp" +#include "satSys.hpp" + +using std::array; +using std::map; +using std::set; +using std::string; + +struct ReceiverMap; + +struct BiasEntry +{ + GTime tini; ///< start time + GTime tfin; ///< end time + GTime refTime; ///< reference time of bias value + E_MeasType measType = CODE; ///< Measurement type + E_ObsCode cod1 = E_ObsCode::NONE; ///< Measurement code 1 + E_ObsCode cod2 = E_ObsCode::NONE; ///< Measurement code 2 + double bias = 0; ///< hardware bias in meters + double slop = 0; ///< hardware bias slope in meters/second + double var = 0; ///< hardware bias variance in meters^2 + double slpv = 0; ///< hardware bias slope variance in (meters/second)^2 + string name; ///< receiver name for receiver bias + SatSys Sat; ///< satellite prn for satellite bias / satellite system for receiver bias + string source = "X"; + + long int posInOutFile = -1; ///< Position this entry is written in biasSINEX file +}; + +struct TimeBiasMap : map> +{ +}; + +struct ObsObsBiasMap : map> +{ +}; + +struct BiasMap : array, NUM_MEAS_TYPES> +{ +}; + +E_ObsCode str2code(string& input, E_MeasType& measType); + +void updateRefTime(BiasEntry& entry); + +void pushBiasEntry(string id, BiasEntry entry); + +void initialiseBias(); + +void addDefaultBias(); + +void cullOldBiases(GTime time); + +bool decomposeDSBBias(string id, BiasEntry& DSB); + +bool decomposeTGDBias(SatSys Sat, double tgd); + +bool decomposeBGDBias(SatSys Sat, double bgd1, double bgd2); + +bool readBiasSinex(string& file); + +bool getBias( + Trace& trace, + GTime time, + string id, + SatSys Sat, + E_ObsCode obsCode1, + E_MeasType measType, + double& bias, + double& var, + KFState* kfState_ptr = nullptr +); + +void writeBiasSinex( + Trace& trace, + string biasfile, + GTime time, + KFState& kfState, + KFState& ionState, + ReceiverMap& receiverMap +); + +bool queryBiasOutput( + Trace& trace, + GTime time, + KFState& kfState, + KFState& ionState, + SatSys Sat, + string Rec, + E_ObsCode obsCode, + double& bias_out, + double& variance, + E_MeasType type +); + +void loadStateBiases(KFState& kfState); + +extern BiasMap biasMaps; diff --git a/src/cpp/common/cache.hpp b/src/cpp/common/cache.hpp index 0bee362e8..8d8077800 100644 --- a/src/cpp/common/cache.hpp +++ b/src/cpp/common/cache.hpp @@ -1,38 +1,36 @@ - #pragma once #include -template +template struct Cache : TYPE { - std::function lambda; - - TYPE output; - - bool initialised = false; - - Cache(){} - - Cache& uninit() - { - initialised = false; - - return *this; - } - - const TYPE& useCache( - std::function lambda) - { - if (initialised) - return output; - - this->lambda = lambda; - - output = lambda(); - - initialised = true; - - return output; - } + std::function lambda; + + TYPE output; + + bool initialised = false; + + Cache() {} + + Cache& uninit() + { + initialised = false; + + return *this; + } + + const TYPE& useCache(std::function lambda) + { + if (initialised) + return output; + + this->lambda = lambda; + + output = lambda(); + + initialised = true; + + return output; + } }; diff --git a/src/cpp/common/common.cpp b/src/cpp/common/common.cpp index 8c5699248..dedd30871 100644 --- a/src/cpp/common/common.cpp +++ b/src/cpp/common/common.cpp @@ -1,125 +1,140 @@ - // #pragma GCC optimize ("O0") +#include "common/common.hpp" #include - #include +#include "common/constants.hpp" -#include "constants.hpp" -#include "common.hpp" - -const double ura_eph[] = ///< URA values (ref [3] 20.3.3.3.1.1) -{ - 2.4, 3.4, 4.85, 6.85, 9.65, 13.65, 24, 48, 96, 192, 384, 768, 1536, 3072, 6144 -}; +const double ura_eph[] = ///< URA values (ref [3] 20.3.3.3.1.1) + {2.4, 3.4, 4.85, 6.85, 9.65, 13.65, 24, 48, 96, 192, 384, 768, 1536, 3072, 6144}; /** URA index to URA value (m) -* GLOBAL POSITIONING SYSTEM -* STANDARD POSITIONING SERVICE -* SIGNAL SPECIFICATION -* 2nd Ed, June 2,1995 -* see section - 2.5.3 User Range Accuracy -*/ + * GLOBAL POSITIONING SYSTEM + * STANDARD POSITIONING SERVICE + * SIGNAL SPECIFICATION + * 2nd Ed, June 2,1995 + * see section - 2.5.3 User Range Accuracy + */ double svaToUra(int sva) { - double ura = 0; - - if (sva < 0) - ura = -1; - else if (sva <= 6) - { - ura = 10 * pow(2, 1 + ((double)sva / 2.0)); - ura = round(ura) / 10.0; - } - else if (sva < 15) - ura = pow(2, (double)sva - 2.0); - else - ura = -1; - - return ura; + double ura = 0; + + if (sva < 0) + ura = -1; + else if (sva <= 6) + { + ura = 10 * pow(2, 1 + ((double)sva / 2.0)); + ura = round(ura) / 10.0; + } + else if (sva < 15) + ura = pow(2, (double)sva - 2.0); + else + ura = -1; + + return ura; } /** URA value (m) to URA index -*/ + */ int uraToSva(double ura) { - int sva = 0; - - if (ura < 0) - sva = 15; - else - { - for (sva = 0; sva < 15; sva++) - if (ura_eph[sva] >= ura) - break; - } - - return sva; + int sva = 0; + + if (ura < 0) + sva = 15; + else + { + for (sva = 0; sva < 15; sva++) + if (ura_eph[sva] >= ura) + break; + } + + return sva; } /** Galileo SISA index to SISA value (m) -* EUROPEAN GNSS (GALILEO) OPEN SERVICE SIGNAL-IN-SPACE INTERFACE CONTROL DOCUMENT -* Issue 2.0, January 2021 -* See Section, 5.1.12. Signal In Space Accuracy (SISA) -*/ + * EUROPEAN GNSS (GALILEO) OPEN SERVICE SIGNAL-IN-SPACE INTERFACE CONTROL DOCUMENT + * Issue 2.0, January 2021 + * See Section, 5.1.12. Signal In Space Accuracy (SISA) + */ double svaToSisa(int sva) { - if (sva < 0) return -1; - else if (sva <= 49) return (0.0 + (sva - 0) * 0.01); - else if (sva <= 74) return (0.5 + (sva - 50) * 0.02); - else if (sva <= 99) return (1.0 + (sva - 75) * 0.04); - else if (sva <= 125) return (2.0 + (sva - 100) * 0.16); - else return -1; + if (sva < 0) + return -1; + else if (sva <= 49) + return (0.0 + (sva - 0) * 0.01); + else if (sva <= 74) + return (0.5 + (sva - 50) * 0.02); + else if (sva <= 99) + return (1.0 + (sva - 75) * 0.04); + else if (sva <= 125) + return (2.0 + (sva - 100) * 0.16); + else + return -1; } /** Galileo SISA value (m) to SISA index -*/ + */ int sisaToSva(double sisa) { - if (sisa < 0) return 255; - else if (sisa <= 0.49) return (int)((((sisa - 0.0) / 0.01) + 0) + 0.5); - else if (sisa <= 0.98) return (int)((((sisa - 0.5) / 0.02) + 50) + 0.5); - else if (sisa <= 1.96) return (int)((((sisa - 1.0) / 0.04) + 75) + 0.5); - else if (sisa <= 6.00) return (int)((((sisa - 2.0) / 0.16) + 100) + 0.5); - else return 255; + if (sisa < 0) + return 255; + else if (sisa <= 0.49) + return (int)((((sisa - 0.0) / 0.01) + 0) + 0.5); + else if (sisa <= 0.98) + return (int)((((sisa - 0.5) / 0.02) + 50) + 0.5); + else if (sisa <= 1.96) + return (int)((((sisa - 1.0) / 0.04) + 75) + 0.5); + else if (sisa <= 6.00) + return (int)((((sisa - 2.0) / 0.16) + 100) + 0.5); + else + return 255; } /** crc-24q parity -* compute crc-24q parity for sbas, rtcm3 -* see reference [2] A.4.3.3 Parity -*/ + * compute crc-24q parity for sbas, rtcm3 + * see reference [2] A.4.3.3 Parity + */ unsigned int crc24q( - const unsigned char* buff, ///< data - int len) ///< data length (bytes) + const unsigned char* buff, ///< data + int len ///< data length (bytes) +) { -// trace(4,"%s: len=%d\n",__FUNCTION__, len); + // trace(4,"%s: len=%d\n",__FUNCTION__, len); - unsigned int crc = 0; + unsigned int crc = 0; - for (int i = 0; i < len; i++) - crc = ((crc<<8) & 0xFFFFFF) ^ tbl_CRC24Q[(crc >> 16) ^ buff[i]]; + for (int i = 0; i < len; i++) + crc = ((crc << 8) & 0xFFFFFF) ^ tbl_CRC24Q[(crc >> 16) ^ buff[i]]; - return crc; + return crc; } /** Wrap angle between (-pi, pi] */ -void wrapPlusMinusPi( - double& angle) ///< Angle to wrap +void wrapPlusMinusPi(double& angle) ///< Angle to wrap { - while (angle <= -PI) angle += 2 * PI; - while (angle > PI) angle -= 2 * PI; + while (angle <= -PI) + angle += 2 * PI; + while (angle > PI) + angle -= 2 * PI; } /** Wrap angle between [0, 2pi) */ -void wrap2Pi( - double& angle) ///< Angle to wrap +void wrap2Pi(double& angle) ///< Angle to wrap { - while (angle < 0) angle += 2 * PI; - while (angle >= 2 * PI) angle -= 2 * PI; + while (angle < 0) + angle += 2 * PI; + while (angle >= 2 * PI) + angle -= 2 * PI; } - -double VectorPos::latDeg() const { return x() * R2D; } -double VectorPos::lonDeg() const { return y() * R2D; } +double VectorPos::latDeg() const +{ + return x() * R2D; +} +double VectorPos::lonDeg() const +{ + return y() * R2D; +} diff --git a/src/cpp/common/common.hpp b/src/cpp/common/common.hpp index 391f71ca9..7c1375b8f 100644 --- a/src/cpp/common/common.hpp +++ b/src/cpp/common/common.hpp @@ -1,24 +1,53 @@ - #pragma once +// Windows defines OUT and IN as empty macros in some headers +#ifdef _WIN32 +#ifdef OUT +#undef OUT +#endif +#ifdef IN +#undef IN +#endif +#endif + +#include #include - -/* constants/macros */ -#define SQR(x) ((x)*(x)) -#define POW4(x) ((x)*(x)*(x)*(x)) -#define SQRT(x) ((x)<=0.0?0.0:sqrt(x)) -#define ROUND(x) (int)floor((x)+0.5) -#define SWAP(x,y) do {double tmp_; tmp_=x; x=y; y=tmp_;} while (0) -#define SGN(x) ((x)<=0.0?-1.0:1.0) - -#include "eigenIncluder.hpp" -#include "gTime.hpp" -#include "erp.hpp" -#include "enums.h" +#include +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/erp.hpp" +#include "common/gTime.hpp" using std::multimap; -using std::vector; using std::pair; +using std::vector; + +/* constants/macros */ +template +constexpr T SQR(T x) +{ + return x * x; +} +template +constexpr T POW4(T x) +{ + return x * x * x * x; +} +template +constexpr T SQRT(T x) +{ + return (x) <= 0.0 ? 0.0 : sqrt(x); +} +template +constexpr T ROUND(T x) +{ + return (int)floor((x) + 0.5); +} +template +constexpr T SGN(T x) +{ + return (x) <= 0.0 ? -1.0 : 1.0; +} struct SatSys; struct SatPos; @@ -26,252 +55,196 @@ struct AzEl; struct Average { - double mean = 0; - double var = 0; + double mean = 0; + double var = 0; }; struct descope { - //used to temporarily prevent accessing things that shouldnt be by throwing compiler errors + // used to temporarily prevent accessing things that shouldnt be by throwing compiler errors }; -void lowPassFilter( - Average& avg, - double meas, - double procNoise, - double measVar = 1); +void lowPassFilter(Average& avg, double meas, double procNoise, double measVar = 1); -void wrapPlusMinusPi( - double& angle); +void wrapPlusMinusPi(double& angle); -void wrap2Pi( - double& angle); +void wrap2Pi(double& angle); double geodist(Vector3d& rs, Vector3d& rr, Vector3d& e); -double sagnac( - Vector3d& rSource, - Vector3d& rDest, - Vector3d vel = Vector3d::Zero()); +double sagnac(Vector3d& rSource, Vector3d& rDest, Vector3d vel = Vector3d::Zero()); +void satazel(const VectorPos& pos, const VectorEcef& e, AzEl& azel); -void satazel( - const VectorPos& pos, - const VectorEcef& e, - AzEl& azel); - -unsigned int crc24q (const unsigned char *buff, int len); +unsigned int crc24q(const unsigned char* buff, int len); struct Dops { - double gdop = 0; - double pdop = 0; - double hdop = 0; - double vdop = 0; + double gdop = 0; + double pdop = 0; + double hdop = 0; + double vdop = 0; }; -Dops dopCalc( - const vector& azels); +Dops dopCalc(const vector& azels); -bool satFreqs( - E_Sys sys, - E_FType& frq1, - E_FType& frq2, - E_FType& frq3); +bool satFreqs(E_Sys sys, E_FType& frq1, E_FType& frq2, E_FType& frq3); -int sisaToSva(double sisa); +int sisaToSva(double sisa); double svaToSisa(int sva); -int uraToSva(double ura); +int uraToSva(double ura); double svaToUra(int sva); -void updateLamMap( - const GTime& time, - SatPos& obs); - - +void updateLamMap(const GTime& time, SatPos& obs); /** An iterator that trys to cast elements to the desired type before using them */ -template< - typename OUTTYPE, - typename INTYPE, - typename VOIDTYPE> +template struct IteratorType { - typename INTYPE::iterator ptr_ptr; - typename INTYPE::iterator endPtr_ptr; - - IteratorType( - typename INTYPE::iterator startPtr_ptr, - typename INTYPE::iterator endPtr_ptr) - : ptr_ptr (startPtr_ptr), - endPtr_ptr (endPtr_ptr) - { - ptr_ptr--; - incrementUntilGood(); - } - - bool operator !=(IteratorType rhs) - { - return ptr_ptr != rhs.ptr_ptr; - } - - OUTTYPE& operator*() - { - return static_cast(**ptr_ptr); - } - - void incrementUntilGood() - { - while (1) - { - ++ptr_ptr; - if (ptr_ptr == endPtr_ptr) - return; - - try - { - (void) dynamic_cast(**ptr_ptr); - //no throw, sucess, stop - return; - } - catch(...){} - } - } - - void operator++() - { - incrementUntilGood(); - } + typename INTYPE::iterator ptr_ptr; + typename INTYPE::iterator endPtr_ptr; + + IteratorType(typename INTYPE::iterator startPtr_ptr, typename INTYPE::iterator endPtr_ptr) + : ptr_ptr(startPtr_ptr), endPtr_ptr(endPtr_ptr) + { + ptr_ptr--; + incrementUntilGood(); + } + + bool operator!=(IteratorType rhs) { return ptr_ptr != rhs.ptr_ptr; } + + OUTTYPE& operator*() { return static_cast(**ptr_ptr); } + + void incrementUntilGood() + { + while (1) + { + ++ptr_ptr; + if (ptr_ptr == endPtr_ptr) + return; + + try + { + (void)dynamic_cast(**ptr_ptr); + // no throw, sucess, stop + return; + } + catch (...) + { + } + } + } + + void operator++() { incrementUntilGood(); } }; /** An iterator that trys to cast elements to the desired type before using them */ -template< - typename OUTTYPE, - typename INTYPE, - typename KEYTYPE> +template struct MapIteratorType { - typename INTYPE::iterator ptr_ptr; - typename INTYPE::iterator endPtr_ptr; - - MapIteratorType( - typename INTYPE::iterator startPtr_ptr, - typename INTYPE::iterator endPtr_ptr) - : ptr_ptr (startPtr_ptr), - endPtr_ptr (endPtr_ptr) - { - if (ptr_ptr == endPtr_ptr) - return; - - try - { - (void) dynamic_cast(*ptr_ptr->second); - //no throw, sucess, stop - return; - } - catch(...){} - - incrementUntilGood(); - } - - bool operator !=(MapIteratorType rhs) - { - return ptr_ptr != rhs.ptr_ptr; - } - - const pair operator*() - { - auto& thing = *ptr_ptr->second; - return {ptr_ptr->first, dynamic_cast(thing)}; - } - - void incrementUntilGood() - { - while (1) - { - ++ptr_ptr; - if (ptr_ptr == endPtr_ptr) - return; - - try - { - (void) dynamic_cast(*ptr_ptr->second); - //no throw, sucess, stop - return; - } - catch(...){} - } - } - - void operator++() - { - incrementUntilGood(); - } + typename INTYPE::iterator ptr_ptr; + typename INTYPE::iterator endPtr_ptr; + + MapIteratorType(typename INTYPE::iterator startPtr_ptr, typename INTYPE::iterator endPtr_ptr) + : ptr_ptr(startPtr_ptr), endPtr_ptr(endPtr_ptr) + { + if (ptr_ptr == endPtr_ptr) + return; + + try + { + (void)dynamic_cast(*ptr_ptr->second); + // no throw, sucess, stop + return; + } + catch (...) + { + } + + incrementUntilGood(); + } + + bool operator!=(MapIteratorType rhs) { return ptr_ptr != rhs.ptr_ptr; } + + const pair operator*() + { + auto& thing = *ptr_ptr->second; + return {ptr_ptr->first, dynamic_cast(thing)}; + } + + void incrementUntilGood() + { + while (1) + { + ++ptr_ptr; + if (ptr_ptr == endPtr_ptr) + return; + + try + { + (void)dynamic_cast(*ptr_ptr->second); + // no throw, sucess, stop + return; + } + catch (...) + { + } + } + } + + void operator++() { incrementUntilGood(); } }; /** An object just for templating the other functions without over-verbosity */ template < - template typename ITERATOR, - typename TYPE, - typename KEYTYPE, - typename INTYPE> + template typename ITERATOR, + typename TYPE, + typename KEYTYPE, + typename INTYPE> struct Typer { - INTYPE& baseContainer; - - Typer( - INTYPE& baseContainer) - : baseContainer (baseContainer) - { - - } - - ITERATOR begin() { return ITERATOR(baseContainer.begin(), baseContainer.end()); } - const ITERATOR begin() const { return ITERATOR(baseContainer.begin(), baseContainer.end()); } - ITERATOR end() { return ITERATOR(baseContainer.end(), baseContainer.end()); } - const ITERATOR end() const { return ITERATOR(baseContainer.end(), baseContainer.end()); } + INTYPE& baseContainer; + + Typer(INTYPE& baseContainer) : baseContainer(baseContainer) {} + + ITERATOR begin() + { + return ITERATOR(baseContainer.begin(), baseContainer.end()); + } + const ITERATOR begin() const + { + return ITERATOR(baseContainer.begin(), baseContainer.end()); + } + ITERATOR end() + { + return ITERATOR(baseContainer.end(), baseContainer.end()); + } + const ITERATOR end() const + { + return ITERATOR(baseContainer.end(), baseContainer.end()); + } }; - /** Use only a subset of a vector that can be cast to a desired type * \private */ -template< - typename OUT, - typename ENTRY - > -Typer< - IteratorType, - OUT, - void, - vector> -only( - vector& in) +template +Typer > only(std::vector& in) { - return Typer >(in); + return Typer >(in); } - /** Use only a subset of a map that can be cast to a desired type */ -template< - typename OUT, - typename KEYTYPE, - typename VALUE> -Typer< - MapIteratorType, - OUT, - KEYTYPE, - multimap> -only( - multimap& in) +template +Typer > +only(std::multimap& in) { - return Typer >(in); + return Typer >(in); } - -extern int epoch; -extern GTime tsync; +extern int epoch; +extern GTime tsync; diff --git a/src/cpp/common/compare.cpp b/src/cpp/common/compare.cpp index 1400df431..418737574 100644 --- a/src/cpp/common/compare.cpp +++ b/src/cpp/common/compare.cpp @@ -1,248 +1,337 @@ - // #pragma GCC optimize ("O0") #include #include +#include "common/acsConfig.hpp" +#include "common/attitude.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/ephemeris.hpp" +#include "common/navigation.hpp" +#include "common/rinex.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "pea/inputsOutputs.hpp" +#include "pea/minimumConstraints.hpp" using std::string; using std::vector; -#include "minimumConstraints.hpp" -#include "eigenIncluder.hpp" -#include "inputsOutputs.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "ephemeris.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "attitude.hpp" -#include "trace.hpp" -#include "rinex.hpp" - struct TraceDummy { - string jsonTraceFilename; - string traceFilename; - string id; + string jsonTraceFilename; + string traceFilename; + string id; }; - vector navVec; -void compareClocks( - vector files) -{ - -} +void compareClocks(vector files) {} - -map> pephMapMap0; -map> pephMapMap1; - -void compareOrbits( - vector files) +void compareOrbits(vector files) { - TraceDummy traceDummy; - - if (acsConfig.output_network_trace) - { - boost::posix_time::ptime logptime = currentLogptime(); - - createDirectories(logptime); - - createNewTraceFile("Network", logptime, acsConfig.network_trace_filename, traceDummy.traceFilename, true, acsConfig.output_config); - } - - auto trace = getTraceFile(traceDummy); - - for (auto& file : files) - { - Navigation nav; - readSp3ToNav(file, nav, 0); - - navVec.push_back(nav); - } - - for (int i = 1; i < navVec.size(); i++) - { - std::cout << "\n" << "Comparing files:" - << "\n" << files[0] - << "\n" << files[i] - << "\n"; - - //invert maps - pephMapMap0.clear(); - pephMapMap1.clear(); - - for (auto& [id, pephMap] : navVec[0].pephMap) - for (auto& [time, peph] : pephMap) - { - pephMapMap0[time][id] = peph; - } - for (auto& [id, pephMap] : navVec[i].pephMap) - for (auto& [time, peph] : pephMap) - { - pephMapMap1[time][id] = peph; - } - - - MinconStatistics minconStatistics1; - map> transformStatisticsMap; - - for (auto& [time, pephMap0] : pephMapMap0) - { - auto it = pephMapMap1.find(time); - if (it == pephMapMap1.end()) - { - std::cout << "\n" << time << " not found in " << files[i]; - continue; - } - - auto& [dummy, pephMap1] = *it; - - KFState kfState; - - ERPValues erpv = getErp(nav.erp, time); - - FrameSwapper frameSwapper(time, erpv); - - for (auto& [id, peph0] : pephMap0) - { - auto it2 = pephMap1.find(id); - if (it2 == pephMap1.end()) - { - // std::cout << "\n" << id << " not found in " << files[i] << " for " << time; - continue; - } - - auto& [dummy, peph1] = *it2; - - if (peph1.vel.isZero()) - { - GTime interpStartTime = time; - Vector3d interpStartPos = peph1.pos; - GTime interpStopTime = time; - Vector3d interpStopPos = peph1.pos; - - it++; if (it != pephMapMap1.end()) { interpStopTime = it->first; interpStopPos = it->second[id].pos; } - it--; if (it != pephMapMap1.begin()) { it--; interpStartTime = it->first; interpStartPos = it->second[id].pos; it++; } - - peph1.vel = (interpStopPos - interpStartPos) - / (interpStopTime - interpStartTime).to_double(); - } - - //set noise value - auto& satOpts = acsConfig.getSatOpts(peph1.Sat); - - //set apriori value - auto& satNav = nav.satNavMap[peph1.Sat]; //this is the global nav used by mincon - satNav.aprioriPos = frameSwapper(peph0.pos); - - //set test value - KFKey kfKey; - kfKey.type = KF::ORBIT; - kfKey.Sat = peph1.Sat; - VectorEci velEci; - VectorEci posEci = frameSwapper(peph1.pos, &peph1.vel, &velEci); - - for (int i = 0; i < 3; i++) - { - kfKey.num = i; kfState.addKFState(kfKey, {.x = posEci[i], .P = 1}); - kfKey.num = i + 3; kfState.addKFState(kfKey, {.x = velEci[i], .P = 1}); - } - } - - kfState.stateTransition (trace, time); -// kfState.outputStates (trace); - - if (acsConfig.process_minimum_constraints) - { - KFState kfStateTransform; - - MinconStatistics minconStatistics0; - - mincon(trace, kfState, &minconStatistics0, &minconStatistics1, false, &kfStateTransform); - - outputMinconStatistics(trace, minconStatistics0, "/" + time.to_string()); - - for (auto& [kfKey, index] : kfStateTransform.kfIndexMap) - { - if (kfKey.type == KF::ONE) - { - continue; - } - - double val = kfStateTransform.x(index); - - transformStatisticsMap[kfKey].push_back(val); - } - } - } - - outputMinconStatistics(trace, minconStatistics1, "/TOTAL"); - outputMinconStatistics(std::cout, minconStatistics1, "/TOTAL"); - - Block block(trace, "HELMERT_STATISTICS"); - for (auto& [key, vec] : transformStatisticsMap) - { - double n = vec.size(); - double avg = 0; - double std = 0; - for (auto& entry : vec) { avg += entry; } avg = avg / n; - for (auto& entry : vec) { std += SQR(entry - avg); } std = sqrt(std) / (n - 1); - - tracepdeex(0, trace, "^ %-10s %d averaged %+12.4e %-5s with std %12.4e over %d epochs\n", KF::_from_integral(key.type)._to_string(), key.num, avg, key.comment.c_str(), std, n); - } - } + TraceDummy traceDummy; + + if (acsConfig.output_network_trace) + { + boost::posix_time::ptime logptime = currentLogptime(); + + createDirectories(logptime); + + createNewTraceFile( + "Network", + "", + logptime, + acsConfig.network_trace_filename, + traceDummy.traceFilename, + true, + acsConfig.output_config + ); + } + + auto trace = getTraceFile(traceDummy); + + for (auto& file : files) + { + Navigation nav; + readSp3ToNav(file, nav, 0); + + navVec.push_back(nav); + } + + for (int i = 1; i < navVec.size(); i++) + { + std::cout << "\n" + << "Comparing files:" << "\n" + << files[0] << "\n" + << files[i] << "\n"; + + // invert maps + map> pephMapMap0; + map> pephMapMap1; + + for (auto& [id, pephMap] : navVec[0].pephMap) + for (auto& [time, peph] : pephMap) + { + auto satOpts = acsConfig.getSatOpts(peph.Sat); + if (satOpts.exclude) + { + continue; + } + + pephMapMap0[time][id] = peph; + } + for (auto& [id, pephMap] : navVec[i].pephMap) + for (auto& [time, peph] : pephMap) + { + auto satOpts = acsConfig.getSatOpts(peph.Sat); + if (satOpts.exclude) + { + continue; + } + + pephMapMap1[time][id] = peph; + } + + MinconStatistics minconStatistics1; + map> transformStatisticsMap; + + KFState kfStateTransform; + + int iterations; + if (acsConfig.minconOpts.once_per_epoch) + iterations = 1; + else + iterations = 2; + + for (int iteration = 1; iteration <= iterations; iteration++) + for (auto& [time, pephMap0] : pephMapMap0) + { + auto it = pephMapMap1.find(time); + if (it == pephMapMap1.end()) + { + std::cout << "\n" << time << " not found in " << files[i]; + continue; + } + + auto& [dummy, pephMap1] = *it; + + if (acsConfig.minconOpts.once_per_epoch) + { + // reset for each epoch + kfStateTransform = KFState(); + } + + KFState kfState; + + ERPValues erpv = getErp(nav.erp, time); + + FrameSwapper frameSwapper(time, erpv); + + for (auto& [id, peph0] : pephMap0) + { + auto it2 = pephMap1.find(id); + if (it2 == pephMap1.end()) + { + // std::cout << "\n" << id << " not found in " << files[i] << " for " << + // time; + continue; + } + + auto& [dummy, peph1] = *it2; + + if (peph1.vel.isZero()) + { + GTime interpStartTime = time; + Vector3d interpStartPos = peph1.pos; + GTime interpStopTime = time; + Vector3d interpStopPos = peph1.pos; + + it++; + if (it != pephMapMap1.end()) + { + interpStopTime = it->first; + interpStopPos = it->second[id].pos; + } + it--; + if (it != pephMapMap1.begin()) + { + it--; + interpStartTime = it->first; + interpStartPos = it->second[id].pos; + it++; + } + + peph1.vel = (interpStopPos - interpStartPos) / + (interpStopTime - interpStartTime).to_double(); + } + + // set noise value + auto& satOpts = acsConfig.getSatOpts(peph1.Sat); + + // set apriori value + auto& satNav = + nav.satNavMap[peph1.Sat]; // this is the global nav used by mincon + satNav.aprioriPos = frameSwapper(peph0.pos); + + // set test value + KFKey kfKey; + kfKey.type = KF::ORBIT; + kfKey.Sat = peph1.Sat; + VectorEci velEci; + VectorEci posEci = frameSwapper(peph1.pos, &peph1.vel, &velEci); + + for (int i = 0; i < 3; i++) + { + kfKey.num = i; + kfState.addKFState(kfKey, {.x = posEci[i], .P = 1}); + kfKey.num = i + 3; + kfState.addKFState(kfKey, {.x = velEci[i], .P = 1}); + } + } + + kfState.stateTransition(trace, time); + // kfState.outputStates (trace); + + if (acsConfig.process_minimum_constraints) + { + bool estimate = true; + MinconStatistics* totalStatistics_ptr = &minconStatistics1; + + if (iterations == 2 && iteration == 2) + { + // dont reestimate the transform the second time around + estimate = false; + } + + if (iterations == 2 && iteration == 1) + { + // dont add statistics the first time around + totalStatistics_ptr = nullptr; + } + + MinconStatistics minconStatistics0; + + mincon( + trace, + kfState, + &minconStatistics0, + totalStatistics_ptr, + false, + &kfStateTransform, + estimate, + !estimate + ); + + if (iterations == 1 || iteration == 2) + { + // only chance, or second time around, output statistics + + outputMinconStatistics(trace, minconStatistics0, "/" + time.to_string()); + + for (auto& [kfKey, index] : kfStateTransform.kfIndexMap) + { + if (kfKey.type == KF::ONE) + { + continue; + } + + double val = kfStateTransform.x(index); + + transformStatisticsMap[kfKey].push_back(val); + } + } + } + } + + outputMinconStatistics(trace, minconStatistics1, "/TOTAL"); + outputMinconStatistics(std::cout, minconStatistics1, "/TOTAL"); + + Block block(trace, "HELMERT_STATISTICS"); + for (auto& [key, vec] : transformStatisticsMap) + { + double n = vec.size(); + double avg = 0; + double std = 0; + for (auto& entry : vec) + { + avg += entry; + } + avg = avg / n; + for (auto& entry : vec) + { + std += SQR(entry - avg); + } + std = sqrt(std) / (n - 1); + + tracepdeex( + 0, + trace, + "^ %-10s %-3s averaged %+12.4e %-5s with std %12.4e over %d epochs\n", + enum_to_string(key.type).c_str(), + key.code().c_str(), + avg, + key.comment.c_str(), + std, + n + ); + } + } } -void compareAttitudes( - vector files) +void compareAttitudes(vector files) { - for (auto& file : files) - { - Navigation nav; - readOrbex(file, nav); - - navVec.push_back(nav); - } - - for (int i = 1; i < navVec.size(); i++) - { - auto& nav0 = navVec[0]; - auto& nav1 = navVec[1]; - - std::cout << "\n" << "Comparing files:" - << "\n" << files[0] - << "\n" << files[i] - << "\n"; - - for (auto& [id, attMap0] : nav0.attMapMap) - { - auto it = nav1.attMapMap.find(id); - if (it == nav1.attMapMap.end()) - { - std::cout << "\n" << id << " not found in " << files[i]; - continue; - } - - auto& [dummy, attMap] = *it; - - for (auto& [time, att0] : attMap0) - { - auto it = attMap.find(time); - if (it == attMap.end()) - { - std::cout << "\n" << time << " not found in " << files[i] << " for " << id; - continue; - } - - auto& [dummy, att] = *it; - - double angle = att0.q.angularDistance(att.q) * R2D; - - tracepdeex(0, std::cout, "\n%s - %s - %6.1fdeg", time.to_string().c_str(), id.c_str(), angle); - } - } - } + for (auto& file : files) + { + Navigation nav; + readOrbex(file, nav); + + navVec.push_back(nav); + } + + for (int i = 1; i < navVec.size(); i++) + { + auto& nav0 = navVec[0]; + auto& nav1 = navVec[1]; + + std::cout << "\n" + << "Comparing files:" << "\n" + << files[0] << "\n" + << files[i] << "\n"; + + for (auto& [id, attMap0] : nav0.attMapMap) + { + auto it = nav1.attMapMap.find(id); + if (it == nav1.attMapMap.end()) + { + std::cout << "\n" << id << " not found in " << files[i]; + continue; + } + + auto& [dummy, attMap] = *it; + + for (auto& [time, att0] : attMap0) + { + auto it = attMap.find(time); + if (it == attMap.end()) + { + std::cout << "\n" << time << " not found in " << files[i] << " for " << id; + continue; + } + + auto& [dummy, att] = *it; + + double angle = att0.q.angularDistance(att.q) * R2D; + + tracepdeex( + 0, + std::cout, + "\n%s - %s - %6.1fdeg", + time.to_string().c_str(), + id.c_str(), + angle + ); + } + } + } } diff --git a/src/cpp/common/compare.hpp b/src/cpp/common/compare.hpp index 6ca9d4c5d..017dc44d4 100644 --- a/src/cpp/common/compare.hpp +++ b/src/cpp/common/compare.hpp @@ -1,12 +1,11 @@ - #pragma once -#include #include +#include -using std::vector; using std::string; +using std::vector; -void compareClocks (vector files); -void compareOrbits (vector files); -void compareAttitudes (vector files); +void compareClocks(vector files); +void compareOrbits(vector files); +void compareAttitudes(vector files); diff --git a/src/cpp/common/constants.cpp b/src/cpp/common/constants.cpp index 1527dd375..3ab12cf34 100644 --- a/src/cpp/common/constants.cpp +++ b/src/cpp/common/constants.cpp @@ -1,449 +1,354 @@ - +#include "common/constants.hpp" +#include +#include #include +#include +#include #include #include -#include -#include +#include "common/enums.h" +#include "common/satSys.hpp" using std::map; +using std::set; + +map genericWavelength = { + {F1, CLIGHT / FREQ1}, + {F2, CLIGHT / FREQ2}, + {F5, CLIGHT / FREQ5}, + {F6, CLIGHT / FREQ6}, + {F7, CLIGHT / FREQ7}, + {F8, CLIGHT / FREQ8}, + {B1, CLIGHT / FREQ1_CMP}, + {B3, CLIGHT / FREQ3_CMP}, + {G1, CLIGHT / FREQ1_GLO}, + {G2, CLIGHT / FREQ2_GLO}, + {G3, CLIGHT / FREQ3_GLO}, + {G4, CLIGHT / FREQ4_GLO}, + {G6, CLIGHT / FREQ6_GLO} +}; -#include "constants.hpp" -#include "enums.h" +map> code2Freq = { + {E_Sys::GPS, + {{E_ObsCode::NONE, NONE}, {E_ObsCode::L1C, F1}, {E_ObsCode::L1S, F1}, {E_ObsCode::L1L, F1}, + {E_ObsCode::L1X, F1}, {E_ObsCode::L1P, F1}, {E_ObsCode::L1W, F1}, {E_ObsCode::L1Y, F1}, + {E_ObsCode::L1M, F1}, {E_ObsCode::L1N, F1}, -#include + {E_ObsCode::L2C, F2}, {E_ObsCode::L2D, F2}, {E_ObsCode::L2S, F2}, {E_ObsCode::L2L, F2}, + {E_ObsCode::L2X, F2}, {E_ObsCode::L2P, F2}, {E_ObsCode::L2W, F2}, {E_ObsCode::L2Y, F2}, + {E_ObsCode::L2M, F2}, {E_ObsCode::L2N, F2}, -map genericWavelength = -{ - {F1, CLIGHT / FREQ1}, - {F2, CLIGHT / FREQ2}, - {F5, CLIGHT / FREQ5}, - {F6, CLIGHT / FREQ6}, - {F7, CLIGHT / FREQ7}, - {F8, CLIGHT / FREQ8}, - {B1, CLIGHT / FREQ1_CMP}, - {B3, CLIGHT / FREQ3_CMP}, - {G1, CLIGHT / FREQ1_GLO}, - {G2, CLIGHT / FREQ2_GLO}, - {G3, CLIGHT / FREQ3_GLO}, - {G4, CLIGHT / FREQ4_GLO}, - {G6, CLIGHT / FREQ6_GLO} -}; + {E_ObsCode::L5I, F5}, {E_ObsCode::L5Q, F5}, {E_ObsCode::L5X, F5}}}, -map> code2Freq = -{ - { E_Sys::GPS, - { - {E_ObsCode::NONE, FTYPE_NONE}, - {E_ObsCode::L1C, F1 }, - {E_ObsCode::L1S, F1 }, - {E_ObsCode::L1L, F1 }, - {E_ObsCode::L1X, F1 }, - {E_ObsCode::L1P, F1 }, - {E_ObsCode::L1W, F1 }, - {E_ObsCode::L1Y, F1 }, - {E_ObsCode::L1M, F1 }, - {E_ObsCode::L1N, F1 }, - - {E_ObsCode::L2C, F2 }, - {E_ObsCode::L2D, F2 }, - {E_ObsCode::L2S, F2 }, - {E_ObsCode::L2L, F2 }, - {E_ObsCode::L2X, F2 }, - {E_ObsCode::L2P, F2 }, - {E_ObsCode::L2W, F2 }, - {E_ObsCode::L2Y, F2 }, - {E_ObsCode::L2M, F2 }, - {E_ObsCode::L2N, F2 }, - - {E_ObsCode::L5I, F5 }, - {E_ObsCode::L5Q, F5 }, - {E_ObsCode::L5X, F5 } - } - }, - - { E_Sys::GLO, - { - {E_ObsCode::NONE, FTYPE_NONE}, - {E_ObsCode::L1C, G1 }, - {E_ObsCode::L1P, G1 }, - - {E_ObsCode::L2C, G2 }, - {E_ObsCode::L2P, G2 }, - - {E_ObsCode::L3I, G3 }, - {E_ObsCode::L3Q, G3 }, - {E_ObsCode::L3X, G3 }, - - {E_ObsCode::L4A, G4 }, - {E_ObsCode::L4B, G4 }, - {E_ObsCode::L4X, G4 }, - - {E_ObsCode::L6A, G6 }, - {E_ObsCode::L6B, G6 }, - {E_ObsCode::L6X, G6 } - } - }, - - { E_Sys::GAL, - { - {E_ObsCode::NONE, FTYPE_NONE}, - {E_ObsCode::L1A, F1 }, - {E_ObsCode::L1B, F1 }, - {E_ObsCode::L1C, F1 }, - {E_ObsCode::L1X, F1 }, - {E_ObsCode::L1Z, F1 }, - - {E_ObsCode::L5I, F5 }, - {E_ObsCode::L5Q, F5 }, - {E_ObsCode::L5X, F5 }, - - {E_ObsCode::L6A, F6 }, - {E_ObsCode::L6B, F6 }, - {E_ObsCode::L6C, F6 }, - {E_ObsCode::L6X, F6 }, - {E_ObsCode::L6Z, F6 }, - - {E_ObsCode::L7I, F7 }, - {E_ObsCode::L7Q, F7 }, - {E_ObsCode::L7X, F7 }, - - {E_ObsCode::L8I, F8 }, - {E_ObsCode::L8Q, F8 }, - {E_ObsCode::L8X, F8 } - } - }, - - { E_Sys::BDS, - { - {E_ObsCode::NONE, FTYPE_NONE}, - {E_ObsCode::L1D, F1 }, - {E_ObsCode::L1P, F1 }, - {E_ObsCode::L1X, F1 }, - {E_ObsCode::L1S, F1 }, - {E_ObsCode::L1L, F1 }, - {E_ObsCode::L1Z, F1 }, - {E_ObsCode::L1A, F1 }, - {E_ObsCode::L1N, F1 }, - - {E_ObsCode::L2I, B1 }, - {E_ObsCode::L2Q, B1 }, - {E_ObsCode::L2X, B1 }, - - {E_ObsCode::L5D, F5 }, - {E_ObsCode::L5P, F5 }, - {E_ObsCode::L5X, F5 }, - - {E_ObsCode::L6I, B3 }, - {E_ObsCode::L6Q, B3 }, - {E_ObsCode::L6X, B3 }, - {E_ObsCode::L6D, B3 }, - {E_ObsCode::L6P, B3 }, - {E_ObsCode::L6Z, B3 }, - {E_ObsCode::L6A, B3 }, - - {E_ObsCode::L7I, F7 }, - {E_ObsCode::L7Q, F7 }, - {E_ObsCode::L7X, F7 }, - {E_ObsCode::L7D, F7 }, - {E_ObsCode::L7P, F7 }, - {E_ObsCode::L7Z, F7 }, - - {E_ObsCode::L8D, F8 }, - {E_ObsCode::L8P, F8 }, - {E_ObsCode::L8Z, F8 } - } - }, - - { E_Sys::QZS, - { - {E_ObsCode::NONE, FTYPE_NONE}, - {E_ObsCode::L1C, F1 }, - {E_ObsCode::L1E, F1 }, - {E_ObsCode::L1S, F1 }, - {E_ObsCode::L1L, F1 }, - {E_ObsCode::L1X, F1 }, - {E_ObsCode::L1Z, F1 }, - {E_ObsCode::L1B, F1 }, - - {E_ObsCode::L2S, F2 }, - {E_ObsCode::L2L, F2 }, - {E_ObsCode::L2X, F2 }, - - {E_ObsCode::L5I, F5 }, - {E_ObsCode::L5Q, F5 }, - {E_ObsCode::L5X, F5 }, - {E_ObsCode::L5D, F5 }, - {E_ObsCode::L5P, F5 }, - {E_ObsCode::L5Z, F5 }, - - {E_ObsCode::L6S, F6 }, - {E_ObsCode::L6L, F6 }, - {E_ObsCode::L6X, F6 }, - {E_ObsCode::L6E, F6 }, - {E_ObsCode::L6Z, F6 } - } - }, - - { E_Sys::IRN, - { - /* NavIC F1 in the works... */ - - {E_ObsCode::NONE, FTYPE_NONE}, - {E_ObsCode::L5A, F5 }, - {E_ObsCode::L5B, F5 }, - {E_ObsCode::L5C, F5 }, - {E_ObsCode::L5X, F5 }, - - {E_ObsCode::L9A, I9 }, - {E_ObsCode::L9B, I9 }, - {E_ObsCode::L9C, I9 }, - {E_ObsCode::L9X, I9 } - } - }, - - { E_Sys::SBS, - { - {E_ObsCode::NONE, FTYPE_NONE}, - {E_ObsCode::L1C, F1 }, - - {E_ObsCode::L5I, F5 }, - {E_ObsCode::L5Q, F5 }, - {E_ObsCode::L5X, F5 } - } - }, - - { E_Sys::LEO, - { - {E_ObsCode::NONE, FTYPE_NONE}, - {E_ObsCode::L1C, F1 }, - {E_ObsCode::L1S, F1 }, - {E_ObsCode::L1L, F1 }, - {E_ObsCode::L1X, F1 }, - {E_ObsCode::L1P, F1 }, - {E_ObsCode::L1W, F1 }, - {E_ObsCode::L1Y, F1 }, - {E_ObsCode::L1M, F1 }, - {E_ObsCode::L1N, F1 }, - - {E_ObsCode::L2C, F2 }, - {E_ObsCode::L2D, F2 }, - {E_ObsCode::L2S, F2 }, - {E_ObsCode::L2L, F2 }, - {E_ObsCode::L2X, F2 }, - {E_ObsCode::L2P, F2 }, - {E_ObsCode::L2W, F2 }, - {E_ObsCode::L2Y, F2 }, - {E_ObsCode::L2M, F2 }, - {E_ObsCode::L2N, F2 }, - - {E_ObsCode::L5I, F5 }, - {E_ObsCode::L5Q, F5 }, - {E_ObsCode::L5X, F5 } - } - } -}; + {E_Sys::GLO, + {{E_ObsCode::NONE, NONE}, + {E_ObsCode::L1C, G1}, + {E_ObsCode::L1P, G1}, + {E_ObsCode::L2C, G2}, + {E_ObsCode::L2P, G2}, + {E_ObsCode::L3I, G3}, + {E_ObsCode::L3Q, G3}, + {E_ObsCode::L3X, G3}, + {E_ObsCode::L4A, G4}, + {E_ObsCode::L4B, G4}, + {E_ObsCode::L4X, G4}, + {E_ObsCode::L6A, G6}, + {E_ObsCode::L6B, G6}, + {E_ObsCode::L6X, G6}}}, -const unsigned int tbl_CRC24Q[]= -{ - 0x000000,0x864CFB,0x8AD50D,0x0C99F6,0x93E6E1,0x15AA1A,0x1933EC,0x9F7F17, - 0xA18139,0x27CDC2,0x2B5434,0xAD18CF,0x3267D8,0xB42B23,0xB8B2D5,0x3EFE2E, - 0xC54E89,0x430272,0x4F9B84,0xC9D77F,0x56A868,0xD0E493,0xDC7D65,0x5A319E, - 0x64CFB0,0xE2834B,0xEE1ABD,0x685646,0xF72951,0x7165AA,0x7DFC5C,0xFBB0A7, - 0x0CD1E9,0x8A9D12,0x8604E4,0x00481F,0x9F3708,0x197BF3,0x15E205,0x93AEFE, - 0xAD50D0,0x2B1C2B,0x2785DD,0xA1C926,0x3EB631,0xB8FACA,0xB4633C,0x322FC7, - 0xC99F60,0x4FD39B,0x434A6D,0xC50696,0x5A7981,0xDC357A,0xD0AC8C,0x56E077, - 0x681E59,0xEE52A2,0xE2CB54,0x6487AF,0xFBF8B8,0x7DB443,0x712DB5,0xF7614E, - 0x19A3D2,0x9FEF29,0x9376DF,0x153A24,0x8A4533,0x0C09C8,0x00903E,0x86DCC5, - 0xB822EB,0x3E6E10,0x32F7E6,0xB4BB1D,0x2BC40A,0xAD88F1,0xA11107,0x275DFC, - 0xDCED5B,0x5AA1A0,0x563856,0xD074AD,0x4F0BBA,0xC94741,0xC5DEB7,0x43924C, - 0x7D6C62,0xFB2099,0xF7B96F,0x71F594,0xEE8A83,0x68C678,0x645F8E,0xE21375, - 0x15723B,0x933EC0,0x9FA736,0x19EBCD,0x8694DA,0x00D821,0x0C41D7,0x8A0D2C, - 0xB4F302,0x32BFF9,0x3E260F,0xB86AF4,0x2715E3,0xA15918,0xADC0EE,0x2B8C15, - 0xD03CB2,0x567049,0x5AE9BF,0xDCA544,0x43DA53,0xC596A8,0xC90F5E,0x4F43A5, - 0x71BD8B,0xF7F170,0xFB6886,0x7D247D,0xE25B6A,0x641791,0x688E67,0xEEC29C, - 0x3347A4,0xB50B5F,0xB992A9,0x3FDE52,0xA0A145,0x26EDBE,0x2A7448,0xAC38B3, - 0x92C69D,0x148A66,0x181390,0x9E5F6B,0x01207C,0x876C87,0x8BF571,0x0DB98A, - 0xF6092D,0x7045D6,0x7CDC20,0xFA90DB,0x65EFCC,0xE3A337,0xEF3AC1,0x69763A, - 0x578814,0xD1C4EF,0xDD5D19,0x5B11E2,0xC46EF5,0x42220E,0x4EBBF8,0xC8F703, - 0x3F964D,0xB9DAB6,0xB54340,0x330FBB,0xAC70AC,0x2A3C57,0x26A5A1,0xA0E95A, - 0x9E1774,0x185B8F,0x14C279,0x928E82,0x0DF195,0x8BBD6E,0x872498,0x016863, - 0xFAD8C4,0x7C943F,0x700DC9,0xF64132,0x693E25,0xEF72DE,0xE3EB28,0x65A7D3, - 0x5B59FD,0xDD1506,0xD18CF0,0x57C00B,0xC8BF1C,0x4EF3E7,0x426A11,0xC426EA, - 0x2AE476,0xACA88D,0xA0317B,0x267D80,0xB90297,0x3F4E6C,0x33D79A,0xB59B61, - 0x8B654F,0x0D29B4,0x01B042,0x87FCB9,0x1883AE,0x9ECF55,0x9256A3,0x141A58, - 0xEFAAFF,0x69E604,0x657FF2,0xE33309,0x7C4C1E,0xFA00E5,0xF69913,0x70D5E8, - 0x4E2BC6,0xC8673D,0xC4FECB,0x42B230,0xDDCD27,0x5B81DC,0x57182A,0xD154D1, - 0x26359F,0xA07964,0xACE092,0x2AAC69,0xB5D37E,0x339F85,0x3F0673,0xB94A88, - 0x87B4A6,0x01F85D,0x0D61AB,0x8B2D50,0x145247,0x921EBC,0x9E874A,0x18CBB1, - 0xE37B16,0x6537ED,0x69AE1B,0xEFE2E0,0x709DF7,0xF6D10C,0xFA48FA,0x7C0401, - 0x42FA2F,0xC4B6D4,0xC82F22,0x4E63D9,0xD11CCE,0x575035,0x5BC9C3,0xDD8538 + {E_Sys::GAL, {{E_ObsCode::NONE, NONE}, {E_ObsCode::L1A, F1}, {E_ObsCode::L1B, F1}, + {E_ObsCode::L1C, F1}, {E_ObsCode::L1X, F1}, {E_ObsCode::L1Z, F1}, + + {E_ObsCode::L5I, F5}, {E_ObsCode::L5Q, F5}, {E_ObsCode::L5X, F5}, + + {E_ObsCode::L6A, F6}, {E_ObsCode::L6B, F6}, {E_ObsCode::L6C, F6}, + {E_ObsCode::L6X, F6}, {E_ObsCode::L6Z, F6}, + + {E_ObsCode::L7I, F7}, {E_ObsCode::L7Q, F7}, {E_ObsCode::L7X, F7}, + + {E_ObsCode::L8I, F8}, {E_ObsCode::L8Q, F8}, {E_ObsCode::L8X, F8}}}, + + {E_Sys::BDS, + {{E_ObsCode::NONE, NONE}, {E_ObsCode::L1D, F1}, {E_ObsCode::L1P, F1}, {E_ObsCode::L1X, F1}, + {E_ObsCode::L1S, F1}, {E_ObsCode::L1L, F1}, {E_ObsCode::L1Z, F1}, {E_ObsCode::L1A, F1}, + {E_ObsCode::L1N, F1}, + + {E_ObsCode::L2I, B1}, {E_ObsCode::L2Q, B1}, {E_ObsCode::L2X, B1}, + + {E_ObsCode::L5D, F5}, {E_ObsCode::L5P, F5}, {E_ObsCode::L5X, F5}, + + {E_ObsCode::L6I, B3}, {E_ObsCode::L6Q, B3}, {E_ObsCode::L6X, B3}, {E_ObsCode::L6D, B3}, + {E_ObsCode::L6P, B3}, {E_ObsCode::L6Z, B3}, {E_ObsCode::L6A, B3}, + + {E_ObsCode::L7I, F7}, {E_ObsCode::L7Q, F7}, {E_ObsCode::L7X, F7}, {E_ObsCode::L7D, F7}, + {E_ObsCode::L7P, F7}, {E_ObsCode::L7Z, F7}, + + {E_ObsCode::L8D, F8}, {E_ObsCode::L8P, F8}, {E_ObsCode::L8X, F8}}}, + + {E_Sys::QZS, + {{E_ObsCode::NONE, NONE}, {E_ObsCode::L1C, F1}, {E_ObsCode::L1E, F1}, {E_ObsCode::L1S, F1}, + {E_ObsCode::L1L, F1}, {E_ObsCode::L1X, F1}, {E_ObsCode::L1Z, F1}, {E_ObsCode::L1B, F1}, + + {E_ObsCode::L2S, F2}, {E_ObsCode::L2L, F2}, {E_ObsCode::L2X, F2}, + + {E_ObsCode::L5I, F5}, {E_ObsCode::L5Q, F5}, {E_ObsCode::L5X, F5}, {E_ObsCode::L5D, F5}, + {E_ObsCode::L5P, F5}, {E_ObsCode::L5Z, F5}, + + {E_ObsCode::L6S, F6}, {E_ObsCode::L6L, F6}, {E_ObsCode::L6X, F6}, {E_ObsCode::L6E, F6}, + {E_ObsCode::L6Z, F6}}}, + + {E_Sys::IRN, + {/* NavIC F1 in the works... */ + + {E_ObsCode::NONE, NONE}, + {E_ObsCode::L5A, F5}, + {E_ObsCode::L5B, F5}, + {E_ObsCode::L5C, F5}, + {E_ObsCode::L5X, F5}, + + {E_ObsCode::L9A, I9}, + {E_ObsCode::L9B, I9}, + {E_ObsCode::L9C, I9}, + {E_ObsCode::L9X, I9} + }}, + + {E_Sys::SBS, + {{E_ObsCode::NONE, NONE}, + {E_ObsCode::L1C, F1}, + + {E_ObsCode::L5I, F5}, + {E_ObsCode::L5Q, F5}, + {E_ObsCode::L5X, F5}}}, + + {E_Sys::LEO, + {{E_ObsCode::NONE, NONE}, {E_ObsCode::L1C, F1}, {E_ObsCode::L1S, F1}, {E_ObsCode::L1L, F1}, + {E_ObsCode::L1X, F1}, {E_ObsCode::L1P, F1}, {E_ObsCode::L1W, F1}, {E_ObsCode::L1Y, F1}, + {E_ObsCode::L1M, F1}, {E_ObsCode::L1N, F1}, + + {E_ObsCode::L2C, F2}, {E_ObsCode::L2D, F2}, {E_ObsCode::L2S, F2}, {E_ObsCode::L2L, F2}, + {E_ObsCode::L2X, F2}, {E_ObsCode::L2P, F2}, {E_ObsCode::L2W, F2}, {E_ObsCode::L2Y, F2}, + {E_ObsCode::L2M, F2}, {E_ObsCode::L2N, F2}, + + {E_ObsCode::L5I, F5}, {E_ObsCode::L5Q, F5}, {E_ObsCode::L5X, F5}}} +}; + +// Map satellite block types to their broadcast frequency bands +// Based on RINEX 4.02 specification and satellite constellation signal plans +map> blockTypeFrequencies = { + // GPS Block Types + {E_Block::GPS_I, {F1, F2}}, // L1, L2 + {E_Block::GPS_II, {F1, F2}}, // L1, L2 + {E_Block::GPS_IIA, {F1, F2}}, // L1, L2 + {E_Block::GPS_IIR_A, {F1, F2}}, // L1, L2 + {E_Block::GPS_IIR_B, {F1, F2}}, // L1, L2 + {E_Block::GPS_IIR_M, {F1, F2}}, // L1, L2 + {E_Block::GPS_IIF, {F1, F2, F5}}, // L1, L2, L5 + {E_Block::GPS_IIIA, {F1, F2, F5}}, // L1, L2, L5 + + // GLONASS Block Types + {E_Block::GLO_M, {G1, G2}}, // G1, G2 + {E_Block::GLO, {G1, G2}}, // G1, G2 + {E_Block::GLO_K1A, {G1, G2}}, // G1, G2 + {E_Block::GLO_K1B, {G1, G2, G3}}, // G1, G2, G3 + {E_Block::GLO_K2, {G1, G2, G3, G4, G6}}, // G1, G2, G3, G4, G6 + {E_Block::GLO_MP, {G1, G2, G3}}, // G1, G2, G3 + + // Galileo Block Types + {E_Block::GAL_0A, {F1, F5, F7, F8, F6}}, // E1, E5a, E5b, E5, E6 + {E_Block::GAL_0B, {F1, F5, F7, F8, F6}}, // E1, E5a, E5b, E5, E6 + {E_Block::GAL_1, {F1, F5, F7, F8, F6}}, // E1, E5a, E5b, E5, E6 + {E_Block::GAL_2, {F1, F5, F7, F8, F6}}, // E1, E5a, E5b, E5, E6 + + // BeiDou Block Types + // BDS-2: B1I, B2I, B3I + // Note: RINEX uses L2 designation for B1I (B1=1561.098 MHz), L7 for B2I, L6 for B3I + {E_Block::BDS_2M, {B1, F7, B3}}, + {E_Block::BDS_2G, {B1, F7, B3}}, + {E_Block::BDS_2I, {B1, F7, B3}}, + + // BDS-3: B1I, B1C, B2a, B2I, B2(a+b), B3I + // Note: RINEX uses L2 for B1I, L1 for B1C, L5 for B2a, L7 for B2I, L8 for B2(a+b), L6 for B3I + {E_Block::BDS_3SI_SECM, {B1, F1, F5, F7, F8, B3}}, + {E_Block::BDS_3SM_CAST, {B1, F1, F5, F7, F8, B3}}, + {E_Block::BDS_3SI_CAST, {B1, F1, F5, F7, F8, B3}}, + {E_Block::BDS_3SM_SECM, {B1, F1, F5, F7, F8, B3}}, + {E_Block::BDS_3M_CAST, {B1, F1, F5, F7, F8, B3}}, + {E_Block::BDS_3M_SECM_A, {B1, F1, F5, F7, F8, B3}}, + {E_Block::BDS_3G, {B1, F1, F5, F7, F8, B3}}, + {E_Block::BDS_3I, {B1, F1, F5, F7, F8, B3}}, + {E_Block::BDS_3M_SECM_B, {B1, F1, F5, F7, F8, B3}}, + + // QZSS Block Types + {E_Block::QZS_1, {F1, F2, F5}}, // L1, L2, L5 + {E_Block::QZS_2I, {F1, F2, F5, F6}}, // L1, L2, L5, L6 + {E_Block::QZS_2G, {F1, F2, F5, F6}}, // L1, L2, L5, L6 + {E_Block::QZS_2A, {F1, F2, F5, F6}}, // L1, L2, L5, L6 + + // NavIC/IRNSS Block Types + {E_Block::IRS_1I, {F5, I9}}, // L5, S9 + {E_Block::IRS_1G, {F5, I9}}, // L5, S9 + {E_Block::IRS_2G, {F5, I9}}, // L5, S9 + + // SBAS Block Types + {E_Block::SBS, {F1, F5}}, // L1, L5 + + // LEO Block Types + {E_Block::LEO, {F1, F2, F5}}, // L1, L2, L5 }; -const boost::bimap mCodes_gps = boost::assign::list_of::relation> - (E_ObsCode::L1C,0) - (E_ObsCode::L1P,1) - (E_ObsCode::L1W,2) - (E_ObsCode::L1S,3) - (E_ObsCode::L1L,4) - (E_ObsCode::L2C,5) - (E_ObsCode::L2D,6) - (E_ObsCode::L2S,7) - (E_ObsCode::L2L,8) - (E_ObsCode::L2X,9) - (E_ObsCode::L2P,10) - (E_ObsCode::L2W,11) - (E_ObsCode::L2Y,12) - (E_ObsCode::L2M,13) - (E_ObsCode::L5I,14) - (E_ObsCode::L5Q,15) - (E_ObsCode::L5X,16); - -const boost::bimap mCodes_glo = boost::assign::list_of::relation> - (E_ObsCode::L1C,0) - (E_ObsCode::L1P,1) - (E_ObsCode::L2C,2) - (E_ObsCode::L2P,3); - -const boost::bimap mCodes_gal = boost::assign::list_of::relation> - (E_ObsCode::L1A,0) - (E_ObsCode::L1B,1) - (E_ObsCode::L1C,2) - (E_ObsCode::L1X,3) - (E_ObsCode::L1Z,4) - (E_ObsCode::L5I,5) - (E_ObsCode::L5Q,6) - (E_ObsCode::L5X,7) - (E_ObsCode::L7I,8) - (E_ObsCode::L7Q,9) - (E_ObsCode::L7X,10) - (E_ObsCode::L8I,11) - (E_ObsCode::L8Q,12) - (E_ObsCode::L8X,13) - (E_ObsCode::L6A,14) - (E_ObsCode::L6B,15) - (E_ObsCode::L6C,16) - (E_ObsCode::L6X,17) - (E_ObsCode::L6Z,18); - - -const boost::bimap mCodes_qzs = boost::assign::list_of::relation> - (E_ObsCode::L1C,0) - (E_ObsCode::L1S,1) - (E_ObsCode::L1L,2) - (E_ObsCode::L2S,3) - (E_ObsCode::L2L,4) - (E_ObsCode::L2X,5) - (E_ObsCode::L5I,6) - (E_ObsCode::L5Q,7) - (E_ObsCode::L5X,8) - (E_ObsCode::L6S,9) - (E_ObsCode::L6L,10) - (E_ObsCode::L6X,11) - (E_ObsCode::L1X,12); - -const boost::bimap mCodes_bds = boost::assign::list_of::relation> - (E_ObsCode::L2I,0) - (E_ObsCode::L2Q,1) - (E_ObsCode::L2X,2) - (E_ObsCode::L6I,3) - (E_ObsCode::L6Q,4) - (E_ObsCode::L6X,5) - (E_ObsCode::L7I,6) - (E_ObsCode::L7Q,7) - (E_ObsCode::L7X,8) - (E_ObsCode::L1D,9) - (E_ObsCode::L1P,10) - (E_ObsCode::NONE,11) - (E_ObsCode::L5D,12) - (E_ObsCode::L5P,13) - (E_ObsCode::NONE,14) - (E_ObsCode::L1A,15); - -const boost::bimap mCodes_sbs = boost::assign::list_of::relation> - (E_ObsCode::L1C,0) - (E_ObsCode::L5I,1) - (E_ObsCode::L5Q,2); - - - -map> codeHax = +// Map of block types to their unsupported signal codes +// Background: GPS satellites have evolved their signal capabilities over time: +// - Older blocks (I, II, IIA, IIR-A, IIR-B): Only legacy L2 signals (L2P, L2W, L2Y) +// - IIR-M and newer (IIF, IIIA): Added modernised L2C signals (L2C, L2S, L2L, L2X) +// +// This map lists signals that are NOT supported by each block type. +// If a block type is not in this map, all signals are supported (default behavior). +static const map> unsupportedSignalsByBlockType = { + // Older GPS blocks do not support modernised L2C signals + {E_Block::GPS_I, {E_ObsCode::L2C, E_ObsCode::L2S, E_ObsCode::L2L, E_ObsCode::L2X}}, + {E_Block::GPS_II, {E_ObsCode::L2C, E_ObsCode::L2S, E_ObsCode::L2L, E_ObsCode::L2X}}, + {E_Block::GPS_IIA, {E_ObsCode::L2C, E_ObsCode::L2S, E_ObsCode::L2L, E_ObsCode::L2X}}, + {E_Block::GPS_IIR_A, {E_ObsCode::L2C, E_ObsCode::L2S, E_ObsCode::L2L, E_ObsCode::L2X}}, + {E_Block::GPS_IIR_B, {E_ObsCode::L2C, E_ObsCode::L2S, E_ObsCode::L2L, E_ObsCode::L2X}}, + // IIR-M and newer support L2C signals, so they're not in this list +}; + +// Filter signal codes based on block type capabilities +// Returns true if the signal code is supported by the given block type +bool isSignalSupportedByBlockType(E_ObsCode code, E_Block blockType) { - { E_Sys::GPS, - { - {F1, E_ObsCode::L1C }, - {F2, E_ObsCode::L2W }, - {F5, E_ObsCode::L5I } - } - }, - - { E_Sys::GLO, - { - {G1, E_ObsCode::NONE }, - {G2, E_ObsCode::NONE }, - {G3, E_ObsCode::L3I }, - {G4, E_ObsCode::L4B }, - {G6, E_ObsCode::L6B } - } - }, - - { E_Sys::GAL, - { - {F1, E_ObsCode::L1C }, - {F5, E_ObsCode::L5I }, - {F6, E_ObsCode::L6C }, - {F7, E_ObsCode::L7I }, - {F8, E_ObsCode::L8I } - } - }, - - { E_Sys::BDS, - { - {F1, E_ObsCode::L1P }, - {B1, E_ObsCode::L2I }, - {F5, E_ObsCode::L5P }, - {B3, E_ObsCode::L6I }, - {F7, E_ObsCode::L7P }, - {F8, E_ObsCode::L8P }, - } - }, - - { E_Sys::QZS, - { - {F1, E_ObsCode::L1C }, - {F2, E_ObsCode::L2L }, - {F5, E_ObsCode::L5I }, - {F6, E_ObsCode::L6S } - } - }, - - { E_Sys::IRN, - { - {F1, E_ObsCode::L1A }, - {I9, E_ObsCode::L9A } - } - }, - - { E_Sys::SBS, - { - {F1, E_ObsCode::L1C }, - {F5, E_ObsCode::L5I } - } - } + auto it = unsupportedSignalsByBlockType.find(blockType); + if (it != unsupportedSignalsByBlockType.end()) + { + // Block type has restrictions - check if this signal is unsupported + return it->second.find(code) == it->second.end(); + } + + // Block type not in map - all signals supported (default behavior) + return true; +} + +const unsigned int tbl_CRC24Q[] = { + 0x000000, 0x864CFB, 0x8AD50D, 0x0C99F6, 0x93E6E1, 0x15AA1A, 0x1933EC, 0x9F7F17, 0xA18139, + 0x27CDC2, 0x2B5434, 0xAD18CF, 0x3267D8, 0xB42B23, 0xB8B2D5, 0x3EFE2E, 0xC54E89, 0x430272, + 0x4F9B84, 0xC9D77F, 0x56A868, 0xD0E493, 0xDC7D65, 0x5A319E, 0x64CFB0, 0xE2834B, 0xEE1ABD, + 0x685646, 0xF72951, 0x7165AA, 0x7DFC5C, 0xFBB0A7, 0x0CD1E9, 0x8A9D12, 0x8604E4, 0x00481F, + 0x9F3708, 0x197BF3, 0x15E205, 0x93AEFE, 0xAD50D0, 0x2B1C2B, 0x2785DD, 0xA1C926, 0x3EB631, + 0xB8FACA, 0xB4633C, 0x322FC7, 0xC99F60, 0x4FD39B, 0x434A6D, 0xC50696, 0x5A7981, 0xDC357A, + 0xD0AC8C, 0x56E077, 0x681E59, 0xEE52A2, 0xE2CB54, 0x6487AF, 0xFBF8B8, 0x7DB443, 0x712DB5, + 0xF7614E, 0x19A3D2, 0x9FEF29, 0x9376DF, 0x153A24, 0x8A4533, 0x0C09C8, 0x00903E, 0x86DCC5, + 0xB822EB, 0x3E6E10, 0x32F7E6, 0xB4BB1D, 0x2BC40A, 0xAD88F1, 0xA11107, 0x275DFC, 0xDCED5B, + 0x5AA1A0, 0x563856, 0xD074AD, 0x4F0BBA, 0xC94741, 0xC5DEB7, 0x43924C, 0x7D6C62, 0xFB2099, + 0xF7B96F, 0x71F594, 0xEE8A83, 0x68C678, 0x645F8E, 0xE21375, 0x15723B, 0x933EC0, 0x9FA736, + 0x19EBCD, 0x8694DA, 0x00D821, 0x0C41D7, 0x8A0D2C, 0xB4F302, 0x32BFF9, 0x3E260F, 0xB86AF4, + 0x2715E3, 0xA15918, 0xADC0EE, 0x2B8C15, 0xD03CB2, 0x567049, 0x5AE9BF, 0xDCA544, 0x43DA53, + 0xC596A8, 0xC90F5E, 0x4F43A5, 0x71BD8B, 0xF7F170, 0xFB6886, 0x7D247D, 0xE25B6A, 0x641791, + 0x688E67, 0xEEC29C, 0x3347A4, 0xB50B5F, 0xB992A9, 0x3FDE52, 0xA0A145, 0x26EDBE, 0x2A7448, + 0xAC38B3, 0x92C69D, 0x148A66, 0x181390, 0x9E5F6B, 0x01207C, 0x876C87, 0x8BF571, 0x0DB98A, + 0xF6092D, 0x7045D6, 0x7CDC20, 0xFA90DB, 0x65EFCC, 0xE3A337, 0xEF3AC1, 0x69763A, 0x578814, + 0xD1C4EF, 0xDD5D19, 0x5B11E2, 0xC46EF5, 0x42220E, 0x4EBBF8, 0xC8F703, 0x3F964D, 0xB9DAB6, + 0xB54340, 0x330FBB, 0xAC70AC, 0x2A3C57, 0x26A5A1, 0xA0E95A, 0x9E1774, 0x185B8F, 0x14C279, + 0x928E82, 0x0DF195, 0x8BBD6E, 0x872498, 0x016863, 0xFAD8C4, 0x7C943F, 0x700DC9, 0xF64132, + 0x693E25, 0xEF72DE, 0xE3EB28, 0x65A7D3, 0x5B59FD, 0xDD1506, 0xD18CF0, 0x57C00B, 0xC8BF1C, + 0x4EF3E7, 0x426A11, 0xC426EA, 0x2AE476, 0xACA88D, 0xA0317B, 0x267D80, 0xB90297, 0x3F4E6C, + 0x33D79A, 0xB59B61, 0x8B654F, 0x0D29B4, 0x01B042, 0x87FCB9, 0x1883AE, 0x9ECF55, 0x9256A3, + 0x141A58, 0xEFAAFF, 0x69E604, 0x657FF2, 0xE33309, 0x7C4C1E, 0xFA00E5, 0xF69913, 0x70D5E8, + 0x4E2BC6, 0xC8673D, 0xC4FECB, 0x42B230, 0xDDCD27, 0x5B81DC, 0x57182A, 0xD154D1, 0x26359F, + 0xA07964, 0xACE092, 0x2AAC69, 0xB5D37E, 0x339F85, 0x3F0673, 0xB94A88, 0x87B4A6, 0x01F85D, + 0x0D61AB, 0x8B2D50, 0x145247, 0x921EBC, 0x9E874A, 0x18CBB1, 0xE37B16, 0x6537ED, 0x69AE1B, + 0xEFE2E0, 0x709DF7, 0xF6D10C, 0xFA48FA, 0x7C0401, 0x42FA2F, 0xC4B6D4, 0xC82F22, 0x4E63D9, + 0xD11CCE, 0x575035, 0x5BC9C3, 0xDD8538 +}; + +const boost::bimap mCodes_gps = + boost::assign::list_of< + boost::bimap:: + relation>(E_ObsCode::L1C, 0)(E_ObsCode::L1P, 1)(E_ObsCode::L1W, 2)(E_ObsCode::L1S, 3)(E_ObsCode::L1L, 4)(E_ObsCode::L2C, 5)(E_ObsCode::L2D, 6)(E_ObsCode::L2S, 7)(E_ObsCode::L2L, 8)(E_ObsCode::L2X, 9)(E_ObsCode::L2P, 10)(E_ObsCode::L2W, 11)(E_ObsCode::L2Y, 12)(E_ObsCode::L2M, 13)(E_ObsCode::L5I, 14)(E_ObsCode::L5Q, 15)( + E_ObsCode::L5X, + 16 + ); + +const boost::bimap mCodes_glo = + boost::assign::list_of:: + relation>(E_ObsCode::L1C, 0)(E_ObsCode::L1P, 1)(E_ObsCode::L2C, 2)( + E_ObsCode::L2P, + 3 + ); + +const boost::bimap mCodes_gal = + boost::assign::list_of< + boost::bimap:: + relation>(E_ObsCode::L1A, 0)(E_ObsCode::L1B, 1)(E_ObsCode::L1C, 2)(E_ObsCode::L1X, 3)(E_ObsCode::L1Z, 4)(E_ObsCode::L5I, 5)(E_ObsCode::L5Q, 6)(E_ObsCode::L5X, 7)(E_ObsCode::L7I, 8)(E_ObsCode::L7Q, 9)(E_ObsCode::L7X, 10)(E_ObsCode::L8I, 11)(E_ObsCode::L8Q, 12)(E_ObsCode::L8X, 13)(E_ObsCode::L6A, 14)(E_ObsCode::L6B, 15)(E_ObsCode::L6C, 16)(E_ObsCode::L6X, 17)( + E_ObsCode::L6Z, + 18 + ); + +const boost::bimap mCodes_qzs = + boost::assign::list_of< + boost::bimap:: + relation>(E_ObsCode::L1C, 0)(E_ObsCode::L1S, 1)(E_ObsCode::L1L, 2)(E_ObsCode::L2S, 3)(E_ObsCode::L2L, 4)(E_ObsCode::L2X, 5)(E_ObsCode::L5I, 6)(E_ObsCode::L5Q, 7)(E_ObsCode::L5X, 8)(E_ObsCode::L6S, 9)(E_ObsCode::L6L, 10)(E_ObsCode::L6X, 11)( + E_ObsCode::L1X, + 12 + ); + +const boost::bimap mCodes_bds = boost::assign::list_of< + boost::bimap::relation>(E_ObsCode::L2I, 0)(E_ObsCode::L2Q, 1)(E_ObsCode::L2X, 2)(E_ObsCode::L6I, 3)(E_ObsCode::L6Q, 4)(E_ObsCode::L6X, 5)(E_ObsCode::L7I, 6)(E_ObsCode::L7Q, 7)(E_ObsCode::L7X, 8)(E_ObsCode::L1D, 9)(E_ObsCode::L1P, 10)(E_ObsCode::NONE, 11)(E_ObsCode::L5D, 12)(E_ObsCode::L5P, 13)(E_ObsCode::NONE, 14)( + E_ObsCode::L1A, + 15 +); + +const boost::bimap mCodes_sbs = boost::assign::list_of< + boost::bimap::relation>(E_ObsCode::L1C, 0)(E_ObsCode::L5I, 1)( + E_ObsCode::L5Q, + 2 +); + +map> codeHax = { + {E_Sys::GPS, {{F1, E_ObsCode::L1C}, {F2, E_ObsCode::L2W}, {F5, E_ObsCode::L5I}}}, + + {E_Sys::GLO, + {{G1, E_ObsCode::NONE}, + {G2, E_ObsCode::NONE}, + {G3, E_ObsCode::L3I}, + {G4, E_ObsCode::L4B}, + {G6, E_ObsCode::L6B}}}, + + {E_Sys::GAL, + {{F1, E_ObsCode::L1C}, + {F5, E_ObsCode::L5I}, + {F6, E_ObsCode::L6C}, + {F7, E_ObsCode::L7I}, + {F8, E_ObsCode::L8I}}}, + + {E_Sys::BDS, + { + {F1, E_ObsCode::L1P}, + {B1, E_ObsCode::L2I}, + {F5, E_ObsCode::L5P}, + {B3, E_ObsCode::L6I}, + {F7, E_ObsCode::L7P}, + {F8, E_ObsCode::L8P}, + }}, + + {E_Sys::QZS, + {{F1, E_ObsCode::L1C}, {F2, E_ObsCode::L2L}, {F5, E_ObsCode::L5I}, {F6, E_ObsCode::L6S}}}, + + {E_Sys::IRN, {{F1, E_ObsCode::L1A}, {I9, E_ObsCode::L9A}}}, + + {E_Sys::SBS, {{F1, E_ObsCode::L1C}, {F5, E_ObsCode::L5I}}} }; -E_ObsCode freq2CodeHax( - E_Sys sys, - E_FType ft) +E_ObsCode freq2CodeHax(E_Sys sys, E_FType ft) { - if (codeHax.find(sys) == codeHax.end()) - return E_ObsCode::NONE; - - if (codeHax[sys].find(ft) == codeHax[sys].end()) - return E_ObsCode::NONE; - - return codeHax[sys][ft]; + if (codeHax.find(sys) == codeHax.end()) + return E_ObsCode::NONE; + + if (codeHax[sys].find(ft) == codeHax[sys].end()) + return E_ObsCode::NONE; + + return codeHax[sys][ft]; } diff --git a/src/cpp/common/constants.hpp b/src/cpp/common/constants.hpp index 036c68452..303205e8e 100644 --- a/src/cpp/common/constants.hpp +++ b/src/cpp/common/constants.hpp @@ -1,149 +1,146 @@ - #pragma once -#include - -using std::map; - -#include "enums.h" - #include +#include +#include +#include "common/enums.h" +struct SatSys; -#define TEC_CONSTANT 40.308193e16 -#define CLIGHT 299792458.0 /* speed of light (m/s) */ - -#define FREQ1 1.57542E9 /* L1/E1 frequency (Hz) */ -#define FREQ2 1.22760E9 /* L2 frequency (Hz) */ -#define FREQ5 1.17645E9 /* L5/E5a frequency (Hz) */ -#define FREQ6 1.27875E9 /* E6/LEX frequency (Hz) */ -#define FREQ7 1.20714E9 /* E5b frequency (Hz) */ -#define FREQ8 1.191795E9 /* E5a+b frequency (Hz) */ -#define FREQ1_GLO 1.60200E9 /* GLONASS G1 base frequency (Hz) */ -#define DFRQ1_GLO 0.56250E6 /* GLONASS G1 bias frequency (Hz/n) */ -#define FREQ2_GLO 1.24600E9 /* GLONASS G2 base frequency (Hz) */ -#define DFRQ2_GLO 0.43750E6 /* GLONASS G2 bias frequency (Hz/n) */ -#define FREQ3_GLO 1.202025E9 /* GLONASS G3 frequency (Hz) */ -#define FREQ4_GLO 1.600995E9 /* GLONASS G4 (CDMA G1) frequency (Hz) */ -#define FREQ6_GLO 1.248060E9 /* GLONASS G6 (CDMA G2) frequency (Hz) */ -#define FREQ1_CMP 1.561098E9 /* BeiDou B1 frequency (Hz) */ -#define FREQ2_CMP 1.207140E9 /* BeiDou B2 frequency (Hz) */ -#define FREQ3_CMP 1.268520E9 /* BeiDou B3 frequency (Hz) */ -#define FREQ9_IRN 2.492028E9 /* NavIC / IRNSS S9 frequency (Hz) */ - -#define PI 3.141592653589793238462643383279502884197169399375105820974 /* pi */ -#define PI2 (PI*2) /* pi*2 */ -#define D2R (PI/180.0) /* deg to rad */ -#define R2D (180.0/PI) /* rad to deg */ -#define SC2RAD 3.1415926535898 /* semi-circle to radian (IS-GPS) */ -#define AU 149597870691.0 /* 1 AU (m) */ -#define AS2R (D2R/3600.0) /* arc sec to radian */ -#define R2AS (1/AS2R) -#define MAS2R (2 * PI / (360 * 60 * 60 * 1000)) -#define MTS2R (2 * PI / (24 * 60 * 60 * 1000)) -#define S2MTS (1000.0) -#define R2MAS (1/MAS2R) -#define R2MTS (1/MTS2R) -#define MTS2S (1/S2MTS) - - -#define OMGE 7.2921151467E-5 /* earth angular velocity (IS-GPS) (rad/s) */ - -#define G_CONST 6.67408E-11 -#define RE_GLO 6378136.0 /* radius of earth (m) ref [2] */ -#define MU_GPS 3.9860050E14 /* gravitational constant ref [1] */ -#define MU_GLO 3.9860044E14 /* gravitational constant ref [2] */ -#define MU_GAL 3.986004418E14 /* earth gravitational constant ref [7] */ -#define MU_CMP 3.986004418E14 /* earth gravitational constant ref [9] */ - -#define MU MU_GPS - -#define RE_WGS84 6378137.0 /* earth semimajor axis (WGS84) (m) */ -#define FE_WGS84 (1.0/298.257223563) /* earth flattening (WGS84) */ - -#define IONO_HEIGHT 350000.0 /* ionosphere height (m) */ - -#define RE_MEAN 6371000.0 ///< mean Earth radius (m) -#define RE_IGRF 6371200.0 ///< geomagnetic conventional mean Earth radius for IGRF model (m) - -#define JD2MJD 2400000.5 /* JD to MJD */ -#define S_IN_DAY 86400.0 /* Number of seconds in a day */ -#define AUPerDay (AU/8.64e04) /* AU/Day (IAU 2009)[m/s] */ - -#define GM_Earth 3.986004418e14 ///< Geocentric gravitation constant (WGS84) [m^3/s^2] -#define M_Earth 5.9722e24 - -#define GM_Moon 4.9027949e12 ///< moon gravitation constant [m^3/s^2] -#define MoonRadius 1738200.0 ///< Equatorial radius of the Moon [m] - -#define GM_Sun 1.327122E20 ///< Heliocentric gravitation constant [m^3/s^2] -#define SunRadius 695990000.0 ///< Equatorial radius of the Sun [m], Seidelmann 1992 - -#define ZEROC 273.15 - -#define GRAVITY 9.80665 /* mean gravity (m/s^2) */ -#define MOLARDRY 0.028965 /* molar mass dry air (kg/mol) */ -#define UGAS 8.3143 /* universal gas constant (J/K/mol) */ - -#define DTTOL 0.005 /* tolerance of time difference (s) */ -#define MAXDTOE 7200.0 /* max time difference to GPS Toe (s) */ -#define MAXDTOE_QZS 3600.0 /* max time difference to QZS Toe (s) */ -#define MAXDTOE_GAL 9600.0 /* max time difference to GAL Toe (s) */ -#define MAXDTOE_CMP 3600.0 /* max time difference to BDS Toe (s) */ -#define MAXDTOE_GLO 7200.0 /* max time difference to GLO Toe (s) */ -#define MAXDTOE_SBS 360.0 /* max time difference to SBAS Toe (s) */ - - -#define P2_5 3.125000000000000E-02 /* 2^-5 */ -#define P2_6 1.562500000000000E-02 /* 2^-6 */ -#define P2_10 9.765625000000000E-04 /* 2^-10 */ -#define P2_11 4.882812500000000E-04 /* 2^-11 */ -#define P2_12 2.441406250000000E-04 /* 2^-12 */ -#define P2_15 3.051757812500000E-05 /* 2^-15 */ -#define P2_17 7.629394531250000E-06 /* 2^-17 */ -#define P2_19 1.907348632812500E-06 /* 2^-19 */ -#define P2_20 9.536743164062500E-07 /* 2^-20 */ -#define P2_21 4.768371582031250E-07 /* 2^-21 */ -#define P2_23 1.192092895507813E-07 /* 2^-23 */ -#define P2_24 5.960464477539063E-08 /* 2^-24 */ -#define P2_27 7.450580596923828E-09 /* 2^-27 */ -#define P2_28 3.725290298461914E-09 /* 2^-28 */ -#define P2_29 1.862645149230957E-09 /* 2^-29 */ -#define P2_30 9.313225746154785E-10 /* 2^-30 */ -#define P2_31 4.656612873077393E-10 /* 2^-31 */ -#define P2_32 2.328306436538696E-10 /* 2^-32 */ -#define P2_33 1.164153218269348E-10 /* 2^-33 */ -#define P2_34 5.820766091346741E-11 /* 2^-34 */ -#define P2_35 2.910383045673370E-11 /* 2^-35 */ -#define P2_38 3.637978807091713E-12 /* 2^-38 */ -#define P2_39 1.818989403545856E-12 /* 2^-39 */ -#define P2_40 9.094947017729282E-13 /* 2^-40 */ -#define P2_41 4.547473508864641E-13 /* 2^-41 */ -#define P2_43 1.136868377216160E-13 /* 2^-43 */ -#define P2_46 1.421085471520200E-14 /* 2^-46 */ -#define P2_48 3.552713678800501E-15 /* 2^-48 */ -#define P2_50 8.881784197001252E-16 /* 2^-50 */ -#define P2_55 2.775557561562891E-17 /* 2^-55 */ -#define P2_59 1.734723475976807E-18 /* 2^-59 */ -#define P2_66 1.355252715606881E-20 /* 2^-66 */ - - -#define SMOOTHED_SUFFIX "_smoothed" -#define FORWARD_SUFFIX "_forward" -#define BACKWARD_SUFFIX "_backward" - -extern map> code2Freq; -extern map genericWavelength; +using std::map; +using std::vector; + +constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974; /* pi */ +constexpr double PI2 = (PI * 2); /* pi*2 */ +constexpr double D2R = (PI / 180.0); /* deg to rad */ +constexpr double R2D = (180.0 / PI); /* rad to deg */ +constexpr double SC2RAD = 3.1415926535898; /* semi-circle to radian (IS-GPS) */ +constexpr double AU = 149597870691.0; /* 1 AU (m) */ +constexpr double AS2R = (D2R / 3600.0); /* arc sec to radian */ + +constexpr double TEC_CONSTANT = 40.308193e16; +constexpr double R2AS = (1 / AS2R); +constexpr double MAS2R = (2 * PI / (360 * 60 * 60 * 1000)); +constexpr double MTS2R = (2 * PI / (24 * 60 * 60 * 1000)); +constexpr double S2MTS = (1000.0); +constexpr double R2MAS = (1 / MAS2R); +constexpr double R2MTS = (1 / MTS2R); +constexpr double MTS2S = (1 / S2MTS); +constexpr double G_CONST = 6.67408E-11; +constexpr double RE_MEAN = 6371000.0; ///< mean Earth radius (m) +constexpr double RE_IGRF = + 6371200.0; ///< geomagnetic conventional mean Earth radius for IGRF model (m) +constexpr double GM_Earth = 3.986004418e14; ///< Geocentric gravitation constant (WGS84) [m^3/s^2] +constexpr double M_Earth = 5.9722e24; +constexpr double GM_Moon = 4.9027949e12; ///< moon gravitation constant [m^3/s^2] +constexpr double MoonRadius = 1738200.0; ///< Equatorial radius of the Moon [m] +constexpr double GM_Sun = 1.327122E20; ///< Heliocentric gravitation constant [m^3/s^2] +constexpr double SunRadius = 695990000.0; ///< Equatorial radius of the Sun [m], Seidelmann 1992 +constexpr double ZEROC = 273.15; +constexpr const char* SMOOTHED_SUFFIX = "_smoothed"; +constexpr const char* FORWARD_SUFFIX = "_forward"; +constexpr const char* BACKWARD_SUFFIX = "_backward"; + +constexpr double CLIGHT = 299792458.0; /* speed of light (m/s) */ + +constexpr double FREQ1 = 1.57542E9; /* L1/E1 frequency (Hz) */ +constexpr double FREQ2 = 1.22760E9; /* L2 frequency (Hz) */ +constexpr double FREQ5 = 1.17645E9; /* L5/E5a frequency (Hz) */ +constexpr double FREQ6 = 1.27875E9; /* E6/LEX frequency (Hz) */ +constexpr double FREQ7 = 1.20714E9; /* E5b frequency (Hz) */ +constexpr double FREQ8 = 1.191795E9; /* E5a+b frequency (Hz) */ +constexpr double FREQ1_GLO = 1.60200E9; /* GLONASS G1 base frequency (Hz) */ +constexpr double DFRQ1_GLO = 0.56250E6; /* GLONASS G1 bias frequency (Hz/n) */ +constexpr double FREQ2_GLO = 1.24600E9; /* GLONASS G2 base frequency (Hz) */ +constexpr double DFRQ2_GLO = 0.43750E6; /* GLONASS G2 bias frequency (Hz/n) */ +constexpr double FREQ3_GLO = 1.202025E9; /* GLONASS G3 frequency (Hz) */ +constexpr double FREQ4_GLO = 1.600995E9; /* GLONASS G4 (CDMA G1) frequency (Hz) */ +constexpr double FREQ6_GLO = 1.248060E9; /* GLONASS G6 (CDMA G2) frequency (Hz) */ +constexpr double FREQ1_CMP = 1.561098E9; /* BeiDou B1 frequency (Hz) */ +constexpr double FREQ2_CMP = 1.207140E9; /* BeiDou B2 frequency (Hz) */ +constexpr double FREQ3_CMP = 1.268520E9; /* BeiDou B3 frequency (Hz) */ +constexpr double FREQ9_IRN = 2.492028E9; /* NavIC / IRNSS S9 frequency (Hz) */ + +constexpr double OMGE = 7.2921151467E-5; /* earth angular velocity (IS-GPS) (rad/s) */ + +constexpr double RE_GLO = 6378136.0; /* radius of earth (m) ref [2] */ +constexpr double MU_GPS = 3.9860050E14; /* gravitational constant ref [1] */ +constexpr double MU_GLO = 3.9860044E14; /* gravitational constant ref [2] */ +constexpr double MU_GAL = 3.986004418E14; /* earth gravitational constant ref [7] */ +constexpr double MU_CMP = 3.986004418E14; /* earth gravitational constant ref [9] */ +constexpr double MU = MU_GPS; + +constexpr double RE_WGS84 = 6378137.0; /* earth semimajor axis (WGS84) (m) */ +constexpr double FE_WGS84 = (1.0 / 298.257223563); /* earth flattening (WGS84) */ + +constexpr double IONO_HEIGHT = 350000.0; /* ionosphere height (m) */ + +constexpr double JD2MJD = 2400000.5; /* JD to MJD */ +constexpr double S_IN_DAY = 86400.0; /* Number of seconds in a day */ +constexpr double AUPerDay = (AU / 8.64e04); /* AU/Day (IAU 2009)[m/s] */ + +constexpr double GRAVITY = 9.80665; /* mean gravity (m/s^2) */ +constexpr double MOLARDRY = 0.028965; /* molar mass dry air (kg/mol) */ +constexpr double UGAS = 8.3143; /* universal gas constant (J/K/mol) */ + +constexpr double DTTOL = 0.005; /* tolerance of time difference (s) */ +constexpr double MAXDTOE = 14400.0; /* max time difference to GPS Toe (s) */ +constexpr double MAXDTOE_QZS = 7200.0; /* max time difference to QZS Toe (s) */ +constexpr double MAXDTOE_GAL = 9600.0; /* max time difference to GAL Toe (s) */ +constexpr double MAXDTOE_CMP = 7200.0; /* max time difference to BDS Toe (s) */ +constexpr double MAXDTOE_GLO = 7200.0; /* max time difference to GLO Toe (s) */ +constexpr double MAXDTOE_SBS = 360.0; /* max time difference to SBAS Toe (s) */ + +constexpr double P2_5 = 3.125000000000000E-02; /* 2^-5 */ +constexpr double P2_6 = 1.562500000000000E-02; /* 2^-6 */ +constexpr double P2_10 = 9.765625000000000E-04; /* 2^-10 */ +constexpr double P2_11 = 4.882812500000000E-04; /* 2^-11 */ +constexpr double P2_12 = 2.441406250000000E-04; /* 2^-12 */ +constexpr double P2_15 = 3.051757812500000E-05; /* 2^-15 */ +constexpr double P2_17 = 7.629394531250000E-06; /* 2^-17 */ +constexpr double P2_19 = 1.907348632812500E-06; /* 2^-19 */ +constexpr double P2_20 = 9.536743164062500E-07; /* 2^-20 */ +constexpr double P2_21 = 4.768371582031250E-07; /* 2^-21 */ +constexpr double P2_23 = 1.192092895507813E-07; /* 2^-23 */ +constexpr double P2_24 = 5.960464477539063E-08; /* 2^-24 */ +constexpr double P2_27 = 7.450580596923828E-09; /* 2^-27 */ +constexpr double P2_28 = 3.725290298461914E-09; /* 2^-28 */ +constexpr double P2_29 = 1.862645149230957E-09; /* 2^-29 */ +constexpr double P2_30 = 9.313225746154785E-10; /* 2^-30 */ +constexpr double P2_31 = 4.656612873077393E-10; /* 2^-31 */ +constexpr double P2_32 = 2.328306436538696E-10; /* 2^-32 */ +constexpr double P2_33 = 1.164153218269348E-10; /* 2^-33 */ +constexpr double P2_34 = 5.820766091346741E-11; /* 2^-34 */ +constexpr double P2_35 = 2.910383045673370E-11; /* 2^-35 */ +constexpr double P2_38 = 3.637978807091713E-12; /* 2^-38 */ +constexpr double P2_39 = 1.818989403545856E-12; /* 2^-39 */ +constexpr double P2_40 = 9.094947017729282E-13; /* 2^-40 */ +constexpr double P2_41 = 4.547473508864641E-13; /* 2^-41 */ +constexpr double P2_43 = 1.136868377216160E-13; /* 2^-43 */ +constexpr double P2_46 = 1.421085471520200E-14; /* 2^-46 */ +constexpr double P2_48 = 3.552713678800501E-15; /* 2^-48 */ +constexpr double P2_50 = 8.881784197001252E-16; /* 2^-50 */ +constexpr double P2_55 = 2.775557561562891E-17; /* 2^-55 */ +constexpr double P2_59 = 1.734723475976807E-18; /* 2^-59 */ +constexpr double P2_66 = 1.355252715606881E-20; /* 2^-66 */ + +extern map> code2Freq; +extern map genericWavelength; +extern map> blockTypeFrequencies; + +// Filter signal codes based on block type capabilities +// Returns true if the signal code is supported by the given block type +bool isSignalSupportedByBlockType(E_ObsCode code, E_Block blockType); extern const unsigned int tbl_CRC24Q[]; const unsigned char RTCM_PREAMBLE = 0xD3; -extern const boost::bimap mCodes_gps; -extern const boost::bimap mCodes_glo; -extern const boost::bimap mCodes_gal; -extern const boost::bimap mCodes_qzs; -extern const boost::bimap mCodes_bds; -extern const boost::bimap mCodes_sbs; +extern const boost::bimap mCodes_gps; +extern const boost::bimap mCodes_glo; +extern const boost::bimap mCodes_gal; +extern const boost::bimap mCodes_qzs; +extern const boost::bimap mCodes_bds; +extern const boost::bimap mCodes_sbs; extern E_ObsCode freq2CodeHax(E_Sys sys, E_FType ft); diff --git a/src/cpp/common/cost.cpp b/src/cpp/common/cost.cpp index 770d0e551..fd5e45f3b 100644 --- a/src/cpp/common/cost.cpp +++ b/src/cpp/common/cost.cpp @@ -1,327 +1,368 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -FileType COST__() -{ - -} - -#include - +#include "common/cost.hpp" #include +#include +#include "3rdparty/egm96/EGM96.h" +#include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/receiver.hpp" +#include "common/sinex.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "pea/ppp.hpp" +#include "trop/tropModels.hpp" -#include "coordinates.hpp" -#include "tropModels.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "sinex.hpp" -#include "trace.hpp" -#include "cost.hpp" -#include "EGM96.h" -#include "ppp.hpp" - - -static map> filePosMap; -static map startTimeMap; -static map numSamplesMap; +FileType COST__() {} +static map> filePosMap; +static map startTimeMap; +static map numSamplesMap; /** Replaces first instance of string 'toReplace' with 'replaceWith' within 's' -*/ + */ bool replaceStr( - string& s, ///< String to modify - string toReplace, ///< String to replace - string replaceWith) ///< String to replace with + string& s, ///< String to modify + string toReplace, ///< String to replace + string replaceWith ///< String to replace with +) { - std::size_t pos = s.find(toReplace); - if (pos == string::npos) return false; - s.replace(pos, toReplace.length(), replaceWith); - return true; + std::size_t pos = s.find(toReplace); + if (pos == string::npos) + return false; + s.replace(pos, toReplace.length(), replaceWith); + return true; } /** Outputs troposphere COST file -*/ + */ void outputCost( - string filename, ///< Filename - KFState& kfState, ///< KF object containing positioning & trop solutions - Receiver& rec) ///< Receiver + string filename, ///< Filename + KFState& kfState, ///< KF object containing positioning & trop solutions + Receiver& rec ///< Receiver +) { - std::ofstream fout(filename, std::fstream::in | std::fstream::out); - if (!fout) - { - return; - } - - auto& recOpts = acsConfig.getRecOpts(rec.id); - - auto time = kfState.time; - - fout.seekp(0, fout.end); // seek to end of file - bool firstWrite = (fout.tellp() == 0); // file is empty if current position is 0 - - if (firstWrite) - startTimeMap[filename] = time; - long int duration = (time - startTimeMap[filename]).to_int(); - if (duration % acsConfig.cost_time_interval != 0) - return; - - if (firstWrite) - { - tracepdeex(0, fout, "%-20s %-20s %-20s\n", - acsConfig.cost_format, // Format name & version number - acsConfig.cost_project, // Project name - acsConfig.cost_status); // File status - - string locationName = rec.snx.id_ptr->desc; - boost::trim(locationName); - - bool commaReplaced = replaceStr(locationName, ", ", " ("); - if (commaReplaced) - locationName.push_back(')'); - - tracepdeex(0, fout, "%-4s %-9s %-60s\n", - rec.snx.id_ptr->sitecode .c_str(), // Rec ID - rec.snx.id_ptr->domes .c_str(), // DOMES ID - locationName .c_str()); // Site name with country - - tracepdeex(0, fout, "%-20s %-20s\n", - rec.receiverType .c_str(), // Receiver type - rec.antennaType .c_str()); // Antenna type - } - - if (firstWrite) filePosMap[filename][E_FilePos::COORD] = fout.tellp(); - fout.seekp( filePosMap[filename][E_FilePos::COORD]); // Overwrite - - - VectorEcef recPosEcef; - bool kfFound = true; - for (int i = 0; i < 3; i++) - { - kfFound &= kfState.getKFValue({KF::REC_POS, {}, rec.id, i}, recPosEcef(i)); - } - - if (kfFound == false) - { - recPosEcef = rec.aprioriPos; - } - - VectorEcef eccEcef = body2ecef(rec.attStatus, rec.snx.ecc_ptr->ecc); - VectorPos recPos = ecef2pos(recPosEcef + eccEcef); - - if (recPos[1] < 0) - recPos[1] += 2 * PI; - - double geoidOffset = egm96_compute_altitude_offset(recPos.latDeg(), recPos.lonDeg()); - - tracepdeex(0, fout, "%12.6lf%12.6lf%12.3lf%12.3lf%12.3lf\n", - recPos.latDeg(), - recPos.lonDeg(), - recPos.hgt(), // ARP height above ellipsoid - recPos.hgt() - geoidOffset, // ARP height above geoid - rec.snx.ecc_ptr->ecc.u()); // ARP height above benchmark - - if (firstWrite) - tracepdeex(0, fout, "%-20s ", time.gregString().c_str()); // Time of first sample - - if (firstWrite) filePosMap[filename][E_FilePos::CURR_TIME] = fout.tellp(); - fout.seekp( filePosMap[filename][E_FilePos::CURR_TIME]); - tracepdeex(0, fout, "%-20s\n", time.gregString().c_str()); // Time of processing - - if (firstWrite) - { - tracepdeex(0, fout, "%-20s %-20s %-20s %-20s\n", - acsConfig.cost_centre, // Processing centre - acsConfig.cost_method, // Processing method - acsConfig.cost_orbit_type, // Orbit type - acsConfig.cost_met_source); // Source of met. data - - tracepdeex(0, fout, "%5d%5d", - acsConfig.cost_time_interval / 60, // Nominal time increment between data samples (min) - acsConfig.cost_time_interval / 60); // Batch updating interval (min) - } - - if (firstWrite) filePosMap[filename][E_FilePos::TOTAL_TIME] = fout.tellp(); - fout.seekp( filePosMap[filename][E_FilePos::TOTAL_TIME]); - tracepdeex(0, fout, "%5d\n", duration / 60); // Total length of batch time series - - if (firstWrite) filePosMap[filename][E_FilePos::PCDH] = fout.tellp(); - fout.seekp( filePosMap[filename][E_FilePos::PCDH]); - - bool isRealTime = (acsConfig.obs_rtcm_inputs.size() > 0); - - union - { - unsigned int all = 0; - struct - { - unsigned isRealTime : 1; ///< Data processed in near-real time [false: (re-)processed off-line] - unsigned climate : 1; ///< Data processed to climate quality [false: NRT quality] - 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 gpsUsed : 1; ///< GPS satellite(s) used - unsigned gloUsed : 1; ///< GLONASS satellite(s) used - unsigned galUsed : 1; ///< Galileo satellite(s) used - unsigned reserved : 22; ///< Reserved - unsigned invalid : 1; ///< PCDH is missing or invalid - }; - } pcdh; ///< Product Confidence Data - Header - pcdh.isRealTime = isRealTime; - pcdh.climate = false; - pcdh.otl = recOpts.tideModels.otl; - pcdh.atc = false; - pcdh.localMetData = false; - pcdh.centredTime = false; - pcdh.gpsUsed = acsConfig.process_sys[E_Sys::GPS]; - pcdh.gloUsed = acsConfig.process_sys[E_Sys::GLO]; - pcdh.galUsed = acsConfig.process_sys[E_Sys::GAL]; - pcdh.invalid = false; - - tracepdeex(0, fout, "%08x\n", (uint32_t)pcdh.all); // Product confidence data - header - - auto& numSamplesPos = filePosMap[filename][E_FilePos::NUM_SAMPLES]; - - if (firstWrite) - numSamplesPos = fout.tellp(); - - fout.seekp(numSamplesPos); - - numSamplesMap[filename]++; - - tracepdeex(0, fout, "%4d\n", numSamplesMap[filename]); - - - // Body - if (firstWrite == false) - fout.seekp(filePosMap[filename][E_FilePos::FOOTER]); - - - int obsCount = 0; - for (auto& obs : only(rec.obsList)) - { - if ( acsConfig.process_sys[obs.Sat.sys] == false - ||obs.exclude) - { - continue; - } - - obsCount++; - } - - union - { - unsigned int all = 0; - struct - { - unsigned numSats : 5; ///< Num GNSS sasts in solution [31 = missing] - unsigned obsMetData : 1; ///< Observed met. data used [false: NWP met data] - unsigned ztdPoorQuality : 1; ///< ZTD data quality is considered poor - unsigned reserved : 24; ///< Reserved - unsigned invalid : 1; ///< PCDD is missing or invalid - }; - } pcdd; ///< Product Confidence Data - Data - - pcdd.numSats = std::min(obsCount, 31); - pcdd.obsMetData = false; - pcdd.ztdPoorQuality = false; - pcdd.invalid = false; - - double tropStates[3] = {}; - double tropVars [3] = {}; - bool tropFound [3] = {}; - - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( key.type != KF::TROP - &&key.type != KF::TROP_GRAD) - { - continue; - } - - if ( acsConfig.pppOpts.equate_tropospheres == false - &&key.str != rec.id) - { - continue; - } - - double state = kfState.x(index); - double var = kfState.P(index,index); - - int num; - if (key.type == KF::TROP) num = 0; - else num = key.num + 1; - - tropFound [num] = true; - tropStates [num] += state; - tropVars [num] += var; - } - - double ztd = -9.9 / 1000; - double zwd = -9.9 / 1000; - double ztdStd = -9.9 / 1000; - double nsGrad = -9.99 / 1000; - double ewGrad = -9.99 / 1000; - double nsGradStd = -9.99 / 1000; - double ewGradStd = -9.99 / 1000; - - if (tropFound[0]) - { - ztd = tropStates [0]; - ztdStd = sqrt( tropVars [0]); - - double zhd = tropDryZTD(nullStream, recOpts.tropModel.models, time, recPos); - zwd = ztd - zhd; - } - - double gradM = gradMapFn(30 * D2R); - - if (tropFound[1]) { nsGrad = gradM * tropStates[1]; nsGradStd = sqrt(tropVars[1]); } - if (tropFound[2]) { ewGrad = gradM * tropStates[2]; ewGradStd = sqrt(tropVars[2]); } - - GEpoch epoch = time; - tracepdeex(0, fout, " %02d %02d %02d %08x%7.1lf%7.1lf%7.1lf%7.1lf%7.1lf%7.1lf%7.1lf%7.2lf%7.2lf%7.2lf%7.2lf%8.3lf\n", - (int)epoch.hour, // Timestamp (hr) - (int)epoch.min, // Timestamp (min) - (int)epoch.sec, // Timestamp (sec) - (uint32_t)pcdd.all, // Product confidence data - data - ztd * 1000, // ZTD (mm) - ztdStd * 1000, // ZTD std-dev (mm) - zwd * 1000, // ZWD (mm) - -9.9, // IWV (kg.m^-2) - -9.9, // Pressure (hPa) - -9.9, // Temperature used for IWV (K) - -9.9, // Relative humidity used for IWV (%) - nsGrad * 1000, // N/S delay gradient (mm) - ewGrad * 1000, // E/W delay gradient (mm) - nsGradStd * 1000, // N/S delay gradient std-dev (mm) - ewGradStd * 1000, // E/W delay gradient std-dev (mm) - -99.999); // Vertically integrated TEC (TECU) - - tracepdeex(0, fout, "%4d\n", obsCount); // Number of slant samples to follow - - for (auto& obs : only(rec.obsList)) - { - if ( acsConfig.process_sys[obs.Sat.sys] == false - ||obs.exclude - ||obs.tropSlant == 0) - { - continue; - } - - SatStat& satStat = *obs.satStat_ptr; - - tracepdeex(0, fout, "%-4s%7.1lf%7.1lf%7.1lf%7.1lf\n", - obs.Sat.id().c_str(), // Sat ID - obs.tropSlant * 1000, // Total slant delay (mm) - sqrt(obs.tropSlantVar) * 1000, // Total slant delay std-dev (mm) - satStat.az * R2D, // Slant azi angle (CW from true North) - satStat.el * R2D); // Slant ele angle (from local horizon) - } - - filePosMap[filename][E_FilePos::FOOTER] = fout.tellp(); - - tracepdeex(0, fout, "----------------------------------------------------------------------------------------------------\n"); + std::ofstream fout(filename, std::fstream::in | std::fstream::out); + if (!fout) + { + return; + } + + auto& recOpts = acsConfig.getRecOpts(rec.id); + + auto time = kfState.time; + + fout.seekp(0, fout.end); // seek to end of file + bool firstWrite = (fout.tellp() == 0); // file is empty if current position is 0 + + if (firstWrite) + startTimeMap[filename] = time; + long int duration = (time - startTimeMap[filename]).to_int(); + if (duration % acsConfig.cost_time_interval != 0) + return; + + if (firstWrite) + { + tracepdeex( + 0, + fout, + "%-20s %-20s %-20s\n", + acsConfig.cost_format, // Format name & version number + acsConfig.cost_project, // Project name + acsConfig.cost_status + ); // File status + + string locationName = rec.snx.id_ptr->desc; + boost::trim(locationName); + + bool commaReplaced = replaceStr(locationName, ", ", " ("); + if (commaReplaced) + locationName.push_back(')'); + + tracepdeex( + 0, + fout, + "%-4s %-9s %-60s\n", + rec.snx.id_ptr->sitecode.c_str(), // Rec ID + rec.snx.id_ptr->domes.c_str(), // DOMES ID + locationName.c_str() + ); // Site name with country + + tracepdeex( + 0, + fout, + "%-20s %-20s\n", + rec.receiverType.c_str(), // Receiver type + rec.antennaType.c_str() + ); // Antenna type + } + + if (firstWrite) + filePosMap[filename][E_FilePos::COORD] = fout.tellp(); + fout.seekp(filePosMap[filename][E_FilePos::COORD]); // Overwrite + + VectorEcef recPosEcef; + bool kfFound = true; + for (int i = 0; i < 3; i++) + { + kfFound = kfFound && (kfState.getKFValue({KF::REC_POS, {}, rec.id, i}, recPosEcef(i)) != + E_Source::NONE); + } + + if (kfFound == false) + { + recPosEcef = rec.aprioriPos; + } + + VectorEcef eccEcef = body2ecef(rec.attStatus, rec.snx.ecc_ptr->ecc); + VectorPos recPos = ecef2pos(recPosEcef + eccEcef); + + if (recPos[1] < 0) + recPos[1] += 2 * PI; + + double geoidOffset = egm96_compute_altitude_offset(recPos.latDeg(), recPos.lonDeg()); + + tracepdeex( + 0, + fout, + "%12.6lf%12.6lf%12.3lf%12.3lf%12.3lf\n", + recPos.latDeg(), + recPos.lonDeg(), + recPos.hgt(), // ARP height above ellipsoid + recPos.hgt() - geoidOffset, // ARP height above geoid + rec.snx.ecc_ptr->ecc.u() + ); // ARP height above benchmark + + if (firstWrite) + tracepdeex(0, fout, "%-20s ", time.gregString().c_str()); // Time of first sample + + if (firstWrite) + filePosMap[filename][E_FilePos::CURR_TIME] = fout.tellp(); + fout.seekp(filePosMap[filename][E_FilePos::CURR_TIME]); + tracepdeex(0, fout, "%-20s\n", time.gregString().c_str()); // Time of processing + + if (firstWrite) + { + tracepdeex( + 0, + fout, + "%-20s %-20s %-20s %-20s\n", + acsConfig.cost_centre, // Processing centre + acsConfig.cost_method, // Processing method + acsConfig.cost_orbit_type, // Orbit type + acsConfig.cost_met_source + ); // Source of met. data + + tracepdeex( + 0, + fout, + "%5d%5d", + acsConfig.cost_time_interval / 60, // Nominal time increment between data samples (min) + acsConfig.cost_time_interval / 60 + ); // Batch updating interval (min) + } + + if (firstWrite) + filePosMap[filename][E_FilePos::TOTAL_TIME] = fout.tellp(); + fout.seekp(filePosMap[filename][E_FilePos::TOTAL_TIME]); + tracepdeex(0, fout, "%5d\n", duration / 60); // Total length of batch time series + + if (firstWrite) + filePosMap[filename][E_FilePos::PCDH] = fout.tellp(); + fout.seekp(filePosMap[filename][E_FilePos::PCDH]); + + bool isRealTime = (acsConfig.obs_rtcm_inputs.size() > 0); + + union + { + unsigned int all = 0; + struct + { + unsigned isRealTime : 1; ///< Data processed in near-real time [false: (re-)processed + ///< off-line] + unsigned climate : 1; ///< Data processed to climate quality [false: NRT quality] + 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 gpsUsed : 1; ///< GPS satellite(s) used + unsigned gloUsed : 1; ///< GLONASS satellite(s) used + unsigned galUsed : 1; ///< Galileo satellite(s) used + unsigned reserved : 22; ///< Reserved + unsigned invalid : 1; ///< PCDH is missing or invalid + }; + } pcdh; ///< Product Confidence Data - Header + pcdh.isRealTime = isRealTime; + pcdh.climate = false; + pcdh.otl = recOpts.tideModels.otl; + pcdh.atc = false; + pcdh.localMetData = false; + pcdh.centredTime = false; + pcdh.gpsUsed = acsConfig.process_sys[E_Sys::GPS]; + pcdh.gloUsed = acsConfig.process_sys[E_Sys::GLO]; + pcdh.galUsed = acsConfig.process_sys[E_Sys::GAL]; + pcdh.invalid = false; + + tracepdeex(0, fout, "%08x\n", (uint32_t)pcdh.all); // Product confidence data - header + + auto& numSamplesPos = filePosMap[filename][E_FilePos::NUM_SAMPLES]; + + if (firstWrite) + numSamplesPos = fout.tellp(); + + fout.seekp(numSamplesPos); + + numSamplesMap[filename]++; + + tracepdeex(0, fout, "%4d\n", numSamplesMap[filename]); + + // Body + if (firstWrite == false) + fout.seekp(filePosMap[filename][E_FilePos::FOOTER]); + + int obsCount = 0; + for (auto& obs : only(rec.obsList)) + { + if (acsConfig.process_sys[obs.Sat.sys] == false || obs.exclude) + { + continue; + } + + obsCount++; + } + + union + { + unsigned int all = 0; + struct + { + unsigned numSats : 5; ///< Num GNSS sasts in solution [31 = missing] + unsigned obsMetData : 1; ///< Observed met. data used [false: NWP met data] + unsigned ztdPoorQuality : 1; ///< ZTD data quality is considered poor + unsigned reserved : 24; ///< Reserved + unsigned invalid : 1; ///< PCDD is missing or invalid + }; + } pcdd; ///< Product Confidence Data - Data + + pcdd.numSats = std::min(obsCount, 31); + pcdd.obsMetData = false; + pcdd.ztdPoorQuality = false; + pcdd.invalid = false; + + double tropStates[3] = {}; + double tropVars[3] = {}; + bool tropFound[3] = {}; + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::TROP && key.type != KF::TROP_GRAD) + { + continue; + } + + if (acsConfig.pppOpts.equate_tropospheres == false && key.str != rec.id) + { + continue; + } + + double state = kfState.x(index); + double var = kfState.P(index, index); + + int num; + if (key.type == KF::TROP) + num = 0; + else + num = key.num + 1; + + tropFound[num] = true; + tropStates[num] += state; + tropVars[num] += var; + } + + double ztd = -9.9 / 1000; + double zwd = -9.9 / 1000; + double ztdStd = -9.9 / 1000; + double nsGrad = -9.99 / 1000; + double ewGrad = -9.99 / 1000; + double nsGradStd = -9.99 / 1000; + double ewGradStd = -9.99 / 1000; + + if (tropFound[0]) + { + ztd = tropStates[0]; + ztdStd = sqrt(tropVars[0]); + + double zhd = tropDryZTD(nullStream, recOpts.tropModel.models, time, recPos); + zwd = ztd - zhd; + } + + double gradM = gradMapFn(30 * D2R); + + if (tropFound[1]) + { + nsGrad = gradM * tropStates[1]; + nsGradStd = sqrt(tropVars[1]); + } + if (tropFound[2]) + { + ewGrad = gradM * tropStates[2]; + ewGradStd = sqrt(tropVars[2]); + } + + GEpoch epoch = time; + tracepdeex( + 0, + fout, + " %02d %02d %02d " + "%08x%7.1lf%7.1lf%7.1lf%7.1lf%7.1lf%7.1lf%7.1lf%7.2lf%7.2lf%7.2lf%7.2lf%8.3lf\n", + (int)epoch.hour, // Timestamp (hr) + (int)epoch.min, // Timestamp (min) + (int)epoch.sec, // Timestamp (sec) + (uint32_t)pcdd.all, // Product confidence data - data + ztd * 1000, // ZTD (mm) + ztdStd * 1000, // ZTD std-dev (mm) + zwd * 1000, // ZWD (mm) + -9.9, // IWV (kg.m^-2) + -9.9, // Pressure (hPa) + -9.9, // Temperature used for IWV (K) + -9.9, // Relative humidity used for IWV (%) + nsGrad * 1000, // N/S delay gradient (mm) + ewGrad * 1000, // E/W delay gradient (mm) + nsGradStd * 1000, // N/S delay gradient std-dev (mm) + ewGradStd * 1000, // E/W delay gradient std-dev (mm) + -99.999 + ); // Vertically integrated TEC (TECU) + + tracepdeex(0, fout, "%4d\n", obsCount); // Number of slant samples to follow + + for (auto& obs : only(rec.obsList)) + { + if (acsConfig.process_sys[obs.Sat.sys] == false || obs.exclude || obs.tropSlant == 0) + { + continue; + } + + SatStat& satStat = *obs.satStat_ptr; + + tracepdeex( + 0, + fout, + "%-4s%7.1lf%7.1lf%7.1lf%7.1lf\n", + obs.Sat.id().c_str(), // Sat ID + obs.tropSlant * 1000, // Total slant delay (mm) + sqrt(obs.tropSlantVar) * 1000, // Total slant delay std-dev (mm) + satStat.az * R2D, // Slant azi angle (CW from true North) + satStat.el * R2D + ); // Slant ele angle (from local horizon) + } + + filePosMap[filename][E_FilePos::FOOTER] = fout.tellp(); + + tracepdeex( + 0, + fout, + "------------------------------------------------------------------------------------------" + "----------\n" + ); } diff --git a/src/cpp/common/cost.hpp b/src/cpp/common/cost.hpp index 4592fadb4..6d7f93790 100644 --- a/src/cpp/common/cost.hpp +++ b/src/cpp/common/cost.hpp @@ -1,14 +1,9 @@ - #include -#include "gTime.hpp" - -struct KFState; -struct Receiver; +#include "common/gTime.hpp" using std::string; -void outputCost( - string filename, - KFState& kfState, - Receiver& rec); +struct KFState; +struct Receiver; +void outputCost(string filename, KFState& kfState, Receiver& rec); diff --git a/src/cpp/common/customDecoder.cpp b/src/cpp/common/customDecoder.cpp index e4450e3db..d9a8be06f 100644 --- a/src/cpp/common/customDecoder.cpp +++ b/src/cpp/common/customDecoder.cpp @@ -1,186 +1,183 @@ - - // #pragma GCC optimize ("O0") -#include "customDecoder.hpp" -#include "observations.hpp" -#include "navigation.hpp" -#include "constants.hpp" -#include "gTime.hpp" -#include "enums.h" +#include "common/customDecoder.hpp" +#include +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +map>> CustomDecoder::gyroDataMaps; +map>> CustomDecoder::acclDataMaps; +map>> CustomDecoder::tempDataMaps; -map>> CustomDecoder::gyroDataMaps; -map>> CustomDecoder::acclDataMaps; -map>> CustomDecoder::tempDataMaps; - -void CustomDecoder::decodeRAWX( - vector& payload) +void CustomDecoder::decodeRAWX(vector& payload) { -// std::cout << "Recieved RAWX message" << "\n"; - - double rcvTow = *((double*) &payload[0]); - short unsigned int week = *((short unsigned int*) &payload[8]); - char leapS = *((char*) &payload[10]); - unsigned char numMeas = payload[11]; + // std::cout << "Recieved RAWX message" << "\n"; - if (payload.size() != 16 + 32 * numMeas) - { - return; - } + double rcvTow = *((double*)&payload[0]); + short unsigned int week = *((short unsigned int*)&payload[8]); + char leapS = *((char*)&payload[10]); + unsigned char numMeas = payload[11]; -// std::cout << "\n" << "Recieved RAWX message has " << numMeas << " measurements" << "\n"; + if (payload.size() != 16 + 32 * numMeas) + { + return; + } - map obsMap; + // std::cout << "\n" << "Recieved RAWX message has " << numMeas << " measurements" << "\n"; - for (int i = 0; i < numMeas; i++) - { - unsigned char* measPayload = &payload[i*32]; //below offsets dont start at zero, this matches spec + map obsMap; - double pr = *((double*) &measPayload[16]); - double cp = *((double*) &measPayload[24]); - float dop = *((float*) &measPayload[32]); - int gnssId = measPayload[36]; - int satId = measPayload[37]; - int sigId = measPayload[38]; + for (int i = 0; i < numMeas; i++) + { + unsigned char* measPayload = + &payload[i * 32]; // below offsets dont start at zero, this matches spec - } + double pr = *((double*)&measPayload[16]); + double cp = *((double*)&measPayload[24]); + float dop = *((float*)&measPayload[32]); + int gnssId = measPayload[36]; + int satId = measPayload[37]; + int sigId = measPayload[38]; + } - ObsList obsList; + ObsList obsList; - for (auto& [Sat, obs] : obsMap) - { - obsList.push_back((shared_ptr)obs); - } + for (auto& [Sat, obs] : obsMap) + { + obsList.push_back((shared_ptr)obs); + } - obsListList.push_back(obsList); + obsListList.push_back(obsList); - lastTimeTag = 0; - lastTime = gpst2time(week, rcvTow); + lastTimeTag = 0; + lastTime = gpst2time(week, rcvTow); } - -void CustomDecoder::decodeMEAS( - vector& payload) +void CustomDecoder::decodeMEAS(vector& payload) { - unsigned int timeTag = *((unsigned int*) &payload[0]); - short unsigned int flags = *((short unsigned int*) &payload[4]); - short unsigned int id = *((short unsigned int*) &payload[6]); - - int numMeas = flags >> 11; - - //adjust time tags - if (lastTimeTag == 0) - { - lastTimeTag = timeTag; - } - - double timeOffset = ((signed int)(timeTag - lastTimeTag)) * 1e-3; - -// std::cout << "\n" << "Recieved MEAS message has " << numMeas << " measurements at " << timeOffset << "\n"; - - for (int i = 0; i < numMeas; i++) - { - unsigned int data = *((unsigned int*) &payload[8 + 4 * i]); - - data &= 0x3fffffff; - - unsigned int dataType = data >> 24; - int dataField = data &= 0x00ffffff; - - dataField <<= 8; //get leading ones - dataField >>= 8; - - E_MEASDataType measDataType = E_MEASDataType::_from_integral(dataType); - - switch (measDataType) - { - default: - { -// std::cout << "\n" << measDataType._to_string(); - break; - } - case E_MEASDataType::GYRO_X: - case E_MEASDataType::GYRO_Y: - case E_MEASDataType::GYRO_Z: - { - double gyro = dataField * P2_12; -// std::cout << "\n" << measDataType._to_string() << " : " << gyro; - - int index = 0; - if (measDataType == +E_MEASDataType::GYRO_X) index = 0; //ubx indices are dumb and not ordered - else if (measDataType == +E_MEASDataType::GYRO_Y) index = 1; - else if (measDataType == +E_MEASDataType::GYRO_Z) index = 2; - - gyroDataMaps[recId][lastTime + timeOffset][index] = gyro; - - break; - } - case E_MEASDataType::ACCL_X: - case E_MEASDataType::ACCL_Y: - case E_MEASDataType::ACCL_Z: - { - double accl = dataField * P2_10; -// std::cout << "\n" << measDataType._to_string() << " : " << accl; - - int index = 0; - if (measDataType == +E_MEASDataType::ACCL_X) index = 0; - else if (measDataType == +E_MEASDataType::ACCL_Y) index = 1; - else if (measDataType == +E_MEASDataType::ACCL_Z) index = 2; - - acclDataMaps[recId][lastTime + timeOffset][index] = accl; - - break; - } - case E_MEASDataType::GYRO_TEMP: - { - double temp = dataField * 1e-2; -// std::cout << "\n" << measDataType._to_string() << " : " << temp; - - tempDataMaps[recId][lastTime + timeOffset] = temp; - - break; - } - } - } + unsigned int timeTag = *((unsigned int*)&payload[0]); + short unsigned int flags = *((short unsigned int*)&payload[4]); + short unsigned int id = *((short unsigned int*)&payload[6]); + + int numMeas = flags >> 11; + + // adjust time tags + if (lastTimeTag == 0) + { + lastTimeTag = timeTag; + } + + double timeOffset = ((signed int)(timeTag - lastTimeTag)) * 1e-3; + + // std::cout << "\n" << "Recieved MEAS message has " << numMeas << " measurements at " << + // timeOffset << "\n"; + + for (int i = 0; i < numMeas; i++) + { + unsigned int data = *((unsigned int*)&payload[8 + 4 * i]); + + data &= 0x3fffffff; + + unsigned int dataType = data >> 24; + int dataField = data &= 0x00ffffff; + + dataField <<= 8; // get leading ones + dataField >>= 8; + + E_MEASDataType measDataType = int_to_enum(dataType); + + switch (measDataType) + { + default: + { + // std::cout << "\n" << enum_to_string(measDataType); + break; + } + case E_MEASDataType::GYRO_X: + case E_MEASDataType::GYRO_Y: + case E_MEASDataType::GYRO_Z: + { + double gyro = dataField * P2_12; + // std::cout << "\n" << enum_to_string(measDataType) << " : " << gyro; + + int index = 0; + if (measDataType == E_MEASDataType::GYRO_X) + index = 0; // ubx indices are dumb and not ordered + else if (measDataType == E_MEASDataType::GYRO_Y) + index = 1; + else if (measDataType == E_MEASDataType::GYRO_Z) + index = 2; + + gyroDataMaps[recId][lastTime + timeOffset][index] = gyro; + + break; + } + case E_MEASDataType::ACCL_X: + case E_MEASDataType::ACCL_Y: + case E_MEASDataType::ACCL_Z: + { + double accl = dataField * P2_10; + // std::cout << "\n" << enum_to_string(measDataType) << " : " << accl; + + int index = 0; + if (measDataType == E_MEASDataType::ACCL_X) + index = 0; + else if (measDataType == E_MEASDataType::ACCL_Y) + index = 1; + else if (measDataType == E_MEASDataType::ACCL_Z) + index = 2; + + acclDataMaps[recId][lastTime + timeOffset][index] = accl; + + break; + } + case E_MEASDataType::GYRO_TEMP: + { + double temp = dataField * 1e-2; + // std::cout << "\n" << enum_to_string(measDataType) << " : " << temp; + + tempDataMaps[recId][lastTime + timeOffset] = temp; + + break; + } + } + } } -#include -void CustomDecoder::decodeEphFrames( - SatSys Sat) +void CustomDecoder::decodeEphFrames(SatSys Sat) { - Eph eph; - bool pass = true; - - if (pass) - { - std::cout << "\n" << "*"; - eph.Sat = Sat; - eph.type = E_NavMsgType::LNAV; - nav.ephMap[eph.Sat][eph.type][eph.toe] = eph; - - -// if (acsConfig.output_decoded_rtcm_json) -// traceBrdcEph(RtcmMessageType::GPS_EPHEMERIS, eph); -// -// if (acsConfig.localMongo.output_rtcm_messages) 11, iode27 -// mongoBrdcEph(eph); - } + Eph eph; + bool pass = true; + + if (pass) + { + std::cout << "\n" + << "*"; + eph.Sat = Sat; + eph.type = E_NavMsgType::LNAV; + nav.ephMap[eph.Sat][eph.type][eph.toe] = eph; + + // if (acsConfig.output_decoded_rtcm_json) + // traceBrdcEph(RtcmMessageType::GPS_EPHEMERIS, eph); + // + // if (acsConfig.localMongo.output_rtcm_messages) 11, iode27 + // mongoBrdcEph(eph); + } } -void CustomDecoder::decodeSFRBX( - vector& payload) +void CustomDecoder::decodeSFRBX(vector& payload) { -// std::cout << "Recieved SFRBX message" << "\n"; - if (payload.size() < 5) - return; - - int gnssId = payload[0]; - int satId = payload[1]; - int frameLen = payload[4]; + // std::cout << "Recieved SFRBX message" << "\n"; + if (payload.size() < 5) + return; - if (frameLen != (payload.size() - 8) / 4.0) - return; + int gnssId = payload[0]; + int satId = payload[1]; + int frameLen = payload[4]; + if (frameLen != (payload.size() - 8) / 4.0) + return; } - diff --git a/src/cpp/common/customDecoder.hpp b/src/cpp/common/customDecoder.hpp index 8242a7751..f593eaf19 100644 --- a/src/cpp/common/customDecoder.hpp +++ b/src/cpp/common/customDecoder.hpp @@ -1,126 +1,136 @@ - #pragma once - // #pragma GCC optimize ("O0") -#include #include +#include +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/streamObs.hpp" -using std::vector; using std::map; - -#include "streamObs.hpp" -#include "gTime.hpp" -#include "enums.h" +using std::vector; struct CustomDecoder : ObsLister { - static map>> gyroDataMaps; - static map>> acclDataMaps; - static map>> tempDataMaps; - - unsigned int lastTimeTag = 0; - GTime lastTime; - - string recId; - - string raw_custom_filename; - - void decodeEphFrames( - SatSys Sat); - - void decodeRAWX( - vector& payload); - - void decodeSFRBX( - vector& payload); - - void decodeMEAS( - vector& payload); - - void decodeRXM( - vector& payload, - unsigned char id) - { -// printf("\nReceived RXM-0x%02x message", id); - switch (id) - { - default: - { - break; - } - case E_RXMId::RAWX: { decodeRAWX (payload); break; } - case E_RXMId::SFRBX: { decodeSFRBX (payload); break; } - } - } - - void decodeESF( - vector& payload, - unsigned char id) - { -// printf("\nReceived ESF-0x%02x message", id); - switch (id) - { - default: - { - break; - } - case E_ESFId::MEAS: { decodeMEAS (payload); break; } - } - } - - void decode( - unsigned char ubxClass, - unsigned char id, - vector& payload) - { -// printf("\nReceived ubx: 0x%02x : 0x%02x > %ld bytes", ubxClass, id, payload.size()); - - switch (ubxClass) - { - default: { break; } - case E_UBXClass::RXM: { decodeRXM(payload, id); break; } - case E_UBXClass::ESF: { decodeESF(payload, id); break; } - } - } - - void recordFrame( - unsigned char ubxClass, - unsigned char id, - vector& data, - unsigned short int crcRead) - { - if (raw_custom_filename.empty()) - { - return; - } - - std::ofstream ofs(raw_custom_filename, std::ofstream::app); - - if (!ofs) - { - return; - } - -// //Write the custom time stamp message. -// RtcmEncoder encoder; -// encoder.rtcmTraceFilename = rtcmTraceFilename; -// -// auto buffer = encoder.encodeTimeStampRTCM(); -// bool write = encoder.encodeWriteMessageToBuffer(buffer); -// -// if (write) -// { -// encoder.encodeWriteMessages(ofs); -// } - - //copy the message to the output file too - unsigned short int payloadLength = data.size(); - - ofs.write((char *)&ubxClass, 1); - ofs.write((char *)&id, 1); - ofs.write((char *)&payloadLength, 2); - ofs.write((char *)data.data(), data.size()); - ofs.write((char *)&crcRead, 3); - } + static map>> gyroDataMaps; + static map>> acclDataMaps; + static map>> tempDataMaps; + + unsigned int lastTimeTag = 0; + GTime lastTime; + + string recId; + + string raw_custom_filename; + + void decodeEphFrames(SatSys Sat); + + void decodeRAWX(vector& payload); + + void decodeSFRBX(vector& payload); + + void decodeMEAS(vector& payload); + + void decodeRXM(vector& payload, unsigned char id) + { + // printf("\nReceived RXM-0x%02x message", id); + switch (id) + { + default: + { + break; + } + case static_cast(E_RXMId::RAWX): + { + decodeRAWX(payload); + break; + } + case static_cast(E_RXMId::SFRBX): + { + decodeSFRBX(payload); + break; + } + } + } + + void decodeESF(vector& payload, unsigned char id) + { + // printf("\nReceived ESF-0x%02x message", id); + switch (id) + { + default: + { + break; + } + case static_cast(E_ESFId::MEAS): + { + decodeMEAS(payload); + break; + } + } + } + + void decode(unsigned char ubxClass, unsigned char id, vector& payload) + { + // printf("\nReceived ubx: 0x%02x : 0x%02x > %ld bytes", ubxClass, id, payload.size()); + + switch (ubxClass) + { + default: + { + break; + } + case static_cast(E_UBXClass::RXM): + { + decodeRXM(payload, id); + break; + } + case static_cast(E_UBXClass::ESF): + { + decodeESF(payload, id); + break; + } + } + } + + void recordFrame( + unsigned char ubxClass, + unsigned char id, + vector& data, + unsigned short int crcRead + ) + { + if (raw_custom_filename.empty()) + { + return; + } + + std::ofstream ofs(raw_custom_filename, std::ofstream::app); + + if (!ofs) + { + return; + } + + // //Write the custom time stamp message. + // RtcmEncoder encoder; + // encoder.rtcmTraceFilename = rtcmTraceFilename; + // + // auto buffer = encoder.encodeTimeStampRTCM(); + // bool write = encoder.encodeWriteMessageToBuffer(buffer); + // + // if (write) + // { + // encoder.encodeWriteMessages(ofs); + // } + + // copy the message to the output file too + unsigned short int payloadLength = data.size(); + + ofs.write((char*)&ubxClass, 1); + ofs.write((char*)&id, 1); + ofs.write((char*)&payloadLength, 2); + ofs.write((char*)data.data(), data.size()); + ofs.write((char*)&crcRead, 3); + } }; diff --git a/src/cpp/common/debug.cpp b/src/cpp/common/debug.cpp index 767c3e1c2..cfef43ff5 100644 --- a/src/cpp/common/debug.cpp +++ b/src/cpp/common/debug.cpp @@ -1,275 +1,292 @@ +#pragma GCC optimize("O0") -#pragma GCC optimize ("O0") - - - +#include "common/debug.hpp" +#include #include #include +#include "3rdparty/iers2010/iers2010.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/algebraTrace.hpp" +#include "common/attitude.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/erp.hpp" +#include "common/linearCombo.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/rinex.hpp" +#include "common/rtcmEncoder.hpp" +#include "common/rtsSmoothing.hpp" +#include "common/sinex.hpp" +#include "common/ssr.hpp" +#include "common/streamFile.hpp" +#include "common/streamObs.hpp" +#include "common/streamParser.hpp" +#include "common/streamRtcm.hpp" +#include "common/streamSerial.hpp" +#include "common/streamUbx.hpp" +#include "common/tides.hpp" +#include "common/trace.hpp" +#include "common/ubxDecoder.hpp" +#include "iono/geomagField.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/orbitProp.hpp" +#include "orbprop/planets.hpp" +#include "pea/minimumConstraints.hpp" +using iers2010::hisp::ntin; -#include "minimumConstraints.hpp" -#include "eigenIncluder.hpp" -#include "rtsSmoothing.hpp" -#include "observations.hpp" -#include "algebraTrace.hpp" -#include "linearCombo.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "debug.hpp" -#include "sinex.hpp" -#include "trace.hpp" -#include "enums.h" - -std::random_device randoDev; -std::mt19937 randoGen(randoDev()); -std::normal_distribution rando(0, 15); - -void minconglob( - Trace& trace, - KFState& kfStateStations, - bool commentSinex = false); - -void minimumTest( - Trace& trace) -{ - GTime gtime; - gtime++; - trace << "\n"; - map pointMap; - Vector3d p; - - if (0) - { - pointMap["G10"] = {+1, 0, 0}; - pointMap["G11"] = {-1, 0, 0}; - pointMap["G12"] = {0, +1, 0}; - pointMap["G13"] = {0, -1, 0}; - pointMap["G14"] = {0, 0, +1}; - pointMap["G15"] = {0, 0, -1}; - } - else - { - pointMap["G01"] = {+1, 0, 0}; - pointMap["G02"] = {+2, 0, 0}; - pointMap["G03"] = {+3, 0, 0.0001}; - pointMap["G04"] = {+4, 0.0002, 0}; - pointMap["G05"] = {+5, 0, 0}; - pointMap["G06"] = {+6, 0, 0}; - } - - ERPValues erpv = getErp(nav.erp, gtime); - - FrameSwapper frameSwapper(gtime, erpv); - -// pointMap["STN6"] = {1/sqrt(3), 1/sqrt(3), 1/sqrt(3)}; -// pointMap["DONT"] = {-1/sqrt(2), -1/sqrt(2), 0}; -// -// pointMap["STN4"] *= 0.997; -// pointMap["STN5"] *= 0.998; -// pointMap["STN6"] *= 0.999; - - - acsConfig.output_residuals = true; - KFState kfStateStations; - kfStateStations.output_residuals = true; - - map receiverMap; - - - KFKey offSetKey; - offSetKey.type = KF::CODE_BIAS; - - //generate fake station/orbit data - { - KFMeasEntryList stationEntries; - for (auto orbit : {/*true, */false}) - for (auto [id, a] : pointMap) - { - if (orbit) a *= 26000000; // * sqrt(2); - else a *= 6000000; // * sqrt(2); -// a += Vector3d(0.0000001,0.0000001,0.0000001); - - auto& satNav = nav.satNavMap[SatSys(id.c_str())]; - auto& rec = receiverMap[id + "S"]; - - if (orbit) - { - VectorEcef ecef = a; - satNav.aprioriPos = frameSwapper(ecef); - } - else - { - rec.id = id + "S"; - - for (int i = 0; i < 3; i++) - { - rec.snx.pos[i] = a[i]; - rec.aprioriVar(i) = 1; - } - - rec.minconApriori = a; - } - - double angz = 0.000000001 * 5000/2063; - Matrix3d rotz; - rotz << - +cos(angz), +sin(angz), 0, - -sin(angz), +cos(angz), 0, - 0, 0, 1; - - double angx = 0.000000003 * 4000 / 6188; - Matrix3d rotx; - rotx << - 1, 0, 0, - 0, +cos(angx), +sin(angx), - 0, -sin(angx), +cos(angx); - - Vector3d p = a; -// p = rotz * p; -// p = rotx * p; - p += Vector3d{0.001,0.0025,0.003}; - -// p = p * 1.0000001; -// trace << "\t" << p(0) << "\t" << p(1) << "\t" << p(2) << "\n"; - - - - VectorEcef posEcef = p; - VectorEci posEci = frameSwapper(posEcef); - - Vector3d v = posEci.cross(Vector3d::UnitZ())/10000 + stationEntries.size() * Vector3d::UnitZ()*10; - -// posEci += v * 0.0000034; - for (int i = 0; i < 3; i++) - { - KFMeasEntry meas(&kfStateStations); - - auto& rec = receiverMap[id + "S"]; - KFKey kfKey; - if (orbit) {kfKey.type = KF::ORBIT; kfKey.Sat = SatSys(id.c_str()); } - else {kfKey.type = KF::REC_POS; kfKey.str = id + "S"; } - - kfKey.rec_ptr = &rec; - kfKey.num = i; - - meas.addDsgnEntry(kfKey, 1); - - - meas.addDsgnEntry(offSetKey, 1, {.x = 0, .P = 200}); - - if (orbit) - kfStateStations.addKFState((kfKey.num +=3, kfKey), {.x=v(i), .P=1}); - - kfKey.type = KF::REC_SYS_BIAS; - - meas.addDsgnEntry(kfKey, 1, {.x = 0, .P = 0.2}); - - if (orbit) meas.setValue(posEci (i)); - else meas.setValue(posEcef (i)); - - if (stationEntries.size() < 3) - meas.setNoise(5); - else - meas.setNoise(0.001); - - stationEntries.push_back(meas); - } - } - - if (1) - { - KFMeasEntry dummyMeas(&kfStateStations); - dummyMeas.setValue(1); - dummyMeas.setNoise(1); - dummyMeas.addDsgnEntry(offSetKey, 1); - stationEntries.push_back(dummyMeas); - } - - //add process noise to existing states as per their initialisations. - kfStateStations.stateTransition(trace, gtime); - - KFMeas combinedMeas(kfStateStations, stationEntries); - - /* network parameter estimation */ - if (kfStateStations.lsqRequired) - { - trace << "\n" << "-------DOING LEAST SQUARES--------"; - kfStateStations.leastSquareInitStates(trace, combinedMeas, true); - } - - kfStateStations.filterKalman(trace, combinedMeas, "", true); - - } - -// kfStateStations.P(3,3) = 0.4; - -// kfStateStations.P(40,44) = 1.9; -// kfStateStations.P(44,40) = 1.9; -// kfStateStations.P(44,44) = 2; -// kfStateStations.P(40,40) = 2; -// -// kfStateStations.P(13,45) = 1.9; -// kfStateStations.P(45,30) = 1.9; //this fails rigidity test when the orbits constrained is false -// kfStateStations.P(13,13) = 2; -// kfStateStations.P(45,45) = 2; - -// kfStateStations.P(1,9) = 30; -// kfStateStations.P(9,1) = 30; - kfStateStations.outputStates(trace); -// sinexPostProcessing(kfStateStations.time, receiverMap, kfStateStations); - - // kfStateStations.outputCorrelations(trace); - - trace << "\n" << "-------NEW --------"; - { - auto state = kfStateStations; - mincon(trace, state); - state.outputStates(trace, "CONSTRAINED NEW"); - } - - for (auto type : {KF::REC_POS, KF::ORBIT}) - for (auto [kfKey, index] : kfStateStations.kfIndexMap) - { - if (kfKey.type != type) - { - continue; - } - if (kfKey.num != 0) - { - continue; - } - - Vector3d point1 = kfStateStations.x.segment(index, 3) + pointMap["STN" + std::to_string(index/3)]; +std::random_device randoDev; +std::mt19937 randoGen(randoDev()); +std::normal_distribution rando(0, 15); - for (auto [kfKey, index] : kfStateStations.kfIndexMap) - { - if (kfKey.type != type) - { - continue; - } - if (kfKey.num != 0) - { - continue; - } - - Vector3d point2 = kfStateStations.x.segment(index, 3) + pointMap["STN" + std::to_string(index/3)]; - - double exp; - if (type == KF::REC_POS) exp = 6000000 * 2; - else exp = 26000000 * 2; -// std::cout << "\n" << "Distance: " << ((point1-point2).norm() - exp); - } - } -// kfStateStations.outputStates(trace); +void minconglob(Trace& trace, KFState& kfStateStations, bool commentSinex = false); -// kfStateStations.outputCorrelations(trace); +void minimumTest(Trace& trace) +{ + GTime gtime; + gtime++; + trace << "\n"; + map pointMap; + Vector3d p; + + if (0) + { + pointMap["G10"] = {+1, 0, 0}; + pointMap["G11"] = {-1, 0, 0}; + pointMap["G12"] = {0, +1, 0}; + pointMap["G13"] = {0, -1, 0}; + pointMap["G14"] = {0, 0, +1}; + pointMap["G15"] = {0, 0, -1}; + } + else + { + pointMap["G01"] = {+1, 0, 0}; + pointMap["G02"] = {+2, 0, 0}; + pointMap["G03"] = {+3, 0, 0.0001}; + pointMap["G04"] = {+4, 0.0002, 0}; + pointMap["G05"] = {+5, 0, 0}; + pointMap["G06"] = {+6, 0, 0}; + } + + ERPValues erpv = getErp(nav.erp, gtime); + + FrameSwapper frameSwapper(gtime, erpv); + + // pointMap["STN6"] = {1/sqrt(3), 1/sqrt(3), 1/sqrt(3)}; + // pointMap["DONT"] = {-1/sqrt(2), -1/sqrt(2), 0}; + // + // pointMap["STN4"] *= 0.997; + // pointMap["STN5"] *= 0.998; + // pointMap["STN6"] *= 0.999; + + acsConfig.output_residuals = true; + KFState kfStateStations; + kfStateStations.output_residuals = true; + + map receiverMap; + + KFKey offSetKey; + offSetKey.type = KF::CODE_BIAS; + + // generate fake station/orbit data + { + KFMeasEntryList stationEntries; + for (auto orbit : {/*true, */ false}) + for (auto [id, a] : pointMap) + { + if (orbit) + a *= 26000000; // * sqrt(2); + else + a *= 6000000; // * sqrt(2); + // a += Vector3d(0.0000001,0.0000001,0.0000001); + + auto& satNav = nav.satNavMap[SatSys(id.c_str())]; + auto& rec = receiverMap[id + "S"]; + + if (orbit) + { + VectorEcef ecef = a; + satNav.aprioriPos = frameSwapper(ecef); + } + else + { + rec.id = id + "S"; + + for (int i = 0; i < 3; i++) + { + rec.snx.pos[i] = a[i]; + rec.aprioriPosVar(i) = 1; + } + + rec.minconApriori = a; + } + + double angz = 0.000000001 * 5000 / 2063; + Matrix3d rotz; + rotz << +cos(angz), +sin(angz), 0, -sin(angz), +cos(angz), 0, 0, 0, 1; + + double angx = 0.000000003 * 4000 / 6188; + Matrix3d rotx; + rotx << 1, 0, 0, 0, +cos(angx), +sin(angx), 0, -sin(angx), +cos(angx); + + Vector3d p = a; + // p = rotz * p; + // p = rotx * p; + p += Vector3d{0.001, 0.0025, 0.003}; + + // p = p * 1.0000001; + // trace << "\t" << p(0) << "\t" << p(1) << "\t" << p(2) << "\n"; + + VectorEcef posEcef = p; + VectorEci posEci = frameSwapper(posEcef); + + Vector3d v = posEci.cross(Vector3d::UnitZ()) / 10000 + + stationEntries.size() * Vector3d::UnitZ() * 10; + + // posEci += v * 0.0000034; + for (int i = 0; i < 3; i++) + { + KFMeasEntry meas(&kfStateStations); + + auto& rec = receiverMap[id + "S"]; + KFKey kfKey; + if (orbit) + { + kfKey.type = KF::ORBIT; + kfKey.Sat = SatSys(id.c_str()); + } + else + { + kfKey.type = KF::REC_POS; + kfKey.str = id + "S"; + } + + kfKey.rec_ptr = &rec; + kfKey.num = i; + + meas.addDsgnEntry(kfKey, 1); + + meas.addDsgnEntry(offSetKey, 1, {.x = 0, .P = 200}); + + if (orbit) + kfStateStations.addKFState((kfKey.num += 3, kfKey), {.x = v(i), .P = 1}); + + kfKey.type = KF::REC_SYS_BIAS; + + meas.addDsgnEntry(kfKey, 1, {.x = 0, .P = 0.2}); + + if (orbit) + meas.setValue(posEci(i)); + else + meas.setValue(posEcef(i)); + + if (stationEntries.size() < 3) + meas.setNoise(5); + else + meas.setNoise(0.001); + + stationEntries.push_back(meas); + } + } + + if (1) + { + KFMeasEntry dummyMeas(&kfStateStations); + dummyMeas.setValue(1); + dummyMeas.setNoise(1); + dummyMeas.addDsgnEntry(offSetKey, 1); + stationEntries.push_back(dummyMeas); + } + + // add process noise to existing states as per their initialisations. + kfStateStations.stateTransition(trace, gtime); + + KFMeas combinedMeas(kfStateStations, stationEntries); + + /* network parameter estimation */ + if (kfStateStations.lsqRequired) + { + trace << "\n" + << "-------DOING LEAST SQUARES--------"; + kfStateStations.leastSquareInitStates(trace, combinedMeas, "", true); + } + + kfStateStations.filterKalman(trace, combinedMeas, "", true); + } + + // kfStateStations.P(3,3) = 0.4; + + // kfStateStations.P(40,44) = 1.9; + // kfStateStations.P(44,40) = 1.9; + // kfStateStations.P(44,44) = 2; + // kfStateStations.P(40,40) = 2; + // + // kfStateStations.P(13,45) = 1.9; + // kfStateStations.P(45,30) = 1.9; //this fails rigidity test when the orbits + // constrained is false kfStateStations.P(13,13) = 2; kfStateStations.P(45,45) = 2; + + // kfStateStations.P(1,9) = 30; + // kfStateStations.P(9,1) = 30; + kfStateStations.outputStates(trace); + // sinexPostProcessing(kfStateStations.time, receiverMap, kfStateStations); + + // kfStateStations.outputCorrelations(trace); + + trace << "\n" + << "-------NEW --------"; + { + auto state = kfStateStations; + mincon(trace, state); + state.outputStates(trace, "CONSTRAINED NEW"); + } + + for (auto type : {KF::REC_POS, KF::ORBIT}) + for (auto [kfKey, index] : kfStateStations.kfIndexMap) + { + if (kfKey.type != type) + { + continue; + } + if (kfKey.num != 0) + { + continue; + } + + Vector3d point1 = + kfStateStations.x.segment(index, 3) + pointMap["STN" + std::to_string(index / 3)]; + + for (auto [kfKey, index] : kfStateStations.kfIndexMap) + { + if (kfKey.type != type) + { + continue; + } + if (kfKey.num != 0) + { + continue; + } + + Vector3d point2 = kfStateStations.x.segment(index, 3) + + pointMap["STN" + std::to_string(index / 3)]; + + double exp; + if (type == KF::REC_POS) + exp = 6000000 * 2; + else + exp = 26000000 * 2; + // std::cout << "\n" << "Distance: " << ((point1-point2).norm() - exp); + } + } + // kfStateStations.outputStates(trace); + + // kfStateStations.outputCorrelations(trace); } #if 0 -#include "sinex.hpp" #if 0 void outputMeas( Trace& trace, ///< Trace to output to @@ -361,9 +378,6 @@ void testOutlierDetection() #endif -#include "acsConfig.hpp" -#include "mongoWrite.hpp" - void rtsBump() { KFState kfState; @@ -445,7 +459,7 @@ void rtsBump() kfState.outputStates(std::cout); - if (acsConfig.output_mongo_states) + if (acsConfig.output_Constants::Mongo::STATE_DBs) { mongoStates(kfState); } @@ -456,28 +470,18 @@ void rtsBump() rtsSmoothing(kfState); } - - - -#include "streamSerial.hpp" -#include "streamFile.hpp" -#include "streamRtcm.hpp" -#include "streamObs.hpp" -#include "streamUbx.hpp" - #endif -#include "mongoWrite.hpp" - - -//#include "orbitProp.hpp" -//extern map orbitPropagatorMap; +// #include "orbprop/orbitProp.hpp" +// extern map orbitPropagatorMap; /** Compare the orbital states created by pseudo-linear state transitions with the original values. - * The pseudo-linear state transition in the filter (STM + adjustment) is mathematically equivalent to setting a state value directly, - * but numerical precision in a computer does not allow 100% correspondence - this checks its mostly working + * The pseudo-linear state transition in the filter (STM + adjustment) is mathematically equivalent +to setting a state value directly, + * but numerical precision in a computer does not allow 100% correspondence - this checks its mostly +working // */ -//void checkOrbits( +// void checkOrbits( // Trace& trace, // KFState& kfState) //{ @@ -504,36 +508,37 @@ void rtsBump() // Vector6d deltaState = inertialState // - subState.x.head(6); // -// trace << "\norbitPropagator.base.inertialState" << inertialState .transpose().format(HeavyFmt); -// trace << "\nsubState.x.head(6) " << subState.x.head(6) .transpose().format(HeavyFmt); -// trace << "\ndeltaState " << deltaState .transpose().format(HeavyFmt); +// trace << "\norbitPropagator.base.inertialState" << inertialState +//.transpose().format(HeavyFmt); trace << "\nsubState.x.head(6) " << +// subState.x.head(6) .transpose().format(HeavyFmt); trace << "\ndeltaState " << +// deltaState .transpose().format(HeavyFmt); // -// MatrixXd transition = orbitPropagator.states.posVelSTM; +// MatrixXd transition = orbitPropagator.states.posVelSTM; // -// //Convert the absolute transition matrix to an identity matrix (already populated elsewhere) and stm-per-time matrix -// transition -= MatrixXd::Identity(transition.rows(), transition.cols()); +// //Convert the absolute transition matrix to an identity matrix (already populated +// elsewhere) and stm-per-time matrix transition -= MatrixXd::Identity(transition.rows(), +// transition.cols()); // -// MatrixXd thingy = transition * subState.P * transition.transpose(); +// MatrixXd thingy = transition * subState.P * transition.transpose(); // -// // std::cout << "\ntransition\n" << transition << "\n"; -// // std::cout << "\nthingy\n" << thingy << "\n"; -// // if (0) -// for (auto& [key1, index1] : subState.kfIndexMap) -// for (auto& [key2, index2] : subState.kfIndexMap) -// { -// int index1a = kfState.kfIndexMap[key1]; -// int index2a = kfState.kfIndexMap[key2]; +// // std::cout << "\ntransition\n" << transition << "\n"; +// // std::cout << "\nthingy\n" << thingy << "\n"; +// // if (0) +// for (auto& [key1, index1] : subState.kfIndexMap) +// for (auto& [key2, index2] : subState.kfIndexMap) +// { +// int index1a = kfState.kfIndexMap[key1]; +// int index2a = kfState.kfIndexMap[key2]; // -// kfState.P(index1a, index2a) += thingy(index1, index2); -// // kfState.addKFState(key, init); -// } +// kfState.P(index1a, index2a) += thingy(index1, index2); +// // kfState.addKFState(key, init); +// } // } - // MatrixXd transition = orbitPropagator.states.posVelSTM; // -// //Convert the absolute transition matrix to an identity matrix (already populated elsewhere) and stm-per-time matrix -// transition -= MatrixXd::Identity(transition.rows(), transition.cols()); +// //Convert the absolute transition matrix to an identity matrix (already populated elsewhere) and +// stm-per-time matrix transition -= MatrixXd::Identity(transition.rows(), transition.cols()); // transition /= 900; // // MatrixXd thingy = transition * subState.P * transition.transpose(); @@ -565,7 +570,8 @@ void rtsBump() // //nothing to be done except for state types specified below // continue; // } -// case KF::SRP_SCALE: { acceleration = outputsMap["accSrp"]; break; } //this should be whatever was used in the propagator above, for this srp component only +// case KF::SRP_SCALE: { acceleration = outputsMap["accSrp"]; break; } +// //this should be whatever was used in the propagator above, for this srp component only // } // // std::cout << "\n" << "add acceleration: " << acceleration.transpose() << "\n"; @@ -584,385 +590,466 @@ void rtsBump() // } // } - void timecheck() { - PTime now = timeGet(); - GTime gNow = now; - UtcTime uNow = gNow; + PTime now = timeGet(); + GTime gNow = now; + UtcTime uNow = gNow; - std::cout << "now: " << now. bigTime << "\n"; - std::cout << "gNow: " << gNow. bigTime << "\n"; - std::cout << "uNow: " << uNow. bigTime << "\n"; + std::cout << "now: " << now.bigTime << "\n"; + std::cout << "gNow: " << gNow.bigTime << "\n"; + std::cout << "uNow: " << uNow.bigTime << "\n"; } void debugTime() { - auto utcNow = boost::posix_time::microsec_clock::universal_time(); - GTime gTime = timeGet(); - - std::cout << std::setprecision(1) << std::fixed << "\n"; - std::cout << "universal_time(): " << utcNow << "\n"; - std::cout << "timeGet(): " << gTime.bigTime << " " << gTime.to_string(1) << "\n"; - - PTime pTime = gTime; - auto bTime = boost::posix_time::from_time_t((time_t)pTime.bigTime); - UtcTime utcTime = gTime; - GEpoch gEpoch = gTime; - UYds uYds = gTime; - - GWeek gWeek = gTime; - GTow gTow = gTime; - - BWeek bWeek = gTime; - BTow bTow = gTime; - - RTod rTod = gTime; - - std::cout << std::setfill('0') << "\n"; - std::cout << "GTime to anything:" << "\n"; - std::cout << "GTime to PTime: " << pTime .bigTime << " " << bTime << "\n"; - std::cout << "GTime to UtcTime: " << utcTime.bigTime << " " << utcTime.to_string(1) << "\n"; - std::cout << "GTime to GEpoch: " << (int)gEpoch.year << "-" << std::setw(2) << (int)gEpoch.month << "-" << std::setw(2) << (int)gEpoch.day << " " << std::setw(2) << (int)gEpoch.hour << ":" << std::setw(2) << (int)gEpoch.min << ":" << std::setw(4) << gEpoch.sec << "\n"; - std::cout << "GTime to UYds: " << (int)uYds.year << " " << (int)uYds.doy << " " << uYds.sod << "\n"; - std::cout << "GTime to GWeek: " << gWeek.val << "\n"; - std::cout << "GTime to GTow: " << gTow. val << "\n"; - std::cout << "GTime to BWeek: " << bWeek.val << "\n"; - std::cout << "GTime to BTow: " << bTow. val << "\n"; - std::cout << "GTime to RTod: " << rTod. val << "\n"; - - - GTime gTimeP = pTime; - GTime gTimeU = utcTime; - GTime gTimeE = gEpoch; - GTime gTimeY = uYds; - - std::cout << "\n"; - std::cout << "anything to GTime:" << "\n"; - std::cout << "PTime to GTime: " << gTimeP.bigTime << " " << gTimeP.to_string(1) << "\n"; - std::cout << "UtcTime to GTime: " << gTimeU.bigTime << " " << gTimeU.to_string(1) << "\n"; - std::cout << "GEpoch to GTime: " << gTimeE.bigTime << " " << gTimeE.to_string(1) << "\n"; - std::cout << "UYds to GTime: " << gTimeY.bigTime << " " << gTimeY.to_string(1) << "\n"; - - GTime gTimeG = GTime(gWeek, gTow); - GTime gTimeB = GTime(bWeek, bTow); - - std::cout << "GTime(GWeek, GTow): " << gTimeG.bigTime << " " << gTimeG.to_string(1) << "\n"; - std::cout << "GTime(BWeek, BTow): " << gTimeB.bigTime << " " << gTimeB.to_string(1) << "\n"; - - GTime nearTime= timeGet(); - gTimeG = GTime(gTow, nearTime); - gTimeB = GTime(bTow, nearTime); - GTime gTimeR = GTime(rTod, nearTime); - - std::cout << "GTime(GTow, GTime): " << gTimeG.bigTime << " " << gTimeG.to_string(1) << "\n"; - std::cout << "GTime(BTow, GTime): " << gTimeB.bigTime << " " << gTimeB.to_string(1) << "\n"; - std::cout << "GTime(RTod, GTime): " << gTimeR.bigTime << " " << gTimeR.to_string(1) << "\n"; - - - double ep[6]; - - std::cout << "\n"; - std::cout << "GTime to epoch to GTime:" << "\n"; - time2epoch(gTime, ep, E_TimeSys::GPST); std::cout << "GTime to GPST epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; - GTime gTimeEpochGPS = epoch2time(ep, E_TimeSys::GPST); std::cout << "GPST epoch to GTime: " << gTimeEpochGPS.to_string(1) << "\n"; - time2epoch(gTime, ep, E_TimeSys::GLONASST); std::cout << "GTime to GLONASST epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; - GTime gTimeEpochGLO = epoch2time(ep, E_TimeSys::GLONASST); std::cout << "GLONASST epoch to GTime: " << gTimeEpochGLO.to_string(1) << "\n"; - time2epoch(gTime, ep, E_TimeSys::GST); std::cout << "GTime to GST epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; - GTime gTimeEpochGST = epoch2time(ep, E_TimeSys::GST); std::cout << "GST epoch to GTime: " << gTimeEpochGST.to_string(1) << "\n"; - time2epoch(gTime, ep, E_TimeSys::BDT); std::cout << "GTime to BDT epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; - GTime gTimeEpochBDT = epoch2time(ep, E_TimeSys::BDT); std::cout << "BDT epoch to GTime: " << gTimeEpochBDT.to_string(1) << "\n"; - time2epoch(gTime, ep, E_TimeSys::QZSST); std::cout << "GTime to QZSST epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; - GTime gTimeEpochQZS = epoch2time(ep, E_TimeSys::QZSST); std::cout << "QZSST epoch to GTime: " << gTimeEpochQZS.to_string(1) << "\n"; - time2epoch(gTime, ep, E_TimeSys::TAI); std::cout << "GTime to TAI epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; - GTime gTimeEpochTAI = epoch2time(ep, E_TimeSys::TAI); std::cout << "TAI epoch to GTime: " << gTimeEpochTAI.to_string(1) << "\n"; - time2epoch(gTime, ep, E_TimeSys::UTC); std::cout << "GTime to UTC epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; - GTime gTimeEpochUTC = epoch2time(ep, E_TimeSys::UTC); std::cout << "UTC epoch to GTime: " << gTimeEpochUTC.to_string(1) << "\n"; - - - double yds[6]; - - std::cout << "\n"; - std::cout << "GTime to yds to GTime:" << "\n"; - time2yds(gTime, yds, E_TimeSys::GPST); std::cout << "GTime to GPST yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] << "\n"; - GTime gTimeYdsGPS = yds2time(yds, E_TimeSys::GPST); std::cout << "GPST yds to GTime: " << gTimeYdsGPS.to_string(1) << "\n"; - time2yds(gTime, yds, E_TimeSys::GLONASST); std::cout << "GTime to GLONASST yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] << "\n"; - GTime gTimeYdsGLO = yds2time(yds, E_TimeSys::GLONASST); std::cout << "GLONASST yds to GTime: " << gTimeYdsGLO.to_string(1) << "\n"; - time2yds(gTime, yds, E_TimeSys::GST); std::cout << "GTime to GST yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] << "\n"; - GTime gTimeYdsGST = yds2time(yds, E_TimeSys::GST); std::cout << "GST yds to GTime: " << gTimeYdsGST.to_string(1) << "\n"; - time2yds(gTime, yds, E_TimeSys::BDT); std::cout << "GTime to BDT yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] << "\n"; - GTime gTimeYdsBDT = yds2time(yds, E_TimeSys::BDT); std::cout << "BDT yds to GTime: " << gTimeYdsBDT.to_string(1) << "\n"; - time2yds(gTime, yds, E_TimeSys::QZSST); std::cout << "GTime to QZSST yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] << "\n"; - GTime gTimeYdsQZS = yds2time(yds, E_TimeSys::QZSST); std::cout << "QZSST yds to GTime: " << gTimeYdsQZS.to_string(1) << "\n"; - time2yds(gTime, yds, E_TimeSys::TAI); std::cout << "GTime to TAI yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] << "\n"; - GTime gTimeYdsTAI = yds2time(yds, E_TimeSys::TAI); std::cout << "TAI yds to GTime: " << gTimeYdsTAI.to_string(1) << "\n"; - time2yds(gTime, yds, E_TimeSys::UTC); std::cout << "GTime to UTC yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] << "\n"; - GTime gTimeYdsUTC = yds2time(yds, E_TimeSys::UTC); std::cout << "UTC yds to GTime: " << gTimeYdsUTC.to_string(1) << "\n"; - - - GTow nearGTow= 1; - nearTime= GTime(gWeek, nearGTow); - gTow = 604800 - 1.1; - gTimeG = GTime(gTow, nearTime); - - std::cout << std::setfill(' ') << "\n"; - std::cout << "Week/Day roll-over:" << "\n"; - std::cout << "nearGTow: " << std::setw(8) << nearGTow.val << "\tnearTime: " << nearTime.to_string(1) << "\n"; - std::cout << "gTow: " << std::setw(8) << gTow. val << "\tGTime(gTow, nearTime): " << gTimeG. to_string(1) << "\n"; - - nearGTow= 604800 - 1; - nearTime= GTime(gWeek, nearGTow); - gTow = 1.1; - gTimeG = GTime(gTow, nearTime); - - std::cout << "nearGTow: " << std::setw(8) << nearGTow.val << "\tnearTime: " << nearTime.to_string(1) << "\n"; - std::cout << "gTow: " << std::setw(8) << gTow. val << "\tGTime(gTow, nearTime): " << gTimeG. to_string(1) << "\n"; - - BTow nearBTow= 1; - nearTime= GTime(bWeek, nearBTow); - bTow = 604800 - 1.1; - gTimeB = GTime(bTow, nearTime); - - std::cout << "nearBTow: " << std::setw(8) << nearBTow.val << "\tnearTime: " << nearTime.to_string(1) << "\n"; - std::cout << "bTow: " << std::setw(8) << bTow. val << "\tGTime(bTow, nearTime): " << gTimeB. to_string(1) << "\n"; - - nearBTow= 604800 - 1; - nearTime= GTime(bWeek, nearBTow); - bTow = 1.1; - gTimeB = GTime(bTow, nearTime); - - std::cout << "nearBTow: " << std::setw(8) << nearBTow.val << "\tnearTime: " << nearTime.to_string(1) << "\n"; - std::cout << "bTow: " << std::setw(8) << bTow. val << "\tGTime(bTow, nearTime): " << gTimeB. to_string(1) << "\n"; - - UYds nearYds = uYds; - nearYds.sod = 86400 - 10800 + 1; - nearTime= nearYds; - RTod nearTod = nearTime; - rTod = 86400 - 1.1; - gTimeR = GTime(rTod, nearTime); - - std::cout << "nearSod: " << std::setw(8) << nearYds. sod << "\tnearTime: " << nearTime.to_string(1) << "\tnearTod: " << nearTod. val << "\n"; - std::cout << "rTod: " << std::setw(8) << rTod. val << "\tGTime(rTod, nearTime): " << gTimeR. to_string(1) << "\n"; - - nearYds.sod = 86400 - 10800 - 1; - nearTime= nearYds; - nearTod = nearTime; - rTod = 1.1; - gTimeR = GTime(rTod, nearTime); - - std::cout << "nearSod: " << std::setw(8) << nearYds. sod << "\tnearTime: " << nearTime.to_string(1) << "\tnearTod: " << nearTod. val << "\n"; - std::cout << "rTod: " << std::setw(8) << rTod. val << "\tGTime(rTod, nearTime): " << gTimeR. to_string(1) << "\n"; - - gEpoch = {2017, 1, 1, 0, 0, 17.9}; - gTimeE = gEpoch; - utcTime = gTimeE; - gTimeU = utcTime; - - std::cout << "\n"; - std::cout << "Leap second roll-over:" << "\n"; - std::cout << "GTime: " << gTimeE. bigTime << " " << gTimeE .to_string(1) << "\n"; - std::cout << "GTime to UtcTime: " << utcTime.bigTime << " " << utcTime.to_string(1) << "\n"; - std::cout << "UtcTime to GTime: " << gTimeU. bigTime << " " << gTimeU .to_string(1) << "\n"; + auto utcNow = boost::posix_time::microsec_clock::universal_time(); + GTime gTime = timeGet(); + + std::cout << std::setprecision(1) << std::fixed << "\n"; + std::cout << "universal_time(): " << utcNow << "\n"; + std::cout << "timeGet(): " << gTime.bigTime << " " << gTime.to_string(1) << "\n"; + + PTime pTime = gTime; + auto bTime = boost::posix_time::from_time_t((time_t)pTime.bigTime); + UtcTime utcTime = gTime; + GEpoch gEpoch = gTime; + UYds uYds = gTime; + + GWeek gWeek = gTime; + GTow gTow = gTime; + + BWeek bWeek = gTime; + BTow bTow = gTime; + + RTod rTod = gTime; + + std::cout << std::setfill('0') << "\n"; + std::cout << "GTime to anything:" << "\n"; + std::cout << "GTime to PTime: " << pTime.bigTime << " " << bTime << "\n"; + std::cout << "GTime to UtcTime: " << utcTime.bigTime << " " << utcTime.to_string(1) << "\n"; + std::cout << "GTime to GEpoch: " << (int)gEpoch.year << "-" << std::setw(2) + << (int)gEpoch.month << "-" << std::setw(2) << (int)gEpoch.day << " " << std::setw(2) + << (int)gEpoch.hour << ":" << std::setw(2) << (int)gEpoch.min << ":" << std::setw(4) + << gEpoch.sec << "\n"; + std::cout << "GTime to UYds: " << (int)uYds.year << " " << (int)uYds.doy << " " << uYds.sod + << "\n"; + std::cout << "GTime to GWeek: " << gWeek.val << "\n"; + std::cout << "GTime to GTow: " << gTow.val << "\n"; + std::cout << "GTime to BWeek: " << bWeek.val << "\n"; + std::cout << "GTime to BTow: " << bTow.val << "\n"; + std::cout << "GTime to RTod: " << rTod.val << "\n"; + + GTime gTimeP = pTime; + GTime gTimeU = utcTime; + GTime gTimeE = gEpoch; + GTime gTimeY = uYds; + + std::cout << "\n"; + std::cout << "anything to GTime:" << "\n"; + std::cout << "PTime to GTime: " << gTimeP.bigTime << " " << gTimeP.to_string(1) << "\n"; + std::cout << "UtcTime to GTime: " << gTimeU.bigTime << " " << gTimeU.to_string(1) << "\n"; + std::cout << "GEpoch to GTime: " << gTimeE.bigTime << " " << gTimeE.to_string(1) << "\n"; + std::cout << "UYds to GTime: " << gTimeY.bigTime << " " << gTimeY.to_string(1) << "\n"; + + GTime gTimeG = GTime(gWeek, gTow); + GTime gTimeB = GTime(bWeek, bTow); + + std::cout << "GTime(GWeek, GTow): " << gTimeG.bigTime << " " << gTimeG.to_string(1) << "\n"; + std::cout << "GTime(BWeek, BTow): " << gTimeB.bigTime << " " << gTimeB.to_string(1) << "\n"; + + GTime nearTime = timeGet(); + gTimeG = GTime(gTow, nearTime); + gTimeB = GTime(bTow, nearTime); + GTime gTimeR = GTime(rTod, nearTime); + + std::cout << "GTime(GTow, GTime): " << gTimeG.bigTime << " " << gTimeG.to_string(1) << "\n"; + std::cout << "GTime(BTow, GTime): " << gTimeB.bigTime << " " << gTimeB.to_string(1) << "\n"; + std::cout << "GTime(RTod, GTime): " << gTimeR.bigTime << " " << gTimeR.to_string(1) << "\n"; + + double ep[6]; + + std::cout << "\n"; + std::cout << "GTime to epoch to GTime:" << "\n"; + time2epoch(gTime, ep, E_TimeSys::GPST); + std::cout << "GTime to GPST epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] + << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" + << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; + GTime gTimeEpochGPS = epoch2time(ep, E_TimeSys::GPST); + std::cout << "GPST epoch to GTime: " << gTimeEpochGPS.to_string(1) << "\n"; + time2epoch(gTime, ep, E_TimeSys::GLONASST); + std::cout << "GTime to GLONASST epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] + << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" + << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; + GTime gTimeEpochGLO = epoch2time(ep, E_TimeSys::GLONASST); + std::cout << "GLONASST epoch to GTime: " << gTimeEpochGLO.to_string(1) << "\n"; + time2epoch(gTime, ep, E_TimeSys::GST); + std::cout << "GTime to GST epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] + << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" + << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; + GTime gTimeEpochGST = epoch2time(ep, E_TimeSys::GST); + std::cout << "GST epoch to GTime: " << gTimeEpochGST.to_string(1) << "\n"; + time2epoch(gTime, ep, E_TimeSys::BDT); + std::cout << "GTime to BDT epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] + << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" + << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; + GTime gTimeEpochBDT = epoch2time(ep, E_TimeSys::BDT); + std::cout << "BDT epoch to GTime: " << gTimeEpochBDT.to_string(1) << "\n"; + time2epoch(gTime, ep, E_TimeSys::QZSST); + std::cout << "GTime to QZSST epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] + << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" + << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; + GTime gTimeEpochQZS = epoch2time(ep, E_TimeSys::QZSST); + std::cout << "QZSST epoch to GTime: " << gTimeEpochQZS.to_string(1) << "\n"; + time2epoch(gTime, ep, E_TimeSys::TAI); + std::cout << "GTime to TAI epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] + << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" + << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; + GTime gTimeEpochTAI = epoch2time(ep, E_TimeSys::TAI); + std::cout << "TAI epoch to GTime: " << gTimeEpochTAI.to_string(1) << "\n"; + time2epoch(gTime, ep, E_TimeSys::UTC); + std::cout << "GTime to UTC epoch: " << (int)ep[0] << "-" << std::setw(2) << (int)ep[1] + << "-" << std::setw(2) << (int)ep[2] << " " << std::setw(2) << (int)ep[3] << ":" + << std::setw(2) << (int)ep[4] << ":" << std::setw(4) << ep[5] << "\n"; + GTime gTimeEpochUTC = epoch2time(ep, E_TimeSys::UTC); + std::cout << "UTC epoch to GTime: " << gTimeEpochUTC.to_string(1) << "\n"; + + double yds[6]; + + std::cout << "\n"; + std::cout << "GTime to yds to GTime:" << "\n"; + time2yds(gTime, yds, E_TimeSys::GPST); + std::cout << "GTime to GPST yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] + << "\n"; + GTime gTimeYdsGPS = yds2time(yds, E_TimeSys::GPST); + std::cout << "GPST yds to GTime: " << gTimeYdsGPS.to_string(1) << "\n"; + time2yds(gTime, yds, E_TimeSys::GLONASST); + std::cout << "GTime to GLONASST yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] + << "\n"; + GTime gTimeYdsGLO = yds2time(yds, E_TimeSys::GLONASST); + std::cout << "GLONASST yds to GTime: " << gTimeYdsGLO.to_string(1) << "\n"; + time2yds(gTime, yds, E_TimeSys::GST); + std::cout << "GTime to GST yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] + << "\n"; + GTime gTimeYdsGST = yds2time(yds, E_TimeSys::GST); + std::cout << "GST yds to GTime: " << gTimeYdsGST.to_string(1) << "\n"; + time2yds(gTime, yds, E_TimeSys::BDT); + std::cout << "GTime to BDT yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] + << "\n"; + GTime gTimeYdsBDT = yds2time(yds, E_TimeSys::BDT); + std::cout << "BDT yds to GTime: " << gTimeYdsBDT.to_string(1) << "\n"; + time2yds(gTime, yds, E_TimeSys::QZSST); + std::cout << "GTime to QZSST yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] + << "\n"; + GTime gTimeYdsQZS = yds2time(yds, E_TimeSys::QZSST); + std::cout << "QZSST yds to GTime: " << gTimeYdsQZS.to_string(1) << "\n"; + time2yds(gTime, yds, E_TimeSys::TAI); + std::cout << "GTime to TAI yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] + << "\n"; + GTime gTimeYdsTAI = yds2time(yds, E_TimeSys::TAI); + std::cout << "TAI yds to GTime: " << gTimeYdsTAI.to_string(1) << "\n"; + time2yds(gTime, yds, E_TimeSys::UTC); + std::cout << "GTime to UTC yds: " << (int)yds[0] << " " << (int)yds[1] << " " << yds[2] + << "\n"; + GTime gTimeYdsUTC = yds2time(yds, E_TimeSys::UTC); + std::cout << "UTC yds to GTime: " << gTimeYdsUTC.to_string(1) << "\n"; + + GTow nearGTow = 1; + nearTime = GTime(gWeek, nearGTow); + gTow = 604800 - 1.1; + gTimeG = GTime(gTow, nearTime); + + std::cout << std::setfill(' ') << "\n"; + std::cout << "Week/Day roll-over:" << "\n"; + std::cout << "nearGTow: " << std::setw(8) << nearGTow.val + << "\tnearTime: " << nearTime.to_string(1) << "\n"; + std::cout << "gTow: " << std::setw(8) << gTow.val + << "\tGTime(gTow, nearTime): " << gTimeG.to_string(1) << "\n"; + + nearGTow = 604800 - 1; + nearTime = GTime(gWeek, nearGTow); + gTow = 1.1; + gTimeG = GTime(gTow, nearTime); + + std::cout << "nearGTow: " << std::setw(8) << nearGTow.val + << "\tnearTime: " << nearTime.to_string(1) << "\n"; + std::cout << "gTow: " << std::setw(8) << gTow.val + << "\tGTime(gTow, nearTime): " << gTimeG.to_string(1) << "\n"; + + BTow nearBTow = 1; + nearTime = GTime(bWeek, nearBTow); + bTow = 604800 - 1.1; + gTimeB = GTime(bTow, nearTime); + + std::cout << "nearBTow: " << std::setw(8) << nearBTow.val + << "\tnearTime: " << nearTime.to_string(1) << "\n"; + std::cout << "bTow: " << std::setw(8) << bTow.val + << "\tGTime(bTow, nearTime): " << gTimeB.to_string(1) << "\n"; + + nearBTow = 604800 - 1; + nearTime = GTime(bWeek, nearBTow); + bTow = 1.1; + gTimeB = GTime(bTow, nearTime); + + std::cout << "nearBTow: " << std::setw(8) << nearBTow.val + << "\tnearTime: " << nearTime.to_string(1) << "\n"; + std::cout << "bTow: " << std::setw(8) << bTow.val + << "\tGTime(bTow, nearTime): " << gTimeB.to_string(1) << "\n"; + + UYds nearYds = uYds; + nearYds.sod = 86400 - 10800 + 1; + nearTime = nearYds; + RTod nearTod = nearTime; + rTod = 86400 - 1.1; + gTimeR = GTime(rTod, nearTime); + + std::cout << "nearSod: " << std::setw(8) << nearYds.sod + << "\tnearTime: " << nearTime.to_string(1) + << "\tnearTod: " << nearTod.val << "\n"; + std::cout << "rTod: " << std::setw(8) << rTod.val + << "\tGTime(rTod, nearTime): " << gTimeR.to_string(1) << "\n"; + + nearYds.sod = 86400 - 10800 - 1; + nearTime = nearYds; + nearTod = nearTime; + rTod = 1.1; + gTimeR = GTime(rTod, nearTime); + + std::cout << "nearSod: " << std::setw(8) << nearYds.sod + << "\tnearTime: " << nearTime.to_string(1) + << "\tnearTod: " << nearTod.val << "\n"; + std::cout << "rTod: " << std::setw(8) << rTod.val + << "\tGTime(rTod, nearTime): " << gTimeR.to_string(1) << "\n"; + + gEpoch = {2017, 1, 1, 0, 0, 17.9}; + gTimeE = gEpoch; + utcTime = gTimeE; + gTimeU = utcTime; + + std::cout << "\n"; + std::cout << "Leap second roll-over:" << "\n"; + std::cout << "GTime: " << gTimeE.bigTime << " " << gTimeE.to_string(1) << "\n"; + std::cout << "GTime to UtcTime: " << utcTime.bigTime << " " << utcTime.to_string(1) << "\n"; + std::cout << "UtcTime to GTime: " << gTimeU.bigTime << " " << gTimeU.to_string(1) << "\n"; } -#include "coordinates.hpp" -#include "iers2010.hpp" - -const GTime j2000TT = GEpoch{2000, E_Month::JAN, 1, 11, 58, 55.816 + GPS_SUB_UTC_2000}; // defined in utc 11:58:55.816 +const GTime j2000TT = GEpoch{ + 2000, + static_cast(E_Month::JAN), + 1, + 11, + 58, + 55.816 + GPS_SUB_UTC_2000 +}; // defined in utc 11:58:55.816 void rotationTest() { - GTime time = GEpoch{2019, E_Month::FEB, 6, 0, 0, 0}; + GTime time = GEpoch{2019, static_cast(E_Month::FEB), 6, 0, 0, 0}; - Vector3d nowPosI = Vector3d::Zero(); - nowPosI(0) = 20000000; - Vector3d lastPosE = nowPosI; + Vector3d nowPosI = Vector3d::Zero(); + nowPosI(0) = 20000000; + Vector3d lastPosE = nowPosI; - MjDateUt1 lastmjDate; + MjDateUt1 lastmjDate; -// change to inherit long double, then require to_double() for conversions, deny otherwise. + // change to inherit long double, then require to_double() for conversions, deny otherwise. - //do a day's worth of 20 second increment tests - for (; time < GEpoch{2019, E_Month::FEB, 9, 0, 0, 0}; time += 10) - { - Matrix3d i2tMatrix = Matrix3d::Identity(); + // do a day's worth of 20 second increment tests + for (; time < GEpoch{2019, static_cast(E_Month::FEB), 9, 0, 0, 0}; time += 10) + { + Matrix3d i2tMatrix = Matrix3d::Identity(); - //convert to terrestrial - ERPValues erpv = getErp(nav.erp, time); - eci2ecef(time, erpv, i2tMatrix); + // convert to terrestrial + ERPValues erpv = getErp(nav.erp, time); + eci2ecef(time, erpv, i2tMatrix); - MjDateUt1 mjDate (time, erpv.ut1Utc); + MjDateUt1 mjDate(time, erpv.ut1Utc); - Vector3d rSatEcef = i2tMatrix * nowPosI; - Vector3d deltaP = rSatEcef - lastPosE; + Vector3d rSatEcef = i2tMatrix * nowPosI; + Vector3d deltaP = rSatEcef - lastPosE; - long double deltamjdtt = mjDate.val - lastmjDate.val; + long double deltamjdtt = mjDate.val - lastmjDate.val; - printf("\n%s - {%15.6f} [%20.32e %30.23e %30.23e] %30.23e", - time.to_string().c_str(), - rSatEcef.dot(deltaP), - (double)mjDate.val, - (double)deltamjdtt, - (double)(mjDate.val), - rSatEcef.dot(lastPosE) - ); + printf( + "\n%s - {%15.6f} [%20.32e %30.23e %30.23e] %30.23e", + time.to_string().c_str(), + rSatEcef.dot(deltaP), + (double)mjDate.val, + (double)deltamjdtt, + (double)(mjDate.val), + rSatEcef.dot(lastPosE) + ); - lastPosE = rSatEcef; + lastPosE = rSatEcef; -// std::cout << rSatEcef.dot(lastPosE); - lastmjDate = mjDate; - } + // std::cout << rSatEcef.dot(lastPosE); + lastmjDate = mjDate; + } } void longDoubleTest() { - long double a; - std::cout << "\n" << "This system uses " << sizeof(a) * 8 << " bits for long doubles. (Hopefully that number is 128...)" << "\n"; + long double a; + std::cout << "\n" + << "This system uses " << sizeof(a) * 8 + << " bits for long doubles. (Hopefully that number is 128...)" << "\n"; } -#include "rtcmEncoder.hpp" -#include "ssr.hpp" /* void debugSSR(GTime t0, GTime targetTime, E_Sys sys, SsrOutMap& ssrOutMap) { - int iodPos; - int iodEph; - int iodClk; - GTime ephValidStart; - GTime ephValidStop; - GTime clkValidStart; - GTime clkValidStop; - Vector3d dPos[2]; - double dClk[3] = {}; - double refClk = 0; - bool refClkFound = false; - bool posDeltaPass[2]; - bool clkDeltaPass[2]; - Vector3d dPosDiff; - double dClkDiff[2]; - GTime ephTime = t0; - - for (auto& Sat : getSysSats(sys)) - { - GObs obs; - obs.Sat = Sat; - obs.satNav_ptr = &nav.satNavMap[Sat]; - - dPos[0] = Vector3d::Zero(); - dPos[1] = Vector3d::Zero(); - - posDeltaPass[1] = ssrPosDelta(t0, ephTime, obs, obs.satNav_ptr->receivedSSR, dPos[1], iodPos, iodEph, ephValidStart, ephValidStop); - clkDeltaPass[1] = ssrClkDelta(t0, ephTime, obs, obs.satNav_ptr->receivedSSR, dClk[1], iodClk, clkValidStart, clkValidStop); - - if ( posDeltaPass[0] && clkDeltaPass[0] - &&posDeltaPass[1] && clkDeltaPass[1]) - { - if ( refClkFound == false - &&abs(dClk[0]) < 1E-6) - { - refClk = dClk[1]; - refClkFound = true; - } - - dClk[2] = dClk[1] - refClk; - - dPosDiff = dPos[0] - dPos[1]; - dClkDiff[0] = dClk[0] - dClk[1]; - dClkDiff[1] = dClk[0] - dClk[2]; - - std::cout << std::setprecision(4) << std::fixed; - std::cout << "Debugging ssr: " - << "\tnow time (GPST): " << timeGet().to_string(1) - << "\ttarget time (GPST): " << targetTime.to_string(1) - << "\tt0 (GPST): " << t0.to_string(1) - << "\tsat: " << Sat.id() - << "\tdecoded: " << std::setw(7) << dPos[1] .transpose() << "\t" << std::setw(8) << dClk[1] << "\t" << std::setw(8) << dClk[2] - << "\tencoded: " << std::setw(7) << dPos[0] .transpose() << "\t" << std::setw(8) << dClk[0] - << "\tencoded-decoded: " << std::setw(7) << dPosDiff.transpose() << "\t" << std::setw(8) << dClkDiff[1] - << "\n"; - } - } - - std::cout << std::setprecision(4) << std::fixed; - for (auto& [sat, ssrOut] : ssrOutMap) - { - std::cout << "Straddle clks: " - << "\tnow time (GPST): " << timeGet().to_string(1) - << "\ttarget time (GPST): " << targetTime.to_string(1) - << "\tt0 (GPST): " << t0.to_string(1) - << "\tsat: " << sat.id() - << "\ttime[0]: " << ssrOut.clkInput.vals[0].time.to_string(1) - << "\tiode[0]: " << std::setw( 3) << ssrOut.clkInput.vals[0].iode - << "\tbrdc[0]: " << std::setw(12) << ssrOut.clkInput.vals[0].brdcClk - << "\tprec[0]: " << std::setw(12) << ssrOut.clkInput.vals[0].precClk - << "\ttime[1]: " << ssrOut.clkInput.vals[1].time.to_string(1) - << "\tiode[1]: " << std::setw( 3) << ssrOut.clkInput.vals[1].iode - << "\tbrdc[1]: " << std::setw(12) << ssrOut.clkInput.vals[1].brdcClk - << "\tprec[1]: " << std::setw(12) << ssrOut.clkInput.vals[1].precClk - << "\n"; - } + int iodPos; + int iodEph; + int iodClk; + GTime ephValidStart; + GTime ephValidStop; + GTime clkValidStart; + GTime clkValidStop; + Vector3d dPos[2]; + double dClk[3] = {}; + double refClk = 0; + bool refClkFound = false; + bool posDeltaPass[2]; + bool clkDeltaPass[2]; + Vector3d dPosDiff; + double dClkDiff[2]; + GTime ephTime = t0; + + for (auto& Sat : getSysSats(sys)) + { + GObs obs; + obs.Sat = Sat; + obs.satNav_ptr = &nav.satNavMap[Sat]; + + dPos[0] = Vector3d::Zero(); + dPos[1] = Vector3d::Zero(); + + posDeltaPass[1] = ssrPosDelta(t0, ephTime, obs, obs.satNav_ptr->receivedSSR, dPos[1], +iodPos, iodEph, ephValidStart, ephValidStop); clkDeltaPass[1] = ssrClkDelta(t0, ephTime, obs, +obs.satNav_ptr->receivedSSR, dClk[1], iodClk, clkValidStart, clkValidStop); + + if ( posDeltaPass[0] && clkDeltaPass[0] + &&posDeltaPass[1] && clkDeltaPass[1]) + { + if ( refClkFound == false + &&abs(dClk[0]) < 1E-6) + { + refClk = dClk[1]; + refClkFound = true; + } + + dClk[2] = dClk[1] - refClk; + + dPosDiff = dPos[0] - dPos[1]; + dClkDiff[0] = dClk[0] - dClk[1]; + dClkDiff[1] = dClk[0] - dClk[2]; + + std::cout << std::setprecision(4) << std::fixed; + std::cout << "Debugging ssr: " + << "\tnow time (GPST): " << timeGet().to_string(1) + << "\ttarget time (GPST): " << targetTime.to_string(1) + << "\tt0 (GPST): " << t0.to_string(1) + << "\tsat: " << Sat.id() + << "\tdecoded: " << std::setw(7) << dPos[1] .transpose() << "\t" +<< std::setw(8) << dClk[1] << "\t" << std::setw(8) << dClk[2] + << "\tencoded: " << std::setw(7) << dPos[0] .transpose() << "\t" +<< std::setw(8) << dClk[0] + << "\tencoded-decoded: " << std::setw(7) << dPosDiff.transpose() << "\t" +<< std::setw(8) << dClkDiff[1] + << "\n"; + } + } + + std::cout << std::setprecision(4) << std::fixed; + for (auto& [sat, ssrOut] : ssrOutMap) + { + std::cout << "Straddle clks: " + << "\tnow time (GPST): " << timeGet().to_string(1) + << "\ttarget time (GPST): " << targetTime.to_string(1) + << "\tt0 (GPST): " << t0.to_string(1) + << "\tsat: " << sat.id() + << "\ttime[0]: " << +ssrOut.clkInput.vals[0].time.to_string(1) + << "\tiode[0]: " << std::setw( 3) << ssrOut.clkInput.vals[0].iode + << "\tbrdc[0]: " << std::setw(12) << ssrOut.clkInput.vals[0].brdcClk + << "\tprec[0]: " << std::setw(12) << ssrOut.clkInput.vals[0].precClk + << "\ttime[1]: " << +ssrOut.clkInput.vals[1].time.to_string(1) + << "\tiode[1]: " << std::setw( 3) << ssrOut.clkInput.vals[1].iode + << "\tbrdc[1]: " << std::setw(12) << ssrOut.clkInput.vals[1].brdcClk + << "\tprec[1]: " << std::setw(12) << ssrOut.clkInput.vals[1].precClk + << "\n"; + } }*/ void reflector() { - Vector3d face[3]; - for (int i = 0; i < 3; i++) - { - face[i] = Vector3d::Zero(); - face[i](i) = 1; - } - - double absorbtion [3] = {}; - double specularity [3] = {}; - - absorbtion [0] = 1; - specularity [1] = 1; -// specularity [2] = 0; - - for (int x : {0, 1}) - for (int y : {0, 1}) - for (int z : {0, 1}) - { - Vector3d source; - source(0) = x; - source(1) = y; - source(2) = z; - - source.normalize(); - - Vector3d totalMomentum = Vector3d::Zero(); - - double totalFrontalarea = 0; - - for (int i = 0; i < 3; i++) - { - Vector3d correctFace = face[i]; - - if (correctFace.dot(source) < 0) - { - correctFace *= -1; - } - - double frontalArea = 1 * source.dot(face[i]); - totalFrontalarea += frontalArea; - - Vector3d incoming = frontalArea * source; - Vector3d reflected = -frontalArea * (source - 2 * (source.dot(correctFace)) * correctFace) * specularity[i]; - Vector3d emissive = frontalArea * correctFace * (1-specularity[i]) * 0.7; - - Vector3d outgoing = (1 - absorbtion[i]) * (reflected + emissive); - - Vector3d momentum = (incoming + outgoing); - - -// std::cout << incoming.transpose() << "\n"; -// std::cout << reflected.transpose() << "\n"; -// std::cout << emissive.transpose() << "\n"; - totalMomentum += momentum; - } - totalMomentum /= totalFrontalarea; - printf("%10.4f %10.4f %10.4f -> %10.4f %10.4f %10.4f \n", source(0), source(1), source(2), totalMomentum(0), totalMomentum(1), totalMomentum(2)); - } - + Vector3d face[3]; + for (int i = 0; i < 3; i++) + { + face[i] = Vector3d::Zero(); + face[i](i) = 1; + } + + double absorbtion[3] = {}; + double specularity[3] = {}; + + absorbtion[0] = 1; + specularity[1] = 1; + // specularity [2] = 0; + + for (int x : {0, 1}) + for (int y : {0, 1}) + for (int z : {0, 1}) + { + Vector3d source; + source(0) = x; + source(1) = y; + source(2) = z; + + source.normalize(); + + Vector3d totalMomentum = Vector3d::Zero(); + + double totalFrontalarea = 0; + + for (int i = 0; i < 3; i++) + { + Vector3d correctFace = face[i]; + + if (correctFace.dot(source) < 0) + { + correctFace *= -1; + } + + double frontalArea = 1 * source.dot(face[i]); + totalFrontalarea += frontalArea; + + Vector3d incoming = frontalArea * source; + Vector3d reflected = -frontalArea * + (source - 2 * (source.dot(correctFace)) * correctFace) * + specularity[i]; + Vector3d emissive = frontalArea * correctFace * (1 - specularity[i]) * 0.7; + + Vector3d outgoing = (1 - absorbtion[i]) * (reflected + emissive); + + Vector3d momentum = (incoming + outgoing); + + // std::cout << incoming.transpose() << "\n"; + // std::cout << reflected.transpose() << "\n"; + // std::cout << emissive.transpose() << "\n"; + totalMomentum += momentum; + } + totalMomentum /= totalFrontalarea; + printf( + "%10.4f %10.4f %10.4f -> %10.4f %10.4f %10.4f \n", + source(0), + source(1), + source(2), + totalMomentum(0), + totalMomentum(1), + totalMomentum(2) + ); + } } // void Spawn() @@ -980,1271 +1067,1254 @@ void reflector() // // } -#include "streamParser.hpp" -#include "streamFile.hpp" -#include "rinex.hpp" -#include "coordinates.hpp" -#include "erp.hpp" - -#include "geomagField.hpp" - void debugIGRF() { - std::cout << "\nDebugging IGRF:" << "\n"; - - // // test 1 - file reading - // std::cout << " n m g h" << "\n"; - // for (auto& [year, igrfMF] : igrfMFMap) - // { - // std::cout << igrfMF.year << ":" << "\n"; - // for (int i = 0; i <= igrfMF.maxDegree; i++) - // for (int j = 0; j <= i; j++) - // { - // std::cout << std::setprecision(2) << std::fixed; - // std::cout << " " << std::setw(2) << i - // << " " << std::setw(2) << j - // << " " << std::setw(9) << igrfMF.gnm(i, j) - // << " " << std::setw(9) << igrfMF.hnm(i, j) - // << "\n"; - // } - // } - - // { - // std::cout << igrfSV.year << "-" << igrfSV.yearEnd << ":" << "\n"; - // for (int i = 0; i <= igrfSV.maxDegree; i++) - // for (int j = 0; j <= i; j++) - // { - // std::cout << std::setprecision(2) << std::fixed; - // std::cout << " " << std::setw(2) << i - // << " " << std::setw(2) << j - // << " " << std::setw(9) << igrfSV.gnm(i, j) - // << " " << std::setw(9) << igrfSV.hnm(i, j) - // << "\n"; - // } - // } - - // // test 2 - get coefficients - // { - // std::cout << " "; - // for (int i = 0; i <= 13; i++) - // for (int j = 0; j <= i; j++) - // { - // std::cout << " g " << std::setw(2) << i << "," << std::setw(2) << j - // << " h " << std::setw(2) << i << "," << std::setw(2) << j; - // } - // std::cout << "\n"; - // } - - // for (int year = 1981; year <= 2026; year++) - // { - // GEpoch ep = {year, 1, 1, 0, 0, 0.0}; - // GTime time = ep; - - // GeomagMainField igrfMF; - // bool pass = getSHCoef(time, igrfMF); - - // if (!pass) - // return; - - // std::cout << time.to_string() << ":"; - // for (int i = 0; i <= igrfMF.maxDegree; i++) - // for (int j = 0; j <= i; j++) - // { - // std::cout << std::setprecision(2) << std::fixed; - // std::cout << " " << std::setw(9) << igrfMF.gnm(i, j) - // << " " << std::setw(9) << igrfMF.hnm(i, j); - // } - // std::cout << "\n"; - // } - - // test 3.1 - time series - { - Vector3d r = {-4.05205271694605e+06, 4.21283598092691e+06, -2.54510460797403e+06}; // Cartesian - ALIC - VectorPos pos = ecef2pos(r); - pos[0] = asin(r.z()/r.norm()); - pos[1] = atan2(r.y(), r.x()); - pos[2] = r.norm(); - - std::cout << std::setprecision(5) << std::fixed; - std::cout << "\n\tGeocentric pos: " << pos[0]*R2D << " " << pos[1]*R2D << " " << pos[2]/1000 << "\n"; - - for (int year = 1981; year <= 2023; year++) - { - GEpoch ep = {year, 1, 1, 0, 0, 0.0}; - GTime time = ep; - - Vector3d intensity = getGeomagIntensity(time, pos); - - std::cout << std::setprecision(5) << std::fixed; - std::cout << "\tyear: " << ep.year; - std::cout << std::setprecision(1) << std::fixed; - std::cout << "\tX: " << std::setw(8) << intensity.x() - << "\tY: " << std::setw(8) << intensity.y() - << "\tZ: " << std::setw(8) << intensity.z() - << "\n"; - } - } - - // test 3.2 - grid (including singularity) - { - GEpoch ep = {2019, 7, 18, 0, 0, 0.0}; - GTime time = ep; - double year = decimalYear(time); - - std::cout << std::setprecision(5) << std::fixed; - std::cout << "\n\tyear: " << year << "\n"; - - for (int lat = -90; lat <= 90; lat += 10) - for (int lon = -180; lon <= 180; lon += 20) - { - VectorPos pos = Vector3d(lat*D2R, lon*D2R, 6371000); - - Vector3d intensity = getGeomagIntensity(time, pos); - - std::cout << std::setprecision(1) << std::fixed; - std::cout << "\tGeocentric pos: " << std::setw(5) << pos[0]*R2D << " " << std::setw(6) << pos[1]*R2D << " " << std::setw(4) << pos[2]/1000; - std::cout << std::setprecision(1) << std::fixed; - std::cout << "\tX: " << std::setw(8) << intensity.x() - << "\tY: " << std::setw(8) << intensity.y() - << "\tZ: " << std::setw(8) << intensity.z() - << "\n"; - } - } + std::cout << "\nDebugging IGRF:" << "\n"; + + // // test 1 - file reading + // std::cout << " n m g h" << "\n"; + // for (auto& [year, igrfMF] : igrfMFMap) + // { + // std::cout << igrfMF.year << ":" << "\n"; + // for (int i = 0; i <= igrfMF.maxDegree; i++) + // for (int j = 0; j <= i; j++) + // { + // std::cout << std::setprecision(2) << std::fixed; + // std::cout << " " << std::setw(2) << i + // << " " << std::setw(2) << j + // << " " << std::setw(9) << igrfMF.gnm(i, j) + // << " " << std::setw(9) << igrfMF.hnm(i, j) + // << "\n"; + // } + // } + + // { + // std::cout << igrfSV.year << "-" << igrfSV.yearEnd << ":" << "\n"; + // for (int i = 0; i <= igrfSV.maxDegree; i++) + // for (int j = 0; j <= i; j++) + // { + // std::cout << std::setprecision(2) << std::fixed; + // std::cout << " " << std::setw(2) << i + // << " " << std::setw(2) << j + // << " " << std::setw(9) << igrfSV.gnm(i, j) + // << " " << std::setw(9) << igrfSV.hnm(i, j) + // << "\n"; + // } + // } + + // // test 2 - get coefficients + // { + // std::cout << " "; + // for (int i = 0; i <= 13; i++) + // for (int j = 0; j <= i; j++) + // { + // std::cout << " g " << std::setw(2) << i << "," << std::setw(2) << j + // << " h " << std::setw(2) << i << "," << std::setw(2) << j; + // } + // std::cout << "\n"; + // } + + // for (int year = 1981; year <= 2026; year++) + // { + // GEpoch ep = {year, 1, 1, 0, 0, 0.0}; + // GTime time = ep; + + // GeomagMainField igrfMF; + // bool pass = getSHCoef(time, igrfMF); + + // if (!pass) + // return; + + // std::cout << time.to_string() << ":"; + // for (int i = 0; i <= igrfMF.maxDegree; i++) + // for (int j = 0; j <= i; j++) + // { + // std::cout << std::setprecision(2) << std::fixed; + // std::cout << " " << std::setw(9) << igrfMF.gnm(i, j) + // << " " << std::setw(9) << igrfMF.hnm(i, j); + // } + // std::cout << "\n"; + // } + + // test 3.1 - time series + { + Vector3d r = { + -4.05205271694605e+06, + 4.21283598092691e+06, + -2.54510460797403e+06 + }; // Cartesian - ALIC + VectorPos pos = ecef2pos(r); + pos[0] = asin(r.z() / r.norm()); + pos[1] = atan2(r.y(), r.x()); + pos[2] = r.norm(); + + std::cout << std::setprecision(5) << std::fixed; + std::cout << "\n\tGeocentric pos: " << pos[0] * R2D << " " << pos[1] * R2D << " " + << pos[2] / 1000 << "\n"; + + for (int year = 1981; year <= 2023; year++) + { + GEpoch ep = {year, 1, 1, 0, 0, 0.0}; + GTime time = ep; + + Vector3d intensity = getGeomagIntensity(time, pos); + + std::cout << std::setprecision(5) << std::fixed; + std::cout << "\tyear: " << ep.year; + std::cout << std::setprecision(1) << std::fixed; + std::cout << "\tX: " << std::setw(8) << intensity.x() << "\tY: " << std::setw(8) + << intensity.y() << "\tZ: " << std::setw(8) << intensity.z() << "\n"; + } + } + + // test 3.2 - grid (including singularity) + { + GEpoch ep = {2019, 7, 18, 0, 0, 0.0}; + GTime time = ep; + double year = decimalYear(time); + + std::cout << std::setprecision(5) << std::fixed; + std::cout << "\n\tyear: " << year << "\n"; + + for (int lat = -90; lat <= 90; lat += 10) + for (int lon = -180; lon <= 180; lon += 20) + { + VectorPos pos = Vector3d(lat * D2R, lon * D2R, 6371000); + + Vector3d intensity = getGeomagIntensity(time, pos); + + std::cout << std::setprecision(1) << std::fixed; + std::cout << "\tGeocentric pos: " << std::setw(5) << pos[0] * R2D << " " + << std::setw(6) << pos[1] * R2D << " " << std::setw(4) << pos[2] / 1000; + std::cout << std::setprecision(1) << std::fixed; + std::cout << "\tX: " << std::setw(8) << intensity.x() << "\tY: " << std::setw(8) + << intensity.y() << "\tZ: " << std::setw(8) << intensity.z() << "\n"; + } + } } -#include "attitude.hpp" -#include "planets.hpp" - void debugAttitude() { - // GPS - SatSys Sat(E_Sys::GPS, 1); - GEpoch ep = {2023, 8, 28, 0, 0, 0}; -// GEpoch ep = {2019, 07, 18, 0, 0, 0}; - int nEpoch = 288; - double interval = 300; - - // // GRACE C - // SatSys Sat(E_Sys::LEO, 65); - // // GEpoch ep = {2019, 02, 13, 0, 0, 0}; - // GEpoch ep = {2022, 01, 01, 0, 0, 0}; - // int nEpoch = 8640; - // double interval = 10; - - // // GRACE D - // SatSys Sat(E_Sys::LEO, 65); - // GEpoch ep = {2022, 01, 01, 0, 0, 0}; - // int nEpoch = 8640; - // double interval = 10; - - // // COSMIC2 - 1 - // SatSys Sat(E_Sys::LEO, 80); - // GEpoch ep = {2022, 12, 31, 23, 39, 43}; - // int nEpoch = 2261; - // double interval = 1; - - // SPIRE -// SatSys Sat(E_Sys::LEO, 99); -// GEpoch ep = {2023, 01, 01, 9, 59, 46}; -// int nEpoch = 5853; -// double interval = 1; - -// GObs obs; -// obs.Sat = Sat; -// obs.time = ep; -// obs.satNav_ptr = &nav.satNavMap[Sat]; - // // SPIRE - // SatSys Sat(E_Sys::LEO, 99); - // GEpoch ep = {2023, 01, 01, 9, 59, 46}; - // int nEpoch = 5853; - // double interval = 1; - - GTime time = ep; - - SatPos satPos; - satPos.Sat = Sat; - - Receiver rec; - rec.id = Sat.id(); - - auto& satOpts = acsConfig.getSatOpts(Sat); - auto& recOpts = acsConfig.getRecOpts(rec.id); - - auto& satNav = nav.satNavMap[Sat]; - satNav.antBoresight = satOpts.antenna_boresight; - satNav.antAzimuth = satOpts.antenna_azimuth; - satPos.satNav_ptr = &satNav; - - AttStatus attStatus = {}; - VectorEcef rSat; - VectorEcef rSun; - VectorEcef eSun; - - printf("\n"); - printf("Debugging satellite attitude:\n"); - for (int i=0; iattStatus; - - // // for LEO satellites - // recAtt(rec, time, recOpts.rec_attitude.sources); - // attStatus = rec.attStatus; - - planetPosEcef(time, E_ThirdBody::SUN, rSun); - eSun = rSun.normalized(); - - printf("%d %8.1f\t%13.3f %13.3f %13.3f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\n", - week, tow, - rSat .x(), rSat .y(), rSat .z(), // for GNSS satellites - // rec.pos .x(), rec.pos .y(), rec.pos .z(), // (not ready) for LEO satellites - attStatus.eXBody.x(), attStatus.eXBody.y(), attStatus.eXBody.z(), - attStatus.eYBody.x(), attStatus.eYBody.y(), attStatus.eYBody.z(), - attStatus.eZBody.x(), attStatus.eZBody.y(), attStatus.eZBody.z(), - attStatus.eXAnt .x(), attStatus.eXAnt .y(), attStatus.eXAnt .z(), - attStatus.eYAnt .x(), attStatus.eYAnt .y(), attStatus.eYAnt .z(), - attStatus.eZAnt .x(), attStatus.eZAnt .y(), attStatus.eZAnt .z(), - eSun .x(), eSun .y(), eSun .z()); - - time += interval; - } + // GPS + SatSys Sat(E_Sys::GPS, 1); + GEpoch ep = {2023, 8, 28, 0, 0, 0}; + // GEpoch ep = {2019, 07, 18, 0, 0, 0}; + int nEpoch = 288; + double interval = 300; + + // // GRACE C + // SatSys Sat(E_Sys::LEO, 65); + // // GEpoch ep = {2019, 02, 13, 0, 0, 0}; + // GEpoch ep = {2022, 01, 01, 0, 0, 0}; + // int nEpoch = 8640; + // double interval = 10; + + // // GRACE D + // SatSys Sat(E_Sys::LEO, 65); + // GEpoch ep = {2022, 01, 01, 0, 0, 0}; + // int nEpoch = 8640; + // double interval = 10; + + // // COSMIC2 - 1 + // SatSys Sat(E_Sys::LEO, 80); + // GEpoch ep = {2022, 12, 31, 23, 39, 43}; + // int nEpoch = 2261; + // double interval = 1; + + // SPIRE + // SatSys Sat(E_Sys::LEO, 99); + // GEpoch ep = {2023, 01, 01, 9, 59, 46}; + // int nEpoch = 5853; + // double interval = 1; + + // GObs obs; + // obs.Sat = Sat; + // obs.time = ep; + // obs.satNav_ptr = &nav.satNavMap[Sat]; + // // SPIRE + // SatSys Sat(E_Sys::LEO, 99); + // GEpoch ep = {2023, 01, 01, 9, 59, 46}; + // int nEpoch = 5853; + // double interval = 1; + + GTime time = ep; + + SatPos satPos; + satPos.Sat = Sat; + + Receiver rec; + rec.id = Sat.id(); + + auto& satOpts = acsConfig.getSatOpts(Sat); + auto& recOpts = acsConfig.getRecOpts(rec.id); + + auto& satNav = nav.satNavMap[Sat]; + satNav.antBoresight = satOpts.antenna_boresight; + satNav.antAzimuth = satOpts.antenna_azimuth; + satPos.satNav_ptr = &satNav; + + AttStatus attStatus = {}; + VectorEcef rSat; + VectorEcef rSun; + VectorEcef eSun; + + printf("\n"); + printf("Debugging satellite attitude:\n"); + for (int i = 0; i < nEpoch; i++) + { + int week = GWeek(time); + double tow = GTow(time); + + // for GNSS satellites + satPos.posTime = time; + satpos(nullStream, time, time, satPos, satOpts.posModel.sources, E_OffsetType::COM, nav); + updateSatAtts(satPos); + rSat = satPos.rSatCom; + attStatus = satPos.satNav_ptr->attStatus; + + // // for LEO satellites + // recAtt(rec, time, recOpts.rec_attitude.sources); + // attStatus = rec.attStatus; + + planetPosEcef(time, E_ThirdBody::SUN, rSun); + eSun = rSun.normalized(); + + printf( + "%d %8.1f\t%13.3f %13.3f %13.3f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f " + "%9.6f\t%9.6f %9.6f " + "%9.6f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\t%9.6f %9.6f %9.6f\n", + week, + tow, + rSat.x(), + rSat.y(), + rSat.z(), // for GNSS satellites + // rec.pos .x(), rec.pos .y(), rec.pos .z(), // (not + // ready) for LEO satellites + attStatus.eXBody.x(), + attStatus.eXBody.y(), + attStatus.eXBody.z(), + attStatus.eYBody.x(), + attStatus.eYBody.y(), + attStatus.eYBody.z(), + attStatus.eZBody.x(), + attStatus.eZBody.y(), + attStatus.eZBody.z(), + attStatus.eXAnt.x(), + attStatus.eXAnt.y(), + attStatus.eXAnt.z(), + attStatus.eYAnt.x(), + attStatus.eYAnt.y(), + attStatus.eYAnt.z(), + attStatus.eZAnt.x(), + attStatus.eZAnt.y(), + attStatus.eZAnt.z(), + eSun.x(), + eSun.y(), + eSun.z() + ); + + time += interval; + } } - struct Thing { - }; /** This function calls nothing */ void debugErp() { - Thing thing; + Thing thing; } -#include -#include "tides.hpp" - -using iers2010::hisp::ntin; - void debugBlq() { - string id = "ALIC"; - - Receiver rec; - rec.id = id; - - for (auto& blqfile : acsConfig.ocean_tide_loading_blq_files) - { - bool found = readBlq(blqfile, rec, E_LoadingType::OCEAN); - } - - for (auto& blqfile : acsConfig.atmos_tide_loading_blq_files) - { - bool found = readBlq(blqfile, rec, E_LoadingType::ATMOSPHERIC); - } - - std::cout << std::fixed; - - std::cout << "\nDebugging OTL BLQ: " << rec.id << std::fixed << "\n"; - for (auto& [wave, disp] : rec.otlDisplacement) - { - std::cout << wave._to_string() << ":"; - for (int i = 0; i < 3; i++) std::cout << "\t" << std::setprecision(5) << std::setw(9) << disp.amplitude[i]; - for (int i = 0; i < 3; i++) std::cout << "\t" << std::setprecision(1) << std::setw(9) << disp.phase[i]; - std::cout << "\n"; - } - - std::cout << "\nDebugging ATL BLQ: " << rec.id << std::fixed << "\n"; - for (auto& [wave, disp] : rec.atlDisplacement) - { - std::cout << wave._to_string() << ":"; - for (int i = 0; i < 3; i++) std::cout << "\t" << std::setprecision(5) << std::setw(9) << disp.amplitude[i]; - for (int i = 0; i < 3; i++) std::cout << "\t" << std::setprecision(1) << std::setw(9) << disp.phase[i]; - std::cout << "\n"; - } + string id = "ALIC"; + + Receiver rec; + rec.id = id; + + for (auto& blqfile : acsConfig.ocean_tide_loading_blq_files) + { + bool found = readBlq(blqfile, E_LoadingType::OCEAN); + } + + for (auto& blqfile : acsConfig.atmos_tide_loading_blq_files) + { + bool found = readBlq(blqfile, E_LoadingType::ATMOSPHERIC); + } + + std::cout << std::fixed; + + std::cout << "\nDebugging OTL BLQ: " << rec.id << std::fixed << "\n"; + for (auto& [wave, disp] : otlDisplacementMap[id]) + { + std::cout << enum_to_string(wave) << ":"; + for (int i = 0; i < 3; i++) + std::cout << "\t" << std::setprecision(5) << std::setw(9) << disp.amplitude[i]; + for (int i = 0; i < 3; i++) + std::cout << "\t" << std::setprecision(1) << std::setw(9) << disp.phase[i]; + std::cout << "\n"; + } + + std::cout << "\nDebugging ATL BLQ: " << rec.id << std::fixed << "\n"; + for (auto& [wave, disp] : atlDisplacementMap[id]) + { + std::cout << enum_to_string(wave) << ":"; + for (int i = 0; i < 3; i++) + std::cout << "\t" << std::setprecision(5) << std::setw(9) << disp.amplitude[i]; + for (int i = 0; i < 3; i++) + std::cout << "\t" << std::setprecision(1) << std::setw(9) << disp.phase[i]; + std::cout << "\n"; + } } map> readRefOtlDisp(string file) { - map> dispMap; + map> dispMap; - std::ifstream fileStream(file); - if (!fileStream) - { - return dispMap; - } + std::ifstream fileStream(file); + if (!fileStream) + { + return dispMap; + } - while (fileStream) - { - string line; - getline(fileStream, line); + while (fileStream) + { + string line; + getline(fileStream, line); - if (line[0] == '*') - continue; + if (line[0] == '*') + continue; - char* buff = &line[0]; + char* buff = &line[0]; - string id = line.substr(0, 4); - char dummy[5]; - double mjd; - double v[3]; - int found = sscanf(buff, "%4s %lf %lf %lf %lf", dummy, &mjd, &v[0], &v[1], &v[2]); + string id = line.substr(0, 4); + char dummy[5]; + double mjd; + double v[3]; + int found = sscanf(buff, "%4s %lf %lf %lf %lf", dummy, &mjd, &v[0], &v[1], &v[2]); - if (found != 5) - continue; + if (found != 5) + continue; - VectorEnu denu; - denu[0] = -v[2]; - denu[1] = -v[1]; - denu[2] = v[0]; + VectorEnu denu; + denu[0] = -v[2]; + denu[1] = -v[1]; + denu[2] = v[0]; - dispMap[id][mjd] = denu; - } + dispMap[id][mjd] = denu; + } - return dispMap; + return dispMap; } void debugTideOcean() { - std::cout << "\nDebugging OTL:" << "\n"; - - string filename = "testData/oload.test"; - auto dispRefMap = readRefOtlDisp(filename); - - for (auto& [id, dispTimeMap] : dispRefMap) - { - Receiver rec; - rec.id = id; - - for (auto& blqfile : acsConfig.ocean_tide_loading_blq_files) - { - bool found = readBlq(blqfile, rec, E_LoadingType::OCEAN); - } - - if (rec.otlDisplacement.empty()) - return; - - for (auto& [mjdval, denuRef] : dispTimeMap) - { - MjDateUtc mjdUtc; - mjdUtc.val = mjdval; - - GTime time = GTime(mjdUtc); - ERPValues erpv = getErp(nav.erp, time); - MjDateUt1 mjdUt1(time, erpv.ut1Utc); - - // VectorEnu denu = tideOceanLoad (std::cout, mjdUt1, rec.otlDisplacement); - VectorEnu denu = tideOceanLoadAdjusted(std::cout, time, mjdUt1, rec.otlDisplacement); - VectorEnu diff = denu - denuRef; - - std::cout << std::setprecision( 7) << std::fixed - << "\t" << id - << "\t" << mjdUtc.to_double() - << "\t" << std::setw(10) << denuRef.e() - << "\t" << std::setw(10) << denuRef.n() - << "\t" << std::setw(10) << denuRef.u() - << "\t" << std::setw(10) << denu.e() - << "\t" << std::setw(10) << denu.n() - << "\t" << std::setw(10) << denu.u() - << "\t" << std::setw(10) << diff.e() - << "\t" << std::setw(10) << diff.n() - << "\t" << std::setw(10) << diff.u() - << "\n"; - } - std::cout << "\n"; - } + std::cout << "\nDebugging OTL:" << "\n"; + + string filename = "testData/oload.test"; + auto dispRefMap = readRefOtlDisp(filename); + + for (auto& [id, dispTimeMap] : dispRefMap) + { + Receiver rec; + rec.id = id; + + for (auto& blqfile : acsConfig.ocean_tide_loading_blq_files) + { + readBlq(blqfile, E_LoadingType::OCEAN); + } + + if (otlDisplacementMap[id].empty()) + return; + + for (auto& [mjdval, denuRef] : dispTimeMap) + { + MjDateUtc mjdUtc; + mjdUtc.val = mjdval; + + GTime time = GTime(mjdUtc); + ERPValues erpv = getErp(nav.erp, time); + MjDateUt1 mjdUt1(time, erpv.ut1Utc); + + // VectorEnu denu = tideOceanLoad (std::cout, mjdUt1, otlDisplacementMap[id]); + VectorEnu denu = tideOceanLoadAdjusted(std::cout, time, mjdUt1, otlDisplacementMap[id]); + VectorEnu diff = denu - denuRef; + + std::cout << std::setprecision(7) << std::fixed << "\t" << id << "\t" + << mjdUtc.to_double() << "\t" << std::setw(10) << denuRef.e() << "\t" + << std::setw(10) << denuRef.n() << "\t" << std::setw(10) << denuRef.u() + << "\t" << std::setw(10) << denu.e() << "\t" << std::setw(10) << denu.n() + << "\t" << std::setw(10) << denu.u() << "\t" << std::setw(10) << diff.e() + << "\t" << std::setw(10) << diff.n() << "\t" << std::setw(10) << diff.u() + << "\n"; + } + std::cout << "\n"; + } } void debugHardisp() { - /// Read in ocean loading coefficients from stdin - std::cout << "\nDebugging OTL Hardisp:" << "\n"; - - string filename = "testData/oload.test"; - auto dispRefMap = readRefOtlDisp(filename); - - for (auto& [id, dispTimeMap] : dispRefMap) - { - Receiver rec; - rec.id = id; - - for (auto& blqfile : acsConfig.ocean_tide_loading_blq_files) - { - bool found = readBlq(blqfile, rec, E_LoadingType::OCEAN); - } - - if (rec.otlDisplacement.empty()) - return; - - for (auto& [mjdval, denuRef] : dispTimeMap) - { - MjDateUtc mjdUtc; - mjdUtc.val = mjdval; - - GTime time = GTime(mjdUtc); - - VectorEnu denu = tideOceanLoadHardisp(std::cout, time, rec.otlDisplacement); - VectorEnu diff = denu - denuRef; - - std::cout << std::setprecision( 7) << std::fixed - << "\t" << id - << "\t" << mjdUtc.to_double() - << "\t" << std::setw(10) << denuRef.e() - << "\t" << std::setw(10) << denuRef.n() - << "\t" << std::setw(10) << denuRef.u() - << "\t" << std::setw(10) << denu.e() - << "\t" << std::setw(10) << denu.n() - << "\t" << std::setw(10) << denu.u() - << "\t" << std::setw(10) << diff.e() - << "\t" << std::setw(10) << diff.n() - << "\t" << std::setw(10) << diff.u() - << "\n"; - } - std::cout << "\n"; - } + /// Read in ocean loading coefficients from stdin + std::cout << "\nDebugging OTL Hardisp:" << "\n"; + + string filename = "testData/oload.test"; + auto dispRefMap = readRefOtlDisp(filename); + + for (auto& [id, dispTimeMap] : dispRefMap) + { + Receiver rec; + rec.id = id; + + for (auto& blqfile : acsConfig.ocean_tide_loading_blq_files) + { + readBlq(blqfile, E_LoadingType::OCEAN); + } + + if (otlDisplacementMap[id].empty()) + return; + + for (auto& [mjdval, denuRef] : dispTimeMap) + { + MjDateUtc mjdUtc; + mjdUtc.val = mjdval; + + GTime time = GTime(mjdUtc); + + VectorEnu denu = tideOceanLoadHardisp(std::cout, time, otlDisplacementMap[id]); + VectorEnu diff = denu - denuRef; + + std::cout << std::setprecision(7) << std::fixed << "\t" << id << "\t" + << mjdUtc.to_double() << "\t" << std::setw(10) << denuRef.e() << "\t" + << std::setw(10) << denuRef.n() << "\t" << std::setw(10) << denuRef.u() + << "\t" << std::setw(10) << denu.e() << "\t" << std::setw(10) << denu.n() + << "\t" << std::setw(10) << denu.u() << "\t" << std::setw(10) << diff.e() + << "\t" << std::setw(10) << diff.n() << "\t" << std::setw(10) << diff.u() + << "\n"; + } + std::cout << "\n"; + } } map readRefAtlDisp(string file) { - map dispMap; + map dispMap; - std::ifstream fileStream(file); - if (!fileStream) - { - return dispMap; - } + std::ifstream fileStream(file); + if (!fileStream) + { + return dispMap; + } - while (fileStream) - { - string line; - getline(fileStream, line); + while (fileStream) + { + string line; + getline(fileStream, line); - if (line.substr(1, 2) == "$$") - continue; + if (line.substr(1, 2) == "$$") + continue; - char* buff = &line[0]; + char* buff = &line[0]; - char dummy[5]; - double mjd; - double v[3]; - int found = sscanf(buff, "%lf %lf %lf %lf", &mjd, &v[0], &v[1], &v[2]); + char dummy[5]; + double mjd; + double v[3]; + int found = sscanf(buff, "%lf %lf %lf %lf", &mjd, &v[0], &v[1], &v[2]); - if (found != 4) - continue; + if (found != 4) + continue; - VectorEnu denu; - denu[0] = v[2] * 1E-3; - denu[1] = v[1] * 1E-3; - denu[2] = v[0] * 1E-3; + VectorEnu denu; + denu[0] = v[2] * 1E-3; + denu[1] = v[1] * 1E-3; + denu[2] = v[0] * 1E-3; - dispMap[mjd] = denu; - } + dispMap[mjd] = denu; + } - return dispMap; + return dispMap; } map> readRefAplDisp(string file) { - map> dispMap; + map> dispMap; - std::ifstream fileStream(file); - if (!fileStream) - { - return dispMap; - } + std::ifstream fileStream(file); + if (!fileStream) + { + return dispMap; + } - while (fileStream) - { - string line; - getline(fileStream, line); + while (fileStream) + { + string line; + getline(fileStream, line); - if (line[0] == '!') - continue; + if (line[0] == '!') + continue; - char* buff = &line[0]; + char* buff = &line[0]; - string id = line.substr(0, 4); - char dummy[5]; - double mjd; - double v[3]; - int found = sscanf(buff, "%4s %lf %lf %lf %lf", dummy, &mjd, &v[0], &v[1], &v[2]); + string id = line.substr(0, 4); + char dummy[5]; + double mjd; + double v[3]; + int found = sscanf(buff, "%4s %lf %lf %lf %lf", dummy, &mjd, &v[0], &v[1], &v[2]); - if (found != 5) - continue; + if (found != 5) + continue; - VectorEnu denu; - denu[0] = v[1]; - denu[1] = v[2]; - denu[2] = v[0]; + VectorEnu denu; + denu[0] = v[1]; + denu[1] = v[2]; + denu[2] = v[0]; - dispMap[id][mjd] = denu; - } + dispMap[id][mjd] = denu; + } - return dispMap; + return dispMap; } void debugTideAtmos() { - std::cout << "\nDebugging ATL:" << "\n"; - - string id = "ALIC"; - Receiver rec; - rec.id = id; - - for (auto& blqfile : acsConfig.atmos_tide_loading_blq_files) - { - bool found = readBlq(blqfile, rec, E_LoadingType::ATMOSPHERIC); - } - - if (rec.atlDisplacement.empty()) - return; - - // Test 1 - single station, multiple days - string filename = "testData/grdintrp.dat"; - auto dispRefMap = readRefAtlDisp(filename); - - double mjdval = 58682; - for (auto& [mjdval, denuRef] : dispRefMap) - { - MjDateUt1 mjdUt1; - mjdUt1.val = mjdval; - - - VectorEnu denu = tideAtmosLoad(std::cout, mjdUt1, rec.atlDisplacement); - VectorEnu diff = denu - denuRef; - - std::cout << std::setprecision( 7) << std::fixed - << "\t" << id - << "\t" << mjdUt1.to_double() - << "\t" << std::setw(10) << denuRef.e() - << "\t" << std::setw(10) << denuRef.n() - << "\t" << std::setw(10) << denuRef.u() - << "\t" << std::setw(10) << denu.e() - << "\t" << std::setw(10) << denu.n() - << "\t" << std::setw(10) << denu.u() - << "\t" << std::setw(10) << diff.e() - << "\t" << std::setw(10) << diff.n() - << "\t" << std::setw(10) << diff.u() - << "\n"; - } - - // // Test 2 - single station, multiple days (one year) - // string filename = "testData/y2019.apl_g.txt"; - // auto dispRefMap = readRefAplDisp(filename); - - // auto dispTimeMap = dispRefMap[id]; - // for (auto& [mjdval, denuRef] : dispTimeMap) - // { - // MjDateUt1 mjdUt1; - // mjdUt1.val = mjdval; - - // VectorEnu denu = tideAtmosLoad(std::cout, mjdUt1, rec.atlDisplacement); - // VectorEnu diff = denu - denuRef; - - // std::cout << std::setprecision( 7) << std::fixed - // << "\t" << id - // << "\t" << mjdUt1.to_double() - // << "\t" << std::setw(10) << denuRef.e() - // << "\t" << std::setw(10) << denuRef.n() - // << "\t" << std::setw(10) << denuRef.u() - // << "\t" << std::setw(10) << denu.e() - // << "\t" << std::setw(10) << denu.n() - // << "\t" << std::setw(10) << denu.u() - // << "\t" << std::setw(10) << diff.e() - // << "\t" << std::setw(10) << diff.n() - // << "\t" << std::setw(10) << diff.u() - // << "\n"; - // } - - // // Test 3 - multiple stations, single day - // string filename = "testData/2019199.apl_g.txt"; - // auto dispRefMap = readRefAplDisp(filename); - - // for (auto& [id, dispTimeMap] : dispRefMap) - // for (auto& [mjdval, denuRef] : dispTimeMap) - // { - // Receiver rec; - // rec.id = id; - - // for (auto& blqfile : acsConfig.atmos_tide_loading_blq_files) - // { - // bool found = readBlq(blqfile, rec, E_LoadingType::ATMOSPHERIC); - // } - - // if (rec.atlDisplacement.empty()) - // continue; - - // MjDateUt1 mjdUt1; - // mjdUt1.val = mjdval; - - // VectorEnu denu = tideAtmosLoad(std::cout, mjdUt1, rec.atlDisplacement); - // VectorEnu diff = denu - denuRef; - - // std::cout << std::setprecision( 7) << std::fixed - // << "\t" << id - // << "\t" << mjdUt1.to_double() - // << "\t" << std::setw(10) << denuRef.e() - // << "\t" << std::setw(10) << denuRef.n() - // << "\t" << std::setw(10) << denuRef.u() - // << "\t" << std::setw(10) << denu.e() - // << "\t" << std::setw(10) << denu.n() - // << "\t" << std::setw(10) << denu.u() - // << "\t" << std::setw(10) << diff.e() - // << "\t" << std::setw(10) << diff.n() - // << "\t" << std::setw(10) << diff.u() - // << "\n"; - // } + std::cout << "\nDebugging ATL:" << "\n"; + + string id = "ALIC"; + Receiver rec; + rec.id = id; + + for (auto& blqfile : acsConfig.atmos_tide_loading_blq_files) + { + readBlq(blqfile, E_LoadingType::ATMOSPHERIC); + } + + if (atlDisplacementMap[id].empty()) + return; + + // Test 1 - single station, multiple days + string filename = "testData/grdintrp.dat"; + auto dispRefMap = readRefAtlDisp(filename); + + double mjdval = 58682; + for (auto& [mjdval, denuRef] : dispRefMap) + { + MjDateUt1 mjdUt1; + mjdUt1.val = mjdval; + + VectorEnu denu = tideAtmosLoad(std::cout, mjdUt1, atlDisplacementMap[id]); + VectorEnu diff = denu - denuRef; + + std::cout << std::setprecision(7) << std::fixed << "\t" << id << "\t" << mjdUt1.to_double() + << "\t" << std::setw(10) << denuRef.e() << "\t" << std::setw(10) << denuRef.n() + << "\t" << std::setw(10) << denuRef.u() << "\t" << std::setw(10) << denu.e() + << "\t" << std::setw(10) << denu.n() << "\t" << std::setw(10) << denu.u() << "\t" + << std::setw(10) << diff.e() << "\t" << std::setw(10) << diff.n() << "\t" + << std::setw(10) << diff.u() << "\n"; + } + + // // Test 2 - single station, multiple days (one year) + // string filename = "testData/y2019.apl_g.txt"; + // auto dispRefMap = readRefAplDisp(filename); + + // auto dispTimeMap = dispRefMap[id]; + // for (auto& [mjdval, denuRef] : dispTimeMap) + // { + // MjDateUt1 mjdUt1; + // mjdUt1.val = mjdval; + + // VectorEnu denu = tideAtmosLoad(std::cout, mjdUt1, atlDisplacementMap[id]); + // VectorEnu diff = denu - denuRef; + + // std::cout << std::setprecision( 7) << std::fixed + // << "\t" << id + // << "\t" << mjdUt1.to_double() + // << "\t" << std::setw(10) << denuRef.e() + // << "\t" << std::setw(10) << denuRef.n() + // << "\t" << std::setw(10) << denuRef.u() + // << "\t" << std::setw(10) << denu.e() + // << "\t" << std::setw(10) << denu.n() + // << "\t" << std::setw(10) << denu.u() + // << "\t" << std::setw(10) << diff.e() + // << "\t" << std::setw(10) << diff.n() + // << "\t" << std::setw(10) << diff.u() + // << "\n"; + // } + + // // Test 3 - multiple stations, single day + // string filename = "testData/2019199.apl_g.txt"; + // auto dispRefMap = readRefAplDisp(filename); + + // for (auto& [id, dispTimeMap] : dispRefMap) + // for (auto& [mjdval, denuRef] : dispTimeMap) + // { + // Receiver rec; + // rec.id = id; + + // for (auto& blqfile : acsConfig.atmos_tide_loading_blq_files) + // { + // readBlq(blqfile, E_LoadingType::ATMOSPHERIC); + // } + + // if (atlDisplacementMap[id].empty()) + // continue; + + // MjDateUt1 mjdUt1; + // mjdUt1.val = mjdval; + + // VectorEnu denu = tideAtmosLoad(std::cout, mjdUt1, atlDisplacementMap[id]); + // VectorEnu diff = denu - denuRef; + + // std::cout << std::setprecision( 7) << std::fixed + // << "\t" << id + // << "\t" << mjdUt1.to_double() + // << "\t" << std::setw(10) << denuRef.e() + // << "\t" << std::setw(10) << denuRef.n() + // << "\t" << std::setw(10) << denuRef.u() + // << "\t" << std::setw(10) << denu.e() + // << "\t" << std::setw(10) << denu.n() + // << "\t" << std::setw(10) << denu.u() + // << "\t" << std::setw(10) << diff.e() + // << "\t" << std::setw(10) << diff.n() + // << "\t" << std::setw(10) << diff.u() + // << "\n"; + // } } void debugTideSolid() { - std::cout << "\nDebugging solid Earth tide:" << "\n"; - - // Test cases from DEHANTTIDEINEL.F - // Note that the last test case should be incorrect - std::vector ep = - { - {2009, 4, 13, 0, 0, 0}, - {2012, 7, 13, 0, 0, 0}, - {2015, 7, 15, 0, 0, 0}, - {2017, 1, 15, 0, 0, 0}, - {2019, 7, 18, 4, 59, 12} - }; - - std::vector recPos = - { - { 4075578.385, 931852.890, 4801570.154}, - { 1112189.660, -4842955.026, 3985352.284}, - { 1112200.5696, -4842957.8511, 3985345.9122}, - { 1112152.8166, -4842857.5435, 3985496.1783}, - { 2587384.1007872052, -1043033.5652423096, 5716564.3449383173} - }; - - std::vector rSun = - { - { 137859926952.0150, 54228127881.4350, 23509422341.6960}, - { -54537460436.2357, 130244288385.2790, 56463429031.5996}, - { 100210282451.6279, 103055630398.3160, 56855096480.4475}, - { 8382471154.1312895, 10512408445.356153, -5360583240.3763866}, - { -40911673203.204002, 135847503359.17343, 54660205331.259735} - }; - - std::vector rMoon = - { - {-179996231.920342, -312468450.131567, -169288918.592160}, - { 300396716.912, 243238281.451, 120548075.939}, - { 369817604.4348, 1897917.5258, 120804980.8284}, - { 380934092.93550891, 2871428.1904491195, 79015680.553570181}, - { 202952994.54523405, -319652979.70541549, -135514223.45872557} - }; - - std::vector dxyzRef = - { - { 0.07700420357108125891, 0.06304056321824967613, 0.05516568152597246810}, - {-0.02036831479592075833, 0.05658254776225972449, -0.07597679676871742227}, - { 0.00509570869172363845, 0.08286630259835287000, -0.06366349254041896170}, - { 0.00509570869172363840, 0.08286630259835287000, -0.06366349254041896200}, - {-0.05560417990980600500, 0.02320056584074919900, -0.12297592439382479000} - }; - - for (int i = 0; i < 5; i++) - { - GTime time = epoch2time(ep[i].data(), E_TimeSys::UTC); - - ERPValues erpv = getErp(nav.erp, time); - MjDateUt1 mjdUt1(time, erpv.ut1Utc); - - VectorPos pos; - pos.lat() = asin(recPos[i].z() / recPos[i].norm()); - pos.lon() = atan2(recPos[i].y(), recPos[i].x()); - - // Vector3d dxyz = tideSolidEarth(std::cout, time, mjdUt1, rSun[i], rMoon[i], pos); - Vector3d dxyz = tideSolidEarthDehant(std::cout, time, rSun[i], rMoon[i], recPos[i]); - Vector3d diff = dxyz - dxyzRef[i]; - - std::cout << std::setprecision( 7) << std::fixed - << "\t" << mjdUt1.to_double() - << "\t" << std::setw(10) << dxyzRef[i].x() - << "\t" << std::setw(10) << dxyzRef[i].y() - << "\t" << std::setw(10) << dxyzRef[i].z() - << "\t" << std::setw(10) << dxyz.x() - << "\t" << std::setw(10) << dxyz.y() - << "\t" << std::setw(10) << dxyz.z() - << "\t" << std::setw(10) << diff.x() - << "\t" << std::setw(10) << diff.y() - << "\t" << std::setw(10) << diff.z() - << "\n"; - } + std::cout << "\nDebugging solid Earth tide:" << "\n"; + + // Test cases from DEHANTTIDEINEL.F + // Note that the last test case should be incorrect + std::vector ep = { + {2009, 4, 13, 0, 0, 0}, + {2012, 7, 13, 0, 0, 0}, + {2015, 7, 15, 0, 0, 0}, + {2017, 1, 15, 0, 0, 0}, + {2019, 7, 18, 4, 59, 12} + }; + + std::vector recPos = { + {4075578.385, 931852.890, 4801570.154}, + {1112189.660, -4842955.026, 3985352.284}, + {1112200.5696, -4842957.8511, 3985345.9122}, + {1112152.8166, -4842857.5435, 3985496.1783}, + {2587384.1007872052, -1043033.5652423096, 5716564.3449383173} + }; + + std::vector rSun = { + {137859926952.0150, 54228127881.4350, 23509422341.6960}, + {-54537460436.2357, 130244288385.2790, 56463429031.5996}, + {100210282451.6279, 103055630398.3160, 56855096480.4475}, + {8382471154.1312895, 10512408445.356153, -5360583240.3763866}, + {-40911673203.204002, 135847503359.17343, 54660205331.259735} + }; + + std::vector rMoon = { + {-179996231.920342, -312468450.131567, -169288918.592160}, + {300396716.912, 243238281.451, 120548075.939}, + {369817604.4348, 1897917.5258, 120804980.8284}, + {380934092.93550891, 2871428.1904491195, 79015680.553570181}, + {202952994.54523405, -319652979.70541549, -135514223.45872557} + }; + + std::vector dxyzRef = { + {0.07700420357108125891, 0.06304056321824967613, 0.05516568152597246810}, + {-0.02036831479592075833, 0.05658254776225972449, -0.07597679676871742227}, + {0.00509570869172363845, 0.08286630259835287000, -0.06366349254041896170}, + {0.00509570869172363840, 0.08286630259835287000, -0.06366349254041896200}, + {-0.05560417990980600500, 0.02320056584074919900, -0.12297592439382479000} + }; + + for (int i = 0; i < 5; i++) + { + GTime time = epoch2time(ep[i].data(), E_TimeSys::UTC); + + ERPValues erpv = getErp(nav.erp, time); + MjDateUt1 mjdUt1(time, erpv.ut1Utc); + + VectorPos pos; + pos.lat() = asin(recPos[i].z() / recPos[i].norm()); + pos.lon() = atan2(recPos[i].y(), recPos[i].x()); + + // Vector3d dxyz = tideSolidEarth(std::cout, time, mjdUt1, rSun[i], rMoon[i], pos); + Vector3d dxyz = tideSolidEarthDehant(std::cout, time, rSun[i], rMoon[i], recPos[i]); + Vector3d diff = dxyz - dxyzRef[i]; + + std::cout << std::setprecision(7) << std::fixed << "\t" << mjdUt1.to_double() << "\t" + << std::setw(10) << dxyzRef[i].x() << "\t" << std::setw(10) << dxyzRef[i].y() + << "\t" << std::setw(10) << dxyzRef[i].z() << "\t" << std::setw(10) << dxyz.x() + << "\t" << std::setw(10) << dxyz.y() << "\t" << std::setw(10) << dxyz.z() << "\t" + << std::setw(10) << diff.x() << "\t" << std::setw(10) << diff.y() << "\t" + << std::setw(10) << diff.z() << "\n"; + } } map readRefSPoleDisp(string file) { - map dispMap; + map dispMap; - std::ifstream fileStream(file); - if (!fileStream) - { - return dispMap; - } + std::ifstream fileStream(file); + if (!fileStream) + { + return dispMap; + } - while (fileStream) - { - string line; - getline(fileStream, line); + while (fileStream) + { + string line; + getline(fileStream, line); - char* buff = &line[0]; + char* buff = &line[0]; - double mjd; - double lat; - double lon; - double dr; - int found = sscanf(buff, "%lf,%lf,%lf,%lf", &mjd, &lat, &lon, &dr); + double mjd; + double lat; + double lon; + double dr; + int found = sscanf(buff, "%lf,%lf,%lf,%lf", &mjd, &lat, &lon, &dr); - if (found != 4) - continue; + if (found != 4) + continue; - Vector3d disp; - disp[0] = lat; - disp[1] = lon; - disp[2] = dr; + Vector3d disp; + disp[0] = lat; + disp[1] = lon; + disp[2] = dr; - dispMap[mjd] = disp; - } + dispMap[mjd] = disp; + } - return dispMap; + return dispMap; } void debugTideSolidPole() { - std::cout << "\nDebugging solid Earth pole tide:" << "\n"; - - string filename = "testData/test_pole_tide.csv"; - auto dispRefMap = readRefSPoleDisp(filename); - - for (auto& [mjdval, dispRef] : dispRefMap) - { - VectorPos pos; - pos.lat() = dispRef(0) * D2R; - pos.lon() = dispRef(1) * D2R; - - MjDateUtc mjdUtc; - mjdUtc.val = mjdval; - GTime time = GTime(mjdUtc); - - ERPValues erpv = getErp(nav.erp, time); - MjDateUt1 mjdUt1(time, erpv.ut1Utc); - - VectorEnu denu = tideSolidPole(std::cout, mjdUt1, pos, erpv); - double diff = denu.u() - dispRef(2); - - std::cout << std::setprecision( 7) << std::fixed - << "\t" << mjdUtc.to_double(); - std::cout << std::setprecision( 7) << std::scientific - << "\t" << std::setw(10) << dispRef(2) - << "\t" << std::setw(10) << denu.u() - << "\t" << std::setw(10) << diff - << "\n"; - } + std::cout << "\nDebugging solid Earth pole tide:" << "\n"; + + string filename = "testData/test_pole_tide.csv"; + auto dispRefMap = readRefSPoleDisp(filename); + + for (auto& [mjdval, dispRef] : dispRefMap) + { + VectorPos pos; + pos.lat() = dispRef(0) * D2R; + pos.lon() = dispRef(1) * D2R; + + MjDateUtc mjdUtc; + mjdUtc.val = mjdval; + GTime time = GTime(mjdUtc); + + ERPValues erpv = getErp(nav.erp, time); + MjDateUt1 mjdUt1(time, erpv.ut1Utc); + MjDateTT mjdTT(time); + VectorEnu denu = tideSolidPole(std::cout, mjdTT, pos, erpv); + double diff = denu.u() - dispRef(2); + + std::cout << std::setprecision(7) << std::fixed << "\t" << mjdUtc.to_double(); + std::cout << std::setprecision(7) << std::scientific << "\t" << std::setw(10) << dispRef(2) + << "\t" << std::setw(10) << denu.u() << "\t" << std::setw(10) << diff << "\n"; + } } map readRefOPoleDisp(string file) { - map dispMap; - - std::ifstream fileStream(file); - if (!fileStream) - { - return dispMap; - } - - while (fileStream) - { - string line; - getline(fileStream, line); - - char* buff = &line[0]; - - double mjd; - double v[9]; - int found = sscanf(buff, "%lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", &mjd, &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7], &v[8]); - - if (found != 10) - continue; - - VectorEnu denu; - denu[0] = v[8]; - denu[1] = v[7]; - denu[2] = v[6]; - - dispMap[mjd] = denu; - } - - return dispMap; + map dispMap; + + std::ifstream fileStream(file); + if (!fileStream) + { + return dispMap; + } + + while (fileStream) + { + string line; + getline(fileStream, line); + + char* buff = &line[0]; + + double mjd; + double v[9]; + int found = sscanf( + buff, + "%lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", + &mjd, + &v[0], + &v[1], + &v[2], + &v[3], + &v[4], + &v[5], + &v[6], + &v[7], + &v[8] + ); + + if (found != 10) + continue; + + VectorEnu denu; + denu[0] = v[8]; + denu[1] = v[7]; + denu[2] = v[6]; + + dispMap[mjd] = denu; + } + + return dispMap; } void debugTideOceanPole() { - std::cout << "\nDebugging ocean pole tide:" << "\n"; - - string filename = acsConfig.inputs_root + "tables/opoleloadcoefcmcor.txt"; - readOceanPoleCoeff(filename); - - // // Grid retrieval test - // { - // MjDateUtc mjdUtc; - // mjdUtc.val = 52640; - // GTime time = GTime(mjdUtc); - - // ERPValues erpv = getErp(nav.erp, time); - // MjDateUt1 mjdUt1(time, erpv.ut1Utc); - - // vector vecPos; - // VectorPos pos; - // pos.lat() = -90 * D2R; pos.lon() = 0 * D2R; vecPos.push_back(pos); - // pos.lat() = -89.85 * D2R; pos.lon() = 180 * D2R; vecPos.push_back(pos); - // pos.lat() = -89.85 * D2R; pos.lon() = 359.85 * D2R; vecPos.push_back(pos); - // pos.lat() = 0 * D2R; pos.lon() = 0 * D2R; vecPos.push_back(pos); - // pos.lat() = 0 * D2R; pos.lon() = 180 * D2R; vecPos.push_back(pos); - // pos.lat() = 0 * D2R; pos.lon() = 359.85 * D2R; vecPos.push_back(pos); - // pos.lat() = +89.85 * D2R; pos.lon() = 0 * D2R; vecPos.push_back(pos); - // pos.lat() = +89.75 * D2R; pos.lon() = 180 * D2R; vecPos.push_back(pos); - // pos.lat() = +90 * D2R; pos.lon() = 359.85 * D2R; vecPos.push_back(pos); - - // for (auto& pos : vecPos) VectorEnu denu = tideOceanPole(std::cout, mjdUt1, pos, erpv); - // } - - // Test case in opoleloadcmcor.test, note that the mean pole model used is different from IERS 2010 Conventions - filename = "testData/opoleloadcmcor.test"; - auto dispRefMap = readRefOPoleDisp(filename); - - VectorPos pos; - pos.lat() = -43.75 * D2R; - pos.lon() = 232.25 * D2R; - for (auto& [mjdval, denuRef] : dispRefMap) - { - MjDateUtc mjdUtc; - mjdUtc.val = mjdval; - GTime time = GTime(mjdUtc); - - ERPValues erpv = getErp(nav.erp, time); - // MjDateUt1 mjdUt1(time, erpv.ut1Utc); - MjDateUt1 mjdUt1(time, 0); - - VectorEnu denu = tideOceanPole(std::cout, mjdUt1, pos, erpv); - VectorEnu diff = denu - denuRef; - - std::cout << std::setprecision( 7) << std::fixed - << "\t" << mjdUtc.to_double() - << std::setprecision( 7) << std::scientific - << "\t" << std::setw(10) << denuRef.e() - << "\t" << std::setw(10) << denuRef.n() - << "\t" << std::setw(10) << denuRef.u() - << "\t" << std::setw(10) << denu.e() - << "\t" << std::setw(10) << denu.n() - << "\t" << std::setw(10) << denu.u() - << "\t" << std::setw(10) << diff.e() - << "\t" << std::setw(10) << diff.n() - << "\t" << std::setw(10) << diff.u() - << "\n"; - } + std::cout << "\nDebugging ocean pole tide:" << "\n"; + + string filename = acsConfig.inputs_root + "tables/opoleloadcoefcmcor.txt"; + readOceanPoleCoeff(filename); + + // // Grid retrieval test + // { + // MjDateUtc mjdUtc; + // mjdUtc.val = 52640; + // GTime time = GTime(mjdUtc); + + // ERPValues erpv = getErp(nav.erp, time); + // MjDateUt1 mjdUt1(time, erpv.ut1Utc); + + // vector vecPos; + // VectorPos pos; + // pos.lat() = -90 * D2R; pos.lon() = 0 * D2R; vecPos.push_back(pos); + // pos.lat() = -89.85 * D2R; pos.lon() = 180 * D2R; vecPos.push_back(pos); + // pos.lat() = -89.85 * D2R; pos.lon() = 359.85 * D2R; vecPos.push_back(pos); + // pos.lat() = 0 * D2R; pos.lon() = 0 * D2R; vecPos.push_back(pos); + // pos.lat() = 0 * D2R; pos.lon() = 180 * D2R; vecPos.push_back(pos); + // pos.lat() = 0 * D2R; pos.lon() = 359.85 * D2R; vecPos.push_back(pos); + // pos.lat() = +89.85 * D2R; pos.lon() = 0 * D2R; vecPos.push_back(pos); + // pos.lat() = +89.75 * D2R; pos.lon() = 180 * D2R; vecPos.push_back(pos); + // pos.lat() = +90 * D2R; pos.lon() = 359.85 * D2R; vecPos.push_back(pos); + + // for (auto& pos : vecPos) VectorEnu denu = tideOceanPole(std::cout, mjdUt1, pos, erpv); + // } + + // Test case in opoleloadcmcor.test, note that the mean pole model used is different from IERS + // 2010 Conventions + filename = "testData/opoleloadcmcor.test"; + auto dispRefMap = readRefOPoleDisp(filename); + + VectorPos pos; + pos.lat() = -43.75 * D2R; + pos.lon() = 232.25 * D2R; + for (auto& [mjdval, denuRef] : dispRefMap) + { + MjDateUtc mjdUtc; + mjdUtc.val = mjdval; + GTime time = GTime(mjdUtc); + + ERPValues erpv = getErp(nav.erp, time); + // MjDateUt1 mjdUt1(time, erpv.ut1Utc); + MjDateUt1 mjdUt1(time, 0); + MjDateTT mjdTT(time); + + VectorEnu denu = tideOceanPole(std::cout, mjdTT, pos, erpv); + VectorEnu diff = denu - denuRef; + + std::cout << std::setprecision(7) << std::fixed << "\t" << mjdUtc.to_double() + << std::setprecision(7) << std::scientific << "\t" << std::setw(10) << denuRef.e() + << "\t" << std::setw(10) << denuRef.n() << "\t" << std::setw(10) << denuRef.u() + << "\t" << std::setw(10) << denu.e() << "\t" << std::setw(10) << denu.n() << "\t" + << std::setw(10) << denu.u() << "\t" << std::setw(10) << diff.e() << "\t" + << std::setw(10) << diff.n() << "\t" << std::setw(10) << diff.u() << "\n"; + } } -void alternatePostfits( - Trace& trace, - KFMeas& kfMeas, - KFState& kfState); +void alternatePostfits(Trace& trace, KFMeas& kfMeas, KFState& kfState); void infiniteTest() { - KFState kfState; - - GTime time; - time += 60; + KFState kfState; -// for (int i = 0; i < 1000; i++) - { - KFMeasEntryList kfMeasEntryList; - - KFKey ionoKey; - KFKey ambKey; + GTime time; + time += 60; - ionoKey.type = KF::IONO_STEC; - ambKey.type = KF::AMBIGUITY; + // for (int i = 0; i < 1000; i++) + { + KFMeasEntryList kfMeasEntryList; - kfState.advanced_postfits = true; + KFKey ionoKey; + KFKey ambKey; - InitialState ionoInit; - ionoInit.P = 100; - ionoInit.Q = -1; + ionoKey.type = KF::IONO_STEC; + ambKey.type = KF::AMBIGUITY; - InitialState ambInit; - ambInit.P = 100; + kfState.advanced_postfits = true; - { - KFKey obsKey; - obsKey.num = 1; - obsKey.type = KF::PHAS_MEAS; + InitialState ionoInit; + ionoInit.P = 100; + ionoInit.Q = -1; - KFMeasEntry measEntry(&kfState); + InitialState ambInit; + ambInit.P = 100; - measEntry.addDsgnEntry(ionoKey, 1, ionoInit); + { + KFKey obsKey; + obsKey.num = 1; + obsKey.type = KF::PHAS_MEAS; - measEntry.setInnov(5); -// measEntry.setNoise(1); + KFMeasEntry measEntry(&kfState); - measEntry.addNoiseEntry(obsKey, 1, 1); + measEntry.addDsgnEntry(ionoKey, 1, ionoInit); - kfMeasEntryList.push_back(measEntry); - } + measEntry.setInnov(5); + // measEntry.setNoise(1); - { - KFKey obsKey; - obsKey.num = 2; - obsKey.type = KF::PHAS_MEAS; + measEntry.addNoiseEntry(obsKey, 1, 1); - KFMeasEntry measEntry(&kfState); + kfMeasEntryList.push_back(measEntry); + } - measEntry.addDsgnEntry(ionoKey, 1, ionoInit); - measEntry.addDsgnEntry(ambKey, 1, ambInit); + { + KFKey obsKey; + obsKey.num = 2; + obsKey.type = KF::PHAS_MEAS; - measEntry.setInnov(8); -// measEntry.setNoise(1); + KFMeasEntry measEntry(&kfState); - measEntry.addNoiseEntry(obsKey, 1, 1); + measEntry.addDsgnEntry(ionoKey, 1, ionoInit); + measEntry.addDsgnEntry(ambKey, 1, ambInit); - kfMeasEntryList.push_back(measEntry); - } + measEntry.setInnov(8); + // measEntry.setNoise(1); - kfState.output_residuals = true; + measEntry.addNoiseEntry(obsKey, 1, 1); - kfState.stateTransition(std::cout, time); + kfMeasEntryList.push_back(measEntry); + } - kfState.outputStates(std::cout); + kfState.output_residuals = true; - KFMeas kfMeas(kfState, kfMeasEntryList, time); + kfState.stateTransition(std::cout, time); + kfState.outputStates(std::cout); - alternatePostfits(std::cout, kfMeas, kfState); + KFMeas kfMeas(kfState, kfMeasEntryList, time); - kfState.filterKalman(std::cout, kfMeas, "", true); + alternatePostfits(std::cout, kfMeas, kfState); - kfState.outputStates(std::cout); + kfState.filterKalman(std::cout, kfMeas, "", true); + kfState.outputStates(std::cout); - time++; - } + time++; + } } -#include -#include "ubxDecoder.hpp" - void getAccData() { - std::ifstream inputStream("../inputData/otherProducts/Grace/ACT1B_2019-02-14_C_04.txt"); + std::ifstream inputStream("../inputData/otherProducts/Grace/ACT1B_2019-02-14_C_04.txt"); - string line; - while (std::getline(inputStream, line)) - { - if (line[0] == '#') - break; - } + string line; + while (std::getline(inputStream, line)) + { + if (line[0] == '#') + break; + } - while (std::getline(inputStream, line)) - { - if (line.empty()) - { - break; - } + while (std::getline(inputStream, line)) + { + if (line.empty()) + { + break; + } - long int intTime; - char dummy; - Vector3d accl; + long int intTime; + char dummy; + Vector3d accl; - sscanf(line.c_str(), "%ld %c %lf %lf %lf", &intTime, &dummy, &accl[1], &accl[2], &accl[0]); + sscanf(line.c_str(), "%ld %c %lf %lf %lf", &intTime, &dummy, &accl[1], &accl[2], &accl[0]); -// accl[2] = 0; -// accl[1] = 0; + // accl[2] = 0; + // accl[1] = 0; - GTime time; - time.bigTime = 630763200; - time += intTime; + GTime time; + time.bigTime = 630763200; + time += intTime; -// std::cout << "\n" << time << " " << accl.transpose(); + // std::cout << "\n" << time << " " << accl.transpose(); - UbxDecoder::acclDataMaps["L64"][time] = accl; - } + UbxDecoder::acclDataMaps["L64"][time] = accl; + } } -#include "orbitProp.hpp" - -void perEpochPropTest( - GTime time) +void perEpochPropTest(GTime time) { - SatSys Sat = SatSys("L51"); - - SatPos satPos0; - satPos0.Sat = Sat; - - bool pass = satPosPrecise(std::cout, time, satPos0, nav); - - if (!pass) - return; - - ERPValues erpv0 = getErp(nav.erp, time); - FrameSwapper frameSwapper0(time, erpv0); - satPos0.rSatEci0 = frameSwapper0(satPos0.rSatCom, &satPos0.satVel, &satPos0.vSatEci0); - - KFState kfState; - kfState.time = time; - - for (int i = 0; i < 3; i++) - { - KFKey kfKey; - kfKey.type = KF::ORBIT; - kfKey.Sat = Sat; - - kfKey.num = i; - kfState.addKFState(kfKey, {.x = satPos0.rSatEci0[i]}); - - kfKey.num = i + 3; - kfState.addKFState(kfKey, {.x = satPos0.vSatEci0[i]}); - } - kfState.stateTransition(std::cout, kfState.time); - - KFState copy = kfState; - - double dt = 60; + SatSys Sat = SatSys("L51"); - static double propSumSqr = 0; - static double ellipseSumSqr = 0; - static double j2SumSqr = 0; + SatPos satPos0; + satPos0.Sat = Sat; - static double num = 0; + bool pass = satPosPrecise(std::cout, time, satPos0, nav); - GTime newTime = kfState.time + dt; - - predictOrbits(std::cout, copy, newTime); - copy.stateTransition(std::cout, newTime); - - std::cout << "\n" << "\n" << kfState.time; - - SatPos satPosPrec; - SatPos satPosProp; - SatPos satPosEllipse; - SatPos satPosJ2; - - satPosPrec .Sat = SatSys("L51"); - satPosProp .Sat = SatSys("L51"); - satPosEllipse .Sat = SatSys("L51"); - satPosJ2 .Sat = SatSys("L51"); - - pass = satPosPrecise(std::cout, newTime, satPosPrec, nav); std::cout << " precise passed: " << pass; - pass = satPosKalman (std::cout, newTime, satPosProp, ©); std::cout << " prop Passed: " << pass; - // pass = satPosKalman (std::cout, newTime, satPosEllipse, &kfState); std::cout << " ellp Passed: " << pass; - pass = satPosKalman (std::cout, newTime, satPosJ2, &kfState); std::cout << " j2 Passed: " << pass; - - ERPValues erpv = getErp(nav.erp, newTime); - FrameSwapper frameSwapper(newTime, erpv); - satPosPrec.rSatEci0 = frameSwapper(satPosPrec.rSatCom, &satPosPrec.satVel, &satPosPrec.vSatEci0); - - Matrix3d E = ecef2rac(satPosPrec.rSatEci0, satPosPrec.vSatEci0); - - VectorEci differenceProp = satPosProp .rSatEciDt - satPosPrec.rSatEci0; Vector3d rtnProp = E * differenceProp; - VectorEci differenceEllipse = satPosEllipse .rSatEciDt - satPosPrec.rSatEci0; Vector3d rtnEllipse = E * differenceEllipse; - VectorEci differenceJ2 = satPosJ2 .rSatEciDt - satPosPrec.rSatEci0; Vector3d rtnJ2 = E * differenceJ2; - - std::cout << "\r\nPrecise:\t" << satPosPrec .rSatEci0 .transpose().format(heavyFmt); - std::cout << "\r\nPropFull:\t" << satPosProp .rSatEciDt .transpose().format(heavyFmt) << "\t" << rtnProp .transpose().format(heavyFmt) << " \t" << rtnProp .norm(); - std::cout << "\r\nEllipse:\t" << satPosEllipse .rSatEciDt .transpose().format(heavyFmt) << "\t" << rtnEllipse .transpose().format(heavyFmt) << " \t" << rtnEllipse.norm(); - std::cout << "\r\nEllipseJ2:\t" << satPosJ2 .rSatEciDt .transpose().format(heavyFmt) << "\t" << rtnJ2 .transpose().format(heavyFmt) << " \t" << rtnJ2 .norm(); - - propSumSqr += rtnProp .squaredNorm(); - ellipseSumSqr += rtnEllipse .squaredNorm(); - j2SumSqr += rtnJ2 .squaredNorm(); - - num++; - - double propRms = sqrt(propSumSqr / num); - double ellipseRms = sqrt(ellipseSumSqr / num); - double j2Rms = sqrt(j2SumSqr / num); - - std::cout << "\n"; - std::cout << "\n" << "PropFull rms from prec with t=" << dt << " : " << propRms; - std::cout << "\n" << "Ellipse rms from prec with t=" << dt << " : " << ellipseRms; - std::cout << "\n" << "EllipseJ2 rms from prec with t=" << dt << " : " << j2Rms; + if (!pass) + return; + + ERPValues erpv0 = getErp(nav.erp, time); + FrameSwapper frameSwapper0(time, erpv0); + satPos0.rSatEci0 = frameSwapper0(satPos0.rSatCom, &satPos0.satVel, &satPos0.vSatEci0); + + KFState kfState; + kfState.time = time; + + for (int i = 0; i < 3; i++) + { + KFKey kfKey; + kfKey.type = KF::ORBIT; + kfKey.Sat = Sat; + + kfKey.num = i; + kfState.addKFState(kfKey, {.x = satPos0.rSatEci0[i]}); + + kfKey.num = i + 3; + kfState.addKFState(kfKey, {.x = satPos0.vSatEci0[i]}); + } + kfState.stateTransition(std::cout, kfState.time); + + KFState copy = kfState; + + double dt = 60; + + static double propSumSqr = 0; + static double ellipseSumSqr = 0; + static double j2SumSqr = 0; + + static double num = 0; + + GTime newTime = kfState.time + dt; + + predictOrbits(std::cout, copy, newTime); + copy.stateTransition(std::cout, newTime); + + std::cout << "\n" + << "\n" + << kfState.time; + + SatPos satPosPrec; + SatPos satPosProp; + SatPos satPosEllipse; + SatPos satPosJ2; + + satPosPrec.Sat = SatSys("L51"); + satPosProp.Sat = SatSys("L51"); + satPosEllipse.Sat = SatSys("L51"); + satPosJ2.Sat = SatSys("L51"); + + pass = satPosPrecise(std::cout, newTime, satPosPrec, nav); + std::cout << " precise passed: " << pass; + pass = satPosKalman(std::cout, newTime, satPosProp, ©); + std::cout << " prop Passed: " << pass; + // pass = satPosKalman (std::cout, newTime, satPosEllipse, &kfState); + // std::cout << " ellp Passed: " << pass; + pass = satPosKalman(std::cout, newTime, satPosJ2, &kfState); + std::cout << " j2 Passed: " << pass; + + ERPValues erpv = getErp(nav.erp, newTime); + FrameSwapper frameSwapper(newTime, erpv); + satPosPrec.rSatEci0 = + frameSwapper(satPosPrec.rSatCom, &satPosPrec.satVel, &satPosPrec.vSatEci0); + + Matrix3d E = ecef2rac(satPosPrec.rSatEci0, satPosPrec.vSatEci0); + + VectorEci differenceProp = satPosProp.rSatEciDt - satPosPrec.rSatEci0; + Vector3d rtnProp = E * differenceProp; + VectorEci differenceEllipse = satPosEllipse.rSatEciDt - satPosPrec.rSatEci0; + Vector3d rtnEllipse = E * differenceEllipse; + VectorEci differenceJ2 = satPosJ2.rSatEciDt - satPosPrec.rSatEci0; + Vector3d rtnJ2 = E * differenceJ2; + + std::cout << "\r\nPrecise:\t" << satPosPrec.rSatEci0.transpose().format(heavyFmt); + std::cout << "\r\nPropFull:\t" << satPosProp.rSatEciDt.transpose().format(heavyFmt) << "\t" + << rtnProp.transpose().format(heavyFmt) << " \t" << rtnProp.norm(); + std::cout << "\r\nEllipse:\t" << satPosEllipse.rSatEciDt.transpose().format(heavyFmt) << "\t" + << rtnEllipse.transpose().format(heavyFmt) << " \t" << rtnEllipse.norm(); + std::cout << "\r\nEllipseJ2:\t" << satPosJ2.rSatEciDt.transpose().format(heavyFmt) << "\t" + << rtnJ2.transpose().format(heavyFmt) << " \t" << rtnJ2.norm(); + + propSumSqr += rtnProp.squaredNorm(); + ellipseSumSqr += rtnEllipse.squaredNorm(); + j2SumSqr += rtnJ2.squaredNorm(); + + num++; + + double propRms = sqrt(propSumSqr / num); + double ellipseRms = sqrt(ellipseSumSqr / num); + double j2Rms = sqrt(j2SumSqr / num); + + std::cout << "\n"; + std::cout << "\n" + << "PropFull rms from prec with t=" << dt << " : " << propRms; + std::cout << "\n" + << "Ellipse rms from prec with t=" << dt << " : " << ellipseRms; + std::cout << "\n" + << "EllipseJ2 rms from prec with t=" << dt << " : " << j2Rms; } void accel() { - KFState kfState; - - GTime time; - time += 60; - - double actualX = 0; - double actualV = 0; - double actualA = 0; - - double actualScale [2] = {1,1};//{1.2, 0.95}; - double actualBias [2] = {0.02, -0.1}; - - InitialState init; - init.P = 100; + KFState kfState; - InitialState sInit; - sInit.P = 100; - sInit.x = 1; + GTime time; + time += 60; - InitialState vInit; - vInit.P = 100; - // vInit.Q = 100; + double actualX = 0; + double actualV = 0; + double actualA = 0; - InitialState aInit; - aInit.P = 100; + double actualScale[2] = {1, 1}; //{1.2, 0.95}; + double actualBias[2] = {0.02, -0.1}; + InitialState init; + init.P = 100; - KFKey posKey = {.type = KF::REC_POS}; - KFKey velKey = {.type = KF::REC_VEL}; - KFKey accKey = {.type = KF::REC_ACC}; - KFKey scaleKey = {.type = KF::ACCL_SCALE}; - KFKey biasKey = {.type = KF::ACCL_BIAS}; + InitialState sInit; + sInit.P = 100; + sInit.x = 1; + InitialState vInit; + vInit.P = 100; + // vInit.Q = 100; - //not really about indirectly estimating acceleration, - //here, i indirectly estimated velocity instead as a first pass + InitialState aInit; + aInit.P = 100; - kfState.output_residuals = true; + KFKey posKey = {.type = KF::REC_POS}; + KFKey velKey = {.type = KF::REC_VEL}; + KFKey accKey = {.type = KF::REC_ACC}; + KFKey scaleKey = {.type = KF::ACCL_SCALE}; + KFKey biasKey = {.type = KF::ACCL_BIAS}; - kfState.addKFState(posKey, init); + // not really about indirectly estimating acceleration, + // here, i indirectly estimated velocity instead as a first pass - for (int i = 0; i < 400; i++) - { - if (i > 100) - { - actualA = 1; - } - if (i > 200) - { - actualA = -1; - } + kfState.output_residuals = true; - actualV += actualA; - actualX += actualV; + kfState.addKFState(posKey, init); - kfState.removeState(accKey); + for (int i = 0; i < 400; i++) + { + if (i > 100) + { + actualA = 1; + } + if (i > 200) + { + actualA = -1; + } - kfState.stateTransition(std::cout, time); + actualV += actualA; + actualX += actualV; - kfState.outputStates(std::cout, "/Deleted"); - { - KFMeasEntryList kfMeasEntryList; + kfState.removeState(accKey); - if (1) - for (int i = 0; i < 2; i++) - { - scaleKey.num = i; - biasKey.num = i; + kfState.stateTransition(std::cout, time); - KFMeasEntry measEntry(&kfState); + kfState.outputStates(std::cout, "/Deleted"); + { + KFMeasEntryList kfMeasEntryList; - double stateBias = 0; - double stateScale = 1; - double stateAcceleration = 0; + if (1) + for (int i = 0; i < 2; i++) + { + scaleKey.num = i; + biasKey.num = i; - // kfState.addKFState(scaleKey, sInit); - kfState.addKFState(biasKey, init); + KFMeasEntry measEntry(&kfState); - // kfState.getKFValue(scaleKey, stateScale); - kfState.getKFValue(biasKey, stateBias); - // kfState.getKFValue(velKey, stateVelocity); + double stateBias = 0; + double stateScale = 1; + double stateAcceleration = 0; - double measuredAcceleration = actualA * actualScale[i] + actualBias[i]; + // kfState.addKFState(scaleKey, sInit); + kfState.addKFState(biasKey, init); - double estimatedAcceleration = (measuredAcceleration - stateBias) / stateScale; + // kfState.getKFValue(scaleKey, stateScale); + kfState.getKFValue(biasKey, stateBias); + // kfState.getKFValue(velKey, stateVelocity); - measEntry.addDsgnEntry(accKey, stateScale, vInit); - measEntry.addDsgnEntry(biasKey, -stateScale, init); - // measEntry.addDsgnEntry(scaleKey,(measuredVelocity - stateBias), sInit); + double measuredAcceleration = actualA * actualScale[i] + actualBias[i]; - double omc = measuredAcceleration - - stateAcceleration - + stateBias; + double estimatedAcceleration = (measuredAcceleration - stateBias) / stateScale; - std::cout << "\n" << "Acceleration : " << actualA; - std::cout << "\n" << "Measured : " << measuredAcceleration; - std::cout << "\n" << "Estimated : " << estimatedAcceleration; - std::cout << "\n" << "OMC : " << omc; - std::cout << "\n"; + measEntry.addDsgnEntry(accKey, stateScale, vInit); + measEntry.addDsgnEntry(biasKey, -stateScale, init); + // measEntry.addDsgnEntry(scaleKey,(measuredVelocity - stateBias), sInit); + double omc = measuredAcceleration - stateAcceleration + stateBias; - measEntry.setInnov(omc); - measEntry.setNoise(0.01); - measEntry.obsKey.comment = "Acceleration"; + std::cout << "\n" + << "Acceleration : " << actualA; + std::cout << "\n" + << "Measured : " << measuredAcceleration; + std::cout << "\n" + << "Estimated : " << estimatedAcceleration; + std::cout << "\n" + << "OMC : " << omc; + std::cout << "\n"; - kfMeasEntryList.push_back(measEntry); - } + measEntry.setInnov(omc); + measEntry.setNoise(0.01); + measEntry.obsKey.comment = "Acceleration"; - kfState.setKFTransRate(posKey, velKey, +1, vInit); - kfState.setKFTransRate(velKey, accKey, +1, aInit); + kfMeasEntryList.push_back(measEntry); + } - kfState.stateTransition(std::cout, time); + kfState.setKFTransRate(posKey, velKey, +1, vInit); + kfState.setKFTransRate(velKey, accKey, +1, aInit); - KFMeas combinedMeas(kfState, kfMeasEntryList, time); + kfState.stateTransition(std::cout, time); - kfState.filterKalman(std::cout, combinedMeas, "", true); + KFMeas combinedMeas(kfState, kfMeasEntryList, time); - kfState.outputStates(std::cout, "/Accelerations"); - } + kfState.filterKalman(std::cout, combinedMeas, "", true); - time++; + kfState.outputStates(std::cout, "/Accelerations"); + } + time++; - kfState.stateTransition(std::cout, time); + kfState.stateTransition(std::cout, time); - kfState.outputStates(std::cout, "/PREDICTED"); + kfState.outputStates(std::cout, "/PREDICTED"); - //add measurement for position, - { - KFMeasEntryList kfMeasEntryList; + // add measurement for position, + { + KFMeasEntryList kfMeasEntryList; - KFMeasEntry measEntry(&kfState); + KFMeasEntry measEntry(&kfState); - double stateX = 0; + double stateX = 0; - kfState.getKFValue(posKey, stateX); + kfState.getKFValue(posKey, stateX); - measEntry.addDsgnEntry(posKey, 1, init); + measEntry.addDsgnEntry(posKey, 1, init); - double omc = actualX - - stateX; + double omc = actualX - stateX; - std::cout << "\n" << "actualX : " << actualX; - std::cout << "\n" << "stateX : " << stateX; + std::cout << "\n" + << "actualX : " << actualX; + std::cout << "\n" + << "stateX : " << stateX; - measEntry.setInnov(omc); - measEntry.setNoise(1); - measEntry.obsKey.comment = "Position"; + measEntry.setInnov(omc); + measEntry.setNoise(1); + measEntry.obsKey.comment = "Position"; - kfMeasEntryList.push_back(measEntry); + kfMeasEntryList.push_back(measEntry); - KFMeas combinedMeas(kfState, kfMeasEntryList, time); + KFMeas combinedMeas(kfState, kfMeasEntryList, time); - kfState.filterKalman(std::cout, combinedMeas, "", true); - } - } + kfState.filterKalman(std::cout, combinedMeas, "", true); + } + } } - -void doDebugs() -{ - -} +void doDebugs() {} diff --git a/src/cpp/common/debug.hpp b/src/cpp/common/debug.hpp index 76611491b..74917bba4 100644 --- a/src/cpp/common/debug.hpp +++ b/src/cpp/common/debug.hpp @@ -1,8 +1,3 @@ - #pragma once - -void doDebugs(); - -void plumber(); - -void walkthrough(); \ No newline at end of file +void doDebugs(); +void plumber(); diff --git a/src/cpp/common/eigenIncluder.hpp b/src/cpp/common/eigenIncluder.hpp index 6258cdcd7..e71aef739 100644 --- a/src/cpp/common/eigenIncluder.hpp +++ b/src/cpp/common/eigenIncluder.hpp @@ -1,187 +1,170 @@ - #pragma once - -#include - -using std::isnan; - +#define EIGEN_DENSEBASE_PLUGIN "3rdparty/EigenDenseBaseAddons.h" #include -#define EIGEN_DENSEBASE_PLUGIN "EigenDenseBaseAddons.h" - - +#include +#include #include #include +#include +#include +#include #include #include #include -#include -#include -#include -#include -using Eigen::SimplicialLLT; -using Eigen::SimplicialLDLT; using Eigen::COLAMDOrdering; using Eigen::LDLT; using Eigen::LLT; -using Eigen::PartialPivLU; using Eigen::Matrix; -using Eigen::MatrixXd; using Eigen::Matrix2d; using Eigen::Matrix3d; using Eigen::Matrix4d; +using Eigen::MatrixXd; +using Eigen::PartialPivLU; +using Eigen::SimplicialLDLT; +using Eigen::SimplicialLLT; using Eigen::VectorXd; -using Array6d = Eigen::Array; -using Vector6d = Eigen::Vector; -using Matrix6d = Eigen::Matrix; -using Array10d = Eigen::Array; -using Vector10d = Eigen::Vector; -using Matrix10d = Eigen::Matrix; -using Eigen::Vector4d; -using Eigen::Vector3d; -using Eigen::Vector2d; +using std::isnan; +using Array6d = Eigen::Array; +using Vector6d = Eigen::Vector; +using Matrix6d = Eigen::Matrix; +using Array10d = Eigen::Array; +using Vector10d = Eigen::Vector; +using Matrix10d = Eigen::Matrix; +using Eigen::ArrayXd; +using Eigen::Map; using Eigen::MatrixXi; +using Eigen::Quaterniond; using Eigen::SparseMatrix; -using Eigen::SparseVector; using Eigen::SparseQR; -using Eigen::Map; -using Eigen::Quaterniond; +using Eigen::SparseVector; using Eigen::Triplet; -using Eigen::ArrayXd; +using Eigen::Vector2d; +using Eigen::Vector3d; +using Eigen::Vector4d; using Eigen::placeholders::all; -typedef Eigen::Array ArrayXb; +typedef Eigen::Array ArrayXb; template using Vector = Matrix; - struct Vector3dInit : Vector3d { - Vector3dInit() - { - Vector3d::setZero(); - } + Vector3dInit() { Vector3d::setZero(); } - Vector3dInit& operator=(const Vector3d in) - { - Vector3d::operator=(in); + Vector3dInit& operator=(const Vector3d in) + { + Vector3d::operator=(in); - return *this; - } + return *this; + } }; struct VectorEnu : Vector3d { - VectorEnu() - { - Vector3d::setZero(); - } + VectorEnu() { Vector3d::setZero(); } - VectorEnu(const Vector3d& in) - { - Vector3d::operator=(in); - } + VectorEnu(const Vector3d& in) { Vector3d::operator=(in); } - VectorEnu& operator=(const Vector3d in) - { - Vector3d::operator=(in); + VectorEnu& operator=(const Vector3d in) + { + Vector3d::operator=(in); - return *this; - } + return *this; + } - VectorEnu operator*(const double rhs) { return Vector3d(((Vector3d)*this) * ( rhs)); } - VectorEnu operator-(const VectorEnu& rhs) { return Vector3d(((Vector3d)*this) - ((Vector3d) rhs)); } - VectorEnu operator+(const VectorEnu& rhs) { return Vector3d(((Vector3d)*this) + ((Vector3d) rhs)); } + VectorEnu operator*(const double rhs) { return Vector3d(((Vector3d) * this) * (rhs)); } + VectorEnu operator-(const VectorEnu& rhs) + { + return Vector3d(((Vector3d) * this) - ((Vector3d)rhs)); + } + VectorEnu operator+(const VectorEnu& rhs) + { + return Vector3d(((Vector3d) * this) + ((Vector3d)rhs)); + } - double& e() { return x(); } - double& n() { return y(); } - double& u() { return z(); } + double& e() { return x(); } + double& n() { return y(); } + double& u() { return z(); } - double& r() { return x(); } - double& f() { return y(); } + double& r() { return x(); } + double& f() { return y(); } }; struct VectorEcef : Vector3d { - VectorEcef() - { - Vector3d::setZero(); - } - - VectorEcef(const Vector3d& in) - { - Vector3d::operator=(in); - } - - VectorEcef& operator=(const Vector3d& in) - { - Vector3d::operator=(in); - - return *this; - } - - VectorEcef operator*(const double rhs) { return Vector3d(((Vector3d)*this) * ( rhs)); } - VectorEcef operator-(const VectorEcef& rhs) { return Vector3d(((Vector3d)*this) - ((Vector3d) rhs)); } - VectorEcef operator+(const VectorEcef& rhs) { return Vector3d(((Vector3d)*this) + ((Vector3d) rhs)); } + VectorEcef() { Vector3d::setZero(); } + + VectorEcef(const Vector3d& in) { Vector3d::operator=(in); } + + VectorEcef& operator=(const Vector3d& in) + { + Vector3d::operator=(in); + + return *this; + } + + VectorEcef operator*(const double rhs) { return Vector3d(((Vector3d) * this) * (rhs)); } + VectorEcef operator-(const VectorEcef& rhs) + { + return Vector3d(((Vector3d) * this) - ((Vector3d)rhs)); + } + VectorEcef operator+(const VectorEcef& rhs) + { + return Vector3d(((Vector3d) * this) + ((Vector3d)rhs)); + } }; struct VectorEci : Vector3d { - VectorEci() - { - Vector3d::setZero(); - } - - VectorEci(const Vector3d& in) - { - Vector3d::operator=(in); - } - - VectorEci& operator=(const Vector3d& in) - { - Vector3d::operator=(in); - - return *this; - } - - VectorEci operator*(const double rhs) const { return Vector3d(((Vector3d)*this) * ( rhs)); } - VectorEci operator-(const VectorEci& rhs) const { return Vector3d(((Vector3d)*this) - ((Vector3d) rhs)); } - VectorEci operator+(const VectorEci& rhs) const { return Vector3d(((Vector3d)*this) + ((Vector3d) rhs)); } + VectorEci() { Vector3d::setZero(); } + + VectorEci(const Vector3d& in) { Vector3d::operator=(in); } + + VectorEci& operator=(const Vector3d& in) + { + Vector3d::operator=(in); + + return *this; + } + + VectorEci operator*(const double rhs) const { return Vector3d(((Vector3d) * this) * (rhs)); } + VectorEci operator-(const VectorEci& rhs) const + { + return Vector3d(((Vector3d) * this) - ((Vector3d)rhs)); + } + VectorEci operator+(const VectorEci& rhs) const + { + return Vector3d(((Vector3d) * this) + ((Vector3d)rhs)); + } }; struct VectorPos : Vector3d { - VectorPos() - { - Vector3d::setZero(); - } + VectorPos() { Vector3d::setZero(); } - VectorPos(const Vector3d& in) - { - Vector3d::operator=(in); - } + VectorPos(const Vector3d& in) { Vector3d::operator=(in); } - VectorPos& operator=(const Vector3d& in) - { - Vector3d::operator=(in); + VectorPos& operator=(const Vector3d& in) + { + Vector3d::operator=(in); - return *this; - } + return *this; + } - double& lat() { return x(); } - double& lon() { return y(); } - double& hgt() { return z(); } + double& lat() { return x(); } + double& lon() { return y(); } + double& hgt() { return z(); } - const double& lat() const { return x(); } - const double& lon() const { return y(); } - const double& hgt() const { return z(); } + const double& lat() const { return x(); } + const double& lon() const { return y(); } + const double& hgt() const { return z(); } - double latDeg() const; - double lonDeg() const; + double latDeg() const; + double lonDeg() const; }; const Eigen::IOFormat heavyFmt(Eigen::FullPrecision, 0, ",\t", ",\n", "[", "]", "[", "]"); const Eigen::IOFormat lightFmt(Eigen::FullPrecision, 0, ",\t", ",\n", "", "", "[", "]"); - diff --git a/src/cpp/common/enumHelpers.hpp b/src/cpp/common/enumHelpers.hpp new file mode 100644 index 000000000..dce010aad --- /dev/null +++ b/src/cpp/common/enumHelpers.hpp @@ -0,0 +1,143 @@ +#pragma once + +#include +#include +#include +#include "3rdparty/magic_enum.hpp" + +/** + * Helper functions to simplify magic_enum usage and provide + * drop-in replacements for BETTER_ENUM functionality + */ + +// Enum to string conversion (replaces ._to_string()) +template +inline std::string enum_to_string(E value) +{ + return 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{}) +{ + return magic_enum::enum_cast(str).value_or(default_value); +} + +// String to enum with optional return +template +inline std::optional string_to_enum_opt(const std::string& str) +{ + return magic_enum::enum_cast(str); +} + +// Case-insensitive string to enum (replaces ::_from_string_nocase()) +template +inline E string_to_enum_nocase(const std::string& str, E default_value = E{}) +{ + auto result = magic_enum::enum_cast(str, magic_enum::case_insensitive); + return result.value_or(default_value); +} + +// Case-insensitive string to enum with exception (for compatibility) +template +inline E string_to_enum_nocase_throw(const char* str) +{ + auto result = magic_enum::enum_cast(str, magic_enum::case_insensitive); + if (!result.has_value()) + { + throw std::runtime_error(std::string("Invalid enum value: ") + str); + } + return result.value(); +} + +template +inline E string_to_enum_nocase_throw(std::string str) +{ + return string_to_enum_nocase_throw(str.c_str()); +} + +// Integer to enum conversion (replaces ::_from_integral()) +template +inline E int_to_enum(int value) +{ + return static_cast(value); +} + +// Integer to enum with validation +template +inline std::optional int_to_enum_opt(int value) +{ + return magic_enum::enum_cast(value); +} + +// Integer to enum with default fallback +template +inline E int_to_enum_safe(int value, E default_value = E{}) +{ + return magic_enum::enum_cast(value).value_or(default_value); +} + +// Enum to int helper (explicit conversion) +template +inline int enum_to_int(E value) +{ + return static_cast(value); +} + +// Enum arithmetic helper - add offset to enum +template +inline E enum_add(E base, int offset) +{ + return static_cast(static_cast(base) + offset); +} + +// Enum arithmetic helper - subtract to get offset +template +inline int enum_diff(E a, E b) +{ + return static_cast(a) - static_cast(b); +} + +// Check if string is valid enum value (replaces ::_is_valid()) +template +inline bool is_valid_enum_string(const std::string& str) +{ + return magic_enum::enum_cast(str).has_value(); +} + +// Check if integer is valid enum value (replaces ::_is_valid()) +template +inline bool is_valid_enum_int(int value) +{ + return magic_enum::enum_cast(value).has_value(); +} + +// Get enum count (replaces ::_size()) +template +inline constexpr std::size_t enum_count() +{ + return magic_enum::enum_count(); +} + +// Get enum values array (replaces ::_values()) +template +inline constexpr auto enum_values() +{ + return magic_enum::enum_values(); +} + +// Get enum names array +template +inline constexpr auto enum_names() +{ + return magic_enum::enum_names(); +} + +// Stream output operator for enums (enables logging/printing) +template +inline typename std::enable_if::value, std::ostream&>::type +operator<<(std::ostream& os, E value) +{ + return os << enum_to_string(value); +} diff --git a/src/cpp/common/enums.h b/src/cpp/common/enums.h index 7e4ca8dcb..0b43036d4 100644 --- a/src/cpp/common/enums.h +++ b/src/cpp/common/enums.h @@ -1,1067 +1,1614 @@ +#pragma once +#include -#pragma once +// Reduce magic_enum memory usage during compilation +#define MAGIC_ENUM_RANGE_MIN 0 +#define MAGIC_ENUM_RANGE_MAX 128 -#define BETTER_ENUMS_DEFAULT_CONSTRUCTOR(Enum) \ -public: \ - Enum() = default; +// Forward declarations for magic_enum customization +enum class E_ObsCode : int; +enum class E_Period : int; +enum class RtcmMessageType : std::uint16_t; +enum class IgsSSRSubtype : std::uint16_t; -#define BETTER_ENUMS_MACRO_FILE "enum_macros.h" //BETTER_ENUM Extended -#include -#include "enum.h" //BETTER_ENUM +#include "3rdparty/magic_enum.hpp" + +#include "common/enumHelpers.hpp" typedef enum { - FTYPE_NONE, - - /* Base carrier frequencies */ - F1 = 1, // 1575.42 MHz: GPS L1, GAL E1, BDS B1, QZS L1, SBS L1, - F2 = 2, // 1227.60 MHz: GPS L2, QZS L2, - F5 = 5, // 1176.45 MHz: GPS L5, GAL E5A, BDS B2A, QZS L5, SBS L5 - F6 = 6, // 1278.75 MHz: GAL E6, QZS L6, - F7 = 7, // 1207.14 MHz: GAL E5B, BDS B2B - F8 = 8, // 1191.795 MHz: GAL E5, BDS B2, - G1 = 11, // ~1602 MHz: GLO G1, - G2 = 12, // ~1246 MHz: GLO G2, - G3 = 13, // 1202.025 MHz: GLO G3, - G4 = 14, // 1600.995 MHz GLO G1A - G6 = 16, // 1248.08 MHz: GLO G2A, - B1 = 21, // 1561.098 MHz: BDS B2-1, - B3 = 23, // 1268.52 MHz: BDS B3 - I9 = 39, // 2492.028 MHz: IRN S9 - NUM_FTYPES, + NONE, + + /* Base carrier frequencies */ + F1 = 1, // 1575.42 MHz: GPS L1, GAL E1, BDS B1, QZS L1, SBS L1, + F2 = 2, // 1227.60 MHz: GPS L2, QZS L2, + F5 = 5, // 1176.45 MHz: GPS L5, GAL E5A, BDS B2A, QZS L5, SBS L5 + F6 = 6, // 1278.75 MHz: GAL E6, QZS L6, + F7 = 7, // 1207.14 MHz: GAL E5B, BDS B2B + F8 = 8, // 1191.795 MHz: GAL E5, BDS B2, + G1 = 11, // ~1602 MHz: GLO G1, + G2 = 12, // ~1246 MHz: GLO G2, + G3 = 13, // 1202.025 MHz: GLO G3, + G4 = 14, // 1600.995 MHz GLO G1A + G6 = 16, // 1248.08 MHz: GLO G2A, + B1 = 21, // 1561.098 MHz: BDS B2-1, + B3 = 23, // 1268.52 MHz: BDS B3 + I9 = 39, // 2492.028 MHz: IRN S9 + NUM_FTYPES, } E_FType; typedef enum { - CODE, - PHAS, - NUM_MEAS_TYPES + CODE, + PHAS, + NUM_MEAS_TYPES } E_MeasType; typedef enum { - SVH_OK, - SVH_UNHEALTHY = -1 //implicitly used in rtcm + SVH_OK, + SVH_UNHEALTHY = -1 // implicitly used in rtcm } E_Svh; +/** + * Warning: do not change the order, used by RAIM + * The larger is the number better the solution is. + * FAILED -> SINGLE_X -> SINGLE (-> PPP) + */ +enum class E_Solution : short int +{ + NONE, + FAILED, + SINGLE_X, + SINGLE, + PPP +}; + +enum class E_Radio : short int +{ + TRANSMITTER, + RECEIVER +}; + +enum class E_Sys : short int +{ + NONE, + GPS, + GAL, + GLO, + QZS, + SBS, + BDS, + LEO, + SUPPORTED, + IRN, + IMS, + COMB +}; + +enum class E_Block : short int +{ + UNKNOWN, + GPS_I, + GPS_II, + GPS_IIA, + GPS_IIR_A, + GPS_IIR_B, + GPS_IIR_M, + GPS_IIF, + GPS_IIIA, + GLO_M, + GLO, + GLO_K1A, + GLO_K1B, + GLO_K2, + GLO_MP, // GLO-M+ + GAL_0A, + GAL_0B, + GAL_1, + GAL_2, + BDS_2M, + BDS_2G, + BDS_2I, + BDS_3SI_SECM, + BDS_3SM_CAST, + BDS_3SI_CAST, + BDS_3SM_SECM, + BDS_3M_CAST, + BDS_3M_SECM_A, + BDS_3G, + BDS_3I, + BDS_3M_SECM_B, + QZS_1, + QZS_2I, + QZS_2G, + QZS_2A, + IRS_1I, + IRS_1G, + IRS_2G, + SBS, + LEO +}; + +enum class E_OffsetType : short int +{ + UNSPECIFIED, + APC, + COM +}; + +enum class E_TrigType : short int +{ + NONE, + COS, + SIN +}; + +enum class E_EmpAxis : short int +{ + NONE, + D, + Y, + B, + R, + T, + N, + P, + Q +}; + +enum class KF : short int +{ + NONE, + ONE, + ALL, + + REC_POS, + REC_VEL, + REC_POS_RATE = REC_VEL, + REC_ACC, + + STRAIN_RATE, + + POS, + VEL, + ACC, + + HEADING, + + ORIENTATION, + + REF_SYS_BIAS, + + BEGIN_CLOCK_STATES, + REC_CLOCK, + REC_SYS_BIAS = REC_CLOCK, + REC_CLOCK_RATE, + REC_SYS_BIAS_RATE = REC_CLOCK_RATE, + REC_CLOCK_RATE_GM, + REC_SYS_BIAS_RATE_GM = REC_CLOCK_RATE_GM, + + SAT_CLOCK, + SAT_CLOCK_RATE, + SAT_CLOCK_RATE_GM, + END_CLOCK_STATES, + + TROP, + TROP_GRAD, + TROP_MODEL, + + IONOSPHERIC, + IONO_STEC, + REC_PCO_X, + REC_PCO_Y, + REC_PCO_Z, + SAT_PCO_X, + SAT_PCO_Y, + SAT_PCO_Z, + + REC_PCV, + + ANT_DELTA, + + EOP, + EOP_RATE, + + CALC, + + SLR_REC_RANGE_BIAS, + SLR_REC_TIME_BIAS, + + XFORM_XLATE, + XFORM_RTATE, + XFORM_SCALE, + XFORM_DELAY, + + XFORM_XLATE_RATE, + XFORM_RTATE_RATE, + XFORM_SCALE_RATE, + XFORM_DELAY_RATE, + + AMBIGUITY, + CODE_BIAS, + PHASE_BIAS, + + Z_AMB, + + REFERENCE, + + BEGIN_MEAS_STATES, + CODE_MEAS, + PHAS_MEAS, + LASER_MEAS, + PSEUDO_MEAS, + ORBIT_MEAS, + FILTER_MEAS, + END_MEAS_STATES, + + BEGIN_ORBIT_STATES, + ORBIT, + CR, + CD, + EMP_D_0, + EMP_D_1, + EMP_D_2, + EMP_D_3, + EMP_D_4, + + EMP_Y_0, + EMP_Y_1, + EMP_Y_2, + EMP_Y_3, + EMP_Y_4, + + EMP_B_0, + EMP_B_1, + EMP_B_2, + EMP_B_3, + EMP_B_4, + + EMP_R_0, + EMP_R_1, + EMP_R_2, + EMP_R_3, + EMP_R_4, + + EMP_T_0, + EMP_T_1, + EMP_T_2, + EMP_T_3, + EMP_T_4, + + EMP_N_0, + EMP_N_1, + EMP_N_2, + EMP_N_3, + EMP_N_4, + + EMP_P_0, + EMP_P_1, + EMP_P_2, + EMP_P_3, + EMP_P_4, + + EMP_Q_0, + EMP_Q_1, + EMP_Q_2, + EMP_Q_3, + EMP_Q_4, + END_ORBIT_STATES, + + BEGIN_INERTIAL_STATES, + GYRO_BIAS, + GYRO_SCALE, + ACCL_BIAS, + ACCL_SCALE, + IMU_OFFSET, + END_INERTIAL_STATES, + + RANGE +}; + +enum class E_StateComponent : short int +{ + NONE, + + X, + Y, + Z, + VX, + VY, + VZ, + + E, + N, + U, + + XP, + YP, + UT1_UTC, + + W, + QX, + QY, + QZ +}; + +enum class KEPLER : short int +{ + LX, + LY, + LZ, + EU, + EV, + M +}; + +enum class E_PolyType : short int +{ + CONSTANT, + LAT, + LON, + LAT_LON, + LAT_SQRD, + LON_SQRD +}; + +enum class E_BasisType : short int +{ + POLYNOMIAL, + GRIDPOINT +}; + +enum class E_Relativity : short int +{ + OFF, + ON +}; + +enum class E_FilterStage : int +{ + LSQ, + PREFIT, + POSTFIT +}; + +enum class E_ChiSqMode : int +{ + INNOVATION, + MEASUREMENT, + STATE +}; + +enum class E_TropModel : int +{ + STANDARD, + SBAS, + VMF3, + GPT2, + CSSR +}; + +enum class E_NoiseModel : int +{ + UNIFORM, + ELEVATION_DEPENDENT +}; + +enum class E_IonoModel : int +{ + NONE, + MEAS_OUT, + BSPLINE, + SPHERICAL_CAPS, + SPHERICAL_HARMONICS, + LOCAL +}; + +enum class E_IonoMode : int +{ + OFF, ///< ionosphere option: correction off + BROADCAST, ///< ionosphere option: broadcast model + SBAS, ///< ionosphere option: SBAS model + IONO_FREE_LINEAR_COMBO, ///< ionosphere option: L1/L2 or L1/L5 iono-free LC + ESTIMATE, ///< ionosphere option: estimation + TOTAL_ELECTRON_CONTENT, ///< ionosphere option: IONEX TEC model + QZS, ///< ionosphere option: QZSS broadcast model + LEX, ///< ionosphere option: QZSS LEX ionospehre + STEC ///< ionosphere option: SLANT TEC model +}; + +enum class E_IonoMapFn : int +{ + SLM, ///< single layer model mapping function + + MSLM, ///< modified single layer model mapping function + MLM, ///< multiple layer model mapping function + KLOBUCHAR ///< Klobuchar mapping function +}; + +enum class E_IonoFrame : int +{ + EARTH_FIXED, ///< Earth-fixed reference frame + SUN_FIXED ///< Sun-fixed reference frame +}; + +enum class E_Period : int +{ + SECOND = 1, + MINUTE, + HOUR, + DAY, + // WEEK, + // YEAR, + + SECONDS = SECOND, + MINUTES = MINUTE, + HOURS = HOUR, + DAYS = DAY, + // WEEKS = WEEK, + // YEARS = YEAR, + SEC = SECOND, + MIN = MINUTE, + HR = HOUR, + DY = DAY, + // WK = WEEK, + // YR = YEAR, + SECS = SECOND, + MINS = MINUTE, + HRS = HOUR, + DYS = DAY, + // WKS = WEEK, + // YRS = YEAR, + SQRT_SEC = SECOND, + SQRT_MIN = MINUTE, + SQRT_HR = HOUR, + SQRT_DY = DAY, + // SQRT_WK = WEEK, + // SQRT_YR = YEAR, + SQRT_SECS = SECOND, + SQRT_MINS = MINUTE, + SQRT_HRS = HOUR, + SQRT_DYS = DAY, + // SQRT_WKS = WEEK, + // SQRT_YRS = YEAR, + SQRT_SECOND = SECOND, + SQRT_MINUTE = MINUTE, + SQRT_HOUR = HOUR, + SQRT_DAY = DAY, + // SQRT_WEEK = WEEK, + // SQRT_YEAR = YEAR, + SQRT_SECONDS = SECOND, + SQRT_MINUTES = MINUTE, + SQRT_HOURS = HOUR, + SQRT_DAYS = DAY, + // SQRT_WEEKS = WEEK, + // SQRT_YEARS = YEAR +}; + +// Conversion function from E_Period enum to seconds +inline constexpr int periodToSeconds(E_Period period) +{ + switch (period) + { + case E_Period::SECOND: + return 1; + case E_Period::MINUTE: + return 60; + case E_Period::HOUR: + return 3600; + case E_Period::DAY: + return 86400; + // case E_Period::WEEK: return 604800; + // case E_Period::YEAR: return 31536000; + default: + return 1; + } +} + +enum class E_TimeSys : int +{ + NONE, ///< NONE for unknown + GPST, ///< GPS Time + GLONASST, ///< GLONASS Time + GST, ///< Galileo System Time + BDT, ///< BeiDou Time + QZSST, ///< QZSS Time + TAI, ///< International Atomic Time + UTC, ///< Universal Coordinated Time + UT1, ///< Universal Time corrected for polar motion + TT ///< Terrestrial Time +}; + +enum class E_PosFrame : int +{ + NONE, + XYZ, + NED, + RTN +}; + +enum class E_ObxFrame : short int +{ + OTHER, + ECEF, + ECI, + BCRS +}; + +enum class E_FilterMode : int +{ + LSQ, + KALMAN +}; + +enum class E_Inverter : int +{ + NONE, + INV, + LLT, + LDLT, + COLPIVHQR, + BDCSVD, + JACOBISVD, + FULLPIVLU, + FIRST_UNSUPPORTED = FULLPIVLU, + FULLPIVHQR +}; + +enum class E_MongoType : int +{ + NONE, + STATES, + STATES_AVAILABLE, + RESIDUALS, + TRACE, + LIST +}; + +enum class E_ObsDesc : int +{ + C, // Code / Pseudorange + L, // Phase + D, // Doppler + S, // Raw signal strength (carrier to noise ratio) + X // Receiver channel numbers +}; + +enum class E_ObsCode : int +{ + NONE = 0, ///< none or unknown + L1C = 1, ///< L1C/A,G1C/A,E1C (GPS,GLO,GAL,QZS,SBS) + L1P = 2, ///< L1P,G1P (GPS,GLO) + L1W = 3, ///< L1 Z-track (GPS) + L1Y = 4, ///< L1Y (GPS) + L1M = 5, ///< L1M (GPS) + L1N = 6, ///< L1codeless (GPS) + L1S = 7, ///< L1C(D) (GPS,QZS) + L1L = 8, ///< L1C(P) (GPS,QZS) + L1E = 9, ///< L1C/B (QZS) + L1A = 10, ///< E1A (GAL) + L1B = 11, ///< E1B (GAL) + L1X = 12, ///< E1B+C,L1C(D+P) (GAL,QZS) + L1Z = 13, ///< E1A+B+C,L1-SAIF (GAL,QZS) + L2C = 14, ///< L2C/A,G1C/A (GPS,GLO) + L2D = 15, ///< L2 L1C/A-(P2-P1) (GPS) + L2S = 16, ///< L2C(M) (GPS,QZS) + L2L = 17, ///< L2C(L) (GPS,QZS) + L2X = 18, ///< L2C(M+L),B1-2I+Q (GPS,QZS,BDS) + L2P = 19, ///< L2P,G2P (GPS,GLO) + L2W = 20, ///< L2 Z-track (GPS) + L2Y = 21, ///< L2Y (GPS) + L2M = 22, ///< L2M (GPS) + L2N = 23, ///< L2codeless (GPS) + L5I = 24, ///< L5/E5aI (GPS,GAL,QZS,SBS) + L5Q = 25, ///< L5/E5aQ (GPS,GAL,QZS,SBS) + L5X = 26, ///< L5/E5aI+Q (GPS,GAL,QZS,SBS) + L7I = 27, ///< E5bI,B2aI (GAL,BDS) + L7Q = 28, ///< E5bQ,B2aQ (GAL,BDS) + L7X = 29, ///< E5bI+Q,B2aI+Q (GAL,BDS) + L6A = 30, ///< E6A, L2OCd (GAL,GLO) + L6B = 31, ///< E6B, L2OCp (GAL,GLO) + L6C = 32, ///< E6C, L2OCd+L2OCp (GAL,GLO) + L6X = 33, ///< E6B+C,LEXS+L,B3I+Q (GAL,QZS,BDS) + L6Z = 34, ///< E6A+B+C (GAL) + L6S = 35, ///< L6S (QZS) + L6L = 36, ///< L6L (QZS) + L8I = 37, ///< E5(a+b)I (GAL) + L8Q = 38, ///< E5(a+b)Q (GAL) + L8X = 39, ///< E5(a+b)I+Q (GAL, BDS) + L2I = 40, ///< B1-2I (BDS) + L2Q = 41, ///< B1-2Q (BDS) + L6I = 42, ///< B3I (BDS) + L6Q = 43, ///< B3Q (BDS) + L3I = 44, ///< G3I (GLO) + L3Q = 45, ///< G3Q (GLO) + L3X = 46, ///< G3I+Q (GLO) + L1I = 47, ///< B1I (BDS) + L1Q = 48, ///< B1Q (BDS) + L4A = 49, ///< L1OCd (GLO) + L4B = 50, ///< L1OCp (GLO) + L4X = 51, ///< L1OCd+L1OCp (GLO) + L6E = 52, ///< L6E (QZS) + L1D = 53, ///< B1D (BDS) + L5D = 54, ///< B2aD (BDS) + L5P = 55, ///< B2aP (BDS) + L9A = 57, ///< S9 A SPS (IRN) + L9B = 58, ///< S9 B RS(D) (IRN) + L9C = 59, ///< S9 C RS(P) (IRN) + L9X = 60, ///< S9 B+C (IRN) + L5A = 61, ///< L5 A SPS (IRN) + L5B = 62, ///< L5 B RS(D) (IRN) + L5C = 63, ///< L5 C RS(P) (IRN) + L5Z = 64, ///< L5 B+C (IRN) + L6D = 65, ///< L6 Data (BDS) + L6P = 66, ///< L6 Pilot (BDS) + L7D = 67, ///< L7 Data (BDS) + L7P = 68, ///< L7 Pilot (BDS) + L7Z = 69, ///< L7 Data+Pilot (BDS) + L8D = 70, ///< L8 Data (BDS) + L8P = 71, ///< L8 Pilot (BDS) + AUTO = 99 +}; + +enum class E_ObsCode2 : int +{ + NONE, + P1, + P2, + C1, + C2, + C3, + C4, + C5, + C6, + C7, + C8, + L1, + L2, + L3, + L4, + L5, + L6, + L7, + L8, + LA +}; + +enum class E_ARmode : short int +{ + OFF, + ROUND, + ITER_RND, + BOOTST, + LAMBDA, + LAMBDA_ALT, + LAMBDA_AL2, + LAMBDA_BIE +}; + +enum class E_NavRecType : short int +{ + NONE, ///< NONE for unknown */ + EPH, ///< Ephemerides data including orbit, clock, biases, accuracy and status parameters */ + STO, ///< System Time and UTC proxy offset parameters */ + EOP, ///< Earth Orientation Parameters */ + ION ///< Global/Regional ionospheric model parameters */ +}; + +enum class E_NavMsgType : short int +{ + NONE, ///< NONE for unknown + LNAV, ///< GPS/QZSS/NavIC Legacy Navigation Messages + FDMA, ///< GLONASS Legacy FDMA Navigation Message + FNAV, ///< Galileo Free Navigation Message + INAV, ///< Galileo Integrity Navigation Message + IFNV, ///< Galileo INAV or FNAV Navigation Message + D1, ///< BeiDou-2/3 MEO/IGSO Navigation Message + D2, ///< BeiDou-2/3 GEO Navigation Message + D1D2, ///< BeiDou-2/3 MEO/IGSO and GEO Navigation Message + SBAS, ///< SBAS Navigation Message + CNAV, ///< GPS/QZSS CNAV Navigation Message + CNV1, ///< BeiDou-3 CNAV-1 Navigation Message + CNV2, ///< GPS/QZSS CNAV-2 Navigation Message BeiDou-3 CNAV-2 Navigation Message + CNV3, ///< BeiDou-3 CNAV-3 Navigation Message + CNVX ///< GPS/QZSS CNAV or CNAV-2 Navigation Message BeiDou-3 CNAV-1, CNAV-2 or CNAV-3 + ///< Navigation +}; +///< Message + +enum class E_SatType : short int +{ + NONE, + GEO, + IGSO, + MEO +}; + +enum class E_StoCode : short int +{ + NONE, + GPUT, + GLUT, + GLGP, + GAUT, + GAGP, + GPGA = GAGP, // From RINEX 3.04 the GPGA label is replaced by GAGP, while the value and sign + // for the Galileo minus GPS time offset remains unchanged. + GAGL, + BDUT, + BDGP, + BDGL, + BDGA, + QZUT, + QZGP, + QZGL, + QZGA, + QZBD, + IRUT, + IRGP, + IRGL, + IRGA, + IRBD, + IRQZ, + SBUT, + SBGP, + SBGL, + SBGA, + SBBD, + SBQZ, + SBIR +}; + +enum class E_UtcId : short int +{ + NONE, + UTC_USNO, + UTC_SU, + UTCGAL, + UTC_NTSC, + UTC_NICT, + UTC_NPLI, + UTCIRN, + UTC_OP, + UTC_NIST +}; + +enum class E_SbasId : short int +{ + NONE, + WAAS, + EGNOS, + MSAS, + GAGAN, + SDCM, + BDSBAS, + KASS, + A_SBAS, + SPAN +}; + +enum class RtcmMessageType : uint16_t +{ + NONE = 0, + + GPS_EPHEMERIS = 1, + GLO_EPHEMERIS, + BDS_EPHEMERIS, + QZS_EPHEMERIS, + GAL_FNAV_EPHEMERIS, + GAL_INAV_EPHEMERIS, + + GPS_SSR_ORB_CORR, + GPS_SSR_CLK_CORR, + GPS_SSR_CODE_BIAS, + GPS_SSR_COMB_CORR, + GPS_SSR_URA, + GPS_SSR_HR_CLK_CORR, + GPS_SSR_PHASE_BIAS, + + GLO_SSR_ORB_CORR, + GLO_SSR_CLK_CORR, + GLO_SSR_CODE_BIAS, + GLO_SSR_COMB_CORR, + 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, + + GAL_SSR_ORB_CORR, + GAL_SSR_CLK_CORR, + GAL_SSR_CODE_BIAS, + GAL_SSR_COMB_CORR, + GAL_SSR_URA, + GAL_SSR_HR_CLK_CORR, + GAL_SSR_PHASE_BIAS, + + QZS_SSR_ORB_CORR, + QZS_SSR_CLK_CORR, + QZS_SSR_CODE_BIAS, + QZS_SSR_COMB_CORR, + QZS_SSR_URA, + QZS_SSR_HR_CLK_CORR, + QZS_SSR_PHASE_BIAS, + + SBS_SSR_ORB_CORR, + SBS_SSR_CLK_CORR, + SBS_SSR_CODE_BIAS, + SBS_SSR_COMB_CORR, + SBS_SSR_URA, + SBS_SSR_HR_CLK_CORR, + SBS_SSR_PHASE_BIAS, + + BDS_SSR_ORB_CORR, + BDS_SSR_CLK_CORR, + BDS_SSR_CODE_BIAS, + BDS_SSR_COMB_CORR, + BDS_SSR_URA, + BDS_SSR_HR_CLK_CORR, + BDS_SSR_PHASE_BIAS, + + COMPACT_SSR, + IGS_SSR, + CUSTOM +}; + +// Conversion from RtcmMessageType enum to actual RTCM message number +inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) +{ + switch (type) + { + case RtcmMessageType::NONE: + return 0; + case RtcmMessageType::GPS_EPHEMERIS: + return 1019; + case RtcmMessageType::GLO_EPHEMERIS: + return 1020; + case RtcmMessageType::BDS_EPHEMERIS: + return 1042; + case RtcmMessageType::QZS_EPHEMERIS: + return 1044; + case RtcmMessageType::GAL_FNAV_EPHEMERIS: + return 1045; + case RtcmMessageType::GAL_INAV_EPHEMERIS: + return 1046; + + case RtcmMessageType::GPS_SSR_ORB_CORR: + return 1057; + case RtcmMessageType::GPS_SSR_CLK_CORR: + return 1058; + case RtcmMessageType::GPS_SSR_CODE_BIAS: + return 1059; + case RtcmMessageType::GPS_SSR_COMB_CORR: + return 1060; + case RtcmMessageType::GPS_SSR_URA: + return 1061; + case RtcmMessageType::GPS_SSR_HR_CLK_CORR: + return 1062; + case RtcmMessageType::GPS_SSR_PHASE_BIAS: + return 1265; + + case RtcmMessageType::GLO_SSR_ORB_CORR: + return 1063; + case RtcmMessageType::GLO_SSR_CLK_CORR: + return 1064; + case RtcmMessageType::GLO_SSR_CODE_BIAS: + return 1065; + case RtcmMessageType::GLO_SSR_COMB_CORR: + return 1066; + case RtcmMessageType::GLO_SSR_URA: + return 1067; + case RtcmMessageType::GLO_SSR_HR_CLK_CORR: + return 1068; + case RtcmMessageType::GLO_SSR_PHASE_BIAS: + return 1266; + + case RtcmMessageType::MSM4_GPS: + return 1074; + case RtcmMessageType::MSM5_GPS: + return 1075; + case RtcmMessageType::MSM6_GPS: + return 1076; + case RtcmMessageType::MSM7_GPS: + return 1077; + + case RtcmMessageType::MSM4_GLONASS: + return 1084; + case RtcmMessageType::MSM5_GLONASS: + return 1085; + case RtcmMessageType::MSM6_GLONASS: + return 1086; + case RtcmMessageType::MSM7_GLONASS: + return 1087; + + case RtcmMessageType::MSM4_GALILEO: + return 1094; + case RtcmMessageType::MSM5_GALILEO: + return 1095; + case RtcmMessageType::MSM6_GALILEO: + return 1096; + case RtcmMessageType::MSM7_GALILEO: + return 1097; + + case RtcmMessageType::MSM4_QZSS: + return 1114; + case RtcmMessageType::MSM5_QZSS: + return 1115; + case RtcmMessageType::MSM6_QZSS: + return 1116; + case RtcmMessageType::MSM7_QZSS: + return 1117; + + case RtcmMessageType::MSM4_BEIDOU: + return 1124; + case RtcmMessageType::MSM5_BEIDOU: + return 1125; + case RtcmMessageType::MSM6_BEIDOU: + return 1126; + case RtcmMessageType::MSM7_BEIDOU: + return 1127; + + case RtcmMessageType::GAL_SSR_ORB_CORR: + return 1240; + case RtcmMessageType::GAL_SSR_CLK_CORR: + return 1241; + case RtcmMessageType::GAL_SSR_CODE_BIAS: + return 1242; + case RtcmMessageType::GAL_SSR_COMB_CORR: + return 1243; + case RtcmMessageType::GAL_SSR_URA: + return 1244; + case RtcmMessageType::GAL_SSR_HR_CLK_CORR: + return 1245; + case RtcmMessageType::GAL_SSR_PHASE_BIAS: + return 1267; + + case RtcmMessageType::QZS_SSR_ORB_CORR: + return 1246; + case RtcmMessageType::QZS_SSR_CLK_CORR: + return 1247; + case RtcmMessageType::QZS_SSR_CODE_BIAS: + return 1248; + case RtcmMessageType::QZS_SSR_COMB_CORR: + return 1249; + case RtcmMessageType::QZS_SSR_URA: + return 1250; + case RtcmMessageType::QZS_SSR_HR_CLK_CORR: + return 1251; + case RtcmMessageType::QZS_SSR_PHASE_BIAS: + return 1268; + + case RtcmMessageType::SBS_SSR_ORB_CORR: + return 1252; + case RtcmMessageType::SBS_SSR_CLK_CORR: + return 1253; + case RtcmMessageType::SBS_SSR_CODE_BIAS: + return 1254; + case RtcmMessageType::SBS_SSR_COMB_CORR: + return 1255; + case RtcmMessageType::SBS_SSR_URA: + return 1256; + case RtcmMessageType::SBS_SSR_HR_CLK_CORR: + return 1257; + case RtcmMessageType::SBS_SSR_PHASE_BIAS: + return 1269; + + case RtcmMessageType::BDS_SSR_ORB_CORR: + return 1258; + case RtcmMessageType::BDS_SSR_CLK_CORR: + return 1259; + case RtcmMessageType::BDS_SSR_CODE_BIAS: + return 1260; + case RtcmMessageType::BDS_SSR_COMB_CORR: + return 1261; + case RtcmMessageType::BDS_SSR_URA: + return 1262; + case RtcmMessageType::BDS_SSR_HR_CLK_CORR: + 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; + } +} + +// Conversion from RTCM message number to RtcmMessageType enum +inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) +{ + switch (msgNum) + { + case 0: + return RtcmMessageType::NONE; + case 1019: + return RtcmMessageType::GPS_EPHEMERIS; + case 1020: + return RtcmMessageType::GLO_EPHEMERIS; + case 1042: + return RtcmMessageType::BDS_EPHEMERIS; + case 1044: + return RtcmMessageType::QZS_EPHEMERIS; + case 1045: + return RtcmMessageType::GAL_FNAV_EPHEMERIS; + case 1046: + return RtcmMessageType::GAL_INAV_EPHEMERIS; + + case 1057: + return RtcmMessageType::GPS_SSR_ORB_CORR; + case 1058: + return RtcmMessageType::GPS_SSR_CLK_CORR; + case 1059: + return RtcmMessageType::GPS_SSR_CODE_BIAS; + case 1060: + return RtcmMessageType::GPS_SSR_COMB_CORR; + case 1061: + return RtcmMessageType::GPS_SSR_URA; + case 1062: + 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: + return RtcmMessageType::GLO_SSR_CLK_CORR; + case 1065: + return RtcmMessageType::GLO_SSR_CODE_BIAS; + case 1066: + return RtcmMessageType::GLO_SSR_COMB_CORR; + case 1067: + return RtcmMessageType::GLO_SSR_URA; + case 1068: + return RtcmMessageType::GLO_SSR_HR_CLK_CORR; + case 1266: + return RtcmMessageType::GLO_SSR_PHASE_BIAS; + + case 1074: + return RtcmMessageType::MSM4_GPS; + case 1075: + return RtcmMessageType::MSM5_GPS; + case 1076: + return RtcmMessageType::MSM6_GPS; + case 1077: + return RtcmMessageType::MSM7_GPS; + + case 1084: + return RtcmMessageType::MSM4_GLONASS; + case 1085: + return RtcmMessageType::MSM5_GLONASS; + case 1086: + return RtcmMessageType::MSM6_GLONASS; + case 1087: + return RtcmMessageType::MSM7_GLONASS; + + case 1094: + return RtcmMessageType::MSM4_GALILEO; + case 1095: + return RtcmMessageType::MSM5_GALILEO; + case 1096: + return RtcmMessageType::MSM6_GALILEO; + case 1097: + return RtcmMessageType::MSM7_GALILEO; + + case 1114: + return RtcmMessageType::MSM4_QZSS; + case 1115: + return RtcmMessageType::MSM5_QZSS; + case 1116: + return RtcmMessageType::MSM6_QZSS; + case 1117: + return RtcmMessageType::MSM7_QZSS; + + case 1124: + return RtcmMessageType::MSM4_BEIDOU; + case 1125: + return RtcmMessageType::MSM5_BEIDOU; + case 1126: + return RtcmMessageType::MSM6_BEIDOU; + case 1127: + return RtcmMessageType::MSM7_BEIDOU; + + case 1240: + return RtcmMessageType::GAL_SSR_ORB_CORR; + case 1241: + return RtcmMessageType::GAL_SSR_CLK_CORR; + case 1242: + return RtcmMessageType::GAL_SSR_CODE_BIAS; + case 1243: + return RtcmMessageType::GAL_SSR_COMB_CORR; + case 1244: + return RtcmMessageType::GAL_SSR_URA; + case 1245: + 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: + return RtcmMessageType::QZS_SSR_CLK_CORR; + case 1248: + return RtcmMessageType::QZS_SSR_CODE_BIAS; + case 1249: + return RtcmMessageType::QZS_SSR_COMB_CORR; + case 1250: + return RtcmMessageType::QZS_SSR_URA; + case 1251: + 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: + return RtcmMessageType::SBS_SSR_CLK_CORR; + case 1254: + return RtcmMessageType::SBS_SSR_CODE_BIAS; + case 1255: + return RtcmMessageType::SBS_SSR_COMB_CORR; + case 1256: + return RtcmMessageType::SBS_SSR_URA; + case 1257: + 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: + return RtcmMessageType::BDS_SSR_CLK_CORR; + case 1260: + return RtcmMessageType::BDS_SSR_CODE_BIAS; + case 1261: + return RtcmMessageType::BDS_SSR_COMB_CORR; + case 1262: + return RtcmMessageType::BDS_SSR_URA; + case 1263: + 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; + } +} + +enum class CompactSSRSubtype : unsigned short +{ + NONE = 0, + MSK = 1, + ORB = 2, + CLK = 3, + COD = 4, + PHS = 5, + BIA = 6, + URA = 7, + TEC = 8, + GRD = 9, + SRV = 10, + CMB = 11, + ATM = 12 +}; + +// the order and spacing of these is magic, dont modify +enum class IgsSSRSubtype : unsigned short +{ + NONE = 0, + + GROUP_ORB = 1, + GROUP_CLK = 2, + GROUP_CMB = 3, + GROUP_HRC = 4, + GROUP_COD = 5, + GROUP_PHS = 6, + GROUP_URA = 7, + GROUP_ION = 8, + + GPS_OFFSET = 20, + GPS_ORB = 21, + GPS_CLK = 22, + GPS_CMB = 23, + GPS_HRC = 24, + GPS_COD = 25, + GPS_PHS = 26, + GPS_URA = 27, + + GLO_OFFSET = 40, + GLO_ORB = 41, + GLO_CLK = 42, + GLO_CMB = 43, + GLO_HRC = 44, + GLO_COD = 45, + GLO_PHS = 46, + GLO_URA = 47, + + GAL_OFFSET = 60, + GAL_ORB = 61, + GAL_CLK = 62, + GAL_CMB = 63, + GAL_HRC = 64, + GAL_COD = 65, + GAL_PHS = 66, + GAL_URA = 67, + + QZS_OFFSET = 80, + QZS_ORB = 81, + QZS_CLK = 82, + QZS_CMB = 83, + QZS_HRC = 84, + QZS_COD = 85, + QZS_PHS = 86, + QZS_URA = 87, + + BDS_OFFSET = 100, + BDS_ORB = 101, + BDS_CLK = 102, + BDS_CMB = 103, + BDS_HRC = 104, + BDS_COD = 105, + BDS_PHS = 106, + BDS_URA = 107, + + SBS_OFFSET = 120, + SBS_ORB = 121, + SBS_CLK = 122, + SBS_CMB = 123, + SBS_HRC = 124, + SBS_COD = 125, + SBS_PHS = 126, + SBS_URA = 127, + + IONVTEC = 201 +}; + +enum class E_Source : short int +{ + NONE, + SPP, + CONFIG, + PRECISE, + SSR, + SBAS, + KALMAN, + BROADCAST, + NOMINAL, + MODEL, + PSEUDO, + REMOTE +}; + +enum class E_OrbexRecord : short int +{ + PCS, + VCS, + CPC, + CVC, + POS, + VEL, + CLK, + CRT, + ATT +}; + +enum class E_RTCMSubmessage : short int +{ + TIMESTAMP = 1 +}; + +enum class E_CrdEpochEvent : int +{ + REC_RX = 0, // ground receive time (at SRP) (two-way) + SAT_BN = 1, // spacecraft bounce time (two-way) + REC_TX = 2, // ground transmit time (at SRP) (two-way) + SAT_RX = 3, // spacecraft receive time (one-way) + SAT_TX = 4, // spacecraft transmit time (one-way) + REC_TX_SAT_RX = 5, // ground transmit time (at SRP) and spacecraft receive time (one-way) + SAT_TX_REC_RX = 6, // spacecraft transmit time and ground receive time (at SRP) (one-way) + NONE = 7 +}; + +enum class E_ObsAgeCode : short int +{ + OK, + NO_OBS, + PAST_OBS, + CURRENT_OBS, + FUTURE_OBS +}; + +enum class E_SRPModel : int +{ + NONE, + CANNONBALL, + BOXWING +}; + +enum class E_TidesModel : short int +{ + ELASTIC, + ANELASTIC +}; + +enum class E_ThirdBody : short int +{ + MERCURY = 1, + VENUS = 2, + EARTH = 3, + MARS = 4, + JUPITER = 5, + SATURN = 6, + URANUS = 7, + NEPTUNE = 8, + PLUTO = 9, + MOON = 10, + SUN = 11 +}; // from jpl, do not modify + +enum class E_SigWarning : short int +{ + SIG_OUTG = 1, // Minor (one signal) outage + LOW_ELEV = 2, // Low elevation + CYC_SLIP = 3, // Cycle slip + MAJ_OUTG = 4, // Major (whole satellite/receiver) outage + USR_DISC = 5 +}; // User defined + +enum class E_SlrRangeType : short int // from crd_v2.01.pdf p7 +{ + TX_ONLY = 0, // no ranges (i.e., transmit time only) + ONE_WAY = 1, // one-way ranging + TWO_WAY = 2, // two-way ranging + RX_ONLY = 3, // receive times only + MIXED = 3 +}; // mixed (for real-time data recording, and combination of one- and two-way ranging, e.g., T2L2) + +enum class E_UBXClass : short int +{ + NAV = 0x01, + RXM = 0x02, + INF = 0x04, + ACK = 0x05, + CFG = 0x06, + MON = 0x0A, + AID = 0x0B, + TIM = 0x0D, + ESF = 0x10 +}; + +enum class E_RXMId : short int +{ + SFRBX = 0x13, + MEASX = 0x14, + RAWX = 0x15 +}; + +enum class E_ESFId : short int +{ + MEAS = 0x02 +}; + +enum class E_MEASDataType : short int +{ + NONE = 0, + GYRO_Z = 5, + WHEEL_FL = 6, + WHEEL_FR = 7, + WHEEL_RL = 8, + WHEEL_RR = 9, + SPEED_TICK = 10, + SPEED = 11, + GYRO_TEMP = 12, + GYRO_Y = 13, + GYRO_X = 14, + ACCL_X = 16, + ACCL_Y = 17, + ACCL_Z = 18 +}; + +enum class E_Month : short int +{ + NONE, + JAN, + FEB, + MAR, + ARP, + MAY, + JUN, + JUL, + AUG, + SEP, + OCT, + NOV, + DEC +}; + +enum class E_FilePos : short int +{ + COORD, + CURR_TIME, + TOTAL_TIME, + PCDH, + NUM_SAMPLES, + FOOTER +}; + +enum class E_SSROutTiming : short int +{ + GPS_TIME, + LATEST_CLOCK_ESTIMATE +}; -BETTER_ENUM(E_Solution, short int, - NONE, - SINGLE, - SINGLE_X, - PPP -) - -BETTER_ENUM(E_Radio, short int, - TRANSMITTER, - RECEIVER -) - - -BETTER_ENUM(E_Sys, short int, - NONE, - GPS, - GAL, - GLO, - QZS, - SBS, - BDS, - LEO, - SUPPORTED, - IRN, - IMS, - COMB) - -BETTER_ENUM(E_Block, short int, - UNKNOWN, - GPS_I, - GPS_II, - GPS_IIA, - GPS_IIR_A, - GPS_IIR_B, - GPS_IIR_M, - GPS_IIF, - GPS_IIIA, - GLO_M, - GLO, - GLO_K1A, - GLO_K1B, - GLO_K2, - GLO_MP, // GLO-M+ - GAL_0A, - GAL_0B, - GAL_1, - GAL_2, - BDS_2M, - BDS_2G, - BDS_2I, - BDS_3SI_SECM, - BDS_3SM_CAST, - BDS_3SI_CAST, - BDS_3SM_SECM, - BDS_3M_CAST, - BDS_3M_SECM_A, - BDS_3G, - BDS_3I, - BDS_3M_SECM_B, - QZS_1, - QZS_2I, - QZS_2G, - QZS_2A, - IRS_1I, - IRS_1G, - IRS_2G) - -BETTER_ENUM(E_OffsetType, short int, - UNSPECIFIED, - APC, - COM) - -BETTER_ENUM(E_TrigType, short int, - COS, - SIN) - - -BETTER_ENUM(E_EmpAxis, short int, - NONE, - D, - Y, - B, - R, - T, - N, - P, - Q) - -BETTER_ENUM(KF, short int, - NONE, - ONE, - ALL, - - REC_POS, - REC_VEL, REC_POS_RATE = REC_VEL, - REC_ACC, - - STRAIN_RATE, - - POS, - VEL, - ACC, - - HEADING, - - ORIENTATION, - - REF_SYS_BIAS, - - REC_CLOCK, REC_SYS_BIAS = REC_CLOCK, - REC_CLOCK_RATE, REC_SYS_BIAS_RATE = REC_CLOCK_RATE, - REC_CLOCK_RATE_GM, REC_SYS_BIAS_RATE_GM = REC_CLOCK_RATE_GM, - - SAT_CLOCK, - SAT_CLOCK_RATE, - SAT_CLOCK_RATE_GM, - - TROP, - TROP_GRAD, - TROP_MODEL, - - IONOSPHERIC, - IONO_STEC, - REC_PCO_X, - REC_PCO_Y, - REC_PCO_Z, - SAT_PCO_X, - SAT_PCO_Y, - SAT_PCO_Z, - - REC_PCV, - - ANT_DELTA, - - EOP, - EOP_RATE, - - CALC, - - SLR_REC_RANGE_BIAS, - SLR_REC_TIME_BIAS, - - XFORM_XLATE, - XFORM_RTATE, - XFORM_SCALE, - XFORM_DELAY, - - - AMBIGUITY, - CODE_BIAS, - PHASE_BIAS, - - Z_AMB, - - REFERENCE, - - BEGIN_MEAS_STATES, - CODE_MEAS, - PHAS_MEAS, - LASER_MEAS, - PSEUDO_MEAS, - ORBIT_MEAS, - FILTER_MEAS, - END_MEAS_STATES, - - BEGIN_ORBIT_STATES, - ORBIT, - EMP_D_0, - EMP_D_1, - EMP_D_2, - EMP_D_3, - EMP_D_4, - - EMP_Y_0, - EMP_Y_1, - EMP_Y_2, - EMP_Y_3, - EMP_Y_4, - - EMP_B_0, - EMP_B_1, - EMP_B_2, - EMP_B_3, - EMP_B_4, - - EMP_R_0, - EMP_R_1, - EMP_R_2, - EMP_R_3, - EMP_R_4, - - EMP_T_0, - EMP_T_1, - EMP_T_2, - EMP_T_3, - EMP_T_4, - - EMP_N_0, - EMP_N_1, - EMP_N_2, - EMP_N_3, - EMP_N_4, - - EMP_P_0, - EMP_P_1, - EMP_P_2, - EMP_P_3, - EMP_P_4, - - EMP_Q_0, - EMP_Q_1, - EMP_Q_2, - EMP_Q_3, - EMP_Q_4, - END_ORBIT_STATES, - - BEGIN_INERTIAL_STATES, - GYRO_BIAS, - GYRO_SCALE, - ACCL_BIAS, - ACCL_SCALE, - IMU_OFFSET, - END_INERTIAL_STATES, - - RANGE -) - - -BETTER_ENUM(KEPLER, short int, - LX, - LY, - LZ, - EU, - EV, - M -) - -BETTER_ENUM(E_PolyType, short int, - CONSTANT, - LAT, - LON, - LAT_LON, - LAT_SQRD, - LON_SQRD) - -BETTER_ENUM(E_BasisType, short int, - POLYNOMIAL, - GRIDPOINT) - -BETTER_ENUM(E_Relativity, short int, - OFF, - ON) - -BETTER_ENUM(E_ChiSqMode, int, - INNOVATION, - MEASUREMENT, - STATE) - -BETTER_ENUM(E_TropModel, int, - STANDARD, - SBAS, - VMF3, - GPT2, - CSSR) - -BETTER_ENUM(E_NoiseModel, int, - UNIFORM, - ELEVATION_DEPENDENT) - -BETTER_ENUM(E_LogLevel, int, - DEBUG, - WARN, - ERROR) - -BETTER_ENUM(E_IonoModel, int, - NONE, - MEAS_OUT, - BSPLINE, - SPHERICAL_CAPS, - SPHERICAL_HARMONICS, - LOCAL) - -BETTER_ENUM(E_IonoMode, int, - OFF, ///< ionosphere option: correction off - BROADCAST, ///< ionosphere option: broadcast model - SBAS, ///< ionosphere option: SBAS model - IONO_FREE_LINEAR_COMBO, ///< ionosphere option: L1/L2 or L1/L5 iono-free LC - ESTIMATE, ///< ionosphere option: estimation - TOTAL_ELECTRON_CONTENT, ///< ionosphere option: IONEX TEC model - QZS, ///< ionosphere option: QZSS broadcast model - LEX, ///< ionosphere option: QZSS LEX ionospehre - STEC) ///< ionosphere option: SLANT TEC model - -BETTER_ENUM(E_IonoMapFn, int, - SLM, ///< single layer model mapping function - MSLM, ///< modified single layer model mapping function - MLM, ///< multiple layer model mapping function - KLOBUCHAR) ///< Klobuchar mapping function - -BETTER_ENUM(E_IonoFrame, int, - EARTH_FIXED, ///< Earth-fixed reference frame - SUN_FIXED) ///< Sun-fixed reference frame - -BETTER_ENUM(E_Period, int, - SECOND = 1, - MINUTE = 60, - HOUR = 60 * 60, - DAY = 60 * 60 * 24, - WEEK = 60 * 60 * 24 * 7, - YEAR = 60 * 60 * 24 * 365, - - SECONDS = SECOND, MINUTES = MINUTE, HOURS = HOUR, DAYS = DAY, WEEKS = WEEK, YEARS = YEAR, - SEC = SECOND, MIN = MINUTE, HR = HOUR, DY = DAY, WK = WEEK, YR = YEAR, - SECS = SECOND, MINS = MINUTE, HRS = HOUR, DYS = DAY, WKS = WEEK, YRS = YEAR, - SQRT_SEC = SECOND, SQRT_MIN = MINUTE, SQRT_HR = HOUR, SQRT_DY = DAY, SQRT_WK = WEEK, SQRT_YR = YEAR, - SQRT_SECS = SECOND, SQRT_MINS = MINUTE, SQRT_HRS = HOUR, SQRT_DYS = DAY, SQRT_WKS = WEEK, SQRT_YRS = YEAR, - SQRT_SECOND = SECOND, SQRT_MINUTE = MINUTE, SQRT_HOUR = HOUR, SQRT_DAY = DAY, SQRT_WEEK = WEEK, SQRT_YEAR = YEAR, - SQRT_SECONDS = SECOND, SQRT_MINUTES = MINUTE, SQRT_HOURS = HOUR, SQRT_DAYS = DAY, SQRT_WEEKS = WEEK, SQRT_YEARS = YEAR) - -BETTER_ENUM(E_TimeSys, int, - NONE, ///< NONE for unknown - GPST, ///< GPS Time - GLONASST, ///< GLONASS Time - GST, ///< Galileo System Time - BDT, ///< BeiDou Time - QZSST, ///< QZSS Time - TAI, ///< International Atomic Time - UTC, ///< Universal Coordinated Time - UT1, ///< Universal Time corrected for polar motion - TT) ///< Terrestrial Time - -BETTER_ENUM(E_PosFrame, int, - NONE, - XYZ, - NED, - RTN) - -BETTER_ENUM(E_ObxFrame, short int, - OTHER, - ECEF, - ECI, - BCRS) - -BETTER_ENUM(E_FilterMode, int, - LSQ, - KALMAN) - -BETTER_ENUM(E_Inverter, int, - NONE, - INV, - LLT, - LDLT, - COLPIVHQR, - BDCSVD, - JACOBISVD, - FULLPIVLU, FIRST_UNSUPPORTED = FULLPIVLU, - FULLPIVHQR) - -BETTER_ENUM(E_MongoType, int, - NONE, - STATES, - STATES_AVAILABLE, - RESIDUALS, - TRACE, - LIST) - - -BETTER_ENUM(E_ObsDesc, int, - C, // Code / Pseudorange - L, // Phase - D, // Doppler - S, // Raw signal strength (carrier to noise ratio) - X // Receiver channel numbers -) - -BETTER_ENUM(E_ObsCode, int, - NONE = 0 , ///< none or unknown - L1C = 1 , ///< L1C/A,G1C/A,E1C (GPS,GLO,GAL,QZS,SBS) - L1P = 2 , ///< L1P,G1P (GPS,GLO) - L1W = 3 , ///< L1 Z-track (GPS) - L1Y = 4 , ///< L1Y (GPS) - L1M = 5 , ///< L1M (GPS) - L1N = 6 , ///< L1codeless (GPS) - L1S = 7 , ///< L1C(D) (GPS,QZS) - L1L = 8 , ///< L1C(P) (GPS,QZS) - L1E = 9 , ///< L1C/B (QZS) - L1A = 10, ///< E1A (GAL) - L1B = 11, ///< E1B (GAL) - L1X = 12, ///< E1B+C,L1C(D+P) (GAL,QZS) - L1Z = 13, ///< E1A+B+C,L1-SAIF (GAL,QZS) - L2C = 14, ///< L2C/A,G1C/A (GPS,GLO) - L2D = 15, ///< L2 L1C/A-(P2-P1) (GPS) - L2S = 16, ///< L2C(M) (GPS,QZS) - L2L = 17, ///< L2C(L) (GPS,QZS) - L2X = 18, ///< L2C(M+L),B1-2I+Q (GPS,QZS,BDS) - L2P = 19, ///< L2P,G2P (GPS,GLO) - L2W = 20, ///< L2 Z-track (GPS) - L2Y = 21, ///< L2Y (GPS) - L2M = 22, ///< L2M (GPS) - L2N = 23, ///< L2codeless (GPS) - L5I = 24, ///< L5/E5aI (GPS,GAL,QZS,SBS) - L5Q = 25, ///< L5/E5aQ (GPS,GAL,QZS,SBS) - L5X = 26, ///< L5/E5aI+Q (GPS,GAL,QZS,SBS) - L7I = 27, ///< E5bI,B2aI (GAL,BDS) - L7Q = 28, ///< E5bQ,B2aQ (GAL,BDS) - L7X = 29, ///< E5bI+Q,B2aI+Q (GAL,BDS) - L6A = 30, ///< E6A, L2OCd (GAL,GLO) - L6B = 31, ///< E6B, L2OCp (GAL,GLO) - L6C = 32, ///< E6C, L2OCd+L2OCp (GAL,GLO) - L6X = 33, ///< E6B+C,LEXS+L,B3I+Q (GAL,QZS,BDS) - L6Z = 34, ///< E6A+B+C (GAL) - L6S = 35, ///< L6S (QZS) - L6L = 36, ///< L6L (QZS) - L8I = 37, ///< E5(a+b)I (GAL) - L8Q = 38, ///< E5(a+b)Q (GAL) - L8X = 39, ///< E5(a+b)I+Q (GAL) - L2I = 40, ///< B1-2I (BDS) - L2Q = 41, ///< B1-2Q (BDS) - L6I = 42, ///< B3I (BDS) - L6Q = 43, ///< B3Q (BDS) - L3I = 44, ///< G3I (GLO) - L3Q = 45, ///< G3Q (GLO) - L3X = 46, ///< G3I+Q (GLO) - L1I = 47, ///< B1I (BDS) - L1Q = 48, ///< B1Q (BDS) - L4A = 49, ///< L1OCd (GLO) - L4B = 50, ///< L1OCp (GLO) - L4X = 51, ///< L1OCd+L1OCp (GLO) - L6E = 52, ///< L6E (QZS) - L1D = 53, ///< B1D (BDS) - L5D = 54, ///< B2aD (BDS) - L5P = 55, ///< B2aP (BDS) - L9A = 57, ///< S9 A SPS (IRN) - L9B = 58, ///< S9 B RS(D) (IRN) - L9C = 59, ///< S9 C RS(P) (IRN) - L9X = 60, ///< S9 B+C (IRN) - L5A = 61, ///< L5 A SPS (IRN) - L5B = 62, ///< L5 B RS(D) (IRN) - L5C = 63, ///< L5 C RS(P) (IRN) - L5Z = 64, ///< L5 B+C (IRN) - L6D = 65, ///< L6 Data (BDS) - L6P = 66, ///< L6 Pilot (BDS) - L7D = 67, ///< L7 Data (BDS) - L7P = 68, ///< L7 Pilot (BDS) - L7Z = 69, ///< L7 Data+Pilot (BDS) - L8D = 70, ///< L8 Data (BDS) - L8P = 71, ///< L8 Pilot (BDS) - L8Z = 72, ///< L8 Data+Pilot (BDS) - AUTO = 9001 -) - -BETTER_ENUM(E_ObsCode2, int, - NONE, - P1, - P2, - C1, - C2, - C3, - C4, - C5, - C6, - C7, - C8, - L1, - L2, - L3, - L4, - L5, - L6, - L7, - L8, - LA -) - -BETTER_ENUM(E_ARmode, short int, - OFF, - ROUND, - ITER_RND, - BOOTST, - LAMBDA, - LAMBDA_ALT, - LAMBDA_AL2, - LAMBDA_BIE) - -BETTER_ENUM(E_NavRecType, short int, - NONE, ///< NONE for unknown */ - EPH, ///< Ephemerides data including orbit, clock, biases, accuracy and status parameters */ - STO, ///< System Time and UTC proxy offset parameters */ - EOP, ///< Earth Orientation Parameters */ - ION) ///< Global/Regional ionospheric model parameters */ - -BETTER_ENUM(E_NavMsgType, short int, - NONE, ///< NONE for unknown - LNAV, ///< GPS/QZSS/NavIC Legacy Navigation Messages - FDMA, ///< GLONASS Legacy FDMA Navigation Message - FNAV, ///< Galileo Free Navigation Message - INAV, ///< Galileo Integrity Navigation Message - IFNV, ///< Galileo INAV or FNAV Navigation Message - D1, ///< BeiDou-2/3 MEO/IGSO Navigation Message - D2, ///< BeiDou-2/3 GEO Navigation Message - D1D2, ///< BeiDou-2/3 MEO/IGSO and GEO Navigation Message - SBAS, ///< SBAS Navigation Message - CNAV, ///< GPS/QZSS CNAV Navigation Message - CNV1, ///< BeiDou-3 CNAV-1 Navigation Message - CNV2, ///< GPS/QZSS CNAV-2 Navigation Message BeiDou-3 CNAV-2 Navigation Message - CNV3, ///< BeiDou-3 CNAV-3 Navigation Message - CNVX) ///< GPS/QZSS CNAV or CNAV-2 Navigation Message BeiDou-3 CNAV-1, CNAV-2 or CNAV-3 Navigation Message - -BETTER_ENUM(E_SatType, short int, - NONE, - GEO, - IGSO, - MEO) - -BETTER_ENUM(E_StoCode, short int, - NONE, - GPUT, - GLUT, - GLGP, - GAUT, - GAGP, GPGA = GAGP, // From RINEX 3.04 the GPGA label is replaced by GAGP, while the value and sign for the Galileo minus GPS time offset remains unchanged. - GAGL, - BDUT, - BDGP, - BDGL, - BDGA, - QZUT, - QZGP, - QZGL, - QZGA, - QZBD, - IRUT, - IRGP, - IRGL, - IRGA, - IRBD, - IRQZ, - SBUT, - SBGP, - SBGL, - SBGA, - SBBD, - SBQZ, - SBIR) - -BETTER_ENUM(E_UtcId, short int, - NONE, - UTC_USNO, - UTC_SU, - UTCGAL, - UTC_NTSC, - UTC_NICT, - UTC_NPLI, - UTCIRN, - UTC_OP, - UTC_NIST) - -BETTER_ENUM(E_SbasId, short int, - NONE, - WAAS, - EGNOS, - MSAS, - GAGAN, - SDCM, - BDSBAS, - KASS, - A_SBAS, - SPAN) - -BETTER_ENUM(RtcmMessageType, uint16_t, - NONE = 0, - - GPS_EPHEMERIS = 1019, - - GLO_EPHEMERIS = 1020, - - //GPS_NETWORK_RTK_RESIDUAL = 1030, - //RECEIVER_AND_ANTENNA_DESC = 1033, - - BDS_EPHEMERIS = 1042, - - QZS_EPHEMERIS = 1044, - - GAL_FNAV_EPHEMERIS = 1045, - GAL_INAV_EPHEMERIS = 1046, - - GPS_SSR_ORB_CORR = 1057, - GPS_SSR_CLK_CORR = 1058, - GPS_SSR_CODE_BIAS = 1059, - GPS_SSR_COMB_CORR = 1060, - GPS_SSR_URA = 1061, - GPS_SSR_HR_CLK_CORR = 1062, - GPS_SSR_PHASE_BIAS = 1265, - - GLO_SSR_ORB_CORR = 1063, - GLO_SSR_CLK_CORR = 1064, - GLO_SSR_CODE_BIAS = 1065, - GLO_SSR_COMB_CORR = 1066, - GLO_SSR_URA = 1067, - GLO_SSR_HR_CLK_CORR = 1068, - GLO_SSR_PHASE_BIAS = 1266, - - MSM4_GPS = 1074, - MSM5_GPS = 1075, - MSM6_GPS = 1076, - MSM7_GPS = 1077, - - MSM4_GLONASS = 1084, - MSM5_GLONASS = 1085, - MSM6_GLONASS = 1086, - MSM7_GLONASS = 1087, - - MSM4_GALILEO = 1094, - MSM5_GALILEO = 1095, - MSM6_GALILEO = 1096, - MSM7_GALILEO = 1097, - - MSM4_QZSS = 1114, - MSM5_QZSS = 1115, - MSM6_QZSS = 1116, - MSM7_QZSS = 1117, - - MSM4_BEIDOU = 1124, - MSM5_BEIDOU = 1125, - MSM6_BEIDOU = 1126, - MSM7_BEIDOU = 1127, - - //GLONASS_AUX_OPERATION_INFO = 1230 - - GAL_SSR_ORB_CORR = 1240, - GAL_SSR_CLK_CORR = 1241, - GAL_SSR_CODE_BIAS = 1242, - GAL_SSR_COMB_CORR = 1243, - GAL_SSR_URA = 1244, - GAL_SSR_HR_CLK_CORR = 1245, - GAL_SSR_PHASE_BIAS = 1267, - - QZS_SSR_ORB_CORR = 1246, - QZS_SSR_CLK_CORR = 1247, - QZS_SSR_CODE_BIAS = 1248, - QZS_SSR_COMB_CORR = 1249, - QZS_SSR_URA = 1250, - QZS_SSR_HR_CLK_CORR = 1251, - QZS_SSR_PHASE_BIAS = 1268, - - SBS_SSR_ORB_CORR = 1252, - SBS_SSR_CLK_CORR = 1253, - SBS_SSR_CODE_BIAS = 1254, - SBS_SSR_COMB_CORR = 1255, - SBS_SSR_URA = 1256, - SBS_SSR_HR_CLK_CORR = 1257, - SBS_SSR_PHASE_BIAS = 1269, - - BDS_SSR_ORB_CORR = 1258, - BDS_SSR_CLK_CORR = 1259, - BDS_SSR_CODE_BIAS = 1260, - BDS_SSR_COMB_CORR = 1261, - BDS_SSR_URA = 1262, - BDS_SSR_HR_CLK_CORR = 1263, - BDS_SSR_PHASE_BIAS = 1270, - - COMPACT_SSR = 4073, - IGS_SSR = 4076, - CUSTOM = 4082 -) - -BETTER_ENUM(CompactSSRSubtype, unsigned short, - NONE = 0, - MSK = 1, - ORB = 2, - CLK = 3, - COD = 4, - PHS = 5, - BIA = 6, - URA = 7, - TEC = 8, - GRD = 9, - SRV = 10, - CMB = 11, - ATM = 12) - -//the order and spacing of these is magic, dont modify -BETTER_ENUM(IgsSSRSubtype, unsigned short, - NONE = 0, - - GROUP_ORB = 1, - GROUP_CLK = 2, - GROUP_CMB = 3, - GROUP_HRC = 4, - GROUP_COD = 5, - GROUP_PHS = 6, - GROUP_URA = 7, - GROUP_ION = 8, - - GPS_OFFSET = 20, - GPS_ORB = 21, - GPS_CLK = 22, - GPS_CMB = 23, - GPS_HRC = 24, - GPS_COD = 25, - GPS_PHS = 26, - GPS_URA = 27, - - GLO_OFFSET = 40, - GLO_ORB = 41, - GLO_CLK = 42, - GLO_CMB = 43, - GLO_HRC = 44, - GLO_COD = 45, - GLO_PHS = 46, - GLO_URA = 47, - - GAL_OFFSET = 60, - GAL_ORB = 61, - GAL_CLK = 62, - GAL_CMB = 63, - GAL_HRC = 64, - GAL_COD = 65, - GAL_PHS = 66, - GAL_URA = 67, - - QZS_OFFSET = 80, - QZS_ORB = 81, - QZS_CLK = 82, - QZS_CMB = 83, - QZS_HRC = 84, - QZS_COD = 85, - QZS_PHS = 86, - QZS_URA = 87, - - BDS_OFFSET = 100, - BDS_ORB = 101, - BDS_CLK = 102, - BDS_CMB = 103, - BDS_HRC = 104, - BDS_COD = 105, - BDS_PHS = 106, - BDS_URA = 107, - - SBS_OFFSET = 120, - SBS_ORB = 121, - SBS_CLK = 122, - SBS_CMB = 123, - SBS_HRC = 124, - SBS_COD = 125, - SBS_PHS = 126, - SBS_URA = 127, - - IONVTEC = 201 -) - -BETTER_ENUM(E_Source, short int, - NONE, - SPP, - CONFIG, - PRECISE, - SSR, - KALMAN, - BROADCAST, - NOMINAL, - MODEL, - REMOTE) - -BETTER_ENUM(E_OrbexRecord, short int, - PCS, - VCS, - CPC, - CVC, - POS, - VEL, - CLK, - CRT, - ATT) - -BETTER_ENUM(E_RTCMSubmessage, short int, - TIMESTAMP = 1 -) - -BETTER_ENUM(E_CrdEpochEvent, int, - REC_RX = 0, // ground receive time (at SRP) (two-way) - SAT_BN = 1, // spacecraft bounce time (two-way) - REC_TX = 2, // ground transmit time (at SRP) (two-way) - SAT_RX = 3, // spacecraft receive time (one-way) - SAT_TX = 4, // spacecraft transmit time (one-way) - REC_TX_SAT_RX = 5, // ground transmit time (at SRP) and spacecraft receive time (one-way) - SAT_TX_REC_RX = 6, // spacecraft transmit time and ground receive time (at SRP) (one-way) - NONE = 7 -) - -BETTER_ENUM(E_ObsWaitCode, short int, - OK, - EARLY_DATA, - NO_DATA_WAIT, - NO_DATA_EVER) - -BETTER_ENUM(E_SRPModel, int, - NONE, - CANNONBALL, - BOXWING) - -BETTER_ENUM(E_TidesModel, short int, - ELASTIC, - ANELASTIC) - -BETTER_ENUM(E_ThirdBody, short int, - MERCURY = 1, - VENUS = 2, - EARTH = 3, - MARS = 4, - JUPITER = 5, - SATURN = 6, - URANUS = 7, - NEPTUNE = 8, - PLUTO = 9, - MOON = 10, - SUN = 11) //from jpl, do not modify - - -BETTER_ENUM(E_SigWarning, short int, - SIG_OUTG = 1, // Minor (one signal) outage - LOW_ELEV = 2, // Low elevation - CYC_SLIP = 3, // Cycle slip - MAJ_OUTG = 4, // Major (whole satellite/receiver) outage - USR_DISC = 5) // User defined - - -BETTER_ENUM(E_SlrRangeType, short int, // from crd_v2.01.pdf p7 - TX_ONLY = 0, // no ranges (i.e., transmit time only) - ONE_WAY = 1, // one-way ranging - TWO_WAY = 2, // two-way ranging - RX_ONLY = 3, // receive times only - MIXED = 3) // mixed (for real-time data recording, and combination of one- and two-way ranging, e.g., T2L2) - -BETTER_ENUM(E_UBXClass, short int, - NAV = 0x01, - RXM = 0x02, - INF = 0x04, - ACK = 0x05, - CFG = 0x06, - MON = 0x0A, - AID = 0x0B, - TIM = 0x0D, - ESF = 0x10) - - - -BETTER_ENUM(E_RXMId, short int, - SFRBX = 0x13, - MEASX = 0x14, - RAWX = 0x15) - -BETTER_ENUM(E_ESFId, short int, - MEAS = 0x02) - -BETTER_ENUM(E_MEASDataType, short int, - NONE = 0, - GYRO_Z = 5, - WHEEL_FL = 6, - WHEEL_FR = 7, - WHEEL_RL = 8, - WHEEL_RR = 9, - SPEED_TICK = 10, - SPEED = 11, - GYRO_TEMP = 12, - GYRO_Y = 13, - GYRO_X = 14, - ACCL_X = 16, - ACCL_Y = 17, - ACCL_Z = 18) - -BETTER_ENUM(E_Month, short int, - NONE, - JAN, - FEB, - MAR, - ARP, - MAY, - JUN, - JUL, - AUG, - SEP, - OCT, - NOV, - DEC) - -BETTER_ENUM(E_FilePos, short int, - COORD, - CURR_TIME, - TOTAL_TIME, - PCDH, - NUM_SAMPLES, - FOOTER) - -BETTER_ENUM(E_SSROutTiming, short int, - GPS_TIME, - LATEST_CLOCK_ESTIMATE) - - -BETTER_ENUM(E_Component, short int, - NONE, - X, - P, - DX, - PREFIT, - POSTFIT, - VARIANCE, - OBSERVED, - HEADING, - RANGE, - REC_CLOCK, - SAT_CLOCK, - SAGNAC, - RELATIVITY1, - RELATIVITY2, - REC_ANTENNA_DELTA, - REC_PCO, - SAT_PCO, - REC_PCV, - SAT_PCV, - TIDES_SOLID, - TIDES_OTL, - TIDES_ATL, - TIDES_SPOLE, - TIDES_OPOLE, - TROPOSPHERE, - IONOSPHERIC_COMPONENT, - IONOSPHERIC_COMPONENT1, - IONOSPHERIC_COMPONENT2, - IONOSPHERIC_MODEL, - PHASE_WIND_UP, - PHASE_AMBIGUITY, - REC_RANGE_BIAS, - REC_TIME_BIAS, - REC_PHASE_BIAS, - SAT_PHASE_BIAS, - REC_CODE_BIAS, - SAT_CODE_BIAS, - TROPOSPHERE_MODEL, - EOP, - SAT_REFLECTOR_DELTA, - NET_RESIDUAL, - CENTRAL_FORCE, - ALBEDO, - ALBEDO_BOXWING, - INDIRECT_J2, - EMPIRICAL, - GENERAL_RELATIVITY, - EGM, - SRP_CANNONBALL, - SRP_BOXWING, - ANTENNA_THRUST, - PLANETARY_PERTURBATION) +enum class E_Component : short int +{ + NONE, + X, + P, + DX, + PREFIT, + POSTFIT, + VARIANCE, + OBSERVED, + HEADING, + RANGE, + REC_CLOCK, + SAT_CLOCK, + SAGNAC, + RELATIVITY1, + RELATIVITY2, + REC_ANTENNA_DELTA, + REC_PCO, + SAT_PCO, + REC_PCV, + SAT_PCV, + TIDES_SOLID, + TIDES_OTL, + TIDES_ATL, + TIDES_SPOLE, + TIDES_OPOLE, + TROPOSPHERE, + IONOSPHERIC_COMPONENT, + IONOSPHERIC_COMPONENT1, + IONOSPHERIC_COMPONENT2, + IONOSPHERIC_MODEL, + PHASE_WIND_UP, + PHASE_AMBIGUITY, + REC_RANGE_BIAS, + REC_TIME_BIAS, + REC_PHASE_BIAS, + SAT_PHASE_BIAS, + REC_CODE_BIAS, + SAT_CODE_BIAS, + TROPOSPHERE_MODEL, + EOP, + SAT_REFLECTOR_DELTA, + NET_RESIDUAL, + CENTRAL_FORCE, + ALBEDO_CANNONBALL, + ALBEDO_BOXWING, + INDIRECT_J2, + DRAG, + EMPIRICAL, + GENERAL_RELATIVITY, + EGM, + SRP_CANNONBALL, + SRP_BOXWING, + ANTENNA_THRUST, + PLANETARY_PERTURBATION +}; enum E_ReturnType { - UNSUPPORTED, - OK, - WAIT, - GOT_OBS, - BAD_LENGTH -}; - -BETTER_ENUM(E_LoadingType, short int, - NONE, - OCEAN, - ATMOSPHERIC) - -BETTER_ENUM(E_TidalConstituent, short int, - M2, - S2, - N2, - K2, - S1, - K1, - O1, - P1, - Q1, - MF, - MM, - SSA) - -BETTER_ENUM(E_TidalComponent, short int, - EAST, - WEST, - NORTH, - SOUTH, - UP, - DOWN) - -BETTER_ENUM(E_Mongo, short int, - NONE, - PRIMARY, - SECONDARY, - BOTH) - -BETTER_ENUM(E_Mincon, short int, - PSEUDO_OBS, - WEIGHT_MATRIX, - VARIANCE_INVERSE, - COVARIANCE_INVERSE) - -BETTER_ENUM(E_InteractiveMode, short int, - Syncing, - PropagatingOrbits, - Preprocessing, - StateTransition1, - OMCCalculations, - StateTransition2, - Filtering, - MinimumConstraints, - PredictingStates, - Outputs, - Complete) - -BETTER_ENUM(E_InteractMode, short int, - None, - Page, - Scroll) + UNSUPPORTED, + OK, + WAIT, + GOT_OBS, + BAD_LENGTH +}; + +enum class E_LoadingType : short int +{ + NONE, + OCEAN, + ATMOSPHERIC +}; + +enum class E_TidalConstituent : short int +{ + M2, + S2, + N2, + K2, + S1, + K1, + O1, + P1, + Q1, + MF, + MM, + SSA +}; + +enum class E_TidalComponent : short int +{ + EAST, + WEST, + NORTH, + SOUTH, + UP, + DOWN +}; +enum class E_Mongo : short int +{ + NONE, + PRIMARY, + SECONDARY, + BOTH +}; + +enum class E_Mincon : short int +{ + PSEUDO_OBS, + WEIGHT_MATRIX, + VARIANCE_INVERSE, + COVARIANCE_INVERSE +}; + +// Extend magic_enum range for enums with outlier values +namespace magic_enum +{ +namespace customize +{ +template <> +struct enum_range +{ + static constexpr int min = 0; + static constexpr int max = 100; // Must be > AUTO (99) +}; +template <> +struct enum_range +{ + static constexpr int min = 0; + static constexpr int max = 210; // Must be > IONVTEC (201) +}; +} // namespace customize +} // namespace magic_enum diff --git a/src/cpp/common/ephBroadcast.cpp b/src/cpp/common/ephBroadcast.cpp index 38a36c3de..42687cc46 100644 --- a/src/cpp/common/ephBroadcast.cpp +++ b/src/cpp/common/ephBroadcast.cpp @@ -1,642 +1,748 @@ +#include "common/acsConfig.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/trace.hpp" -#include "eigenIncluder.hpp" -#include "observations.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "trace.hpp" -#include "gTime.hpp" +constexpr double STD_BRDCCLK = 1.5; ///< error of broadcast clock (m) +constexpr double J2_GLO = 1.0826257E-3; ///< 2nd zonal harmonic of geopot ref [2] -#define STD_BRDCCLK 30.0 ///< error of broadcast clock (m) +constexpr double SIN_5 = -0.0871557427476582; ///< sin(-5.0 deg) +constexpr double COS_5 = +0.9961946980917456; ///< cos(-5.0 deg) -#define J2_GLO 1.0826257E-3 ///< 2nd zonal harmonic of geopot ref [2] +constexpr double ERREPH_GLO = 5.0; ///< error of glonass ephemeris (m) +constexpr double TSTEP = 60.0; ///< integration step glonass ephemeris (s) +constexpr double RTOL_KEPLER = 1E-14; ///< relative tolerance for Kepler equation -#define SIN_5 -0.0871557427476582 ///< sin(-5.0 deg) -#define COS_5 +0.9961946980917456 ///< cos(-5.0 deg) +constexpr int MAX_ITER_KEPLER = 30; ///< max number of iteration of Kelpler -#define ERREPH_GLO 5.0 ///< error of glonass ephemeris (m) -#define TSTEP 60.0 ///< integration step glonass ephemeris (s) -#define RTOL_KEPLER 1E-14 ///< relative tolerance for Kepler equation - -#define MAX_ITER_KEPLER 30 ///< max number of iteration of Kelpler - -#define OMGE_GLO 7.292115E-5 ///< earth angular velocity (rad/s) ref [2] -#define OMGE_CMP 7.292115E-5 ///< earth angular velocity (rad/s) ref [9] -#define OMGE_GAL 7.2921151467E-5 ///< earth angular velocity (rad/s) ref [7] +constexpr double OMGE_GLO = 7.292115E-5; ///< earth angular velocity (rad/s) ref [2] +constexpr double OMGE_CMP = 7.292115E-5; ///< earth angular velocity (rad/s) ref [9] +constexpr double OMGE_GAL = 7.2921151467E-5; ///< earth angular velocity (rad/s) ref [7] /** variance by ura ephemeris (ref [1] 20.3.3.3.1.1) -*/ -double var_uraeph( - int ura) + */ +double var_uraeph(int ura) { - const double ura_value[] = - { - 2.4, 3.4, 4.85, 6.85, 9.65, 13.65, 24.0, 48.0, 96.0, 192.0, 384.0, 768.0, 1536.0, 3072.0, 6144.0 - }; - - return ura < 0 || 15 <= ura ? SQR(6144.0) : SQR(ura_value[ura]); + const double ura_value[] = { + 2.4, + 3.4, + 4.85, + 6.85, + 9.65, + 13.65, + 24.0, + 48.0, + 96.0, + 192.0, + 384.0, + 768.0, + 1536.0, + 3072.0, + 6144.0 + }; + + return ura < 0 || 15 <= ura ? SQR(6144.0) : SQR(ura_value[ura]); } /** select EOP/ION messages -*/ -template + */ +template EPHTYPE* selSysEphFromMap( - Trace& trace, - GTime time, - E_Sys sys, - E_NavMsgType type, - map>>>& ephMap) + Trace& trace, + GTime time, + E_Sys sys, + E_NavMsgType type, + map>>>& ephMap +) { -// trace(4,__FUNCTION__ " : time=%s sat=%2d iode=%d\n",time.to_string(3).c_str(),Sat,iode); - - if (ephMap.find(sys) == ephMap.end()) return nullptr; auto& sysMap = ephMap[sys]; - if (sysMap.find(type) == sysMap.end()) return nullptr; auto& sysEphMap = sysMap[type]; - - auto it = sysEphMap.lower_bound(time); - if (it == sysEphMap.end()) - { - tracepdeex(5, trace, "\nno broadcast ephemeris (EOP/ION): %s sys=%s", time.to_string().c_str(), sys._to_string()); - if (sysEphMap.empty() == false) - { - tracepdeex(5, trace, " last is %s", sysEphMap.begin()->first.to_string().c_str()); - } - return nullptr; - } - - auto& [ephTime, eph] = *it; - - return &eph; + // trace(4,__FUNCTION__ " : time=%s sat=%2d iode=%d\n",time.to_string(3).c_str(),Sat,iode); + + if (ephMap.find(sys) == ephMap.end()) + return nullptr; + auto& sysMap = ephMap[sys]; + if (sysMap.find(type) == sysMap.end()) + return nullptr; + auto& sysEphMap = sysMap[type]; + + auto it = sysEphMap.lower_bound(time); + if (it == sysEphMap.end()) + { + tracepdeex( + 5, + trace, + "\nno broadcast ephemeris (EOP/ION): %s sys=%s", + time.to_string().c_str(), + enum_to_string(sys) + ); + if (sysEphMap.empty() == false) + { + tracepdeex(5, trace, " last is %s", sysEphMap.begin()->first.to_string().c_str()); + } + return nullptr; + } + + auto& [ephTime, eph] = *it; + + return &eph; } /** select ephememeris -*/ -template + */ +template EPHTYPE* selSatEphFromMap( - Trace& trace, - GTime time, - SatSys Sat, - E_NavMsgType type, - int& iode, - map>>>& ephMap) + Trace& trace, + GTime time, + SatSys Sat, + E_NavMsgType type, + int& iode, + map>>>& ephMap +) { -// trace(4,__FUNCTION__ " : time=%s sat=%2d iode=%d\n",time.to_string(3).c_str(),Sat,iode); - - double tmax; - switch (Sat.sys) - { - case E_Sys::GAL: tmax = MAXDTOE_GAL; break; - case E_Sys::QZS: tmax = MAXDTOE_QZS; break; - case E_Sys::BDS: tmax = MAXDTOE_CMP; break; - case E_Sys::GLO: tmax = MAXDTOE_GLO; break; - case E_Sys::SBS: tmax = MAXDTOE_SBS; break; - default: tmax = MAXDTOE; break; - } - - auto& satEphMap = ephMap[Sat][type]; - - if (iode >= 0) - { - for (auto& [dummy, eph] : satEphMap) - { - if ( iode != eph.iode - ||fabs((eph.toe - time).to_double()) > tmax) - { - continue; - } - - return &eph; - } - - tracepdeex(5, trace, "\nno broadcast ephemeris: %s sat=%s with iode=%3d", time.to_string().c_str(), Sat.id().c_str(), iode); - - return nullptr; - } - - auto it = satEphMap.lower_bound(time + tmax); - if (it == satEphMap.end()) - { - tracepdeex(5, trace, "\nno broadcast ephemeris: %s sat=%s within MAXDTOE+ ", time.to_string().c_str(), Sat.id().c_str()); - if (satEphMap.empty() == false) - { - tracepdeex(5, trace, " last is %s", satEphMap.begin()->first.to_string().c_str()); - } - - return nullptr; - } - - auto& [ephTime, eph] = *it; - - if (fabs((eph.toe - time).to_double()) > tmax) - { - tracepdeex(5, trace, "\nno broadcast ephemeris: %s sat=%s within MAXDTOE-", time.to_string().c_str(), Sat.id().c_str()); - - return nullptr; - } - - iode = eph.iode; - - return &eph; + // trace(4,__FUNCTION__ " : time=%s sat=%2d iode=%d\n",time.to_string(3).c_str(),Sat,iode); + + double tdelay = 0; + if (acsConfig.simulate_real_time) + { + tdelay = acsConfig.eph_time_delay[Sat.sys]; + } + + double tmax; + switch (Sat.sys) + { + case E_Sys::GAL: + tmax = MAXDTOE_GAL; + break; + case E_Sys::QZS: + tmax = MAXDTOE_QZS; + break; + case E_Sys::BDS: + tmax = MAXDTOE_CMP; + break; + case E_Sys::GLO: + tmax = MAXDTOE_GLO; + break; + case E_Sys::SBS: + tmax = MAXDTOE_SBS; + break; + default: + tmax = MAXDTOE; + break; + } + + if (tdelay > tmax) + { + tracepdeex( + 2, + trace, + "\nSet time delay is larger than time of Validity: tmax=%f, tdelay=%f ", + tmax, + tdelay + ); + return nullptr; + } + + auto& satEphMap = ephMap[Sat][type]; + + auto it = satEphMap.lower_bound(time + tmax); + if (acsConfig.simulate_real_time // Ephemeris should be no later than (time - tdelay) when + // simulating real-time + || + iode < 0) // Start with the last available ephemeris when iode not provided (== ANY_IODE) + { + it = satEphMap.lower_bound(time - tdelay); + } + + while (it != satEphMap.end()) + { + auto& [ephTime, eph] = *it; + + if (fabs((eph.toe - time).to_double()) > tmax) + { + break; + } + + if (iode >= 0 && iode != eph.iode) // Go one-way back in time (forward in map) from (time + + // tmax) when iode is provided (>= 0) + { + it++; + continue; + } + + iode = eph.iode; + return &eph; + } + + if (it != satEphMap.begin() && acsConfig.simulate_real_time == false && iode < 0) + { + // If no suitable ephemeris found in the past, only try future ones (go backward in map) + // when iode not provided (== ANY_IODE) in post-processing + it--; + auto& [ephTime, eph] = *it; + + if (fabs((eph.toe - time).to_double()) <= tmax) + { + iode = eph.iode; + return &eph; + } + } + + tracepdeex( + 5, + trace, + "\nno broadcast ephemeris: %s sat=%s within MAXDTOE: %f ", + time.to_string().c_str(), + Sat.id().c_str(), + tmax + ); + + return nullptr; } - -template<> Eph* seleph (Trace& trace, GTime time, SatSys Sat, E_NavMsgType type, int iode, Navigation& nav){ return selSatEphFromMap(trace, time, Sat, type, iode, nav.ephMap); } -template<> Geph* seleph(Trace& trace, GTime time, SatSys Sat, E_NavMsgType type, int iode, Navigation& nav){ return selSatEphFromMap(trace, time, Sat, type, iode, nav.gephMap); } -template<> Seph* seleph(Trace& trace, GTime time, SatSys Sat, E_NavMsgType type, int iode, Navigation& nav){ return selSatEphFromMap(trace, time, Sat, type, iode, nav.sephMap); } -template<> ION* seleph (Trace& trace, GTime time, E_Sys sys, E_NavMsgType type, Navigation& nav){ return selSysEphFromMap(trace, time, sys, type, nav.ionMap); } -template<> EOP* seleph (Trace& trace, GTime time, E_Sys sys, E_NavMsgType type, Navigation& nav){ return selSysEphFromMap(trace, time, sys, type, nav.eopMap); } - - +template <> +Eph* seleph(Trace& trace, GTime time, SatSys Sat, E_NavMsgType type, int iode, Navigation& nav) +{ + return selSatEphFromMap(trace, time, Sat, type, iode, nav.ephMap); +} +template <> +Geph* seleph< + Geph>(Trace& trace, GTime time, SatSys Sat, E_NavMsgType type, int iode, Navigation& nav) +{ + return selSatEphFromMap(trace, time, Sat, type, iode, nav.gephMap); +} +template <> +Seph* seleph< + Seph>(Trace& trace, GTime time, SatSys Sat, E_NavMsgType type, int iode, Navigation& nav) +{ + return selSatEphFromMap(trace, time, Sat, type, iode, nav.sephMap); +} +template <> +ION* seleph(Trace& trace, GTime time, E_Sys sys, E_NavMsgType type, Navigation& nav) +{ + return selSysEphFromMap(trace, time, sys, type, nav.ionMap); +} +template <> +EOP* seleph(Trace& trace, GTime time, E_Sys sys, E_NavMsgType type, Navigation& nav) +{ + return selSysEphFromMap(trace, time, sys, type, nav.eopMap); +} /** glonass orbit differential equations */ -void deq( - const double* x, - double* xdot, - Vector3d& acc) +void deq(const double* x, double* xdot, Vector3d& acc) { - double r2 = dot(x, x, 3); - double r3 = r2 * sqrt(r2); - double omg2 = SQR(OMGE_GLO); - - if (r2 <= 0) - { - xdot[0] = 0; - xdot[1] = 0; - xdot[2] = 0; - xdot[3] = 0; - xdot[4] = 0; - xdot[5] = 0; - - return; - } - - /* ref [2] A.3.1.2 with bug fix for xdot[4],xdot[5] */ - double a = 1.5 * J2_GLO * MU_GLO * SQR(RE_GLO) / r2 / r3; /* 3/2*J2*mu*Ae^2/r^5 */ - double b = 5.0 * SQR(x[2]) / r2; /* 5*z^2/r^2 */ - double c = -MU_GLO / r3 - a * (1 - b); /* -mu/r^3-a(1-b) */ - - xdot[0] = x[3]; - xdot[1] = x[4]; - xdot[2] = x[5]; - - xdot[3] = (c + omg2) * x[0] + 2 * OMGE_GLO * x[4] + acc[0]; - xdot[4] = (c + omg2) * x[1] - 2 * OMGE_GLO * x[3] + acc[1]; - xdot[5] = (c - 2 * a) * x[2] + acc[2]; + double r2 = dot(x, x, 3); + double r3 = r2 * sqrt(r2); + double omg2 = SQR(OMGE_GLO); + + if (r2 <= 0) + { + xdot[0] = 0; + xdot[1] = 0; + xdot[2] = 0; + xdot[3] = 0; + xdot[4] = 0; + xdot[5] = 0; + + return; + } + + /* ref [2] A.3.1.2 with bug fix for xdot[4],xdot[5] */ + double a = 1.5 * J2_GLO * MU_GLO * SQR(RE_GLO) / r2 / r3; /* 3/2*J2*mu*Ae^2/r^5 */ + double b = 5.0 * SQR(x[2]) / r2; /* 5*z^2/r^2 */ + double c = -MU_GLO / r3 - a * (1 - b); /* -mu/r^3-a(1-b) */ + + xdot[0] = x[3]; + xdot[1] = x[4]; + xdot[2] = x[5]; + + xdot[3] = (c + omg2) * x[0] + 2 * OMGE_GLO * x[4] + acc[0]; + xdot[4] = (c + omg2) * x[1] - 2 * OMGE_GLO * x[3] + acc[1]; + xdot[5] = (c - 2 * a) * x[2] + acc[2]; } /* glonass position and velocity by numerical integration --------------------*/ -void glorbit( - double t, - double* x, - Vector3d& acc) +void glorbit(double t, double* x, Vector3d& acc) { - double k1[6]; - double k2[6]; - double k3[6]; - double k4[6]; - double w [6]; - - deq(x, k1, acc); for (int i = 0; i < 6; i++) w[i] = x[i] + k1[i] * t / 2; - deq(w, k2, acc); for (int i = 0; i < 6; i++) w[i] = x[i] + k2[i] * t / 2; - deq(w, k3, acc); for (int i = 0; i < 6; i++) w[i] = x[i] + k3[i] * t; - deq(w, k4, acc); - - for (int i = 0; i < 6; i++) - x[i] += (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) * t / 6; + double k1[6]; + double k2[6]; + double k3[6]; + double k4[6]; + double w[6]; + + deq(x, k1, acc); + for (int i = 0; i < 6; i++) + w[i] = x[i] + k1[i] * t / 2; + deq(w, k2, acc); + for (int i = 0; i < 6; i++) + w[i] = x[i] + k2[i] * t / 2; + deq(w, k3, acc); + for (int i = 0; i < 6; i++) + w[i] = x[i] + k3[i] * t; + deq(w, k4, acc); + + for (int i = 0; i < 6; i++) + x[i] += (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) * t / 6; } - - - - - - - - /** broadcast ephemeris to satellite clock bias -* compute satellite clock bias with broadcast ephemeris (gps, galileo, qzss) -* satellite clock does not include relativity correction and tdg -*/ -double eph2Clk(GTime time, Eph& eph, int recurse = 0) + * compute satellite clock bias with broadcast ephemeris (gps, galileo, qzss) + * satellite clock does not include relativity correction and tdg + */ +double eph2Clk(GTime time, Eph& eph, int recurse = 0) { - double t = (time - eph.toe).to_double(); t -= recurse ? eph2Clk(time - t, eph, recurse - 1) : 0; + double t = (time - eph.toe).to_double(); + t -= recurse ? eph2Clk(time - t, eph, recurse - 1) : 0; - return + eph.f0 - + eph.f1 * t - + eph.f2 * t * t; + return +eph.f0 + eph.f1 * t + eph.f2 * t * t; } /** glonass ephemeris to satellite clock bias * satellite clock includes relativity correction */ -double eph2Clk(GTime time, Geph& geph, int recurse = 0) +double eph2Clk(GTime time, Geph& geph, int recurse = 0) { - double t = (time - geph.toe).to_double(); t -= recurse ? eph2Clk(time - t, geph, recurse - 1) : 0; + double t = (time - geph.toe).to_double(); + t -= recurse ? eph2Clk(time - t, geph, recurse - 1) : 0; - return - geph.taun - + geph.gammaN * t; + return -geph.taun + geph.gammaN * t; } /** sbas ephemeris to satellite clock bias -*/ -double eph2Clk(GTime time, Seph& seph, int recurse = 0) + */ +double eph2Clk(GTime time, Seph& seph, int recurse = 0) { - double t = (time - seph.toe).to_double(); t -= recurse ? eph2Clk(time - t, seph, recurse - 1) : 0; + double t = (time - seph.toe).to_double(); + t -= recurse ? eph2Clk(time - t, seph, recurse - 1) : 0; - return + seph.af0 - + seph.af1 * t; + return +seph.af0 + seph.af1 * t; } - - - - /** broadcast ephemeris to satellite position and clock bias -* compute satellite position and clock bias with broadcast ephemeris (gps, galileo, qzss) -* satellite clock includes relativity correction without code bias (tgd or bgd) -*/ + * compute satellite position and clock bias with broadcast ephemeris (gps, galileo, qzss) + * satellite clock includes relativity correction without code bias (tgd or bgd) + */ void eph2Pos( - GTime time, ///< time (gpst) - Eph& eph, ///< broadcast ephemeris - Vector3d& rSat, ///< satellite position (ecef) {x,y,z} (m) - double* var_ptr = nullptr) ///< satellite position and clock variance (m^2) + GTime time, ///< time (gpst) + Eph& eph, ///< broadcast ephemeris + Vector3d& rSat, ///< satellite position (ecef) {x,y,z} (m) + double* var_ptr = nullptr ///< satellite position and clock variance (m^2) +) { -// trace(4, __FUNCTION__ " : time=%s sat=%2d\n",time.to_string(3).c_str(),eph->Sat); - - if (eph.A <= 0) - { - rSat = Vector3d::Zero(); - - if (var_ptr) - *var_ptr = 0; - - return; - } - - double tk = (time - eph.toe).to_double(); - int prn = eph.Sat.prn; - int sys = eph.Sat.sys; - - double mu; - double omge; - switch (sys) - { - case E_Sys::GAL: mu = MU_GAL; omge = OMGE_GAL; break; - case E_Sys::BDS: mu = MU_CMP; omge = OMGE_CMP; break; - default: mu = MU_GPS; omge = OMGE; break; - } - - double M = eph.M0 + (sqrt(mu / (eph.A * eph.A * eph.A)) + eph.deln) * tk; - - double E = M; - double Ek = 0; - int n; - for (n = 0; fabs(E - Ek) > RTOL_KEPLER && n < MAX_ITER_KEPLER; n++) - { - Ek = E; - E -= (E - eph.e * sin(E) - M) / (1 - eph.e * cos(E)); - } - - if (n >= MAX_ITER_KEPLER) - { + if (eph.A <= 0) + { + rSat = Vector3d::Zero(); + + if (var_ptr) + *var_ptr = 0; + + return; + } + + double tk = (time - eph.toe).to_double(); + int prn = eph.Sat.prn; + E_Sys sys = eph.Sat.sys; + + double mu; + double omge; + switch (sys) + { + case E_Sys::GAL: + mu = MU_GAL; + omge = OMGE_GAL; + break; + case E_Sys::BDS: + mu = MU_CMP; + omge = OMGE_CMP; + break; + default: + mu = MU_GPS; + omge = OMGE; + break; + } + + double M = eph.M0 + (sqrt(mu / (eph.A * eph.A * eph.A)) + eph.deln) * tk; + + double E = M; + double Ek = 0; + int n; + for (n = 0; fabs(E - Ek) > RTOL_KEPLER && n < MAX_ITER_KEPLER; n++) + { + Ek = E; + E -= (E - eph.e * sin(E) - M) / (1 - eph.e * cos(E)); + } + + if (n >= MAX_ITER_KEPLER) + { printf("kepler iteration overflow sat=%s\n", eph.Sat.id().c_str()); - return; - } - - double sinE = sin(E); - double cosE = cos(E); - -// trace(4,"kepler: sat=%2d e=%8.5f n=%2d del=%10.3e\n",eph->Sat,eph->e,n,E-Ek); - - double u = atan2(sqrt(1 - eph.e * eph.e) * sinE, cosE - eph.e) + eph.omg; - double r = eph.A * (1 - eph.e * cosE); - double i = eph.i0 - + eph.idot * tk; - - double sin2u = sin(2 * u); - double cos2u = cos(2 * u); - - u += eph.cus * sin2u + eph.cuc * cos2u; //argument of latitude - r += eph.crs * sin2u + eph.crc * cos2u; //radius - i += eph.cis * sin2u + eph.cic * cos2u; //inclination - - double x = r * cos(u); - double y = r * sin(u); - double cosi = cos(i); - - /* beidou geo satellite (ref [9]), prn range may change in the future */ - if ( ( sys == +E_Sys::BDS) - &&( prn <= 5 - ||prn >= 59)) - { - double O = eph.OMG0 - + eph.OMGd * tk - - omge * eph.toes; - - double sinO = sin(O); - double cosO = cos(O); - - double xg = x * cosO - y * cosi * sinO; - double yg = x * sinO + y * cosi * cosO; - double zg = y * sin(i); - - double sino = sin(omge * tk); - double coso = cos(omge * tk); - - rSat[0] = +xg * coso + yg * sino * COS_5 + zg * sino * SIN_5; - rSat[1] = -xg * sino + yg * coso * COS_5 + zg * coso * SIN_5; - rSat[2] = -yg * SIN_5 + zg * COS_5; - } - else - { - double O = eph.OMG0 - + (eph.OMGd - omge) * tk - - omge * eph.toes; - - double sinO = sin(O); - double cosO = cos(O); - - rSat[0] = x * cosO - y * cosi * sinO; - rSat[1] = x * sinO + y * cosi * cosO; - rSat[2] = y * sin(i); - } - - /* position and clock error variance */ - if (var_ptr) - *var_ptr = var_uraeph(eph.sva); + return; + } + + double sinE = sin(E); + double cosE = cos(E); + + double u = atan2(sqrt(1 - eph.e * eph.e) * sinE, cosE - eph.e) + eph.omg; + double r = eph.A * (1 - eph.e * cosE); + double i = eph.i0 + eph.idot * tk; + + double sin2u = sin(2 * u); + double cos2u = cos(2 * u); + + u += eph.cus * sin2u + eph.cuc * cos2u; // argument of latitude + r += eph.crs * sin2u + eph.crc * cos2u; // radius + i += eph.cis * sin2u + eph.cic * cos2u; // inclination + + double x = r * cos(u); + double y = r * sin(u); + double cosi = cos(i); + + /* beidou geo satellite (ref [9]), prn range may change in the future */ + if ((sys == E_Sys::BDS) && (prn <= 5 || prn >= 59)) + { + double O = eph.OMG0 + eph.OMGd * tk - omge * eph.toes; + + double sinO = sin(O); + double cosO = cos(O); + + double xg = x * cosO - y * cosi * sinO; + double yg = x * sinO + y * cosi * cosO; + double zg = y * sin(i); + + double sino = sin(omge * tk); + double coso = cos(omge * tk); + + rSat[0] = +xg * coso + yg * sino * COS_5 + zg * sino * SIN_5; + rSat[1] = -xg * sino + yg * coso * COS_5 + zg * coso * SIN_5; + rSat[2] = -yg * SIN_5 + zg * COS_5; + } + else + { + double O = eph.OMG0 + (eph.OMGd - omge) * tk - omge * eph.toes; + + double sinO = sin(O); + double cosO = cos(O); + + rSat[0] = x * cosO - y * cosi * sinO; + rSat[1] = x * sinO + y * cosi * cosO; + rSat[2] = y * sin(i); + } + + /* position and clock error variance */ + if (var_ptr) + *var_ptr = SQR(eph.ura[0]); + // *var_ptr = var_uraeph(eph.sva); } - /** glonass ephemeris to satellite position and clock bias. -* compute satellite position and clock bias with glonass ephemeris -*/ + * compute satellite position and clock bias with glonass ephemeris + */ void eph2Pos( - GTime time, ///< time (gpst) - Geph& geph, ///< glonass ephemeris - Vector3d& rSat, ///< satellite position {x,y,z} (ecef) (m) - double* var = nullptr) ///< satellite position and clock variance (m^2) + GTime time, ///< time (gpst) + Geph& geph, ///< glonass ephemeris + Vector3d& rSat, ///< satellite position {x,y,z} (ecef) (m) + double* var = nullptr ///< satellite position and clock variance (m^2) +) { -// trace(4, __FUNCTION__ ": time=%s sat=%2d\n",time.to_string(3).c_str(),geph->Sat); + double t = (time - geph.toe).to_double(); - double t = (time - geph.toe).to_double(); + double x[6]; + for (int i = 0; i < 3; i++) + { + x[i] = geph.pos[i]; + x[i + 3] = geph.vel[i]; + } - double x[6]; - for (int i = 0; i < 3; i++) - { - x[i ] = geph.pos[i]; - x[i + 3] = geph.vel[i]; - } + for (double tt = t < 0 ? -TSTEP : TSTEP; fabs(t) > 1E-9; t -= tt) + { + if (fabs(t) < TSTEP) + tt = t; - for (double tt = t < 0 ? -TSTEP : TSTEP; fabs(t) > 1E-9; t -= tt) - { - if (fabs(t) < TSTEP) - tt = t; + glorbit(tt, x, geph.acc); + } - glorbit(tt, x, geph.acc); - } + for (int i = 0; i < 3; i++) + rSat[i] = x[i]; - for (int i = 0; i < 3; i++) - rSat[i] = x[i]; - - if (var) - *var = SQR(ERREPH_GLO); + if (var) + *var = SQR(ERREPH_GLO); } /** sbas ephemeris to satellite position and clock bias -* compute satellite position and clock bias with sbas ephemeris -*/ + * compute satellite position and clock bias with sbas ephemeris + */ void eph2Pos( - GTime time, ///< time (gpst) - Seph& seph, ///< sbas ephemeris - Vector3d& rSat, ///< satellite position {x,y,z} (ecef) (m) - double* var = nullptr) ///< satellite position and clock variance (m^2) + GTime time, ///< time (gpst) + Seph& seph, ///< sbas ephemeris + Vector3d& rSat, ///< satellite position {x,y,z} (ecef) (m) + double* var = nullptr ///< satellite position and clock variance (m^2) +) { -// trace(4, __FUNCTION__ ": time=%s sat=%2d\n",time.to_string(3).c_str(),seph->Sat); - - double t = (time - seph.t0).to_double(); + double t = (time - seph.t0).to_double(); - for (int i = 0; i < 3; i++) - { - rSat[i] = seph.pos[i] - + seph.vel[i] * t - + seph.acc[i] * t * t / 2; - } + for (int i = 0; i < 3; i++) + { + rSat[i] = seph.pos[i] + seph.vel[i] * t + seph.acc[i] * t * t / 2; + } - if (var) - *var = var_uraeph(seph.sva); + if (var) + *var = SQR(seph.ura); + // *var = var_uraeph(seph.sva); } - - - - - /* satellite clock by broadcast ephemeris */ -template +template bool satClkBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatSys Sat, - double& satClk, - double& satClkVel, - double& ephVar, - bool& ephClkValid, - int& iode, - Navigation& nav) + Trace& trace, + GTime time, + GTime teph, + SatSys Sat, + double& satClk, + double& satClkVel, + double& clkVar, + bool& ephClkValid, + int& iode, + Navigation& nav +) { - double satClk1; - double tt = 1E-3; + double satClk1; + double tt = 1E-3; - ephVar = SQR(STD_BRDCCLK); + clkVar = SQR(STD_BRDCCLK / CLIGHT); -// trace(4, "%s: time=%s sat=%2d iode=%d\n",__FUNCTION__,time.to_string(3).c_str(),obs.Sat,iode); + E_Sys sys = Sat.sys; - int sys = Sat.sys; + ephClkValid = false; - ephClkValid = false; + auto type = acsConfig.used_nav_types[Sat.sys]; - auto type = acsConfig.used_nav_types[Sat.sys]; + auto eph_ptr = seleph(trace, teph, Sat, type, iode, nav); - auto eph_ptr = seleph(trace, teph, Sat, type, iode, nav); + if (eph_ptr == nullptr) + { + tracepdeex( + 2, + trace, + "\nCould not find Broadcast Ephemeris for sat: %s, %s", + Sat.id().c_str(), + teph.to_string().c_str() + ); + return false; + } - if (eph_ptr == nullptr) - { - tracepdeex(2, trace, "Could not find Broadcast Ephemeris for sat: %s, %s\n", Sat.id().c_str(), teph.to_string().c_str()); - return false; - } + auto& eph = *eph_ptr; - auto& eph = *eph_ptr; + tracepdeex( + 5, + trace, + "\nSelected ephemeris sat: %s, iode=%d, teph=%s, toe=%s", + Sat.id().c_str(), + iode, + teph.to_string().c_str(), + eph.toe.to_string().c_str() + ); - satClk = eph2Clk(time, eph); - satClk1 = eph2Clk(time + tt, eph); + satClk = eph2Clk(time, eph); + satClk1 = eph2Clk(time + tt, eph); - if (eph.svh == E_Svh::SVH_OK) - { - ephClkValid = true; - } + if (eph.svh == E_Svh::SVH_OK) + { + ephClkValid = true; + } - iode = eph.iode; + iode = eph.iode; - /* satellite velocity and clock drift by differential approx */ - satClkVel = (satClk1 - satClk) / tt; + /* satellite velocity and clock drift by differential approx */ + satClkVel = (satClk1 - satClk) / tt; - return true; + return true; } - - /* satellite position by broadcast ephemeris */ -template +template bool satPosBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatSys Sat, - Vector3d& rSat, - Vector3d& satVel, - double& ephVar, - bool& ephPosValid, - int& iode, - Navigation& nav) + Trace& trace, + GTime time, + GTime teph, + SatSys Sat, + Vector3d& rSat, + Vector3d& satVel, + double& ephVar, + bool& ephPosValid, + int& iode, + Navigation& nav +) { - Vector3d rSat1; - Vector3d rSat2; - double tt = 10e-3; + Vector3d rSat1; + Vector3d rSat2; + double tt = 10e-3; -// trace(4, "%s: time=%s sat=%2d iode=%d\n",__FUNCTION__,time.to_string(3).c_str(),obs.Sat,iode); + E_Sys sys = Sat.sys; - int sys = Sat.sys; + auto type = acsConfig.used_nav_types[Sat.sys]; - auto type = acsConfig.used_nav_types[Sat.sys]; + auto eph_ptr = seleph(trace, teph, Sat, type, iode, nav); - auto eph_ptr = seleph(trace, teph, Sat, type, iode, nav); + if (eph_ptr == nullptr) + { + tracepdeex( + 2, + trace, + "\nCould not find Broadcast Ephemeris for sat: %s, %s", + Sat.id().c_str(), + teph.to_string().c_str() + ); + return false; + } - if (eph_ptr == nullptr) - { - tracepdeex(2, trace, "Could not find Broadcast Ephemeris for sat: %s, %s\n", Sat.id().c_str(), teph.to_string().c_str()); - return false; - } + auto& eph = *eph_ptr; - auto& eph = *eph_ptr; + eph2Pos(time - tt, eph, rSat1, &ephVar); + eph2Pos(time + tt, eph, rSat2); - eph2Pos(time - tt, eph, rSat1, &ephVar); - eph2Pos(time + tt, eph, rSat2); + if (eph.svh == E_Svh::SVH_OK) + { + ephPosValid = true; + } - if (eph.svh == E_Svh::SVH_OK) - { - ephPosValid = true; - } + iode = eph.iode; - iode = eph.iode; + /* satellite velocity and clock drift by differential approx */ + rSat = (rSat2 + rSat1) / 2; + satVel = (rSat2 - rSat1) / (2 * tt); - /* satellite velocity and clock drift by differential approx */ - rSat = (rSat2 + rSat1) / 2; - satVel = (rSat2 - rSat1) / (2 * tt); - - return true; + return true; } /* satellite clock by broadcast ephemeris */ bool satClkBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatSys Sat, - double& satClk, - double& satClkVel, - double& ephVar, - bool& ephClkValid, - int& iode, - Navigation& nav) + Trace& trace, + GTime time, + GTime teph, + SatSys Sat, + double& satClk, + double& satClkVel, + double& clkVar, + bool& ephClkValid, + int& iode, + Navigation& nav +) { - int sys = Sat.sys; - - ephClkValid = false; - - if ( sys == +E_Sys::GPS - || sys == +E_Sys::GAL - || sys == +E_Sys::QZS - || sys == +E_Sys::BDS) { return satClkBroadcast (trace, time, teph, Sat, satClk, satClkVel, ephVar, ephClkValid, iode, nav); } - else if ( sys == +E_Sys::GLO) { return satClkBroadcast (trace, time, teph, Sat, satClk, satClkVel, ephVar, ephClkValid, iode, nav); } - else if ( sys == +E_Sys::SBS) { return satClkBroadcast (trace, time, teph, Sat, satClk, satClkVel, ephVar, ephClkValid, iode, nav); } - else { return false; } + E_Sys sys = Sat.sys; + + ephClkValid = false; + + if (sys == E_Sys::GPS || sys == E_Sys::GAL || sys == E_Sys::QZS || sys == E_Sys::BDS) + { + return satClkBroadcast< + Eph>(trace, time, teph, Sat, satClk, satClkVel, clkVar, ephClkValid, iode, nav); + } + else if (sys == E_Sys::GLO) + { + return satClkBroadcast< + Geph>(trace, time, teph, Sat, satClk, satClkVel, clkVar, ephClkValid, iode, nav); + } + else if (sys == E_Sys::SBS) + { + return satClkBroadcast< + Seph>(trace, time, teph, Sat, satClk, satClkVel, clkVar, ephClkValid, iode, nav); + } + else + { + return false; + } } - /* satellite position by broadcast ephemeris */ bool satPosBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatSys Sat, - Vector3d& rSat, - Vector3d& satVel, - double& ephVar, - bool& ephPosValid, - int& iode, - Navigation& nav) + Trace& trace, + GTime time, + GTime teph, + SatSys Sat, + Vector3d& rSat, + Vector3d& satVel, + double& ephVar, + bool& ephPosValid, + int& iode, + Navigation& nav +) { - int sys = Sat.sys; - - ephPosValid = false; - - if ( sys == +E_Sys::GPS - || sys == +E_Sys::GAL - || sys == +E_Sys::QZS - || sys == +E_Sys::BDS) { return satPosBroadcast (trace, time, teph, Sat, rSat, satVel, ephVar, ephPosValid, iode, nav); } - else if ( sys == +E_Sys::GLO) { return satPosBroadcast (trace, time, teph, Sat, rSat, satVel, ephVar, ephPosValid, iode, nav); } - else if ( sys == +E_Sys::SBS) { return satPosBroadcast (trace, time, teph, Sat, rSat, satVel, ephVar, ephPosValid, iode, nav); } - else { return false; } - + E_Sys sys = Sat.sys; + + ephPosValid = false; + + if (sys == E_Sys::GPS || sys == E_Sys::GAL || sys == E_Sys::QZS || sys == E_Sys::BDS) + { + return satPosBroadcast< + Eph>(trace, time, teph, Sat, rSat, satVel, ephVar, ephPosValid, iode, nav); + } + else if (sys == E_Sys::GLO) + { + return satPosBroadcast< + Geph>(trace, time, teph, Sat, rSat, satVel, ephVar, ephPosValid, iode, nav); + } + else if (sys == E_Sys::SBS) + { + return satPosBroadcast< + Seph>(trace, time, teph, Sat, rSat, satVel, ephVar, ephPosValid, iode, nav); + } + else + { + return false; + } } bool satClkBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - Navigation& nav, - int iode) + Trace& trace, + GTime time, + GTime teph, + SatPos& satPos, + Navigation& nav, + int iode +) { - satPos.iodeClk = iode; - - return satClkBroadcast( - trace, - time, - teph, - satPos.Sat, - satPos.satClk, - satPos.satClkVel, - satPos.satClkVar, - satPos.ephClkValid, - satPos.iodeClk, - nav); + satPos.iodeClk = iode; + + return satClkBroadcast( + trace, + time, + teph, + satPos.Sat, + satPos.satClk, + satPos.satClkVel, + satPos.satClkVar, + satPos.ephClkValid, + satPos.iodeClk, + nav + ); } bool satPosBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - Navigation& nav, - int iode) + Trace& trace, + GTime time, + GTime teph, + SatPos& satPos, + Navigation& nav, + int iode +) { - satPos.iodePos = iode; - - return satPosBroadcast( - trace, - time, - teph, - satPos.Sat, - satPos.rSatApc, - satPos.satVel, - satPos.posVar, - satPos.ephPosValid, - satPos.iodePos, - nav); + satPos.iodePos = iode; + + return satPosBroadcast( + trace, + time, + teph, + satPos.Sat, + satPos.rSatApc, + satPos.satVel, + satPos.posVar, + satPos.ephPosValid, + satPos.iodePos, + nav + ); } diff --git a/src/cpp/common/ephKalman.cpp b/src/cpp/common/ephKalman.cpp index 5a70ccd26..3ab34be35 100644 --- a/src/cpp/common/ephKalman.cpp +++ b/src/cpp/common/ephKalman.cpp @@ -1,150 +1,143 @@ - - // #pragma GCC optimize ("O0") -#include "eigenIncluder.hpp" -#include "observations.hpp" -#include "navigation.hpp" -#include "algebra.hpp" -#include "trace.hpp" -#include "gTime.hpp" +#include "common/algebra.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/trace.hpp" - -bool satClkKalman( - Trace& trace, - GTime time, - SatPos& satPos, - const KFState* kfState_ptr) +bool satClkKalman(Trace& trace, GTime time, SatPos& satPos, const KFState* kfState_ptr) { - if (kfState_ptr == nullptr) - { - return false; - } + if (kfState_ptr == nullptr) + { + return false; + } + + auto& kfState = *kfState_ptr; - auto& kfState = *kfState_ptr; + double clk = 0; + double vel = 0; - double clk = 0; - double vel = 0; + // get clocks from the state + KFKey kfKey; + kfKey.Sat = satPos.Sat; - //get clocks from the state - KFKey kfKey; - kfKey.Sat = satPos.Sat; + bool anyFound = false; + while (1) + { + double thisClk = 0; + double thisVel = 0; - bool anyFound = false; - while (1) - { - double thisClk = 0; - double thisVel = 0; + kfKey.type = KF::SAT_CLOCK; - kfKey.type = KF::SAT_CLOCK; + bool found = true; + found = found && (kfState.getKFValue(kfKey, thisClk) != E_Source::NONE); - bool found = true; - found &= kfState.getKFValue(kfKey, thisClk); + if (found == false) + { + break; + } - if (found == false) - { - break; - } + anyFound = true; - anyFound = true; + kfKey.type = KF::SAT_CLOCK_RATE; - kfKey.type = KF::SAT_CLOCK_RATE; + found = found && (kfState.getKFValue(kfKey, thisVel) != E_Source::NONE); - found &= kfState.getKFValue(kfKey, thisVel); + kfKey.num++; - kfKey.num++; + clk += thisClk; + vel += thisVel; + } - clk += thisClk; - vel += thisVel; - } + if (anyFound == false) + { + return false; + } - if (anyFound == false) - { - return false; - } + double dt = (time - kfState.time).to_double(); - double dt = (time - kfState.time).to_double(); + clk /= CLIGHT; + vel /= CLIGHT; - clk /= CLIGHT; - vel /= CLIGHT; + satPos.satClk = clk + vel * dt; - satPos.satClk = clk - + vel * dt; + satPos.satClkVel = vel; - satPos.satClkVel = vel; + satPos.satClkVar = 0; // todo Eugene: get actual variances from filter - return anyFound; + return anyFound; } -bool satPosKalman( - Trace& trace, - GTime time, - SatPos& satPos, - const KFState* kfState_ptr) +bool satPosKalman(Trace& trace, GTime time, SatPos& satPos, const KFState* kfState_ptr) { - VectorEci rSat0; - VectorEci vSat0; - GTime t0; - - auto& satNav = nav.satNavMap[satPos.Sat]; - - if ( satNav.satPos0.rSatEci0.isZero() == false - &&satNav.satPos0.vSatEci0.isZero() == false - &&satNav.satPos0.posTime != GTime::noTime()) - { - rSat0 = satNav.satPos0.rSatEci0; - vSat0 = satNav.satPos0.vSatEci0; - t0 = satNav.satPos0.posTime; - } - else - { - if (kfState_ptr == nullptr) - { - return false; - } - - auto& kfState = *kfState_ptr; - - //get orbit things from the state - for (int i = 0; i < 3; i++) - { - KFKey kfKey; - kfKey.type = KF::ORBIT; - kfKey.Sat = satPos.Sat; - - kfKey.num = i; - double dummy; - - bool found = (kfKey.num = i, kfState.getKFValue(kfKey, rSat0(i), &dummy, &dummy, false)) - &&(kfKey.num = i + 3, kfState.getKFValue(kfKey, vSat0(i), &dummy, &dummy, false)); - - if (found == false) - { - return false; - } - } - - t0 = kfState.time; - } - - double dt = (time - t0).to_double(); - - // trace << "\n" << time << " " << satPos.Sat.id() << " dt: " << dt; - - if ( rSat0.isZero() == false - &&vSat0.isZero() == false) - { - auto& satOpts = acsConfig.getSatOpts(satPos.Sat); - - if (dt <= satOpts.ellipse_propagation_time_tolerance) - { - satPos.rSatEciDt = propagateEllipse (trace, t0, dt, rSat0, vSat0, satPos, true); - } - else - { - satPos.rSatEciDt = propagateFull (trace, t0, dt, rSat0, vSat0, satPos); - } - } - - return true; + VectorEci rSat0; + VectorEci vSat0; + GTime t0; + + auto& satNav = nav.satNavMap[satPos.Sat]; + + if (satNav.satPos0.rSatEci0.isZero() == false && satNav.satPos0.vSatEci0.isZero() == false && + satNav.satPos0.posTime != GTime::noTime()) + { + rSat0 = satNav.satPos0.rSatEci0; + vSat0 = satNav.satPos0.vSatEci0; + t0 = satNav.satPos0.posTime; + } + else + { + if (kfState_ptr == nullptr) + { + return false; + } + + auto& kfState = *kfState_ptr; + + // get orbit things from the state + for (int i = 0; i < 3; i++) + { + KFKey kfKey; + kfKey.type = KF::ORBIT; + kfKey.Sat = satPos.Sat; + + kfKey.num = i; + double dummy; + + bool found = + (kfKey.num = i, + kfState.getKFValue(kfKey, rSat0(i), &dummy, &dummy, false) != E_Source::NONE) && + (kfKey.num = i + 3, + kfState.getKFValue(kfKey, vSat0(i), &dummy, &dummy, false) != E_Source::NONE); + + if (found == false) + { + return false; + } + } + + t0 = kfState.time; + } + + double dt = (time - t0).to_double(); + + // trace << "\n" << time << " " << satPos.Sat.id() << " dt: " << dt; + + if (rSat0.isZero() == false && vSat0.isZero() == false) + { + auto& satOpts = acsConfig.getSatOpts(satPos.Sat); + + if (dt <= satOpts.ellipse_propagation_time_tolerance) + { + satPos.rSatEciDt = propagateEllipse(trace, t0, dt, rSat0, vSat0, satPos, true); + } + else + { + satPos.rSatEciDt = propagateFull(trace, t0, dt, rSat0, vSat0, satPos); + } + } + + satPos.posVar = 0; // todo Eugene: get actual variances from filter + + return true; } diff --git a/src/cpp/common/ephPrecise.cpp b/src/cpp/common/ephPrecise.cpp index dfd63c3c2..66a613fb8 100644 --- a/src/cpp/common/ephPrecise.cpp +++ b/src/cpp/common/ephPrecise.cpp @@ -1,525 +1,594 @@ - // #pragma GCC optimize ("O0") -#include -#include +#include "common/ephPrecise.hpp" #include -#include +#include #include +#include +#include +#include +#include "common/algebra.hpp" +#include "common/biases.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/ephemeris.hpp" +#include "common/gTime.hpp" +#include "common/mongoRead.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/planets.hpp" -using std::string; using std::array; using std::map; +using std::string; -#include - - -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "ephPrecise.hpp" -#include "constants.hpp" -#include "mongoRead.hpp" -#include "ephemeris.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "planets.hpp" -#include "common.hpp" -#include "biases.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" - -#define NMAX 10 /* order of polynomial interpolation */ -#define MAXDTE 900.0 /* max time difference to ephem time (s) */ -#define EXTERR_CLK 1E-3 /* extrapolation error for clock (m/s) */ -#define EXTERR_EPH 5E-7 /* extrapolation error for ephem (m/s^2) */ +constexpr int NMAX = 10; /* order of polynomial interpolation */ +constexpr double MAXDTE = 900.0; /* max time difference to ephem time (s) */ +constexpr double EXTERR_CLK = 1E-3; /* extrapolation error for clock (m/s) */ +constexpr double EXTERR_EPH = 5E-7; /* extrapolation error for ephem (m/s^2) */ /** read dcb parameters file -*/ -int readdcb( - string file) + */ +int readdcb(string file) { - std::ifstream inputStream(file); - if (!inputStream) - { -// trace(2,"dcb parameters file open error: %s\n",file); - return 0; - } - - BiasEntry entry; -// trace(3,"readdcbf: file=%s\n",file); - - entry.tini.bigTime = 3; - entry.measType = CODE; - entry.source = "dcb"; - - string line; - string type; - while (std::getline(inputStream, line)) - { - char* buff = &line[0]; - - if (strstr(buff,"DIFFERENTIAL (P1-P2) CODE BIASES")) type = "P1_P2"; - else if (strstr(buff,"DIFFERENTIAL (P1-C1) CODE BIASES")) type = "P1_C1"; - else if (strstr(buff,"DIFFERENTIAL (P2-C2) CODE BIASES")) type = "P2_C2"; - - char str1[32] = ""; - char str2[32] = ""; - - if ( type.empty() - ||sscanf(buff,"%31s %31s", str1, str2) < 1) - continue; - - double cbias = str2num(buff,26,9); - double rms = str2num(buff,38,9); - - entry.bias = cbias * 1E-9 * CLIGHT; /* ns -> m */ - entry.var = SQR(rms * 1E-9 * CLIGHT); - - SatSys Sat(str1); - - if (Sat.sys == +E_Sys::GPS) - { - if (type == "P1_P2") { entry.cod1 = E_ObsCode::L1W; entry.cod2 = E_ObsCode::L2W; } - else if (type == "P1_C1") { entry.cod1 = E_ObsCode::L1W; entry.cod2 = E_ObsCode::L1C; } - else if (type == "P2_C2") { entry.cod1 = E_ObsCode::L2W; entry.cod2 = E_ObsCode::L2D; } - else continue; - } - else if (Sat.sys == +E_Sys::GLO) - { - if (type == "P1_P2") { entry.cod1 = E_ObsCode::L1P; entry.cod2 = E_ObsCode::L2P; } - else if (type == "P1_C1") { entry.cod1 = E_ObsCode::L1P; entry.cod2 = E_ObsCode::L1C; } - else if (type == "P2_C2") { entry.cod1 = E_ObsCode::L2P; entry.cod2 = E_ObsCode::L2C; } - else continue; - } - - string id; - if ( !strcmp(str1,"G") - ||!strcmp(str1,"R")) - { - /* receiver dcb */ - entry.Sat = Sat; - entry.name = str2; - id = str2; - } - else if (Sat) - { - /* satellite dcb */ - entry.Sat = Sat; - entry.name = ""; - id = str1; - } - - if ( Sat.sys == +E_Sys::GLO - &&Sat.prn == 0) - { - // this seems to be a receiver - // for ambiguous GLO receiver bias id (i.e. PRN not specified), duplicate bias entry for each satellite - for (int prn = 1; prn <= NSATGLO; prn++) - { - Sat.prn = prn; - id = entry.name + ":" + Sat.id(); - // entry.Sat = Sat; - pushBiasEntry(id, entry); - } - } - else if ( Sat.sys == +E_Sys::GLO - &&Sat.prn != 0) - { - // this can be a receiver or satellite - id = id + ":" + Sat.id(); - pushBiasEntry(id, entry); - } - else - { - // this can be a receiver or satellite - id = id + ":" + Sat.sysChar(); - pushBiasEntry(id, entry); - } - } - - return 1; + std::ifstream inputStream(file); + if (!inputStream) + { + // trace(2,"dcb parameters file open error: %s\n",file); + return 0; + } + + BiasEntry entry; + // trace(3,"readdcbf: file=%s\n",file); + + entry.tini.bigTime = 3; + entry.measType = CODE; + entry.source = "dcb"; + + string line; + string type; + while (std::getline(inputStream, line)) + { + char* buff = &line[0]; + + if (strstr(buff, "DIFFERENTIAL (P1-P2) CODE BIASES")) + type = "P1_P2"; + else if (strstr(buff, "DIFFERENTIAL (P1-C1) CODE BIASES")) + type = "P1_C1"; + else if (strstr(buff, "DIFFERENTIAL (P2-C2) CODE BIASES")) + type = "P2_C2"; + + char str1[32] = ""; + char str2[32] = ""; + + if (type.empty() || sscanf(buff, "%31s %31s", str1, str2) < 1) + continue; + + double cbias = str2num(buff, 26, 9); + double rms = str2num(buff, 38, 9); + + entry.bias = cbias * 1E-9 * CLIGHT; /* ns -> m */ + entry.var = SQR(rms * 1E-9 * CLIGHT); + + SatSys Sat(str1); + + if (Sat.sys == E_Sys::GPS) + { + if (type == "P1_P2") + { + entry.cod1 = E_ObsCode::L1W; + entry.cod2 = E_ObsCode::L2W; + } + else if (type == "P1_C1") + { + entry.cod1 = E_ObsCode::L1W; + entry.cod2 = E_ObsCode::L1C; + } + else if (type == "P2_C2") + { + entry.cod1 = E_ObsCode::L2W; + entry.cod2 = E_ObsCode::L2D; + } + else + continue; + } + else if (Sat.sys == E_Sys::GLO) + { + if (type == "P1_P2") + { + entry.cod1 = E_ObsCode::L1P; + entry.cod2 = E_ObsCode::L2P; + } + else if (type == "P1_C1") + { + entry.cod1 = E_ObsCode::L1P; + entry.cod2 = E_ObsCode::L1C; + } + else if (type == "P2_C2") + { + entry.cod1 = E_ObsCode::L2P; + entry.cod2 = E_ObsCode::L2C; + } + else + continue; + } + + string id; + if (!strcmp(str1, "G") || !strcmp(str1, "R")) + { + /* receiver dcb */ + entry.Sat = Sat; + entry.name = str2; + id = str2; + } + else if (Sat) + { + /* satellite dcb */ + entry.Sat = Sat; + entry.name = ""; + id = str1; + } + updateRefTime(entry); + + if (Sat.sys == E_Sys::GLO && Sat.prn == 0) + { + // this seems to be a receiver + // for ambiguous GLO receiver bias id (i.e. PRN not specified), duplicate bias entry for + // each satellite + for (int prn = 1; prn <= NSATGLO; prn++) + { + Sat.prn = prn; + id = entry.name + ":" + Sat.id(); + // entry.Sat = Sat; + pushBiasEntry(id, entry); + } + } + else if (Sat.sys == E_Sys::GLO && Sat.prn != 0) + { + // this can be a receiver or satellite + id = id + ":" + Sat.id(); + pushBiasEntry(id, entry); + } + else + { + // this can be a receiver or satellite + id = id + ":" + Sat.sysChar(); + pushBiasEntry(id, entry); + } + } + + return 1; } /** polynomial interpolation by Neville's algorithm -*/ -double interpolate(const double *x, double *y, int n) + */ +double interpolate(const double* x, double* y, int n) { - for (int j=1; j < n; j++) - for (int i=0; i < n - j; i++) - { - y[i] = (x[i+j] * y[i] - x[i] * y[i+1]) / (x[i+j] - x[i]); - } + for (int j = 1; j < n; j++) + for (int i = 0; i < n - j; i++) + { + y[i] = (x[i + j] * y[i] - x[i] * y[i + 1]) / (x[i + j] - x[i]); + } - return y[0]; + return y[0]; } /** satellite position by precise ephemeris -*/ -bool pephpos( - Trace& trace, - GTime time, - SatSys Sat, - Navigation& nav, - Vector3d& rSat, - double* vare) + */ +bool pephpos(Trace& trace, GTime time, SatSys Sat, Navigation& nav, Vector3d& rSat, double* vare) { -// trace(4,"%s : time=%s sat=%s\n",__FUNCTION__, time.to_string(3).c_str(),Sat.id().c_str()); - - rSat = Vector3d::Zero(); - - if (nav.pephMap.empty()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Looking for precise positions, but no precise ephemerides found"; - - return false; - } - - auto it = nav.pephMap.find(Sat.id()); - if (it == nav.pephMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Looking for precise position, but no precise ephemerides found for " << Sat.id(); - - return false; - } - - auto& [id, pephMap] = *it; - - auto firstTime = pephMap.begin() ->first; - auto lastTime = pephMap.rbegin() ->first; - - if ( (pephMap.size() < NMAX + 1) - ||(time < firstTime - MAXDTE) - ||(time > lastTime + MAXDTE)) - { - tracepdeex(3, std::cout, "\nNo precise ephemeris for %s at %s, ephemerides cover %s to %s", - Sat.id() .c_str(), - time .to_string() .c_str(), - firstTime .to_string() .c_str(), - lastTime .to_string() .c_str()); - - return false; - } - -// //search for the ephemeris in the map - - auto peph_it = pephMap.lower_bound(time); - if (peph_it == pephMap.end()) - { - peph_it--; - } - - auto middle0 = peph_it; - - //go forward a few steps to make sure we're far from the end of the map. - for (int i = 0; i < NMAX/2; i++) - { - peph_it++; - if (peph_it == pephMap.end()) - { - break; - } - } - - //go backward a few steps to make sure we're far from the beginning of the map - for (int i = 0; i <= NMAX; i++) - { - peph_it--; - if (peph_it == pephMap.begin()) - { - break; - } - } - - auto begin = peph_it; - - vector t(NMAX+1); - vector p(NMAX+1); - double c[2]; - double s[3]; - - //get interpolation parameters and check all ephemerides have values. - peph_it = begin; - for (int i = 0; i <= NMAX; i++, peph_it++) - { - Peph& peph = peph_it->second; - if (peph.pos.isZero()) - { -// trace(3,"prec ephem outage %s sat=%s\n",time.to_string().c_str(), Sat.id().c_str()); - return false; - } - - auto& pos = peph.pos; - - t[i] = (peph.time - time).to_double(); - p[i] = pos; - } - - rSat = interpolate(t, p); - - if (vare) - { - double std = middle0->second.posStd.norm(); - - /* extrapolation error for orbit */ - if (t[0 ] > 0) std += EXTERR_EPH * SQR(t[0 ]) / 2; - else if (t[NMAX] < 0) std += EXTERR_EPH * SQR(t[NMAX]) / 2; - - *vare = SQR(std); - } - - return true; + // trace(4,"%s : time=%s sat=%s\n",__FUNCTION__, + // time.to_string(3).c_str(),Sat.id().c_str()); + + rSat = Vector3d::Zero(); + + if (nav.pephMap.empty()) + { + BOOST_LOG_TRIVIAL(warning) + << "Looking for precise positions, but no precise ephemerides found"; + + return false; + } + + auto it = nav.pephMap.find(Sat.id()); + if (it == nav.pephMap.end()) + { + BOOST_LOG_TRIVIAL(warning) + << "Looking for precise position, but no precise ephemerides found for " << Sat.id(); + + return false; + } + + auto& [id, pephMap] = *it; + + auto firstTime = pephMap.begin()->first; + auto lastTime = pephMap.rbegin()->first; + + if ((pephMap.size() < NMAX + 1) || (time < firstTime - MAXDTE) || (time > lastTime + MAXDTE)) + { + tracepdeex( + 3, + std::cout, + "\nNo precise ephemeris for %s at %s, ephemerides cover %s to %s", + Sat.id().c_str(), + time.to_string().c_str(), + firstTime.to_string().c_str(), + lastTime.to_string().c_str() + ); + + return false; + } + + // //search for the ephemeris in the map + + auto peph_it = pephMap.lower_bound(time); + if (peph_it == pephMap.end()) + { + peph_it--; + } + + auto middle_it = peph_it; + + // go forward a few steps to make sure we're far from the end of the map. + for (int i = 0; i < NMAX / 2; i++) + { + peph_it++; + if (peph_it == pephMap.end()) + { + break; + } + } + + // go backward a few steps to make sure we're far from the beginning of the map + for (int i = 0; i <= NMAX; i++) + { + peph_it--; + if (peph_it == pephMap.begin()) + { + break; + } + } + + auto begin = peph_it; + + vector t(NMAX + 1); + vector p(NMAX + 1); + double c[2]; + double s[3]; + + // get interpolation parameters and check all ephemerides have values. + peph_it = begin; + for (int i = 0; i <= NMAX; i++, peph_it++) + { + Peph& peph = peph_it->second; + if (peph.pos.isZero()) + { + // trace(3,"prec ephem outage %s sat=%s\n",time.to_string().c_str(), + // Sat.id().c_str()); + return false; + } + + auto& pos = peph.pos; + + t[i] = (peph.time - time).to_double(); + p[i] = pos; + } + + rSat = interpolate(t, p); + + if (vare) + { + double std = middle_it->second.posStd.norm(); + + /* extrapolation error for orbit */ + if (t[0] > 0) + std += EXTERR_EPH * SQR(t[0]) / 2; + else if (t[NMAX] < 0) + std += EXTERR_EPH * SQR(t[NMAX]) / 2; + + *vare = SQR(std); + } + + return true; } -template +template bool pclkMapClk( - Trace& trace, - GTime time, - string id, - Navigation& nav, - double& clk, - double* varc, - TYPE& pclkMaps) + Trace& trace, + GTime time, + string id, + Navigation& nav, + double& clk, + double* varc, + TYPE& pclkMaps +) { - auto it = pclkMaps.find(id); - if (it == pclkMaps.end()) - { - return false; - } - - auto& [key, pclkMap] = *it; - - if ( (pclkMap.size() < 2) - ||(time < pclkMap.begin() ->first - MAXDTE) - ||(time > pclkMap.rbegin() ->first + MAXDTE)) - { - BOOST_LOG_TRIVIAL(debug) << "no prec clock " << time.to_string() << " for " << id; - - return false; - } - - auto pclk_it = pclkMap.lower_bound(time); - if (pclk_it == pclkMap.end()) - { - pclk_it--; - } - - auto middle0_it = pclk_it; - - auto middle1_it = middle0_it; - if (middle0_it != pclkMap.begin()) - { - middle0_it--; - } - - auto& [time0, middle0] = *middle0_it; - auto& [time1, middle1] = *middle1_it; - - //linear interpolation - double t[2]; - double c[2]; - t[0] = (time - time0).to_double(); - t[1] = (time - time1).to_double(); - c[0] = middle0.clk; - c[1] = middle1.clk; - - bool use0 = true; - bool use1 = true; - - if (c[0] == INVALID_CLOCK_VALUE) { use0 = false; } - if (c[1] == INVALID_CLOCK_VALUE) { use1 = false; } - if (t[0] <= 0) { use1 = false; } - if (t[1] >= 0) { use0 = false; } - - if ( use0 == false - && use1 == false) - { - BOOST_LOG_TRIVIAL(debug) << "Precise clock outage " << time.to_string() << " for " << id; - - clk = 0; - - return false; - } - - double std = 0; - - if (use0 && use1) { clk = (c[1] * t[0] - c[0] * t[1]) / (t[0] - t[1]); double inv0 = 1 / middle0.clkStd * CLIGHT + EXTERR_CLK * fabs(t[0]); - double inv1 = 1 / middle1.clkStd * CLIGHT + EXTERR_CLK * fabs(t[1]); - std = 1 / (inv0 + inv1); } - else if (use0) { clk = c[0]; std = middle0.clkStd * CLIGHT + EXTERR_CLK * fabs(t[0]); } - else if (use1) { clk = c[1]; std = middle1.clkStd * CLIGHT + EXTERR_CLK * fabs(t[1]); } - - if (varc) - *varc = SQR(std); - - return true; + auto it = pclkMaps.find(id); + if (it == pclkMaps.end()) + { + return false; + } + + 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 + { + BOOST_LOG_TRIVIAL(debug) << "no prec clock " << time.to_string() << " for " << id; + + return false; + } + + auto pclk_it = pclkMap.lower_bound(time); + if (pclk_it == pclkMap.end()) + { + pclk_it--; + } + + auto point0_it = pclk_it; + auto point1_it = pclk_it; + if (point0_it != pclkMap.begin()) + { + point0_it--; + } + else // No need to check pclkMap.end() as pclkMap.size() >= 2 + { + point1_it++; + } + + auto& [time0, point0] = *point0_it; + auto& [time1, point1] = *point1_it; + + // linear interpolation + double t[2]; + double c[2]; + t[0] = (time - time0).to_double(); + t[1] = (time - time1).to_double(); + c[0] = point0.clk; + c[1] = point1.clk; + + double dt = t[0] - t[1]; // == (time1 - time0).to_double() + + bool use0 = true; + bool use1 = true; + + if (c[0] == INVALID_CLOCK_VALUE) + { + use0 = false; + } + if (c[1] == INVALID_CLOCK_VALUE) + { + use1 = false; + } + + if (t[0] == 0) + { + use1 = false; + } // Use point0 if valid + else if (t[1] == 0) + { + use0 = false; + } // Use point1 if valid + else if (dt > nav.pclkInterval) + { + use0 = false; + use1 = false; + } // Exclude interpolation within data gaps + + if (use0 == false && use1 == false) + { + BOOST_LOG_TRIVIAL(debug) << "Precise clock outage " << time.to_string() << " for " << id; + + clk = 0; + + return false; + } + + double clkVar = 0; + + if (use0 && use1) + { + clk = (c[1] * t[0] - c[0] * t[1]) / dt; + clkVar = (SQR(point1.clkStd * t[0]) + SQR(point0.clkStd * t[1])) / SQR(dt); + } + else if (use0) + { + clk = c[0]; + clkVar = SQR(point0.clkStd); + } + else if (use1) + { + clk = c[1]; + clkVar = SQR(point1.clkStd); + } + + if (varc) + *varc = clkVar; + + return true; } /** clock by precise clock -*/ -bool pephclk( - Trace& trace, - GTime time, - string id, - Navigation& nav, - double& clk, - double* varc) + */ +bool pephclk(Trace& trace, GTime time, string id, Navigation& nav, double& clk, double* varc) { -// BOOST_LOG_TRIVIAL(debug) << "pephclk : time=" << time.to_string(3) << " id=" << id; + // BOOST_LOG_TRIVIAL(debug) << "pephclk : time=" << time.to_string(3) << " id=" << id; - bool pass; - pass = pclkMapClk(trace, time, id, nav, clk, varc, nav.pclkMap); if (pass) return true; - pass = pclkMapClk(trace, time, id, nav, clk, varc, nav.pephMap); if (pass) return true; + bool pass; + pass = pclkMapClk(trace, time, id, nav, clk, varc, nav.pclkMap); + if (pass) + return true; + pass = pclkMapClk(trace, time, id, nav, clk, varc, nav.pephMap); + if (pass) + return true; - return false; + return false; } /** satellite antenna phase center offset in ecef -*/ + */ VectorEcef satAntOff( - Trace& trace, ///< Trace file to output to - GTime time, ///< Solution time - AttStatus& attStatus, ///< attitude status - SatSys& Sat, ///< Satellite ID - map& lamMap) ///< Lambda (wavelengths) map + Trace& trace, ///< Trace file to output to + GTime time, ///< Solution time + AttStatus& attStatus, ///< attitude status + SatSys& Sat, ///< Satellite ID + map& lamMap ///< Lambda (wavelengths) map +) { - tracepdeex(4, trace, "\n%-10s: time=%s sat=%s", __FUNCTION__, time.to_string().c_str(), Sat.id().c_str()); - - VectorEcef dAnt; - - E_FType j; - E_FType k; - E_FType l; - E_Sys sys = Sat.sys; - if (!satFreqs(sys,j,k,l)) - return dAnt; - - if ( lamMap[j] == 0 - ||lamMap[k] == 0) - { - return dAnt; - } - - double gamma = SQR(lamMap[k]) / SQR(lamMap[j]); - double C1 = gamma / (gamma - 1); - double C2 = -1 / (gamma - 1); - - /* iono-free LC */ - double varDummy = 0; - Vector3d pcoJ = antPco(Sat.id(), Sat.sys, j, time, varDummy, E_Radio::TRANSMITTER); - Vector3d pcoK = antPco(Sat.id(), Sat.sys, k, time, varDummy, E_Radio::TRANSMITTER); - - VectorEcef dant1 = body2ecef(attStatus, pcoJ); - VectorEcef dant2 = body2ecef(attStatus, pcoK); - - dAnt = C1 * dant1 - + C2 * dant2; - - return dAnt; + tracepdeex( + 4, + trace, + "\n%-10s: time=%s sat=%s", + __FUNCTION__, + time.to_string().c_str(), + Sat.id().c_str() + ); + + VectorEcef dAnt; + + E_FType j; + E_FType k; + E_FType l; + E_Sys sys = Sat.sys; + if (!satFreqs(sys, j, k, l)) + return dAnt; + + if (lamMap[j] == 0 || lamMap[k] == 0) + { + return dAnt; + } + + double gamma = SQR(lamMap[k]) / SQR(lamMap[j]); + double C1 = gamma / (gamma - 1); + double C2 = -1 / (gamma - 1); + + /* iono-free LC */ + double varDummy = 0; + Vector3d pcoJ = antPco(Sat.id(), Sat.sys, j, time, varDummy, E_Radio::TRANSMITTER); + Vector3d pcoK = antPco(Sat.id(), Sat.sys, k, time, varDummy, E_Radio::TRANSMITTER); + + VectorEcef dant1 = body2ecef(attStatus, pcoJ); + VectorEcef dant2 = body2ecef(attStatus, pcoK); + + dAnt = C1 * dant1 + C2 * dant2; + + return dAnt; } bool satClkPrecise( - Trace& trace, - GTime time, - SatSys& Sat, - double& clk, - double& clkVel, - double& clkVar, - Navigation& nav) + Trace& trace, + GTime time, + SatSys& Sat, + double& clk, + double& clkVel, + double& clkVar, + Navigation& nav +) { - clk = 0; - clkVel = 0; + clk = 0; + clkVel = 0; - tracepdeex(4, trace, "\n%-10s: time=%s sat=%s", __FUNCTION__, time.to_string().c_str(), Sat.id().c_str()); + tracepdeex( + 4, + trace, + "\n%-10s: time=%s sat=%s", + __FUNCTION__, + time.to_string().c_str(), + Sat.id().c_str() + ); - double tt = 1E-3; + double tt = 1E-3; - double clk2 = 0; + double clk2 = 0; - bool pass = pephclk(trace, time, Sat, nav, clk, &clkVar) - && pephclk(trace, time + tt, Sat, nav, clk2); + bool pass = + pephclk(trace, time, Sat, nav, clk, &clkVar) && pephclk(trace, time + tt, Sat, nav, clk2); - if ( pass == false - || clk == INVALID_CLOCK_VALUE) - { - tracepdeex(4, trace, " - pephclk failed"); - clk = 0; + if (pass == false || clk == INVALID_CLOCK_VALUE) + { + tracepdeex(4, trace, " - pephclk failed"); + clk = 0; - return false; - } + return false; + } - clkVel = (clk2 - clk) / tt; + clkVel = (clk2 - clk) / tt; - return true; + return true; } - /** Satellite position/clock by precise ephemeris/clock -*/ + */ bool satPosPrecise( - Trace& trace, - GTime time, - SatSys& Sat, - Vector3d& rSat, - Vector3d& satVel, - double& ephVar, - Navigation& nav) + Trace& trace, + GTime time, + SatSys& Sat, + Vector3d& rSat, + Vector3d& satVel, + double& posVar, + Navigation& nav +) { - rSat = Vector3d::Zero(); - satVel = Vector3d::Zero(); + rSat = Vector3d::Zero(); + satVel = Vector3d::Zero(); - tracepdeex(4, trace, "\n%-10s: time=%s sat=%s", __FUNCTION__, time.to_string().c_str(), Sat.id().c_str()); + tracepdeex( + 4, + trace, + "\n%-10s: time=%s sat=%s", + __FUNCTION__, + time.to_string().c_str(), + Sat.id().c_str() + ); - double tt = 10e-3; + double tt = 10e-3; - Vector3d rSat1 = Vector3d::Zero(); - Vector3d rSat2 = Vector3d::Zero(); + Vector3d rSat1 = Vector3d::Zero(); + Vector3d rSat2 = Vector3d::Zero(); - bool pass = pephpos(trace, time - tt, Sat, nav, rSat1, &ephVar) - && pephpos(trace, time + tt, Sat, nav, rSat2); + bool pass = pephpos(trace, time - tt, Sat, nav, rSat1, &posVar) && + pephpos(trace, time + tt, Sat, nav, rSat2); - if (pass == false) - { - tracepdeex(4, trace, " - pephpos failed"); + if (pass == false) + { + tracepdeex(4, trace, " - pephpos failed"); - return false; - } + return false; + } - rSat = (rSat2 + rSat1) / 2; - satVel = (rSat2 - rSat1) / (2 * tt); + rSat = (rSat2 + rSat1) / 2; + satVel = (rSat2 - rSat1) / (2 * tt); - return true; + return true; } -bool satPosPrecise( - Trace& trace, - GTime time, - SatPos& satPos, - Navigation& nav) +bool satPosPrecise(Trace& trace, GTime time, SatPos& satPos, Navigation& nav) { - return satPosPrecise( - trace, - time, - satPos.Sat, - satPos.rSatCom, - satPos.satVel, - satPos.posVar, - nav); + return satPosPrecise( + trace, + time, + satPos.Sat, + satPos.rSatCom, + satPos.satVel, + satPos.posVar, + nav + ); } -bool satClkPrecise( - Trace& trace, - GTime time, - SatPos& satPos, - Navigation& nav) +bool satClkPrecise(Trace& trace, GTime time, SatPos& satPos, Navigation& nav) { - return satClkPrecise( - trace, - time, - satPos.Sat, - satPos.satClk, - satPos.satClkVel, - satPos.satClkVar, - nav); + return satClkPrecise( + trace, + time, + satPos.Sat, + satPos.satClk, + satPos.satClkVel, + satPos.satClkVar, + nav + ); } diff --git a/src/cpp/common/ephPrecise.hpp b/src/cpp/common/ephPrecise.hpp index e91ceab54..5f22d7d85 100644 --- a/src/cpp/common/ephPrecise.hpp +++ b/src/cpp/common/ephPrecise.hpp @@ -1,16 +1,14 @@ - #pragma once #include #include +#include "common/gTime.hpp" +#include "common/satSys.hpp" +#include "common/trace.hpp" using std::string; -#include "satSys.hpp" -#include "gTime.hpp" -#include "trace.hpp" - -//forward declarations +// forward declarations struct Navigation; struct SatPos; struct GObs; @@ -18,49 +16,47 @@ struct Peph; int readdcb(string file); -void readSp3ToNav( - string& file, - Navigation& nav, - int opt); +void readSp3ToNav(string& file, Navigation& nav, int opt); bool readsp3( - std::istream& fileStream, - vector& pephList, - int opt, - E_TimeSys& tsys, - double* bfact); + std::istream& fileStream, + vector& pephList, + int opt, + E_TimeSys& tsys, + double* bfact +); -double interpolate(const double *x, double *y, int n); +double interpolate(const double* x, double* y, int n); /** polynomial interpolation by Neville's algorithm. * Sketchy formatting to only require +, * operators on TYPE */ -template -TYPE interpolate( - vector& x, - vector& y) +template +TYPE interpolate(vector& x, vector& y) { - for (int j = 1; j < x.size(); j++) - for (int i = 0; i < x.size() - j; i++) - { - y[i] = (y[i] * x[i+j] + y[i+1] * x[i] * -1) * (1 / (x[i+j] - x[i])); - } + for (int j = 1; j < x.size(); j++) + for (int i = 0; i < x.size() - j; i++) + { + y[i] = (y[i] * x[i + j] + y[i + 1] * x[i] * -1) * (1 / (x[i + j] - x[i])); + } - return y[0]; + return y[0]; } bool pephclk( - Trace& trace, - GTime time, - string id, - Navigation& nav, - double& dtSat, - double* varc = nullptr); + Trace& trace, + GTime time, + string id, + Navigation& nav, + double& dtSat, + double* varc = nullptr +); bool pephpos( - Trace& trace, - GTime time, - SatSys Sat, - Navigation& nav, - Vector3d& rSat, - double* vare = nullptr); + Trace& trace, + GTime time, + SatSys Sat, + Navigation& nav, + Vector3d& rSat, + double* vare = nullptr +); diff --git a/src/cpp/common/ephSBAS.cpp b/src/cpp/common/ephSBAS.cpp index d443e9b2b..a8e9e6979 100644 --- a/src/cpp/common/ephSBAS.cpp +++ b/src/cpp/common/ephSBAS.cpp @@ -1,42 +1,156 @@ -#if (0) -/* satellite position and clock with sbas correction -------------------------*/ -// int satpos_sbas(gtime_t time, gtime_t teph, SatSys Sat, const nav_t* nav, -// double* rs, double* dtSat, double* var, int* svh) -// { -// const sbssatp_t* sbs; -// int i; -// -// trace(4, __FUNCTION__ ": time=%s sat=%2d\n", time.to_string(3).c_str(), Sat); -// -// /* search sbas satellite correciton */ -// for (i = 0; i < nav->sbssat.nsat; i++) -// { -// sbs = nav->sbssat.sat + i; -// -// if (sbs->Sat == Sat) -// break; -// } -// -// if (i >= nav->sbssat.nsat) -// { -// trace(2, "no sbas correction for orbit: %s sat=%2d\n", time.to_string(0).c_str(), Sat); -// ephpos(time, teph, Sat, nav, -1, rs, dts, var, svh); -// *svh = -1; -// -// return 0; -// } -// -// /* satellite postion and clock by broadcast ephemeris */ -// if (!ephpos(time, teph, Sat, nav, sbs->lcorr.iode, rs, dts, var, svh)) -// return 0; -// -// /* sbas satellite correction (long term and fast) */ -// if (sbssatcorr(time, Sat, nav, rs, dts, var)) -// return 1; -// -// *svh = -1; -// -// return 0; -// } -#endif +#include "common/acsConfig.hpp" +#include "common/eigenIncluder.hpp" +#include "common/ephemeris.hpp" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "sbas/sbas.hpp" + +bool dfmc2Pos(Trace& trace, GTime time, SatPos& satPos, Navigation& nav) +{ + return false; +} + +bool satPosSBAS(Trace& trace, GTime time, GTime teph, SatPos& satPos, Navigation& nav) +{ + loadSBASdata(trace, teph, nav); + SBASMaps& sbsMaps = satPos.satNav_ptr->currentSBAS; + SatSys& Sat = satPos.Sat; + Vector3d& rSat = satPos.rSatApc; + Vector3d& satVel = satPos.satVel; + double& satClk = satPos.satClk; + double& satClkVel = satPos.satClkVel; + bool& ephPosValid = satPos.ephPosValid; + bool& ephClkValid = satPos.ephClkValid; + int& iodeClk = satPos.iodeClk; + int& iodePos = satPos.iodePos; + double& posVar = satPos.posVar; + double& clkVar = satPos.satClkVar; + + ephPosValid = false; + ephClkValid = false; + + double maxdt = 30; + switch (acsConfig.sbsInOpts.freq) + { + case 1: + maxdt = acsConfig.sbsInOpts.prec_aproach ? 12 : 16; + break; + case 5: + maxdt = acsConfig.sbsInOpts.prec_aproach ? 12 : 16; + break; + default: + return false; + } + + bool pass = false; + SBASIntg sbsInt; + for (auto& [iodm, intData] : sbsMaps.Integrity) + { + if (fabs((time - intData.trec).to_double()) > maxdt) + continue; + pass = true; + sbsInt = intData; + } + if (!pass) + { + tracepdeex(4, trace, "\nSBASEPH No Integrity data for %s", Sat.id().c_str()); + return false; + } + + pass = false; + int selIode = -1; + for (auto& [updtTime, iode] : sbsMaps.corrUpdt) + { + auto& slowCorr = sbsMaps.slowCorr[iode]; + + if (slowCorr.Ivalid < 0) + { + continue; + } + + if (fabs((time - slowCorr.trec).to_double()) > slowCorr.Ivalid) + { + continue; + } + + pass = true; + pass &= satPosBroadcast(trace, time, teph, satPos, nav, iode); + pass &= satClkBroadcast(trace, time, teph, satPos, nav, iode); + if (pass) + { + selIode = iode; + break; + } + } + if (!pass) + { + tracepdeex(4, trace, "\nSBASEPH No Correction data for %s", Sat.id().c_str()); + return false; + } + tracepdeex( + 5, + trace, + "\nBRDCEPH %s %s %13.3f %13.3f %13.3f %13.3f %4d", + time.to_string().c_str(), + Sat.id().c_str(), + rSat[0], + rSat[1], + rSat[2], + satClk * CLIGHT, + selIode + ); + + switch (acsConfig.sbsInOpts.freq) + { + case 5: + clkVar = estimateDFMCVar(trace, time, satPos, sbsInt) / SQR(CLIGHT); + break; + default: + return false; + } + posVar = 0.0; + if (clkVar < 0) + { + tracepdeex(4, trace, "\nSBASEPH Unknown Vairance for %s", Sat.id().c_str()); + return false; + } + auto& sbs = sbsMaps.slowCorr[selIode]; + double dt = (time - sbs.toe).to_double(); + for (int i = 0; i < 3; i++) + { + rSat[i] += sbs.dPos[i] + dt * sbs.ddPos[i]; + satVel[i] += sbs.ddPos[i]; + } + satClk += (sbs.dPos[3] + dt * sbs.ddPos[3]) / CLIGHT; + satClkVel += (sbs.ddPos[3]) / CLIGHT; + + if (Sat.sys == E_Sys::GPS && acsConfig.sbsInOpts.use_do259) + { + Eph* eph_ptr = seleph(trace, time, Sat, E_NavMsgType::LNAV, selIode, nav); + if (eph_ptr == nullptr) + return false; + satClk -= eph_ptr->tgd[0]; + } + + tracepdeex( + 5, + trace, + "\nSBASEPH %s %s %13.3f %13.3f %13.3f %13.3f %4d", + time.to_string().c_str(), + Sat.id().c_str(), + rSat[0], + rSat[1], + rSat[2], + satClk * CLIGHT, + selIode + ); + + ephPosValid = true; + ephClkValid = true; + + iodeClk = selIode; + iodePos = selIode; + + return true; +} diff --git a/src/cpp/common/ephSSR.cpp b/src/cpp/common/ephSSR.cpp index 229e5ee49..a2d861d1f 100644 --- a/src/cpp/common/ephSSR.cpp +++ b/src/cpp/common/ephSSR.cpp @@ -1,392 +1,527 @@ +#include "common/acsConfig.hpp" +#include "common/eigenIncluder.hpp" +#include "common/ephemeris.hpp" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/ssr.hpp" -#include "eigenIncluder.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "ephemeris.hpp" -#include "gTime.hpp" -#include "ssr.hpp" - - -#define DEFURASSR 0.03 ///< default accurary of ssr corr (m) -#define MAXECORSSR 15 ///< max orbit correction of ssr (m) -#define MAXCCORSSR (1E-6*CLIGHT) ///< max clock correction of ssr (m) +constexpr double DEFURASSR = 0.03; ///< default accurary of ssr corr (m) +constexpr double MAXECORSSR = 15; ///< max orbit correction of ssr (m) +constexpr double MAXCCORSSR = (1E-6 * CLIGHT); ///< max clock correction of ssr (m) /** variance by ura ssr (ref [4]) -*/ -double var_urassr( - int ura) + */ +double var_urassr(int ura) { - if (ura <= 0) return SQR(DEFURASSR); - if (ura >= 63) return SQR(5.4665); + if (ura <= 0) + return SQR(DEFURASSR); + if (ura >= 63) + return SQR(5.4665); - double std = (pow(3, (ura >> 3) & 7) * (1.0 + (ura & 7) / 4.0) - 1.0) * 1E-3; - return SQR(std); + double std = (pow(3, (ura >> 3) & 7) * (1.0 + (ura & 7) / 4.0) - 1.0) * 1E-3; + return SQR(std); } Matrix3d rac2ecef( - Vector3d& rSat, // Sat position (ECEF) - Vector3d& satVel) // Sat velocity (ECEF) + Vector3d& rSat, // Sat position (ECEF) + Vector3d& satVel +) // Sat velocity (ECEF) { - Matrix3d ecef2racMat = ecef2rac(rSat, satVel); + Matrix3d ecef2racMat = ecef2rac(rSat, satVel); - return ecef2racMat.transpose(); + return ecef2racMat.transpose(); } -template -void cullSSRMap( - GTime time, - TYPE& map) +template +void cullSSRMap(GTime time, TYPE& map) { - for (auto it = map.begin(); it != map.end(); ) - { - auto& [ssrtime, ssr] = *it; - - if (ssr.t0 < time - ssr.udi * acsConfig.validity_interval_factor) - { - it = map.erase(it); - } - else - { - ++it; - } - } + if (map.empty()) + { + return; + } + + for (auto it = std::next(map.begin()); // Always reserve the latest entry when culling + it != map.end();) + { + auto& [ssrtime, ssr] = *it; + + if (ssr.t0 < time - ssr.udi * acsConfig.validity_interval_factor) + { + it = map.erase(it); + } + else + { + ++it; + } + } } -void cullOldSSRs( - GTime time) +void cullOldSSRs(GTime time) { - for (auto& [Sat, satNav] : nav.satNavMap) - { - cullSSRMap(time, satNav.receivedSSR.ssrCodeBias_map); - cullSSRMap(time, satNav.receivedSSR.ssrPhasBias_map); - cullSSRMap(time, satNav.receivedSSR.ssrClk_map); - cullSSRMap(time, satNav.receivedSSR.ssrEph_map); - cullSSRMap(time, satNav.receivedSSR.ssrHRClk_map); - cullSSRMap(time, satNav.receivedSSR.ssrUra_map); - } + for (auto& [Sat, satNav] : nav.satNavMap) + { + cullSSRMap(time, satNav.receivedSSR.ssrCodeBias_map); + cullSSRMap(time, satNav.receivedSSR.ssrPhasBias_map); + cullSSRMap(time, satNav.receivedSSR.ssrClk_map); + cullSSRMap(time, satNav.receivedSSR.ssrEph_map); + cullSSRMap(time, satNav.receivedSSR.ssrHRClk_map); + cullSSRMap(time, satNav.receivedSSR.ssrUra_map); + } } - bool ssrPosDelta( - Trace& trace, - GTime time, - GTime ephTime, - SatPos& satPos, - const SSRMaps& ssrMaps, - Vector3d& dPos, - int& iodPos, - int& iodEph, - GTime& validStart, - GTime& validStop) + Trace& trace, + GTime time, + GTime ephTime, + SatPos& satPos, + const SSRMaps& ssrMaps, + Vector3d& dPos, + int& iodPos, + int& iodEph, + GTime& validStart, + GTime& validStop +) { - if (ssrMaps.ssrEph_map.empty()) - { - satPos.failureSsrPosEmpty = true; - - return false; - } - - //get 'price is right' closest ssr components to ephemeris time. - auto ephIt = ssrMaps.ssrEph_map.lower_bound(ephTime); - if (ephIt == ssrMaps.ssrEph_map.end()) - { - satPos.failureSsrPosTime = true; - - return false; - } - - auto& [t_e, ssrEph] = *ephIt; - - iodPos = ssrEph.iod; - iodEph = ssrEph.iode; - - double tEph = (time - ssrEph.t0).to_double(); - - validStart = ssrEph.t0 - ssrEph.udi / 2; - validStop = ssrEph.t0 + ssrEph.udi / 2; - - /* ssr orbit and clock correction (ref [4]) */ - if (fabs(tEph) > ssrEph.udi * acsConfig.validity_interval_factor) - { - satPos.failureSsrPosUdi = true; - - tracepdeex(2, std::cout, "age of ssr error: %s t=%.0f > %.0f\n", time.to_string().c_str(), tEph, ssrEph.udi * acsConfig.validity_interval_factor); - return false; - } - - dPos = ssrEph.deph - + ssrEph.ddeph * tEph; - - if (dPos.norm() > MAXECORSSR) - { - satPos.failureSsrPosMag = true; - - tracepdeex(2, std::cout, "SSR pos correction too large : %s %s deph=%.1fm\n", time.to_string().c_str(), satPos.Sat.id().c_str(), dPos.norm()); - return false; - } - - tracepdeex(5, trace, "\n%s %2d %2d %lf %s %s %lf", __FUNCTION__, iodPos, iodEph, tEph, validStart.to_string().c_str(), validStop.to_string().c_str(), dPos.norm()); - - return true; + if (ssrMaps.ssrEph_map.empty()) + { + satPos.failureSsrPosEmpty = true; + + return false; + } + + // get 'price is right' closest ssr components to ephemeris time. + auto ephIt = ssrMaps.ssrEph_map.lower_bound(ephTime); + if (ephIt == ssrMaps.ssrEph_map.end()) + { + satPos.failureSsrPosTime = true; + + return false; + } + + auto& [t_e, ssrEph] = *ephIt; + + iodPos = ssrEph.iod; + iodEph = ssrEph.iode; + + double tEph = (time - ssrEph.t0).to_double(); + + validStart = ssrEph.t0 - ssrEph.udi / 2; + validStop = ssrEph.t0 + ssrEph.udi / 2; + + /* ssr orbit and clock correction (ref [4]) */ + if (fabs(tEph) > ssrEph.udi * acsConfig.validity_interval_factor) + { + satPos.failureSsrPosUdi = true; + + tracepdeex( + 2, + std::cout, + "age of ssr error: %s t=%.0f > %.0f\n", + time.to_string().c_str(), + tEph, + ssrEph.udi * acsConfig.validity_interval_factor + ); + return false; + } + + dPos = ssrEph.deph + ssrEph.ddeph * tEph; + + if (dPos.norm() > MAXECORSSR) + { + satPos.failureSsrPosMag = true; + + tracepdeex( + 2, + std::cout, + "SSR pos correction too large : %s %s deph=%.1fm\n", + time.to_string().c_str(), + satPos.Sat.id().c_str(), + dPos.norm() + ); + return false; + } + + tracepdeex( + 5, + trace, + "\n%s %2d %2d %lf %s %s %lf", + __FUNCTION__, + iodPos, + iodEph, + tEph, + validStart.to_string().c_str(), + validStop.to_string().c_str(), + dPos.norm() + ); + + return true; } bool ssrClkDelta( - Trace& trace, - GTime time, - GTime ephTime, - SatPos& satPos, - const SSRMaps& ssrMaps, - double& dclk, - int& iodClk, - GTime& validStart, - GTime& validStop) + Trace& trace, + GTime time, + GTime ephTime, + SatPos& satPos, + const SSRMaps& ssrMaps, + double& dclk, + int& iodClk, + GTime& validStart, + GTime& validStop +) { - if (ssrMaps.ssrClk_map.empty()) - { - satPos.failureSsrClkEmpty = true; - tracepdeex(4, std::cout, "No SSR corrections for sat=%s\n", satPos.Sat.id().c_str()); - return false; - } - - //get 'price is right' closest ssr components to ephemeris time - auto clkIt = ssrMaps.ssrClk_map.lower_bound(ephTime); - if (clkIt == ssrMaps.ssrClk_map.end()) - { - satPos.failureSsrClkTime = true; - tracepdeex(4, std::cout, "No SSR corrections before %s sat=%s\n", ephTime.to_string().c_str(), satPos.Sat.id().c_str()); - return false; - } - - auto& [t_c, ssrClk] = *clkIt; - - iodClk = ssrClk.iod; - - double tClk = (time - ssrClk.t0).to_double(); - - validStart = ssrClk.t0 - ssrClk.udi / 2; - validStop = ssrClk.t0 + ssrClk.udi / 2; - - /* ssr orbit and clock correction (ref [4]) */ - if (fabs(tClk) > ssrClk.udi * acsConfig.validity_interval_factor) - { - satPos.failureSsrClkUdi = true; - - tracepdeex(4, std::cout, "age of ssr error: %s sat=%s\n", time.to_string().c_str(), satPos.Sat.id().c_str()); - return false; - } - - dclk = ssrClk.dclk[0] - + ssrClk.dclk[1] * tClk - + ssrClk.dclk[2] * tClk * tClk; - - /* ssr highrate clock correction (ref [4]) */ - auto hrcIt = ssrMaps.ssrHRClk_map.lower_bound(ephTime); - if (hrcIt != ssrMaps.ssrHRClk_map.end()) - { - auto& [t_h, ssrHrc] = *hrcIt; - - double tHrc = (time - ssrHrc.t0).to_double(); - - if ( ssrClk.iod == ssrHrc.iod - && fabs(tHrc) < ssrClk.udi * acsConfig.validity_interval_factor) - { - dclk += ssrHrc.hrclk; - } - } - - if (fabs(dclk) > MAXCCORSSR) - { - satPos.failureSsrClkMag = true; - - tracepdeex(2, std::cout,"SSR clk correction too large : %s %s dclk=%.1f\n", time.to_string().c_str(), satPos.Sat.id().c_str(), dclk); - return 0; - } - - tracepdeex(5, trace, "\n%s %2d %2d %lf %s %s %lf", __FUNCTION__, iodClk, 0, tClk, validStart.to_string().c_str(), validStop.to_string().c_str(), dclk); - - return true; + if (ssrMaps.ssrClk_map.empty()) + { + satPos.failureSsrClkEmpty = true; + tracepdeex(4, std::cout, "No SSR corrections for sat=%s\n", satPos.Sat.id().c_str()); + return false; + } + + // get 'price is right' closest ssr components to ephemeris time + auto clkIt = ssrMaps.ssrClk_map.lower_bound(ephTime); + if (clkIt == ssrMaps.ssrClk_map.end()) + { + satPos.failureSsrClkTime = true; + tracepdeex( + 4, + std::cout, + "No SSR corrections before %s sat=%s\n", + ephTime.to_string().c_str(), + satPos.Sat.id().c_str() + ); + return false; + } + + auto& [t_c, ssrClk] = *clkIt; + + iodClk = ssrClk.iod; + + double tClk = (time - ssrClk.t0).to_double(); + + validStart = ssrClk.t0 - ssrClk.udi / 2; + validStop = ssrClk.t0 + ssrClk.udi / 2; + + /* ssr orbit and clock correction (ref [4]) */ + if (fabs(tClk) > ssrClk.udi * acsConfig.validity_interval_factor) + { + satPos.failureSsrClkUdi = true; + + tracepdeex( + 4, + std::cout, + "age of ssr error: %s sat=%s\n", + time.to_string().c_str(), + satPos.Sat.id().c_str() + ); + return false; + } + + dclk = ssrClk.dclk[0] + ssrClk.dclk[1] * tClk + ssrClk.dclk[2] * tClk * tClk; + + /* ssr highrate clock correction (ref [4]) */ + auto hrcIt = ssrMaps.ssrHRClk_map.lower_bound(ephTime); + if (hrcIt != ssrMaps.ssrHRClk_map.end()) + { + auto& [t_h, ssrHrc] = *hrcIt; + + double tHrc = (time - ssrHrc.t0).to_double(); + + if (ssrClk.iod == ssrHrc.iod && + fabs(tHrc) < ssrClk.udi * acsConfig.validity_interval_factor) + { + dclk += ssrHrc.hrclk; + } + } + + if (fabs(dclk) > MAXCCORSSR) + { + satPos.failureSsrClkMag = true; + + tracepdeex( + 2, + std::cout, + "SSR clk correction too large : %s %s dclk=%.1f\n", + time.to_string().c_str(), + satPos.Sat.id().c_str(), + dclk + ); + return 0; + } + + tracepdeex( + 5, + trace, + "\n%s %2d %2d %lf %s %s %lf", + __FUNCTION__, + iodClk, + 0, + tClk, + validStart.to_string().c_str(), + validStop.to_string().c_str(), + dclk + ); + + return true; } /** satellite position and clock with ssr correction -*/ -bool satPosSSR( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - Navigation& nav) + */ +bool satPosSSR(Trace& trace, GTime time, GTime teph, SatPos& satPos, Navigation& nav) { - SSRMaps& ssrMaps = satPos.satNav_ptr->receivedSSR; - SatSys& Sat = satPos.Sat; - Vector3d& rSat = satPos.rSatCom; - Vector3d& satVel = satPos.satVel; - double& satClk = satPos.satClk; - double& satClkVel = satPos.satClkVel; - bool& ephPosValid = satPos.ephPosValid; - bool& ephClkValid = satPos.ephClkValid; - int& iodeClk = satPos.iodeClk; - int& iodePos = satPos.iodePos; - double& posVar = satPos.posVar; - double& clkVar = satPos.satClkVar; - -// tracepdeex(4, trace, __FUNCTION__ ": time=%s sat=%2d\n", time.to_string().c_str(), satPos.Sat); - ephPosValid = false; - ephClkValid = false; - - int iodPos; - int iodEph; - int iodClk; - GTime ephValidStart; - GTime ephValidStop; - GTime clkValidStart; - GTime clkValidStop; - Vector3d rSat0; - Vector3d dPos; - double satClk0; - double dClk; - GTime ephTime = time; - - bool once = true; - - while (true) - { - bool posDeltaPass = ssrPosDelta(trace, time, ephTime, satPos, ssrMaps, dPos, iodPos, iodEph, ephValidStart, ephValidStop); - bool clkDeltaPass = ssrClkDelta(trace, time, ephTime, satPos, ssrMaps, dClk, iodClk, clkValidStart, clkValidStop); - - if ( posDeltaPass == false - || clkDeltaPass == false) - { - satPos.failureSSRFail = true; - - BOOST_LOG_TRIVIAL(warning) << "Warning: SSR Corrections not found for " << satPos.Sat.id(); - trace << "\n" << "Warning: SSR Corrections not found for " << satPos.Sat.id(); - - return false; - } - - if ( ephValidStart >= clkValidStop - ||clkValidStart >= ephValidStop) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Timing inconsistent for " << satPos.Sat.id() << " : " << ephValidStart.to_string(0) << " - " << ephValidStop.to_string(0) << " " << clkValidStart.to_string(0) << " - " << clkValidStop.to_string(0); - trace << "\n" << "Warning: Timing inconsistent for " << satPos.Sat.id() << " : " << ephValidStart.to_string(0) << " - " << ephValidStop.to_string(0) << " " << clkValidStart.to_string(0) << " - " << clkValidStop.to_string(0); - - if (ephValidStart >= clkValidStop) ephTime = clkValidStop - 0.5; - if (clkValidStart >= ephValidStop) ephTime = ephValidStop - 0.5; - continue; - } - - if (iodClk != iodPos) - { - satPos.failureIodeConsistency = true; - - BOOST_LOG_TRIVIAL(warning) << "Warning: IOD inconsistent for " << satPos.Sat.id() << iodClk << " " << iodPos; - trace << "\n" << "Warning: IOD inconsistent for " << satPos.Sat.id() << iodClk << " " << iodPos; - - return false; - } - - iodePos = iodEph; - iodeClk = iodEph; - - bool pass = true; - pass &= satPosBroadcast(trace, time, teph, Sat, rSat0, satVel, posVar, ephPosValid, iodePos, nav); - pass &= satClkBroadcast(trace, time, teph, Sat, satClk0, satClkVel, clkVar, ephClkValid, iodeClk, nav); - - if (pass == false) - { - if (once) - { - once = false; - - if (clkValidStart < ephValidStart) ephTime = clkValidStart - 0.5; - else ephTime = ephValidStart - 0.5; - - BOOST_LOG_TRIVIAL(warning) << "Warning: IODE BRDC not found for " << satPos.Sat.id() << " - adjusting ephTime"; - trace << "\n" << "Warning: IODE BRDC not found for " << satPos.Sat.id() << " - adjusting ephTime"; - - continue; - } - satPos.failureBroadcastEph = true; - - BOOST_LOG_TRIVIAL(warning) << "Warning: IODE BRDC not found for " << satPos.Sat.id(); - trace << "\n" << "Warning: IODE BRDC not found for " << satPos.Sat.id(); - - return false; - } - - break; - } - - tracepdeex(4, trace, "\nBRDCEPH %s %s %13.3f %13.3f %13.3f %11.3f %2d", time.to_string().c_str(), Sat.id().c_str(), rSat0[0], rSat0[1], rSat0[2], 1e9*satClk0, iodePos); - - Matrix3d rac2ecefMat = rac2ecef(rSat0, satVel); - - Vector3d dPosECEF = rac2ecefMat * dPos; - - rSat = rSat0 - dPosECEF; - - /* t_corr = t_sv - (dtSat(brdc) + dClk(ssr) / CLIGHT) (ref [10] eq.3.12-7) */ - satClk = satClk0 + dClk / CLIGHT; - - /* variance by ssr ura */ - double ura = -1; - - auto uraIt = ssrMaps.ssrUra_map.lower_bound(time); - if (uraIt != ssrMaps.ssrUra_map.end()) - { - auto& [t_u, ssrUra] = *uraIt; - - ura = ssrUra.ura; - } - - clkVar = var_urassr(ura); - posVar = 0; - - tracepdeex(3, trace, "\nSSR_EPH %s %s %13.3f %13.3f %13.3f %11.3f ", time.to_string().c_str(), Sat.id().c_str(), rSat[0], rSat[1], rSat[2], 1e9*satClk); - - tracepdeex(5, trace, "\n%s: %s sat=%s deph=%6.3f %6.3f %6.3f dclk=%6.3f var=%6.3f iode=%d clktimes:%s %s ephtimes%s %s\n", - __FUNCTION__, - time.to_string().c_str(), - Sat.id().c_str(), - dPos[0], - dPos[1], - dPos[2], - dClk, - clkVar, - iodEph, - clkValidStart .to_string().c_str(), - clkValidStop .to_string().c_str(), - ephValidStart .to_string().c_str(), - ephValidStop .to_string().c_str()); - - - if (round(time.bigTime) == time.bigTime) - { - traceJson(1, nullStream, time, - { - {"data", __FUNCTION__ }, - {"Sat", satPos.Sat.id() } - }, - { - {"rSat0[0]", rSat0[0]}, - {"rSat0[1]", rSat0[1]}, - {"rSat0[2]", rSat0[2]}, - {"rSat[0]", rSat[0]}, - {"rSat[1]", rSat[1]}, - {"rSat[2]", rSat[2]}, - {"dPos[0]", dPos[0]}, - {"dPos[1]", dPos[1]}, - {"dPos[2]", dPos[2]}, - {"satClk0", satClk0}, - {"satClk", satClk}, - {"dClk", dClk}, - {"iode", iodEph}, - {"iodClk", iodClk}, - {"iodPos", iodPos}, - {"clkValidStart", (long int)clkValidStart.bigTime}, - {"ephValidStart", (long int)ephValidStart.bigTime} - }); - } - - return true; + SSRMaps& ssrMaps = satPos.satNav_ptr->receivedSSR; + SatSys& Sat = satPos.Sat; + Vector3d& rSat = satPos.rSatCom; + Vector3d& satVel = satPos.satVel; + double& satClk = satPos.satClk; + double& satClkVel = satPos.satClkVel; + bool& ephPosValid = satPos.ephPosValid; + bool& ephClkValid = satPos.ephClkValid; + int& iodeClk = satPos.iodeClk; + int& iodePos = satPos.iodePos; + double& posVar = satPos.posVar; + double& satClkVar = satPos.satClkVar; + + // tracepdeex(4, trace, __FUNCTION__ ": time=%s sat=%2d\n", time.to_string().c_str(), + // satPos.Sat); + ephPosValid = false; + ephClkValid = false; + + int iodPos; + int iodEph; + int iodClk; + GTime ephValidStart; + GTime ephValidStop; + GTime clkValidStart; + GTime clkValidStop; + Vector3d rSat0; + Vector3d dPos; + double satClk0; + double dClk; + GTime ephTime = time; + + bool once = true; + + while (true) + { + bool posDeltaPass = ssrPosDelta( + trace, + time, + ephTime, + satPos, + ssrMaps, + dPos, + iodPos, + iodEph, + ephValidStart, + ephValidStop + ); + bool clkDeltaPass = ssrClkDelta( + trace, + time, + ephTime, + satPos, + ssrMaps, + dClk, + iodClk, + clkValidStart, + clkValidStop + ); + + if (posDeltaPass == false || clkDeltaPass == false) + { + satPos.failureSSRFail = true; + + BOOST_LOG_TRIVIAL(warning) << "SSR Corrections not found for " << satPos.Sat.id(); + trace << "\n" + << "Warning: SSR Corrections not found for " << satPos.Sat.id(); + + return false; + } + + if (ephValidStart >= clkValidStop || clkValidStart >= ephValidStop) + { + BOOST_LOG_TRIVIAL(warning) + << "Timing inconsistent for " << satPos.Sat.id() << " : " + << ephValidStart.to_string(0) << " - " << ephValidStop.to_string(0) << " " + << clkValidStart.to_string(0) << " - " << clkValidStop.to_string(0); + trace << "\n" + << "Warning: Timing inconsistent for " << satPos.Sat.id() << " : " + << ephValidStart.to_string(0) << " - " << ephValidStop.to_string(0) << " " + << clkValidStart.to_string(0) << " - " << clkValidStop.to_string(0); + + if (ephValidStart >= clkValidStop) + ephTime = clkValidStop - 0.5; + if (clkValidStart >= ephValidStop) + ephTime = ephValidStop - 0.5; + continue; + } + + if (iodClk != iodPos) + { + satPos.failureIodeConsistency = true; + + BOOST_LOG_TRIVIAL(warning) + << "IOD inconsistent for " << satPos.Sat.id() << iodClk << " " << iodPos; + trace << "\n" + << "Warning: IOD inconsistent for " << satPos.Sat.id() << iodClk << " " << iodPos; + + return false; + } + + iodePos = iodEph; + iodeClk = iodEph; + + bool pass = true; + pass &= satPosBroadcast( + trace, + time, + teph, + Sat, + rSat0, + satVel, + posVar, + ephPosValid, + iodePos, + nav + ); + pass &= satClkBroadcast( + trace, + time, + teph, + Sat, + satClk0, + satClkVel, + satClkVar, + ephClkValid, + iodeClk, + nav + ); + + if (pass == false) + { + if (once) + { + once = false; + + if (clkValidStart < ephValidStart) + ephTime = clkValidStart - 0.5; + else + ephTime = ephValidStart - 0.5; + + BOOST_LOG_TRIVIAL(warning) + << "IODE BRDC not found for " << satPos.Sat.id() << " - adjusting ephTime"; + trace << "\n" + << "Warning: IODE BRDC not found for " << satPos.Sat.id() + << " - adjusting ephTime"; + + continue; + } + satPos.failureBroadcastEph = true; + + BOOST_LOG_TRIVIAL(warning) << "IODE BRDC not found for " << satPos.Sat.id(); + trace << "\n" + << "Warning: IODE BRDC not found for " << satPos.Sat.id(); + + return false; + } + + break; + } + + tracepdeex( + 4, + trace, + "\nBRDCEPH %s %s %13.3f %13.3f %13.3f %11.3f %2d", + time.to_string().c_str(), + Sat.id().c_str(), + rSat0[0], + rSat0[1], + rSat0[2], + 1e9 * satClk0, + iodePos + ); + + Matrix3d rac2ecefMat = rac2ecef(rSat0, satVel); + + Vector3d dPosECEF = rac2ecefMat * dPos; + + rSat = rSat0 - dPosECEF; + + /* t_corr = t_sv - (dtSat(brdc) + dClk(ssr) / CLIGHT) (ref [10] eq.3.12-7) */ + satClk = satClk0 + dClk / CLIGHT; + + /* variance by ssr ura */ + double ura = -1; + + auto uraIt = ssrMaps.ssrUra_map.lower_bound(time); + if (uraIt != ssrMaps.ssrUra_map.end()) + { + auto& [t_u, ssrUra] = *uraIt; + + ura = ssrUra.ura; + } + + double clkVar = var_urassr(ura); + satClkVar = clkVar / SQR(CLIGHT); + posVar = 0; + + tracepdeex( + 3, + trace, + "\nSSR_EPH %s %s %13.3f %13.3f %13.3f %11.3f ", + time.to_string().c_str(), + Sat.id().c_str(), + rSat[0], + rSat[1], + rSat[2], + 1e9 * satClk + ); + + tracepdeex( + 5, + trace, + "\n%s: %s sat=%s deph=%6.3f %6.3f %6.3f dclk=%6.3f var=%6.3f iode=%d clktimes:%s %s " + "ephtimes%s %s\n", + __FUNCTION__, + time.to_string().c_str(), + Sat.id().c_str(), + dPos[0], + dPos[1], + dPos[2], + dClk, + clkVar, + iodEph, + clkValidStart.to_string().c_str(), + clkValidStop.to_string().c_str(), + ephValidStart.to_string().c_str(), + ephValidStop.to_string().c_str() + ); + + if (round(time.bigTime) == time.bigTime) + { + traceJson( + 1, + nullStream, + time, + {{"data", "satPosSSR"}, {"Sat", satPos.Sat.id()}}, + {{"rSat0[0]", rSat0[0]}, + {"rSat0[1]", rSat0[1]}, + {"rSat0[2]", rSat0[2]}, + {"rSat[0]", rSat[0]}, + {"rSat[1]", rSat[1]}, + {"rSat[2]", rSat[2]}, + {"dPos[0]", dPos[0]}, + {"dPos[1]", dPos[1]}, + {"dPos[2]", dPos[2]}, + {"satClk0", satClk0}, + {"satClk", satClk}, + {"dClk", dClk}, + {"iode", iodEph}, + {"iodClk", iodClk}, + {"iodPos", iodPos}, + {"clkValidStart", (long int)clkValidStart.bigTime}, + {"ephValidStart", (long int)ephValidStart.bigTime}} + ); + } + + return true; } diff --git a/src/cpp/common/ephemeris.cpp b/src/cpp/common/ephemeris.cpp index 472c65c5d..dbfab6c74 100644 --- a/src/cpp/common/ephemeris.cpp +++ b/src/cpp/common/ephemeris.cpp @@ -1,399 +1,510 @@ - // #pragma GCC optimize ("O0") -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "ephPrecise.hpp" -#include "mongoRead.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "ephemeris.hpp" -#include "testUtils.hpp" -#include "algebra.hpp" -#include "orbits.hpp" -#include "satSys.hpp" -#include "common.hpp" -#include "trace.hpp" -#include "enums.h" -#include "ssr.hpp" +#include "common/ephemeris.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/ephPrecise.hpp" +#include "common/mongoRead.hpp" +#include "common/navigation.hpp" +#include "common/orbits.hpp" +#include "common/satSys.hpp" +#include "common/ssr.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" /** URA SSR by variance -*/ -double ephVarToUra( - double ephVar) + */ +double ephVarToUra(double ephVar) { - // to be implemented + // to be implemented - return ephVar; + return ephVar; } -double relativity1( - Vector3d& rSat, - Vector3d& satVel) +double relativity1(Vector3d& rSat, Vector3d& satVel) { - return 2 * rSat.dot(satVel) / CLIGHT / CLIGHT; + return 2 * rSat.dot(satVel) / CLIGHT / CLIGHT; } -template -void cullEphMap( - GTime time, - TYPE& map) +template +void cullEphMap(GTime time, TYPE& map) { - for (auto& [satid, satEphMap] : map) - { - SatSys Sat; - Sat.fromHash(satid); - - double tmax; - switch (Sat.sys) - { - case E_Sys::QZS: tmax = MAXDTOE_QZS + 1; break; - case E_Sys::GAL: tmax = MAXDTOE_GAL + 1; break; - case E_Sys::BDS: tmax = MAXDTOE_CMP + 1; break; - default: tmax = MAXDTOE + 1; break; - } - - for (auto& [navtyp, navMap] : satEphMap) - for (auto it = navMap.begin(); it != navMap.end(); ) - { - auto& [ephtime, eph] = *it; - - if (ephtime < time - tmax) - { - it = navMap.erase(it); - } - else - { - ++it; - } - } - } + for (auto& [satid, satEphMap] : map) + { + SatSys Sat; + Sat.fromHash(satid); + + double tmax; + switch (Sat.sys) + { + case E_Sys::QZS: + tmax = MAXDTOE_QZS + 1; + break; + case E_Sys::GAL: + tmax = MAXDTOE_GAL + 1; + break; + case E_Sys::BDS: + tmax = MAXDTOE_CMP + 1; + break; + default: + tmax = MAXDTOE + 1; + break; + } + + for (auto& [navtyp, navMap] : satEphMap) + { + if (navMap.empty()) + { + continue; + } + + for (auto it = + std::next(navMap.begin()); // Always reserve the latest entry when culling + it != navMap.end();) + { + auto& [ephtime, eph] = *it; + + if (ephtime < time - tmax) + { + it = navMap.erase(it); + } + else + { + ++it; + } + } + } + } } -void cullOldEphs( - GTime time) +void cullOldEphs(GTime time) { - cullEphMap(time, nav.ephMap); - cullEphMap(time, nav.gephMap); - cullEphMap(time, nav.sephMap); - cullEphMap(time, nav.cephMap); + cullEphMap(time, nav.ephMap); + cullEphMap(time, nav.gephMap); + cullEphMap(time, nav.sephMap); + cullEphMap(time, nav.cephMap); } bool satclk( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - vector ephTypes, - Navigation& nav, - const KFState* kfState_ptr, - const KFState* remote_ptr) + Trace& trace, + GTime time, + GTime teph, + SatPos& satPos, + vector ephTypes, + Navigation& nav, + const KFState* kfState_ptr, + const KFState* remote_ptr +) { - satPos.ephClkValid = false; - - bool returnValue = false; - - for (auto& ephType : ephTypes) - { - tracepdeex(4, trace, "\n%-10s: time=%s sat=%s ephType=%d", __FUNCTION__, time.to_string().c_str(), satPos.Sat.id().c_str(), ephType); - - switch (ephType) - { - case E_Source::SSR: //fallthrough - case E_Source::BROADCAST: returnValue = satClkBroadcast (trace, time, teph, satPos, nav ); break; - case E_Source::PRECISE: returnValue = satClkPrecise (trace, time, satPos, nav ); break; - case E_Source::KALMAN: returnValue = satClkKalman (trace, time, satPos, kfState_ptr ); break; - case E_Source::REMOTE: returnValue = satClkKalman (trace, time, satPos, remote_ptr ); break; - default: continue; - } - - if (returnValue == false) - { - continue; - } - - satPos.clkSource = ephType; - satPos.ephClkValid = true; - - if (acsConfig.check_broadcast_differences - &&ephType != +E_Source::BROADCAST) - { - SatPos copy = satPos; - - bool pass = satClkBroadcast(trace, time, teph, copy, nav); - double delta = (copy.satClk - satPos.satClk) * CLIGHT; - if ( pass - &&fabs(delta) > 30) - { - BOOST_LOG_TRIVIAL(warning) << "Warning, clock for " << satPos.Sat.id() << " is " << delta << " from broadcast"; - } - } - - break; - } - - return returnValue; + satPos.ephClkValid = false; + + bool returnValue = false; + + for (auto& ephType : ephTypes) + { + tracepdeex( + 4, + trace, + "\n%-10s: time=%s sat=%s ephType=%d", + __FUNCTION__, + time.to_string().c_str(), + satPos.Sat.id().c_str(), + ephType + ); + + switch (ephType) + { + case E_Source::SSR: // fallthrough + case E_Source::SBAS: // fallthrough + case E_Source::BROADCAST: + returnValue = satClkBroadcast(trace, time, teph, satPos, nav); + break; + case E_Source::PRECISE: + returnValue = satClkPrecise(trace, time, satPos, nav); + break; + case E_Source::KALMAN: + returnValue = satClkKalman(trace, time, satPos, kfState_ptr); + break; + case E_Source::REMOTE: + returnValue = satClkKalman(trace, time, satPos, remote_ptr); + break; + default: + continue; + } + + if (returnValue == false) + { + continue; + } + + satPos.clkSource = ephType; + satPos.ephClkValid = true; + + if (acsConfig.check_broadcast_differences && ephType != E_Source::BROADCAST) + { + SatPos copy = satPos; + + bool pass = satClkBroadcast(trace, time, teph, copy, nav); + double delta = (copy.satClk - satPos.satClk) * CLIGHT; + if (pass && fabs(delta) > 30) + { + BOOST_LOG_TRIVIAL(warning) + << "Clock for " << satPos.Sat.id() << " is " << delta << " from broadcast"; + } + } + + break; + } + + return returnValue; } /** compute satellite position and clock -* satellite clock does not include code bias correction (tgd or bgd) -*/ + * satellite clock does not include code bias correction (tgd or bgd) + */ bool satpos( - Trace& trace, ///< Trace to output to - GTime time, ///< time (gpst) - GTime teph, ///< time to select ephemeris (gpst) - SatPos& satPos, ///< Data required for determining and storing satellite positions/clocks - vector ephTypes, ///< Source of ephemeris - E_OffsetType offsetType, ///< Type of antenna offset to apply - Navigation& nav, ///< navigation data - const KFState* kfState_ptr, ///< Optional pointer to a kalman filter to take values from - const KFState* remote_ptr) ///< Optional pointer to a kalman filter to take values from + Trace& trace, ///< Trace to output to + GTime time, ///< time (gpst) + GTime teph, ///< time to select ephemeris (gpst) + SatPos& satPos, ///< Data required for determining and storing satellite positions/clocks + vector ephTypes, ///< Source of ephemeris + E_OffsetType offsetType, ///< Type of antenna offset to apply + Navigation& nav, ///< navigation data + const KFState* kfState_ptr, ///< Optional pointer to a kalman filter to take values from + const KFState* remote_ptr ///< Optional pointer to a kalman filter to take values from +) { - double antennaScalar = 0; - bool returnValue = false; - - for (auto& ephType : ephTypes) - { - tracepdeex(4, trace, "\n%-10s: time=%s sat=%s ephType=%s offsetType=%d", __FUNCTION__, time.to_string().c_str(), satPos.Sat.id().c_str(), ephType._to_string(), offsetType); - - if (returnValue == false) - switch (ephType) - { - case E_Source::BROADCAST: returnValue = satPosBroadcast (trace, time, teph, satPos, nav ); break; - case E_Source::SSR: returnValue = satPosSSR (trace, time, teph, satPos, nav ); break; - case E_Source::PRECISE: returnValue = satPosPrecise (trace, time, satPos, nav ); break; - case E_Source::KALMAN: returnValue = satPosKalman (trace, time, satPos, kfState_ptr ); break; - case E_Source::REMOTE: returnValue = satPosKalman (trace, time, satPos, remote_ptr ); break; - case E_Source::CONFIG: continue; - default: continue; - } - - if (returnValue == false) - { - continue; - } - - switch (ephType) - { - case E_Source::BROADCAST: satPos.rSatCom = satPos.rSatApc; break; - case E_Source::SSR: satPos.rSatApc = satPos.rSatCom; break; - case E_Source::PRECISE: satPos.rSatApc = satPos.rSatCom; break; - case E_Source::KALMAN: satPos.rSatApc = satPos.rSatCom; break; - case E_Source::REMOTE: satPos.rSatApc = satPos.rSatCom; break; - } - - if ( acsConfig.check_broadcast_differences - &&ephType != +E_Source::BROADCAST) - { - SatPos copy = satPos; - bool pass = satPosBroadcast(trace, time, teph, copy, nav); - double delta = (satPos.rSatApc - copy.rSatApc).norm(); - if ( pass - &&delta > 10) - { - BOOST_LOG_TRIVIAL(warning) << "Warning, orbit for " << satPos.Sat.id() << " is " << delta << " from broadcast"; - } - } - - tracepdeex(4, trace, " - FOUND"); - - satPos.posTime = time; - satPos.posSource = ephType; - satPos.ephPosValid = true; - - if (ephType == +E_Source::SSR) - { - satPos.clkSource = ephType; - satPos.ephClkValid = true; - } - - break; - } - - if (satPos.posSource == +E_Source::SSR && acsConfig.ssr_input_antenna_offset == +E_OffsetType::UNSPECIFIED) - BOOST_LOG_TRIVIAL(error) << "Error: ssr_antenna_offset has not been set in config.\n"; - - if (satPos.posSource == +E_Source::SSR && offsetType == +E_OffsetType::APC && acsConfig.ssr_input_antenna_offset == +E_OffsetType::COM) antennaScalar = +1; - if (satPos.posSource == +E_Source::SSR && offsetType == +E_OffsetType::COM && acsConfig.ssr_input_antenna_offset == +E_OffsetType::APC) antennaScalar = -1; - if (satPos.posSource == +E_Source::PRECISE && offsetType == +E_OffsetType::APC) antennaScalar = +1; - if (satPos.posSource == +E_Source::KALMAN && offsetType == +E_OffsetType::APC) antennaScalar = +1; - if (satPos.posSource == +E_Source::REMOTE && offsetType == +E_OffsetType::APC) antennaScalar = +1; - if (satPos.posSource == +E_Source::BROADCAST && offsetType == +E_OffsetType::COM) antennaScalar = -1; - - // satellite antenna offset correction - if (antennaScalar) - { - if (satPos.satNav_ptr == nullptr) - { - BOOST_LOG_TRIVIAL(debug) << "Sat nav pointer undefined"; - return returnValue; - } - - auto& attStatus = satPos.satNav_ptr->attStatus; - - if ( attStatus.eXBody.isZero() - ||attStatus.eYBody.isZero() - ||attStatus.eZBody.isZero()) - { - BOOST_LOG_TRIVIAL(debug) << "Satellite attitude of " << satPos.Sat.id() << " not available, antenna offset not corrected."; - return returnValue; - } - - E_FType j; - E_FType k; - E_FType l; - E_Sys sys = satPos.Sat.sys; - if (!satFreqs(sys,j,k,l)) - return false; - - if ( satPos.satNav_ptr->lamMap.empty() - ||satPos.satNav_ptr->lamMap[j] == 0 - ||satPos.satNav_ptr->lamMap[k] == 0) - { - // satAntOff() requries lamMap - updateLamMap(time, satPos); - } - - Vector3d dAnt = Vector3d::Zero(); - if (acsConfig.common_sat_pco) - { - double varDummy = 0; - - Vector3d bodyPCO = antPco(satPos.Sat.id(), satPos.Sat.sys, j, time, varDummy, E_Radio::TRANSMITTER); - - dAnt = body2ecef(attStatus, bodyPCO); - } - else - { - dAnt = satAntOff(trace, time, attStatus, satPos.Sat, nav.satNavMap[satPos.Sat].lamMap); - } - - if (antennaScalar > 0) satPos.rSatApc = satPos.rSatCom + dAnt; - if (antennaScalar < 0) satPos.rSatCom = satPos.rSatApc - dAnt; - } - - return returnValue; + double antennaScalar = 0; + bool returnValue = false; + + for (auto& ephType : ephTypes) + { + tracepdeex( + 4, + trace, + "\n%-10s: time=%s sat=%s ephType=%s offsetType=%d", + __FUNCTION__, + time.to_string().c_str(), + satPos.Sat.id().c_str(), + enum_to_string(ephType), + offsetType + ); + + if (returnValue == false) + switch (ephType) + { + case E_Source::BROADCAST: + returnValue = satPosBroadcast(trace, time, teph, satPos, nav); + break; + case E_Source::SBAS: + returnValue = satPosSBAS(trace, time, teph, satPos, nav); + break; + case E_Source::SSR: + returnValue = satPosSSR(trace, time, teph, satPos, nav); + break; + case E_Source::PRECISE: + returnValue = satPosPrecise(trace, time, satPos, nav); + break; + case E_Source::KALMAN: + returnValue = satPosKalman(trace, time, satPos, kfState_ptr); + break; + case E_Source::REMOTE: + returnValue = satPosKalman(trace, time, satPos, remote_ptr); + break; + case E_Source::CONFIG: + continue; + default: + continue; + } + + if (returnValue == false) + { + continue; + } + + switch (ephType) + { + case E_Source::BROADCAST: + satPos.rSatCom = satPos.rSatApc; + break; + case E_Source::SBAS: + satPos.rSatCom = satPos.rSatApc; + break; + case E_Source::SSR: + satPos.rSatApc = satPos.rSatCom; + break; + case E_Source::PRECISE: + satPos.rSatApc = satPos.rSatCom; + break; + case E_Source::KALMAN: + satPos.rSatApc = satPos.rSatCom; + break; + case E_Source::REMOTE: + satPos.rSatApc = satPos.rSatCom; + break; + } + + if (acsConfig.check_broadcast_differences && ephType != E_Source::BROADCAST) + { + SatPos copy = satPos; + bool pass = satPosBroadcast(trace, time, teph, copy, nav); + double delta = (satPos.rSatApc - copy.rSatApc).norm(); + if (pass && delta > 10) + { + BOOST_LOG_TRIVIAL(warning) + << "Orbit for " << satPos.Sat.id() << " is " << delta << " from broadcast"; + } + } + + tracepdeex(4, trace, " - FOUND"); + + satPos.posTime = time; + satPos.posSource = ephType; + satPos.ephPosValid = true; + + if (ephType == E_Source::SSR) + { + satPos.clkSource = ephType; + satPos.ephClkValid = true; + } + + break; + } + + if (satPos.posSource == E_Source::SSR && + acsConfig.ssr_input_antenna_offset == E_OffsetType::UNSPECIFIED) + BOOST_LOG_TRIVIAL(error) << "ssr_antenna_offset has not been set in config.\n"; + + if (satPos.posSource == E_Source::SSR && offsetType == E_OffsetType::APC && + acsConfig.ssr_input_antenna_offset == E_OffsetType::COM) + antennaScalar = +1; + if (satPos.posSource == E_Source::SSR && offsetType == E_OffsetType::COM && + acsConfig.ssr_input_antenna_offset == E_OffsetType::APC) + antennaScalar = -1; + if (satPos.posSource == E_Source::PRECISE && offsetType == E_OffsetType::APC) + antennaScalar = +1; + if (satPos.posSource == E_Source::KALMAN && offsetType == E_OffsetType::APC) + antennaScalar = +1; + if (satPos.posSource == E_Source::REMOTE && offsetType == E_OffsetType::APC) + antennaScalar = +1; + if (satPos.posSource == E_Source::BROADCAST && offsetType == E_OffsetType::COM) + antennaScalar = -1; + if (satPos.posSource == E_Source::SBAS && offsetType == E_OffsetType::COM) + antennaScalar = -1; + + // satellite antenna offset correction + if (antennaScalar) + { + if (satPos.satNav_ptr == nullptr) + { + BOOST_LOG_TRIVIAL(debug) << "Sat nav pointer undefined"; + return returnValue; + } + + auto& attStatus = satPos.satNav_ptr->attStatus; + + if (attStatus.eXBody.isZero() || attStatus.eYBody.isZero() || attStatus.eZBody.isZero()) + { + BOOST_LOG_TRIVIAL(debug) << "Satellite attitude of " << satPos.Sat.id() + << " not available, antenna offset not corrected."; + return returnValue; + } + + E_FType j; + E_FType k; + E_FType l; + E_Sys sys = satPos.Sat.sys; + if (!satFreqs(sys, j, k, l)) + return false; + + if (satPos.satNav_ptr->lamMap.empty() || satPos.satNav_ptr->lamMap[j] == 0 || + satPos.satNav_ptr->lamMap[k] == 0) + { + // satAntOff() requries lamMap + updateLamMap(time, satPos); + } + + Vector3d dAnt = Vector3d::Zero(); + if (acsConfig.common_sat_pco) + { + double varDummy = 0; + + Vector3d bodyPCO = + antPco(satPos.Sat.id(), satPos.Sat.sys, j, time, varDummy, E_Radio::TRANSMITTER); + + dAnt = body2ecef(attStatus, bodyPCO); + } + else + { + dAnt = satAntOff(trace, time, attStatus, satPos.Sat, nav.satNavMap[satPos.Sat].lamMap); + } + + if (antennaScalar > 0) + satPos.rSatApc = satPos.rSatCom + dAnt; + if (antennaScalar < 0) + satPos.rSatCom = satPos.rSatApc - dAnt; + } + + return returnValue; } -void adjustRelativity( - SatPos& satPos, - E_Relativity applyRelativity) +void adjustRelativity(SatPos& satPos, E_Relativity applyRelativity) { - E_Relativity clockHasRelativity; + E_Relativity clockHasRelativity; - if (satPos.clkSource == +E_Source::BROADCAST && satPos.Sat.sys == +E_Sys::GLO) clockHasRelativity = E_Relativity::ON; // Ref: RTCM STANDARD 10403.3 - else clockHasRelativity = E_Relativity::OFF; + if (satPos.clkSource == E_Source::BROADCAST && satPos.Sat.sys == E_Sys::GLO) + clockHasRelativity = E_Relativity::ON; // Ref: RTCM STANDARD 10403.3 + else + clockHasRelativity = E_Relativity::OFF; - if (clockHasRelativity == applyRelativity) - { - return; - } + if (clockHasRelativity == applyRelativity) + { + return; + } - double scalar = 0; + double scalar = 0; - if (clockHasRelativity == +E_Relativity::ON && applyRelativity == +E_Relativity::OFF) scalar = -1; - else if (clockHasRelativity == +E_Relativity::OFF && applyRelativity == +E_Relativity::ON) scalar = +1; + if (clockHasRelativity == E_Relativity::ON && applyRelativity == E_Relativity::OFF) + scalar = -1; + else if (clockHasRelativity == E_Relativity::OFF && applyRelativity == E_Relativity::ON) + scalar = +1; - satPos.satClk -= scalar * relativity1(satPos.rSatCom, satPos.satVel); + satPos.satClk -= scalar * relativity1(satPos.rSatCom, satPos.satVel); } /** satellite positions and clocks. -* satellite position and clock are values at signal transmission time. -* satellite clock does not include code bias correction (tgd or bgd). -* any pseudorange and broadcast ephemeris are always needed to get signal transmission time. -*/ + * satellite position and clock are values at signal transmission time. + * satellite clock does not include code bias correction (tgd or bgd). + * any pseudorange and broadcast ephemeris are always needed to get signal transmission time. + */ bool satPosClk( - Trace& trace, ///< Trace to output to - GTime teph, ///< time to select ephemeris (gpst) - GObs& obs, ///< observations to complete with satellite positions - Navigation& nav, ///< Navigation data - vector posSources, ///< Source of ephemeris data - vector clkSources, ///< Source of ephemeris data - const KFState* kfState_ptr, ///< Optional pointer to a kalman filter to take values from - const KFState* remote_ptr, ///< Optional pointer to a kalman filter to take values from - E_OffsetType offsetType, ///< Point of satellite to output position of - E_Relativity applyRelativity) ///< Option to apply relativistic correction to clock + Trace& trace, ///< Trace to output to + GTime teph, ///< time to select ephemeris (gpst) + GObs& obs, ///< observations to complete with satellite positions + Navigation& nav, ///< Navigation data + vector posSources, ///< Source of ephemeris data + vector clkSources, ///< Source of ephemeris data + const KFState* kfState_ptr, ///< Optional pointer to a kalman filter to take values from + const KFState* remote_ptr, ///< Optional pointer to a kalman filter to take values from + E_OffsetType offsetType, ///< Point of satellite to output position of + E_Relativity applyRelativity ///< Option to apply relativistic correction to clock +) { - if (obs.exclude) - { - obs.failureExclude = true; - - return false; - } - - tracepdeex(3, trace, "\n%-10s: teph=%s %s", __FUNCTION__, teph.to_string().c_str(), obs.Sat.id()); - - double pr = 0; - - for (auto& [a, sig] : obs.sigs) - { - if (sig.P == 0) - continue; - - pr = sig.P; - - break; - } - - if (pr == 0) - { - obs.failureNoPseudorange = true; - - tracepdeex(2, trace, "\nno pseudorange %s sat=%s", obs.time.to_string().c_str(), obs.Sat.id().c_str()); - return false; - } - - obs.tof = pr / CLIGHT; - - // transmission time by satellite clock - GTime time = obs.time; - - time -= obs.tof; - - bool pass; - - pass = satclk(trace, time, teph, obs, clkSources, nav, kfState_ptr, remote_ptr); - - if (pass == false) - { - obs.failureNoSatClock = true; - - tracepdeex(2, trace, "\nno satellite clock %s sat=%s", time.to_string().c_str(), obs.Sat.id().c_str()); - return false; - } - - tracepdeex(5, trace, "\neph time %s %s pr=%.5f, satClk= %.5f", obs.Sat.id().c_str(), time.to_string().c_str(), pr / CLIGHT, obs.satClk); - - time -= obs.satClk; // Eugene: what if using ssr? - - // satellite position and clock at transmission time - pass = satpos(trace, time, teph, obs, posSources, offsetType, nav, kfState_ptr, remote_ptr); - - if (pass == false) - { - obs.failureNoSatPos = true; - - tracepdeex(3, trace, "\n%s failed (no ephemeris?) %s sat=%s", __FUNCTION__, time.to_string().c_str(), obs.Sat.id().c_str()); - - return false; - } - - adjustRelativity(obs, applyRelativity); - - tracepdeex(3, trace, "\n%s sat=%s rs=%13.3f %13.3f %13.3f dtSat=%12.3f varPos=%7.3f varClk=%7.3f ephPosValid=%1X %s ephClkValid=%1X %s", - obs.time.to_string().c_str(), - obs.Sat.id().c_str(), - obs.rSatCom[0], - obs.rSatCom[1], - obs.rSatCom[2], - obs.satClk * 1E9, - obs.posVar, - obs.satClkVar, - obs.ephPosValid, - obs.posSource._to_string(), - obs.ephClkValid, - obs.clkSource._to_string()); - - return true; + tracepdeex( + 3, + trace, + "\n%-10s: teph=%s %s", + __FUNCTION__, + teph.to_string().c_str(), + obs.Sat.id() + ); + + double pr = 0; + + for (auto& [a, sig] : obs.sigs) + { + if (sig.P == 0) + continue; + + pr = sig.P; + + break; + } + + if (pr == 0) + { + obs.failureNoPseudorange = true; + + tracepdeex( + 2, + trace, + "\nno pseudorange %s sat=%s", + obs.time.to_string().c_str(), + obs.Sat.id().c_str() + ); + return false; + } + + obs.tof = pr / CLIGHT; + + // transmission time by satellite clock + GTime time = obs.time; + + time -= obs.tof; + + bool pass; + + pass = satclk(trace, time, teph, obs, clkSources, nav, kfState_ptr, remote_ptr); + + if (pass == false) + { + obs.failureNoSatClock = true; + + tracepdeex( + 2, + trace, + "\nno satellite clock %s sat=%s", + time.to_string().c_str(), + obs.Sat.id().c_str() + ); + return false; + } + + tracepdeex( + 5, + trace, + "\neph time %s %s pr=%.5f, satClk= %.5f", + obs.Sat.id().c_str(), + time.to_string().c_str(), + pr / CLIGHT, + obs.satClk + ); + + time -= obs.satClk; // Eugene: what if using ssr? + // satellite position and clock at transmission time + pass = satpos(trace, time, teph, obs, posSources, offsetType, nav, kfState_ptr, remote_ptr); + + if (pass == false) + { + obs.failureNoSatPos = true; + + tracepdeex( + 3, + trace, + "\n%s failed (no ephemeris?) %s sat=%s", + __FUNCTION__, + time.to_string().c_str(), + obs.Sat.id().c_str() + ); + + return false; + } + + adjustRelativity(obs, applyRelativity); + + tracepdeex( + 3, + trace, + "\n%s sat=%s rs=%13.3f %13.3f %13.3f dtSat=%12.3f varPos=%7.3f varClk=%7.3f " + "ephPosValid=%1X %s ephClkValid=%1X " + "%s", + obs.time.to_string().c_str(), + obs.Sat.id().c_str(), + obs.rSatCom[0], + obs.rSatCom[1], + obs.rSatCom[2], + obs.satClk * 1E9, + obs.posVar, + obs.satClkVar * SQR(CLIGHT), + obs.ephPosValid, + enum_to_string(obs.posSource).c_str(), + obs.ephClkValid, + enum_to_string(obs.clkSource).c_str() + ); + + return true; } diff --git a/src/cpp/common/ephemeris.hpp b/src/cpp/common/ephemeris.hpp index 9e4fc58c1..d19a95ddf 100644 --- a/src/cpp/common/ephemeris.hpp +++ b/src/cpp/common/ephemeris.hpp @@ -1,530 +1,495 @@ - #pragma once - -#include "eigenIncluder.hpp" -#include "observations.hpp" -#include "constants.hpp" -#include "satSys.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" - - #include +#include #include #include -#include +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/observations.hpp" +#include "common/satSys.hpp" +#include "common/trace.hpp" +using std::map; using std::string; using std::vector; -using std::map; - -//forward declarations +// forward declarations struct Navigation; struct GObs; struct Peph; -#define ANY_IODE -1 -#define NO_SP3_CLK 999999.999999 -#define INVALID_CLOCK_VALUE NO_SP3_CLK +constexpr int ANY_IODE = -1; +constexpr double NO_SP3_CLK = 999999.999999; +constexpr double INVALID_CLOCK_VALUE = NO_SP3_CLK; struct KeplerEph { - double A = 0; ///< semi major axis - double e = 0; ///< eccentricity - double i0 = 0; ///< inclination - double OMG0 = 0; ///< right ascension of ascending node - double omg = 0; ///< argument of perigee - double M0 = 0; ///< mean anomoly - double deln = 0; ///< correction mean motion - double OMGd = 0; ///< rate of OMG - double idot = 0; ///< rate of inclination - double crc = 0; ///< correction radial cosine - double crs = 0; ///< correction radial sine - double cuc = 0; ///< correction lattitude cosine - double cus = 0; ///< correction lattitude sine - double cic = 0; ///< correction inclination cosine - double cis = 0; ///< correction inclination sine - double dn0d = 0; ///< rate of correction mean motion - double Adot = 0; ///< rate of A + double A = 0; ///< semi major axis + double e = 0; ///< eccentricity + double i0 = 0; ///< inclination + double OMG0 = 0; ///< right ascension of ascending node + double omg = 0; ///< argument of perigee + double M0 = 0; ///< mean anomoly + double deln = 0; ///< correction mean motion + double OMGd = 0; ///< rate of OMG + double idot = 0; ///< rate of inclination + double crc = 0; ///< correction radial cosine + double crs = 0; ///< correction radial sine + double cuc = 0; ///< correction lattitude cosine + double cus = 0; ///< correction lattitude sine + double cic = 0; ///< correction inclination cosine + double cis = 0; ///< correction inclination sine + double dn0d = 0; ///< rate of correction mean motion + double Adot = 0; ///< rate of A }; struct BrdcEph { - }; /** GPS/QZS/GAL/BDS broadcast ephemeris */ struct Eph : BrdcEph, KeplerEph { - E_NavMsgType type = E_NavMsgType::NONE; ///< message type - SatSys Sat; ///< satellite number - int iode = -1; ///< GPS/QZS: IODE, GAL: IODnav - int iodc = 0; ///< IODC - int aode; ///< BDS AODE - int aodc; ///< BDS AODC - 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 - 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, isc[2]=ISC_L5I5, isc[3]=ISC_L5Q5, isc[4]=ISC_L1Cd, isc[5]=ISC_L1Cp - ///< BDS CNV1: isc[0]=ISC_B1Cd - ///< BDS CNV2: isc[1]=ISC_B2ad - double sis[5] = {}; ///< signal in space accuracy index - ///< BDS CNVX sis[0]=SISAI_oe, sis[1]=SISAI_ocb, sis[2]=SISAI_oc1, sis[3]=SISAI_oc2, sis[4]=SISMAI - - - // original messages from stream/rinex for debugging - double tocs; ///< TOC (s) within week - int weekRollOver; ///< week number (rolled over) - double sqrtA; ///< sqrt A - int e5a_hs = 0; ///< GAL E5a signal health status - int e5a_dvs = 0; ///< GAL E5a data validity status - int e5b_hs = 0; ///< GAL E5b signal health status - int e5b_dvs = 0; ///< GAL E5b data validity status - int e1_hs = 0; ///< GAL E1 signal health status - int e1_dvs = 0; ///< GAL E1 data validity status - double ttms = 0; ///< transmission time (s) within week - int fitFlag = 0; ///< fit flag - - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - ar & iode; - ar & iodc; - ar & aode; - ar & aodc; - ar & sva; - ar & svh; - ar & week; - ar & code; - ar & flag; - ar & toe; - ar & toc; - ar & ttm; - ar & A; - ar & e; - ar & i0; - ar & OMG0; - ar & omg; - ar & M0; - ar & deln; - ar & OMGd; - ar & idot; - ar & crc; - ar & crs; - ar & cuc; - ar & cus; - ar & cic; - ar & cis; - ar & toes; - ar & fit; - ar & f0; - ar & f1; - ar & f2; - ar & tgd[0]; - ar & tgd[1]; - ar & tgd[2]; - ar & tgd[3]; - ar & Sat; - } + E_NavMsgType type = E_NavMsgType::NONE; ///< message type + SatSys Sat; ///< satellite number + int iode = -1; ///< GPS/QZS: IODE, GAL: IODnav + int iodc = 0; ///< IODC + int aode; ///< BDS AODE + int aodc; ///< BDS AODC + 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 + 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, + ///< isc[2]=ISC_L5I5, isc[3]=ISC_L5Q5, isc[4]=ISC_L1Cd, isc[5]=ISC_L1Cp BDS + ///< CNV1: isc[0]=ISC_B1Cd BDS CNV2: isc[1]=ISC_B2ad + double sis[5] = {}; ///< signal in space accuracy index + ///< BDS CNVX sis[0]=SISAI_oe, sis[1]=SISAI_ocb, sis[2]=SISAI_oc1, + ///< sis[3]=SISAI_oc2, sis[4]=SISMAI + + // original messages from stream/rinex for debugging + double tocs; ///< TOC (s) within week + int weekRollOver; ///< week number (rolled over) + double sqrtA; ///< sqrt A + int e5a_hs = 0; ///< GAL E5a signal health status + int e5a_dvs = 0; ///< GAL E5a data validity status + int e5b_hs = 0; ///< GAL E5b signal health status + int e5b_dvs = 0; ///< GAL E5b data validity status + int e1_hs = 0; ///< GAL E1 signal health status + int e1_dvs = 0; ///< GAL E1 data validity status + double ttms = 0; ///< transmission time (s) within week + int fitFlag = 0; ///< fit flag + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + ar & iode; + ar & iodc; + ar & aode; + ar & aodc; + ar & sva; + ar & svh; + ar & week; + ar & code; + ar & flag; + ar & toe; + ar & toc; + ar & ttm; + ar & A; + ar & e; + ar & i0; + ar & OMG0; + ar & omg; + ar & M0; + ar & deln; + ar & OMGd; + ar & idot; + ar & crc; + ar & crs; + ar & cuc; + ar & cus; + ar & cic; + ar & cis; + ar & toes; + ar & fit; + ar & f0; + ar & f1; + ar & f2; + ar& tgd[0]; + ar& tgd[1]; + ar& tgd[2]; + ar& tgd[3]; + ar & Sat; + } }; /** GLONASS broadcast ephemeris */ struct Geph : BrdcEph { - E_NavMsgType type = E_NavMsgType::NONE; ///< message type - SatSys Sat; ///< satellite number - int iode = -1; ///< IODE (0-6 bit of tb field) - int frq; ///< satellite frequency number - E_Svh svh; ///< satellite health - int sva; ///< satellite accuracy - int age; ///< age of operation - GTime toe; ///< epoch of epherides (gpst) - GTime tof; ///< message frame time (gpst) - Vector3d pos; ///< satellite position (ecef) (m) - Vector3d vel; ///< satellite velocity (ecef) (m/s) - Vector3d acc; ///< satellite acceleration (ecef) (m/s^2) - double taun; ///< SV clock bias (s) - double gammaN; ///< SV relative freq bias - double dtaun; ///< delay between L1 and L2 (s) - - - // original messages from stream/rinex for debugging - double tofs; ///< TOF (s) within the current day - int tk_hour; ///< number of hours of TOF - int tk_min; ///< number of minutes of TOF - double tk_sec; ///< seconds of TOF - int tb; ///< number of 15 min of TOE - int glonassM; ///< type of GLO satellites - int NT; ///< calender number of day within 4-year interval - bool moreData; ///< availability of additional data - int N4; ///< 4-year interval number + E_NavMsgType type = E_NavMsgType::NONE; ///< message type + SatSys Sat; ///< satellite number + int iode = -1; ///< IODE (0-6 bit of tb field) + int frq; ///< satellite frequency number + E_Svh svh; ///< satellite health + int sva; ///< satellite accuracy + int age; ///< age of operation + GTime toe; ///< epoch of epherides (gpst) + GTime tof; ///< message frame time (gpst) + Vector3d pos; ///< satellite position (ecef) (m) + Vector3d vel; ///< satellite velocity (ecef) (m/s) + Vector3d acc; ///< satellite acceleration (ecef) (m/s^2) + double taun; ///< SV clock bias (s) + double gammaN; ///< SV relative freq bias + double dtaun; ///< delay between L1 and L2 (s) + + // original messages from stream/rinex for debugging + double tofs; ///< TOF (s) within the current day + int tk_hour; ///< number of hours of TOF + int tk_min; ///< number of minutes of TOF + double tk_sec; ///< seconds of TOF + int tb; ///< number of 15 min of TOE + int glonassM; ///< type of GLO satellites + int NT; ///< calender number of day within 4-year interval + bool moreData; ///< availability of additional data + int N4; ///< 4-year interval number }; /** precise clock */ struct Pclk { - double clk = INVALID_CLOCK_VALUE; ///< satellite clock (s) - double clkStd = 0; ///< satellite clock std (s) - int clkIndex; ///< clock index for multiple files + double clk = INVALID_CLOCK_VALUE; ///< satellite clock (s) + double clkStd = 0; ///< satellite clock std (s) + int clkIndex; ///< clock index for multiple files }; /** precise ephemeris */ struct Peph : Pclk { - SatSys Sat; ///< satellite number - GTime time; ///< time (GPST) - int index; ///< ephemeris index for multiple files - VectorEcef pos; ///< satellite position (m) - Vector3d posStd = Vector3d::Zero(); ///< satellite position std (m) - VectorEcef vel; ///< satellite velocity/clk-rate (m/s) - Vector3d velStd = Vector3d::Zero(); ///< satellite velocity/clk-rate std (m/s) + SatSys Sat; ///< satellite number + GTime time; ///< time (GPST) + int index; ///< ephemeris index for multiple files + VectorEcef pos; ///< satellite position (m) + Vector3d posStd = Vector3d::Zero(); ///< satellite position std (m) + VectorEcef vel; ///< satellite velocity/clk-rate (m/s) + Vector3d velStd = Vector3d::Zero(); ///< satellite velocity/clk-rate std (m/s) }; /** Satellite attitude */ struct Att { - string id; - GTime time; ///< time (GPST) - int index; ///< ephemeris index for multiple files - E_ObxFrame frame; - Quaterniond q = Eigen::Quaterniond::Identity(); ///< satellite attitude represented w/ a quaternion + string id; + GTime time; ///< time (GPST) + int index; ///< ephemeris index for multiple files + E_ObxFrame frame; + Quaterniond q = + Eigen::Quaterniond::Identity(); ///< satellite attitude represented w/ a quaternion }; /** SBAS ephemeris */ struct Seph : BrdcEph { - E_NavMsgType type = E_NavMsgType::NONE; ///< message type - SatSys Sat; ///< satellite number - GTime t0; ///< reference epoch time (GPST) - GTime tof; ///< time of message frame (GPST) - int sva; ///< SV accuracy (URA index) - E_Svh svh; ///< SV health - VectorEcef pos; ///< satellite position (m) (ecef) - VectorEcef vel; ///< satellite velocity (m/s) (ecef) - VectorEcef acc; ///< satellite acceleration (m/s^2) (ecef) - double af0 = 0; ///< satellite clock-offset/drift (s) - double af1 = 0; ///< satellite clock-drift (s/s) - int iode = -1; //unused, for templating only - GTime toe; //unused, for templating only - - double tofs; ///< TOF (s) within the week - }; + E_NavMsgType type = E_NavMsgType::NONE; ///< message type + SatSys Sat; ///< satellite number + GTime t0; ///< reference epoch time (GPST) + GTime tof; ///< time of message frame (GPST) + int sva; ///< SV accuracy (URA index) + double ura; ///< SV accuracy in meters + E_Svh svh; ///< SV health + VectorEcef pos; ///< satellite position (m) (ecef) + VectorEcef vel; ///< satellite velocity (m/s) (ecef) + VectorEcef acc; ///< satellite acceleration (m/s^2) (ecef) + double af0 = 0; ///< satellite clock-offset/drift (s) + double af1 = 0; ///< satellite clock-drift (s/s) + int iode = -1; // unused, for templating only + GTime toe; // unused, for templating only + + double tofs; ///< TOF (s) within the week +}; /** GPS/QZS CNAV/CNAV-2 or BDS CNAV-1/CNAV-2/CNAV-3 ephemeris -*/ + */ struct Ceph : KeplerEph { - E_NavMsgType type = E_NavMsgType::NONE; ///< message type - E_SatType orb = E_SatType::NONE; ///< BDS sat/orbit type - SatSys Sat; ///< satellite number - int iode = -1; ///< BDS CNAV1/CNV2 IODE - int iodc = -1; ///< BDS CNAV1/CNV2 IODC - E_Svh svh; ///< SV health - int wnop = 0; ///< GPS/QZS: GPS week number (of prediction?) with AR - int flag = 0; ///< BDS B1C/B2a+B1C/B2b integrity flags - GTime toc = {}; ///< time of clock - GTime toe = {}; ///< time of ephemeris, for GPS/QZS, TOE==TOC - 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 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, isc[2]=ISC_L5I5, isc[3]=ISC_L5Q5, isc[4]=ISC_L1Cd, isc[5]=ISC_L1Cp - ///< BDS CNV1: isc[0]=ISC_B1Cd - ///< BDS CNV2: isc[1]=ISC_B2ad - double sis[5] = {}; ///< signal in space accuracy index - ///< BDS sis[0]=SISAI_oe, sis[1]=SISAI_ocb, sis[2]=SISAI_oc1, sis[3]=SISAI_oc2, sis[4]=SISMAI - double tops = 0; ///< t_op (s) in week - double toes = 0; ///< TOE (s) in week - double f0 = 0; ///< SV clock parameter (af0) - double f1 = 0; ///< SV clock parameter (af1) - double f2 = 0; ///< SV clock parameter (af2) - double tgd[4] = {}; ///< group delay parameters - ///< GPS/QZS: tgd[0]=TGD - ///< BDS CNAV1/CNV2: tgd[0]=TGD_B1Cp, tgd[1]=TGD_B2ap - ///< BDS CNAV3: tgd[2]=TGD_B2bI - - double ttms = 0; ///< transmission time (s) within week + E_NavMsgType type = E_NavMsgType::NONE; ///< message type + E_SatType orb = E_SatType::NONE; ///< BDS sat/orbit type + SatSys Sat; ///< satellite number + int iode = -1; ///< BDS CNAV1/CNV2 IODE + int iodc = -1; ///< BDS CNAV1/CNV2 IODC + E_Svh svh; ///< SV health + int wnop = 0; ///< GPS/QZS: GPS week number (of prediction?) with AR + int flag = 0; ///< BDS B1C/B2a+B1C/B2b integrity flags + GTime toc = {}; ///< time of clock + GTime toe = {}; ///< time of ephemeris, for GPS/QZS, TOE==TOC + 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 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, + ///< isc[2]=ISC_L5I5, isc[3]=ISC_L5Q5, isc[4]=ISC_L1Cd, isc[5]=ISC_L1Cp BDS + ///< CNV1: isc[0]=ISC_B1Cd BDS CNV2: isc[1]=ISC_B2ad + double sis[5] = {}; ///< signal in space accuracy index + ///< BDS sis[0]=SISAI_oe, sis[1]=SISAI_ocb, sis[2]=SISAI_oc1, + ///< sis[3]=SISAI_oc2, sis[4]=SISMAI + double tops = 0; ///< t_op (s) in week + double toes = 0; ///< TOE (s) in week + double f0 = 0; ///< SV clock parameter (af0) + double f1 = 0; ///< SV clock parameter (af1) + double f2 = 0; ///< SV clock parameter (af2) + double tgd[4] = {}; ///< group delay parameters + ///< GPS/QZS: tgd[0]=TGD + ///< BDS CNAV1/CNV2: tgd[0]=TGD_B1Cp, tgd[1]=TGD_B2ap + ///< BDS CNAV3: tgd[2]=TGD_B2bI + + double ttms = 0; ///< transmission time (s) within week }; /** system Time offset message */ struct STO { - E_NavMsgType type = E_NavMsgType::NONE; ///< message type - SatSys Sat; ///< satellite number - GTime tot; ///< reference epoch for time offset information - GTime ttm; ///< transmission time - E_StoCode code = E_StoCode::NONE; ///< system Time offset code; - E_SbasId sid = E_SbasId::NONE; ///< SBAS ID - E_UtcId uid = E_UtcId::NONE; ///< UTC ID - - double A0 = 0; ///< (sec) - double A1 = 0; ///< (sec/sec) - double A2 = 0; ///< (sec/sec^2) - - double ttms = 0; ///< transmission time (s) within week + E_NavMsgType type = E_NavMsgType::NONE; ///< message type + SatSys Sat; ///< satellite number + GTime tot; ///< reference epoch for time offset information + GTime ttm; ///< transmission time + E_StoCode code = E_StoCode::NONE; ///< system Time offset code; + E_SbasId sid = E_SbasId::NONE; ///< SBAS ID + E_UtcId uid = E_UtcId::NONE; ///< UTC ID + + double A0 = 0; ///< (sec) + double A1 = 0; ///< (sec/sec) + double A2 = 0; ///< (sec/sec^2) + + double ttms = 0; ///< transmission time (s) within week }; /** EOP message */ struct EOP { - E_NavMsgType type = E_NavMsgType::NONE; ///< message type - SatSys Sat; ///< satellite number - GTime teop; ///< reference epoch of EOP data - GTime ttm; ///< transmission time - - double xp = 0; ///< pole offset (rad) - double xpr = 0; ///< pole offset rate (rad/day) - double xprr = 0; ///< pole offset rate rate (rad/day^2) - double yp = 0; ///< pole offset (rad) - double ypr = 0; ///< pole offset rate (rad/day) - double yprr = 0; ///< pole offset rate rate (rad/day^2) - double dut1 = 0; ///< ut1-utc or ut1-gpst (s) - double dur = 0; ///< delta ut1 rate (s/day) - double durr = 0; ///< delta ut1 rate rate (s/day^2) - - double ttms; ///< transmission time (s) within week + E_NavMsgType type = E_NavMsgType::NONE; ///< message type + SatSys Sat; ///< satellite number + GTime teop; ///< reference epoch of EOP data + GTime ttm; ///< transmission time + + double xp = 0; ///< pole offset (rad) + double xpr = 0; ///< pole offset rate (rad/day) + double xprr = 0; ///< pole offset rate rate (rad/day^2) + double yp = 0; ///< pole offset (rad) + double ypr = 0; ///< pole offset rate (rad/day) + double yprr = 0; ///< pole offset rate rate (rad/day^2) + double dut1 = 0; ///< ut1-utc or ut1-gpst (s) + double dur = 0; ///< delta ut1 rate (s/day) + double durr = 0; ///< delta ut1 rate rate (s/day^2) + + double ttms; ///< transmission time (s) within week }; /** ionosphere message */ struct ION { - E_NavMsgType type = E_NavMsgType::NONE; ///< message type - SatSys Sat; ///< satellite number - GTime ttm; ///< transmission time - int code = 0; ///< rgion code for QZS - int flag = 0; ///< disturbance flags for GAL - - union - { - double vals[9] = {}; - struct - { - // Klobuchar model: GPS/QZS LNAV/CNVX and BDS D1D2 - double a0; - double a1; - double a2; - double a3; - double b0; - double b1; - double b2; - double b3; - }; - struct - { - // NEQUICK-G model: GAL IFNV - double ai0; - double ai1; - double ai2; - }; - struct - { - // BDGIM model: BDS CNVX - double alpha1; - double alpha2; - double alpha3; - double alpha4; - double alpha5; - double alpha6; - double alpha7; - double alpha8; - double alpha9; - }; - }; + E_NavMsgType type = E_NavMsgType::NONE; ///< message type + SatSys Sat; ///< satellite number + GTime ttm; ///< transmission time + int code = 0; ///< rgion code for QZS + int flag = 0; ///< disturbance flags for GAL + + union + { + double vals[9] = {}; + struct + { + // Klobuchar model: GPS/QZS LNAV/CNVX and BDS D1D2 + double a0; + double a1; + double a2; + double a3; + double b0; + double b1; + double b2; + double b3; + }; + struct + { + // NEQUICK-G model: GAL IFNV + double ai0; + double ai1; + double ai2; + }; + struct + { + // BDGIM model: BDS CNVX + double alpha1; + double alpha2; + double alpha3; + double alpha4; + double alpha5; + double alpha6; + double alpha7; + double alpha8; + double alpha9; + }; + }; }; struct Navigation; -Matrix3d ecef2rac( - Vector3d& rSat, - Vector3d& satVel); - +Matrix3d ecef2rac(Vector3d& rSat, Vector3d& satVel); -void cullOldEphs( - GTime time); - -void cullOldSSRs( - GTime time); +void cullOldEphs(GTime time); +void cullOldSSRs(GTime time); struct KFState; - -template TYPE* seleph(Trace& trace, GTime time, SatSys Sat, E_NavMsgType type, int iode, Navigation& nav); -template TYPE* seleph(Trace& trace, GTime time, E_Sys sys, E_NavMsgType type, Navigation& nav); - +template +TYPE* seleph(Trace& trace, GTime time, SatSys Sat, E_NavMsgType type, int iode, Navigation& nav); +template +TYPE* seleph(Trace& trace, GTime time, E_Sys sys, E_NavMsgType type, Navigation& nav); bool satclk( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - vector ephTypes, - Navigation& nav, - const KFState* kfState_ptr = nullptr, - const KFState* remote_ptr = nullptr); + Trace& trace, + GTime time, + GTime teph, + SatPos& satPos, + vector ephTypes, + Navigation& nav, + const KFState* kfState_ptr = nullptr, + const KFState* remote_ptr = nullptr +); bool satpos( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - vector ephTypes, - E_OffsetType offsetType, - Navigation& nav, - const KFState* kfState_ptr = nullptr, - const KFState* remote_ptr = nullptr); + Trace& trace, + GTime time, + GTime teph, + SatPos& satPos, + vector ephTypes, + E_OffsetType offsetType, + Navigation& nav, + const KFState* kfState_ptr = nullptr, + const KFState* remote_ptr = nullptr +); bool satPosClk( - Trace& trace, - GTime teph, - GObs& obs, - Navigation& nav, - vector posSources, - vector clkSources, - const KFState* kfState_ptr = nullptr, - const KFState* remote_ptr = nullptr, - E_OffsetType offsetType = E_OffsetType::COM, - E_Relativity applyRelativity = E_Relativity::ON); - -void readSp3ToNav( - string& file, - Navigation& nav, - int opt); + Trace& trace, + GTime teph, + GObs& obs, + Navigation& nav, + vector posSources, + vector clkSources, + const KFState* kfState_ptr = nullptr, + const KFState* remote_ptr = nullptr, + E_OffsetType offsetType = E_OffsetType::COM, + E_Relativity applyRelativity = E_Relativity::ON +); + +void readSp3ToNav(string& file, Navigation& nav, int opt); bool readsp3( - std::istream& fileStream, - vector& pephList, - int opt, - E_TimeSys& tsys, - double* bfact); + std::istream& fileStream, + vector& pephList, + int opt, + E_TimeSys& tsys, + double* bfact +); -void readOrbex( - string filepath, - Navigation& nav); +void readOrbex(string filepath, Navigation& nav); +bool satPosKalman(Trace& trace, GTime time, SatPos& satPos, const KFState* kfState_ptr); -bool satPosKalman( - Trace& trace, - GTime time, - SatPos& satPos, - const KFState* kfState_ptr); - -bool satClkKalman( - Trace& trace, - GTime time, - SatPos& satPos, - const KFState* kfState_ptr); +bool satClkKalman(Trace& trace, GTime time, SatPos& satPos, const KFState* kfState_ptr); bool satClkBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatSys Sat, - double& satClk, - double& satClkVel, - double& ephVar, - bool& ephClkValid, - int& obsIode, - Navigation& nav); + Trace& trace, + GTime time, + GTime teph, + SatSys Sat, + double& satClk, + double& satClkVel, + double& clkVar, + bool& ephClkValid, + int& obsIode, + Navigation& nav +); bool satPosBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatSys Sat, - Vector3d& rSat, - Vector3d& satVel, - double& ephVar, - bool& ephPosValid, - int& obsIode, - Navigation& nav); - + Trace& trace, + GTime time, + GTime teph, + SatSys Sat, + Vector3d& rSat, + Vector3d& satVel, + double& ephVar, + bool& ephPosValid, + int& obsIode, + Navigation& nav +); bool satClkBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - Navigation& nav, - int iode = ANY_IODE); + Trace& trace, + GTime time, + GTime teph, + SatPos& satPos, + Navigation& nav, + int iode = ANY_IODE +); bool satPosBroadcast( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - Navigation& nav, - int iode = ANY_IODE); - - -bool satPosPrecise( - Trace& trace, - GTime time, - SatPos& satPos, - Navigation& nav); - -bool satClkPrecise( - Trace& trace, - GTime time, - SatPos& satPos, - Navigation& nav); - - -bool satPosSSR( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - Navigation& nav); - -bool satClkSSR( - Trace& trace, - GTime time, - GTime teph, - SatPos& satPos, - Navigation& nav); - -double relativity1( - Vector3d& rSat, - Vector3d& satVel); + Trace& trace, + GTime time, + GTime teph, + SatPos& satPos, + Navigation& nav, + int iode = ANY_IODE +); + +bool satPosPrecise(Trace& trace, GTime time, SatPos& satPos, Navigation& nav); + +bool satClkPrecise(Trace& trace, GTime time, SatPos& satPos, Navigation& nav); + +bool satPosSSR(Trace& trace, GTime time, GTime teph, SatPos& satPos, Navigation& nav); + +bool satClkSSR(Trace& trace, GTime time, GTime teph, SatPos& satPos, Navigation& nav); + +double relativity1(Vector3d& rSat, Vector3d& satVel); + +bool satPosSBAS(Trace& trace, GTime time, GTime teph, SatPos& satPos, Navigation& nav); + +bool satClkSBAS(Trace& trace, GTime time, GTime teph, SatPos& satPos, Navigation& nav); \ No newline at end of file diff --git a/src/cpp/common/erp.cpp b/src/cpp/common/erp.cpp index e9045c4df..6af627606 100644 --- a/src/cpp/common/erp.cpp +++ b/src/cpp/common/erp.cpp @@ -1,634 +1,731 @@ - // #pragma GCC optimize ("O0") +#include "common/erp.hpp" #include - -#include +#include #include +#include #include -#include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/constants.hpp" +#include "common/ephPrecise.hpp" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/trace.hpp" +#include "pea/peaCommitStrings.hpp" -using std::chrono::system_clock; -using std::stringstream; using std::string; +using std::stringstream; +using std::chrono::system_clock; + +constexpr int NMAX = 3; /* order of polynomial interpolation */ + +ERPValues ERPValues::operator+(const ERPValues& rhs) +{ + ERPValues erpv = *this; + + erpv.time += rhs.time; + erpv.xp += rhs.xp; + erpv.yp += rhs.yp; + erpv.ut1Utc += rhs.ut1Utc; + erpv.lod += rhs.lod; -#include "peaCommitStrings.hpp" -#include "navigation.hpp" -#include "ephPrecise.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "algebra.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "erp.hpp" + erpv.xpr += rhs.xpr; + erpv.ypr += rhs.ypr; -#define NMAX 3 /* order of polynomial interpolation */ + erpv.xpSigma = sqrt(SQR(erpv.xpSigma) + SQR(rhs.xpSigma)); + erpv.ypSigma = sqrt(SQR(erpv.ypSigma) + SQR(rhs.ypSigma)); + erpv.xprSigma = sqrt(SQR(erpv.xprSigma) + SQR(rhs.xprSigma)); + erpv.yprSigma = sqrt(SQR(erpv.yprSigma) + SQR(rhs.yprSigma)); + erpv.ut1UtcSigma = sqrt(SQR(erpv.ut1UtcSigma) + SQR(rhs.ut1UtcSigma)); + erpv.lodSigma = sqrt(SQR(erpv.lodSigma) + SQR(rhs.lodSigma)); + erpv.isPredicted |= rhs.isPredicted; -string ERPValues::toString() + return erpv; +} + +bool ERPValues::operator==(const ERPValues& rhs) const { - Vector3d erp; - erp << xp, yp, ut1Utc; + bool equal = time == rhs.time && xp == rhs.xp && yp == rhs.yp && ut1Utc == rhs.ut1Utc && + lod == rhs.lod && xpr == rhs.xpr && ypr == rhs.ypr; + + return equal; +}; + +ERPValues ERPValues::operator*(const double scalar) +{ + ERPValues erpv = *this; + + erpv.time.bigTime *= scalar; + erpv.xp *= scalar; + erpv.yp *= scalar; + erpv.ut1Utc *= scalar; + erpv.lod *= scalar; + + erpv.xpr *= scalar; + erpv.ypr *= scalar; - stringstream ss; - ss << erp.transpose().format(heavyFmt); - return ss.str(); + erpv.xpSigma *= scalar; + erpv.ypSigma *= scalar; + erpv.xprSigma *= scalar; + erpv.yprSigma *= scalar; + erpv.ut1UtcSigma *= scalar; + erpv.lodSigma *= scalar; + + return erpv; +}; + +string ERPValues::toString() const +{ + Vector3d erp; + erp << xp, yp, ut1Utc; + + stringstream ss; + ss << erp.transpose().format(heavyFmt); + return ss.str(); } -string ERPValues::toReadableString() +string ERPValues::toReadableString() const { - Vector3d erp; - erp << - xp * R2MAS, - yp * R2MAS, - ut1Utc * S2MTS; - - stringstream ss; - ss << erp.transpose().format(heavyFmt); - return ss.str(); + Vector3d erp; + erp << xp * R2MAS, yp * R2MAS, ut1Utc * S2MTS; + + stringstream ss; + ss << erp.transpose().format(heavyFmt); + return ss.str(); } /** read IGS ERP products */ void readIgsErp( - string line, ///< line to read for IGS ERP file (IGS ERP ver.2) - map& erpMap) ///< earth rotation parameters + string line, ///< line to read for IGS ERP file (IGS ERP ver.2) + map& erpMap ///< earth rotation parameters +) { - double mjdval = 0; - double v[13] = {}; - int found = sscanf(line.c_str(),"%lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", - &mjdval, - &v[0], - &v[1], - &v[2], - &v[3], - &v[4], - &v[5], - &v[6], - &v[7], - &v[8], - &v[9], - &v[10], - &v[11], - &v[12]); - - if (found < 14) - { - return; - } - - MjDateUtc mjd; - mjd.val = mjdval; - - ERPValues erpv; - - erpv.time = mjd; - erpv.xp = v[0] * 1E-6*AS2R; - erpv.yp = v[1] * 1E-6*AS2R; - erpv.ut1Utc = v[2] * 1E-7; - erpv.lod = v[3] * 1E-7; - erpv.xpSigma = v[4] * 1E-6*AS2R; - erpv.ypSigma = v[5] * 1E-6*AS2R; - erpv.ut1UtcSigma = v[6] * 1E-7; - erpv.lodSigma = v[7] * 1E-7; - erpv.xpr = v[11] * 1E-6*AS2R; - erpv.ypr = v[12] * 1E-6*AS2R; - - erpMap[erpv.time] = erpv; + double mjdval = 0; + double v[13] = {}; + int found = sscanf( + line.c_str(), + "%lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", + &mjdval, + &v[0], + &v[1], + &v[2], + &v[3], + &v[4], + &v[5], + &v[6], + &v[7], + &v[8], + &v[9], + &v[10], + &v[11], + &v[12] + ); + + if (found < 14) + { + return; + } + + MjDateUtc mjd; + mjd.val = mjdval; + + ERPValues erpv; + + erpv.time = mjd; + erpv.xp = v[0] * 1E-6 * AS2R; + erpv.yp = v[1] * 1E-6 * AS2R; + erpv.ut1Utc = v[2] * 1E-7; + erpv.lod = v[3] * 1E-7; + erpv.xpSigma = v[4] * 1E-6 * AS2R; + erpv.ypSigma = v[5] * 1E-6 * AS2R; + erpv.ut1UtcSigma = v[6] * 1E-7; + erpv.lodSigma = v[7] * 1E-7; + erpv.xpr = v[11] * 1E-6 * AS2R; + erpv.ypr = v[12] * 1E-6 * AS2R; + + erpMap[erpv.time] = erpv; } /** read IERS C04 EOP products */ void readIers14C04( - string line, ///< line to read for IERS C04 file - map& erpMap) ///< earth rotation parameters + string line, ///< line to read for IERS C04 file + map& erpMap ///< earth rotation parameters +) { - int date[3]; - double mjdval = 0; - double v[12] = {}; - - int found = sscanf(line.c_str(),"%d %d %d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", - &date[0], - &date[1], - &date[2], - &mjdval, - &v[0], - &v[1], - &v[2], - &v[3], - &v[4], - &v[5], - &v[6], - &v[7], - &v[8], - &v[9], - &v[10], - &v[11]); - - if (found < 16) - { - return; - } - - MjDateUtc mjd; - mjd.val = mjdval; - - ERPValues erpv; - - erpv.time = mjd; - if (erpv.time < GTime::noTime()) - { - return; - } - - erpv.xp = v[0] * AS2R; - erpv.yp = v[1] * AS2R; - erpv.ut1Utc = v[2]; - erpv.lod = v[3]; - erpv.xpSigma = v[6] * AS2R; - erpv.ypSigma = v[7] * AS2R; - erpv.ut1UtcSigma = v[8]; - erpv.lodSigma = v[9]; - - erpMap[erpv.time] = erpv; + int date[3]; + double mjdval = 0; + double v[12] = {}; + + int found = sscanf( + line.c_str(), + "%d %d %d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", + &date[0], + &date[1], + &date[2], + &mjdval, + &v[0], + &v[1], + &v[2], + &v[3], + &v[4], + &v[5], + &v[6], + &v[7], + &v[8], + &v[9], + &v[10], + &v[11] + ); + + if (found < 16) + { + return; + } + + MjDateUtc mjd; + mjd.val = mjdval; + + ERPValues erpv; + + erpv.time = mjd; + if (erpv.time < GTime::noTime()) + { + return; + } + + erpv.xp = v[0] * AS2R; + erpv.yp = v[1] * AS2R; + erpv.ut1Utc = v[2]; + erpv.lod = v[3]; + erpv.xpSigma = v[6] * AS2R; + erpv.ypSigma = v[7] * AS2R; + erpv.ut1UtcSigma = v[8]; + erpv.lodSigma = v[9]; + + erpMap[erpv.time] = erpv; } /** read IERS C04 EOP products */ void readIers20C04( - string line, ///< line to read for IERS C04 file - map& erpMap) ///< earth rotation parameters + string line, ///< line to read for IERS C04 file + map& erpMap ///< earth rotation parameters +) { - int date[4]; - double mjdval = 0; - double v[17] = {}; - - int found = sscanf(line.c_str(),"%d %d %d %d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", - &date[0], - &date[1], - &date[2], - &date[4], - &mjdval, - &v[0], - &v[1], - &v[2], - &v[3], - &v[4], - &v[5], - &v[6], - &v[7], - &v[8], - &v[9], - &v[10], - &v[11], - &v[12], - &v[13], - &v[14], - &v[15]); - - if (found < 21) - { - return; - } - - MjDateUtc mjd; - mjd.val = mjdval; - - ERPValues erpv; - - erpv.time = mjd; - if (erpv.time < GTime::noTime()) - { - return; - } - - erpv.xp = v[0] * AS2R; - erpv.yp = v[1] * AS2R; - erpv.ut1Utc = v[2]; - erpv.xpr = v[5] * AS2R; - erpv.ypr = v[6] * AS2R; - erpv.lod = v[7]; - - erpv.xpSigma = v[8] * AS2R; - erpv.ypSigma = v[9] * AS2R; - erpv.ut1UtcSigma = v[10]; - erpv.xprSigma = v[13] * AS2R; - erpv.yprSigma = v[14] * AS2R; - erpv.lodSigma = v[15]; - - erpMap[erpv.time] = erpv; + int date[4]; + double mjdval = 0; + double v[17] = {}; + + int found = sscanf( + line.c_str(), + "%d %d %d %d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", + &date[0], + &date[1], + &date[2], + &date[4], + &mjdval, + &v[0], + &v[1], + &v[2], + &v[3], + &v[4], + &v[5], + &v[6], + &v[7], + &v[8], + &v[9], + &v[10], + &v[11], + &v[12], + &v[13], + &v[14], + &v[15] + ); + + if (found < 21) + { + return; + } + + MjDateUtc mjd; + mjd.val = mjdval; + + ERPValues erpv; + + erpv.time = mjd; + if (erpv.time < GTime::noTime()) + { + return; + } + + erpv.xp = v[0] * AS2R; + erpv.yp = v[1] * AS2R; + erpv.ut1Utc = v[2]; + erpv.xpr = v[5] * AS2R; + erpv.ypr = v[6] * AS2R; + erpv.lod = v[7]; + + erpv.xpSigma = v[8] * AS2R; + erpv.ypSigma = v[9] * AS2R; + erpv.ut1UtcSigma = v[10]; + erpv.xprSigma = v[13] * AS2R; + erpv.yprSigma = v[14] * AS2R; + erpv.lodSigma = v[15]; + + erpMap[erpv.time] = erpv; } /** read IERS final EOP products */ void readIersFinal( - string line, ///< line to read for IERS final file - map& erpMap) ///< earth rotation parameters + string line, ///< line to read for IERS final file + map& erpMap ///< earth rotation parameters +) { - if (line.size() < 78) - { - return; - } - char* buff = &line[0]; - - MjDateUtc mjd; - mjd.val = str2num(buff, 7, 8); - double xp = str2num(buff, 18, 9); - double xpSigma = str2num(buff, 27, 9); - double yp = str2num(buff, 37, 9); - double ypSigma = str2num(buff, 46, 9); - double ut1Utc = str2num(buff, 58, 10); - double ut1UtcSigma = str2num(buff, 68, 10); - - ERPValues erpv; - - erpv.time = mjd; - erpv.xp = xp * AS2R; - erpv.yp = yp * AS2R; - erpv.ut1Utc = ut1Utc; - erpv.xpSigma = xpSigma * AS2R; - erpv.ypSigma = ypSigma * AS2R; - erpv.ut1UtcSigma = ut1UtcSigma; - - if ( buff[16] == 'P' - ||buff[57] == 'P') - { - erpv.isPredicted = true; - } - - if (line.size() >= 93) - { - double lod = str2num(buff, 79, 7); - double lodSigma = str2num(buff, 86, 7); - - erpv.lod = lod * 1E-3; - erpv.lodSigma = lodSigma * 1E-3; - } - - erpMap[erpv.time] = erpv; + if (line.size() < 78) + { + return; + } + char* buff = &line[0]; + + MjDateUtc mjd; + mjd.val = str2num(buff, 7, 8); + double xp = str2num(buff, 18, 9); + double xpSigma = str2num(buff, 27, 9); + double yp = str2num(buff, 37, 9); + double ypSigma = str2num(buff, 46, 9); + double ut1Utc = str2num(buff, 58, 10); + double ut1UtcSigma = str2num(buff, 68, 10); + + ERPValues erpv; + + erpv.time = mjd; + erpv.xp = xp * AS2R; + erpv.yp = yp * AS2R; + erpv.ut1Utc = ut1Utc; + erpv.xpSigma = xpSigma * AS2R; + erpv.ypSigma = ypSigma * AS2R; + erpv.ut1UtcSigma = ut1UtcSigma; + + if (buff[16] == 'P' || buff[57] == 'P') + { + erpv.isPredicted = true; + } + + if (line.size() >= 93) + { + double lod = str2num(buff, 79, 7); + double lodSigma = str2num(buff, 86, 7); + + erpv.lod = lod * 1E-3; + erpv.lodSigma = lodSigma * 1E-3; + } + + erpMap[erpv.time] = erpv; } /** read IERS Bulletin-A EOP products */ void readIersBulletinA( - string line, ///< line to read for IERS Bulletin-A file - map& erpMap) ///< earth rotation parameters + string line, ///< line to read for IERS Bulletin-A file + map& erpMap ///< earth rotation parameters +) { - int date[3]; - double mjdval = 0; - double v[6] = {}; - - int found = sscanf(line.c_str(),"%d %d %d %lf %lf %lf %lf %lf %lf %lf", - &date[0], - &date[1], - &date[2], - &mjdval, - &v[0], - &v[1], - &v[2], - &v[3], - &v[4], - &v[5]); - - if (found < 7) - { - return; - } - - MjDateUtc mjd; - ERPValues erpv; - - if (found == 10) // Combined Earth Orientation Parameters (Rapid) - { - mjd.val = mjdval; - erpv.time = mjd; - erpv.xp = v[0] * AS2R; - erpv.xpSigma = v[1] * AS2R; - erpv.yp = v[2] * AS2R; - erpv.ypSigma = v[3] * AS2R; - erpv.ut1Utc = v[4]; - erpv.ut1UtcSigma = v[5]; - - erpMap[erpv.time] = erpv; - } - else if (found == 7) // Predictions - { - mjd.val = mjdval; - erpv.time = mjd; - erpv.xp = v[0] * AS2R; - erpv.yp = v[1] * AS2R; - erpv.ut1Utc = v[2]; - erpv.isPredicted = true; - - erpMap[erpv.time] = erpv; - } + int date[3]; + double mjdval = 0; + double v[6] = {}; + + int found = sscanf( + line.c_str(), + "%d %d %d %lf %lf %lf %lf %lf %lf %lf", + &date[0], + &date[1], + &date[2], + &mjdval, + &v[0], + &v[1], + &v[2], + &v[3], + &v[4], + &v[5] + ); + + if (found < 7) + { + return; + } + + MjDateUtc mjd; + ERPValues erpv; + + if (found == 10) // Combined Earth Orientation Parameters (Rapid) + { + mjd.val = mjdval; + erpv.time = mjd; + erpv.xp = v[0] * AS2R; + erpv.xpSigma = v[1] * AS2R; + erpv.yp = v[2] * AS2R; + erpv.ypSigma = v[3] * AS2R; + erpv.ut1Utc = v[4]; + erpv.ut1UtcSigma = v[5]; + + erpMap[erpv.time] = erpv; + } + else if (found == 7) // Predictions + { + mjd.val = mjdval; + erpv.time = mjd; + erpv.xp = v[0] * AS2R; + erpv.yp = v[1] * AS2R; + erpv.ut1Utc = v[2]; + erpv.isPredicted = true; + + erpMap[erpv.time] = erpv; + } } /** Get earth rotation parameter values */ ERPValues getErp( - ERP& erp, ///< earth rotation parameters - GTime time, ///< Time - bool useFilter) ///< Optionally use the filter values stored in the erp object + ERP& erp, ///< earth rotation parameters + GTime time, ///< Time + bool useFilter ///< Optionally use the filter values stored in the erp object +) { - if ( useFilter - &&erp.filterValues.isFiltered) - { - ERPValues erpv = erp.filterValues; - - double dt = (time - erpv.time).to_double() / S_IN_DAY; - if (dt) - { - erpv.time += dt; - erpv.xp += erpv.xpr * dt; - erpv.yp += erpv.ypr * dt; - erpv.ut1Utc -= erpv.lod * dt; - } - - return erpv; - } - - ERPValues erpv; - - auto& recOpts = acsConfig.getRecOpts("global"); - - if (recOpts.eop == false) - { - return erpv; - } - - double dtMax = 2 * S_IN_DAY; - - for (auto rit = erp.erpMaps.rbegin(); rit != erp.erpMaps.rend(); rit++) - { - auto& erpMap = *rit; - - // at least two data points required - if (erpMap.size() < 2) - continue; - - int nMax = NMAX; - auto erp_it = erpMap.lower_bound(time); - if (erp_it == erpMap.end()) - { - // exceed the end of the map, do linear extrapolation - nMax = 1; - erp_it--; - } - else if (erp_it == erpMap.begin()) - { - // before the beginning of the map, do linear extrapolation - nMax = 1; - } - - //go forward a few steps to make sure we're far from the end of the map. - for (int i = 0; i <= nMax/2; i++) - { - erp_it++; - if (erp_it == erpMap.end()) - { - break; - } - } - - //go backward a few steps to make sure we're far from the beginning of the map - for (int i = 0; i <= nMax; i++) - { - erp_it--; - if (erp_it == erpMap.begin()) - { - break; - } - } - - vector erpvs; - vector dt; - - //get interpolation parameters - - for (int i = 0; i <= nMax; i++, erp_it++) - { - if (erp_it == erpMap.end()) - { - break; - } - - auto& [itTime, erpv] = *erp_it; - - dt .push_back((itTime - time).to_double()); - erpvs .push_back(erpv); - } - - if ( dt.front() >= +dtMax - ||dt.back() <= -dtMax) - { - // we're further away than last time, try the next map/file - continue; - } - - erpv = interpolate(dt, erpvs); - - if ( dt.front() <= 0 - &&dt.back() >= 0) - { - if (erpvs.back().isPredicted) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Predicted ERP used for interpolation.\n"; - } - - // interpolation done - return erpv; - } - - if ( dt.front() > 0 && dt.front() < +dtMax) { dtMax = dt.front(); continue; } - else if ( dt.back() < 0 && dt.back() > -dtMax) { dtMax = -dt.back(); continue; } - } - - if (erpv.time > GTime::noTime()) - { - erpv.isPredicted = true; - - BOOST_LOG_TRIVIAL(warning) << "Warning: No suitable data for ERP interpolation, extrapolated ERP in use.\n"; - } - else - { - BOOST_LOG_TRIVIAL(warning) << "Warning: failed to get ERP at " << time.to_string() << ".\n"; - } - - return erpv; + if (useFilter && erp.filterValues.isFiltered) + { + ERPValues erpv = erp.filterValues; + + double dt = (time - erpv.time).to_double() / S_IN_DAY; + if (dt) + { + erpv.time += dt; + erpv.xp += erpv.xpr * dt; + erpv.yp += erpv.ypr * dt; + erpv.ut1Utc -= erpv.lod * dt; + } + + return erpv; + } + + ERPValues erpv; + + auto& recOpts = acsConfig.getRecOpts("global"); + + if (recOpts.eop == false) + { + return erpv; + } + + double dtMax = 2 * S_IN_DAY; + + for (auto rit = erp.erpMaps.rbegin(); rit != erp.erpMaps.rend(); rit++) + { + auto& erpMap = *rit; + + // at least two data points required + if (erpMap.size() < 2) + continue; + + int nMax = NMAX; + auto erp_it = erpMap.lower_bound(time); + if (erp_it == erpMap.end()) + { + // exceed the end of the map, do linear extrapolation + nMax = 1; + erp_it--; + } + else if (erp_it == erpMap.begin()) + { + // before the beginning of the map, do linear extrapolation + nMax = 1; + } + + // go forward a few steps to make sure we're far from the end of the map. + for (int i = 0; i <= nMax / 2; i++) + { + erp_it++; + if (erp_it == erpMap.end()) + { + break; + } + } + + // go backward a few steps to make sure we're far from the beginning of the map + for (int i = 0; i <= nMax; i++) + { + erp_it--; + if (erp_it == erpMap.begin()) + { + break; + } + } + + vector erpvs; + vector dt; + + // get interpolation parameters + + for (int i = 0; i <= nMax; i++, erp_it++) + { + if (erp_it == erpMap.end()) + { + break; + } + + auto& [itTime, erpv] = *erp_it; + + dt.push_back((itTime - time).to_double()); + erpvs.push_back(erpv); + } + + if (dt.front() >= +dtMax || dt.back() <= -dtMax) + { + // we're further away than last time, try the next map/file + continue; + } + + erpv = interpolate(dt, erpvs); + + if (dt.front() <= 0 && dt.back() >= 0) + { + if (erpvs.back().isPredicted) + { + BOOST_LOG_TRIVIAL(warning) << "Predicted ERP used for interpolation.\n"; + } + + // interpolation done + return erpv; + } + + if (dt.front() > 0 && dt.front() < +dtMax) + { + dtMax = dt.front(); + continue; + } + else if (dt.back() < 0 && dt.back() > -dtMax) + { + dtMax = -dt.back(); + continue; + } + } + + if (erpv.time > GTime::noTime()) + { + erpv.isPredicted = true; + + BOOST_LOG_TRIVIAL(warning) + << "No suitable data for ERP interpolation, extrapolated ERP in use.\n"; + } + else + { + BOOST_LOG_TRIVIAL(warning) << "Failed to get ERP at " << time.to_string() << ".\n"; + } + + return erpv; } -void writeErp( - string filename, - ERPValues& erp) +void writeErp(string filename, ERPValues& erp) { - if (filename.empty()) - { - return; - } - - std::ofstream erpStream(filename, std::ios::app); - - if (!erpStream) - { - BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for ERP file."; - - return; - } - - erpStream.seekp(0, erpStream.end); // seek to end of file - - if (erpStream.tellp() == 0) - { - erpStream << "VERSION 2" << "\n"; - erpStream << " Generated by GINAN " << ginanCommitVersion() << " branch " << ginanBranchName() << "\n"; - - erpStream << "----------------------------------------------------------------------------------------------------------------" << "\n"; - erpStream << " MJD Xpole Ypole UT1-UTC LOD Xsig Ysig UTsig LODsig Nr Nf Nt Xrt Yrt" << "\n"; - erpStream << " 1E-6as 1E-6as 1E-7s 1E-7s/d 1E-6as 1E-6as 1E-7s 1E-7s/d 1E-6/d 1E-6/d" << "\n"; - } - - int numRecs = 0; - int numFixedRecs = 0; - int numSats = 0; - - MjDateUtc mjd = erp.time; - - tracepdeex(0, erpStream, "%8.4f %8d %8d %8d %8d %8d %8d %8d %8d %3d %3d %3d %8d %8d\n", - mjd.to_double(), - (int) (erp.xp * 1E6 * R2AS), - (int) (erp.yp * 1E6 * R2AS), - (int) (erp.ut1Utc * 1E7), - (int) (erp.lod * 1E7), - (int) (erp.xpSigma * 1E6 * R2AS), - (int) (erp.ypSigma * 1E6 * R2AS), - (int) (erp.ut1UtcSigma * 1E7), - (int) (erp.lodSigma * 1E7), - numRecs, - numFixedRecs, - numSats, - (int) (erp.xpr * 1E6 * R2AS), - (int) (erp.ypr * 1E6 * R2AS)); + if (filename.empty()) + { + return; + } + + std::ofstream erpStream(filename, std::ios::app); + + if (!erpStream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for ERP file."; + + return; + } + + erpStream.seekp(0, erpStream.end); // seek to end of file + + if (erpStream.tellp() == 0) + { + erpStream << "VERSION 2" << "\n"; + erpStream << " Generated by GINAN " << ginanCommitVersion() << " branch " + << ginanBranchName() << "\n"; + + erpStream << "-----------------------------------------------------------------------------" + "--------------------" + "---------------" + << "\n"; + erpStream << " MJD Xpole Ypole UT1-UTC LOD Xsig Ysig UTsig " + "LODsig Nr Nf Nt " + " Xrt Yrt" + << "\n"; + erpStream << " 1E-6as 1E-6as 1E-7s 1E-7s/d 1E-6as 1E-6as 1E-7s " + "1E-7s/d " + "1E-6/d 1E-6/d" + << "\n"; + } + + int numRecs = 0; + int numFixedRecs = 0; + int numSats = 0; + + MjDateUtc mjd = erp.time; + + tracepdeex( + 0, + erpStream, + "%8.4f %8d %8d %8d %8d %8d %8d %8d %8d %3d %3d %3d %8d %8d\n", + mjd.to_double(), + (int)(erp.xp * 1E6 * R2AS), + (int)(erp.yp * 1E6 * R2AS), + (int)(erp.ut1Utc * 1E7), + (int)(erp.lod * 1E7), + (int)(erp.xpSigma * 1E6 * R2AS), + (int)(erp.ypSigma * 1E6 * R2AS), + (int)(erp.ut1UtcSigma * 1E7), + (int)(erp.lodSigma * 1E7), + numRecs, + numFixedRecs, + numSats, + (int)(erp.xpr * 1E6 * R2AS), + (int)(erp.ypr * 1E6 * R2AS) + ); } /** Get earth rotation parameter values */ -ERPValues getErpFromFilter( - const KFState& kfState) +ERPValues getErpFromFilter(const KFState& kfState) { - ERPValues erpv; - - bool found = false; - for (int i = 0; i < 3; i++) - { - double val = 0; - double valVar = 0; - double rate = 0; - double rateVar = 0; - - KFKey kfKey; - kfKey.num = i; - - kfKey.type = KF::EOP; - found |= kfState.getKFValue(kfKey, val, &valVar); - - kfKey.type = KF::EOP_RATE; - found |= kfState.getKFValue(kfKey, rate, &rateVar); - - switch (i) - { - case 0: erpv.xp = +val * MAS2R; erpv.xpSigma = sqrt(valVar) * MAS2R; - erpv.xpr = +rate * MAS2R; erpv.xprSigma = sqrt(rateVar) * MAS2R; break; - case 1: erpv.yp = +val * MAS2R; erpv.ypSigma = sqrt(valVar) * MAS2R; - erpv.ypr = +rate * MAS2R; erpv.yprSigma = sqrt(rateVar) * MAS2R; break; - case 2: erpv.ut1Utc = +val * MTS2S; erpv.ut1UtcSigma = sqrt(valVar) * MTS2S; - erpv.lod = -rate * MTS2S; erpv.lodSigma = sqrt(rateVar) * MTS2S; break; - default: - break; - } - } - - if (found) - { - erpv.isFiltered = true; - erpv.time = kfState.time; - } - - return erpv; + ERPValues erpv; + + bool found = true; + for (int i = 0; i < 3; i++) + { + double val = 0; + double valVar = 0; + double rate = 0; + double rateVar = 0; + + KFKey kfKey; + kfKey.num = i; + + kfKey.type = KF::EOP; + found = found && (kfState.getKFValue(kfKey, val, &valVar) != E_Source::NONE); + kfKey.type = KF::EOP_RATE; + found = found && (kfState.getKFValue(kfKey, rate, &rateVar) != E_Source::NONE); + + switch (i) + { + case 0: + erpv.xp = +val * MAS2R; + erpv.xpSigma = sqrt(valVar) * MAS2R; + erpv.xpr = +rate * MAS2R; + erpv.xprSigma = sqrt(rateVar) * MAS2R; + break; + case 1: + erpv.yp = +val * MAS2R; + erpv.ypSigma = sqrt(valVar) * MAS2R; + erpv.ypr = +rate * MAS2R; + erpv.yprSigma = sqrt(rateVar) * MAS2R; + break; + case 2: + erpv.ut1Utc = +val * MTS2S; + erpv.ut1UtcSigma = sqrt(valVar) * MTS2S; + erpv.lod = -rate * MTS2S; + erpv.lodSigma = sqrt(rateVar) * MTS2S; + break; + default: + break; + } + } + + if (found) + { + erpv.isFiltered = true; + erpv.time = kfState.time; + } + + return erpv; } -void writeErpFromNetwork( - string filename, - KFState& kfState) +void writeErpFromNetwork(string filename, KFState& kfState) { - static GTime lastTime = GTime::noTime(); + static GTime lastTime = GTime::noTime(); - if (abs((lastTime - kfState.time).to_double()) < 10) - { - //dont write duplicate lines (closer than 10s (4dp mjd)) - return; - } + if (abs((lastTime - kfState.time).to_double()) < 10) + { + // dont write duplicate lines (closer than 10s (4dp mjd)) + return; + } - lastTime = kfState.time; + lastTime = kfState.time; - ERPValues erpv = getErpFromFilter(kfState); + ERPValues erpv = getErpFromFilter(kfState); - writeErp(filename, erpv); + writeErp(filename, erpv); } - /** read earth rotation parameters */ void readErp( - string filename, ///< ERP file - ERP& erp) ///< earth rotation parameters + string filename, ///< ERP file + ERP& erp ///< earth rotation parameters +) { - std::ifstream filestream(filename); - if (!filestream) - { -// trace(2, "erp file open error: file=%s\n", file); - return; - } - - map erpMap; - - while (filestream) - { - string line; - - getline(filestream, line); - - if ( (line[16] == 'I' || line[16] == 'P') - &&(line[57] == 'I' || line[57] == 'P')) readIersFinal (line, erpMap); - else if ( line.size() == 218) readIers20C04 (line, erpMap); - else if ( line.size() == 155) readIers14C04 (line, erpMap); - else if ( line.size() <= 127 && line.size() >= 106) readIgsErp (line, erpMap); - else if ( line.size() <= 79) readIersBulletinA (line, erpMap); - } - - erp.erpMaps.push_back(erpMap); + std::ifstream filestream(filename); + if (!filestream) + { + // trace(2, "erp file open error: file=%s\n", file); + return; + } + + map erpMap; + + while (filestream) + { + string line; + + getline(filestream, line); + // Skip empty or short lines + if (line.size() < 58) + continue; + if ((line[16] == 'I' || line[16] == 'P') && (line[57] == 'I' || line[57] == 'P')) + readIersFinal(line, erpMap); + else if (line.size() == 218) + readIers20C04(line, erpMap); + else if (line.size() == 155) + readIers14C04(line, erpMap); + else if (line.size() <= 146 && line.size() >= 106) + readIgsErp(line, erpMap); + else if (line.size() <= 79) + readIersBulletinA(line, erpMap); + } + + erp.erpMaps.push_back(erpMap); } -Matrix3d stationEopPartials( - Vector3d& rRec) +Matrix3d receiverEopPartials(Vector3d& rRec) { - //compute partials and convert to units of MxS + // compute partials and convert to units of MxS - Matrix3d partials; - auto& X = rRec(0); - auto& Y = rRec(1); - auto& Z = rRec(2); - partials(0,0) = +Z * MAS2R; //dx/dxp = dx/dRotY - partials(0,1) = 0; //dy/dxp = dy/dRotY - partials(0,2) = -X * MAS2R; //dz/dxp = dz/dRotY + Matrix3d partials; + auto& X = rRec(0); + auto& Y = rRec(1); + auto& Z = rRec(2); + partials(0, 0) = +Z * MAS2R; // dx/dxp = dx/dRotY + partials(0, 1) = 0; // dy/dxp = dy/dRotY + partials(0, 2) = -X * MAS2R; // dz/dxp = dz/dRotY - partials(1,0) = 0; //dx/dyp = dx/dRotX - partials(1,1) = -Z * MAS2R; //dy/dyp = dy/dRotX - partials(1,2) = +Y * MAS2R; //dz/dyp = dz/dRotX + partials(1, 0) = 0; // dx/dyp = dx/dRotX + partials(1, 1) = -Z * MAS2R; // dy/dyp = dy/dRotX + partials(1, 2) = +Y * MAS2R; // dz/dyp = dz/dRotX - partials(2,0) = +Y * MTS2R; //dx/dut1 = dx/dRotZ - partials(2,1) = -X * MTS2R; //dy/dut1 = dy/dRotZ - partials(2,2) = 0; //dz/dut1 = dz/dRotZ + partials(2, 0) = +Y * MTS2R; // dx/dut1 = dx/dRotZ + partials(2, 1) = -X * MTS2R; // dy/dut1 = dy/dRotZ + partials(2, 2) = 0; // dz/dut1 = dz/dRotZ - return partials; + return partials; } diff --git a/src/cpp/common/erp.hpp b/src/cpp/common/erp.hpp index 681c47e90..6a7e063e4 100644 --- a/src/cpp/common/erp.hpp +++ b/src/cpp/common/erp.hpp @@ -1,16 +1,14 @@ - #pragma once +#include #include #include -#include +#include "common/common.hpp" +#include "common/gTime.hpp" +using std::map; using std::string; using std::vector; -using std::map; - -#include "common.hpp" - constexpr char eopComments[][16] = {"XP (MAS)", "YP (MAS)", "UT1(MTS)"}; @@ -18,119 +16,51 @@ constexpr char eopComments[][16] = {"XP (MAS)", "YP (MAS)", "UT1(MTS)"}; */ struct ERPValues { - GTime time; - - double xp = 0; ///< pole offset (rad) - double yp = 0; ///< pole offset (rad) - double ut1Utc = 0; ///< ut1-utc (s) - double lod = 0; ///< delta length of day (s/day) - - double xpr = 0; ///< pole offset rate (rad/day) - double ypr = 0; ///< pole offset rate (rad/day) - - double xpSigma = 0; - double ypSigma = 0; - double xprSigma = 0; - double yprSigma = 0; - double ut1UtcSigma = 0; - double lodSigma = 0; - - bool isPredicted = false; - bool isFiltered = false; - - ERPValues operator +(const ERPValues& rhs) - { - ERPValues erpv = *this; - - erpv.time += rhs.time; - erpv.xp += rhs.xp; - erpv.yp += rhs.yp; - erpv.ut1Utc += rhs.ut1Utc; - erpv.lod += rhs.lod; - - erpv.xpr += rhs.xpr; - erpv.ypr += rhs.ypr; - - erpv.xpSigma = sqrt(SQR(erpv.xpSigma) + SQR(rhs.xpSigma)); - erpv.ypSigma = sqrt(SQR(erpv.ypSigma) + SQR(rhs.ypSigma)); - erpv.xprSigma = sqrt(SQR(erpv.xprSigma) + SQR(rhs.xprSigma)); - erpv.yprSigma = sqrt(SQR(erpv.yprSigma) + SQR(rhs.yprSigma)); - erpv.ut1UtcSigma = sqrt(SQR(erpv.ut1UtcSigma) + SQR(rhs.ut1UtcSigma)); - erpv.lodSigma = sqrt(SQR(erpv.lodSigma) + SQR(rhs.lodSigma)); - - erpv.isPredicted |= rhs.isPredicted; - - return erpv; - } - - bool operator ==(const ERPValues& rhs) const - { - bool equal = time == rhs.time - && xp == rhs.xp - && yp == rhs.yp - && ut1Utc == rhs.ut1Utc - && lod == rhs.lod - && xpr == rhs.xpr - && ypr == rhs.ypr; - - return equal; - } - - ERPValues operator *(const double scalar) - { - ERPValues erpv = *this; - - erpv.time.bigTime *= scalar; - erpv.xp *= scalar; - erpv.yp *= scalar; - erpv.ut1Utc *= scalar; - erpv.lod *= scalar; - - erpv.xpr *= scalar; - erpv.ypr *= scalar; - - erpv.xpSigma *= scalar; - erpv.ypSigma *= scalar; - erpv.xprSigma *= scalar; - erpv.yprSigma *= scalar; - erpv.ut1UtcSigma *= scalar; - erpv.lodSigma *= scalar; - - return erpv; - } - - string toString(); - string toReadableString(); + GTime time; + + double xp = 0; ///< pole offset (rad) + double yp = 0; ///< pole offset (rad) + double ut1Utc = 0; ///< ut1-utc (s) + double lod = 0; ///< delta length of day (s/day) + + double xpr = 0; ///< pole offset rate (rad/day) + double ypr = 0; ///< pole offset rate (rad/day) + + double xpSigma = 0; + double ypSigma = 0; + double xprSigma = 0; + double yprSigma = 0; + double ut1UtcSigma = 0; + double lodSigma = 0; + + bool isPredicted = false; + bool isFiltered = false; + + ERPValues operator+(const ERPValues& rhs); + bool operator==(const ERPValues& rhs) const; + ERPValues operator*(const double scalar); + + string toString() const; + string toReadableString() const; }; struct ERP { - vector> erpMaps; + vector> erpMaps; - ERPValues filterValues; + ERPValues filterValues; }; struct KFState; -void readErp( - string filename, - ERP& erp); +void readErp(string filename, ERP& erp); -ERPValues getErp( - ERP& erp, - GTime time, - bool useFilter = true); +ERPValues getErp(ERP& erp, GTime time, bool useFilter = true); -void writeErp( - string filename, - ERPValues& erp); +void writeErp(string filename, ERPValues& erp); -void writeErpFromNetwork( - string filename, - KFState& kfState); +void writeErpFromNetwork(string filename, KFState& kfState); -ERPValues getErpFromFilter( - const KFState& kfState); +ERPValues getErpFromFilter(const KFState& kfState); -Matrix3d stationEopPartials( - Vector3d& rRec); +Matrix3d receiverEopPartials(Vector3d& rRec); diff --git a/src/cpp/common/fileLog.cpp b/src/cpp/common/fileLog.cpp index 39ee0f44a..28384c857 100644 --- a/src/cpp/common/fileLog.cpp +++ b/src/cpp/common/fileLog.cpp @@ -1,73 +1,82 @@ - // #pragma GCC optimize ("O0") -#include "fileLog.hpp" - -#include -#include - -#include +#include "common/fileLog.hpp" #include +#include +#include #include - -#include -#include - -using bsoncxx::builder::basic::kvp; - +#include +#include namespace sinks = boost::log::sinks; - +using LogSink = sinks::synchronous_sink; string FileLog::path_log; void FileLog::consume( - boost::log::record_view const& rec, - sinks::basic_formatted_sink_backend::string_type const& log_string) + boost::log::record_view const& rec, + sinks::basic_formatted_sink_backend::string_type const& + log_string +) { - string mess = log_string.c_str(); - boost::erase_all(mess, "\n"); - if (mess.empty()) - return; - - int logLevel = 2; - auto attrs = rec.attribute_values(); - auto sev = attrs[boost::log::trivial::severity].get(); - switch (sev) - { - case boost::log::trivial::trace: logLevel = 5; break; - case boost::log::trivial::debug: logLevel = 4; break; - case boost::log::trivial::info: logLevel = 3; break; - case boost::log::trivial::warning: logLevel = 2; break; - case boost::log::trivial::error: logLevel = 1; break; - case boost::log::trivial::fatal: logLevel = 0; break; - } - - std::ofstream logStream(FileLog::path_log, std::ofstream::app); - - if (!logStream) - return; - - GTime time = timeGet(); - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("label", "message")); - doc.append(kvp("Time", time.to_string())); - doc.append(kvp("level", logLevel)); - doc.append(kvp("str", mess)); - - logStream << bsoncxx::to_json(doc) << "\n"; + string mess = log_string.c_str(); + boost::erase_all(mess, "\n"); + if (mess.empty()) + return; + + int logLevel = 2; + auto attrs = rec.attribute_values(); + auto sev = attrs[boost::log::trivial::severity].get(); + switch (sev) + { + case boost::log::trivial::trace: + logLevel = 5; + break; + case boost::log::trivial::debug: + logLevel = 4; + break; + case boost::log::trivial::info: + logLevel = 3; + break; + case boost::log::trivial::warning: + logLevel = 2; + break; + case boost::log::trivial::error: + logLevel = 1; + break; + case boost::log::trivial::fatal: + logLevel = 0; + break; + } + + std::ofstream logStream(FileLog::path_log, std::ofstream::app); + + if (!logStream) + return; + + GTime time = timeGet(); + + boost::json::object doc = {}; + doc["label"] = "message"; + doc["Time"] = time.to_string(); + doc["level"] = logLevel; + doc["str"] = mess; + + if (json) + logStream << boost::json::serialize(doc) << "\n"; + else + logStream << logLevel << ": " << mess << "\n"; } - -void addFileLog() +void addFileLog(bool json) { - // Construct the sink - using LogSink = sinks::synchronous_sink; + // Construct the sink + + boost::shared_ptr logSink = boost::make_shared(); - boost::shared_ptr logSink = boost::make_shared(); + logSink->locked_backend()->json = json; - // Register the sink in the logging core - boost::log::core::get()->add_sink(logSink); + // Register the sink in the logging core + boost::log::core::get()->add_sink(logSink); } diff --git a/src/cpp/common/fileLog.hpp b/src/cpp/common/fileLog.hpp index 78188a0b0..6135553ea 100644 --- a/src/cpp/common/fileLog.hpp +++ b/src/cpp/common/fileLog.hpp @@ -1,25 +1,26 @@ - #pragma once -#include "acsConfig.hpp" -#include "gTime.hpp" - #include +#include +#include +#include "common/acsConfig.hpp" +#include "common/gTime.hpp" namespace sinks = boost::log::sinks; -#include -#include using std::string; - struct FileLog : public sinks::basic_formatted_sink_backend { - static string path_log; - - void consume( - boost::log::record_view const& rec, - sinks::basic_formatted_sink_backend::string_type const& log_string); + static string path_log; + + bool json = false; + + void consume( + boost::log::record_view const& rec, + sinks::basic_formatted_sink_backend::string_type const& + log_string + ); }; -void addFileLog(); +void addFileLog(bool json); diff --git a/src/cpp/common/gTime.cpp b/src/cpp/common/gTime.cpp index 12b091a1f..6c45cf58e 100644 --- a/src/cpp/common/gTime.cpp +++ b/src/cpp/common/gTime.cpp @@ -1,663 +1,784 @@ - // #pragma GCC optimize ("O0") -#include -#include -#include -#include +#include "common/gTime.hpp" +#include #include +#include +#include +#include +#include "common/acsConfig.hpp" +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/navigation.hpp" using std::ostream; -#include "navigation.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "gTime.hpp" -#include "enums.h" - - -const GTime j2000TT = GEpoch{2000, E_Month::JAN, 1, 11, 58, 55.816 + GPS_SUB_UTC_2000}; // defined in utc 11:58:55.816 -const GTime j2000Utc = GEpoch{2000, E_Month::JAN, 1, 12, 0, 0}; // right answer, wrong reason? todo - -const GTime GPS_t0 = GEpoch{1980, E_Month::JAN, 6, 0, 0, 0}; // gps time reference -const GTime GLO_t0 = GEpoch{1980, E_Month::JAN, 6, 21, 0, 0}; // glo time reference (without leap seconds) -const GTime GAL_t0 = GEpoch{1999, E_Month::AUG, 22, 0, 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) -const GTime BDS_t0 = GEpoch{2006, E_Month::JAN, 1, 0, 0, 0 + GPS_SUB_UTC_2006}; // beidou time reference as gps time - defined in utc 11:58:55.816 - -const int GPS_t0_sub_POSIX_t0 = 315964800; -const double MJD_j2000 = 51544.5; - -const auto POSIX_GPS_t0 = boost::posix_time::from_time_t(GPS_t0_sub_POSIX_t0); - - -const int secondsInWeek = 60 * 60 * 24 * 7; -const int secondsInDay = 60 * 60 * 24; -const long double secondsInDayP = 60 * 60 * 24; - -map> leapSecondMap = -{ - { GEpoch{2017, 1, 1, 0, 0, 18}, 18}, - { GEpoch{2015, 7, 1, 0, 0, 17}, 17}, - { GEpoch{2012, 7, 1, 0, 0, 16}, 16}, - { GEpoch{2009, 1, 1, 0, 0, 15}, 15}, - { GEpoch{2006, 1, 1, 0, 0, 14}, 14}, - { GEpoch{1999, 1, 1, 0, 0, 13}, 13}, - { GEpoch{1997, 7, 1, 0, 0, 12}, 12}, - { GEpoch{1996, 1, 1, 0, 0, 11}, 11}, - { GEpoch{1994, 7, 1, 0, 0, 10}, 10}, - { GEpoch{1993, 7, 1, 0, 0, 9}, 9}, - { GEpoch{1992, 7, 1, 0, 0, 8}, 8}, - { GEpoch{1991, 1, 1, 0, 0, 7}, 7}, - { GEpoch{1990, 1, 1, 0, 0, 6}, 6}, - { GEpoch{1988, 1, 1, 0, 0, 5}, 5}, - { GEpoch{1985, 7, 1, 0, 0, 4}, 4}, - { GEpoch{1983, 7, 1, 0, 0, 3}, 3}, - { GEpoch{1982, 7, 1, 0, 0, 2}, 2}, - { GEpoch{1981, 7, 1, 0, 0, 1}, 1}, - { GEpoch{1980, 1, 6, 0, 0, 0}, 0} +const GTime j2000TT = GEpoch{ + 2000, + static_cast(E_Month::JAN), + 1, + 11, + 58, + 55.816 + GPS_SUB_UTC_2000 +}; // defined in utc 11:58:55.816 +const GTime j2000Utc = + GEpoch{2000, static_cast(E_Month::JAN), 1, 12, 0, 0}; // right answer, wrong reason? todo + +const GTime GPS_t0 = + GEpoch{1980, static_cast(E_Month::JAN), 6, 0, 0, 0}; // gps time reference +const GTime GLO_t0 = GEpoch{ + 1980, + static_cast(E_Month::JAN), + 6, + 21, + 0, + 0 +}; // glo time reference (without leap seconds) +const GTime GAL_t0 = GEpoch{ + 1999, + static_cast(E_Month::AUG), + 22, + 0, + 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) +const GTime BDS_t0 = GEpoch{ + 2006, + static_cast(E_Month::JAN), + 1, + 0, + 0, + 0 + GPS_SUB_UTC_2006 +}; // beidou time reference as gps time + // - defined in utc 11:58:55.816 + +const int GPS_t0_sub_POSIX_t0 = 315964800; +const double MJD_j2000 = 51544.5; + +const auto POSIX_GPS_t0 = boost::posix_time::from_time_t(GPS_t0_sub_POSIX_t0); + +const int secondsInWeek = 60 * 60 * 24 * 7; +const int secondsInDay = 60 * 60 * 24; +const long double secondsInDayP = 60 * 60 * 24; + +map> leapSecondMap = { + {GEpoch{2017, 1, 1, 0, 0, 18}, 18}, + {GEpoch{2015, 7, 1, 0, 0, 17}, 17}, + {GEpoch{2012, 7, 1, 0, 0, 16}, 16}, + {GEpoch{2009, 1, 1, 0, 0, 15}, 15}, + {GEpoch{2006, 1, 1, 0, 0, 14}, 14}, + {GEpoch{1999, 1, 1, 0, 0, 13}, 13}, + {GEpoch{1997, 7, 1, 0, 0, 12}, 12}, + {GEpoch{1996, 1, 1, 0, 0, 11}, 11}, + {GEpoch{1994, 7, 1, 0, 0, 10}, 10}, + {GEpoch{1993, 7, 1, 0, 0, 9}, 9}, + {GEpoch{1992, 7, 1, 0, 0, 8}, 8}, + {GEpoch{1991, 1, 1, 0, 0, 7}, 7}, + {GEpoch{1990, 1, 1, 0, 0, 6}, 6}, + {GEpoch{1988, 1, 1, 0, 0, 5}, 5}, + {GEpoch{1985, 7, 1, 0, 0, 4}, 4}, + {GEpoch{1983, 7, 1, 0, 0, 3}, 3}, + {GEpoch{1982, 7, 1, 0, 0, 2}, 2}, + {GEpoch{1981, 7, 1, 0, 0, 1}, 1}, + {GEpoch{1980, 1, 6, 0, 0, 0}, 0} }; - -ostream& operator <<(ostream& stream, const GTime& time) +ostream& operator<<(ostream& stream, const GTime& time) { - stream << time.to_string(); - return stream; + stream << time.to_string(); + return stream; } -ostream& operator <<(ostream& stream, const Duration& duration) +ostream& operator<<(ostream& stream, const Duration& duration) { - char buff[64]; + char buff[64]; - int decimal = (duration.bigTime - floor(duration.bigTime)) * 100; + int decimal = (duration.bigTime - floor(duration.bigTime)) * 100; - snprintf(buff, sizeof(buff), "%02d:%02d:%02d.%02d", - (int) duration.bigTime / 60 / 60, - (int) duration.bigTime / 60 % 60, - (int) duration.bigTime % 60, - decimal); + snprintf( + buff, + sizeof(buff), + "%02d:%02d:%02d.%02d", + (int)duration.bigTime / 60 / 60, + (int)duration.bigTime / 60 % 60, + (int)duration.bigTime % 60, + decimal + ); - stream << buff; + stream << buff; - return stream; + return stream; } /* convert substring in string to number -* args : char *s I string ("... nnn.nnn ...") -* int i,n I substring position and width -* return : converted number (0.0:error) -*/ -double str2num(const char *s, int i, int n) + * args : char *s I string ("... nnn.nnn ...") + * int i,n I substring position and width + * return : converted number (0.0:error) + */ +double str2num(const char* s, int i, int n) { - double value; - char str[256],*p=str; + double value; + char str[256], *p = str; - if ( i<0 - ||(int)strlen(s)=0;s++) - *p++ = *s == 'd' || *s == 'D' ? 'E' : *s; + for (s += i; *s && --n >= 0; s++) + *p++ = *s == 'd' || *s == 'D' ? 'E' : *s; - *p='\0'; - return sscanf(str, "%lf", &value) == 1 ? value : 0; + *p = '\0'; + return sscanf(str, "%lf", &value) == 1 ? value : 0; } /* convert substring in string to GTime struct -* args : char *s I string ("... yyyy mm dd hh mm ss ...") -* int i,n I substring position and width -* GTime *t O GTime struct -* return : status (0:ok,0>:error)*/ -int str2time( - const char* s, - int i, - int n, - GTime& t, - E_TimeSys tsys) -{ - double ep[6]; - char str[256],*p=str; - - if ( i < 0 - ||(int)strlen(s) < i - ||(int)sizeof(str)-1< i) - { - return -1; - } - - for (s+=i;*s&&--n>=0;) - { - *p++=*s++; - } - *p='\0'; - - int readCount = sscanf(str,"%lf %lf %lf %lf %lf %lf", - ep, - ep+1, - ep+2, - ep+3, - ep+4, - ep+5); - - if (readCount < 6) - { - return -1; - } - - if (ep[0] < 100) - ep[0] += ep[0] < 80 ? 2000 : 1900; - - t = epoch2time(ep, tsys); - - return 0; -} - -GTime yds2time( - const double* yds, - E_TimeSys tsys) -{ - int year = (int) yds[0]; - int doy = (int) yds[1]; - double sec = yds[2]; - - if ( year < 1970 - ||doy < 1 - ||doy > 366) - { - return GTime::noTime(); - } - - int leapDays = (year-1968-1) / 4 // -1968 = last leap year before 1970; -1 = year must end before applying leap-day - - (year-1900-1) / 100 - + (year-1600-1) / 400; - - int days = (year-1970)*365 + leapDays + doy - 1; - - PTime pTime = {}; - pTime.bigTime = days * S_IN_DAY + sec; //.0 to prevent eventual overflow - - GTime time = pTime; - switch (tsys) - { - case E_TimeSys::GPST: break; // nothing to do for now - case E_TimeSys::GST: break; // nothing to do for now - case E_TimeSys::QZSST: break; // nothing to do for now - case E_TimeSys::BDT: { time += GPS_SUB_UTC_2006; } break; - case E_TimeSys::GLONASST: { time -= 10800; } // fallthough to account for leap seconds further - case E_TimeSys::UTC: { UtcTime utcTime; utcTime.bigTime = time.bigTime; time = utcTime; } break; - case E_TimeSys::TAI: { time += GPS_SUB_TAI; } break; - default: - { - BOOST_LOG_TRIVIAL(error) << "Unsupported / Unknown time system: " << tsys._to_string() << ", use GPST by default." << "\n"; - } - } - - return time; -} - -void time2yds( - GTime time, - double* yds, - E_TimeSys tsys) -{ - GEpoch gEpoch; - time2epoch(time, gEpoch.data(), tsys); - - //make new time with only the year of the input one, - GEpoch gEpoch0(2000, 1, 1, 0, 0, 0); - gEpoch0[0] = gEpoch[0]; + * args : char *s I string ("... yyyy mm dd hh mm ss ...") + * int i,n I substring position and width + * GTime *t O GTime struct + * return : status (0:ok,0>:error)*/ +int str2time(const char* s, int i, int n, GTime& t, E_TimeSys tsys) +{ + double ep[6]; + char str[256], *p = str; - //subtract off the years - Duration toy = (GTime)gEpoch - (GTime)gEpoch0; + if (i < 0 || (int)strlen(s) < i || (int)sizeof(str) - 1 < i) + { + return -1; + } - yds[0] = gEpoch0[0]; - yds[1] = toy.to_int() / 86400 + 1; //(doy in bias SINEX (where yds is common) starts at 1) - yds[2] = fmod(toy.to_double(), 86400); + for (s += i; *s && --n >= 0;) + { + *p++ = *s++; + } + *p = '\0'; + + int readCount = + sscanf(str, "%lf %lf %lf %lf %lf %lf", ep, ep + 1, ep + 2, ep + 3, ep + 4, ep + 5); + + if (readCount < 6) + { + return -1; + } + + if (ep[0] < 100) + ep[0] += ep[0] < 80 ? 2000 : 1900; + + t = epoch2time(ep, tsys); + + return 0; } +GTime yds2time(const double* yds, E_TimeSys tsys) +{ + int year = (int)yds[0]; + int doy = (int)yds[1]; + double sec = yds[2]; + + if (year < 1970 || doy < 1 || doy > 366) + { + return GTime::noTime(); + } + + int leapDays = + (year - 1968 - 1) / + 4 // -1968 = last leap year before 1970; -1 = year must end before applying leap-day + - (year - 1900 - 1) / 100 + (year - 1600 - 1) / 400; + + int days = (year - 1970) * 365 + leapDays + doy - 1; + + PTime pTime = {}; + pTime.bigTime = days * S_IN_DAY + sec; //.0 to prevent eventual overflow + + GTime time = pTime; + switch (tsys) + { + case E_TimeSys::GPST: + break; // nothing to do for now + case E_TimeSys::GST: + break; // nothing to do for now + case E_TimeSys::QZSST: + break; // nothing to do for now + case E_TimeSys::BDT: + { + time += GPS_SUB_UTC_2006; + } + break; + case E_TimeSys::GLONASST: + { + time -= 10800; + } // fallthough to account for leap seconds further + case E_TimeSys::UTC: + { + UtcTime utcTime; + utcTime.bigTime = time.bigTime; + time = utcTime; + } + break; + case E_TimeSys::TAI: + { + time += GPS_SUB_TAI; + } + break; + default: + { + BOOST_LOG_TRIVIAL(error) + << "Unsupported / Unknown time system: " << enum_to_string(tsys) + << ", use GPST by default." << "\n"; + } + } + + return time; +} + +void time2yds(GTime time, double* yds, E_TimeSys tsys) +{ + GEpoch gEpoch; + time2epoch(time, gEpoch.data(), tsys); + + // make new time with only the year of the input one, + GEpoch gEpoch0(2000, 1, 1, 0, 0, 0); + gEpoch0[0] = gEpoch[0]; + // subtract off the years + Duration toy = (GTime)gEpoch - (GTime)gEpoch0; + + yds[0] = gEpoch0[0]; + yds[1] = toy.to_int() / 86400 + 1; //(doy in bias SINEX (where yds is common) starts at 1) + yds[2] = fmod(toy.to_double(), 86400); +} UYds::operator GTime() const { - return yds2time(this->data(), E_TimeSys::UTC); -} - - -GTime epoch2time( - const double* ep, - E_TimeSys tsys) -{ - int year = (int) ep[0]; - int mon = (int) ep[1]; - int day = (int) ep[2]; - int hour = (int) ep[3]; - int min = (int) ep[4]; - double sec = ep[5]; - - if ( year < 1970 - || mon < 1 - || mon > 12) - { - return GTime::noTime(); - } - - const int dayOffsetFromMonth[] = {0,31,59,90,120,151,181,212,243,273,304,334}; - int dayOfYear = dayOffsetFromMonth[mon-1] + day; - - if ( ( mon >= 3) //after feb29 - &&( year % 4 == 0) //every 4 years - &&( year % 100 != 0 - ||year % 400 == 0)) - { - dayOfYear++; - } - - double secOfDay = hour * 60 * 60 - + min * 60 - + sec; - - double yds[3]; - yds[0] = year; - yds[1] = dayOfYear; - yds[2] = secOfDay; - GTime time = yds2time(yds, tsys); - - return time; -} - -void time2epoch( - GTime time, - double* ep, - E_TimeSys tsys) -{ - switch (tsys) - { - case E_TimeSys::GPST: break; // nothing to do for now - case E_TimeSys::GST: break; // nothing to do for now - case E_TimeSys::QZSST: break; // nothing to do for now - case E_TimeSys::BDT: { time -= GPS_SUB_UTC_2006; } break; - case E_TimeSys::GLONASST: { time += 10800; } // fallthough to account for leap seconds further - case E_TimeSys::UTC: { UtcTime utcTime = time; time.bigTime = utcTime.bigTime; } break; - case E_TimeSys::TAI: { time -= GPS_SUB_TAI; } break; - default: - { - BOOST_LOG_TRIVIAL(error) << "Unsupported / Unknown time system: " << tsys._to_string() << ", use GPST by default." << "\n"; - } - } - - PTime pTime = time; - - const int mday[] = - { - /* # of days in a month */ - 31,28,31,30,31,30,31,31,30,31,30,31, - 31,28,31,30,31,30,31,31,30,31,30,31, - 31,29,31,30,31,30,31,31,30,31,30,31, - 31,28,31,30,31,30,31,31,30,31,30,31 - }; - - /* leap year if year % 4 == 0 in 1901-2099 */ - - int days = (int) (pTime.bigTime / secondsInDayP); - double remSecs = pTime.bigTime - (time_t) days * secondsInDay; - - int doy = days % (365*4+1); - int mon; - for (mon = 0; mon < 48; mon++) - { - if (doy >= mday[mon]) - doy -= mday[mon]; - else - break; - } - ep[0] = 1970 - + days / 1461 * 4 //1461 = 365.25 * 4 - + mon / 12; - ep[1] = mon % 12 + 1; - ep[2] = doy + 1; - - ep[3] = (int) (remSecs / 3600); remSecs -= ep[3] * 3600; - ep[4] = (int) (remSecs / 60); remSecs -= ep[4] * 60; - ep[5] = (remSecs); + return yds2time(this->data(), E_TimeSys::UTC); +} + +GTime epoch2time(const double* ep, E_TimeSys tsys) +{ + int year = (int)ep[0]; + int mon = (int)ep[1]; + int day = (int)ep[2]; + int hour = (int)ep[3]; + int min = (int)ep[4]; + double sec = ep[5]; + + if (year < 1970 || mon < 1 || mon > 12) + { + return GTime::noTime(); + } + + const int dayOffsetFromMonth[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + int dayOfYear = dayOffsetFromMonth[mon - 1] + day; + + if ((mon >= 3) // after feb29 + && (year % 4 == 0) // every 4 years + && (year % 100 != 0 || year % 400 == 0)) + { + dayOfYear++; + } + + double secOfDay = hour * 60 * 60 + min * 60 + sec; + + double yds[3]; + yds[0] = year; + yds[1] = dayOfYear; + yds[2] = secOfDay; + GTime time = yds2time(yds, tsys); + + return time; } +void time2epoch(GTime time, double* ep, E_TimeSys tsys) +{ + switch (tsys) + { + case E_TimeSys::GPST: + break; // nothing to do for now + case E_TimeSys::GST: + break; // nothing to do for now + case E_TimeSys::QZSST: + break; // nothing to do for now + case E_TimeSys::BDT: + { + time -= GPS_SUB_UTC_2006; + } + break; + case E_TimeSys::GLONASST: + { + time += 10800; + } // fallthough to account for leap seconds further + case E_TimeSys::UTC: + { + UtcTime utcTime = time; + time.bigTime = utcTime.bigTime; + } + break; + case E_TimeSys::TAI: + { + time -= GPS_SUB_TAI; + } + break; + default: + { + BOOST_LOG_TRIVIAL(error) + << "Unsupported / Unknown time system: " << enum_to_string(tsys) + << ", use GPST by default." << "\n"; + } + } + + PTime pTime = time; + + const int mday[] = {/* # of days in a month */ + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 28, 31, 30, + 31, 30, 31, 31, 30, 31, 30, 31, 31, 29, 31, 30, 31, 30, 31, 31, + 30, 31, 30, 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + }; + + /* leap year if year % 4 == 0 in 1901-2099 */ + + int days = (int)(pTime.bigTime / secondsInDayP); + double remSecs = pTime.bigTime - (time_t)days * secondsInDay; + + int doy = days % (365 * 4 + 1); + int mon; + for (mon = 0; mon < 48; mon++) + { + if (doy >= mday[mon]) + doy -= mday[mon]; + else + break; + } + ep[0] = 1970 + days / 1461 * 4 // 1461 = 365.25 * 4 + + mon / 12; + ep[1] = mon % 12 + 1; + ep[2] = doy + 1; + + ep[3] = (int)(remSecs / 3600); + remSecs -= ep[3] * 3600; + ep[4] = (int)(remSecs / 60); + remSecs -= ep[4] * 60; + ep[5] = (remSecs); +} /* convert week and tow in gps time to GTime struct -* args : int week I week number in gps time -* double sec I time of week in gps time (s) -* return : GTime struct -*/ + * args : int week I week number in gps time + * double sec I time of week in gps time (s) + * return : GTime struct + */ GTime gpst2time(int week, double sec) { - GTime t = GPS_t0; + GTime t = GPS_t0; - if ( sec <-1E9 - ||sec > 1E9) - { - sec = 0; - } - t.bigTime += secondsInWeek * week; - t.bigTime += sec; + if (sec < -1E9 || sec > 1E9) + { + sec = 0; + } + t.bigTime += secondsInWeek * week; + t.bigTime += sec; - return t; + return t; } GTime timeGet() { - auto posixUtcNow = boost::posix_time::microsec_clock::universal_time(); - auto duration = posixUtcNow - POSIX_GPS_t0; // UtcTime lines up w/ GTime at GPS_t0 + auto posixUtcNow = boost::posix_time::microsec_clock::universal_time(); + auto duration = posixUtcNow - POSIX_GPS_t0; // UtcTime lines up w/ GTime at GPS_t0 - UtcTime utcTime; - utcTime.bigTime = duration.total_microseconds() * 1e-6; + UtcTime utcTime; + utcTime.bigTime = duration.total_microseconds() * 1e-6; - return utcTime; + return utcTime; } /* GPStime - UTC */ -double leapSeconds( - GTime time) +double leapSeconds(GTime time) { - if (nav.leaps >= 0) - return nav.leaps; + if (nav.leaps >= 0) + return nav.leaps; - if (acsConfig.leap_seconds >= 0) - return acsConfig.leap_seconds; + if (acsConfig.leap_seconds >= 0) + return acsConfig.leap_seconds; - auto it = leapSecondMap.lower_bound(time); - if (it == leapSecondMap.end()) - { - return 0; - } + auto it = leapSecondMap.lower_bound(time); + if (it == leapSecondMap.end()) + { + return 0; + } - auto& [leapTime, seconds] = *it; + auto& [leapTime, seconds] = *it; - return seconds; + return seconds; } - -UtcTime gpst2utc( - GTime time) +UtcTime gpst2utc(GTime time) { - long double leaps = leapSeconds(time); + long double leaps = leapSeconds(time); - UtcTime utcTime; - utcTime.bigTime = time.bigTime - leaps; + UtcTime utcTime; + utcTime.bigTime = time.bigTime - leaps; - return utcTime; + return utcTime; } GTime utc2gpst(UtcTime utcTime) { - GTime gTime; - gTime.bigTime = utcTime.bigTime; + GTime gTime; + gTime.bigTime = utcTime.bigTime; - double leaps = leapSeconds(gTime); - leaps = leapSeconds(gTime + leaps); + double leaps = leapSeconds(gTime); + leaps = leapSeconds(gTime + leaps); - gTime.bigTime += leaps; + gTime.bigTime += leaps; - return gTime; + return gTime; } -string GTime::to_string( - int n) -const +string GTime::to_string(int n) const { - if ( cacheTime == bigTime - &&cacheN == n) - { - return cacheString; - } - - GTime t = *this; - - if (n < 0) n = 0; - else if (n > 12) n = 12; - - double exper = pow(10, n); - - long double val = t.bigTime * exper; - val -= (long int) val; - - if (val > 0.5) - { - t.bigTime += 0.5 / exper; - }; - - GEpoch ep(t); - - char buff[64]; - snprintf(buff, sizeof(buff),"%04.0f-%02.0f-%02.0f %02.0f:%02.0f:%0*.*f", - ep.year, - ep.month, - ep.day, - ep.hour, - ep.min, - n <=0?2:n+3, - n, - ep.sec); - - cacheString = buff; - cacheTime = bigTime; - cacheN = n; - - return cacheString; + if (cacheTime == bigTime && cacheN == n) + { + return cacheString; + } + + GTime t = *this; + + if (n < 0) + n = 0; + else if (n > 12) + n = 12; + + double exper = pow(10, n); + + long double val = t.bigTime * exper; + val -= (long int)val; + + if (val > 0.5) + { + t.bigTime += 0.5 / exper; + }; + + GEpoch ep(t); + + char buff[64]; + snprintf( + buff, + sizeof(buff), + "%04.0f-%02.0f-%02.0f %02.0f:%02.0f:%0*.*f", + ep.year, + ep.month, + ep.day, + ep.hour, + ep.min, + n <= 0 ? 2 : n + 3, + n, + ep.sec + ); + + cacheString = buff; + cacheTime = bigTime; + cacheN = n; + + return cacheString; } -string GTime::to_ISOstring( - int n) -const +string GTime::to_ISOstring(int n) const { - string s = this->to_string(n); - s[10] = 'T'; - return s; + string s = this->to_string(n); + s[10] = 'T'; + return s; } double GTime::to_decYear() const { - UYds yds = *this; + UYds yds = *this; - double year = yds.year; - double doy = yds.doy; - double sod = yds.sod; + double year = yds.year; + double doy = yds.doy; + 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; + // 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; - return year + (doy + sod / secondsInDay) / totalDaysInYear; + return year + (doy + sod / secondsInDay) / totalDaysInYear; } GTime::operator GEpoch() const { - GEpoch gEpoch; - time2epoch(*this, gEpoch.data()); + GEpoch gEpoch; + time2epoch(*this, gEpoch.data()); - return gEpoch; + return gEpoch; } -GTime GTime::floorTime( - double period) const +GTime GTime::floorTime(double period) const { - GTime roundedTime = *this; + GTime roundedTime = *this; - //need separate functions for fractional / whole seconds - //ignore fractions greater than one - if (period < 1) - { - double fractionalSeconds = bigTime - (long int) bigTime; + // need separate functions for fractional / whole seconds + // ignore fractions greater than one + if (period < 1) + { + double fractionalSeconds = bigTime - (long int)bigTime; - int wholePeriods = fractionalSeconds / period; + int wholePeriods = fractionalSeconds / period; - fractionalSeconds = wholePeriods * period; + fractionalSeconds = wholePeriods * period; - roundedTime.bigTime = fractionalSeconds + (long int) bigTime; - } - else - { - //round to nearest chunk by integer arithmetic - roundedTime.bigTime = ((long int) (roundedTime.bigTime / period)) * period; - } + roundedTime.bigTime = fractionalSeconds + (long int)bigTime; + } + else + { + // round to nearest chunk by integer arithmetic + roundedTime.bigTime = ((long int)(roundedTime.bigTime / period)) * period; + } - return roundedTime; + return roundedTime; } /** Returns GTime in "dd-mmm-yyyy hh:mm:ss" format -*/ + */ string GTime::gregString() { - GEpoch epoch = *this; - char buffer[25]; - snprintf(buffer, 25, "%02d-%3s-%04d %02d:%02d:%02d", - (int)epoch.day, - E_Month::_from_integral((int)epoch.month)._to_string(), - (int)epoch.year, - (int)epoch.hour, - (int)epoch.min, - (int)epoch.sec); - return buffer; + GEpoch epoch = *this; + char buffer[25]; + snprintf( + buffer, + 25, + "%02d-%3s-%04d %02d:%02d:%02d", + (int)epoch.day, + enum_to_string(int_to_enum((int)epoch.month)).c_str(), + (int)epoch.year, + (int)epoch.hour, + (int)epoch.min, + (int)epoch.sec + ); + return buffer; } - - /** Use a time of modulus and recent time to calculate the new time */ GTime nearestTime( - GTime referenceEpoch, // - double tom, - GTime nearTime, - int mod) + GTime referenceEpoch, // + double tom, + GTime nearTime, + int mod +) { - time_t seconds = (time_t) (nearTime.bigTime - referenceEpoch.bigTime); - int nearMod = seconds / mod; - int nearTom = seconds % mod; + time_t seconds = (time_t)(nearTime.bigTime - referenceEpoch.bigTime); + int nearMod = seconds / mod; + int nearTom = seconds % mod; - int deltaTom = tom - nearTom; + int deltaTom = tom - nearTom; - int newMod = nearMod; + int newMod = nearMod; - if (deltaTom > + mod / 2) { newMod--; } - else if (deltaTom < - mod / 2) { newMod++; } + if (deltaTom > +mod / 2) + { + newMod--; + } + else if (deltaTom < -mod / 2) + { + newMod++; + } - GTime newTime = referenceEpoch - + newMod * mod - + tom; + GTime newTime = referenceEpoch + newMod * mod + tom; - return newTime; + return newTime; } - GTime::operator MjDateTT() const { - long double thisDate = *this; - long double thenDate = j2000TT; + long double thisDate = *this; + long double thenDate = j2000TT; - long double deltaDate = thisDate - thenDate; - deltaDate /= secondsInDayP; + long double deltaDate = thisDate - thenDate; + deltaDate /= secondsInDayP; - MjDateTT mjd; - mjd.val = MJD_j2000 - + deltaDate; + MjDateTT mjd; + mjd.val = MJD_j2000 + deltaDate; - return mjd; + return mjd; } -GTime::GTime( - MjDateTT mjdTT) +GTime::GTime(MjDateTT mjdTT) { - long double deltaDays = mjdTT.val - MJD_j2000; + long double deltaDays = mjdTT.val - MJD_j2000; - bigTime = j2000TT.bigTime - + deltaDays * secondsInDayP; + bigTime = j2000TT.bigTime + deltaDays * secondsInDayP; } -GTime::GTime( - MjDateUtc mjdUtc) +GTime::GTime(MjDateUtc mjdUtc) { - long double deltaDays = mjdUtc.val - MJD_j2000; + long double deltaDays = mjdUtc.val - MJD_j2000; - bigTime = j2000Utc.bigTime - + deltaDays * secondsInDayP; + bigTime = j2000Utc.bigTime + deltaDays * secondsInDayP; - long double leaps = leapSeconds(*this); + long double leaps = leapSeconds(*this); - bigTime += leaps; + bigTime += leaps; } -MjDateUt1::MjDateUt1( - GTime time, - double ut1_utc) +MjDateUt1::MjDateUt1(GTime time, double ut1_utc) { - MjDateUtc mjdUtc = time; + MjDateUtc mjdUtc = time; - val = mjdUtc.val - + ut1_utc / secondsInDayP; + val = mjdUtc.val + ut1_utc / secondsInDayP; } -MjDateUtc::MjDateUtc( - GTime time) +MjDateUtc::MjDateUtc(GTime time) { - long double thisDate = time; - long double thenDate = j2000Utc; + long double thisDate = time; + long double thenDate = j2000Utc; - long double deltaDate = thisDate - thenDate; - deltaDate /= secondsInDayP; + long double deltaDate = thisDate - thenDate; + deltaDate /= secondsInDayP; - long double leaps = leapSeconds(time); + long double leaps = leapSeconds(time); - val = MJD_j2000 - + deltaDate - - leaps / secondsInDayP; + val = MJD_j2000 + deltaDate - leaps / secondsInDayP; } -GTime::GTime(GWeek gpsWeek, GTow tow) { *this = GPS_t0 + gpsWeek * secondsInWeek + tow; } -GTime::GTime(BWeek bdsWeek, BTow tow) { *this = BDS_t0 + bdsWeek * secondsInWeek + tow; } - -GEpoch ::operator GTime() const{ return epoch2time(this->data()); } -UtcTime ::operator GTime() const{ return utc2gpst(*this); } -GTime ::operator UtcTime() const{ return gpst2utc(*this); } +GTime::GTime(GWeek gpsWeek, GTow tow) +{ + *this = GPS_t0 + gpsWeek * secondsInWeek + tow; +} +GTime::GTime(BWeek bdsWeek, BTow tow) +{ + *this = BDS_t0 + bdsWeek * secondsInWeek + tow; +} +GEpoch ::operator GTime() const +{ + return epoch2time(this->data()); +} +UtcTime ::operator GTime() const +{ + return utc2gpst(*this); +} +GTime ::operator UtcTime() const +{ + return gpst2utc(*this); +} -GTime::GTime(GTow tow, GTime nearTime) { *this = nearestTime(GPS_t0, tow, nearTime, secondsInWeek); } -GTime::GTime(BTow tow, GTime nearTime) { *this = nearestTime(BDS_t0, tow, nearTime, secondsInWeek); } +GTime::GTime(GTow tow, GTime nearTime) +{ + *this = nearestTime(GPS_t0, tow, nearTime, secondsInWeek); +} +GTime::GTime(BTow tow, GTime nearTime) +{ + *this = nearestTime(BDS_t0, tow, nearTime, secondsInWeek); +} -GTime::GTime(RTod tod, GTime nearTime) +GTime::GTime(RTod tod, GTime nearTime) { - RTod nearTod = nearTime; + RTod nearTod = nearTime; - double delta = tod - nearTod; + double delta = tod - nearTod; - while (delta > +secondsInDay / 2) delta -= secondsInDay; - while (delta < -secondsInDay / 2) delta += secondsInDay; + while (delta > +secondsInDay / 2) + delta -= secondsInDay; + while (delta < -secondsInDay / 2) + delta += secondsInDay; - *this = nearTime + delta; + *this = nearTime + delta; } +GTime::operator long double() const +{ + return bigTime; +} -GTime::operator long double() const{ return bigTime; } - -GTime::operator GWeek() const{ Duration seconds = *this - GPS_t0; GWeek gWeek = seconds.to_int() / secondsInWeek; return gWeek; } -GTime::operator BWeek() const{ Duration seconds = *this - BDS_t0; BWeek bWeek = seconds.to_int() / secondsInWeek; return bWeek; } -GTime::operator GTow() const{ Duration seconds = *this - GPS_t0; GTow gTow = fmod(seconds.to_double(), (double)secondsInWeek); return gTow; } -GTime::operator BTow() const{ Duration seconds = *this - BDS_t0; BTow bTow = fmod(seconds.to_double(), (double)secondsInWeek); return bTow; } -GTime::operator RTod() const{ Duration seconds = *this - GLO_t0; RTod rTod = fmod(seconds.to_double()-leapSeconds(*this), (double)secondsInDay); return rTod; } +GTime::operator GWeek() const +{ + Duration seconds = *this - GPS_t0; + GWeek gWeek = seconds.to_int() / secondsInWeek; + return gWeek; +} +GTime::operator BWeek() const +{ + Duration seconds = *this - BDS_t0; + BWeek bWeek = seconds.to_int() / secondsInWeek; + return bWeek; +} +GTime::operator GTow() const +{ + Duration seconds = *this - GPS_t0; + GTow gTow = fmod(seconds.to_double(), (double)secondsInWeek); + return gTow; +} +GTime::operator BTow() const +{ + Duration seconds = *this - BDS_t0; + BTow bTow = fmod(seconds.to_double(), (double)secondsInWeek); + return bTow; +} +GTime::operator RTod() const +{ + Duration seconds = *this - GLO_t0; + RTod rTod = fmod(seconds.to_double() - leapSeconds(*this), (double)secondsInDay); + return rTod; +} -PTime::operator GTime() const{ GTime gTime; gTime.bigTime = bigTime - GPS_t0_sub_POSIX_t0; return gTime;} -GTime::operator PTime() const{ PTime pTime; pTime.bigTime = bigTime + GPS_t0_sub_POSIX_t0; return pTime;} +PTime::operator GTime() const +{ + GTime gTime; + gTime.bigTime = bigTime - GPS_t0_sub_POSIX_t0; + return gTime; +} +GTime::operator PTime() const +{ + PTime pTime; + pTime.bigTime = bigTime + GPS_t0_sub_POSIX_t0; + return pTime; +} -GTime::operator string() const{ return to_string(); } +GTime::operator string() const +{ + return to_string(); +} /** Returns (posix) for current epoch */ boost::posix_time::ptime currentLogptime() { - PTime logtime = tsync.floorTime(acsConfig.rotate_period); + PTime logtime = tsync.floorTime(acsConfig.rotate_period); - boost::posix_time::ptime logptime = boost::posix_time::from_time_t((time_t)logtime.bigTime); + boost::posix_time::ptime logptime = boost::posix_time::from_time_t((time_t)logtime.bigTime); - if ((GTime)logtime == GTime::noTime()) - { - logptime = boost::posix_time::not_a_date_time; - } - return logptime; + if ((GTime)logtime == GTime::noTime()) + { + logptime = boost::posix_time::not_a_date_time; + } + return logptime; } + +double ymdhms2jd(const double time[6]) ///< civil date time [YMDHMS] +{ + double i; + double j; + double yr = time[0]; + double mon = time[1]; + // double day = time[2]; + double hr = time[3]; + double min = time[4]; + double sec = time[5]; + if (yr <= 0 || yr >= 2099) + return 0; + if (mon > 2) + { + i = yr; + j = mon; + } + else + { + i = yr - 1; + j = mon + 12; + } + double day = time[2] + hr / 24 + min / 24 / 60 + sec / secondsInDay; + double jd = floor(365.25 * i) + floor(30.6001 * (j + 1)) + day + 1720981.5; + // BOOST_LOG_TRIVIAL(debug) << "YMDH to JD: year=" << yr << " mon=" << mon << " day=" << day << + // " hour=" << hr << + // ", jd=" << jd; + return jd; +}; \ No newline at end of file diff --git a/src/cpp/common/gTime.hpp b/src/cpp/common/gTime.hpp index 6387e81a4..f2a4294d7 100644 --- a/src/cpp/common/gTime.hpp +++ b/src/cpp/common/gTime.hpp @@ -1,19 +1,15 @@ - #pragma once -#include -#include -#include #include - - #include +#include +#include +#include +#include "common/enums.h" -#include "enums.h" - +using std::array; using std::ostream; using std::string; -using std::array; struct PTime; struct GTime; @@ -24,643 +20,521 @@ struct GTow; struct MjDateTT; struct MjDateUtc; +constexpr int GPS_SUB_UTC_2000 = +13; +constexpr int GPS_SUB_UTC_2006 = +14; +constexpr int GPS_SUB_TAI = -19; +extern const GTime GPS_t0; +extern const double MJD_j2000; +extern const int secondsInDay; -#define GPS_SUB_UTC_2000 +13 -#define GPS_SUB_UTC_2006 +14 -#define GPS_SUB_TAI -19 +string time2str(GTime t, int n); -extern const GTime GPS_t0; -extern const double MJD_j2000; -extern const int secondsInDay; +GTime yds2time(const double* yds, E_TimeSys tsys = E_TimeSys::GPST); -string time2str(GTime t, int n); +void time2yds(GTime time, double* yds, E_TimeSys tsys = E_TimeSys::GPST); -GTime yds2time( - const double* yds, - E_TimeSys tsys = E_TimeSys::GPST); +GTime epoch2time(const double* ep, E_TimeSys tsys = E_TimeSys::GPST); -void time2yds( - GTime time, - double* yds, - E_TimeSys tsys = E_TimeSys::GPST); +void time2epoch(GTime time, double* ep, E_TimeSys tsys = E_TimeSys::GPST); -GTime epoch2time( - const double* ep, - E_TimeSys tsys = E_TimeSys::GPST); - -void time2epoch( - GTime time, - double* ep, - E_TimeSys tsys = E_TimeSys::GPST); - -double leapSeconds( GTime time ); +double leapSeconds(GTime time); struct Int { - int val = 0; - - Int() - { - - } + int val = 0; - Int( - const int& in) - : val {in} - { + Int() {} - } + Int(const int& in) : val{in} {} - operator int() const - { - return val; - } + operator int() const { return val; } - Int& operator =( - const int& in) - { - val = in; - return *this; - } + Int& operator=(const int& in) + { + val = in; + return *this; + } }; struct Double { - double val = 0; - - Double() - { - - } + double val = 0; - Double( - const double& in) - : val {in} - { + Double() {} - } + Double(const double& in) : val{in} {} - operator double() const - { - return val; - } + operator double() const { return val; } - Double& operator =( - const double& in) - { - val = in; - return *this; - } + Double& operator=(const double& in) + { + val = in; + return *this; + } }; - -struct GWeek : Int +struct GWeek : Int { - GWeek( - int in) - { - val = in; - } + GWeek(int in) { val = in; } }; -struct BWeek : Int +struct BWeek : Int { - BWeek( - int in) - { - val = in; - } + BWeek(int in) { val = in; } }; -struct GTow : Double +struct GTow : Double { - GTow( - double in) - { - val = in; - } + GTow(double in) { val = in; } }; -struct BTow : Double +struct BTow : Double { - BTow( - double in) - { - val = in; - } + BTow(double in) { val = in; } }; -struct RTod : Double +struct RTod : Double { - RTod( - double in) - { - val = in; - } + RTod(double in) { val = in; } }; struct Duration { - long double bigTime = 0; - - double to_double() const - { - return (double) bigTime; - } - - long int to_int() const - { - return (long int) bigTime; - } - - bool operator < (const double& t2) const - { - if (this->bigTime < t2) return true; - else return false; - } - - bool operator > (const double& t2) const - { - if (this->bigTime > t2) return true; - else return false; - } - - long double operator - (const Duration& t2) const - { - return this->bigTime - t2.bigTime; - } - - double operator / (const Duration& t2) const - { - return this->bigTime / t2.bigTime; - } - - - friend ostream& operator<<(ostream& os, const Duration& time); + long double bigTime = 0; + + double to_double() const { return (double)bigTime; } + + long int to_int() const { return (long int)bigTime; } + + bool operator<(const double& t2) const + { + if (this->bigTime < t2) + return true; + else + return false; + } + + bool operator>(const double& t2) const + { + if (this->bigTime > t2) + return true; + else + return false; + } + + long double operator-(const Duration& t2) const { return this->bigTime - t2.bigTime; } + + double operator/(const Duration& t2) const { return this->bigTime / t2.bigTime; } + + friend ostream& operator<<(ostream& os, const Duration& time); }; /** Time structure used throughout this software -*/ + */ struct GTime { - mutable int cacheN = 0; - mutable long double cacheTime = -1; - mutable string cacheString; + mutable int cacheN = 0; + mutable long double cacheTime = -1; + mutable string cacheString; - long double bigTime = 0; + long double bigTime = 0; + /** Uninitialised time for comparisons + */ + static GTime noTime() + { + GTime nothing; + return nothing; + } - /** Uninitialised time for comparisons - */ - static GTime noTime() - { - GTime nothing; - return nothing; - } + GTime(GTow tow, GTime nearTime); - GTime( - GTow tow, - GTime nearTime); + GTime(BTow tow, GTime nearTime); - GTime( - BTow tow, - GTime nearTime); + GTime(RTod tod, GTime nearTime); - GTime( - RTod tod, - GTime nearTime); + string to_string(int n = 2) const; - string to_string(int n = 2) const; - - string to_ISOstring(int n = 2) const; + string to_ISOstring(int n = 2) const; - double to_decYear() const; - - bool operator == (const GTime& t2) const - { - if (this->bigTime != t2.bigTime) return false; - else return true; - } + double to_decYear() const; - bool operator != (const GTime& t2) const - { - return !(*this == t2); - } + bool operator==(const GTime& t2) const + { + if (this->bigTime != t2.bigTime) + return false; + else + return true; + } - bool operator < (const GTime& t2) const - { - if (this->bigTime < t2.bigTime) return true; - else return false; - } + bool operator!=(const GTime& t2) const { return !(*this == t2); } - bool operator > (const GTime& t2) const - { - if (this->bigTime > t2.bigTime) return true; - else return false; - } + bool operator<(const GTime& t2) const + { + if (this->bigTime < t2.bigTime) + return true; + else + return false; + } - bool operator >= (const GTime& t2) const - { - if (*this > t2) return true; - if (*this == t2) return true; - else return false; - } + bool operator>(const GTime& t2) const + { + if (this->bigTime > t2.bigTime) + return true; + else + return false; + } - friend ostream& operator<<(ostream& os, const GTime& time); + bool operator>=(const GTime& t2) const + { + if (*this > t2) + return true; + if (*this == t2) + return true; + else + return false; + } - GTime operator +(const double t) const - { - GTime gTime = *this; + friend ostream& operator<<(ostream& os, const GTime& time); - gTime.bigTime += t; + GTime operator+(const double t) const + { + GTime gTime = *this; - return gTime; - } - - GTime operator +(const int t) const - { - GTime gTime = *this; - - gTime.bigTime += t; - - return gTime; - } - - GTime operator +(const Duration duration) const - { - GTime gTime = *this; - - gTime.bigTime += duration.bigTime; - - return gTime; - } - - GTime& operator+=(const double rhs) - { - *this = *this + rhs; - return *this; - } - - GTime& operator-=(const double rhs) - { - *this = *this - rhs; - return *this; - } + gTime.bigTime += t; - Duration operator -(const GTime t) const - { - Duration duration; - duration.bigTime = bigTime - t.bigTime; - - return duration; - } - - GTime operator -(const double t) const - { - GTime time = *this + (-t); - return time; - } - - GTime operator -(const Duration duration) const - { - GTime gTime = *this; - gTime.bigTime -= duration.bigTime; - return gTime; - } - - GTime& operator++(int) - { - this->bigTime++; - return *this; - } - - GTime() - { - - } - - GTime( - GWeek gpsWeek, - GTow tow); - - GTime( - BWeek bdsWeek, - BTow tow); - - GTime( - MjDateTT mjdTT); - - GTime( - MjDateUtc mjdUtc); - - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - ar & bigTime; - } - - GTime floorTime( - double period) const; + return gTime; + } - string gregString(); + GTime operator+(const int t) const + { + GTime gTime = *this; - operator long double() const; - operator MjDateTT() const; - operator GEpoch() const; - operator UtcTime() const; - operator GWeek() const; - operator BWeek() const; - operator GTow() const; - operator BTow() const; - operator PTime() const; - operator string() const; - operator RTod() const; + gTime.bigTime += t; + + return gTime; + } + + GTime operator+(const Duration duration) const + { + GTime gTime = *this; + + gTime.bigTime += duration.bigTime; + + return gTime; + } + + GTime& operator+=(const double rhs) + { + *this = *this + rhs; + return *this; + } + + GTime& operator-=(const double rhs) + { + *this = *this - rhs; + return *this; + } + + Duration operator-(const GTime t) const + { + Duration duration; + duration.bigTime = bigTime - t.bigTime; + + return duration; + } + + GTime operator-(const double t) const + { + GTime time = *this + (-t); + return time; + } + + GTime operator-(const Duration duration) const + { + GTime gTime = *this; + gTime.bigTime -= duration.bigTime; + return gTime; + } + + GTime& operator++(int) + { + this->bigTime++; + return *this; + } + + GTime() {} + + GTime(GWeek gpsWeek, GTow tow); + + GTime(BWeek bdsWeek, BTow tow); + + GTime(MjDateTT mjdTT); + + GTime(MjDateUtc mjdUtc); + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + ar & bigTime; + } + + GTime floorTime(double period) const; + + string gregString(); + + operator long double() const; + operator MjDateTT() const; + operator GEpoch() const; + operator UtcTime() const; + operator GWeek() const; + operator BWeek() const; + operator GTow() const; + operator BTow() const; + operator PTime() const; + operator string() const; + operator RTod() const; }; struct PTime { - long double bigTime = 0; + long double bigTime = 0; - PTime() - { + PTime() {} - } - - operator GTime() const; + operator GTime() const; }; - GTime timeGet(); - struct MjDateUtc { - long double val; - - MjDateUtc() - { + long double val; - } + MjDateUtc() {} - MjDateUtc( - GTime time); + MjDateUtc(GTime time); - double to_double() const - { - return (double) val; - } + double to_double() const { return (double)val; } }; - struct MjDateUt1 { - long double val; - - MjDateUt1() - { + long double val; - } + MjDateUt1() {} - MjDateUt1( - GTime time, - double ut1_utc); + MjDateUt1(GTime time, double ut1_utc); - double to_double() const - { - return (double) val; - } + double to_double() const { return (double)val; } - double to_j2000() const - { - return (double) (val - MJD_j2000); - } + double to_j2000() const { return (double)(val - MJD_j2000); } }; struct MjDateTT { - long double val; + long double val; - double to_double() const - { - return (double) val; - } + double to_double() const { return (double)val; } - double to_j2000() const - { - return (double) (val - MJD_j2000); - } + double to_j2000() const { return (double)(val - MJD_j2000); } }; struct UtcTime { - long double bigTime; // Eugene: bigTime can be ambiguous, e.g. 1167264000.5, never know if GPST is 2017-01-01 00:00:17.5 or 2017-01-01 00:00:18.5 + long double bigTime; // Eugene: bigTime can be ambiguous, e.g. 1167264000.5, never know if GPST + // is 2017-01-01 00:00:17.5 or 2017-01-01 00:00:18.5 - UtcTime operator +(const double t) const - { - UtcTime time = *this; - - time.bigTime += t; + UtcTime operator+(const double t) const + { + UtcTime time = *this; - return time; - } + time.bigTime += t; - string to_string(int n = 2) const - { - GTime gTime; - gTime.bigTime = this->bigTime; - string str = gTime.to_string(n); - str += "Z"; - str[10]='T'; - return str; - } + return time; + } - string to_ISOstring(int n = 2) const - { - GTime gTime; - gTime.bigTime = this->bigTime; + string to_string(int n = 2) const + { + GTime gTime; + gTime.bigTime = this->bigTime; + string str = gTime.to_string(n); + str += "Z"; + str[10] = 'T'; + return str; + } - return gTime.to_ISOstring(n)+'Z'; - } + string to_ISOstring(int n = 2) const + { + GTime gTime; + gTime.bigTime = this->bigTime; - UtcTime() - { + return gTime.to_ISOstring(n) + 'Z'; + } - } + UtcTime() {} - operator GTime() const; + operator GTime() const; }; struct GEpoch : array { - GTime toGTime() const; - - operator GTime() const; - - double& year; - double& month; - double& day; - double& hour; - double& min; - double& sec; - - GEpoch( - double yearVal = 0, - double monthVal = 0, - double dayVal = 0, - double hourVal = 0, - double minVal = 0, - double secVal = 0) - : year { (*this)[0]}, - month { (*this)[1]}, - day { (*this)[2]}, - hour { (*this)[3]}, - min { (*this)[4]}, - sec { (*this)[5]} - { - year = yearVal; - month = monthVal; - day = dayVal; - hour = hourVal; - min = minVal; - sec = secVal; - } - - GEpoch( - const GEpoch& other) - : year { (*this)[0]}, - month { (*this)[1]}, - day { (*this)[2]}, - hour { (*this)[3]}, - min { (*this)[4]}, - sec { (*this)[5]} - { - //special copy constructor to deal with aliases - year = other.year; - month = other.month; - day = other.day; - hour = other.hour; - min = other.min; - sec = other.sec; - } - - GEpoch& operator = ( - const GEpoch& other) - { - year = other.year; - month = other.month; - day = other.day; - hour = other.hour; - min = other.min; - sec = other.sec; - - return *this; - } -}; + GTime toGTime() const; + + operator GTime() const; + + double& year; + double& month; + double& day; + double& hour; + double& min; + double& sec; + + GEpoch( + double yearVal = 0, + double monthVal = 0, + double dayVal = 0, + double hourVal = 0, + double minVal = 0, + double secVal = 0 + ) + : year{(*this)[0]}, + month{(*this)[1]}, + day{(*this)[2]}, + hour{(*this)[3]}, + min{(*this)[4]}, + sec{(*this)[5]} + { + year = yearVal; + month = monthVal; + day = dayVal; + hour = hourVal; + min = minVal; + sec = secVal; + } + GEpoch(const GEpoch& other) + : year{(*this)[0]}, + month{(*this)[1]}, + day{(*this)[2]}, + hour{(*this)[3]}, + min{(*this)[4]}, + sec{(*this)[5]} + { + // special copy constructor to deal with aliases + year = other.year; + month = other.month; + day = other.day; + hour = other.hour; + min = other.min; + sec = other.sec; + } + + GEpoch& operator=(const GEpoch& other) + { + year = other.year; + month = other.month; + day = other.day; + hour = other.hour; + min = other.min; + sec = other.sec; + + return *this; + } +}; struct UYds : array { - double& year; - double& doy; - double& sod; - - UYds( - double yearval = 0, - double doyVal = 0, - double sodVal = 0) - : year{ (*this)[0]}, - doy { (*this)[1]}, - sod { (*this)[2]} - { - year = yearval; - doy = doyVal; - sod = sodVal; - } - - UYds( - const UYds& yds) - : year{ (*this)[0]}, - doy { (*this)[1]}, - sod { (*this)[2]} + double& year; + double& doy; + double& sod; + + UYds(double yearval = 0, double doyVal = 0, double sodVal = 0) + : year{(*this)[0]}, doy{(*this)[1]}, sod{(*this)[2]} { - //special copy constructor to deal with aliases - year = yds.year; - doy = yds.doy; - sod = yds.sod; + year = yearval; + doy = doyVal; + sod = sodVal; } - UYds& operator = ( - const UYds& other) - { - year = other.year; - doy = other.doy; - sod = other.sod; - - return *this; - } - - UYds( - const GTime& time) - : year{ (*this)[0]}, - doy { (*this)[1]}, - sod { (*this)[2]} - { - time2yds(time, this->data(), E_TimeSys::UTC); - } - - UYds& operator +=( - const double offset) - { - sod += offset; - while (sod > secondsInDay) { sod -= secondsInDay; doy++; } - while (sod < 0) { sod += secondsInDay; doy--; } - - while (doy > 366) { doy -= 365; year++; } - while (doy < 1) { doy += 365; year--; } - - return *this; - } - - UYds& operator = ( - const GTime& time) - { - *this = UYds(time); - - return *this; - } - - operator GTime() const; -}; + UYds(const UYds& yds) : year{(*this)[0]}, doy{(*this)[1]}, sod{(*this)[2]} + { + // special copy constructor to deal with aliases + year = yds.year; + doy = yds.doy; + sod = yds.sod; + } -UtcTime gpst2utc (GTime t); -GTime utc2gpst (UtcTime t); + UYds& operator=(const UYds& other) + { + year = other.year; + doy = other.doy; + sod = other.sod; + + return *this; + } + + UYds(const GTime& time) : year{(*this)[0]}, doy{(*this)[1]}, sod{(*this)[2]} + { + time2yds(time, this->data(), E_TimeSys::UTC); + } + + UYds& operator+=(const double offset) + { + sod += offset; + while (sod > secondsInDay) + { + sod -= secondsInDay; + doy++; + } + while (sod < 0) + { + sod += secondsInDay; + doy--; + } + + while (doy > 366) + { + doy -= 365; + year++; + } + while (doy < 1) + { + doy += 365; + year--; + } + + return *this; + } + + UYds& operator=(const GTime& time) + { + *this = UYds(time); + + return *this; + } + + operator GTime() const; +}; -double str2num(const char *s, int i, int n); +UtcTime gpst2utc(GTime t); +GTime utc2gpst(UtcTime t); -GTime gpst2time(int week, double sec); -double time2gpst(GTime t, int *week = nullptr); +double str2num(const char* s, int i, int n); -GTime bdt2time(int week, double sec); -double time2bdt(GTime t, int *week = nullptr); +GTime gpst2time(int week, double sec); +double time2gpst(GTime t, int* week = nullptr); -int str2time( - const char* s, - int i, - int n, - GTime& t, - E_TimeSys tsys = E_TimeSys::GPST); +GTime bdt2time(int week, double sec); +double time2bdt(GTime t, int* week = nullptr); -void jd2ymdhms(const double jd, double *ep); +int str2time(const char* s, int i, int n, GTime& t, E_TimeSys tsys = E_TimeSys::GPST); -double ymdhms2jd(const double time[6]); +void jd2ymdhms(const double jd, double* ep); +double ymdhms2jd(const double time[6]); -GTime nearestTime( - GTime referenceEpoch, - double tom, - GTime nearTime, - int mod); +GTime nearestTime(GTime referenceEpoch, double tom, GTime nearTime, int mod); boost::posix_time::ptime currentLogptime(); diff --git a/src/cpp/common/gpx.cpp b/src/cpp/common/gpx.cpp index b3f173fcd..30c17a2cc 100644 --- a/src/cpp/common/gpx.cpp +++ b/src/cpp/common/gpx.cpp @@ -1,30 +1,23 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -FileType GPX__() -{ - -} - #include - -#include "coordinates.hpp" -#include "constants.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "trace.hpp" - -#include #include +#include +#include "architectureDocs.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/gTime.hpp" +#include "common/receiver.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" -using std::string; using std::map; +using std::string; + +FileType GPX__() {} -map gpxEndOfContentPositionMap; +map gpxEndOfContentPositionMap; string gpxHeader = R"GPXHEADER( @@ -46,186 +39,199 @@ string gpxFinaliser = R"GPXHEADER( )GPXHEADER"; -void writeGPXHeader( - Trace& output, - string name, - GTime time) +void writeGPXHeader(Trace& output, string name, GTime time) { - if (name.empty()) - { - name = "Track"; - } - - output << gpxHeader; - output << ""; //todo aaron, check format, different to below - output << " \n"; - output << "" - << "" << name << "\n" - << " \n"; + if (name.empty()) + { + name = "Track"; + } + + output << gpxHeader; + output << ""; // todo aaron, check format, different to below + output << " \n"; + output << "" << "" << name << "\n" + << " \n"; } - struct XmlCloser { - Trace& trace; - string id; - - XmlCloser( - Trace& trace, - string id) - : trace {trace}, - id {id} - { - trace << "<" << id << ">"; - } - - ~XmlCloser() - { - trace << ""; - } - - template - Trace& operator<<( - const TYPE& content) - { - trace << content; - - return trace; - } + Trace& trace; + string id; + + XmlCloser(Trace& trace, string id) : trace{trace}, id{id} { trace << "<" << id << ">"; } + + ~XmlCloser() { trace << ""; } + + template + Trace& operator<<(const TYPE& content) + { + trace << content; + + return trace; + } }; -void writeGPXEntry( - Trace& output, - Receiver& rec, - KFState& kfState) +void writeGPXEntry(Trace& output, Receiver& rec, KFState& kfState) { - VectorEcef apriori = rec.aprioriPos; - VectorEcef xyz = apriori; - VectorEcef var; - VectorEcef covar; - - int covIdx = 0; - for (auto& [kfKey, index] : kfState.kfIndexMap) - { - if ( kfKey.type != KF::REC_POS - ||kfKey.str != rec.id) - { - continue; - } - - xyz[kfKey.num] = kfState.x(index); - var[kfKey.num] = kfState.P(index, index); - - for (auto& [kfKey2, index2] : kfState.kfIndexMap) - { - if ( kfKey2.type != KF::REC_POS - || kfKey2.str != rec.id) - { - continue; - } - - if (kfKey2.num > kfKey.num) - { - covar(covIdx++) = kfState.P(index, index2); - } - } - } - - VectorPos pos = ecef2pos(xyz); - - output << std::setprecision(11); - - output - << " "; - - { XmlCloser(output, "ele") << pos.hgt(); } - { UtcTime utc (kfState.time); - XmlCloser(output, "time") << utc.to_ISOstring(3); - } - - { - auto extensions = XmlCloser(output, "extensions"); - - { - XmlCloser(output, "time") << kfState.time.to_ISOstring(3); - } - { - auto pos = XmlCloser(output, "pos"); - {XmlCloser(output, "x") << xyz.x(); } - {XmlCloser(output, "y") << xyz.y(); } - {XmlCloser(output, "z") << xyz.z(); } - } - { - auto pos = XmlCloser(output, "variances"); - {XmlCloser(output, "xx") << var.x(); } - {XmlCloser(output, "yy") << var.y(); } - {XmlCloser(output, "zz") << var.z(); } - {XmlCloser(output, "xy") << covar(0); } - {XmlCloser(output, "xz") << covar(1); } - {XmlCloser(output, "yz") << covar(2); } - } - { - auto pos = XmlCloser(output, "apriori"); - {XmlCloser(output, "x") << apriori.x(); } - {XmlCloser(output, "y") << apriori.y(); } - {XmlCloser(output, "z") << apriori.z(); } - } - - bool found = true; - Quaterniond quat; - for (int i = 0; i < 4; i++) - { - KFKey kfKey; - kfKey.type = KF::ORIENTATION; - kfKey.str = rec.id; - kfKey.num = i; - - if (i == 0) found &= kfState.getKFValue(kfKey, quat.w()); - if (i == 1) found &= kfState.getKFValue(kfKey, quat.x()); - if (i == 2) found &= kfState.getKFValue(kfKey, quat.y()); - if (i == 3) found &= kfState.getKFValue(kfKey, quat.z()); - } - - if (found) - { - { XmlCloser(output, "Ex") << (quat * Vector3d::UnitX()).transpose(); } - { XmlCloser(output, "Ey") << (quat * Vector3d::UnitY()).transpose(); } - { XmlCloser(output, "Ez") << (quat * Vector3d::UnitZ()).transpose(); } - { XmlCloser(output, "quat") << quat; } - } - } - - output << "\n"; + VectorEcef apriori = rec.aprioriPos; + VectorEcef xyz = apriori; + VectorEcef var; + VectorEcef covar; + + int covIdx = 0; + for (auto& [kfKey, index] : kfState.kfIndexMap) + { + if (kfKey.type != KF::REC_POS || kfKey.str != rec.id) + { + continue; + } + + xyz[kfKey.num] = kfState.x(index); + var[kfKey.num] = kfState.P(index, index); + + for (auto& [kfKey2, index2] : kfState.kfIndexMap) + { + if (kfKey2.type != KF::REC_POS || kfKey2.str != rec.id) + { + continue; + } + + if (kfKey2.num > kfKey.num) + { + covar(covIdx++) = kfState.P(index, index2); + } + } + } + + VectorPos pos = ecef2pos(xyz); + + output << std::setprecision(11); + + output << " "; + + { + XmlCloser(output, "ele") << pos.hgt(); + } + { + UtcTime utc(kfState.time); + XmlCloser(output, "time") << utc.to_ISOstring(3); + } + + { + auto extensions = XmlCloser(output, "extensions"); + + { + XmlCloser(output, "time") << kfState.time.to_ISOstring(3); + } + { + auto pos = XmlCloser(output, "pos"); + { + XmlCloser(output, "x") << xyz.x(); + } + { + XmlCloser(output, "y") << xyz.y(); + } + { + XmlCloser(output, "z") << xyz.z(); + } + } + { + auto pos = XmlCloser(output, "variances"); + { + XmlCloser(output, "xx") << var.x(); + } + { + XmlCloser(output, "yy") << var.y(); + } + { + XmlCloser(output, "zz") << var.z(); + } + { + XmlCloser(output, "xy") << covar(0); + } + { + XmlCloser(output, "xz") << covar(1); + } + { + XmlCloser(output, "yz") << covar(2); + } + } + { + auto pos = XmlCloser(output, "apriori"); + { + XmlCloser(output, "x") << apriori.x(); + } + { + XmlCloser(output, "y") << apriori.y(); + } + { + XmlCloser(output, "z") << apriori.z(); + } + } + + bool found = true; + Quaterniond quat; + for (int i = 0; i < 4; i++) + { + KFKey kfKey; + kfKey.type = KF::ORIENTATION; + kfKey.str = rec.id; + kfKey.num = i; + + if (i == 0) + found = found && (kfState.getKFValue(kfKey, quat.w()) != E_Source::NONE); + if (i == 1) + found = found && (kfState.getKFValue(kfKey, quat.x()) != E_Source::NONE); + if (i == 2) + found = found && (kfState.getKFValue(kfKey, quat.y()) != E_Source::NONE); + if (i == 3) + found = found && (kfState.getKFValue(kfKey, quat.z()) != E_Source::NONE); + } + + if (found) + { + { + XmlCloser(output, "Ex") << (quat * Vector3d::UnitX()).transpose(); + } + { + XmlCloser(output, "Ey") << (quat * Vector3d::UnitY()).transpose(); + } + { + XmlCloser(output, "Ez") << (quat * Vector3d::UnitZ()).transpose(); + } + { + XmlCloser(output, "quat") << quat; + } + } + } + + output << "\n"; } -void writeGPX( - string filename, - KFState& kfState, - Receiver& rec) +void writeGPX(string filename, KFState& kfState, Receiver& rec) { - std::ofstream output(filename, std::fstream::in | std::fstream::out); - if (!output) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Error opening GPX file '" << filename << "'\n"; - return; - } + std::ofstream output(filename, std::fstream::in | std::fstream::out); + if (!output.is_open()) + { + BOOST_LOG_TRIVIAL(warning) << "Error opening GPX file '" << filename << "'\n"; + return; + } - output.seekp(0, output.end); // seek to end of file + output.seekp(0, output.end); // seek to end of file - if (output.tellp() == 0) - { - writeGPXHeader(output, rec.id, kfState.time); - gpxEndOfContentPositionMap[filename] = output.tellp(); - } + if (output.tellp() == 0) + { + writeGPXHeader(output, rec.id, kfState.time); + gpxEndOfContentPositionMap[filename] = output.tellp(); + } - output.seekp(gpxEndOfContentPositionMap[filename]); + output.seekp(gpxEndOfContentPositionMap[filename]); - writeGPXEntry(output, rec, kfState); + writeGPXEntry(output, rec, kfState); - gpxEndOfContentPositionMap[filename] = output.tellp(); + gpxEndOfContentPositionMap[filename] = output.tellp(); - output << gpxFinaliser; + output << gpxFinaliser; } diff --git a/src/cpp/common/gpx.hpp b/src/cpp/common/gpx.hpp index 1eb0b2f7c..f1ae3d900 100644 --- a/src/cpp/common/gpx.hpp +++ b/src/cpp/common/gpx.hpp @@ -1,10 +1,4 @@ - #pragma once - struct KFState; struct Receiver; - -void writeGPX( - string filename, - KFState& kfState, - Receiver& rec); +void writeGPX(string filename, KFState& kfState, Receiver& rec); \ No newline at end of file diff --git a/src/cpp/common/icdDecoder.hpp b/src/cpp/common/icdDecoder.hpp index b91dc4bcd..d5f699be3 100644 --- a/src/cpp/common/icdDecoder.hpp +++ b/src/cpp/common/icdDecoder.hpp @@ -1,321 +1,301 @@ - #pragma once -#include "rtcmDecoder.hpp" -#include "ephemeris.hpp" - +#include "common/ephemeris.hpp" +#include "common/rtcmDecoder.hpp" -signed int gpsBitSFromWord( - vector& words, - int word, - int offset, - int len); +signed int gpsBitSFromWord(vector& words, int word, int offset, int len); -unsigned int gpsBitUFromWord( - vector& words, - int word, - int offset, - int len); +unsigned int gpsBitUFromWord(vector& words, int word, int offset, int len); struct IcdDecoder { - map>> subframeMap; - - /* decode Galileo I/NAV ephemeris ---------------------------------------------- - * decode Galileo I/NAV (ref [5] 4.3) - * args : unsigned char *buff I Galileo I/NAV subframe bits - * buff[ 0-15]: I/NAV word type 0 (128 bit) - * buff[16-31]: I/NAV word type 1 - * buff[32-47]: I/NAV word type 2 - * buff[48-63]: I/NAV word type 3 - * buff[64-79]: I/NAV word type 4 - * buff[80-95]: I/NAV word type 5 - * eph_t *eph IO ephemeris structure - * return : status (1:ok,0:error) - *-----------------------------------------------------------------------------*/ -// int decode_gal_inav( -// const unsigned char* buff, -// eph_t* eph) -// { -// double tow, toc, tt, sqrtA; -// int i, time_f, week, svid, e5b_hs, e1b_hs, e5b_dvs, e1b_dvs, type[6], iod_nav[4]; -// -// i = 0; /* word type 0 */ -// type[0] = getbitu(buff, i, 6); i += 6; -// time_f = getbitu(buff, i, 2); i += 2 + 88; -// week = getbitu(buff, i, 12); i += 12; /* gst-week */ -// tow = getbitu(buff, i, 20); -// -// i = 128; /* word type 1 */ -// type[1] = getbitu(buff, i, 6); i += 6; -// iod_nav[0] = getbitu(buff, i, 10); i += 10; -// eph->toes = getbitu(buff, i, 14) * 60.0; i += 14; -// eph->M0 = getbits(buff, i, 32) * P2_31 * SC2RAD; i += 32; -// eph->e = getbitu(buff, i, 32) * P2_33; i += 32; -// sqrtA = getbitu(buff, i, 32) * P2_19; -// -// i = 128 * 2; /* word type 2 */ -// type[2] = getbitu(buff, i, 6); i += 6; -// iod_nav[1] = getbitu(buff, i, 10); i += 10; -// eph->OMG0 = getbits(buff, i, 32) * P2_31 * SC2RAD; i += 32; -// eph->i0 = getbits(buff, i, 32) * P2_31 * SC2RAD; i += 32; -// eph->omg = getbits(buff, i, 32) * P2_31 * SC2RAD; i += 32; -// eph->idot = getbits(buff, i, 14) * P2_43 * SC2RAD; -// -// i = 128 * 3; /* word type 3 */ -// type[3] = getbitu(buff, i, 6); i += 6; -// iod_nav[2] = getbitu(buff, i, 10); i += 10; -// eph->OMGd = getbits(buff, i, 24) * P2_43 * SC2RAD; i += 24; -// eph->deln = getbits(buff, i, 16) * P2_43 * SC2RAD; i += 16; -// eph->cuc = getbits(buff, i, 16) * P2_29; i += 16; -// eph->cus = getbits(buff, i, 16) * P2_29; i += 16; -// eph->crc = getbits(buff, i, 16) * P2_5; i += 16; -// eph->crs = getbits(buff, i, 16) * P2_5; i += 16; -// eph->sva = getbitu(buff, i, 8); -// -// i = 128 * 4; /* word type 4 */ -// type[4] = getbitu(buff, i, 6); i += 6; -// iod_nav[3] = getbitu(buff, i, 10); i += 10; -// svid = getbitu(buff, i, 6); i += 6; -// eph->cic = getbits(buff, i, 16) * P2_29; i += 16; -// eph->cis = getbits(buff, i, 16) * P2_29; i += 16; -// toc = getbitu(buff, i, 14) * 60.0; i += 14; -// eph->f0 = getbits(buff, i, 31) * P2_34; i += 31; -// eph->f1 = getbits(buff, i, 21) * P2_46; i += 21; -// eph->f2 = getbits(buff, i, 6) * P2_59; -// -// i = 128 * 5; /* word type 5 */ -// type[5] = getbitu(buff, i, 6); i += 6 + 41; -// eph->tgd[0] = getbits(buff, i, 10) * P2_32; i += 10; /* BGD E5a/E1 */ -// eph->tgd[1] = getbits(buff, i, 10) * P2_32; i += 10; /* BGD E5b/E1 */ -// e5b_hs = getbitu(buff, i, 2); i += 2; -// e1b_hs = getbitu(buff, i, 2); i += 2; -// e5b_dvs = getbitu(buff, i, 1); i += 1; -// e1b_dvs = getbitu(buff, i, 1); -// -// /* test word types */ -// if ( type[0] != 0 -// || type[1] != 1 -// || type[2] != 2 -// || type[3] != 3 -// || type[4] != 4) -// { -// trace(3, "decode_gal_inav error: type=%d %d %d %d %d\n", type[0], type[1], type[2], type[3], type[4]); -// return 0; -// } -// -// /* test word type 0 time field */ -// if (time_f != 2) -// { -// trace(3, "decode_gal_inav error: word0-time=%d\n", time_f); -// return 0; -// } -// -// /* test consistency of iod_nav */ -// if ( iod_nav[0] != iod_nav[1] -// || iod_nav[0] != iod_nav[2] -// || iod_nav[0] != iod_nav[3]) -// { -// trace(3, "decode_gal_inav error: ionav=%d %d %d %d\n", iod_nav[0], iod_nav[1], iod_nav[2], iod_nav[3]); -// return 0; -// } -// -// if (!(eph->sat = satno(SYS_GAL, svid))) -// { -// trace(2, "decode_gal_inav svid error: svid=%d\n", svid); -// return 0; -// } -// -// eph->A = sqrtA * sqrtA; -// eph->iode = eph->iodc = iod_nav[0]; -// eph->svh = (e5b_hs << 7) | (e5b_dvs << 6) | (e1b_hs << 1) | e1b_dvs; -// eph->ttr = gst2time(week, tow); -// tt = timediff(gst2time(week, eph->toes), eph->ttr); /* week complient to toe */ -// -// if (tt > +302400.0) week--; -// else if (tt < -302400.0) week++; -// -// eph->toe = gst2time(week, eph->toes); -// eph->toc = gst2time(week, toc); -// eph->week = week + 1024; /* gal-week = gst-week + 1024 */ -// eph->code = 1; /* data source = I/NAV E1B */ -// -// return 1; -// } - - bool decodeGpsTlmWord( - vector& words, - Eph& eph) - { - return true; - } - - bool decodeGpsHowWord( - vector& words, - Eph& eph) - { - eph.howTow = gpsBitUFromWord(words, 1, 1, 17) * 6; - - return true; - } - - /* decode gps/qzss navigation data subframe 1 - */ - bool decodeGPSSubframe1( - vector& words, - Eph& eph) - { - decodeGpsTlmWord(words, eph); - decodeGpsHowWord(words, eph); - - eph.weekRollOver = gpsBitUFromWord(words, 3, 61, 10); //todo aaron, these all need scaling - eph.code = gpsBitUFromWord(words, 3, 71, 2); - eph.sva = gpsBitUFromWord(words, 3, 73, 4); - int svh = gpsBitUFromWord(words, 3, 77, 6); - int iodc_1 = gpsBitUFromWord(words, 3, 83, 2) << 8; - - eph.flag = gpsBitUFromWord(words, 4, 91, 1); - - int tgd = gpsBitSFromWord(words, 7, 197, 8); - - int iodc_2 = gpsBitUFromWord(words, 8, 211, 8); - eph.tocs = gpsBitUFromWord(words, 8, 219, 16) * (1 << 4); - - eph.f2 = gpsBitSFromWord(words, 9, 241, 8) * P2_55; - eph.f1 = gpsBitSFromWord(words, 9, 249, 16) * P2_43; - - eph.f0 = gpsBitSFromWord(words, 10, 271, 22) * P2_31; - - eph.svh = (E_Svh) svh; - eph.tgd[0] = tgd == -128 ? 0 : tgd * P2_31; /* ref [4] */ - eph.iodc = iodc_1 | iodc_2; - - GTime nearTime = timeGet(); //todo aaron rtcmTime() - - //adjgpsweek() - { - GWeek nowWeek = nearTime; - - int dWeeks = nowWeek - eph.weekRollOver; - int roundDWeeks = (dWeeks + 512) / 1024 * 1024; - - eph.week = eph.weekRollOver + roundDWeeks; - } - - return true; - } - - /* decode gps/qzss navigation data subframe 2 - */ - bool decodeGPSSubframe2( - vector& words, - Eph& eph) - { - decodeGpsTlmWord(words, eph); - decodeGpsHowWord(words, eph); - - eph.iode = gpsBitUFromWord(words, 3, 61, 8); - eph.crs = gpsBitSFromWord(words, 3, 69, 16) * P2_5; - - eph.deln = gpsBitSFromWord(words, 4, 91, 16) * P2_43 * SC2RAD; - int M0_1 = gpsBitSFromWord(words, 4, 107, 8) << 24; - - unsigned int M0_2 = gpsBitUFromWord(words, 5, 121, 24); - - eph.cuc = gpsBitSFromWord(words, 6, 151, 16) * P2_29; - unsigned int e_1 = gpsBitUFromWord(words, 6, 167, 8) << 24; - - unsigned int e_2 = gpsBitUFromWord(words, 7, 181, 24); - - eph.cus = gpsBitSFromWord(words, 8, 211, 16) * P2_29; - unsigned int sqrtA_1 = gpsBitUFromWord(words, 8, 227, 8) << 24; - - 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.sqrtA = (sqrtA_1 | sqrtA_2) * P2_19; - eph.M0 = (M0_1 | M0_2) * P2_31 * SC2RAD; - eph.e = (e_1 | e_2) * P2_33; - eph.A = SQR(eph.sqrtA); - - return true; - } - - /* decode gps/qzss navigation data subframe 3 - */ - bool decodeGPSSubframe3( - vector& words, - Eph& eph) - { - decodeGpsTlmWord(words, eph); - decodeGpsHowWord(words, eph); - - eph.cic = gpsBitSFromWord(words, 3, 61, 16) * P2_29; - signed int OMG0_1 = gpsBitSFromWord(words, 3, 77, 8) << 24; - - signed int OMG0_2 = gpsBitUFromWord(words, 4, 91, 24); - - eph.cis = gpsBitSFromWord(words, 5, 121, 16) * P2_29; - signed int i0_1 = gpsBitSFromWord(words, 5, 137, 8) << 24; - - signed int i0_2 = gpsBitUFromWord(words, 6, 151, 24); - - eph.crc = gpsBitSFromWord(words, 7, 181, 16) * P2_5; - signed int omg_1 = gpsBitSFromWord(words, 7, 197, 8) << 24; - - signed int omg_2 = gpsBitUFromWord(words, 8, 211, 24); - - eph.OMGd = gpsBitSFromWord(words, 9, 241, 24) * P2_43 * SC2RAD; - - int iode = gpsBitUFromWord(words, 10, 271, 8); - eph.idot = gpsBitSFromWord(words, 10, 279, 14) * P2_43 * SC2RAD; - - eph.OMG0 = (OMG0_1 | OMG0_2) * P2_31 * SC2RAD; - eph.i0 = (i0_1 | i0_2) * P2_31 * SC2RAD; - eph.omg = (omg_1 | omg_2) * P2_31 * SC2RAD; - - /* check iode and iodc consistency */ - if ( iode != eph.iode - || iode != (eph.iodc & 0xFF)) - { - return false; - } - - eph.ttm = GTime(GWeek(eph.week), GTow(eph.howTow)); - - eph.toc = GTime(GTow(eph.tocs), eph.ttm); - eph.toe = GTime(GTow(eph.toes), eph.toc); - - -// std::cout << "\n" << eph.ttm; -// std::cout << "\n" << eph.toe; -// std::cout << "\n" << eph.toc; -// std::cout << "\n"; - - return true; - } - - /* decode gps/qzss navigation data frame */ - bool decodeGpsSubframe( - vector& words, ///< words[0-29]: 30 bits x 10 words - Eph& eph) ///< output ephemeris - { - if (words.size() < 10) - { - return false; - } - - int id = gpsBitUFromWord(words, 2, 20, 3); - - switch (id) - { - case 1: return decodeGPSSubframe1(words, eph); - case 2: return decodeGPSSubframe2(words, eph); - case 3: return decodeGPSSubframe3(words, eph); - } - return false; - } + map>> subframeMap; + + /* decode Galileo I/NAV ephemeris ---------------------------------------------- + * decode Galileo I/NAV (ref [5] 4.3) + * args : unsigned char *buff I Galileo I/NAV subframe bits + * buff[ 0-15]: I/NAV word type 0 (128 bit) + * buff[16-31]: I/NAV word type 1 + * buff[32-47]: I/NAV word type 2 + * buff[48-63]: I/NAV word type 3 + * buff[64-79]: I/NAV word type 4 + * buff[80-95]: I/NAV word type 5 + * eph_t *eph IO ephemeris structure + * return : status (1:ok,0:error) + *-----------------------------------------------------------------------------*/ + // int decode_gal_inav( + // const unsigned char* buff, + // eph_t* eph) + // { + // double tow, toc, tt, sqrtA; + // int i, time_f, week, svid, e5b_hs, e1b_hs, e5b_dvs, e1b_dvs, type[6], iod_nav[4]; + // + // i = 0; /* word type 0 */ + // type[0] = getbitu(buff, i, 6); i += 6; + // time_f = getbitu(buff, i, 2); i += 2 + 88; + // week = getbitu(buff, i, 12); i += 12; /* gst-week */ + // tow = getbitu(buff, i, 20); + // + // i = 128; /* word type 1 */ + // type[1] = getbitu(buff, i, 6); i += 6; + // iod_nav[0] = getbitu(buff, i, 10); i += 10; + // eph->toes = getbitu(buff, i, 14) * 60.0; i += 14; + // eph->M0 = getbits(buff, i, 32) * P2_31 * SC2RAD; i += 32; + // eph->e = getbitu(buff, i, 32) * P2_33; i += 32; + // sqrtA = getbitu(buff, i, 32) * P2_19; + // + // i = 128 * 2; /* word type 2 */ + // type[2] = getbitu(buff, i, 6); i += 6; + // iod_nav[1] = getbitu(buff, i, 10); i += 10; + // eph->OMG0 = getbits(buff, i, 32) * P2_31 * SC2RAD; i += 32; + // eph->i0 = getbits(buff, i, 32) * P2_31 * SC2RAD; i += 32; + // eph->omg = getbits(buff, i, 32) * P2_31 * SC2RAD; i += 32; + // eph->idot = getbits(buff, i, 14) * P2_43 * SC2RAD; + // + // i = 128 * 3; /* word type 3 */ + // type[3] = getbitu(buff, i, 6); i += 6; + // iod_nav[2] = getbitu(buff, i, 10); i += 10; + // eph->OMGd = getbits(buff, i, 24) * P2_43 * SC2RAD; i += 24; + // eph->deln = getbits(buff, i, 16) * P2_43 * SC2RAD; i += 16; + // eph->cuc = getbits(buff, i, 16) * P2_29; i += 16; + // eph->cus = getbits(buff, i, 16) * P2_29; i += 16; + // eph->crc = getbits(buff, i, 16) * P2_5; i += 16; + // eph->crs = getbits(buff, i, 16) * P2_5; i += 16; + // eph->sva = getbitu(buff, i, 8); + // + // i = 128 * 4; /* word type 4 */ + // type[4] = getbitu(buff, i, 6); i += 6; + // iod_nav[3] = getbitu(buff, i, 10); i += 10; + // svid = getbitu(buff, i, 6); i += 6; + // eph->cic = getbits(buff, i, 16) * P2_29; i += 16; + // eph->cis = getbits(buff, i, 16) * P2_29; i += 16; + // toc = getbitu(buff, i, 14) * 60.0; i += 14; + // eph->f0 = getbits(buff, i, 31) * P2_34; i += 31; + // eph->f1 = getbits(buff, i, 21) * P2_46; i += 21; + // eph->f2 = getbits(buff, i, 6) * P2_59; + // + // i = 128 * 5; /* word type 5 */ + // type[5] = getbitu(buff, i, 6); i += 6 + 41; + // eph->tgd[0] = getbits(buff, i, 10) * P2_32; i += 10; /* BGD E5a/E1 */ + // eph->tgd[1] = getbits(buff, i, 10) * P2_32; i += 10; /* BGD E5b/E1 */ + // e5b_hs = getbitu(buff, i, 2); i += 2; + // e1b_hs = getbitu(buff, i, 2); i += 2; + // e5b_dvs = getbitu(buff, i, 1); i += 1; + // e1b_dvs = getbitu(buff, i, 1); + // + // /* test word types */ + // if ( type[0] != 0 + // || type[1] != 1 + // || type[2] != 2 + // || type[3] != 3 + // || type[4] != 4) + // { + // trace(3, "decode_gal_inav error: type=%d %d %d %d %d\n", type[0], type[1], type[2], + // type[3], type[4]); return 0; + // } + // + // /* test word type 0 time field */ + // if (time_f != 2) + // { + // trace(3, "decode_gal_inav error: word0-time=%d\n", time_f); + // return 0; + // } + // + // /* test consistency of iod_nav */ + // if ( iod_nav[0] != iod_nav[1] + // || iod_nav[0] != iod_nav[2] + // || iod_nav[0] != iod_nav[3]) + // { + // trace(3, "decode_gal_inav error: ionav=%d %d %d %d\n", iod_nav[0], iod_nav[1], + // iod_nav[2], iod_nav[3]); return 0; + // } + // + // if (!(eph->sat = satno(SYS_GAL, svid))) + // { + // trace(2, "decode_gal_inav svid error: svid=%d\n", svid); + // return 0; + // } + // + // eph->A = sqrtA * sqrtA; + // eph->iode = eph->iodc = iod_nav[0]; + // eph->svh = (e5b_hs << 7) | (e5b_dvs << 6) | (e1b_hs << 1) | e1b_dvs; + // eph->ttr = gst2time(week, tow); + // tt = timediff(gst2time(week, eph->toes), eph->ttr); /* week complient to toe */ + // + // if (tt > +302400.0) week--; + // else if (tt < -302400.0) week++; + // + // eph->toe = gst2time(week, eph->toes); + // eph->toc = gst2time(week, toc); + // eph->week = week + 1024; /* gal-week = gst-week + 1024 */ + // eph->code = 1; /* data source = I/NAV E1B */ + // + // return 1; + // } + + bool decodeGpsTlmWord(vector& words, Eph& eph) { return true; } + + bool decodeGpsHowWord(vector& words, Eph& eph) + { + eph.howTow = gpsBitUFromWord(words, 1, 1, 17) * 6; + + return true; + } + + /* decode gps/qzss navigation data subframe 1 + */ + bool decodeGPSSubframe1(vector& words, Eph& eph) + { + decodeGpsTlmWord(words, eph); + decodeGpsHowWord(words, eph); + + eph.weekRollOver = gpsBitUFromWord(words, 3, 61, 10); // todo aaron, these all need scaling + eph.code = gpsBitUFromWord(words, 3, 71, 2); + eph.sva = gpsBitUFromWord(words, 3, 73, 4); + eph.ura[0] = svaToUra(eph.sva); + int svh = gpsBitUFromWord(words, 3, 77, 6); + int iodc_1 = gpsBitUFromWord(words, 3, 83, 2) << 8; + + eph.flag = gpsBitUFromWord(words, 4, 91, 1); + + int tgd = gpsBitSFromWord(words, 7, 197, 8); + + int iodc_2 = gpsBitUFromWord(words, 8, 211, 8); + eph.tocs = gpsBitUFromWord(words, 8, 219, 16) * (1 << 4); + + eph.f2 = gpsBitSFromWord(words, 9, 241, 8) * P2_55; + eph.f1 = gpsBitSFromWord(words, 9, 249, 16) * P2_43; + + eph.f0 = gpsBitSFromWord(words, 10, 271, 22) * P2_31; + + eph.svh = (E_Svh)svh; + eph.tgd[0] = tgd == -128 ? 0 : tgd * P2_31; /* ref [4] */ + eph.iodc = iodc_1 | iodc_2; + + GTime nearTime = timeGet(); // todo aaron rtcmTime() + + // adjgpsweek() + { + GWeek nowWeek = nearTime; + + int dWeeks = nowWeek - eph.weekRollOver; + int roundDWeeks = (dWeeks + 512) / 1024 * 1024; + + eph.week = eph.weekRollOver + roundDWeeks; + } + + return true; + } + + /* decode gps/qzss navigation data subframe 2 + */ + bool decodeGPSSubframe2(vector& words, Eph& eph) + { + decodeGpsTlmWord(words, eph); + decodeGpsHowWord(words, eph); + + eph.iode = gpsBitUFromWord(words, 3, 61, 8); + eph.crs = gpsBitSFromWord(words, 3, 69, 16) * P2_5; + + eph.deln = gpsBitSFromWord(words, 4, 91, 16) * P2_43 * SC2RAD; + int M0_1 = gpsBitSFromWord(words, 4, 107, 8) << 24; + + unsigned int M0_2 = gpsBitUFromWord(words, 5, 121, 24); + + eph.cuc = gpsBitSFromWord(words, 6, 151, 16) * P2_29; + unsigned int e_1 = gpsBitUFromWord(words, 6, 167, 8) << 24; + + unsigned int e_2 = gpsBitUFromWord(words, 7, 181, 24); + + eph.cus = gpsBitSFromWord(words, 8, 211, 16) * P2_29; + unsigned int sqrtA_1 = gpsBitUFromWord(words, 8, 227, 8) << 24; + + 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.sqrtA = (sqrtA_1 | sqrtA_2) * P2_19; + eph.M0 = (M0_1 | M0_2) * P2_31 * SC2RAD; + eph.e = (e_1 | e_2) * P2_33; + eph.A = SQR(eph.sqrtA); + + return true; + } + + /* decode gps/qzss navigation data subframe 3 + */ + bool decodeGPSSubframe3(vector& words, Eph& eph) + { + decodeGpsTlmWord(words, eph); + decodeGpsHowWord(words, eph); + + eph.cic = gpsBitSFromWord(words, 3, 61, 16) * P2_29; + signed int OMG0_1 = gpsBitSFromWord(words, 3, 77, 8) << 24; + + signed int OMG0_2 = gpsBitUFromWord(words, 4, 91, 24); + + eph.cis = gpsBitSFromWord(words, 5, 121, 16) * P2_29; + signed int i0_1 = gpsBitSFromWord(words, 5, 137, 8) << 24; + + signed int i0_2 = gpsBitUFromWord(words, 6, 151, 24); + + eph.crc = gpsBitSFromWord(words, 7, 181, 16) * P2_5; + signed int omg_1 = gpsBitSFromWord(words, 7, 197, 8) << 24; + + signed int omg_2 = gpsBitUFromWord(words, 8, 211, 24); + + eph.OMGd = gpsBitSFromWord(words, 9, 241, 24) * P2_43 * SC2RAD; + + int iode = gpsBitUFromWord(words, 10, 271, 8); + eph.idot = gpsBitSFromWord(words, 10, 279, 14) * P2_43 * SC2RAD; + + eph.OMG0 = (OMG0_1 | OMG0_2) * P2_31 * SC2RAD; + eph.i0 = (i0_1 | i0_2) * P2_31 * SC2RAD; + eph.omg = (omg_1 | omg_2) * P2_31 * SC2RAD; + + /* check iode and iodc consistency */ + if (iode != eph.iode || iode != (eph.iodc & 0xFF)) + { + return false; + } + + eph.ttm = GTime(GWeek(eph.week), GTow(eph.howTow)); + + eph.toc = GTime(GTow(eph.tocs), eph.ttm); + eph.toe = GTime(GTow(eph.toes), eph.toc); + + // std::cout << "\n" << eph.ttm; + // std::cout << "\n" << eph.toe; + // std::cout << "\n" << eph.toc; + // std::cout << "\n"; + + return true; + } + + /* decode gps/qzss navigation data frame */ + bool decodeGpsSubframe( + vector& words, ///< words[0-29]: 30 bits x 10 words + Eph& eph ///< output ephemeris + ) + { + if (words.size() < 10) + { + return false; + } + + int id = gpsBitUFromWord(words, 2, 20, 3); + + switch (id) + { + case 1: + return decodeGPSSubframe1(words, eph); + case 2: + return decodeGPSSubframe2(words, eph); + case 3: + return decodeGPSSubframe3(words, eph); + } + return false; + } }; diff --git a/src/cpp/common/interactiveTerminal.cpp b/src/cpp/common/interactiveTerminal.cpp deleted file mode 100644 index 89f52c616..000000000 --- a/src/cpp/common/interactiveTerminal.cpp +++ /dev/null @@ -1,483 +0,0 @@ - -// #pragma GCC optimize ("O0") - -#include "interactiveTerminal.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -WINDOW* window; -WINDOW* menu; -WINDOW* bar; - -int activeLevel; -vector activeSplit; - - -mutex InteractiveTerminal::dataMutex; -mutex InteractiveTerminal::displayMutex; -string InteractiveTerminal::activePage; -string InteractiveTerminal::epoch; -string InteractiveTerminal::duration; -E_InteractMode InteractiveTerminal::interactMode = E_InteractMode::Page; -E_InteractiveMode InteractiveTerminal::activeMode = E_InteractiveMode::Syncing; -bool InteractiveTerminal::enabled = false; -map InteractiveTerminal::pages; -map InteractiveTerminal::modes; - -InteractiveTerminalDestructor interactiveTerminaldestructor; - -InteractiveTerminalDestructor::~InteractiveTerminalDestructor() -{ - if (InteractiveTerminal::enabled) - { - werase(window); - werase(bar); - werase(menu); - erase(); - endwin(); - - for (auto& line : InteractiveTerminal::pages["Messages/All"].lines) - { - std::cout << line << "\n"; - } - } -} - -void InteractiveTerminal::keyboardHandler() -{ - while (1) - { - int ch = getch(); - - if (activePage.empty()) - { - continue; - } - - lock_guard guard(dataMutex); - - auto& page = pages[activePage]; - - if (ch == '1') interactMode = E_InteractMode::Page; - else if (ch == '2') interactMode = E_InteractMode::Scroll; - else if (interactMode == +E_InteractMode::Page) - { - switch (ch) - { - case KEY_UP: { activeLevel--; break; } - case KEY_DOWN: { activeLevel++; break; } - case KEY_RIGHT: - case KEY_LEFT: - { - auto it = pages.find(activePage); - - while (1) - { - //keep trying inc/decrement pages until the base prefix at the correct level changes - - if (ch == KEY_LEFT) - { - if (it == pages.begin()) - break; - - it--; - } - if (ch == KEY_RIGHT) - { - it++; - - if (it == pages.end()) - { - it--; - break; - } - } - - auto& [testPage, page] = *it; - - auto nthIt = boost::find_nth(activePage, "/", activeLevel); - - int baseChars = std::distance(activePage.begin(), nthIt.begin()); - - bool changed = (activePage.substr(0, baseChars) != testPage.substr(0, baseChars)); - - if (changed) - { - break; - } - } - - //found something with a different base, check if the base's base is common or not to determine if we can get to it from here - auto& [testPage, page] = *it; - - bool changed = false; - - if (activeLevel > 0) - { - auto nthIt = boost::find_nth(activePage, "/", activeLevel-1); - - int baseChars = std::distance(activePage.begin(), nthIt.begin()); - - changed = (activePage.substr(0, baseChars) != testPage.substr(0, baseChars)); - } - - if (changed == false) - { - activePage = testPage; - - boost::algorithm::split(activeSplit, activePage, boost::is_any_of("\t/"), boost::token_compress_on); - } - - break; - } - } - } - else if (interactMode == +E_InteractMode::Scroll) - { - switch (ch) - { - case KEY_UP: { page.currentLine -= 3; page.followEnd = false; break; } - case KEY_DOWN: { page.currentLine += 3; break; } - case KEY_PPAGE: { page.currentLine -= 20; page.followEnd = false; break; } - case KEY_NPAGE: { page.currentLine += 20; break; } - case KEY_HOME: { page.currentLine = 0; page.followEnd = false; break; } - case KEY_END: { page.currentLine = page.lines.size() + 1; break; } - default: - { - break; - } - } - } - - if (activeLevel < 0) { activeLevel = 0; } - if (activeLevel >= activeSplit.size()) { activeLevel = activeSplit .size() - 1; } - if (page.currentLine < 0) { page.currentLine = 0; } - if (page.currentLine > page.lines.size()) { page.currentLine = page.lines .size() - 1; page.followEnd = true; } - - drawWindow(); - drawMenus(); - } -} - -void InteractiveTerminal::enable() -{ - ConsoleLog::useInteractive = true; - - for (E_InteractiveMode mode : E_InteractiveMode::_values()) - { - modes[mode].modeName = mode._to_string(); - } - - initscr(); - - if (has_colors()) - start_color(); - - init_color(COLOR_YELLOW, 1000, 165, 0); - - init_pair(1, COLOR_WHITE, COLOR_RED); - init_pair(2, COLOR_YELLOW, COLOR_BLACK); - - erase(); - refresh(); - cbreak(); - noecho(); - keypad(stdscr, true); - - menu = newwin(6, COLS, 0, 0); - window = newwin(LINES - 5 - 6, COLS, 6, 0); - bar = newwin(5, COLS, LINES - 5, 0); - - std::thread(keyboardHandler).detach(); - - wrefresh(window); - wrefresh(menu); - wrefresh(bar); - wclear (window); - wclear (menu); - wclear (bar); - - enabled = true; - - drawWindow(); - drawMenus(); -} - - -void InteractiveTerminal::drawMenus() -{ - lock_guard guard(displayMutex); - - boost::algorithm::split(activeSplit, activePage, boost::is_any_of("/"), boost::token_compress_on); - - werase (menu); - box (menu, 0, 0); - - if (activePage.empty() == false) - for (int level = 0; level < activeSplit.size(); level++) - { - wmove(menu, level, 1); - - //get activeSplit - - //level 0 - print the 0 split of everything but only once (ignore repeated prefixes) - - //level 1 - print the 1 split of everything that matches active.split0, but only once - - //level 2 - print the 2 split of everything that matches active.split1 and active split2 but only once - string lastPrinted; - - for (auto& [pageName, page] : pages) - { - vector testSplit; - - boost::algorithm::split(testSplit, pageName, boost::is_any_of("/"), boost::token_compress_on); - - if (level >= testSplit.size()) - { - //too deep already, couldnt possibly be anything to print - continue; - } - - bool isVisible = true; - - //check all levels - for (int i = 0; i < level; i++) - { - if (activeSplit[i] != testSplit[i]) - { - //no longer visible at this level - isVisible = false; - break; - } - } - - if (isVisible == false) - { - //difference in lower prefixes - continue; - } - - string subPage = testSplit[level]; - - if (subPage == lastPrinted) - { - //just printed this for the last page which had same prefixes - continue; - } - - if (subPage == activeSplit[level]) - { - //this is (half) selected - wattron (menu, A_BOLD); - wattron (menu, A_UNDERLINE); - - if (level == activeLevel) - wattron (menu, A_STANDOUT); - } - - string label = subPage; - boost::trim(label); - - wprintw(menu, " %s ", label.c_str()); - - wattroff(menu, A_STANDOUT); - wattroff(menu, A_UNDERLINE); - wattroff(menu, A_BOLD); - - lastPrinted = subPage; - } - } - - werase (bar); - box (bar, 0, 0); - - wmove (bar, 0, 1); - wprintw (bar, " Status "); - - wmove (bar, 0, COLS - 30); - - for (int i = 1; i < E_InteractMode::_size(); i++) - { - auto mode = E_InteractMode::_values()[i]; - if (interactMode == +mode) { wattron (bar, A_STANDOUT); wattron (bar, A_BOLD); } - else { wattroff(bar, A_STANDOUT); wattroff (bar, A_BOLD); } - - wprintw(bar, " (%d) %s ", i, mode._to_string()); - - { wattroff(bar, A_STANDOUT); wattroff (bar, A_BOLD); } - } - - wmove (bar, 1, 1); - wprintw (bar, "%35s ", epoch.c_str()); - wprintw (bar, "%s", duration.c_str()); - - wmove (bar, 2,1); - for (auto& [modeName, mode] : modes) - { - if (mode.active == 1) wattron (bar, A_STANDOUT); - if (mode.active >= 1) wattron (bar, A_BOLD); - - int col1 = getcurx(bar); - wprintw(bar, " %3s ", mode.modeName.c_str()); - int col2 = getcurx(bar); - - wattroff(bar, A_BOLD); - wattroff(bar, A_STANDOUT); - - wmove(bar, 3, col1); - wprintw(bar, " %0.2fs", mode.duration); - wmove(bar, 2, col2); - } - - wrefresh(menu); - wrefresh(bar); -} - -void InteractiveTerminal::drawWindow() -{ - lock_guard guard(displayMutex); - wclear(window); - - int offset = LINES - 7 - 6; - - if (activePage.empty() == false) - { - auto& page = pages[activePage]; - - for (int i = page.currentLine; i > 0 && offset > 1; i--, offset--) - { - if (i >= page.lines.size()) - { - continue; - } - - string line = page.lines[i]; - - for (int i = 0; i < line.length(); i++) - { - if (line[i] == '\t') - { - int spaces = 4 - i % 4; - line.replace(i, 1, string(spaces, ' ')); - } - } - - line = line.substr(0, COLS - 2); - - if (has_colors()) - { - if (line.find("Warning") != string::npos) { wattron(window, COLOR_PAIR(2)); wattron (window, A_BOLD); } - if (line.find("Error") != string::npos) { wattron(window, COLOR_PAIR(1)); wattron (window, A_BOLD); } - } - - mvwprintw(window, offset, 1, "%s", line.c_str()); - - if (has_colors()) - { - wattroff(window, COLOR_PAIR(2)); wattroff(window, A_BOLD); - wattroff(window, COLOR_PAIR(1)); wattroff(window, A_BOLD); - } - } - } - - - box(window, 0, 0); - - wrefresh(window); -} - -void InteractiveTerminal::addString( - string pageName, - const string& str, - bool updateWindow) -{ - if (enabled == false) - return; - - boost::replace_all(pageName, "\t", "/"); - - vector split; - - boost::algorithm::split(split, str, boost::is_any_of("\n"), boost::token_compress_on); - - if (split.size() > 1) - { - for (auto& line : split) - { - addString(pageName, line, false); - } - - if (activePage == pageName) - { - drawWindow(); - } - return; - } - - lock_guard guard(dataMutex); - - auto& page = pages[pageName]; - - if (activePage.empty()) - { - activePage = pageName; - } - - if (page.followEnd) - { - page.currentLine = page.lines.size(); - } - - page.lines.push_back(str); - - if ( updateWindow - &&activePage == pageName) - { - drawWindow(); - } -} - - -void InteractiveTerminal::setMode( - E_InteractiveMode modeName) -{ - if (enabled == false) - return; - - if (activeMode == modeName) - return; - - modes[activeMode].stopTime = timeGet(); - - if (modes[activeMode].startTime != GTime::noTime()) - { - modes[activeMode].duration = (modes[activeMode].stopTime - modes[activeMode].startTime).to_double(); - } - - activeMode = modeName; - modes[activeMode].startTime = timeGet(); - - for (auto& [testMode, mode] : modes) - { - if ( testMode < modeName - &&mode.active) - { - mode.active++; - } - - if (testMode == modeName) - { - mode.active = 1; - } - } - - drawMenus(); -} diff --git a/src/cpp/common/interactiveTerminal.hpp b/src/cpp/common/interactiveTerminal.hpp deleted file mode 100644 index 9bba15c0a..000000000 --- a/src/cpp/common/interactiveTerminal.hpp +++ /dev/null @@ -1,131 +0,0 @@ - -#pragma once - -#include -#include -#include -#include -#include - -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" - -using std::ostringstream; -using std::lock_guard; -using std::vector; -using std::string; -using std::mutex; -using std::map; - -struct InteractivePage -{ - vector lines; - int currentLine = 0; - bool followEnd = true; -}; - -struct InteractiveMode -{ - GTime startTime; - GTime stopTime; - double duration = 0; - int active = 0; - string modeName; -}; - -struct InteractiveTerminal : ostringstream -{ - static mutex dataMutex; - static mutex displayMutex; - static string activePage; - static string epoch; - static string duration; - static E_InteractMode interactMode; - static E_InteractiveMode activeMode; - static bool enabled; - static map pages; - static map modes; - - - static void enable(); - static void drawMenus(); - static void drawWindow(); - static void keyboardHandler(); - - static void clearPage( - string pageName) - { - if (enabled == false) - return; - - boost::replace_all(pageName, "\t", "/"); - - lock_guard guard(dataMutex); - - pages[pageName].lines.clear(); - } - - static void clearModes( - string epochStr = "", - string durationStr = "") - { - setMode(E_InteractiveMode::Syncing); - - if (enabled == false) - return; - - epoch = epochStr; - duration = durationStr; - - for (auto& [modeName, mode] : modes) - { - mode.active = 0; - } - } - - static void setMode( - E_InteractiveMode modeName); - - static void addString( - string pageName, - const string& str, - bool updateWindow = true); - - - string name; - Trace& trace; - bool doClear; - - /// Creates a RAII object that acts like an ostringstream and spits results out to another trace when it goes out of scope - /// Also clears and updates an interactive terminal page - InteractiveTerminal( - const string& name, - Trace& trace, - bool doClear = true) - : name {name}, - trace {trace}, - doClear {doClear} - { - - } - - ~InteractiveTerminal() - { - trace << str(); - - if (doClear) - { - clearPage(name); - } - - addString(name, str()); - } -}; - -struct InteractiveTerminalDestructor -{ - ~InteractiveTerminalDestructor(); -}; - -extern InteractiveTerminalDestructor interactiveTerminaldestructor; diff --git a/src/cpp/common/ionModels.cpp b/src/cpp/common/ionModels.cpp index 61a451371..92d543aab 100644 --- a/src/cpp/common/ionModels.cpp +++ b/src/cpp/common/ionModels.cpp @@ -1,447 +1,490 @@ - // #pragma GCC optimize ("O0") +#include "common/ionModels.hpp" #include - -#include "observations.hpp" -#include "navigation.hpp" -#include "coordinates.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "satStat.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" - - -#define VAR_IONO SQR(60.0) // init variance iono-delay -#define VAR_IONEX SQR(0.0) -#define ERR_BRDCI 0.5 // broadcast iono model error factor - -#define VAR_NOTEC SQR(30.0) /* variance of no tec */ -#define MIN_EL 0.0 /* min elevation angle (rad) */ -#define MIN_HGT -1000.0 /* min user height (m) */ - - -int dataindex( - int i, - int j, - int k, - const int* ndata); +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/satStat.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" + +constexpr double VAR_IONO = 60.0 * 60.0; // init variance iono-delay +constexpr double VAR_IONEX = 0.0 * 0.0; +constexpr double ERR_BRDCI = 0.5; // broadcast iono model error factor +constexpr double ERR_ION = 7.0; // ionospheric delay std (m) + +constexpr double VAR_NOTEC = 30.0 * 30.0; // variance of no tec +constexpr double MIN_EL = 0.0; // min elevation angle (rad) +constexpr double MIN_HGT = -1000.0; // min user height (m) + +int dataindex(int i, int j, int k, const int* ndata); /* ionosphere model ------------------------------------------------------------ -* compute ionospheric delay by broadcast ionosphere model (klobuchar model) -* args : gtime_t t I time (gpst) -* double *ion I iono model parameters {a0,a1,a2,a3,b0,b1,b2,b3} -* double *pos I receiver position {lat,lon,h} (rad,m) -* double *azel I azimuth/elevation angle {az,el} (rad) -* return : ionospheric delay (L1) (m) -*-----------------------------------------------------------------------------*/ -double ionmodel( - GTime t, - const double* ion, - const VectorPos& pos, - const AzEl& azel) + * compute ionospheric delay by broadcast ionosphere model (klobuchar model) + * args : gtime_t t I time (gpst) + * double *ion I iono model parameters {a0,a1,a2,a3,b0,b1,b2,b3} + * double *pos I receiver position {lat,lon,h} (rad,m) + * double *azel I azimuth/elevation angle {az,el} (rad) + * return : ionospheric delay (L1) (m) + *-----------------------------------------------------------------------------*/ +double ionmodel(GTime t, const double* ion, const VectorPos& pos, const AzEl& azel) { - const double ion_default[] = /* 2004/1/1 */ - { - 0.1118E-07, -0.7451E-08, -0.5961E-07, 0.1192E-06, - 0.1167E+06, -0.2294E+06, -0.1311E+06, 0.1049E+07 - }; - - if ( pos.hgt() < -1000 - ||azel.el <= 0) - { - return 0; - } - - if ( ion == nullptr - ||norm(ion, 8) <= 0) - { - ion = ion_default; - } - - /* earth centered angle (semi-circle) */ - double psi = 0.0137 / (azel.el / PI + 0.11) - 0.022; - - /* subionospheric latitude/longitude (semi-circle) */ - double phi = pos.lat() / PI + psi * cos(azel.az); - - if (phi > +0.416) phi = +0.416; - else if (phi < -0.416) phi = -0.416; - - double lam = pos.lon() / PI + psi * sin(azel.az) / cos(phi * PI); - - /* geomagnetic latitude (semi-circle) */ - phi += 0.064 * cos((lam - 1.617) * PI); - - /* local time (s) */ - // int week; - // double tt = 43200 * lam + time2gpst(t, &week); - double tt = GTow(t) + 43200.0 * lam; - tt -= floor(tt / 86400) * 86400; /* 0<=tt<86400 */ - - /* slant factor */ - double f = 1 + 16 * pow(0.53 - azel.el / PI, 3); - - /* ionospheric delay */ - double amp = ion[0] + phi * (ion[1] + phi * (ion[2] + phi * ion[3])); - double per = ion[4] + phi * (ion[5] + phi * (ion[6] + phi * ion[7])); - amp = amp < 0 ? 0 : amp; - per = per < 72000 ? 72000 : per; - double x = 2 * PI * (tt - 50400) / per; - - return CLIGHT * f * (fabs(x) < 1.57 ? 5E-9 + amp * (1 + x * x * (-0.5 + x * x / 24)) : 5E-9); + const double ion_default[] = /* 2004/1/1 */ + {0.1118E-07, + -0.7451E-08, + -0.5961E-07, + 0.1192E-06, + 0.1167E+06, + -0.2294E+06, + -0.1311E+06, + 0.1049E+07}; + + if (pos.hgt() < -1000 || azel.el <= 0) + { + return 0; + } + + if (ion == nullptr || norm(ion, 8) <= 0) + { + BOOST_LOG_TRIVIAL( + warning + ) << "ionmodel: ionospheric model parameters are not set, using default values (2004/1/1)"; + ion = ion_default; + } + + /* earth centered angle (semi-circle) */ + double psi = 0.0137 / (azel.el / PI + 0.11) - 0.022; + + /* subionospheric latitude/longitude (semi-circle) */ + double phi = pos.lat() / PI + psi * cos(azel.az); + + if (phi > +0.416) + phi = +0.416; + else if (phi < -0.416) + phi = -0.416; + + double lam = pos.lon() / PI + psi * sin(azel.az) / cos(phi * PI); + + /* geomagnetic latitude (semi-circle) */ + phi += 0.064 * cos((lam - 1.617) * PI); + + /* local time (s) */ + // int week; + // double tt = 43200 * lam + time2gpst(t, &week); + double tt = GTow(t) + 43200.0 * lam; + tt -= floor(tt / 86400) * 86400; /* 0<=tt<86400 */ + + /* slant factor */ + double dummy = 0; + double f = ionmapf(pos, azel, E_IonoMapFn::KLOBUCHAR, dummy); + + /* ionospheric delay */ + double amp = ion[0] + phi * (ion[1] + phi * (ion[2] + phi * ion[3])); + double per = ion[4] + phi * (ion[5] + phi * (ion[6] + phi * ion[7])); + amp = amp < 0 ? 0 : amp; + per = per < 72000 ? 72000 : per; + double x = 2 * PI * (tt - 50400) / per; + + return CLIGHT * f * (fabs(x) < 1.57 ? 5E-9 + amp * (1 + x * x * (-0.5 + x * x / 24)) : 5E-9); } /** ionosphere mapping function -*/ + */ double ionmapf( - const VectorPos& pos, ///< receiver position in geocentric spherical coordinates - const AzEl& azel, ///< satellite azimuth/elevation angle (rad) - E_IonoMapFn mapFn, ///< model of mapping function - double hion) ///< layer height (km) + const VectorPos& pos, ///< receiver position in geocentric spherical coordinates + const AzEl& azel, ///< satellite azimuth/elevation angle (rad) + E_IonoMapFn mapFn, ///< model of mapping function + double hion ///< layer height (km) +) { - double alpha = 1; - switch (mapFn) - { - case E_IonoMapFn::SLM: // fallthrough - case E_IonoMapFn::MLM: break; // same to SLM but need to call the function multiple times - case E_IonoMapFn::MSLM: alpha = 0.9782; break; - case E_IonoMapFn::KLOBUCHAR: return 1 + 16 * pow(0.53 - azel.el / PI, 3); - } - - double rp = RE_MEAN / (RE_MEAN + hion) * sin(alpha * (PI / 2 - azel.el)); - - return 1 / sqrt(1 - SQR(rp)); + double alpha = 1; + switch (mapFn) + { + case E_IonoMapFn::SLM: // fallthrough + case E_IonoMapFn::MLM: + break; // same to SLM but need to call the function multiple times + case E_IonoMapFn::MSLM: + alpha = 0.9782; + break; + case E_IonoMapFn::KLOBUCHAR: + return 1 + 16 * pow(0.53 - azel.el / PI, 3); + } + + double rp = RE_MEAN / (RE_MEAN + hion) * sin(alpha * (PI / 2 - azel.el)); + + return 1 / sqrt(1 - SQR(rp)); } + /* ionospheric pierce point position ------------------------------------------- -* compute ionospheric pierce point (ipp) position and slant factor -* args : double *pos I receiver position {lat,lon,h} (rad,m) -* double *azel I azimuth/elevation angle {az,el} (rad) -* double re I earth radius (km) -* double hion I altitude of ionosphere (km) -* double *posp O pierce point position {lat,lon,r} (rad,m) in geocentric spherical coordinate system -* return : slant factor -* notes : see ref [2], only valid on the earth surface -* fixing bug on ref [2] A.4.4.10.1 A-22,23 -*-----------------------------------------------------------------------------*/ -double ionppp( - const VectorPos& pos, - const AzEl& azel, - double re, - double hion, - VectorPos& posp) + * compute ionospheric pierce point (ipp) position and slant factor + * args : double *pos I receiver position {lat,lon,h} (rad,m) + * double *azel I azimuth/elevation angle {az,el} (rad) + * double re I earth radius (km) + * double hion I altitude of ionosphere (km) + * double *posp O pierce point position {lat,lon,r} (rad,m) in geocentric spherical + *coordinate system return : slant factor notes : see ref [2], only valid on the earth surface + * fixing bug on ref [2] A.4.4.10.1 A-22,23 + *-----------------------------------------------------------------------------*/ +double ionppp(const VectorPos& pos, const AzEl& azel, double re, double hion, VectorPos& posp) { - double ri = re + hion; - double rp = re / ri * cos(azel.el); - double ap = PI / 2 - azel.el - asin(rp); - double sinap = sin(ap); - double tanap = tan(ap); - double cosaz = cos(azel.az); - posp[0] = asin(sin(pos.lat()) * cos(ap) + cos(pos.lat()) * sinap * cosaz); - - if ( (pos.lat() > +70 * D2R && +tanap * cosaz > tan(PI / 2 - pos.lat())) - ||(pos.lat() < -70 * D2R && -tanap * cosaz > tan(PI / 2 + pos.lat()))) - { - posp.lon() = pos.lon() + PI - asin(sinap * sin(azel.az) / cos(posp.lat())); - } - else - { - posp.lon() = pos.lon() + asin(sinap * sin(azel.az) / cos(posp.lat())); - } - - posp[2] = ri * 1000; // geocentric radius - - return 1 / sqrt(1 - SQR(rp)); + double ri = re + hion; + double rp = re / ri * cos(azel.el); + double ap = PI / 2 - azel.el - asin(rp); + double sinap = sin(ap); + double tanap = tan(ap); + double cosaz = cos(azel.az); + posp[0] = asin(sin(pos.lat()) * cos(ap) + cos(pos.lat()) * sinap * cosaz); + + if ((pos.lat() > +70 * D2R && +tanap * cosaz > tan(PI / 2 - pos.lat())) || + (pos.lat() < -70 * D2R && -tanap * cosaz > tan(PI / 2 + pos.lat()))) + { + posp.lon() = pos.lon() + PI - asin(sinap * sin(azel.az) / cos(posp.lat())); + } + else + { + posp.lon() = pos.lon() + asin(sinap * sin(azel.az) / cos(posp.lat())); + } + + posp[2] = ri * 1000; // geocentric radius + + return 1 / sqrt(1 - SQR(rp)); } /** interpolate tec grid data */ -int interpTec( - const TEC& tec, - int k, - const VectorPos& posp, - double& value, - double& rms) +int interpTec(const TEC& tec, int k, const VectorPos& posp, double& value, double& rms) { - tracepdeex(6,std::cout, "%s: k=%d posp=%.2f %.2f\n",__FUNCTION__, k, posp[0]*R2D, posp[1]*R2D); - - value = 0; - rms = 0; - - if ( tec.lats[2] == 0 - ||tec.lons[2] == 0) - { - return 0; - } - - double dlat = posp.latDeg() - tec.lats[0]; - double dlon = posp.lonDeg() - tec.lons[0]; - - if (tec.lons[2] > 0) dlon -= floor( dlon / 360) * 360; /* 0<=dlon<360 */ - else dlon += floor(-dlon / 360) * 360; /* -360 0 - && d[1] > 0 - && d[2] > 0 - && d[3] > 0) - { - /* bilinear interpolation (inside of grid) */ - value = (1 - a) * (1 - b) * d[0] + a * (1 - b) * d[1] + (1 - a) * b * d[2] + a * b * d[3]; - rms = (1 - a) * (1 - b) * r[0] + a * (1 - b) * r[1] + (1 - a) * b * r[2] + a * b * r[3]; - - // if (fdebug) - tracepdeex(6,std::cout, " gridpoints: %8.2f %8.2f %8.2f %8.2f -> %9.3f\n", d[0], d[1], d[2], d[3], value); - } - /* nearest-neighbour extrapolation (outside of grid) */ - else if (a <= 0.5 && b <= 0.5 && d[0] > 0) { value = d[0]; rms = r[0]; } - else if (a > 0.5 && b <= 0.5 && d[1] > 0) { value = d[1]; rms = r[1]; } - else if (a <= 0.5 && b > 0.5 && d[2] > 0) { value = d[2]; rms = r[2]; } - else if (a > 0.5 && b > 0.5 && d[3] > 0) { value = d[3]; rms = r[3]; } - else - { - i = 0; - - for (int n = 0; n < 4; n++) - if (d[n] > 0) - { - i++; - value += d[n]; - rms += r[n]; - } - - if (i == 0) - return 0; - - value /= i; - rms /= i; - } - - return 1; + tracepdeex( + 6, + std::cout, + "%s: k=%d posp=%.2f %.2f\n", + __FUNCTION__, + k, + posp[0] * R2D, + posp[1] * R2D + ); + + value = 0; + rms = 0; + + if (tec.lats[2] == 0 || tec.lons[2] == 0) + { + return 0; + } + + double dlat = posp.latDeg() - tec.lats[0]; + double dlon = posp.lonDeg() - tec.lons[0]; + + if (tec.lons[2] > 0) + dlon -= floor(dlon / 360) * 360; /* 0<=dlon<360 */ + else + dlon += floor(-dlon / 360) * 360; /* -360 0 && d[1] > 0 && d[2] > 0 && d[3] > 0) + { + /* bilinear interpolation (inside of grid) */ + value = (1 - a) * (1 - b) * d[0] + a * (1 - b) * d[1] + (1 - a) * b * d[2] + a * b * d[3]; + rms = (1 - a) * (1 - b) * r[0] + a * (1 - b) * r[1] + (1 - a) * b * r[2] + a * b * r[3]; + + // if (fdebug) + tracepdeex( + 6, + std::cout, + " gridpoints: %8.2f %8.2f %8.2f %8.2f -> %9.3f\n", + d[0], + d[1], + d[2], + d[3], + value + ); + } + /* nearest-neighbour extrapolation (outside of grid) */ + else if (a <= 0.5 && b <= 0.5 && d[0] > 0) + { + value = d[0]; + rms = r[0]; + } + else if (a > 0.5 && b <= 0.5 && d[1] > 0) + { + value = d[1]; + rms = r[1]; + } + else if (a <= 0.5 && b > 0.5 && d[2] > 0) + { + value = d[2]; + rms = r[2]; + } + else if (a > 0.5 && b > 0.5 && d[3] > 0) + { + value = d[3]; + rms = r[3]; + } + else + { + i = 0; + + for (int n = 0; n < 4; n++) + if (d[n] > 0) + { + i++; + value += d[n]; + rms += r[n]; + } + + if (i == 0) + return 0; + + value /= i; + rms /= i; + } + + return 1; } /** ionosphere delay by tec grid data */ bool ionDelay( - GTime time, ///< Time - const TEC& tec, ///< Input electron content data - const VectorPos& pos, ///< Position of receiver - const AzEl& azel, ///< Azimuth and elevation of signal path - E_IonoMapFn mapFn, ///< model of mapping function - double layerHeight, ///< Mapping function layer height - E_IonoFrame frame, ///< reference frame - double& delay, ///< Delay in meters - double& var) ///< Variance + GTime time, ///< Time + const TEC& tec, ///< Input electron content data + const VectorPos& pos, ///< Position of receiver + const AzEl& azel, ///< Azimuth and elevation of signal path + E_IonoMapFn mapFn, ///< model of mapping function + double layerHeight, ///< Mapping function layer height + E_IonoFrame frame, ///< reference frame + double& delay, ///< Delay in meters + double& var ///< Variance +) { - // if (fdebug) - // fprintf(fdebug, "%s: time=%s pos=%.1f %.1f azel=%.1f %.1f\n", __FUNCTION__, time.to_string(0).c_str(), pos[0]*R2D, pos[1]*R2D, azel[0]*R2D, azel[1]*R2D); - - delay = 0; - var = 0; - - for (int i = 0; i < tec.ndata[2]; i++) - { - double hion = tec.hgts[0] + tec.hgts[2] * i; - - /* ionospheric pierce point position */ - VectorPos posp; - ionppp(pos, azel, tec.rb, hion, posp); - double fs = ionmapf(pos, azel, mapFn, layerHeight); - - if (frame == +E_IonoFrame::SUN_FIXED) - { - /* earth rotation correction (sun-fixed coordinate) */ - posp[1] += 2 * PI * (time - tec.time).to_double() / 86400; - } - - /* interpolate tec grid data */ - double rms; - double vtec; - if (interpTec(tec, i, posp, vtec, rms) == false) - return false; - - const double fact = TEC_CONSTANT / SQR(FREQ1); /* tecu->L1 iono (m) */ - delay += fact * fs * vtec; - var += SQR(fact * fs * rms); - } - - tracepdeex(6,std::cout, "%s: delay=%7.2f std=%6.2f\n",__FUNCTION__, delay, sqrt(var)); - - return true; + // if (fdebug) + // fprintf(fdebug, "%s: time=%s pos=%.1f %.1f azel=%.1f %.1f\n", __FUNCTION__, + // time.to_string(0).c_str(), pos[0]*R2D, pos[1]*R2D, azel[0]*R2D, azel[1]*R2D); + + delay = 0; + var = 0; + + for (int i = 0; i < tec.ndata[2]; i++) + { + double hion = tec.hgts[0] + tec.hgts[2] * i; + + /* ionospheric pierce point position */ + VectorPos posp; + ionppp(pos, azel, tec.rb, hion, posp); + double fs = ionmapf(pos, azel, mapFn, layerHeight); + + if (frame == E_IonoFrame::SUN_FIXED) + { + /* earth rotation correction (sun-fixed coordinate) */ + posp[1] += 2 * PI * (time - tec.time).to_double() / 86400; + } + + /* interpolate tec grid data */ + double rms; + double vtec; + if (interpTec(tec, i, posp, vtec, rms) == false) + return false; + + const double fact = TEC_CONSTANT / SQR(FREQ1); /* tecu->L1 iono (m) */ + delay += fact * fs * vtec; + var += SQR(fact * fs * rms); + } + + tracepdeex(6, std::cout, "%s: delay=%7.2f std=%6.2f\n", __FUNCTION__, delay, sqrt(var)); + + return true; } - /** ionosphere model by tec grid data * Before calling the function, read tec grid data by calling readTec() -* return ok with delay=0 and var=VAR_NOTEC if el < MIN_EL or h < MIN_HGT -*/ + * return ok with delay=0 and var=VAR_NOTEC if el < MIN_EL or h < MIN_HGT + */ bool iontec( - GTime time, ///< time (gpst) - const Navigation* nav, ///< navigation data - const VectorPos& pos, ///< receiver position {lat,lon,h} (rad,m) - const AzEl& azel, ///< azimuth/elevation angle {az,el} (rad) - E_IonoMapFn mapFn, ///< model of mapping function - double layerHeight, ///< Mapping function layer height - E_IonoFrame frame, ///< reference frame - double& delay, ///< ionospheric delay (L1) (m) - double& var) ///< ionospheric dealy (L1) variance (m^2) + GTime time, ///< time (gpst) + const Navigation* nav, ///< navigation data + const VectorPos& pos, ///< receiver position {lat,lon,h} (rad,m) + const AzEl& azel, ///< azimuth/elevation angle {az,el} (rad) + E_IonoMapFn mapFn, ///< model of mapping function + double layerHeight, ///< Mapping function layer height + E_IonoFrame frame, ///< reference frame + double& delay, ///< ionospheric delay (L1) (m) + double& var ///< ionospheric dealy (L1) variance (m^2) +) { - // if (fdebug) - // fprintf(fdebug, "iontec : time=%s pos=%.1f %.1f azel=%.1f %.1f nt=%ld\n", time.to_string(0).c_str(), pos[0]*R2D, pos[1]*R2D, azel[0]*R2D, azel[1]*R2D, nav->tecList.size()); - - delay = 0; - var = VAR_NOTEC; - - if ( azel.el < MIN_EL - || pos.hgt() < MIN_HGT) - { - return true; - } - - auto it = nav->tecMap.lower_bound(time); - if (it == nav->tecMap.end()) - { - // if (fdebug) - // fprintf(fdebug, "%s: tec grid out of period\n", time.to_string(0).c_str()); - - return true; - } - - bool pass[2] = {}; - double dels[2]; - double vars[2]; - - auto& [t0, tec0] = *it; - pass[0] = ionDelay(time, tec0, pos, azel, mapFn, layerHeight, frame, dels[0], vars[0]); - - if (it == nav->tecMap.begin()) - { - delay = dels[0]; - var = vars[0]; - return pass[0]; - } - - //go forward and get the next timestep if available - it--; - - auto& [t1, tec1] = *it; - pass[1] = ionDelay(time, tec1, pos, azel, mapFn, layerHeight, frame, dels[1], vars[1]); - - - if ( pass[0] - && pass[1]) - { - /* linear interpolation by time */ - double tt = (tec1.time - tec0.time).to_double(); - double a = (time - tec0.time).to_double() / tt; - - delay = dels[0] * (1 - a) + dels[1] * a; - var = vars[0] * (1 - a) + vars[1] * a; - } - else if (pass[0]) /* nearest-neighbour extrapolation by time */ - { - delay = dels[0]; - var = vars[0]; - } - else if (pass[1]) - { - delay = dels[1]; - var = vars[1]; - } - else - { - // if (fdebug) - // fprintf(fdebug, "%s: tec grid out of area pos=%6.2f %7.2f azel=%6.1f %5.1f\n", time.to_string(0).c_str(), pos[0]*R2D, pos[1]*R2D, azel[0]*R2D, azel[1]*R2D); - - return false; - } - - // if (fdebug) - // fprintf(fdebug, "iontec : delay=%5.2f std=%5.2f\n", delay, sqrt(var)); - - return true; + // if (fdebug) + // fprintf(fdebug, "iontec : time=%s pos=%.1f %.1f azel=%.1f %.1f nt=%ld\n", + // time.to_string(0).c_str(), pos[0]*R2D, pos[1]*R2D, azel[0]*R2D, azel[1]*R2D, + // nav->tecList.size()); + + delay = 0; + var = VAR_NOTEC; + + if (azel.el < MIN_EL || pos.hgt() < MIN_HGT) + { + return true; + } + + auto it = nav->tecMap.lower_bound(time); + if (it == nav->tecMap.end()) + { + // if (fdebug) + // fprintf(fdebug, "%s: tec grid out of period\n", time.to_string(0).c_str()); + + return true; + } + + bool pass[2] = {}; + double dels[2]; + double vars[2]; + + auto& [t0, tec0] = *it; + pass[0] = ionDelay(time, tec0, pos, azel, mapFn, layerHeight, frame, dels[0], vars[0]); + + if (it == nav->tecMap.begin()) + { + delay = dels[0]; + var = vars[0]; + return pass[0]; + } + + // go forward and get the next timestep if available + it--; + + auto& [t1, tec1] = *it; + pass[1] = ionDelay(time, tec1, pos, azel, mapFn, layerHeight, frame, dels[1], vars[1]); + + if (pass[0] && pass[1]) + { + /* linear interpolation by time */ + double tt = (tec1.time - tec0.time).to_double(); + double a = (time - tec0.time).to_double() / tt; + + delay = dels[0] * (1 - a) + dels[1] * a; + var = vars[0] * (1 - a) + vars[1] * a; + } + else if (pass[0]) /* nearest-neighbour extrapolation by time */ + { + delay = dels[0]; + var = vars[0]; + } + else if (pass[1]) + { + delay = dels[1]; + var = vars[1]; + } + else + { + // if (fdebug) + // fprintf(fdebug, "%s: tec grid out of area pos=%6.2f %7.2f azel=%6.1f %5.1f\n", + // time.to_string(0).c_str(), pos[0]*R2D, pos[1]*R2D, azel[0]*R2D, azel[1]*R2D); + + return false; + } + + // if (fdebug) + // fprintf(fdebug, "iontec : delay=%5.2f std=%5.2f\n", delay, sqrt(var)); + + return true; } - /** ionospheric model */ bool ionoModel( - GTime& time, - VectorPos& pos, - AzEl& azel, - E_IonoMapFn mapFn, - E_IonoMode mode, - double layerHeight, - double ionoState, - double& dion, - double& var) + GTime& time, + VectorPos& pos, + AzEl& azel, + E_IonoMapFn mapFn, + E_IonoMode mode, + double layerHeight, + double ionoState, + double& dion, + double& var +) { - switch (mode) - { - case E_IonoMode::TOTAL_ELECTRON_CONTENT: - { - int pass = iontec(time, &nav, pos, azel, mapFn, layerHeight, E_IonoFrame::SUN_FIXED, dion, var); - if (pass) var += VAR_IONEX; // adding some extra errors to reflect modelling errors - else var = VAR_IONO; - - return pass; - } - case E_IonoMode::BROADCAST: // only GPS Klobuchar model implemented - { - E_Sys sys = E_Sys::GPS; - E_NavMsgType type = defNavMsgType[sys]; - - auto ion_ptr = seleph(nullStream, time, sys, type, nav); - - double* vals = nullptr; - if (ion_ptr != nullptr) - vals = ion_ptr->vals; - - dion = ionmodel(time, vals, pos, azel); - var = SQR(dion * ERR_BRDCI); - - return true; - } - case E_IonoMode::ESTIMATE: - { - dion = ionoState; - var = 0; - - return true; - } - case E_IonoMode::IONO_FREE_LINEAR_COMBO: - { - dion = 0; - var = 0; - - return true; - } - default: - { - return false; - } - } + switch (mode) + { + case E_IonoMode::TOTAL_ELECTRON_CONTENT: + { + int pass = iontec( + time, + &nav, + pos, + azel, + mapFn, + layerHeight, + E_IonoFrame::SUN_FIXED, + dion, + var + ); + if (pass) + var += VAR_IONEX; // adding some extra errors to reflect modelling errors + else + var = VAR_IONO; + + return pass; + } + case E_IonoMode::BROADCAST: // only GPS Klobuchar model implemented + { + E_Sys sys = E_Sys::GPS; + E_NavMsgType type = defNavMsgType[sys]; + + auto ion_ptr = seleph(nullStream, time, sys, type, nav); + + double* vals = nullptr; + if (ion_ptr != nullptr) + vals = ion_ptr->vals; + + dion = ionmodel(time, vals, pos, azel); + var = SQR(dion * ERR_BRDCI); + + return true; + } + case E_IonoMode::ESTIMATE: + { + dion = ionoState; // Eugene: need to convert stec to delay (m)? + var = 0; + + return true; + } + case E_IonoMode::IONO_FREE_LINEAR_COMBO: + { + dion = 0; + var = 0; + + return true; + } + default: + { + dion = 0; + var = SQR(ERR_ION); // Undefined variance + + return false; + } + } } diff --git a/src/cpp/common/ionModels.hpp b/src/cpp/common/ionModels.hpp index fad890064..d4399a27a 100644 --- a/src/cpp/common/ionModels.hpp +++ b/src/cpp/common/ionModels.hpp @@ -1,48 +1,31 @@ - #pragma once #include +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" using std::string; -#include "eigenIncluder.hpp" -#include "gTime.hpp" -#include "enums.h" +struct AzEl; +struct Navigation; +double ionmodel(GTime t, const double* ion, const VectorPos& pos, const AzEl& azel); -struct Navigation; +double ionmapf(const VectorPos& pos, const AzEl& azel, E_IonoMapFn mapFn, double hion); -double ionmodel( - GTime t, - const double* ion, - const VectorPos& pos, - const AzEl& azel); - -double ionmapf( - const VectorPos& pos, - const AzEl& azel, - E_IonoMapFn mapFn, - double hion); - -double ionppp( - const VectorPos& pos, - const AzEl& azel, - double re, - double hion, - VectorPos& pppos); +double ionppp(const VectorPos& pos, const AzEl& azel, double re, double hion, VectorPos& pppos); bool iontec( - GTime time, - const Navigation* nav, - const VectorPos& pos, - const AzEl& azel, - E_IonoMapFn mapFn, - double layerHeight, - E_IonoFrame frame, - double& delay, - double& var); - -void readTec( - string file, - Navigation* nav); - + GTime time, + const Navigation* nav, + const VectorPos& pos, + const AzEl& azel, + E_IonoMapFn mapFn, + double layerHeight, + E_IonoFrame frame, + double& delay, + double& var +); + +void readTec(string file, Navigation* nav); diff --git a/src/cpp/common/kalmanBlas.cpp b/src/cpp/common/kalmanBlas.cpp new file mode 100644 index 000000000..d3e47ba10 --- /dev/null +++ b/src/cpp/common/kalmanBlas.cpp @@ -0,0 +1,479 @@ +#include "common/kalmanBlas.hpp" +#include +#include + +// Compute H*P using BLAS +void KalmanFilterBLAS::computeHP(const MatrixXd& H, const MatrixXd& P, MatrixXd& HP) +{ + int numH = H.rows(); + int numX = H.cols(); + + // Resize output if needed + if (HP.rows() != numH || HP.cols() != numX) + { + HP.resize(numH, numX); + } + + // HP = H * P + // C = alpha*A*B + beta*C + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, // Eigen uses column-major storage + LapackWrapper::CblasNoTrans, // Don't transpose H + LapackWrapper::CblasNoTrans, // Don't transpose P + numH, // Rows of H (and HP) + numX, // Cols of P (and HP) + numX, // Cols of H / rows of P + 1.0, // alpha = 1.0 + H.data(), // Matrix H + H.rows(), // Leading dimension of H + P.data(), // Matrix P + P.rows(), // Leading dimension of P + 0.0, // beta = 0.0 (don't accumulate) + HP.data(), // Output matrix HP + HP.rows() // Leading dimension of HP + ); +} + +// Compute innovation covariance Q = H*P*H' + R using BLAS +void KalmanFilterBLAS::computeInnovationCovariance( + const MatrixXd& H, + const MatrixXd& P, + const MatrixXd& R, + MatrixXd& Q +) +{ + int numH = H.rows(); + int numX = H.cols(); + + // Resize output if needed + if (Q.rows() != numH || Q.cols() != numH) + { + Q.resize(numH, numH); + } + + // Step 1: Compute HP = H * P + MatrixXd HP(numH, numX); + computeHP(H, P, HP); + + // Step 2: Copy R into Q (Q will be HP*H' + R) + std::memcpy(Q.data(), R.data(), numH * numH * sizeof(double)); + + // Step 3: Q = HP * H' + Q (Q already contains R) + // C = alpha*A*B' + beta*C + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, // Eigen uses column-major storage + LapackWrapper::CblasNoTrans, // Don't transpose HP + LapackWrapper::CblasTrans, // Transpose H + numH, // Rows of HP (and Q) + numH, // Rows of H (cols of H') + numX, // Cols of HP / cols of H + 1.0, // alpha = 1.0 + HP.data(), // Matrix HP + HP.rows(), // Leading dimension of HP + H.data(), // Matrix H (will be transposed) + H.rows(), // Leading dimension of H + 1.0, // beta = 1.0 (accumulate with R) + Q.data(), // Output matrix Q + Q.rows() // Leading dimension of Q + ); +} + +// Solve linear system Q*X = B using LAPACKE with fallback chain +bool KalmanFilterBLAS::solveLinearSystem(MatrixXd& Q, MatrixXd& B, E_Inverter inverter) +{ + int n = Q.rows(); + int nrhs = B.cols(); + int info; + + // Always keep backup for fallback chain + MatrixXd Q_backup = Q; + MatrixXd B_backup = B; + + bool repeat = true; + int attempts = 0; + const int max_attempts = 10; // Prevent infinite loops + + while (repeat && attempts < max_attempts) + { + repeat = false; + attempts++; + + switch (inverter) + { + case E_Inverter::LLT: + { + // Try 1: Cholesky factorization (dposv) - fastest but requires positive definite + info = LapackWrapper::dposv( + LapackWrapper::COL_MAJOR, + 'U', // Upper triangle + n, // Order of matrix + nrhs, // Number of right-hand sides + Q.data(), // Matrix (modified on output) + n, // Leading dimension + B.data(), // RHS on input, solution on output + n // Leading dimension of B + ); + + if (info != 0) + { + BOOST_LOG_TRIVIAL(warning) << "LapackWrapper::dposv failed with info = " << info + << ", falling back to LDLT"; + + // Restore from backup and retry with LDLT + Q = Q_backup; + B = B_backup; + inverter = E_Inverter::LDLT; + repeat = true; + continue; + } + + break; + } + + case E_Inverter::LDLT: + { + // Try 2: Symmetric indefinite factorization (dsysv) + std::vector ipiv(n); + + info = LapackWrapper::dsysv( + LapackWrapper::COL_MAJOR, + 'U', // Upper triangle + n, // Order of matrix + nrhs, // Number of right-hand sides + Q.data(), // Matrix (modified on output) + n, // Leading dimension + ipiv.data(), // Pivot indices + B.data(), // RHS on input, solution on output + n // Leading dimension of B + ); + + if (info != 0) + { + BOOST_LOG_TRIVIAL(warning) << "LapackWrapper::dsysv failed with info = " << info + << ", falling back to LU factorization"; + + // Restore from backup and retry with LU + Q = Q_backup; + B = B_backup; + inverter = E_Inverter::INV; + repeat = true; + continue; + } + + break; + } + + case E_Inverter::INV: + { + // Try 3: General LU factorization (dgesv) + std::vector ipiv(n); + + info = LapackWrapper::dgesv( + LapackWrapper::COL_MAJOR, + n, // Order of matrix + nrhs, // Number of right-hand sides + Q.data(), // Matrix (modified on output) + n, // Leading dimension + ipiv.data(), // Pivot indices + B.data(), // RHS on input, solution on output + n // Leading dimension of B + ); + + if (info != 0) + { + BOOST_LOG_TRIVIAL(error) << "LapackWrapper::dgesv failed with info = " << info + << " - all solver methods exhausted"; + return false; + } + + break; + } + + default: + { + BOOST_LOG_TRIVIAL(error) << "Unknown inverter type: " << inverter; + return false; + } + } + } + + if (attempts >= max_attempts) + { + BOOST_LOG_TRIVIAL( + error + ) << "Maximum solver attempts reached - possible configuration error"; + return false; + } + + return true; +} + +// Compute Kalman gain K = P*H'*Q^-1 using BLAS/LAPACKE +bool KalmanFilterBLAS::computeKalmanGain( + const MatrixXd& H, + const MatrixXd& P, + MatrixXd& Q, + MatrixXd& K, + MatrixXd* Qinv, + E_Inverter inverter +) +{ + int numH = H.rows(); + int numX = H.cols(); + + // Resize outputs if needed + if (K.rows() != numX || K.cols() != numH) + { + K.resize(numX, numH); + } + + // Step 1: Compute HP = H * P + MatrixXd HP(numH, numX); + computeHP(H, P, HP); + + // Step 2: Solve Q * K' = HP for K' + // This gives us K' = Q^-1 * HP, then K = (K')' = HP' * Q^-1 = P*H'*Q^-1 + MatrixXd KT = HP; // Copy HP, will be overwritten with solution + + if (!solveLinearSystem(Q, KT, inverter)) + { + return false; + } + + // Step 3: Transpose to get K = (K')' + K = KT.transpose(); + + // Step 4: Optionally compute Q inverse + if (Qinv != nullptr) + { + // Solve Q * I = I for Q^-1 + Qinv->resize(numH, numH); + Qinv->setIdentity(); + + MatrixXd Q_copy = Q; // Need copy since Q was modified by previous solve + + if (!solveLinearSystem(Q_copy, *Qinv, inverter)) + { + return false; + } + } + + return true; +} + +// Update state vector using BLAS +void KalmanFilterBLAS::updateStateVector( + const VectorXd& x, + const MatrixXd& K, + const VectorXd& v, + VectorXd& xp, + VectorXd& dx, + int begX, + int numX, + int begH, + int numH +) +{ + // Extract subvectors + auto v_sub = v.segment(begH, numH); + + // Compute dx = K * v for the substate + // y = alpha*A*x + beta*y + LapackWrapper::dgemv( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + numX, // Rows of K + numH, // Cols of K + 1.0, // alpha + K.data(), // Matrix K + K.rows(), // Leading dimension + v_sub.data(), // Vector v + 1, // Stride of v + 0.0, // beta + dx.data() + begX, // Output vector dx (starting at begX) + 1 // Stride of dx + ); + + // Compute xp = x + dx + LapackWrapper::dcopy(numX, x.data() + begX, 1, xp.data() + begX, 1); + LapackWrapper::daxpy(numX, 1.0, dx.data() + begX, 1, xp.data() + begX, 1); +} + +// Update covariance using standard form: Pp = P - K*H*P +void KalmanFilterBLAS::updateCovarianceStandard( + const MatrixXd& P, + const MatrixXd& K, + const MatrixXd& H, + const MatrixXd& HP, + MatrixXd& Pp +) +{ + int numX = P.rows(); + int numH = H.rows(); + + // Resize output if needed + if (Pp.rows() != numX || Pp.cols() != numX) + { + Pp.resize(numX, numX); + } + + // Step 1: Copy P into Pp + std::memcpy(Pp.data(), P.data(), numX * numX * sizeof(double)); + + // Step 2: Compute KHP = K * HP + MatrixXd KHP(numX, numX); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numX, // Rows of K (and KHP) + numX, // Cols of HP (and KHP) + numH, // Cols of K / rows of HP + 1.0, // alpha + K.data(), // Matrix K + K.rows(), // Leading dimension of K + HP.data(), // Matrix HP + HP.rows(), // Leading dimension of HP + 0.0, // beta + KHP.data(), // Output matrix KHP + KHP.rows() // Leading dimension of KHP + ); + + // Step 3: Pp = Pp - KHP = P - K*H*P + LapackWrapper::daxpy(numX * numX, -1.0, KHP.data(), 1, Pp.data(), 1); + + // Symmetrize for numerical stability + symmetrizeMatrix(Pp); +} + +// Update covariance using Joseph form: Pp = (I-K*H)*P*(I-K*H)' + K*R*K' +void KalmanFilterBLAS::updateCovarianceJoseph( + const MatrixXd& P, + const MatrixXd& K, + const MatrixXd& H, + const MatrixXd& R, + MatrixXd& Pp +) +{ + int numX = P.rows(); + int numH = H.rows(); + + // Resize output if needed + if (Pp.rows() != numX || Pp.cols() != numX) + { + Pp.resize(numX, numX); + } + + // Step 1: Compute IKH = I - K*H + MatrixXd IKH = MatrixXd::Identity(numX, numX); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numX, // Rows of K (and IKH) + numX, // Cols of H (and IKH) + numH, // Cols of K / rows of H + -1.0, // alpha = -1 (subtract) + K.data(), // Matrix K + K.rows(), // Leading dimension of K + H.data(), // Matrix H + H.rows(), // Leading dimension of H + 1.0, // beta = 1 (add to identity) + IKH.data(), // Output matrix IKH + IKH.rows() // Leading dimension of IKH + ); + + // Step 2: Compute temp = IKH * P + MatrixXd temp(numX, numX); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numX, // Rows + numX, // Cols + numX, // Inner dimension + 1.0, // alpha + IKH.data(), // Matrix IKH + IKH.rows(), // Leading dimension + P.data(), // Matrix P + P.rows(), // Leading dimension + 0.0, // beta + temp.data(), // Output + temp.rows() // Leading dimension + ); + + // Step 3: Compute Pp = temp * IKH' = IKH * P * IKH' + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasTrans, + numX, // Rows + numX, // Cols + numX, // Inner dimension + 1.0, // alpha + temp.data(), // Matrix temp + temp.rows(), // Leading dimension + IKH.data(), // Matrix IKH (will be transposed) + IKH.rows(), // Leading dimension + 0.0, // beta + Pp.data(), // Output + Pp.rows() // Leading dimension + ); + + // Step 4: Add K*R*K' term + // temp2 = K * R + MatrixXd temp2(numX, numH); + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + numX, // Rows of K + numH, // Cols of R + numH, // Cols of K / rows of R + 1.0, // alpha + K.data(), // Matrix K + K.rows(), // Leading dimension + R.data(), // Matrix R + R.rows(), // Leading dimension + 0.0, // beta + temp2.data(), // Output + temp2.rows() // Leading dimension + ); + + // Pp += temp2 * K' = Pp + K*R*K' + LapackWrapper::dgemm( + LapackWrapper::CblasColMajor, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasTrans, + numX, // Rows + numX, // Cols + numH, // Inner dimension + 1.0, // alpha + temp2.data(), // Matrix temp2 + temp2.rows(), // Leading dimension + K.data(), // Matrix K (will be transposed) + K.rows(), // Leading dimension + 1.0, // beta = 1 (accumulate) + Pp.data(), // Output + Pp.rows() // Leading dimension + ); + + // Symmetrize for numerical stability + symmetrizeMatrix(Pp); +} + +// Symmetrize matrix: A = (A + A') / 2 +void KalmanFilterBLAS::symmetrizeMatrix(MatrixXd& A) +{ + int n = A.rows(); + + for (int i = 0; i < n; i++) + { + for (int j = i + 1; j < n; j++) + { + double avg = (A(i, j) + A(j, i)) / 2.0; + A(i, j) = avg; + A(j, i) = avg; + } + } +} diff --git a/src/cpp/common/kalmanBlas.hpp b/src/cpp/common/kalmanBlas.hpp new file mode 100644 index 000000000..d1542fa7b --- /dev/null +++ b/src/cpp/common/kalmanBlas.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/lapackWrapper.hpp" + +/** + * @brief BLAS/LAPACK optimized operations for Kalman filtering + * + * This class provides high-performance implementations of core Kalman filter + * operations using CBLAS and LapackWrapper while maintaining Eigen matrix interfaces + * for compatibility with existing code. + * + * Design principle: Single Responsibility - handles only mathematical computations + */ +class KalmanFilterBLAS +{ + public: + /** + * @brief Compute innovation covariance Q = H*P*H' + R using BLAS + * + * @param H Design matrix (numH x numX) + * @param P State covariance (numX x numX) + * @param R Measurement noise (numH x numH) + * @param Q Output innovation covariance (numH x numH) + */ + static void computeInnovationCovariance( + const MatrixXd& H, + const MatrixXd& P, + const MatrixXd& R, + MatrixXd& Q + ); + + /** + * @brief Compute Kalman gain K = P*H'*Q^-1 using BLAS/LAPACKE + * + * @param H Design matrix (numH x numX) + * @param P State covariance (numX x numX) + * @param Q Innovation covariance (numH x numH) - will be modified! + * @param K Output Kalman gain (numX x numH) + * @param Qinv Optional output for Q inverse + * @param inverter Inversion method to use + * @return true if successful, false if inversion failed + */ + static bool computeKalmanGain( + const MatrixXd& H, + const MatrixXd& P, + MatrixXd& Q, // Non-const: will be modified by LAPACK + MatrixXd& K, + MatrixXd* Qinv, + E_Inverter inverter + ); + + /** + * @brief Compute H*P matrix product using BLAS + * + * @param H Design matrix (numH x numX) + * @param P State covariance (numX x numX) + * @param HP Output matrix (numH x numX) + */ + static void computeHP(const MatrixXd& H, const MatrixXd& P, MatrixXd& HP); + + /** + * @brief Update state vector: xp = x + K*v using BLAS + * + * @param x Prior state (numX) + * @param K Kalman gain (numX x numH) + * @param v Innovation vector (numH) + * @param xp Output posterior state (numX) + * @param dx Output state change (numX) + * @param begX Starting index in full state vector + * @param numX Number of states to update + * @param begH Starting index in measurement vector + * @param numH Number of measurements + */ + static void updateStateVector( + const VectorXd& x, + const MatrixXd& K, + const VectorXd& v, + VectorXd& xp, + VectorXd& dx, + int begX, + int numX, + int begH, + int numH + ); + + /** + * @brief Update covariance: Pp = P - K*H*P (standard form) using BLAS + * + * @param P Prior covariance (numX x numX) + * @param K Kalman gain (numX x numH) + * @param H Design matrix (numH x numX) + * @param HP Pre-computed H*P (numH x numX) + * @param Pp Output posterior covariance (numX x numX) + */ + static void updateCovarianceStandard( + const MatrixXd& P, + const MatrixXd& K, + const MatrixXd& H, + const MatrixXd& HP, + MatrixXd& Pp + ); + + /** + * @brief Update covariance: Pp = (I-K*H)*P*(I-K*H)' + K*R*K' (Joseph form) using BLAS + * + * @param P Prior covariance (numX x numX) + * @param K Kalman gain (numX x numH) + * @param H Design matrix (numH x numH) + * @param R Measurement noise (numH x numH) + * @param Pp Output posterior covariance (numX x numX) + */ + static void updateCovarianceJoseph( + const MatrixXd& P, + const MatrixXd& K, + const MatrixXd& H, + const MatrixXd& R, + MatrixXd& Pp + ); + + /** + * @brief Solve Q*X = B for multiple right-hand sides using LAPACKE + * + * @param Q Coefficient matrix (n x n) - will be modified! + * @param B Right-hand side / solution matrix (n x m) - will be modified! + * @param inverter Solver method to use + * @return true if successful, false otherwise + */ + static bool solveLinearSystem(MatrixXd& Q, MatrixXd& B, E_Inverter inverter); + + private: + /** + * @brief Symmetrize matrix: A = (A + A') / 2 + * + * @param A Matrix to symmetrize (modified in place) + */ + static void symmetrizeMatrix(MatrixXd& A); +}; diff --git a/src/cpp/common/lapackWrapper.hpp b/src/cpp/common/lapackWrapper.hpp new file mode 100644 index 000000000..279ddc3c2 --- /dev/null +++ b/src/cpp/common/lapackWrapper.hpp @@ -0,0 +1,473 @@ +#pragma once + +#include + +namespace LapackWrapper +{ +// Matrix layout enum to match LAPACKE interface +enum class Layout +{ + ColMajor = 102, + RowMajor = 101 +}; + +// Forward declare Fortran LAPACK/BLAS functions +extern "C" +{ + void dposv_( + const char* uplo, + const int* n, + const int* nrhs, + double* a, + const int* lda, + double* b, + const int* ldb, + int* info + ); + void dsysv_( + const char* uplo, + const int* n, + const int* nrhs, + double* a, + const int* lda, + int* ipiv, + double* b, + const int* ldb, + double* work, + const int* lwork, + int* info + ); + void dgetrf_(const int* m, const int* n, double* a, const int* lda, int* ipiv, int* info); + void dgetrs_( + const char* trans, + const int* n, + const int* nrhs, + const double* a, + const int* lda, + const int* ipiv, + double* b, + const int* ldb, + int* info + ); + void dpotrf_(const char* uplo, const int* n, double* a, const int* lda, int* info); + void dpotrs_( + const char* uplo, + const int* n, + const int* nrhs, + const double* a, + const int* lda, + double* b, + const int* ldb, + int* info + ); + void dpotri_(const char* uplo, const int* n, double* a, const int* lda, int* info); + void dsytrf_( + const char* uplo, + const int* n, + double* a, + const int* lda, + int* ipiv, + double* work, + const int* lwork, + int* info + ); + void dsytrs_( + const char* uplo, + const int* n, + const int* nrhs, + const double* a, + const int* lda, + const int* ipiv, + double* b, + const int* ldb, + int* info + ); + void dsytri_( + const char* uplo, + const int* n, + double* a, + const int* lda, + const int* ipiv, + double* work, + int* info + ); + void dgetri_( + const int* n, + double* a, + const int* lda, + const int* ipiv, + double* work, + const int* lwork, + int* info + ); +} + +// 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) +extern "C" +{ + void dgemm_( + const char* transa, + const char* transb, + const int* m, + const int* n, + const int* k, + const double* alpha, + const double* a, + const int* lda, + const double* b, + const int* ldb, + const double* beta, + double* c, + const int* ldc + ); + void dgemv_( + const char* trans, + const int* m, + const int* n, + const double* alpha, + const double* a, + const int* lda, + const double* x, + const int* incx, + const double* beta, + double* y, + const int* incy + ); + void dcopy_(const int* n, const double* x, const int* incx, double* y, const int* incy); + void daxpy_( + const int* n, + const double* alpha, + const double* x, + const int* incx, + double* y, + const int* incy + ); +} +#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) +{ + if (layout != Layout::ColMajor) + return -1; // Only column-major supported with Fortran LAPACK + + int info = 0; + dpotrf_(&uplo, &n, a, &lda, &info); + return info; +} // Solve using Cholesky factorization +inline int +dpotrs(Layout layout, char uplo, int n, int nrhs, const double* a, int lda, double* b, int ldb) +{ + if (layout != Layout::ColMajor) + return -1; + + int info = 0; + dpotrs_(&uplo, &n, &nrhs, a, &lda, b, &ldb, &info); + return info; +} // Inverse using Cholesky factorization +inline int dpotri(Layout layout, char uplo, int n, double* a, int lda) +{ + if (layout != Layout::ColMajor) + return -1; + + int info = 0; + dpotri_(&uplo, &n, a, &lda, &info); + return info; +} // Symmetric indefinite factorization +inline int dsytrf(Layout layout, char uplo, int n, double* a, int lda, int* ipiv) +{ + if (layout != Layout::ColMajor) + return -1; + + // Query workspace size + int lwork = -1; + double work_query; + int info = 0; + + dsytrf_(&uplo, &n, a, &lda, ipiv, &work_query, &lwork, &info); + + if (info != 0) + return info; + + // Allocate workspace and perform factorization + lwork = static_cast(work_query); + std::vector work(lwork); + dsytrf_(&uplo, &n, a, &lda, ipiv, work.data(), &lwork, &info); + return info; +} + +// Solve using symmetric indefinite factorization +inline int dsytrs( + Layout layout, + char uplo, + int n, + int nrhs, + const double* a, + int lda, + const int* ipiv, + double* b, + int ldb +) +{ + if (layout != Layout::ColMajor) + return -1; + + int info = 0; + dsytrs_(&uplo, &n, &nrhs, a, &lda, ipiv, b, &ldb, &info); + return info; +} // Inverse using symmetric indefinite factorization +inline int dsytri(Layout layout, char uplo, int n, double* a, int lda, int* ipiv) +{ + if (layout != Layout::ColMajor) + return -1; + + // Allocate workspace (dsytri requires work array of size n) + std::vector work(n); + int info = 0; + + dsytri_(&uplo, &n, a, &lda, ipiv, work.data(), &info); + return info; +} + +// LU factorization +inline int dgetrf(Layout layout, int m, int n, double* a, int lda, int* ipiv) +{ + if (layout != Layout::ColMajor) + return -1; + + int info = 0; + dgetrf_(&m, &n, a, &lda, ipiv, &info); + return info; +} + +// Solve using LU factorization +inline int dgetrs( + Layout layout, + char trans, + int n, + int nrhs, + const double* a, + int lda, + const int* ipiv, + double* b, + int ldb +) +{ + if (layout != Layout::ColMajor) + return -1; + + int info = 0; + dgetrs_(&trans, &n, &nrhs, a, &lda, ipiv, b, &ldb, &info); + return info; +} // Inverse using LU factorization +inline int dgetri(Layout layout, int n, double* a, int lda, int* ipiv) +{ + if (layout != Layout::ColMajor) + return -1; + + // Query workspace size + int lwork = -1; + double work_query; + int info = 0; + + dgetri_(&n, a, &lda, ipiv, &work_query, &lwork, &info); + + if (info != 0) + return info; + + // Allocate workspace and perform inversion + lwork = static_cast(work_query); + std::vector work(lwork); + dgetri_(&n, a, &lda, ipiv, work.data(), &lwork, &info); + + return info; +} + +// Combined solve with Cholesky (factorize + solve) +inline int dposv(Layout layout, char uplo, int n, int nrhs, double* a, int lda, double* b, int ldb) +{ + if (layout != Layout::ColMajor) + return -1; + + int info = 0; + dposv_(&uplo, &n, &nrhs, a, &lda, b, &ldb, &info); + return info; +} // Combined solve with symmetric indefinite (factorize + solve) +inline int +dsysv(Layout layout, char uplo, int n, int nrhs, double* a, int lda, int* ipiv, double* b, int ldb) +{ + if (layout != Layout::ColMajor) + return -1; + + // Query workspace size + int lwork = -1; + double work_query; + int info = 0; + + dsysv_(&uplo, &n, &nrhs, a, &lda, ipiv, b, &ldb, &work_query, &lwork, &info); + + if (info != 0) + return info; + + // Allocate workspace and perform solve + lwork = static_cast(work_query); + std::vector work(lwork); + dsysv_(&uplo, &n, &nrhs, a, &lda, ipiv, b, &ldb, work.data(), &lwork, &info); + return info; +} + +// Combined solve with LU (factorize + solve) - general matrix +inline int dgesv(Layout layout, int n, int nrhs, double* a, int lda, int* ipiv, double* b, int ldb) +{ + if (layout != Layout::ColMajor) + return -1; + + int info = 0; + int m = n; // For square matrix + // dgesv requires factorization first + dgetrf_(&m, &n, a, &lda, ipiv, &info); + if (info == 0) + { + char trans = 'N'; + dgetrs_(&trans, &n, &nrhs, a, &lda, ipiv, b, &ldb, &info); + } + return info; +} // Define constants to match LAPACKE/CBLAS +constexpr Layout COL_MAJOR = Layout::ColMajor; +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 +{ + NoTrans = 111, + Trans = 112, + ConjTrans = 113 +}; + +constexpr Transpose CblasNoTrans = Transpose::NoTrans; +constexpr Transpose CblasTrans = Transpose::Trans; +constexpr Transpose CblasConjTrans = Transpose::ConjTrans; +constexpr Layout CblasColMajor = Layout::ColMajor; +constexpr Layout CblasRowMajor = Layout::RowMajor; + +// Helper to convert Transpose enum to char +inline char transpose_to_char(Transpose trans) +{ + switch (trans) + { + case Transpose::NoTrans: + return 'N'; + case Transpose::Trans: + return 'T'; + case Transpose::ConjTrans: + return 'C'; + default: + return 'N'; + } +} + +// Matrix-matrix multiply: C = alpha*op(A)*op(B) + beta*C +inline void dgemm( + Layout layout, + Transpose transa, + Transpose transb, + int m, + int n, + int k, + double alpha, + const double* a, + int lda, + const double* b, + int ldb, + double beta, + double* c, + int ldc +) +{ + if (layout != Layout::ColMajor) + { + // For row-major, swap and transpose + // This is a simplified version - full implementation would need more work + return; + } + + char ta = transpose_to_char(transa); + char tb = transpose_to_char(transb); + dgemm_( + &ta, + &tb, + &m, + &n, + &k, + &alpha, + const_cast(a), + &lda, + const_cast(b), + &ldb, + &beta, + c, + &ldc + ); +} + +// Matrix-vector multiply: y = alpha*op(A)*x + beta*y +inline void dgemv( + Layout layout, + Transpose trans, + int m, + int n, + double alpha, + const double* a, + int lda, + const double* x, + int incx, + double beta, + double* y, + int incy +) +{ + if (layout != Layout::ColMajor) + { + return; + } + + char t = transpose_to_char(trans); + dgemv_( + &t, + &m, + &n, + &alpha, + const_cast(a), + &lda, + const_cast(x), + &incx, + &beta, + y, + &incy + ); +} + +// Vector copy: y = x +inline void dcopy(int n, const double* x, int incx, double* y, int incy) +{ + dcopy_(&n, const_cast(x), &incx, y, &incy); +} + +// Vector addition: y = alpha*x + y +inline void daxpy(int n, double alpha, const double* x, int incx, double* y, int incy) +{ + daxpy_(&n, &alpha, const_cast(x), &incx, y, &incy); +} + +} // namespace LapackWrapper diff --git a/src/cpp/common/linearCombo.cpp b/src/cpp/common/linearCombo.cpp index c7a5a3c73..cfb631cbd 100644 --- a/src/cpp/common/linearCombo.cpp +++ b/src/cpp/common/linearCombo.cpp @@ -1,263 +1,344 @@ - // #pragma GCC optimize ("O0") -#include "observations.hpp" -#include "linearCombo.hpp" -#include "navigation.hpp" -#include "testUtils.hpp" -#include "satStat.hpp" -#include "debug.hpp" -#include "acsQC.hpp" -#include "trace.hpp" - +#include "common/linearCombo.hpp" +#include "common/acsQC.hpp" +#include "common/debug.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/satStat.hpp" +#include "common/trace.hpp" /** Create combinations between specific observation values -*/ + */ S_LC getLC( - double L_A_m, ///< Phase measurement A (in meters) - double L_B_m, ///< Phase measurement B (in meters) - double P_A_m, ///< Code measurement A (in meters) - double P_B_m, ///< Code measurement B (in meters) - double lamA, ///< Wavelength A - double lamB, ///< Wavelength B - double* c1_out, ///< Ionosphere free coefficient 1 - double* c2_out) ///< Ionosphere free coefficient 2 + double L_A_m, ///< Phase measurement A (in meters) + double L_B_m, ///< Phase measurement B (in meters) + double P_A_m, ///< Code measurement A (in meters) + double P_B_m, ///< Code measurement B (in meters) + double lamA, ///< Wavelength A + double lamB, ///< Wavelength B + double* c1_out, ///< Ionosphere free coefficient 1 + double* c2_out ///< Ionosphere free coefficient 2 +) { - S_LC lc = {}; - - if ( P_A_m == 0 - ||P_B_m == 0 - ||L_A_m == 0 - ||L_B_m == 0) - { -// tracepde(lv, fppde, "PDE, code observation insufficient\n"); - return lc; - } + S_LC lc = {}; - lc.lam_A = lamA; - lc.lam_B = lamB; + if (P_A_m == 0 || P_B_m == 0 || L_A_m == 0 || L_B_m == 0) + { + // tracepde(lv, fppde, "PDE, code observation insufficient\n"); + return lc; + } - double L_A_c = L_A_m / lamA; - double L_B_c = L_B_m / lamB; - double P_A_c = P_A_m / lamA; - double P_B_c = P_B_m / lamB; + lc.lam_A = lamA; + lc.lam_B = lamB; - /* phase gf, wl, mw */ - double c1 = lamB * lamB / (lamB * lamB - lamA * lamA); /* IF */ - double c2 = lamA * lamA / (lamB * lamB - lamA * lamA); -// double c3 = lamB / (lamB - lamA); /* WL */ -// double c4 = lamA / (lamB - lamA); + double L_A_c = L_A_m / lamA; + double L_B_c = L_B_m / lamB; + double P_A_c = P_A_m / lamA; + double P_B_c = P_B_m / lamB; - if (c1_out) *c1_out = c1; - if (c2_out) *c2_out = c2; + /* phase gf, wl, mw */ + double c1 = lamB * lamB / (lamB * lamB - lamA * lamA); /* IF */ + double c2 = lamA * lamA / (lamB * lamB - lamA * lamA); + // double c3 = lamB / (lamB - lamA); /* WL */ + // double c4 = lamA / (lamB - lamA); -// lc.GF_Phas_m = L_A_m - L_B_m; -// lc.IF_Phas_m = c1 * L_A_m - c2 * L_B_m; -// lc.WL_Phas_m = c3 * L_A_m - c4 * L_B_m; + if (c1_out) + *c1_out = c1; + if (c2_out) + *c2_out = c2; + // lc.GF_Phas_m = L_A_m - L_B_m; + // lc.IF_Phas_m = c1 * L_A_m - c2 * L_B_m; + // lc.WL_Phas_m = c3 * L_A_m - c4 * L_B_m; -// c3 = lamB / (lamB + lamA); /* MW */ -// c4 = lamA / (lamB + lamA); -// -// lc.GF_Code_m = P_A_m - P_B_m; /* geometry-free codes are independent from phase */ -// lc.IF_Code_m = c1 * P_A_m - c2 * P_B_m; -// lc.NL_Code_m = c3 * P_A_m + c4 * P_B_m; -// -// double lamw = lamA * lamB / (lamB - lamA); -// lc.MW_c = (lc.WL_Phas_m - lc.NL_Code_m) / lamw; /* cycle */ + // c3 = lamB / (lamB + lamA); /* MW */ + // c4 = lamA / (lamB + lamA); + // + // lc.GF_Code_m = P_A_m - P_B_m; /* geometry-free codes are independent from phase */ + // lc.IF_Code_m = c1 * P_A_m - c2 * P_B_m; + // lc.NL_Code_m = c3 * P_A_m + c4 * P_B_m; + // + // double lamw = lamA * lamB / (lamB - lamA); + // lc.MW_c = (lc.WL_Phas_m - lc.NL_Code_m) / lamw; /* cycle */ + lc.lam_WL = lamA * lamB / (lamB - lamA); + lc.lam_NL = lamA * lamB / (lamB + lamA); - lc.lam_WL = lamA * lamB / (lamB - lamA); - lc.lam_NL = lamA * lamB / (lamB + lamA); + lc.WL_Phas_c = L_A_c - L_B_c; + lc.WL_Code_c = P_A_c - P_B_c; - lc.WL_Phas_c = L_A_c - L_B_c; - lc.WL_Code_c = P_A_c - P_B_c; + lc.NL_Phas_c = L_A_c + L_B_c; + lc.NL_Code_c = P_A_c + P_B_c; - lc.NL_Phas_c = L_A_c + L_B_c; - lc.NL_Code_c = P_A_c + P_B_c; + lc.GF_Phas_m = L_A_m - L_B_m; + lc.GF_Code_m = P_A_m - P_B_m; - lc.GF_Phas_m = L_A_m - L_B_m; - lc.GF_Code_m = P_A_m - P_B_m; + lc.IF_Phas_m = c1 * L_A_m - c2 * L_B_m; + lc.IF_Code_m = c1 * P_A_m - c2 * P_B_m; - lc.IF_Phas_m = c1 * L_A_m - c2 * L_B_m; - lc.IF_Code_m = c1 * P_A_m - c2 * P_B_m; + lc.WL_Phas_m = lc.WL_Phas_c * lc.lam_WL; + lc.WL_Code_m = lc.WL_Code_c * lc.lam_NL; - lc.WL_Phas_m = lc.WL_Phas_c * lc.lam_WL; - lc.WL_Code_m = lc.WL_Code_c * lc.lam_NL; + lc.NL_Phas_m = lc.NL_Phas_c * lc.lam_WL; + lc.NL_Code_m = lc.NL_Code_c * lc.lam_NL; - lc.NL_Phas_m = lc.NL_Phas_c * lc.lam_WL; - lc.NL_Code_m = lc.NL_Code_c * lc.lam_NL; + lc.MW_m = lc.WL_Phas_m - lc.NL_Code_m; + lc.MW_c = lc.MW_m / lc.lam_WL; - lc.MW_m = lc.WL_Phas_m - lc.NL_Code_m; - lc.MW_c = lc.MW_m / lc.lam_WL; - - lc.valid = true; - return lc; + lc.valid = true; + return lc; } /** Get combinations from pre-computed values, or return an empty value -*/ + */ S_LC& getLC( - lc_t& lcBase, ///< Linear combination base object - E_FType fA, ///< Frequency type A - E_FType fB) ///< Frequency type B + lc_t& lcBase, ///< Linear combination base object + E_FType fA, ///< Frequency type A + E_FType fB ///< Frequency type B +) { - if (fA > fB) - std::swap(fA, fB); + if (fA > fB) + std::swap(fA, fB); - //try to get existing LC from the observation's satStat object - return lcBase.lcMap[{fA, fB}]; + // try to get existing LC from the observation's satStat object + return lcBase.lcMap[{fA, fB}]; } /** Get/calculate linear combination values for an observation -*/ + */ S_LC& getLC( - GObs& obs, ///< Observation to compute values form - lc_t& lcBase, ///< Linear combination base object - E_FType fA, ///< Frequency type A - E_FType fB) ///< Frequency type B + GObs& obs, ///< Observation to compute values form + lc_t& lcBase, ///< Linear combination base object + E_FType fA, ///< Frequency type A + E_FType fB ///< Frequency type B +) { - //try to get existing LC from the observation's satStat object - S_LC& lc = getLC(lcBase, fA, fB); - - if (lc.valid) - { - return lc; - } - - //make a new linear combination from the observation - lcBase.time = obs.time; - for (E_FType f : {fA, fB}) - { - if (lcBase.L_m[f] == 0) - { - //no L measurement, try to get from observation - lcBase.L_m[f] = obs.sigs[f].L * obs.satNav_ptr->lamMap[f]; - lcBase.P[f] = obs.sigs[f].P; - } - if (lcBase.L_m[f] == 0) - { - //still no measurement, give up - return lc; - } - } - - double L_A = lcBase.L_m[fA]; - double L_B = lcBase.L_m[fB]; - double P_A = lcBase.P[fA]; - double P_B = lcBase.P[fB]; - double lamA = obs.satNav_ptr->lamMap[fA]; - double lamB = obs.satNav_ptr->lamMap[fB]; - - lc = getLC(L_A, L_B, P_A, P_B, lamA, lamB, nullptr, nullptr); - - //special cases - if (fB == F5 && (obs.Sat.sys == +E_Sys::GAL || obs.Sat.sys == +E_Sys::BDS)) - lc.MW_c *= -1; /* cycle */ - - if (fA == F1 && fB == F2) - { - lcBase.mp[F1] = P_A - L_A - 2.0 * lamA * lamA / (lamB * lamB - lamA * lamA) * (L_A - L_B); - lcBase.mp[F2] = P_B - L_B - 2.0 * lamB * lamB / (lamB * lamB - lamA * lamA) * (L_A - L_B); - } - else if (fB == F5 && lcBase.mp[F5] == 0) - { - lcBase.mp[F5] = P_B - L_B - 2.0 * lamB * lamB / (lamB * lamB - lamA * lamA) * (L_A - L_B); - } - - lc.valid = true; - return lc; + // try to get existing LC from the observation's satStat object + S_LC& lc = getLC(lcBase, fA, fB); + + if (lc.valid) + { + return lc; + } + + // make a new linear combination from the observation + lcBase.time = obs.time; + for (E_FType f : {fA, fB}) + { + if (lcBase.L_m[f] == 0) + { + // no L measurement, try to get from observation + lcBase.L_m[f] = obs.sigs[f].L * obs.satNav_ptr->lamMap[f]; + lcBase.P[f] = obs.sigs[f].P; + } + if (lcBase.L_m[f] == 0) + { + // still no measurement, give up + return lc; + } + } + + double L_A = lcBase.L_m[fA]; + double L_B = lcBase.L_m[fB]; + double P_A = lcBase.P[fA]; + double P_B = lcBase.P[fB]; + double lamA = obs.satNav_ptr->lamMap[fA]; + double lamB = obs.satNav_ptr->lamMap[fB]; + + lc = getLC(L_A, L_B, P_A, P_B, lamA, lamB, nullptr, nullptr); + + // special cases + if (fB == F5 && (obs.Sat.sys == E_Sys::GAL || obs.Sat.sys == E_Sys::BDS)) + lc.MW_c *= -1; /* cycle */ + + if (fA == F1 && fB == F2) + { + lcBase.mp[F1] = P_A - L_A - 2.0 * lamA * lamA / (lamB * lamB - lamA * lamA) * (L_A - L_B); + lcBase.mp[F2] = P_B - L_B - 2.0 * lamB * lamB / (lamB * lamB - lamA * lamA) * (L_A - L_B); + } + else if (fB == F5 && lcBase.mp[F5] == 0) + { + lcBase.mp[F5] = P_B - L_B - 2.0 * lamB * lamB / (lamB * lamB - lamA * lamA) * (L_A - L_B); + } + + lc.valid = true; + return lc; } /** Prepare a base object for linear combinations using observation data -*/ + */ void lcPrepareBase( - GObs& obs, ///< Observation data to use - lc_t& lcBase) ///< Linear combination base object to prepare + GObs& obs, ///< Observation data to use + lc_t& lcBase ///< Linear combination base object to prepare +) { - lcBase.time = obs.time; - lcBase.Sat = obs.Sat; - - for (auto& [ft, sig] : obs.sigs) - { - //populate variables for later use. - lcBase.L_m[ft] = sig.L * obs.satNav_ptr->lamMap[ft]; - lcBase.P[ft] = sig.P; - } + lcBase.time = obs.time; + lcBase.Sat = obs.Sat; + + for (auto& [ft, sig] : obs.sigs) + { + // populate variables for later use. + lcBase.L_m[ft] = sig.L * obs.satNav_ptr->lamMap[ft]; + lcBase.P[ft] = sig.P; + } } /** Function to prepare some predefined linear combinations from an observation -*/ + */ void obs2lc( - Trace& trace, ///< Trace to output to - GObs& obs, ///< Observation to prepare combinations for - lc_t& lcBase) ///< Linear combination base object to use + Trace& trace, ///< Trace to output to + GObs& obs, ///< Observation to prepare combinations for + lc_t& lcBase ///< Linear combination base object to use +) { - int sys = obs.Sat.sys; - - E_FType frq1; - E_FType frq2; - E_FType frq3; - - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - - if (pass == false) - return; - - char strprefix[64]; - snprintf(strprefix, sizeof(strprefix), "%3s sat=%4s", obs.time.to_string().c_str(), obs.Sat.id().c_str()); - - lcPrepareBase(obs, lcBase); - - //iterate pairwise over the frequencies. - S_LC& lc12 = getLC(obs, lcBase, frq1, frq2); - S_LC& lc15 = getLC(obs, lcBase, frq1, frq3); - S_LC& lc25 = getLC(obs, lcBase, frq2, frq3); - - tracepdeex(3, trace, "%s zd L -- L1 =%14.4f L2 =%14.4f L5 =%14.4f\n", strprefix, lcBase.L_m [frq1], lcBase.L_m [frq2], lcBase.L_m [frq3]); - tracepdeex(3, trace, "%s zd P -- P1 =%14.4f P2 =%14.4f P5 =%14.4f\n", strprefix, lcBase.P [frq1], lcBase.P [frq2], lcBase.P [frq3]); - tracepdeex(5, trace, "%s mp P -- mp1 =%14.4f mp2 =%14.4f mp5 =%14.4f\n", strprefix, lcBase.mp [frq1], lcBase.mp [frq2], lcBase.mp [frq3]); - tracepdeex(5, trace, "%s gf L -- gf12=%14.4f gf15=%14.4f gf25=%14.4f\n", strprefix, lc12.GF_Phas_m, lc15.GF_Phas_m, lc25.GF_Phas_m); - tracepdeex(5, trace, "%s gf P -- gf12=%14.4f gf15=%14.4f gf25=%14.4f\n", strprefix, lc12.GF_Code_m, lc15.GF_Code_m, lc25.GF_Code_m); - tracepdeex(5, trace, "%s mw L -- mw12=%14.4f mw15=%14.4f mw25=%14.4f\n", strprefix, lc12.MW_c, lc15.MW_c, lc25.MW_c); - tracepdeex(5, trace, "%s wl L -- wl12=%14.4f wl15=%14.4f wl25=%14.4f\n", strprefix, lc12.WL_Phas_m, lc15.WL_Phas_m, lc25.WL_Phas_m); - tracepdeex(5, trace, "%s if L -- if12=%14.4f if15=%14.4f if25=%14.4f\n", strprefix, lc12.IF_Phas_m, lc15.IF_Phas_m, lc25.IF_Phas_m); - tracepdeex(5, trace, "%s if P -- if12=%14.4f if15=%14.4f if25=%14.4f\n", strprefix, lc12.IF_Code_m, lc15.IF_Code_m, lc25.IF_Code_m); - - traceJson(5, trace, obs.time, - { - {"data", __FUNCTION__ }, - {"Sat", obs.Sat.id() } - }, - { - {"L1", lcBase.L_m[frq1] }, {"L2", lcBase.L_m[frq2] } - }); + E_Sys sys = obs.Sat.sys; + + E_FType frq1; + E_FType frq2; + E_FType frq3; + + bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); + + if (pass == false) + return; + + char strprefix[64]; + snprintf( + strprefix, + sizeof(strprefix), + "%3s sat=%4s", + obs.time.to_string().c_str(), + obs.Sat.id().c_str() + ); + + lcPrepareBase(obs, lcBase); + + // iterate pairwise over the frequencies. + S_LC& lc12 = getLC(obs, lcBase, frq1, frq2); + S_LC& lc15 = getLC(obs, lcBase, frq1, frq3); + S_LC& lc25 = getLC(obs, lcBase, frq2, frq3); + + tracepdeex( + 3, + trace, + "%s zd L -- L1 =%14.4f L2 =%14.4f L5 =%14.4f\n", + strprefix, + lcBase.L_m[frq1], + lcBase.L_m[frq2], + lcBase.L_m[frq3] + ); + tracepdeex( + 3, + trace, + "%s zd P -- P1 =%14.4f P2 =%14.4f P5 =%14.4f\n", + strprefix, + lcBase.P[frq1], + lcBase.P[frq2], + lcBase.P[frq3] + ); + tracepdeex( + 3, + trace, + "%s mp P -- mp1 =%14.4f mp2 =%14.4f mp5 =%14.4f\n", + strprefix, + lcBase.mp[frq1], + lcBase.mp[frq2], + lcBase.mp[frq3] + ); + tracepdeex( + 3, + trace, + "%s gf L -- gf12=%14.4f gf15=%14.4f gf25=%14.4f\n", + strprefix, + lc12.GF_Phas_m, + lc15.GF_Phas_m, + lc25.GF_Phas_m + ); + tracepdeex( + 3, + trace, + "%s gf P -- gf12=%14.4f gf15=%14.4f gf25=%14.4f\n", + strprefix, + lc12.GF_Code_m, + lc15.GF_Code_m, + lc25.GF_Code_m + ); + tracepdeex( + 3, + trace, + "%s mw L -- mw12=%14.4f mw15=%14.4f mw25=%14.4f\n", + strprefix, + lc12.MW_c, + lc15.MW_c, + lc25.MW_c + ); + tracepdeex( + 3, + trace, + "%s wl L -- wl12=%14.4f wl15=%14.4f wl25=%14.4f\n", + strprefix, + lc12.WL_Phas_m, + lc15.WL_Phas_m, + lc25.WL_Phas_m + ); + tracepdeex( + 3, + trace, + "%s if L -- if12=%14.4f if15=%14.4f if25=%14.4f\n", + strprefix, + lc12.IF_Phas_m, + lc15.IF_Phas_m, + lc25.IF_Phas_m + ); + tracepdeex( + 3, + trace, + "%s if P -- if12=%14.4f if15=%14.4f if25=%14.4f\n", + strprefix, + lc12.IF_Code_m, + lc15.IF_Code_m, + lc25.IF_Code_m + ); + + traceJson( + 5, + trace, + obs.time, + {{"data", "linearCombos"}, {"Sat", obs.Sat.id()}}, + {{"L1", lcBase.L_m[frq1]}, {"L2", lcBase.L_m[frq2]}} + ); } /** Function to prepare some predefined linear combinations from a list of observations -*/ + */ void obs2lcs( - Trace& trace, ///< Trace to output to - ObsList& obsList) ///< List of bservation to prepare combinations for + Trace& trace, ///< Trace to output to + ObsList& obsList ///< List of bservation to prepare combinations for +) { - int lv = 3; - - if (obsList.empty()) - { - return; - } - - tracepdeex(lv, trace, "\n *-------- PDE form LC %s --------*\n", obsList.front()->time.to_string().c_str()); - - for (auto& obs : only(obsList)) - { - if (obs.exclude) - { - continue; - } - - lc_t& lc = obs.satStat_ptr->lc_new; - obs2lc(trace, obs, lc); - } + int lv = 3; + + if (obsList.empty()) + { + return; + } + + tracepdeex( + lv, + trace, + "\n *-------- PDE form LC %s --------*\n", + obsList.front()->time.to_string().c_str() + ); + + for (auto& obs : only(obsList)) + { + if (obs.exclude) + { + continue; + } + + lc_t& lc = obs.satStat_ptr->lc_new; + obs2lc(trace, obs, lc); + } } - diff --git a/src/cpp/common/linearCombo.hpp b/src/cpp/common/linearCombo.hpp index d157d4248..b84f57756 100644 --- a/src/cpp/common/linearCombo.hpp +++ b/src/cpp/common/linearCombo.hpp @@ -1,68 +1,70 @@ - #pragma once #include +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/satSys.hpp" +#include "common/trace.hpp" using std::map; -#include "satSys.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" - +// forward declarations +struct Navigation; +struct ObsList; +struct GObs; struct S_LC { - bool valid = false; + bool valid = false; - double GF_Phas_m; - double GF_Code_m; + double GF_Phas_m; + double GF_Code_m; - double WL_Phas_m; - double WL_Phas_c; - double WL_Code_m; - double WL_Code_c; + double WL_Phas_m; + double WL_Phas_c; + double WL_Code_m; + double WL_Code_c; - double NL_Phas_m; - double NL_Phas_c; - double NL_Code_m; - double NL_Code_c; + double NL_Phas_m; + double NL_Phas_c; + double NL_Code_m; + double NL_Code_c; - double IF_Phas_m; - double IF_Code_m; + double IF_Phas_m; + double IF_Code_m; - double MW_m; - double MW_c; + double MW_m; + double MW_c; - double lam_A; - double lam_B; - double lam_WL; - double lam_NL; + double lam_A; + double lam_B; + double lam_WL; + double lam_NL; }; struct lc_t { - GTime time; ///< receiver sampling time (GPST) */ - SatSys Sat; + GTime time; ///< receiver sampling time (GPST) */ + SatSys Sat; - map L_m; - map P; - map mp; ///< Multipath info (m) + map L_m; + map P; + map mp; ///< Multipath info (m) - map, S_LC> lcMap; ///< interfrequency linear combination parameters + map, S_LC> lcMap; ///< interfrequency linear combination parameters }; -//forward declarations -struct Navigation; -struct ObsList; -struct GObs; - - -S_LC getLC(double L_A, double L_B, double P_A, double P_B, double lamA, double lamB, double* c1_out, double* c2_out); -S_LC& getLC(lc_t& lcBase, E_FType fA, E_FType fB); -S_LC& getLC(GObs& obs, lc_t& lcBase, E_FType fA, E_FType fB); - -void obs2lcs( - Trace& trace, - ObsList& obsList); - +S_LC getLC( + double L_A, + double L_B, + double P_A, + double P_B, + double lamA, + double lamB, + double* c1_out, + double* c2_out +); +S_LC& getLC(lc_t& lcBase, E_FType fA, E_FType fB); +S_LC& getLC(GObs& obs, lc_t& lcBase, E_FType fA, E_FType fB); + +void obs2lcs(Trace& trace, ObsList& obsList); diff --git a/src/cpp/common/localAtmosRegion.cpp b/src/cpp/common/localAtmosRegion.cpp index 527eb33aa..f666145ce 100644 --- a/src/cpp/common/localAtmosRegion.cpp +++ b/src/cpp/common/localAtmosRegion.cpp @@ -1,412 +1,492 @@ +#include "common/acsConfig.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/coordinates.hpp" +#include "trop/tropModels.hpp" -#include "coordinates.hpp" -#include "tropModels.hpp" -#include "navigation.hpp" -#include "ionoModel.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" +constexpr double DEFAULT_LAT_INTERVAL = 2.5; +constexpr double DEFAULT_LON_INTERVAL = 5.0; - -#define DEFAULT_LAT_INTERVAL 2.5 -#define DEFAULT_LON_INTERVAL 5.0 - -int checkSSRRegion( - VectorPos& pos) +int checkSSRRegion(VectorPos& pos) { - int ssrAtmRegion = -1; - - if (pos[2] < -1000) - return -1; - if (nav.ssrAtm.atmosRegionsMap.empty()) - return -1; - - // tracepdeex(4,std::cout,"\n Checking SSR regions %.4f %.4f... ", pos[0]*R2D, pos[1]*R2D); - - for (auto& [regId, regData] : nav.ssrAtm.atmosRegionsMap) - { - if (pos[0] > regData.maxLatDeg) continue; - if (pos[0] < regData.minLatDeg) continue; - - double midLon = (regData.minLonDeg + regData.maxLonDeg)/2; - double staLon = pos[1]; - if ((staLon - midLon)> 180) staLon -= 360; - else if ((staLon - midLon)<-180) staLon += 360; - - if (staLon > regData.maxLonDeg) continue; - if (staLon < regData.minLonDeg) continue; - - // tracepdeex(4,std::cout," region = %d", regId); - return regId; - } - // tracepdeex(4,std::cout," not found"); - return -1; + int ssrAtmRegion = -1; + + if (pos[2] < -1000) + return -1; + if (nav.ssrAtm.atmosRegionsMap.empty()) + return -1; + + // tracepdeex(4,std::cout,"\n Checking SSR regions %.4f %.4f... ", pos[0]*R2D, pos[1]*R2D); + + for (auto& [regId, regData] : nav.ssrAtm.atmosRegionsMap) + { + if (pos[0] > regData.maxLatDeg) + continue; + if (pos[0] < regData.minLatDeg) + continue; + + double midLon = (regData.minLonDeg + regData.maxLonDeg) / 2; + double staLon = pos[1]; + if ((staLon - midLon) > 180) + staLon -= 360; + else if ((staLon - midLon) < -180) + staLon += 360; + + if (staLon > regData.maxLonDeg) + continue; + if (staLon < regData.minLonDeg) + continue; + + // tracepdeex(4,std::cout," region = %d", regId); + return regId; + } + // tracepdeex(4,std::cout," not found"); + return -1; } /** Initializes local ionosphere maps - Parameters are set from files indicated by nav.SSRAtmMap.AtmosRegions - */ + Parameters are set from files indicated by nav.SSRAtmMap.AtmosRegions + */ bool configAtmosRegion_File() { - auto& regMaps = nav.ssrAtm.atmosRegionsMap; - - regMaps.clear(); - - for (auto& regionFile : acsConfig.atm_reg_definitions) - { - std::ifstream inputStream(regionFile); - if (!inputStream) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Ionosphere region definition file error " << regionFile; - - return false; - } - - char tmp[20]; - int regID = -1; - int gridType = -1; - double lat0 = 0; - int latNgrid = 0; - double latInt = 0; - double lon0 = 0; - int lonNgrid = 0; - double lonInt = 0; - int nind = 0; - - string line; - while (std::getline(inputStream, line)) - { - char* buff = &line[0]; - char* comment = buff + 60; - - if (strlen(buff) < 60 ) { continue; } - if (strstr(comment, "COMMENT")) { continue; } - - if (strstr(comment, "REGION NUMBER")) - { - strncpy(tmp, buff ,3); tmp[3] = '\0'; - regID = atoi(tmp); - regMaps[regID].gridLatDeg.clear(); - regMaps[regID].gridLonDeg.clear(); - nind=0; - // std::cout << " Configuring SSRATM: Region " << regID << "\n"; - continue; - } - - if (strstr(comment, "DEFINITION IOD")) - { - strncpy(tmp,buff , 5); tmp[ 5] = '\0'; int regIOD = atoi(tmp); - regMaps[regID].regionDefIOD = regIOD; - } - - if (strstr(comment, "GRID TYPE")) - { - strncpy(tmp,buff , 5); tmp[ 5] = '\0'; gridType = atoi(tmp); - strncpy(tmp,buff+20, 5); tmp[ 5] = '\0'; int tropGrid = atoi(tmp); - strncpy(tmp,buff+40, 5); tmp[ 5] = '\0'; int ionoGrid = atoi(tmp); - // std::cout << " Configuring SSRATM: Gridtype " << gridType << "\n"; - - regMaps[regID].gridType = gridType; - regMaps[regID].ionoGrid = (ionoGrid==1)?true:false; - regMaps[regID].tropGrid = (tropGrid==1)?true:false; - } - - if (strstr(comment, "POLYNOMIAL SIZE")) - { - strncpy(tmp,buff , 5); tmp[ 5] = '\0'; int polySize1 = atoi(tmp); - strncpy(tmp,buff+20, 5); tmp[ 5] = '\0'; int polySize2 = atoi(tmp); - - regMaps[regID].tropPolySize = polySize1; - regMaps[regID].ionoPolySize = polySize2; - - continue; - } - - if (strstr(comment, "REGION LATITUDE")) - { - strncpy(tmp,buff ,10); tmp[10] = '\0'; lat0 = atof(tmp); - - regMaps[regID].gridLatDeg[0] = lat0; - - if (gridType == 0) - { - latNgrid = 0; - strncpy(tmp,buff+40,10); tmp[10] = '\0'; latInt = atof(tmp); - if (latInt == 0) - latInt = DEFAULT_LAT_INTERVAL; - - regMaps[regID].maxLatDeg = regMaps[regID].gridLatDeg[0] + 0.1; - regMaps[regID].minLatDeg = regMaps[regID].gridLatDeg[0] - 0.1; - nind = 1; - } - else if (gridType > 0) - { - strncpy(tmp,buff+20, 5); tmp[ 5] = '\0'; latNgrid = atoi(tmp); - strncpy(tmp,buff+40,10); tmp[10] = '\0'; latInt = atof(tmp); - - if (gridType == 1) //todo aaron magic numbers - { - regMaps[regID].minLatDeg = lat0 - latNgrid * latInt; - regMaps[regID].maxLatDeg = lat0; - } - - if (gridType == 2) - { - regMaps[regID].minLatDeg = lat0; - regMaps[regID].maxLatDeg = lat0 + latNgrid * latInt; - } - } - - regMaps[regID].intLatDeg = latInt; - - continue; - } - - if (strstr(comment, "REGION LONGITUDE")) - { - strncpy(tmp,buff ,10); tmp[10] = '\0'; lon0 = atof(tmp); - - regMaps[regID].gridLonDeg[0] = lon0; - - if (gridType == 0) - { - lonNgrid = 0; - strncpy(tmp,buff+40,10); tmp[10] = '\0'; lonInt = atof(tmp); - if (lonInt == 0) - lonInt = DEFAULT_LON_INTERVAL; - - regMaps[regID].maxLonDeg = regMaps[regID].gridLonDeg[0] + 0.1; - regMaps[regID].minLonDeg = regMaps[regID].gridLonDeg[0] - 0.1; - nind = 1; - } - else if (gridType > 0) - { - strncpy(tmp,buff+20, 5); tmp[ 5] = '\0'; lonNgrid = atoi(tmp); - strncpy(tmp,buff+40,10); tmp[10] = '\0'; lonInt = atof(tmp); - - regMaps[regID].minLonDeg = lon0; - regMaps[regID].maxLonDeg = lon0 + lonNgrid * lonInt; - } - regMaps[regID].intLonDeg = lonInt; - continue; - } - - if ( strstr(comment, "GRID POINT") - && gridType == 0) - { - strncpy(tmp,buff ,10); tmp[10] = '\0'; regMaps[regID].gridLatDeg[nind] = atof(tmp); - strncpy(tmp,buff+20,10); tmp[10] = '\0'; regMaps[regID].gridLonDeg[nind] = atof(tmp); - - if ( (regMaps[regID].gridLatDeg[nind] + 0.1) > regMaps[regID].maxLatDeg) regMaps[regID].maxLatDeg = regMaps[regID].gridLatDeg[nind] + 0.1; - if ( (regMaps[regID].gridLonDeg[nind] + 0.1) > regMaps[regID].maxLonDeg) regMaps[regID].maxLonDeg = regMaps[regID].gridLonDeg[nind] + 0.1; - if ( (regMaps[regID].gridLatDeg[nind] - 0.1) < regMaps[regID].minLatDeg) regMaps[regID].minLatDeg = regMaps[regID].gridLatDeg[nind] - 0.1; - if ( (regMaps[regID].gridLonDeg[nind] - 0.1) < regMaps[regID].minLonDeg) regMaps[regID].minLonDeg = regMaps[regID].gridLonDeg[nind] - 0.1; - - nind++; - } - - - if (strstr(comment, "REGION END")) - if ( latNgrid > 1 - || lonNgrid > 1) - { - nind = 0; - if (gridType == 1) - for (int i = 0; i <= latNgrid; i++) - for (int j = 0; j <= lonNgrid; j++) - { - regMaps[regID].gridLatDeg[nind] = lat0 - latInt * i; - regMaps[regID].gridLonDeg[nind] = lon0 + lonInt * j; - nind++; - } - - if (gridType == 2) - for (int j = 0; j <= lonNgrid; j++) - for (int i = 0; i <= latNgrid; i++) - { - regMaps[regID].gridLatDeg[nind] = lat0 + latInt * i; - regMaps[regID].gridLonDeg[nind] = lon0 + lonInt * j; - nind++; - } - continue; - } - } - } - - defineLocalTropBasis(); - - bool pass = (regMaps.empty() == false); - return pass; + auto& regMaps = nav.ssrAtm.atmosRegionsMap; + + regMaps.clear(); + + for (auto& regionFile : acsConfig.atm_reg_definitions) + { + std::ifstream inputStream(regionFile); + if (!inputStream) + { + BOOST_LOG_TRIVIAL(error) << "Ionosphere region definition file error " << regionFile; + + return false; + } + + char tmp[20]; + int regID = -1; + int gridType = -1; + double lat0 = 0; + int latNgrid = 0; + double latInt = 0; + double lon0 = 0; + int lonNgrid = 0; + double lonInt = 0; + int nind = 0; + + string line; + while (std::getline(inputStream, line)) + { + char* buff = &line[0]; + char* comment = buff + 60; + + if (strlen(buff) < 60) + { + continue; + } + if (strstr(comment, "COMMENT")) + { + continue; + } + + if (strstr(comment, "REGION NUMBER")) + { + strncpy(tmp, buff, 3); + tmp[3] = '\0'; + regID = atoi(tmp); + regMaps[regID].gridLatDeg.clear(); + regMaps[regID].gridLonDeg.clear(); + nind = 0; + // std::cout << " Configuring SSRATM: Region " << regID << "\n"; + continue; + } + + if (strstr(comment, "DEFINITION IOD")) + { + strncpy(tmp, buff, 5); + tmp[5] = '\0'; + int regIOD = atoi(tmp); + regMaps[regID].regionDefIOD = regIOD; + } + + if (strstr(comment, "GRID TYPE")) + { + strncpy(tmp, buff, 5); + tmp[5] = '\0'; + gridType = atoi(tmp); + strncpy(tmp, buff + 20, 5); + tmp[5] = '\0'; + int tropGrid = atoi(tmp); + strncpy(tmp, buff + 40, 5); + tmp[5] = '\0'; + int ionoGrid = atoi(tmp); + // std::cout << " Configuring SSRATM: Gridtype " << gridType << "\n"; + + regMaps[regID].gridType = gridType; + regMaps[regID].ionoGrid = (ionoGrid == 1) ? true : false; + regMaps[regID].tropGrid = (tropGrid == 1) ? true : false; + } + + if (strstr(comment, "POLYNOMIAL SIZE")) + { + strncpy(tmp, buff, 5); + tmp[5] = '\0'; + int polySize1 = atoi(tmp); + strncpy(tmp, buff + 20, 5); + tmp[5] = '\0'; + int polySize2 = atoi(tmp); + + regMaps[regID].tropPolySize = polySize1; + regMaps[regID].ionoPolySize = polySize2; + + continue; + } + + if (strstr(comment, "REGION LATITUDE")) + { + strncpy(tmp, buff, 10); + tmp[10] = '\0'; + lat0 = atof(tmp); + + regMaps[regID].gridLatDeg[0] = lat0; + + if (gridType == 0) + { + latNgrid = 0; + strncpy(tmp, buff + 40, 10); + tmp[10] = '\0'; + latInt = atof(tmp); + if (latInt == 0) + latInt = DEFAULT_LAT_INTERVAL; + + regMaps[regID].maxLatDeg = regMaps[regID].gridLatDeg[0] + 0.1; + regMaps[regID].minLatDeg = regMaps[regID].gridLatDeg[0] - 0.1; + nind = 1; + } + else if (gridType > 0) + { + strncpy(tmp, buff + 20, 5); + tmp[5] = '\0'; + latNgrid = atoi(tmp); + strncpy(tmp, buff + 40, 10); + tmp[10] = '\0'; + latInt = atof(tmp); + + if (gridType == 1) // todo aaron magic numbers + { + regMaps[regID].minLatDeg = lat0 - latNgrid * latInt; + regMaps[regID].maxLatDeg = lat0; + } + + if (gridType == 2) + { + regMaps[regID].minLatDeg = lat0; + regMaps[regID].maxLatDeg = lat0 + latNgrid * latInt; + } + } + + regMaps[regID].intLatDeg = latInt; + + continue; + } + + if (strstr(comment, "REGION LONGITUDE")) + { + strncpy(tmp, buff, 10); + tmp[10] = '\0'; + lon0 = atof(tmp); + + regMaps[regID].gridLonDeg[0] = lon0; + + if (gridType == 0) + { + lonNgrid = 0; + strncpy(tmp, buff + 40, 10); + tmp[10] = '\0'; + lonInt = atof(tmp); + if (lonInt == 0) + lonInt = DEFAULT_LON_INTERVAL; + + regMaps[regID].maxLonDeg = regMaps[regID].gridLonDeg[0] + 0.1; + regMaps[regID].minLonDeg = regMaps[regID].gridLonDeg[0] - 0.1; + nind = 1; + } + else if (gridType > 0) + { + strncpy(tmp, buff + 20, 5); + tmp[5] = '\0'; + lonNgrid = atoi(tmp); + strncpy(tmp, buff + 40, 10); + tmp[10] = '\0'; + lonInt = atof(tmp); + + regMaps[regID].minLonDeg = lon0; + regMaps[regID].maxLonDeg = lon0 + lonNgrid * lonInt; + } + regMaps[regID].intLonDeg = lonInt; + continue; + } + + if (strstr(comment, "GRID POINT") && gridType == 0) + { + strncpy(tmp, buff, 10); + tmp[10] = '\0'; + regMaps[regID].gridLatDeg[nind] = atof(tmp); + strncpy(tmp, buff + 20, 10); + tmp[10] = '\0'; + regMaps[regID].gridLonDeg[nind] = atof(tmp); + + if ((regMaps[regID].gridLatDeg[nind] + 0.1) > regMaps[regID].maxLatDeg) + regMaps[regID].maxLatDeg = regMaps[regID].gridLatDeg[nind] + 0.1; + if ((regMaps[regID].gridLonDeg[nind] + 0.1) > regMaps[regID].maxLonDeg) + regMaps[regID].maxLonDeg = regMaps[regID].gridLonDeg[nind] + 0.1; + if ((regMaps[regID].gridLatDeg[nind] - 0.1) < regMaps[regID].minLatDeg) + regMaps[regID].minLatDeg = regMaps[regID].gridLatDeg[nind] - 0.1; + if ((regMaps[regID].gridLonDeg[nind] - 0.1) < regMaps[regID].minLonDeg) + regMaps[regID].minLonDeg = regMaps[regID].gridLonDeg[nind] - 0.1; + + nind++; + } + + if (strstr(comment, "REGION END")) + if (latNgrid > 1 || lonNgrid > 1) + { + nind = 0; + if (gridType == 1) + for (int i = 0; i <= latNgrid; i++) + for (int j = 0; j <= lonNgrid; j++) + { + regMaps[regID].gridLatDeg[nind] = lat0 - latInt * i; + regMaps[regID].gridLonDeg[nind] = lon0 + lonInt * j; + nind++; + } + + if (gridType == 2) + for (int j = 0; j <= lonNgrid; j++) + for (int i = 0; i <= latNgrid; i++) + { + regMaps[regID].gridLatDeg[nind] = lat0 + latInt * i; + regMaps[regID].gridLonDeg[nind] = lon0 + lonInt * j; + nind++; + } + continue; + } + } + } + + defineLocalTropBasis(); + + bool pass = (regMaps.empty() == false); + return pass; } - -bool configAtmosRegions( - Trace& trace, - ReceiverMap& receiverMap) +bool configAtmosRegions(Trace& trace, ReceiverMap& receiverMap) { - if (acsConfig.atm_reg_definitions.empty() == false) - return configAtmosRegion_File(); - - int regionId = acsConfig.ssrOpts.region_id; - if (regionId < 0) - return false; - - trace << "\n" << "SSR Atmosphere Region #" << regionId << ": "; - - if ( acsConfig.ssrOpts.grid_type < 0 - && acsConfig.ssrOpts.npoly_trop < 0 - && acsConfig.ssrOpts.npoly_iono < 0) - { - return false; - } - - trace << "gridtype: " << acsConfig.ssrOpts.grid_type << "; TropPoly: " << acsConfig.ssrOpts.npoly_trop << "; IonoPoly: " << acsConfig.ssrOpts.npoly_iono << "\n"; - - bool coordFromRec = false; - if ( acsConfig.ssrOpts.grid_type == 0 - || acsConfig.ssrOpts.lat_max <= acsConfig.ssrOpts.lat_min - || acsConfig.ssrOpts.lon_max <= acsConfig.ssrOpts.lon_min) - { - coordFromRec = true; - } - - SSRAtmRegion& atmRegion = nav.ssrAtm.atmosRegionsMap[regionId]; - - atmRegion.regionDefIOD = acsConfig.ssrOpts.region_iod; - - switch (acsConfig.ssrOpts.npoly_trop) - { - case 1: atmRegion.tropPolySize = 1; break; - case 2: - case 3: atmRegion.tropPolySize = 3; break; - case 4: atmRegion.tropPolySize = 4; break; - default: atmRegion.tropPolySize = -1; - } - - switch (acsConfig.ssrOpts.npoly_iono) - { - case 1: atmRegion.ionoPolySize = 1; break; - case 2: - case 3: atmRegion.ionoPolySize = 3; break; - case 4: atmRegion.ionoPolySize = 4; break; - case 5: - case 6: atmRegion.ionoPolySize = 6; break; - default: atmRegion.ionoPolySize = -1; - } - - atmRegion.gridType = acsConfig.ssrOpts.grid_type; - atmRegion.ionoGrid = acsConfig.ssrOpts.use_grid_iono; - atmRegion.tropGrid = acsConfig.ssrOpts.use_grid_trop; - - int ngrid = 0; - if (coordFromRec) - { - 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; - - auto& pos = rec.pos; - pos = ecef2pos(snxPos); - - if (atmRegion.gridLatDeg.empty()) - { - atmRegion.minLatDeg = pos.latDeg(); - atmRegion.maxLatDeg = pos.latDeg(); - atmRegion.minLonDeg = pos.lonDeg(); - atmRegion.maxLonDeg = pos.lonDeg(); - } - - if (atmRegion.minLatDeg > pos.lat()) atmRegion.minLatDeg = pos.latDeg(); - if (atmRegion.maxLatDeg < pos.lat()) atmRegion.maxLatDeg = pos.latDeg(); - - double midLonDeg = (atmRegion.minLonDeg + atmRegion.maxLonDeg) / 2; - double recLonDeg = pos.lonDeg(); - if ((recLonDeg - midLonDeg) > 180) recLonDeg -= 360; - else if ((recLonDeg - midLonDeg) <-180) recLonDeg += 360; - - if (atmRegion.minLonDeg > recLonDeg) atmRegion.minLonDeg = recLonDeg; - if (atmRegion.maxLonDeg < recLonDeg) atmRegion.maxLonDeg = recLonDeg; - - atmRegion.gridLatDeg[ngrid] = pos.latDeg(); - atmRegion.gridLonDeg[ngrid] = pos.lonDeg(); - ngrid++; - } - atmRegion.minLonDeg -= 0.001; - atmRegion.minLatDeg -= 0.001; - atmRegion.maxLatDeg += 0.001; - atmRegion.maxLonDeg += 0.001; - } - - if (acsConfig.ssrOpts.lat_max > acsConfig.ssrOpts.lat_min) - { - atmRegion.minLatDeg = acsConfig.ssrOpts.lat_min; - atmRegion.maxLatDeg = acsConfig.ssrOpts.lat_max; - } - - if (acsConfig.ssrOpts.lon_max > acsConfig.ssrOpts.lon_min) - { - atmRegion.minLonDeg = acsConfig.ssrOpts.lon_min; - atmRegion.maxLonDeg = acsConfig.ssrOpts.lon_max; - } - - if (acsConfig.ssrOpts.grid_type < 0) - { - ngrid = 0; - atmRegion.gridLatDeg.clear(); - atmRegion.gridLonDeg.clear(); - atmRegion.gridLatDeg[0] = atmRegion.maxLatDeg; - atmRegion.gridLonDeg[0] = atmRegion.minLonDeg; - } - - if (acsConfig.ssrOpts.grid_type > 0) - { - atmRegion.gridLatDeg.clear(); - atmRegion.gridLonDeg.clear(); - - int nIntLat; - int nIntLon; - if (acsConfig.ssrOpts.lat_int > 0) { atmRegion.intLatDeg = acsConfig.ssrOpts.lat_int; nIntLat = floor ((atmRegion.maxLatDeg - atmRegion.minLatDeg) / atmRegion.intLatDeg) + 1; } - else { atmRegion.intLatDeg = atmRegion.maxLatDeg - atmRegion.minLatDeg; nIntLat = 1; } - if (acsConfig.ssrOpts.lon_int > 0) { atmRegion.intLonDeg = acsConfig.ssrOpts.lon_int; nIntLon = floor ((atmRegion.maxLonDeg - atmRegion.minLonDeg) / atmRegion.intLonDeg) + 1; } - else { atmRegion.intLonDeg = atmRegion.maxLonDeg - atmRegion.minLonDeg; nIntLon = 1; } - - - ngrid = 0; - if (acsConfig.ssrOpts.grid_type == 1) - for (int i = 0; i <= nIntLat; i++) - for (int j = 0; j <= nIntLon; j++) - { - atmRegion.gridLatDeg[ngrid] = atmRegion.maxLatDeg - atmRegion.intLatDeg * i; - atmRegion.gridLonDeg[ngrid] = atmRegion.minLonDeg + atmRegion.intLonDeg * j; - ngrid++; - } - - if (acsConfig.ssrOpts.grid_type == 2) - for (int i = 0; i <= nIntLon; i++) - for (int j = 0; j <= nIntLat; j++) - { - atmRegion.gridLatDeg[ngrid] = atmRegion.minLatDeg + atmRegion.intLatDeg * i; - atmRegion.gridLonDeg[ngrid] = atmRegion.minLonDeg + atmRegion.intLonDeg * j; - ngrid++; - } - } - - acsConfig.ssrOpts.ngrid = ngrid; - - if (atmRegion.intLatDeg == 0) atmRegion.intLatDeg = atmRegion.maxLatDeg - atmRegion.minLatDeg; - if (atmRegion.intLonDeg == 0) atmRegion.intLonDeg = atmRegion.maxLonDeg - atmRegion.minLonDeg; - - trace << "; Lats: " << atmRegion.minLatDeg << ", " << atmRegion.maxLatDeg; - trace << "; Lons: " << atmRegion.minLonDeg << ", " << atmRegion.maxLonDeg; - trace << "; Lat0: " << atmRegion.gridLatDeg[0]; - trace << "; Lon0: " << atmRegion.gridLonDeg[0]; - - defineLocalTropBasis(); - - return true; + if (acsConfig.atm_reg_definitions.empty() == false) + return configAtmosRegion_File(); + + int regionId = acsConfig.ssrOpts.region_id; + if (regionId < 0) + return false; + + trace << "\n" + << "SSR Atmosphere Region #" << regionId << ": "; + + if (acsConfig.ssrOpts.grid_type < 0 && acsConfig.ssrOpts.npoly_trop < 0 && + acsConfig.ssrOpts.npoly_iono < 0) + { + return false; + } + + trace << "gridtype: " << acsConfig.ssrOpts.grid_type + << "; TropPoly: " << acsConfig.ssrOpts.npoly_trop + << "; IonoPoly: " << acsConfig.ssrOpts.npoly_iono << "\n"; + + bool coordFromRec = false; + if (acsConfig.ssrOpts.grid_type == 0 || + acsConfig.ssrOpts.lat_max <= acsConfig.ssrOpts.lat_min || + acsConfig.ssrOpts.lon_max <= acsConfig.ssrOpts.lon_min) + { + coordFromRec = true; + } + + SSRAtmRegion& atmRegion = nav.ssrAtm.atmosRegionsMap[regionId]; + + atmRegion.regionDefIOD = acsConfig.ssrOpts.region_iod; + + switch (acsConfig.ssrOpts.npoly_trop) + { + case 1: + atmRegion.tropPolySize = 1; + break; + case 2: + case 3: + atmRegion.tropPolySize = 3; + break; + case 4: + atmRegion.tropPolySize = 4; + break; + default: + atmRegion.tropPolySize = -1; + } + + switch (acsConfig.ssrOpts.npoly_iono) + { + case 1: + atmRegion.ionoPolySize = 1; + break; + case 2: + case 3: + atmRegion.ionoPolySize = 3; + break; + case 4: + atmRegion.ionoPolySize = 4; + break; + case 5: + case 6: + atmRegion.ionoPolySize = 6; + break; + default: + atmRegion.ionoPolySize = -1; + } + + atmRegion.gridType = acsConfig.ssrOpts.grid_type; + atmRegion.ionoGrid = acsConfig.ssrOpts.use_grid_iono; + atmRegion.tropGrid = acsConfig.ssrOpts.use_grid_trop; + + int ngrid = 0; + if (coordFromRec) + { + 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; + + auto& pos = rec.pos; + pos = ecef2pos(snxPos); + + if (atmRegion.gridLatDeg.empty()) + { + atmRegion.minLatDeg = pos.latDeg(); + atmRegion.maxLatDeg = pos.latDeg(); + atmRegion.minLonDeg = pos.lonDeg(); + atmRegion.maxLonDeg = pos.lonDeg(); + } + + if (atmRegion.minLatDeg > pos.lat()) + atmRegion.minLatDeg = pos.latDeg(); + if (atmRegion.maxLatDeg < pos.lat()) + atmRegion.maxLatDeg = pos.latDeg(); + + double midLonDeg = (atmRegion.minLonDeg + atmRegion.maxLonDeg) / 2; + double recLonDeg = pos.lonDeg(); + if ((recLonDeg - midLonDeg) > 180) + recLonDeg -= 360; + else if ((recLonDeg - midLonDeg) < -180) + recLonDeg += 360; + + if (atmRegion.minLonDeg > recLonDeg) + atmRegion.minLonDeg = recLonDeg; + if (atmRegion.maxLonDeg < recLonDeg) + atmRegion.maxLonDeg = recLonDeg; + + atmRegion.gridLatDeg[ngrid] = pos.latDeg(); + atmRegion.gridLonDeg[ngrid] = pos.lonDeg(); + ngrid++; + } + atmRegion.minLonDeg -= 0.001; + atmRegion.minLatDeg -= 0.001; + atmRegion.maxLatDeg += 0.001; + atmRegion.maxLonDeg += 0.001; + } + + if (acsConfig.ssrOpts.lat_max > acsConfig.ssrOpts.lat_min) + { + atmRegion.minLatDeg = acsConfig.ssrOpts.lat_min; + atmRegion.maxLatDeg = acsConfig.ssrOpts.lat_max; + } + + if (acsConfig.ssrOpts.lon_max > acsConfig.ssrOpts.lon_min) + { + atmRegion.minLonDeg = acsConfig.ssrOpts.lon_min; + atmRegion.maxLonDeg = acsConfig.ssrOpts.lon_max; + } + + if (acsConfig.ssrOpts.grid_type < 0) + { + ngrid = 0; + atmRegion.gridLatDeg.clear(); + atmRegion.gridLonDeg.clear(); + atmRegion.gridLatDeg[0] = atmRegion.maxLatDeg; + atmRegion.gridLonDeg[0] = atmRegion.minLonDeg; + } + + if (acsConfig.ssrOpts.grid_type > 0) + { + atmRegion.gridLatDeg.clear(); + atmRegion.gridLonDeg.clear(); + + int nIntLat; + int nIntLon; + if (acsConfig.ssrOpts.lat_int > 0) + { + atmRegion.intLatDeg = acsConfig.ssrOpts.lat_int; + nIntLat = floor((atmRegion.maxLatDeg - atmRegion.minLatDeg) / atmRegion.intLatDeg) + 1; + } + else + { + atmRegion.intLatDeg = atmRegion.maxLatDeg - atmRegion.minLatDeg; + nIntLat = 1; + } + if (acsConfig.ssrOpts.lon_int > 0) + { + atmRegion.intLonDeg = acsConfig.ssrOpts.lon_int; + nIntLon = floor((atmRegion.maxLonDeg - atmRegion.minLonDeg) / atmRegion.intLonDeg) + 1; + } + else + { + atmRegion.intLonDeg = atmRegion.maxLonDeg - atmRegion.minLonDeg; + nIntLon = 1; + } + + ngrid = 0; + if (acsConfig.ssrOpts.grid_type == 1) + for (int i = 0; i <= nIntLat; i++) + for (int j = 0; j <= nIntLon; j++) + { + atmRegion.gridLatDeg[ngrid] = atmRegion.maxLatDeg - atmRegion.intLatDeg * i; + atmRegion.gridLonDeg[ngrid] = atmRegion.minLonDeg + atmRegion.intLonDeg * j; + ngrid++; + } + + if (acsConfig.ssrOpts.grid_type == 2) + for (int i = 0; i <= nIntLon; i++) + for (int j = 0; j <= nIntLat; j++) + { + atmRegion.gridLatDeg[ngrid] = atmRegion.minLatDeg + atmRegion.intLatDeg * i; + atmRegion.gridLonDeg[ngrid] = atmRegion.minLonDeg + atmRegion.intLonDeg * j; + ngrid++; + } + } + + acsConfig.ssrOpts.ngrid = ngrid; + + if (atmRegion.intLatDeg == 0) + atmRegion.intLatDeg = atmRegion.maxLatDeg - atmRegion.minLatDeg; + if (atmRegion.intLonDeg == 0) + atmRegion.intLonDeg = atmRegion.maxLonDeg - atmRegion.minLonDeg; + + trace << "; Lats: " << atmRegion.minLatDeg << ", " << atmRegion.maxLatDeg; + trace << "; Lons: " << atmRegion.minLonDeg << ", " << atmRegion.maxLonDeg; + trace << "; Lat0: " << atmRegion.gridLatDeg[0]; + trace << "; Lon0: " << atmRegion.gridLonDeg[0]; + + defineLocalTropBasis(); + + return true; } diff --git a/src/cpp/common/metaData.hpp b/src/cpp/common/metaData.hpp index 53568a862..46e2321a1 100644 --- a/src/cpp/common/metaData.hpp +++ b/src/cpp/common/metaData.hpp @@ -1,24 +1,21 @@ - #pragma once - #include using std::string; - -#define BSX_FILENAME_STR ((string)"bsxFilename") -#define ORBEX_FILENAME_STR ((string)"orbexFilename") -#define GPX_FILENAME_STR ((string)"gpxFilename") -#define POS_FILENAME_STR ((string)"posFilename") -#define ERP_FILENAME_STR ((string)"erpFilename") -#define TRACE_FILENAME_STR ((string)"traceFilename") -#define JSON_FILENAME_STR ((string)"jsonFilename") -#define TROP_FILENAME_STR ((string)"tropFilename") -#define COST_FILENAME_STR ((string)"costFilename") -#define IONEX_FILENAME_STR ((string)"ionexFilename") -#define IONSTEC_FILENAME_STR ((string)"ionstecFilename") -#define ION_FILENAME_STR ((string)"ionFilename") -#define SP3_FILENAME_STR ((string)"sp3Filename") -#define CLK_FILENAME_STR ((string)"clkFilename") -#define EMS_FILENAME_STR ((string)"emsFilename") \ No newline at end of file +#define BSX_FILENAME_STR ((string) "bsxFilename") +#define ORBEX_FILENAME_STR ((string) "orbexFilename") +#define GPX_FILENAME_STR ((string) "gpxFilename") +#define POS_FILENAME_STR ((string) "posFilename") +#define ERP_FILENAME_STR ((string) "erpFilename") +#define TRACE_FILENAME_STR ((string) "traceFilename") +#define JSON_FILENAME_STR ((string) "jsonFilename") +#define TROP_FILENAME_STR ((string) "tropFilename") +#define COST_FILENAME_STR ((string) "costFilename") +#define IONEX_FILENAME_STR ((string) "ionexFilename") +#define IONSTEC_FILENAME_STR ((string) "ionstecFilename") +#define ION_FILENAME_STR ((string) "ionFilename") +#define SP3_FILENAME_STR ((string) "sp3Filename") +#define CLK_FILENAME_STR ((string) "clkFilename") +#define EMS_FILENAME_STR ((string) "emsFilename") \ No newline at end of file diff --git a/src/cpp/common/mongo.cpp b/src/cpp/common/mongo.cpp index 594e6577c..69373ab78 100644 --- a/src/cpp/common/mongo.cpp +++ b/src/cpp/common/mongo.cpp @@ -1,234 +1,230 @@ - // #pragma GCC optimize ("O0") +#include "common/mongo.hpp" +#include +#include #include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/observations.hpp" +#include "common/rtcmEncoder.hpp" +#include "common/satStat.hpp" +#include "pea/inputsOutputs.hpp" + +namespace sinks = boost::log::sinks; + +using bsoncxx::types::b_date; +using MongoLogSink = sinks::synchronous_sink; /** Persistant formatted storage and inter-process communication. * A mongodb database is mainly used for storage of filter states and measurements. * This data is primarily used for later analysis and plotting using the python utility. * - * Typically the data is stored in States and Measurements collections, with pseudo-indexes collections to allow other applications to see at-a-glance which elements are available for retrieval, without searching the whole db. + * Typically the data is stored in States and Measurements collections, with pseudo-indexes + * collections to allow other applications to see at-a-glance which elements are available for + * retrieval, without searching the whole db. * - * The database's States collection is used as a method of inter-process communication, providing simple configuration of cross-network data passing, integrity and backup. - * It is used for recording clocks and orbits from a POD process, which are then sampled and formatted as RTCM SSR outputs - * - This allows separation of the estimation of parameters, and the generation and transmission of RTCM messages. + * The database's States collection is used as a method of inter-process communication, providing + * simple configuration of cross-network data passing, integrity and backup. It is used for + * recording clocks and orbits from a POD process, which are then sampled and formatted as RTCM SSR + * outputs + * - This allows separation of the estimation of parameters, and the generation and transmission of + * RTCM messages. * - * The states collection may also be used to pass current or predicted values to another Pea instance, allowing several filters to run in a fast/slow configuration. - * In this case the states entries are marked with the time of update/prediction, and other db entries are used to signify validity of complete sets of db entries using the 'updated' time. + * The states collection may also be used to pass current or predicted values to another Pea + * instance, allowing several filters to run in a fast/slow configuration. In this case the states + * entries are marked with the time of update/prediction, and other db entries are used to signify + * validity of complete sets of db entries using the 'updated' time. */ -Database Mongo_Database__() -{ - -} +Database Mongo_Database__() {} +array mongo_ptr_arr = {}; +mongocxx::instance Mongo::instance; // single static instance of the driver -#include "inputsOutputs.hpp" -#include "observations.hpp" -#include "rtcmEncoder.hpp" -#include "acsConfig.hpp" -#include "satStat.hpp" -#include "common.hpp" -#include "mongo.hpp" - -#include -#include - -using bsoncxx::types::b_date; - -namespace sinks = boost::log::sinks; - -array mongo_ptr_arr = {}; - - -mongocxx::instance Mongo::instance; //single static instance of the driver - -vector mongoInstances( - E_Mongo selection) +vector mongoInstances(E_Mongo selection) { - vector instances; + vector instances; - if (selection == +E_Mongo::PRIMARY || selection == +E_Mongo::BOTH) instances.push_back(E_Mongo::PRIMARY); - if (selection == +E_Mongo::SECONDARY || selection == +E_Mongo::BOTH) instances.push_back(E_Mongo::SECONDARY); + if (selection == E_Mongo::PRIMARY || selection == E_Mongo::BOTH) + instances.push_back(E_Mongo::PRIMARY); + if (selection == E_Mongo::SECONDARY || selection == E_Mongo::BOTH) + instances.push_back(E_Mongo::SECONDARY); - return instances; + return instances; } - -void newMongoDatabase( - E_Mongo instance) +void newMongoDatabase(E_Mongo instance) { - auto mongo_ptr = mongo_ptr_arr[instance]; - - if (mongo_ptr == nullptr) - { - MONGO_NOT_INITIALISED_MESSAGE; - return; - } - - auto& mongo = *mongo_ptr; - - auto& config = acsConfig.mongoOpts[instance]; - - getMongoCollection(mongo, "Content"); - - BOOST_LOG_TRIVIAL(info) << "Mongo connecting to database : " << mongo.database << " @ " << config.uri; - try - { - auto dropInstances = mongoInstances(acsConfig.mongoOpts.delete_history); - if (std::find(dropInstances.begin(), dropInstances.end(), instance) != dropInstances.end()) - { - db.drop(); - } - - db[SSR_DB].create_index( - document{} - << "Epoch" << 1 - << "Sat" << 1 - << "Type" << 1 - << "Data" << 1 - << "ObsCode" << 1 - << finalize, - {}); - - auto opts = mongocxx::options::index(); - opts.sparse(true); - - db[STATES_DB].create_index( - document{} - << MONGO_TYPE << 1 - << finalize, - opts); - - auto logInstances = mongoInstances(acsConfig.mongoOpts.output_logs); - if (std::find(logInstances.begin(), logInstances.end(), instance) != logInstances.end()) - { - // Construct the sink - using MongoLogSink = sinks::synchronous_sink; - - boost::shared_ptr mongoLogSink = boost::make_shared(); - - // Register the sink in the logging core - boost::log::core::get()->add_sink(mongoLogSink); - } - - BOOST_LOG_TRIVIAL(info) << "Mongo connected to database : " << mongo.database; - } - catch (...) - { - BOOST_LOG_TRIVIAL(fatal) << "Error: Mongo connection failed - check if service is running at " << config.uri; - } + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; + + if (mongo_ptr == nullptr) + { + MONGO_NOT_INITIALISED_MESSAGE; + return; + } + + auto& mongo = *mongo_ptr; + + auto& config = acsConfig.mongoOpts[static_cast(instance)]; + + getMongoCollection(mongo, "Content"); + + BOOST_LOG_TRIVIAL(info) << "Mongo connecting to database : " << mongo.database << " @ " + << config.uri; + try + { + auto dropInstances = mongoInstances(acsConfig.mongoOpts.delete_history); + if (std::find(dropInstances.begin(), dropInstances.end(), instance) != dropInstances.end()) + { + db.drop(); + } + + db[SSR_DB].create_index( + document{} << "Epoch" << 1 << "Sat" << 1 << "Type" << 1 << "Data" << 1 << "ObsCode" << 1 + << finalize, + {} + ); + + auto opts = mongocxx::options::index(); + opts.sparse(true); + + db[Constants::Mongo::STATES_DB].create_index( + document{} << toString(Constants::Mongo::TYPE_VAR) << 1 << finalize, + opts + ); + + auto logInstances = mongoInstances(acsConfig.mongoOpts.output_logs); + if (std::find(logInstances.begin(), logInstances.end(), instance) != logInstances.end()) + { + // Construct the sink + + boost::shared_ptr mongoLogSink = boost::make_shared(); + + // Register the sink in the logging core + boost::log::core::get()->add_sink(mongoLogSink); + } + + BOOST_LOG_TRIVIAL(info) << "Mongo connected to database : " << mongo.database; + } + catch (...) + { + BOOST_LOG_TRIVIAL(fatal) << "Mongo connection failed - check if service is running at " + << config.uri; + } } -void checkValidDbname( - string& new_database) +void checkValidDbname(string& new_database) { - if (new_database.empty()) - { - BOOST_LOG_TRIVIAL(fatal) << "Error: Mongo database name is empty"; - } - string old_database = new_database; - bool invalid = false; - for (string invalidChar : {"/", "\\", ".", "$", "*", "<", ">", ":", "|", "?"}) - { - invalid |= replaceString(new_database, invalidChar, "", false); - } - if (invalid) - { - BOOST_LOG_TRIVIAL(warning) << "Error: Mongo database name contains invalid characters, new database is: " << new_database << " previously: " << old_database; - } + if (new_database.empty()) + { + BOOST_LOG_TRIVIAL(fatal) << "Mongo database name is empty"; + } + string old_database = new_database; + bool invalid = false; + for (string invalidChar : {"/", "\\", ".", "$", "*", "<", ">", ":", "|", "?"}) + { + invalid |= replaceString(new_database, invalidChar, "", false); + } + if (invalid) + { + BOOST_LOG_TRIVIAL(warning) + << "Mongo database name contains invalid characters, new database is: " << new_database + << " previously: " << old_database; + } } bool startNewMongoDb( - const string& id, - boost::posix_time::ptime logptime, - string new_database, - E_Mongo instance) + const string& id, + boost::posix_time::ptime logptime, + string new_database, + E_Mongo instance +) { - auto& mongo_ptr = mongo_ptr_arr[instance]; + auto& mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - { - return false; - } + if (mongo_ptr == nullptr) + { + return false; + } - auto& mongo = *mongo_ptr; - checkValidDbname(new_database); + auto& mongo = *mongo_ptr; + checkValidDbname(new_database); - replaceString(new_database, "", id); - replaceTimes (new_database, logptime); + replaceString(new_database, "", id); + replaceTimes(new_database, logptime); - // Create the database if its a new name, otherwise, keep the old one - if ( new_database == mongo.database - ||new_database.empty()) - { - //the filename is the same, keep using the old ones - return false; - } + // Create the database if its a new name, otherwise, keep the old one + if (new_database == mongo.database || new_database.empty()) + { + // the filename is the same, keep using the old ones + return false; + } - mongo.database = new_database; + mongo.database = new_database; - newMongoDatabase(instance); + newMongoDatabase(instance); - return true; + return true; } - - void mongoooo() { - DOCS_REFERENCE(Mongo_Database__); + DOCS_REFERENCE(Mongo_Database__); - auto instances = mongoInstances(acsConfig.mongoOpts.enable); + auto instances = mongoInstances(acsConfig.mongoOpts.enable); - for (auto instance : instances) - { - auto& mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : instances) + { + auto& mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr) - continue; + if (mongo_ptr) + continue; - auto& config = acsConfig.mongoOpts[instance]; + auto& config = acsConfig.mongoOpts[static_cast(instance)]; - try - { - BOOST_LOG_TRIVIAL(info) << "Mongo connecting to database @ " << config.uri; - mongo_ptr = new Mongo(config.uri); - } - catch (...) {} // just eat any exception - } + try + { + BOOST_LOG_TRIVIAL(info) << "Mongo connecting to database @ " << config.uri; + mongo_ptr = new Mongo(config.uri); + } + catch (...) + { + } // just eat any exception + } } string formatSeries(const string series) { - string formatted = series; - formatted[0] = '_'; - std::transform(formatted.begin(), formatted.end(), formatted.begin(), ::tolower); - return formatted; + string formatted = series; + formatted[0] = '_'; + std::transform(formatted.begin(), formatted.end(), formatted.begin(), ::tolower); + return formatted; } void MongoLogSinkBackend::consume( - boost::log::record_view const& rec, - sinks::basic_formatted_sink_backend::string_type const& log_string) + boost::log::record_view const& rec, + sinks::basic_formatted_sink_backend::string_type const& + log_string +) { - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - auto mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; + auto& mongo = *mongo_ptr; - getMongoCollection(mongo, "Console"); + getMongoCollection(mongo, "Console"); - //add a azimuth to a chunk - coll.insert_one( - document{} - << "Epoch" << b_date {std::chrono::system_clock::from_time_t((time_t)((PTime)tsync).bigTime)} - << "Log" << log_string.c_str() - << finalize - ); - } + // add a azimuth to a chunk + coll.insert_one( + document{} + << "Epoch" + << b_date{std::chrono::system_clock::from_time_t((time_t)((PTime)tsync).bigTime)} + << "Log" << log_string.c_str() << finalize + ); + } } - diff --git a/src/cpp/common/mongo.hpp b/src/cpp/common/mongo.hpp index 986283976..ac2df7704 100644 --- a/src/cpp/common/mongo.hpp +++ b/src/cpp/common/mongo.hpp @@ -1,38 +1,79 @@ - #pragma once +#include +#include "common/enums.h" -#include +// Constants that are used in trace/logging even when MongoDB is disabled +namespace Constants +{ +/** + * @namespace Mongo + * @brief Contains constant string literals used for MongoDB database and field names. + * + * This namespace provides a collection of constant string literals that represent + * database names and variable/field names commonly used in MongoDB operations. + * + */ +namespace Mongo +{ +constexpr const char* EDITING_DB = "Editing"; +constexpr const char* MEASUREMENTS_DB = "Measurements"; +constexpr const char* STATE_DB = "State"; +constexpr const char* STATES_DB = "States"; ///@todo are STATE_DB and STATES_DB the same? +constexpr const char* CONFIG_DB = "Config"; +constexpr const char* TRACE_DB = "Trace"; +constexpr const char* GEOMETRY_DB = "Geometry"; +constexpr const char* CONTENT_DB = "Content"; + +constexpr const char* DX_VAR = "dx"; +constexpr const char* NUM_VAR = "Num"; +constexpr const char* X_VAR = "x"; +constexpr const char* SIGMA_VAR = "sigma"; +constexpr const char* COVAR_VAR = "Covar"; +constexpr const char* AZIMUTH_VAR = "Azimuth"; +constexpr const char* ELEVATION_VAR = "Elevation"; +constexpr const char* NADIR_VAR = "Nadir"; +constexpr const char* SERIES_VAR = "Series"; +constexpr const char* EPOCH_VAR = "Epoch"; +constexpr const char* SAT_VAR = "Sat"; +constexpr const char* STR_VAR = "Site"; +constexpr const char* TYPE_VAR = "Type"; +constexpr const char* VALUE_VAR = "Values"; + +}; // namespace Mongo +}; // namespace Constants + +/** + * @brief Converts a C-style string (const char*) to a std::string. + * + * @param cstr A pointer to a null-terminated C-style string. + * @return A std::string object containing the same characters as the input C-style string. + */ +inline std::string toString(const char* cstr) +{ + return std::string(cstr); +} +#ifdef ENABLE_MONGODB + +#include +#include +#include +#include +#include #include #include -#include #include - -#include +#include +#include #include +#include #include -#include #include - -#include -#include - - -#include -#include -#include #include -#include -#include - +#include -using std::string; -using std::vector; -using std::array; -using std::deque; -using std::tuple; -using std::map; +namespace sinks = boost::log::sinks; using bsoncxx::builder::stream::close_array; using bsoncxx::builder::stream::close_document; @@ -40,146 +81,170 @@ using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::finalize; using bsoncxx::builder::stream::open_array; using bsoncxx::builder::stream::open_document; - -namespace sinks = boost::log::sinks; - using bsoncxx::types::b_date; - +using std::array; +using std::deque; +using std::map; +using std::string; +using std::tuple; +using std::vector; struct DBEntry; struct GTime; - struct Mongo { - static mongocxx::instance instance; // This should be done only once. - mongocxx::uri uri; - mongocxx::pool pool; - string database; - - Mongo(string uriString) : uri{uriString}, pool{uri} - { + static mongocxx::instance instance; // This should be done only once. + mongocxx::uri uri; + mongocxx::pool pool; + string database; - } + Mongo(string uriString) : uri{uriString}, pool{uri} {} }; -#define SSR_DB "SSRData" - -#define SSR_DATA "Data" -#define SSR_PHAS_BIAS "PBias" -#define SSR_CODE_BIAS "CBias" -#define SSR_EPHEMERIS "Eph" -#define SSR_CLOCK "Clk" - -#define IGS_ION_META "igsSSRMeta" -#define IGS_ION_ENTRY "igsSSREntry" -#define IGS_ION_DCB "igsSSRDCB" -#define CMP_ATM_META "cmpSSRMeta" -#define CMP_ION_META "cmpIonMeta" -#define CMP_TRP_ENTRY "cmpTrpEntry" -#define CMP_ION_ENTRY "cmpIonEntry" - -#define SSR_EPOCH "Epoch" -#define SSR_UPDATED "Updated" -#define SSR_SAT "Sat" -#define SSR_IODE "Iode" -#define SSR_POS "Pos" -#define SSR_VEL "Vel" -#define SSR_OBSCODE "ObsCode" -#define SSR_BIAS "Bias" -#define SSR_VAR "Var" - - -#define IGS_ION_NLAY "ionoMetNlay" -#define IGS_ION_NBAS "ionoMetNbas" -#define IGS_ION_QLTY "ionoMetQlty" -#define SSR_ION_IND "ionoBasInd" -#define IGS_ION_HGT "ionoBasHgt" -#define IGS_ION_DEG "ionoBasDeg" -#define IGS_ION_ORD "ionoBasOrd" -#define IGS_ION_PAR "ionoBasPar" -#define IGS_ION_VAL "ionoBasVal" - -#define SSR_BRDC "Brdc" -#define SSR_PREC "Prec" - -#define REMOTE_DATA_DB "Remote" -#define STATES_DB "States" - -#define REMOTE_DATA "Data" -#define REMOTE_EPOCH "Epoch" -#define REMOTE_ORBIT "Orbit" -#define REMOTE_CLOCK "Clock" -#define REMOTE_SAT "Sat" -#define REMOTE_POS "Pos" -#define REMOTE_VEL "Vel" -#define REMOTE_VAR "Var" -#define REMOTE_CLK "Clk" -#define REMOTE_CLK_DRIFT "ClkRate" -#define REMOTE_STR "Str" - -#define MONGO_CONTENT "Content" -#define MONGO_VALUES "Values" -#define MONGO_UPDATED "Updated" -#define MONGO_EPOCH "Epoch" -#define MONGO_STATE "State" -#define MONGO_SAT "Sat" -#define MONGO_STR "Site" -#define MONGO_MEASUREMENTS "Measurements" -#define MONGO_CONFIG "Config" -#define MONGO_TRACE "Trace" -#define MONGO_GEOMETRY "Geometry" -#define MONGO_SERIES "Series" -#define MONGO_TYPE "Type" -#define MONGO_AVAILABLE "Available" -#define MONGO_DX "dx" -#define MONGO_NUM "Num" -#define MONGO_X "x" -#define MONGO_SIGMA "sigma" -#define MONGO_COVAR "Covar" -#define MONGO_AZIMUTH "Azimuth" -#define MONGO_ELEVATION "Elevation" -#define MONGO_NADIR "Nadir" - -b_date bDate( - const GTime& time); - -struct MongoLogSinkBackend : public sinks::basic_formatted_sink_backend +#define SSR_DB "SSRData" + +#define SSR_DATA "Data" +#define SSR_PHAS_BIAS "PBias" +#define SSR_CODE_BIAS "CBias" +#define SSR_EPHEMERIS "Eph" +#define SSR_CLOCK "Clk" + +#define IGS_ION_META "igsSSRMeta" +#define IGS_ION_ENTRY "igsSSREntry" +#define IGS_ION_DCB "igsSSRDCB" +#define CMP_ATM_META "cmpSSRMeta" +#define CMP_ION_META "cmpIonMeta" +#define CMP_TRP_ENTRY "cmpTrpEntry" +#define CMP_ION_ENTRY "cmpIonEntry" + +#define SSR_EPOCH "Epoch" +#define SSR_UPDATED "Updated" +#define SSR_SAT "Sat" +#define SSR_IODE "Iode" +#define SSR_POS "Pos" +#define SSR_VEL "Vel" +#define SSR_OBSCODE "ObsCode" +#define SSR_BIAS "Bias" +#define SSR_VAR "Var" + +#define IGS_ION_NLAY "ionoMetNlay" +#define IGS_ION_NBAS "ionoMetNbas" +#define IGS_ION_QLTY "ionoMetQlty" +#define SSR_ION_IND "ionoBasInd" +#define IGS_ION_HGT "ionoBasHgt" +#define IGS_ION_DEG "ionoBasDeg" +#define IGS_ION_ORD "ionoBasOrd" +#define IGS_ION_PAR "ionoBasPar" +#define IGS_ION_VAL "ionoBasVal" + +#define SSR_BRDC "Brdc" +#define SSR_PREC "Prec" + +#define REMOTE_DATA_DB "Remote" + +#define REMOTE_DATA "Data" +#define REMOTE_EPOCH "Epoch" +#define REMOTE_ORBIT "Orbit" +#define REMOTE_CLOCK "Clock" +#define REMOTE_SAT "Sat" +#define REMOTE_POS "Pos" +#define REMOTE_VEL "Vel" +#define REMOTE_VAR "Var" +#define REMOTE_CLK "Clk" +#define REMOTE_CLK_DRIFT "ClkRate" +#define REMOTE_STR "Str" + +#define MONGO_UPDATED "Updated" + +#define MONGO_AVAILABLE "Available" + +// @todo seb put all define as const char* in a namespace + +b_date bDate(const GTime& time); + +struct MongoLogSinkBackend + : public sinks::basic_formatted_sink_backend { - // The function consumes the log records that come from the frontend - void consume( - boost::log::record_view const& rec, - sinks::basic_formatted_sink_backend::string_type const& log_string); + // The function consumes the log records that come from the frontend + void consume( + boost::log::record_view const& rec, + sinks::basic_formatted_sink_backend::string_type const& + log_string + ); }; void mongoooo(); -vector mongoInstances( - E_Mongo selection); +vector mongoInstances(E_Mongo selection); bool startNewMongoDb( - const string& id, - boost::posix_time::ptime logptime, - string new_database, - E_Mongo instance); + const string& id, + boost::posix_time::ptime logptime, + string new_database, + E_Mongo instance +); string formatSeries(string series); -document entryToDocument( - DBEntry& entry, - bool type); +document entryToDocument(DBEntry& entry, bool type); -#define getMongoCollection(MONGO,COLLECTION) \ - auto c = MONGO.pool.acquire(); \ - mongocxx::client& client = *c; \ - mongocxx::database db = client[MONGO.database]; \ - auto coll = db[COLLECTION]; +#define getMongoCollection(MONGO, COLLECTION) \ + auto c = MONGO.pool.acquire(); \ + mongocxx::client& client = *c; \ + mongocxx::database db = client[MONGO.database]; \ + auto coll = db[COLLECTION]; +extern array mongo_ptr_arr; +#define MONGO_NOT_INITIALISED_MESSAGE \ + BOOST_LOG_TRIVIAL(warning) << "Mongo actions requested but mongo is not available - check it " \ + "is enabled and connected correctly" -extern array mongo_ptr_arr; +#else // !ENABLE_MONGODB + +// Stub declarations when MongoDB is disabled +#include +#include +#include "common/enums.h" +struct GTime; +struct DBEntry; -#define MONGO_NOT_INITIALISED_MESSAGE BOOST_LOG_TRIVIAL(warning) << "Mongo actions requested but mongo is not available - check it is enabled and connected correctly" +namespace boost +{ +namespace posix_time +{ +class ptime; +} +} // namespace boost + +// Stub function declarations +inline void mongoooo() {} + +inline std::vector mongoInstances(E_Mongo selection) +{ + return {}; +} + +inline bool startNewMongoDb( + const std::string& id, + boost::posix_time::ptime logptime, + std::string new_database, + E_Mongo instance +) +{ + return false; +} + +inline std::string formatSeries(std::string series) +{ + return series; +} +#define MONGO_NOT_INITIALISED_MESSAGE \ + BOOST_LOG_TRIVIAL( \ + warning \ + ) << "MongoDB support is not compiled in - rebuild with ENABLE_MONGODB=ON" +#endif // ENABLE_MONGODB diff --git a/src/cpp/common/mongoRead.cpp b/src/cpp/common/mongoRead.cpp index 8010de869..b9aacea75 100644 --- a/src/cpp/common/mongoRead.cpp +++ b/src/cpp/common/mongoRead.cpp @@ -1,1122 +1,1161 @@ - // #pragma GCC optimize ("O0") +#include "common/mongoRead.hpp" #include +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/ephemeris.hpp" +#include "common/streamRtcm.hpp" using std::deque; -#include "streamRtcm.hpp" -#include "ephemeris.hpp" -#include "acsConfig.hpp" -#include "mongoRead.hpp" -#include "common.hpp" - -short int currentSSRIod = 0; //todo aaron, sketchy global? -map lastBrdcIode; +short int currentSSRIod = 0; // todo aaron, sketchy global? +map lastBrdcIode; template -RETTYPE getStraddle( - GTime referenceTime, - deque& ssrVec) +RETTYPE getStraddle(GTime referenceTime, deque& ssrVec) { - RETTYPE ssr; + RETTYPE ssr; - ssr.valid = true; + ssr.valid = true; - //try to find a set of things that straddle the reference time, with the same iode + // try to find a set of things that straddle the reference time, with the same iode - int bestI = -1; - int bestJ = -1; + int bestI = -1; + int bestJ = -1; - for (int j = 1; j < ssrVec.size(); j++) - { - int i = j - 1; + for (int j = 1; j < ssrVec.size(); j++) + { + int i = j - 1; - auto& entryI = ssrVec[i]; - auto& entryJ = ssrVec[j]; + auto& entryI = ssrVec[i]; + auto& entryJ = ssrVec[j]; - if (entryI.iode != entryJ.iode) - { - //no good, iodes dont match - continue; - } + if (entryI.iode != entryJ.iode) + { + // no good, iodes dont match + continue; + } - //these are acceptable - store them for later - bestI = i; - bestJ = j; + // these are acceptable - store them for later + bestI = i; + bestJ = j; - if (entryJ.time > referenceTime) - { - //this is as close as we will come to a straddle - break; - } - } + if (entryJ.time > referenceTime) + { + // this is as close as we will come to a straddle + break; + } + } - if (bestJ < 0) - { - //nothing found, dont use - ssr.valid = false; + if (bestJ < 0) + { + // nothing found, dont use + ssr.valid = false; - return RETTYPE(); - } + return RETTYPE(); + } - ssr.vals[0] = ssrVec[bestI]; - ssr.vals[1] = ssrVec[bestJ]; + ssr.vals[0] = ssrVec[bestI]; + ssr.vals[1] = ssrVec[bestJ]; - return ssr; + return ssr; } /** Read orbits and clocks from Mongo DB -*/ + */ SsrOutMap mongoReadOrbClk( - GTime referenceTime, ///< reference time (t0) of SSR correction - SSRMeta& ssrMeta, ///< SSR message metadata - int masterIod, ///< IOD SSR - E_Sys targetSys) ///< target system + GTime referenceTime, ///< reference time (t0) of SSR correction + SSRMeta& ssrMeta, ///< SSR message metadata + int masterIod, ///< IOD SSR + E_Sys targetSys ///< target system +) { - SsrOutMap ssrOutMap; - - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; - - if (mongo_ptr == nullptr) - continue; - - auto& mongo = *mongo_ptr; - - getMongoCollection(mongo, SSR_DB); - - // std::cout << "\nTrying to get things for " << targetTime.to_string(0) << "\n"; - - b_date btime = bDate(referenceTime); - - bool changeIod = false; - auto sats = getSysSats(targetSys); - for (auto Sat : sats) - { - deque ephVec; - deque clkVec; - - //try to get up to two entries from either side of the desired time - for (string data : {SSR_EPHEMERIS, SSR_CLOCK}) - for (bool less : {false, true}) - { - string moreLess; - int sortDir; - if (less) { moreLess = "$lte"; sortDir = -1; } - else { moreLess = "$gt"; sortDir = +1; } - - // Find the latest document according to t0_time. - auto docSys = document{} << SSR_SAT << Sat.id() - << SSR_DATA << data - << SSR_EPOCH - << open_document - << moreLess << btime - << close_document - << finalize; - - auto docSort = document{} << SSR_EPOCH << sortDir - << finalize; - - // fout << bsoncxx::to_json(doc) << "\n"; - - auto findOpts = mongocxx::options::find{}; - findOpts.limit(2); - findOpts.sort(docSort.view()); - - auto cursor = coll.find(docSys.view(), findOpts); - - for (auto doc : cursor) - { - PTime timeUpdate; - auto tp = doc[SSR_UPDATED ].get_date(); - timeUpdate.bigTime = std::chrono::system_clock::to_time_t(tp); - - PTime timeEpoch; - tp = doc[SSR_EPOCH ].get_date(); - timeEpoch.bigTime = std::chrono::system_clock::to_time_t(tp); - - if (data == SSR_EPHEMERIS) - { - EphValues ephValues; - ephValues.time = timeEpoch; - - for (int i = 0; i < 3; i++) - { - ephValues.brdcPos(i) = doc[SSR_POS SSR_BRDC + std::to_string(i)].get_double(); - ephValues.precPos(i) = doc[SSR_POS SSR_PREC + std::to_string(i)].get_double(); - ephValues.brdcVel(i) = doc[SSR_VEL SSR_BRDC + std::to_string(i)].get_double(); - ephValues.precVel(i) = doc[SSR_VEL SSR_PREC + std::to_string(i)].get_double(); - } - - ephValues.ephVar = doc[SSR_VAR ].get_double(); - ephValues.iode = doc[SSR_IODE ].get_int32(); - - if (less) ephVec .push_front (ephValues); - else ephVec .push_back (ephValues); - - continue; - } - - if (data == SSR_CLOCK) - { - ClkValues clkValues; - clkValues.time = timeEpoch; - - clkValues.brdcClk = doc[SSR_CLOCK SSR_BRDC].get_double(); - clkValues.precClk = doc[SSR_CLOCK SSR_PREC].get_double(); - - clkValues.iode = doc[SSR_IODE ].get_int32(); - - // std::cout << Sat.id() << " less:" << less << " brdc:" << broadcast << " " << clkValues.time.to_string(0) << "\n"; - - if (less) clkVec .push_front (clkValues); - else clkVec .push_back (clkValues); - - continue; - } - } - } - - // for (auto& a : clkBroadcastVec) - // { - // std::cout << Sat.id() << "Final cbrdcs:" << " iode: " << a.iode << " "<< a.time.to_string(0) << "\n"; - // } - // for (auto& a : clkPreciseVec) - // { - // std::cout << Sat.id() << "Final cprecs:" << " iode: " << a.iode << " "<< a.time.to_string(0) << "\n"; - // } - // for (auto& a : ephBroadcastVec) - // { - // std::cout << Sat.id() << "Final ebrdcs:" << " iode: " << a.iode << " "<< a.time.to_string(0) << "\n"; - // } - // for (auto& a : ephPreciseVec) - // { - // std::cout << Sat.id() << "Final eprecs:" << " iode: " << a.iode << " "<< a.time.to_string(0) << "\n"; - // } - - //try to find a set of things that straddle the reference time, with the same iode - //do for both broadcast and precise values - SSROut ssrOut; - ssrOut.ephInput = getStraddle(referenceTime, ephVec); - ssrOut.clkInput = getStraddle(referenceTime, clkVec); - - if (ssrOut.ephInput.valid == false) - { - tracepdeex(3, std::cout, "Could not retrieve valid ephemeris for %s\n", Sat.id().c_str()); - continue; - } - if ( ssrOut.clkInput.valid == false) - { - tracepdeex(3, std::cout, "Could not retrieve valid clock for %s\n", Sat.id().c_str()); - continue; - } - - ssrOutMap[Sat] = ssrOut; - - if (ssrOut.ephInput.vals[0].iode != lastBrdcIode[Sat]) - { - changeIod = true; - lastBrdcIode[Sat] = ssrOut.ephInput.vals[0].iode; - } - } - - if (changeIod) - { - currentSSRIod++; - - if (currentSSRIod > 15) - currentSSRIod = 0; - } - - masterIod = currentSSRIod; - } - - return ssrOutMap; + SsrOutMap ssrOutMap; + + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; + + if (mongo_ptr == nullptr) + continue; + + auto& mongo = *mongo_ptr; + + getMongoCollection(mongo, SSR_DB); + + // std::cout << "\nTrying to get things for " << targetTime.to_string(0) << "\n"; + + b_date btime = bDate(referenceTime); + + bool changeIod = false; + auto sats = getSysSats(targetSys); + for (auto Sat : sats) + { + deque ephVec; + deque clkVec; + + // try to get up to two entries from either side of the desired time + for (string data : {SSR_EPHEMERIS, SSR_CLOCK}) + for (bool less : {false, true}) + { + string moreLess; + int sortDir; + if (less) + { + moreLess = "$lte"; + sortDir = -1; + } + else + { + moreLess = "$gt"; + sortDir = +1; + } + + // Find the latest document according to t0_time. + auto docSys = document{} << SSR_SAT << Sat.id() << SSR_DATA << data << SSR_EPOCH + << open_document << moreLess << btime << close_document + << finalize; + + auto docSort = document{} << SSR_EPOCH << sortDir << finalize; + + // fout << bsoncxx::to_json(doc) << "\n"; + + auto findOpts = mongocxx::options::find{}; + findOpts.limit(2); + findOpts.sort(docSort.view()); + + auto cursor = coll.find(docSys.view(), findOpts); + + for (auto doc : cursor) + { + PTime timeUpdate; + auto tp = doc[SSR_UPDATED].get_date(); + timeUpdate.bigTime = std::chrono::system_clock::to_time_t(tp); + + PTime timeEpoch; + tp = doc[SSR_EPOCH].get_date(); + timeEpoch.bigTime = std::chrono::system_clock::to_time_t(tp); + + if (data == SSR_EPHEMERIS) + { + EphValues ephValues; + ephValues.time = timeEpoch; + + for (int i = 0; i < 3; i++) + { + ephValues.brdcPos(i) = + doc[SSR_POS SSR_BRDC + std::to_string(i)].get_double(); + ephValues.precPos(i) = + doc[SSR_POS SSR_PREC + std::to_string(i)].get_double(); + ephValues.brdcVel(i) = + doc[SSR_VEL SSR_BRDC + std::to_string(i)].get_double(); + ephValues.precVel(i) = + doc[SSR_VEL SSR_PREC + std::to_string(i)].get_double(); + } + + ephValues.ephVar = doc[SSR_VAR].get_double(); + ephValues.iode = doc[SSR_IODE].get_int32(); + + if (less) + ephVec.push_front(ephValues); + else + ephVec.push_back(ephValues); + + continue; + } + + if (data == SSR_CLOCK) + { + ClkValues clkValues; + clkValues.time = timeEpoch; + + clkValues.brdcClk = doc[SSR_CLOCK SSR_BRDC].get_double(); + clkValues.precClk = doc[SSR_CLOCK SSR_PREC].get_double(); + + clkValues.iode = doc[SSR_IODE].get_int32(); + + // std::cout << Sat.id() << " less:" << less << " + // brdc:" << broadcast << " " << clkValues.time.to_string(0) << "\n"; + + if (less) + clkVec.push_front(clkValues); + else + clkVec.push_back(clkValues); + + continue; + } + } + } + + // for (auto& a : clkBroadcastVec) + // { + // std::cout << Sat.id() << "Final cbrdcs:" << " iode: " << a.iode << " "<< + // a.time.to_string(0) << + // "\n"; + // } + // for (auto& a : clkPreciseVec) + // { + // std::cout << Sat.id() << "Final cprecs:" << " iode: " << a.iode << " "<< + // a.time.to_string(0) << + // "\n"; + // } + // for (auto& a : ephBroadcastVec) + // { + // std::cout << Sat.id() << "Final ebrdcs:" << " iode: " << a.iode << " "<< + // a.time.to_string(0) << + // "\n"; + // } + // for (auto& a : ephPreciseVec) + // { + // std::cout << Sat.id() << "Final eprecs:" << " iode: " << a.iode << " "<< + // a.time.to_string(0) << + // "\n"; + // } + + // try to find a set of things that straddle the reference time, with the same iode + // do for both broadcast and precise values + SSROut ssrOut; + ssrOut.ephInput = getStraddle(referenceTime, ephVec); + ssrOut.clkInput = getStraddle(referenceTime, clkVec); + + if (ssrOut.ephInput.valid == false) + { + tracepdeex( + 3, + std::cout, + "Could not retrieve valid ephemeris for %s\n", + Sat.id().c_str() + ); + continue; + } + if (ssrOut.clkInput.valid == false) + { + tracepdeex( + 3, + std::cout, + "Could not retrieve valid clock for %s\n", + Sat.id().c_str() + ); + continue; + } + + ssrOutMap[Sat] = ssrOut; + + if (ssrOut.ephInput.vals[0].iode != lastBrdcIode[Sat]) + { + changeIod = true; + lastBrdcIode[Sat] = ssrOut.ephInput.vals[0].iode; + } + } + + if (changeIod) + { + currentSSRIod++; + + if (currentSSRIod > 15) + currentSSRIod = 0; + } + + masterIod = currentSSRIod; + } + + return ssrOutMap; } /** Group and sort biases in Mongo DB to avoid unnecessary requests -*/ -void mongoGroupSortBias( - bsoncxx::builder::stream::document& doc) + */ +void mongoGroupSortBias(bsoncxx::builder::stream::document& doc) { - doc << "_id" - << open_document - << SSR_SAT << "$Sat" - << SSR_OBSCODE << "$ObsCode" - << close_document - - << "lastEpoch" - << open_document - << "$max" - << open_document - << "$mergeObjects" - << open_array - << open_document - << SSR_EPOCH << "$Epoch" - << close_document - << "$$ROOT" - << close_array - << close_document - << close_document; + doc << "_id" << open_document << SSR_SAT << "$Sat" << SSR_OBSCODE << "$ObsCode" + << close_document + + << "lastEpoch" << open_document << "$max" << open_document << "$mergeObjects" << open_array + << open_document << SSR_EPOCH << "$Epoch" << close_document << "$$ROOT" << close_array + << close_document << close_document; } /** Read phase biases from Mongo DB -*/ + */ SsrPBMap mongoReadPhaseBias( - SSRMeta& ssrMeta, ///< SSR message metadata - int masterIod, ///< IOD SSR - E_Sys targetSys) ///< target system + SSRMeta& ssrMeta, ///< SSR message metadata + int masterIod, ///< IOD SSR + E_Sys targetSys ///< target system +) { - SsrPBMap ssrPBMap; + SsrPBMap ssrPBMap; - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - auto mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; + auto& mongo = *mongo_ptr; - auto sats = getSysSats(targetSys); + auto sats = getSysSats(targetSys); - getMongoCollection(mongo, SSR_DB); + getMongoCollection(mongo, SSR_DB); - mongocxx::pipeline p; - p.match(bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp(SSR_DATA, SSR_PHAS_BIAS))); - // p.sort (bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp(SSR_EPOCH, 1))); + mongocxx::pipeline p; + p.match( + bsoncxx::builder::basic::make_document( + bsoncxx::builder::basic::kvp(SSR_DATA, SSR_PHAS_BIAS) + ) + ); + // p.sort (bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp(SSR_EPOCH, + // 1))); - bsoncxx::builder::stream::document doc = {}; + bsoncxx::builder::stream::document doc = {}; - mongoGroupSortBias(doc); - p.group(doc.view()); + mongoGroupSortBias(doc); + p.group(doc.view()); - auto cursor = coll.aggregate(p, mongocxx::options::aggregate{}); + auto cursor = coll.aggregate(p, mongocxx::options::aggregate{}); - for (auto resultDoc : cursor) - { - auto entry = resultDoc["lastEpoch"]; - auto strView = entry[SSR_SAT ].get_utf8().value; - string satStr = strView.to_string(); - SatSys Sat(satStr.c_str()); + for (auto resultDoc : cursor) + { + auto entry = resultDoc["lastEpoch"]; + auto strView = entry[SSR_SAT].get_string().value; + std::string satStr(strView.begin(), strView.end()); + SatSys Sat(satStr.c_str()); - if (Sat.sys != targetSys) - continue; + if (Sat.sys != targetSys) + continue; - SSRPhasBias& ssrPhasBias = ssrPBMap[Sat]; - ssrPhasBias.ssrMeta = ssrMeta; - ssrPhasBias.iod = masterIod; + SSRPhasBias& ssrPhasBias = ssrPBMap[Sat]; + ssrPhasBias.ssrMeta = ssrMeta; + ssrPhasBias.iod = masterIod; - auto tp = entry[SSR_EPOCH ].get_date(); - PTime t0; - t0.bigTime = std::chrono::system_clock::to_time_t(tp); + auto tp = entry[SSR_EPOCH].get_date(); + PTime t0; + t0.bigTime = std::chrono::system_clock::to_time_t(tp); - if (!t0.bigTime) - continue; + if (!t0.bigTime) + continue; - ssrPhasBias.t0 = t0; - ssrPhasBias.ssrPhase.dispBiasConistInd = entry["dispBiasConistInd" ].get_int32(); - ssrPhasBias.ssrPhase.MWConistInd = entry["MWConistInd" ].get_int32(); - ssrPhasBias.ssrPhase.yawAngle = entry["yawAngle" ].get_double(); - ssrPhasBias.ssrPhase.yawRate = entry["yawRate" ].get_double(); + ssrPhasBias.t0 = t0; + ssrPhasBias.ssrPhase.dispBiasConistInd = entry["dispBiasConistInd"].get_int32(); + ssrPhasBias.ssrPhase.MWConistInd = entry["MWConistInd"].get_int32(); + ssrPhasBias.ssrPhase.yawAngle = entry["yawAngle"].get_double(); + ssrPhasBias.ssrPhase.yawRate = entry["yawRate"].get_double(); - SSRPhaseCh ssrPhaseCh; - ssrPhaseCh.signalIntInd = entry["signalIntInd" ].get_int32(); - ssrPhaseCh.signalWLIntInd = entry["signalWLIntInd" ].get_int32(); - ssrPhaseCh.signalDisconCnt = entry["signalDisconCnt" ].get_int32(); + SSRPhaseCh ssrPhaseCh; + ssrPhaseCh.signalIntInd = entry["signalIntInd"].get_int32(); + ssrPhaseCh.signalWLIntInd = entry["signalWLIntInd"].get_int32(); + ssrPhaseCh.signalDisconCnt = entry["signalDisconCnt"].get_int32(); - strView = entry[SSR_OBSCODE ].get_utf8().value; - string obsStr = strView.to_string(); - E_ObsCode obsCode = E_ObsCode::_from_string(obsStr.c_str()); + strView = entry[SSR_OBSCODE].get_string().value; + std::string obsStr(strView.begin(), strView.end()); + E_ObsCode obsCode = string_to_enum(obsStr.c_str()); - BiasVar biasVar; - biasVar.bias = entry[SSR_BIAS ].get_double(); - biasVar.var = entry[SSR_VAR ].get_double(); + BiasVar biasVar; + biasVar.bias = entry[SSR_BIAS].get_double(); + biasVar.var = entry[SSR_VAR].get_double(); - ssrPhasBias.obsCodeBiasMap [obsCode] = biasVar; // last entry wins - ssrPhasBias.ssrPhaseChs [obsCode] = ssrPhaseCh; - } - } + ssrPhasBias.obsCodeBiasMap[obsCode] = biasVar; // last entry wins + ssrPhasBias.ssrPhaseChs[obsCode] = ssrPhaseCh; + } + } - return ssrPBMap; + return ssrPBMap; } /** Read code biases from Mongo DB -*/ + */ SsrCBMap mongoReadCodeBias( - SSRMeta& ssrMeta, ///< SSR message metadata - int masterIod, ///< IOD SSR - E_Sys targetSys) ///< target system + SSRMeta& ssrMeta, ///< SSR message metadata + int masterIod, ///< IOD SSR + E_Sys targetSys ///< target system +) { - SsrCBMap ssrCBMap; + SsrCBMap ssrCBMap; - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; + auto& mongo = *mongo_ptr; - auto sats = getSysSats(targetSys); + auto sats = getSysSats(targetSys); - getMongoCollection(mongo, SSR_DB); + getMongoCollection(mongo, SSR_DB); - mongocxx::pipeline p; - p.match(bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp(SSR_DATA, SSR_CODE_BIAS))); - // p.sort (bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp(SSR_EPOCH, 1))); + mongocxx::pipeline p; + p.match( + bsoncxx::builder::basic::make_document( + bsoncxx::builder::basic::kvp(SSR_DATA, SSR_CODE_BIAS) + ) + ); + // p.sort (bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp(SSR_EPOCH, + // 1))); - bsoncxx::builder::stream::document doc = {}; + bsoncxx::builder::stream::document doc = {}; - mongoGroupSortBias(doc); - p.group(doc.view()); + mongoGroupSortBias(doc); + p.group(doc.view()); - auto cursor = coll.aggregate(p, mongocxx::options::aggregate{}); + auto cursor = coll.aggregate(p, mongocxx::options::aggregate{}); - for (auto resultDoc : cursor) - { - auto entry = resultDoc["lastEpoch"]; - auto strView = entry[SSR_SAT ].get_utf8().value; - string satStr = strView.to_string(); - SatSys Sat(satStr.c_str()); + for (auto resultDoc : cursor) + { + auto entry = resultDoc["lastEpoch"]; + auto strView = entry[SSR_SAT].get_string().value; + std::string satStr(strView.begin(), strView.end()); + SatSys Sat(satStr.c_str()); - if (Sat.sys != targetSys) - continue; + if (Sat.sys != targetSys) + continue; - SSRCodeBias& ssrCodeBias = ssrCBMap[Sat]; - ssrCodeBias.ssrMeta = ssrMeta; - ssrCodeBias.iod = masterIod; + SSRCodeBias& ssrCodeBias = ssrCBMap[Sat]; + ssrCodeBias.ssrMeta = ssrMeta; + ssrCodeBias.iod = masterIod; - auto tp = entry[SSR_EPOCH ].get_date(); - PTime t0; - t0.bigTime = std::chrono::system_clock::to_time_t(tp); + auto tp = entry[SSR_EPOCH].get_date(); + PTime t0; + t0.bigTime = std::chrono::system_clock::to_time_t(tp); - if (!t0.bigTime) - continue; + if (!t0.bigTime) + continue; - ssrCodeBias.t0 = t0; + ssrCodeBias.t0 = t0; - strView = entry[SSR_OBSCODE ].get_utf8().value; - string obsStr = strView.to_string(); - E_ObsCode obsCode = E_ObsCode::_from_string(obsStr.c_str()); + strView = entry[SSR_OBSCODE].get_string().value; + std::string obsStr(strView.begin(), strView.end()); + E_ObsCode obsCode = string_to_enum(obsStr.c_str()); - BiasVar biasVar; - biasVar.bias = entry[SSR_BIAS ].get_double(); - biasVar.var = entry[SSR_VAR ].get_double(); + BiasVar biasVar; + biasVar.bias = entry[SSR_BIAS].get_double(); + biasVar.var = entry[SSR_VAR].get_double(); - ssrCodeBias.obsCodeBiasMap[obsCode] = biasVar; // last entry wins - } - } + ssrCodeBias.obsCodeBiasMap[obsCode] = biasVar; // last entry wins + } + } - return ssrCBMap; + return ssrCBMap; } /** Read GPS/GAL/BDS/QZS ephemeris from Mongo DB -*/ -Eph mongoReadEphemeris( - GTime targetTime, ///< target system - SatSys Sat, ///< satellite to read ephemeris of - RtcmMessageType rtcmMessCode) ///< RTCM message code to read ephemeris of + */ +Eph mongoReadEphemeris( + GTime targetTime, ///< target system + SatSys Sat, ///< satellite to read ephemeris of + RtcmMessageType rtcmMessCode ///< RTCM message code to read ephemeris of +) { - Eph eph; - E_NavMsgType type; - - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; - - if (mongo_ptr == nullptr) - continue; - - auto& mongo = *mongo_ptr; - - getMongoCollection(mongo, "Ephemeris"); - - b_date btime{std::chrono::system_clock::from_time_t((time_t)((PTime)targetTime).bigTime)}; - - switch (rtcmMessCode) - { - case +RtcmMessageType:: GPS_EPHEMERIS: // fallthrough - case +RtcmMessageType:: QZS_EPHEMERIS: type = E_NavMsgType::LNAV; break; - case +RtcmMessageType:: BDS_EPHEMERIS: type = E_NavMsgType::D1; break; - case +RtcmMessageType:: GAL_FNAV_EPHEMERIS: type = E_NavMsgType::FNAV; break; - case +RtcmMessageType:: GAL_INAV_EPHEMERIS: type = E_NavMsgType::INAV; break; - default: - BOOST_LOG_TRIVIAL(error) << "Error, attempting to upload incorrect message type.\n"; - return eph; - } - - // Find the latest document according to t0_time. - auto docSys = document{} << "Sat" << Sat.id() - << "Type" << type._to_string() - << finalize; - - auto docSort = document{} << "ToeGPST" << -1 // Newest entry comes first - << finalize; - - auto findOpts = mongocxx::options::find{}; - findOpts.sort(docSort.view()); - findOpts.limit(1); // Only get the first entry - - auto cursor = coll.find(docSys.view(), findOpts); - - for (auto satDoc : cursor) - { - eph.Sat = Sat; - eph.type = type; - - PTime ptoe; - auto toe = satDoc["ToeGPST" ].get_date(); - ptoe.bigTime = std::chrono::system_clock::to_time_t(toe); - eph.toe = ptoe; - - PTime ptoc; - auto toc = satDoc["TocGPST" ].get_date(); - ptoc.bigTime = std::chrono::system_clock::to_time_t(toc); - eph.toc = ptoc; - - eph.weekRollOver = satDoc["WeekDecoded" ].get_int32(); - eph.week = satDoc["WeekAdjusted" ].get_int32(); - eph.toes = satDoc["ToeSecOfWeek" ].get_double(); - eph.tocs = satDoc["TocSecOfWeek" ].get_double(); - - eph.aode = satDoc["AODE" ].get_int32(); - eph.aodc = satDoc["AODC" ].get_int32(); - eph.iode = satDoc["IODE" ].get_int32(); - eph.iodc = satDoc["IODC" ].get_int32(); - - eph.f0 = satDoc["f0" ].get_double(); - eph.f1 = satDoc["f1" ].get_double(); - eph.f2 = satDoc["f2" ].get_double(); - - eph.sqrtA = satDoc["SqrtA" ].get_double(); - eph.A = satDoc["A" ].get_double(); - eph.e = satDoc["e" ].get_double(); - eph.i0 = satDoc["i0" ].get_double(); - eph.idot = satDoc["iDot" ].get_double(); - eph.omg = satDoc["omg" ].get_double(); - eph.OMG0 = satDoc["OMG0" ].get_double(); - eph.OMGd = satDoc["OMGDot" ].get_double(); - eph.M0 = satDoc["M0" ].get_double(); - eph.deln = satDoc["DeltaN" ].get_double(); - eph.crc = satDoc["Crc" ].get_double(); - eph.crs = satDoc["Crs" ].get_double(); - eph.cic = satDoc["Cic" ].get_double(); - eph.cis = satDoc["Cis" ].get_double(); - eph.cuc = satDoc["Cuc" ].get_double(); - eph.cus = satDoc["Cus" ].get_double(); - - eph.tgd[0] = satDoc["TGD0" ].get_double(); - eph.tgd[1] = satDoc["TGD1" ].get_double(); - eph.sva = satDoc["URAIndex" ].get_int32(); - - if ( eph.Sat.sys == +E_Sys::GPS - ||eph.Sat.sys == +E_Sys::QZS) - { - eph.ura[0] = satDoc["URA" ].get_double(); - int svh = satDoc["SVHealth" ].get_int32(); eph.svh = (E_Svh)svh; - eph.code = satDoc["CodeOnL2" ].get_int32(); - eph.flag = satDoc["L2PDataFlag" ].get_int32(); - eph.fitFlag = satDoc["FitFlag" ].get_int32(); - eph.fit = satDoc["FitInterval" ].get_double(); - } - else if (eph.Sat.sys == +E_Sys::GAL) - { - eph.ura[0] = satDoc["SISA" ].get_double(); - int svh = satDoc["SVHealth" ].get_int32(); eph.svh = (E_Svh)svh; - eph.e5a_hs = satDoc["E5aHealth" ].get_int32(); - eph.e5a_dvs = satDoc["E5aDataValidity" ].get_int32(); - eph.e5b_hs = satDoc["E5bHealth" ].get_int32(); - eph.e5b_dvs = satDoc["E5bDataValidity" ].get_int32(); - eph.e1_hs = satDoc["E1Health" ].get_int32(); - eph.e1_dvs = satDoc["E1DataValidity" ].get_int32(); - eph.code = satDoc["DataSource" ].get_int32(); - } - else if (eph.Sat.sys == +E_Sys::BDS) - { - eph.ura[0] = satDoc["URA" ].get_double(); - int svh = satDoc["SVHealth" ].get_int32(); eph.svh = (E_Svh)svh; - } - } - } - - return eph; + Eph eph; + E_NavMsgType type; + + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; + + if (mongo_ptr == nullptr) + continue; + + auto& mongo = *mongo_ptr; + + getMongoCollection(mongo, "Ephemeris"); + + b_date btime{std::chrono::system_clock::from_time_t((time_t)((PTime)targetTime).bigTime)}; + + switch (rtcmMessCode) + { + case RtcmMessageType::GPS_EPHEMERIS: // fallthrough + case RtcmMessageType::QZS_EPHEMERIS: + type = E_NavMsgType::LNAV; + break; + case RtcmMessageType::BDS_EPHEMERIS: + type = E_NavMsgType::D1; + break; + case RtcmMessageType::GAL_FNAV_EPHEMERIS: + type = E_NavMsgType::FNAV; + break; + case RtcmMessageType::GAL_INAV_EPHEMERIS: + type = E_NavMsgType::INAV; + break; + default: + BOOST_LOG_TRIVIAL(error) << "Attempting to upload incorrect message type.\n"; + return eph; + } + + // Find the latest document according to t0_time. + auto docSys = document{} << "Sat" << Sat.id() << "Type" << enum_to_string(type) << finalize; + + auto docSort = document{} << "ToeGPST" << -1 // Newest entry comes first + << finalize; + + auto findOpts = mongocxx::options::find{}; + findOpts.sort(docSort.view()); + findOpts.limit(1); // Only get the first entry + + auto cursor = coll.find(docSys.view(), findOpts); + + for (auto satDoc : cursor) + { + eph.Sat = Sat; + eph.type = type; + + PTime ptoe; + auto toe = satDoc["ToeGPST"].get_date(); + ptoe.bigTime = std::chrono::system_clock::to_time_t(toe); + eph.toe = ptoe; + + PTime ptoc; + auto toc = satDoc["TocGPST"].get_date(); + ptoc.bigTime = std::chrono::system_clock::to_time_t(toc); + eph.toc = ptoc; + + eph.weekRollOver = satDoc["WeekDecoded"].get_int32(); + eph.week = satDoc["WeekAdjusted"].get_int32(); + eph.toes = satDoc["ToeSecOfWeek"].get_double(); + eph.tocs = satDoc["TocSecOfWeek"].get_double(); + + eph.aode = satDoc["AODE"].get_int32(); + eph.aodc = satDoc["AODC"].get_int32(); + eph.iode = satDoc["IODE"].get_int32(); + eph.iodc = satDoc["IODC"].get_int32(); + + eph.f0 = satDoc["f0"].get_double(); + eph.f1 = satDoc["f1"].get_double(); + eph.f2 = satDoc["f2"].get_double(); + + eph.sqrtA = satDoc["SqrtA"].get_double(); + eph.A = satDoc["A"].get_double(); + eph.e = satDoc["e"].get_double(); + eph.i0 = satDoc["i0"].get_double(); + eph.idot = satDoc["iDot"].get_double(); + eph.omg = satDoc["omg"].get_double(); + eph.OMG0 = satDoc["OMG0"].get_double(); + eph.OMGd = satDoc["OMGDot"].get_double(); + eph.M0 = satDoc["M0"].get_double(); + eph.deln = satDoc["DeltaN"].get_double(); + eph.crc = satDoc["Crc"].get_double(); + eph.crs = satDoc["Crs"].get_double(); + eph.cic = satDoc["Cic"].get_double(); + eph.cis = satDoc["Cis"].get_double(); + eph.cuc = satDoc["Cuc"].get_double(); + eph.cus = satDoc["Cus"].get_double(); + + eph.tgd[0] = satDoc["TGD0"].get_double(); + eph.tgd[1] = satDoc["TGD1"].get_double(); + eph.sva = satDoc["URAIndex"].get_int32(); + + if (eph.Sat.sys == E_Sys::GPS || eph.Sat.sys == E_Sys::QZS) + { + eph.ura[0] = satDoc["URA"].get_double(); + int svh = satDoc["SVHealth"].get_int32(); + eph.svh = (E_Svh)svh; + eph.code = satDoc["CodeOnL2"].get_int32(); + eph.flag = satDoc["L2PDataFlag"].get_int32(); + eph.fitFlag = satDoc["FitFlag"].get_int32(); + eph.fit = satDoc["FitInterval"].get_double(); + } + else if (eph.Sat.sys == E_Sys::GAL) + { + eph.ura[0] = satDoc["SISA"].get_double(); + int svh = satDoc["SVHealth"].get_int32(); + eph.svh = (E_Svh)svh; + eph.e5a_hs = satDoc["E5aHealth"].get_int32(); + eph.e5a_dvs = satDoc["E5aDataValidity"].get_int32(); + eph.e5b_hs = satDoc["E5bHealth"].get_int32(); + eph.e5b_dvs = satDoc["E5bDataValidity"].get_int32(); + eph.e1_hs = satDoc["E1Health"].get_int32(); + eph.e1_dvs = satDoc["E1DataValidity"].get_int32(); + eph.code = satDoc["DataSource"].get_int32(); + } + else if (eph.Sat.sys == E_Sys::BDS) + { + eph.ura[0] = satDoc["URA"].get_double(); + int svh = satDoc["SVHealth"].get_int32(); + eph.svh = (E_Svh)svh; + } + } + } + + return eph; } /** Read GLO ephemeris from Mongo DB -*/ + */ Geph mongoReadGloEphemeris( - GTime targetTime, ///< target system - SatSys Sat) ///< satellite to read ephemeris of + GTime targetTime, ///< target system + SatSys Sat ///< satellite to read ephemeris of +) { - Geph geph; + Geph geph; - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; + auto& mongo = *mongo_ptr; - getMongoCollection(mongo, "Ephemeris"); + getMongoCollection(mongo, "Ephemeris"); - b_date btime{std::chrono::system_clock::from_time_t((time_t)((PTime)targetTime).bigTime)}; + b_date btime{std::chrono::system_clock::from_time_t((time_t)((PTime)targetTime).bigTime)}; - // Find the latest document according to t0_time. - auto docSys = document{} << "Sat" << Sat.id() - << finalize; + // Find the latest document according to t0_time. + auto docSys = document{} << "Sat" << Sat.id() << finalize; - auto docSort = document{} << "ToeGPST" << -1 // Newest entry comes first - << finalize; + auto docSort = document{} << "ToeGPST" << -1 // Newest entry comes first + << finalize; - auto findOpts = mongocxx::options::find{}; - findOpts.sort(docSort.view()); - findOpts.limit(1); // Only get the first entry + auto findOpts = mongocxx::options::find{}; + findOpts.sort(docSort.view()); + findOpts.limit(1); // Only get the first entry - auto cursor = coll.find(docSys.view(), findOpts); + auto cursor = coll.find(docSys.view(), findOpts); - for (auto satDoc : cursor) - { - geph.Sat = Sat; - geph.type = E_NavMsgType::FDMA; + for (auto satDoc : cursor) + { + geph.Sat = Sat; + geph.type = E_NavMsgType::FDMA; - PTime ptoe; - auto toe = satDoc["ToeGPST" ].get_date(); - ptoe.bigTime = std::chrono::system_clock::to_time_t(toe); - geph.toe = ptoe; + PTime ptoe; + auto toe = satDoc["ToeGPST"].get_date(); + ptoe.bigTime = std::chrono::system_clock::to_time_t(toe); + geph.toe = ptoe; - PTime ptof; - auto tof = satDoc["TofGPST" ].get_date(); - ptof.bigTime = std::chrono::system_clock::to_time_t(tof); - geph.tof = ptof; + PTime ptof; + auto tof = satDoc["TofGPST"].get_date(); + ptof.bigTime = std::chrono::system_clock::to_time_t(tof); + geph.tof = ptof; - geph.tb = satDoc["ToeSecOfDay" ].get_int32(); - geph.tk_hour = satDoc["TofHour" ].get_int32(); - geph.tk_min = satDoc["TofMin" ].get_int32(); - geph.tk_sec = satDoc["TofSec" ].get_double(); + geph.tb = satDoc["ToeSecOfDay"].get_int32(); + geph.tk_hour = satDoc["TofHour"].get_int32(); + geph.tk_min = satDoc["TofMin"].get_int32(); + geph.tk_sec = satDoc["TofSec"].get_double(); - geph.iode = satDoc["IODE" ].get_int32(); + geph.iode = satDoc["IODE"].get_int32(); - geph.taun = satDoc["TauN" ].get_double(); - geph.gammaN = satDoc["GammaN" ].get_double(); - geph.dtaun = satDoc["DeltaTauN" ].get_double(); + geph.taun = satDoc["TauN"].get_double(); + geph.gammaN = satDoc["GammaN"].get_double(); + geph.dtaun = satDoc["DeltaTauN"].get_double(); - geph.pos[0] = satDoc["PosX" ].get_double(); - geph.pos[1] = satDoc["PosY" ].get_double(); - geph.pos[2] = satDoc["PosZ" ].get_double(); - geph.vel[0] = satDoc["VelX" ].get_double(); - geph.vel[1] = satDoc["VelY" ].get_double(); - geph.vel[2] = satDoc["VelZ" ].get_double(); - geph.acc[0] = satDoc["AccX" ].get_double(); - geph.acc[1] = satDoc["AccY" ].get_double(); - geph.acc[2] = satDoc["AccZ" ].get_double(); + geph.pos[0] = satDoc["PosX"].get_double(); + geph.pos[1] = satDoc["PosY"].get_double(); + geph.pos[2] = satDoc["PosZ"].get_double(); + geph.vel[0] = satDoc["VelX"].get_double(); + geph.vel[1] = satDoc["VelY"].get_double(); + geph.vel[2] = satDoc["VelZ"].get_double(); + geph.acc[0] = satDoc["AccX"].get_double(); + geph.acc[1] = satDoc["AccY"].get_double(); + geph.acc[2] = satDoc["AccZ"].get_double(); - geph.frq = satDoc["FrquencyNumber" ].get_int32(); - int svh = satDoc["SVHealth" ].get_int32(); geph.svh = (E_Svh)svh; - geph.age = satDoc["Age" ].get_int32(); + geph.frq = satDoc["FrquencyNumber"].get_int32(); + int svh = satDoc["SVHealth"].get_int32(); + geph.svh = (E_Svh)svh; + geph.age = satDoc["Age"].get_int32(); - geph.glonassM = satDoc["GLONASSM" ].get_int32(); - geph.NT = satDoc["NumberOfDayIn4Year" ].get_int32(); - geph.moreData = satDoc["AdditionalData" ].get_bool(); - geph.N4 = satDoc["4YearIntervalNumber" ].get_int32(); - } - } + geph.glonassM = satDoc["GLONASSM"].get_int32(); + geph.NT = satDoc["NumberOfDayIn4Year"].get_int32(); + geph.moreData = satDoc["AdditionalData"].get_bool(); + geph.N4 = satDoc["4YearIntervalNumber"].get_int32(); + } + } - return geph; + return geph; } -SSRAtm mongoReadCmpAtmosphere( - GTime time, - SSRMeta ssrMeta) +SSRAtm mongoReadCmpAtmosphere(GTime time, SSRMeta ssrMeta) { - SSRAtm ssrAtm; - - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; - - if (mongo_ptr == nullptr) - continue; - - auto& mongo = *mongo_ptr; - - ssrAtm.ssrMeta = ssrMeta; - - getMongoCollection(mongo, SSR_DB); - - b_date btime = bDate(time); - - auto regDoc = document{} << SSR_DATA << CMP_ATM_META - << finalize; - - auto regSort = document{} << "RegionID" << 1 - << finalize; - - auto regnOpts = mongocxx::options::find{}; - regnOpts.sort(regSort.view()); - - auto cursor = coll.find(regDoc.view(), regnOpts); - - for (auto regDoc : cursor) - { - int reg = regDoc["RegionID" ].get_int32(); - if (reg < 0 || reg > 31) - continue; - - SSRAtmRegion& atmRegion = ssrAtm.atmosRegionsMap[reg]; - atmRegion.regionDefIOD = regDoc["RegionIOD" ].get_int32(); - - atmRegion.minLatDeg = regDoc["minLat" ].get_double(); - atmRegion.maxLatDeg = regDoc["maxLat" ].get_double(); - atmRegion.intLatDeg = regDoc["intLat" ].get_double(); - atmRegion.minLonDeg = regDoc["minLon" ].get_double(); - atmRegion.maxLonDeg = regDoc["maxLon" ].get_double(); - atmRegion.intLonDeg = regDoc["intLon" ].get_double(); - - atmRegion.gridType = regDoc["gridType" ].get_int32(); - atmRegion.tropPolySize = regDoc["tropPoly" ].get_int32(); - atmRegion.ionoPolySize = regDoc["ionoPoly" ].get_int32(); - atmRegion.tropGrid = regDoc["tropGrid" ].get_int32(); - atmRegion.ionoGrid = regDoc["ionoGrid" ].get_int32(); - - atmRegion.gridLatDeg[0] = regDoc["grdLat_0"].get_double(); - atmRegion.gridLonDeg[0] = regDoc["grdLon_0"].get_double(); - - if (atmRegion.gridType==0) - { - int nGrid = regDoc["gridNum" ].get_int32(); - string keyStr; - for (int i=0; i < nGrid; i++) - { - keyStr = "grdLat_" + std::to_string(i); - atmRegion.gridLatDeg[i] = regDoc[keyStr].get_double(); - - keyStr = "grdLon_" + std::to_string(i); - atmRegion.gridLonDeg[i] = regDoc[keyStr].get_double(); - } - } - if (atmRegion.gridType==1) - { - int latNgrid = ROUND((atmRegion.maxLatDeg - atmRegion.minLatDeg)/atmRegion.intLatDeg); - int lonNgrid = ROUND((atmRegion.maxLonDeg - atmRegion.minLonDeg)/atmRegion.intLonDeg); - int nind=0; - for (int i = 0; i <= latNgrid; i++) - for (int j = 0; j <= lonNgrid; j++) - { - atmRegion.gridLatDeg[nind] = atmRegion.gridLatDeg[0] - atmRegion.intLatDeg*i; - atmRegion.gridLonDeg[nind] = atmRegion.gridLonDeg[0] + atmRegion.intLonDeg*j; - nind++; - } - } - if (atmRegion.gridType==2) - { - int latNgrid = ROUND((atmRegion.maxLatDeg - atmRegion.minLatDeg)/atmRegion.intLatDeg); - int lonNgrid = ROUND((atmRegion.maxLonDeg - atmRegion.minLonDeg)/atmRegion.intLonDeg); - int nind=0; - for (int j = 0; j <= lonNgrid; j++) - for (int i = 0; i <= latNgrid; i++) - { - atmRegion.gridLatDeg[nind] = atmRegion.gridLatDeg[0] + atmRegion.intLatDeg*i; - atmRegion.gridLonDeg[nind] = atmRegion.gridLonDeg[0] + atmRegion.intLonDeg*j; - nind++; - } - } - } - - if (ssrAtm.atmosRegionsMap.empty()) - return ssrAtm; - - for (auto& [regId,regData] : ssrAtm.atmosRegionsMap) - { - auto trpSel = document{} << SSR_DATA << CMP_TRP_ENTRY - << "RegionID" << regId - << SSR_EPOCH - << open_document - << "$lt" << btime - << close_document - << finalize; - - auto trpSort = document{} << SSR_EPOCH << 1 - << finalize; - - auto trpOpts = mongocxx::options::find{}; - trpOpts.sort(trpSort.view()); - trpOpts.limit(1); - - auto trpDocs = coll.find(trpSel.view(), trpOpts); - - for (auto atmDoc : trpDocs) - { - PTime t0; - auto tp = atmDoc[SSR_EPOCH ].get_date(); - t0.bigTime = std::chrono::system_clock::to_time_t(tp); - - GTime tatm = t0; - if (abs((time-tatm).to_double()) > 600) - continue; - - regData.tropData[tatm].sigma = atmDoc["trpAcc"].get_double(); - - for (int i = 0; i < regData.tropPolySize; i++) - { - string keyStr = "tropPoly_"+std::to_string(i); - regData.tropData[tatm].polyDry[i] = atmDoc[keyStr].get_double(); - } - - string keyStr; - if (regData.tropGrid) - for (auto& [ind, lat] : regData.gridLatDeg) - { - keyStr = "tropDry_" + std::to_string(ind); - regData.tropData[tatm].gridDry[ind] = atmDoc[keyStr].get_double(); - - keyStr = "tropWet_" + std::to_string(ind); - regData.tropData[tatm].gridWet[ind] = atmDoc[keyStr].get_double(); - } - } - - auto ionSel = document{} << SSR_DATA << CMP_ION_META - << "RegionID" << regId - << SSR_EPOCH - << open_document - << "$lt" << btime - << close_document - << finalize; - - auto ionSort = document{} << SSR_EPOCH << 1 - << finalize; - - auto ionOpts = mongocxx::options::find{}; - ionOpts.sort(ionSort.view()); - ionOpts.limit(1); - - auto ionDocs = coll.find(ionSel.view(), trpOpts); - map regSat; - for (auto atmDoc : ionDocs) - { - PTime t0; - auto tp = atmDoc[SSR_EPOCH ].get_date(); - t0.bigTime = std::chrono::system_clock::to_time_t(tp); - - GTime tatm = t0; - if (abs((time-tatm).to_double()) > 600) - continue; - - int nSat = atmDoc["satNumb" ].get_int32(); - for (int i=0; i 600) - continue; - - regData.stecData[sat][tatm].iod = regData.regionDefIOD; - regData.stecData[sat][tatm].sigma = satDoc["ionAcc"].get_double(); - - for (int i = 0; i < regData.ionoPolySize; i++) - { - string keyStr = "ionoPoly_" + std::to_string(i); - regData.stecData[sat][tatm].poly[i] = satDoc[keyStr].get_double(); - tracepdeex (6,std::cout,"\n Mongo_ionP %s %2d %s %1d %8.4f", tatm.to_string().c_str(), regId, sat.id().c_str(), i, regData.stecData[sat][tatm].poly[i]); - } - - if (regData.ionoGrid) - for (auto& [ind, lat] : regData.gridLatDeg) - { - string keyStr = "ionoGrid_" + std::to_string(ind); - regData.stecData[sat][tatm].grid[ind] = satDoc[keyStr].get_double(); - tracepdeex (6,std::cout,"\n Mongo_ionG %s %2d %s %1d %8.4f", tatm.to_string().c_str(), regId, sat.id().c_str(), ind, regData.stecData[sat][tatm].grid[ind]); - } - } - } - } - } - - return ssrAtm; + SSRAtm ssrAtm; + + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; + + if (mongo_ptr == nullptr) + continue; + + auto& mongo = *mongo_ptr; + + ssrAtm.ssrMeta = ssrMeta; + + getMongoCollection(mongo, SSR_DB); + + b_date btime = bDate(time); + + auto regDoc = document{} << SSR_DATA << CMP_ATM_META << finalize; + + auto regSort = document{} << "RegionID" << 1 << finalize; + + auto regnOpts = mongocxx::options::find{}; + regnOpts.sort(regSort.view()); + + auto cursor = coll.find(regDoc.view(), regnOpts); + + for (auto regDoc : cursor) + { + int reg = regDoc["RegionID"].get_int32(); + if (reg < 0 || reg > 31) + continue; + + SSRAtmRegion& atmRegion = ssrAtm.atmosRegionsMap[reg]; + atmRegion.regionDefIOD = regDoc["RegionIOD"].get_int32(); + + atmRegion.minLatDeg = regDoc["minLat"].get_double(); + atmRegion.maxLatDeg = regDoc["maxLat"].get_double(); + atmRegion.intLatDeg = regDoc["intLat"].get_double(); + atmRegion.minLonDeg = regDoc["minLon"].get_double(); + atmRegion.maxLonDeg = regDoc["maxLon"].get_double(); + atmRegion.intLonDeg = regDoc["intLon"].get_double(); + + atmRegion.gridType = regDoc["gridType"].get_int32(); + atmRegion.tropPolySize = regDoc["tropPoly"].get_int32(); + atmRegion.ionoPolySize = regDoc["ionoPoly"].get_int32(); + atmRegion.tropGrid = regDoc["tropGrid"].get_int32(); + atmRegion.ionoGrid = regDoc["ionoGrid"].get_int32(); + + atmRegion.gridLatDeg[0] = regDoc["grdLat_0"].get_double(); + atmRegion.gridLonDeg[0] = regDoc["grdLon_0"].get_double(); + + if (atmRegion.gridType == 0) + { + int nGrid = regDoc["gridNum"].get_int32(); + string keyStr; + for (int i = 0; i < nGrid; i++) + { + keyStr = "grdLat_" + std::to_string(i); + atmRegion.gridLatDeg[i] = regDoc[keyStr].get_double(); + + keyStr = "grdLon_" + std::to_string(i); + atmRegion.gridLonDeg[i] = regDoc[keyStr].get_double(); + } + } + if (atmRegion.gridType == 1) + { + int latNgrid = + ROUND((atmRegion.maxLatDeg - atmRegion.minLatDeg) / atmRegion.intLatDeg); + int lonNgrid = + ROUND((atmRegion.maxLonDeg - atmRegion.minLonDeg) / atmRegion.intLonDeg); + int nind = 0; + for (int i = 0; i <= latNgrid; i++) + for (int j = 0; j <= lonNgrid; j++) + { + atmRegion.gridLatDeg[nind] = + atmRegion.gridLatDeg[0] - atmRegion.intLatDeg * i; + atmRegion.gridLonDeg[nind] = + atmRegion.gridLonDeg[0] + atmRegion.intLonDeg * j; + nind++; + } + } + if (atmRegion.gridType == 2) + { + int latNgrid = + ROUND((atmRegion.maxLatDeg - atmRegion.minLatDeg) / atmRegion.intLatDeg); + int lonNgrid = + ROUND((atmRegion.maxLonDeg - atmRegion.minLonDeg) / atmRegion.intLonDeg); + int nind = 0; + for (int j = 0; j <= lonNgrid; j++) + for (int i = 0; i <= latNgrid; i++) + { + atmRegion.gridLatDeg[nind] = + atmRegion.gridLatDeg[0] + atmRegion.intLatDeg * i; + atmRegion.gridLonDeg[nind] = + atmRegion.gridLonDeg[0] + atmRegion.intLonDeg * j; + nind++; + } + } + } + + if (ssrAtm.atmosRegionsMap.empty()) + return ssrAtm; + + for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) + { + auto trpSel = document{} << SSR_DATA << CMP_TRP_ENTRY << "RegionID" << regId + << SSR_EPOCH << open_document << "$lt" << btime + << close_document << finalize; + + auto trpSort = document{} << SSR_EPOCH << 1 << finalize; + + auto trpOpts = mongocxx::options::find{}; + trpOpts.sort(trpSort.view()); + trpOpts.limit(1); + + auto trpDocs = coll.find(trpSel.view(), trpOpts); + + for (auto atmDoc : trpDocs) + { + PTime t0; + auto tp = atmDoc[SSR_EPOCH].get_date(); + t0.bigTime = std::chrono::system_clock::to_time_t(tp); + + GTime tatm = t0; + if (abs((time - tatm).to_double()) > 600) + continue; + + regData.tropData[tatm].sigma = atmDoc["trpAcc"].get_double(); + + for (int i = 0; i < regData.tropPolySize; i++) + { + string keyStr = "tropPoly_" + std::to_string(i); + regData.tropData[tatm].polyDry[i] = atmDoc[keyStr].get_double(); + } + + string keyStr; + if (regData.tropGrid) + for (auto& [ind, lat] : regData.gridLatDeg) + { + keyStr = "tropDry_" + std::to_string(ind); + regData.tropData[tatm].gridDry[ind] = atmDoc[keyStr].get_double(); + + keyStr = "tropWet_" + std::to_string(ind); + regData.tropData[tatm].gridWet[ind] = atmDoc[keyStr].get_double(); + } + } + + auto ionSel = document{} << SSR_DATA << CMP_ION_META << "RegionID" << regId << SSR_EPOCH + << open_document << "$lt" << btime << close_document + << finalize; + + auto ionSort = document{} << SSR_EPOCH << 1 << finalize; + + auto ionOpts = mongocxx::options::find{}; + ionOpts.sort(ionSort.view()); + ionOpts.limit(1); + + auto ionDocs = coll.find(ionSel.view(), trpOpts); + map regSat; + for (auto atmDoc : ionDocs) + { + PTime t0; + auto tp = atmDoc[SSR_EPOCH].get_date(); + t0.bigTime = std::chrono::system_clock::to_time_t(tp); + + GTime tatm = t0; + if (abs((time - tatm).to_double()) > 600) + continue; + + int nSat = atmDoc["satNumb"].get_int32(); + for (int i = 0; i < nSat; i++) + { + string keyStr = "regSat_" + std::to_string(i); + auto strView = atmDoc[keyStr].get_string().value; + std::string satStr(strView.begin(), strView.end()); + SatSys Sat(satStr.c_str()); + + regSat[i] = Sat; + } + } + + for (auto& [iSat, sat] : regSat) + { + auto satSel = document{} << SSR_DATA << CMP_ION_ENTRY << "RegionID" << regId + << SSR_SAT << sat.id() << SSR_EPOCH << open_document + << "$lt" << btime << close_document << finalize; + + auto satSort = document{} << SSR_EPOCH << 1 << finalize; + + auto satOpts = mongocxx::options::find{}; + satOpts.sort(satSort.view()); + satOpts.limit(1); + + auto satDocs = coll.find(satSel.view(), satOpts); + for (auto satDoc : satDocs) + { + PTime t0; + auto tp = satDoc[SSR_EPOCH].get_date(); + t0.bigTime = std::chrono::system_clock::to_time_t(tp); + + GTime tatm = t0; + if (abs((time - tatm).to_double()) > 600) + continue; + + regData.stecData[sat][tatm].iod = regData.regionDefIOD; + regData.stecData[sat][tatm].sigma = satDoc["ionAcc"].get_double(); + + for (int i = 0; i < regData.ionoPolySize; i++) + { + string keyStr = "ionoPoly_" + std::to_string(i); + regData.stecData[sat][tatm].poly[i] = satDoc[keyStr].get_double(); + tracepdeex( + 6, + std::cout, + "\n Mongo_ionP %s %2d %s %1d %8.4f", + tatm.to_string().c_str(), + regId, + sat.id().c_str(), + i, + regData.stecData[sat][tatm].poly[i] + ); + } + + if (regData.ionoGrid) + for (auto& [ind, lat] : regData.gridLatDeg) + { + string keyStr = "ionoGrid_" + std::to_string(ind); + regData.stecData[sat][tatm].grid[ind] = satDoc[keyStr].get_double(); + tracepdeex( + 6, + std::cout, + "\n Mongo_ionG %s %2d %s %1d %8.4f", + tatm.to_string().c_str(), + regId, + sat.id().c_str(), + ind, + regData.stecData[sat][tatm].grid[ind] + ); + } + } + } + } + } + + return ssrAtm; } -SSRAtm mongoReadIGSIonosphere( - GTime time, - const SSRMeta& ssrMeta, - int masterIod) +SSRAtm mongoReadIGSIonosphere(GTime time, const SSRMeta& ssrMeta, int masterIod) { - SSRAtm ssrAtm; + SSRAtm ssrAtm; - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; + auto& mongo = *mongo_ptr; - ssrAtm.ssrMeta = ssrMeta; + ssrAtm.ssrMeta = ssrMeta; - getMongoCollection(mongo, SSR_DB); + getMongoCollection(mongo, SSR_DB); - b_date btime = bDate(time); + b_date btime = bDate(time); - // Find the latest document according to t0_time. - auto docSys = document{} << SSR_DATA << IGS_ION_META - << SSR_EPOCH - << open_document - << "$lt" << btime - << close_document - << finalize; + // Find the latest document according to t0_time. + auto docSys = document{} << SSR_DATA << IGS_ION_META << SSR_EPOCH << open_document << "$lt" + << btime << close_document << finalize; - auto docSort = document{} << SSR_EPOCH << 1 - << finalize; + auto docSort = document{} << SSR_EPOCH << 1 << finalize; - auto findOpts = mongocxx::options::find{}; - findOpts.limit(1); + auto findOpts = mongocxx::options::find{}; + findOpts.limit(1); - auto cursor = coll.find(docSys.view(), findOpts); + auto cursor = coll.find(docSys.view(), findOpts); - SSRAtmGlobal atmGlob; - int nbasis; - for (auto atmDoc : cursor) - { - PTime t0; - auto tp = atmDoc[SSR_EPOCH ].get_date(); - t0.bigTime = std::chrono::system_clock::to_time_t(tp); + SSRAtmGlobal atmGlob; + int nbasis; + for (auto atmDoc : cursor) + { + PTime t0; + auto tp = atmDoc[SSR_EPOCH].get_date(); + t0.bigTime = std::chrono::system_clock::to_time_t(tp); - atmGlob.time = t0; + atmGlob.time = t0; - atmGlob.numberLayers = atmDoc[IGS_ION_NLAY ].get_int32(); - nbasis = atmDoc[IGS_ION_NBAS ].get_int32(); - atmGlob.vtecQuality = atmDoc[IGS_ION_QLTY ].get_double(); - for (int i=0; i < atmGlob.numberLayers; i++) - { - string hghStr = "Height_"+std::to_string(i); - atmGlob.layers[i].height = atmDoc[hghStr ].get_double(); - } - } + atmGlob.numberLayers = atmDoc[IGS_ION_NLAY].get_int32(); + nbasis = atmDoc[IGS_ION_NBAS].get_int32(); + atmGlob.vtecQuality = atmDoc[IGS_ION_QLTY].get_double(); + for (int i = 0; i < atmGlob.numberLayers; i++) + { + string hghStr = "Height_" + std::to_string(i); + atmGlob.layers[i].height = atmDoc[hghStr].get_double(); + } + } - auto timobj = b_date {std::chrono::system_clock::from_time_t(atmGlob.time.bigTime)}; - auto docEntr = document{} << SSR_DATA << IGS_ION_ENTRY - << SSR_EPOCH << timobj - << finalize; - auto cursor2 = coll.find(docEntr.view(), mongocxx::options::find{}); + auto timobj = b_date{std::chrono::system_clock::from_time_t(atmGlob.time.bigTime)}; + auto docEntr = document{} << SSR_DATA << IGS_ION_ENTRY << SSR_EPOCH << timobj << finalize; + auto cursor2 = coll.find(docEntr.view(), mongocxx::options::find{}); - map maxBasis; - for (auto atmDoc : cursor2) - { - SphComp sphComp; - sphComp.layer = atmDoc[IGS_ION_HGT ].get_int32(); - sphComp.degree = atmDoc[IGS_ION_DEG ].get_int32(); - sphComp.order = atmDoc[IGS_ION_ORD ].get_int32(); - int trigType = atmDoc[IGS_ION_PAR ].get_int32(); + map maxBasis; + for (auto atmDoc : cursor2) + { + SphComp sphComp; + sphComp.layer = atmDoc[IGS_ION_HGT].get_int32(); + sphComp.degree = atmDoc[IGS_ION_DEG].get_int32(); + sphComp.order = atmDoc[IGS_ION_ORD].get_int32(); + int trigType = atmDoc[IGS_ION_PAR].get_int32(); - sphComp.trigType = E_TrigType::_from_integral(trigType); + sphComp.trigType = int_to_enum(trigType); - sphComp.value = atmDoc[IGS_ION_VAL ].get_double(); - sphComp.variance = 0; + sphComp.value = atmDoc[IGS_ION_VAL].get_double(); + sphComp.variance = 0; - SSRVTEClayer& laydata = atmGlob.layers[sphComp.layer]; + SSRVTEClayer& laydata = atmGlob.layers[sphComp.layer]; - laydata.sphHarmonic[maxBasis[sphComp.layer]] = sphComp; - maxBasis[sphComp.layer]++; + laydata.sphHarmonic[maxBasis[sphComp.layer]] = sphComp; + maxBasis[sphComp.layer]++; - if (laydata.maxDegree < sphComp.degree) laydata.maxDegree = sphComp.degree; - if (laydata.maxOrder < sphComp.order) laydata.maxOrder = sphComp.order; - } + if (laydata.maxDegree < sphComp.degree) + laydata.maxDegree = sphComp.degree; + if (laydata.maxOrder < sphComp.order) + laydata.maxOrder = sphComp.order; + } - atmGlob.iod = masterIod; + atmGlob.iod = masterIod; - ssrAtm.atmosGlobalMap[atmGlob.time] = atmGlob; - } + ssrAtm.atmosGlobalMap[atmGlob.time] = atmGlob; + } - return ssrAtm; + return ssrAtm; } void mongoReadFilter( - KFState& kfState, - GTime time, - const vector& types, - const string& Sat, - const string& str) + KFState& kfState, + GTime time, + const vector& types, + const string& Sat, + const string& str +) { - for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; - - if (mongo_ptr == nullptr) - continue; - - auto& mongo = *mongo_ptr; - - getMongoCollection(mongo, STATES_DB); - - b_date btime = bDate(time); - - // get latest available from collection before next step - - auto docMatch = document(); - auto docSort = document(); - auto docProject = document(); - auto docGroup = document{}; - - docMatch << MONGO_TYPE << MONGO_AVAILABLE; - if (time != GTime::noTime()) docMatch << MONGO_EPOCH - << open_document - << "$lte" << btime - << close_document; - - - docSort << MONGO_EPOCH << -1; - docSort << MONGO_UPDATED << -1; - - docProject << "_id" << 0; - docProject << MONGO_TYPE << 0; - - auto findOpts = mongocxx::options::find(); - findOpts.sort (docSort .view()); - findOpts.projection (docProject .view()); -// - auto matchTemplate = coll.find_one(docMatch.view(), findOpts); - - if (!matchTemplate) - { - continue; - } - - //start a new match - auto docMatch2 = document(); - auto docProject2 = document(); - - docProject2 << MONGO_EPOCH << 0; - docProject2 << MONGO_UPDATED << 0; - docProject2 << MONGO_SERIES << 0; - docProject2 << MONGO_DX << 0; - docProject2 << "_id" << 0; - - if (std::find(types.begin(), types.end(), +KF::ALL) == types.end()) - { - auto array = docMatch2 << "$or" - << open_array; - - for (auto& type : types) - { - array << open_document - << MONGO_STATE << type._to_string() - << close_document; - } - - array << close_array; - } - - auto updateDoc = matchTemplate->view(); - - - PTime pTime; - auto time = updateDoc[MONGO_EPOCH].get_date(); - pTime.bigTime = std::chrono::system_clock::to_time_t(time); - - kfState.time = pTime; - - time = updateDoc[MONGO_UPDATED].get_date(); - pTime.bigTime = std::chrono::system_clock::to_time_t(time); - - std::cout << "\n" << bsoncxx::to_json(updateDoc) << "\n"; - - docMatch2 << MONGO_EPOCH << updateDoc[MONGO_EPOCH] .get_date(); - docMatch2 << MONGO_UPDATED << updateDoc[MONGO_UPDATED] .get_date(); - docMatch2 << MONGO_SERIES << "_predicted"; - if (str.empty() == false) docMatch2 << MONGO_STR << str; - if (Sat.empty() == false) docMatch2 << MONGO_SAT << Sat; - - // std::cout << "\n" << bsoncxx::to_json(docMatch2.view()) << "\n"; + for (auto instance : {E_Mongo::PRIMARY, E_Mongo::SECONDARY}) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - mongocxx::pipeline p; - p.match (docMatch2 .view()); - p.project (docProject2.view()); - // p.group (docGroup .view()); + if (mongo_ptr == nullptr) + continue; - // std::cout << "\n" << bsoncxx::to_json(docMatch); - // std::cout << "\n" << bsoncxx::to_json(docGroup); + auto& mongo = *mongo_ptr; - auto cursor = coll.aggregate(p); + getMongoCollection(mongo, Constants::Mongo::STATES_DB); - vector x; - vector P; - for (int i = 0; i < kfState.x.rows(); i++) - { - x.push_back(kfState.x(i)); - P.push_back(kfState.P(i,i)); - } + b_date btime = bDate(time); - int index = x.size(); + // get latest available from collection before next step - for (auto doc : cursor) - { - // std::cout << bsoncxx::to_json(doc) << "\n"; + auto docMatch = document(); + auto docSort = document(); + auto docProject = document(); + auto docGroup = document{}; - KFKey kfKey; - kfKey.type = KF::_from_string( doc[MONGO_STATE].get_utf8().value.to_string().c_str()); - kfKey.Sat = SatSys( doc[MONGO_SAT] .get_utf8().value.to_string().c_str()); - kfKey.str = doc[MONGO_STR] .get_utf8().value.to_string(); + docMatch << toString(Constants::Mongo::TYPE_VAR) << MONGO_AVAILABLE; + if (time != GTime::noTime()) + docMatch << toString(Constants::Mongo::EPOCH_VAR) << open_document << "$lte" << btime + << close_document; - int i = 0; - for (auto thing : doc[MONGO_NUM].get_array().value) - { - kfKey.num = doc[MONGO_NUM] .get_array().value[i].get_int32(); + docSort << toString(Constants::Mongo::EPOCH_VAR) << -1; + docSort << MONGO_UPDATED << -1; - x.push_back( doc[MONGO_X] .get_array().value[i].get_double()); - P.push_back(SQR( doc[MONGO_SIGMA].get_array().value[i].get_double())); - - kfState.kfIndexMap[kfKey] = index; - index++; - i++; - } - } - - kfState.x = VectorXd (x.size()); - kfState.dx = VectorXd::Zero(x.size()); - kfState.P = MatrixXd::Zero(P.size(), P.size()); + docProject << "_id" << 0; + docProject << toString(Constants::Mongo::TYPE_VAR) << 0; - for (int i = 0; i < x.size(); i++) - { - kfState.x(i) = x[i]; - kfState.P(i,i) = P[i]; - } - } + auto findOpts = mongocxx::options::find(); + findOpts.sort(docSort.view()); + findOpts.projection(docProject.view()); + // + auto matchTemplate = coll.find_one(docMatch.view(), findOpts); + + if (!matchTemplate) + { + continue; + } + + // start a new match + auto docMatch2 = document(); + auto docProject2 = document(); + + docProject2 << toString(Constants::Mongo::EPOCH_VAR) << 0; + docProject2 << MONGO_UPDATED << 0; + docProject2 << toString(Constants::Mongo::SERIES_VAR) << 0; + docProject2 << toString(Constants::Mongo::DX_VAR) << 0; + docProject2 << "_id" << 0; + + if (std::find(types.begin(), types.end(), KF::ALL) == types.end()) + { + auto array = docMatch2 << "$or" << open_array; + + for (auto& type : types) + { + array << open_document << toString(Constants::Mongo::STATE_DB) + << enum_to_string(type) << close_document; + } + + array << close_array; + } + + auto updateDoc = matchTemplate->view(); + + PTime pTime; + auto time = updateDoc[Constants::Mongo::EPOCH_VAR].get_date(); + pTime.bigTime = std::chrono::system_clock::to_time_t(time); + + kfState.time = pTime; + + time = updateDoc[MONGO_UPDATED].get_date(); + pTime.bigTime = std::chrono::system_clock::to_time_t(time); + + std::cout << "\n" << bsoncxx::to_json(updateDoc) << "\n"; + + docMatch2 << toString(Constants::Mongo::EPOCH_VAR) + << updateDoc[Constants::Mongo::EPOCH_VAR].get_date(); + docMatch2 << MONGO_UPDATED << updateDoc[MONGO_UPDATED].get_date(); + docMatch2 << toString(Constants::Mongo::SERIES_VAR) << "_predicted"; + if (str.empty() == false) + docMatch2 << toString(Constants::Mongo::STR_VAR) << str; + if (Sat.empty() == false) + docMatch2 << toString(Constants::Mongo::SAT_VAR) << Sat; + + // std::cout << "\n" << bsoncxx::to_json(docMatch2.view()) << "\n"; + + mongocxx::pipeline p; + p.match(docMatch2.view()); + p.project(docProject2.view()); + // p.group (docGroup .view()); + + // std::cout << "\n" << bsoncxx::to_json(docMatch); + // std::cout << "\n" << bsoncxx::to_json(docGroup); + + auto cursor = coll.aggregate(p); + + vector x; + vector P; + for (int i = 0; i < kfState.x.rows(); i++) + { + x.push_back(kfState.x(i)); + P.push_back(kfState.P(i, i)); + } + + int index = x.size(); + + for (auto doc : cursor) + { + // std::cout << bsoncxx::to_json(doc) << "\n"; + + KFKey kfKey; + kfKey.type = string_to_enum( + std::string(doc[Constants::Mongo::STATE_DB].get_string().value).c_str() + ); + kfKey.Sat = + SatSys(std::string(doc[Constants::Mongo::SAT_VAR].get_string().value).c_str()); + kfKey.str = std::string(doc[Constants::Mongo::STR_VAR].get_string().value).c_str(); + + int i = 0; + for (auto thing : doc[Constants::Mongo::NUM_VAR].get_array().value) + { + kfKey.num = doc[Constants::Mongo::NUM_VAR].get_array().value[i].get_int32(); + + x.push_back(doc[Constants::Mongo::X_VAR].get_array().value[i].get_double()); + P.push_back( + SQR(static_cast( + doc[Constants::Mongo::SIGMA_VAR].get_array().value[i].get_double() + )) + ); + + kfState.kfIndexMap[kfKey] = index; + index++; + i++; + } + } + + kfState.x = VectorXd(x.size()); + kfState.dx = VectorXd::Zero(x.size()); + kfState.P = MatrixXd::Zero(P.size(), P.size()); + + for (int i = 0; i < x.size(); i++) + { + kfState.x(i) = x[i]; + kfState.P(i, i) = P[i]; + } + } } diff --git a/src/cpp/common/mongoRead.hpp b/src/cpp/common/mongoRead.hpp index 5329ab0a7..610f541d7 100644 --- a/src/cpp/common/mongoRead.hpp +++ b/src/cpp/common/mongoRead.hpp @@ -1,51 +1,95 @@ - #pragma once -#include "rtcmEncoder.hpp" -#include "mongo.hpp" +#ifdef ENABLE_MONGODB + +#include "common/mongo.hpp" +#include "common/rtcmEncoder.hpp" struct SSRMeta; -SsrOutMap mongoReadOrbClk( - GTime referenceTime, - SSRMeta& ssrMeta, - int masterIod, - E_Sys targetSys); - -SsrCBMap mongoReadCodeBias( - SSRMeta& ssrMeta, - int masterIod, - E_Sys targetSys); - -SsrPBMap mongoReadPhaseBias( - SSRMeta& ssrMeta, - int masterIod, - E_Sys targetSys); - -Eph mongoReadEphemeris( - GTime targetTime, - SatSys Sat, - RtcmMessageType rtcmMessCode); - -Geph mongoReadGloEphemeris( - GTime targetTime, - SatSys Sat); - -SSRAtm mongoReadIGSIonosphere( - GTime time, - const SSRMeta& ssrMeta, - int masterIod); - -SSRAtm mongoReadCmpAtmosphere( - GTime time, - SSRMeta ssrMeta); - -class KF; +SsrOutMap mongoReadOrbClk(GTime referenceTime, SSRMeta& ssrMeta, int masterIod, E_Sys targetSys); + +SsrCBMap mongoReadCodeBias(SSRMeta& ssrMeta, int masterIod, E_Sys targetSys); + +SsrPBMap mongoReadPhaseBias(SSRMeta& ssrMeta, int masterIod, E_Sys targetSys); + +Eph mongoReadEphemeris(GTime targetTime, SatSys Sat, RtcmMessageType rtcmMessCode); + +Geph mongoReadGloEphemeris(GTime targetTime, SatSys Sat); + +SSRAtm mongoReadIGSIonosphere(GTime time, const SSRMeta& ssrMeta, int masterIod); + +SSRAtm mongoReadCmpAtmosphere(GTime time, SSRMeta ssrMeta); + +#include "enums.h" // For KF enum class definition struct KFState; void mongoReadFilter( - KFState& kfState, - GTime time = GTime::noTime(), - const vector& types = {}, - const string& Sat = "", - const string& str = ""); + KFState& kfState, + GTime time = GTime::noTime(), + const vector& types = {}, + const string& Sat = "", + const string& str = "" +); + +#else // !ENABLE_MONGODB + +// Stub declarations when MongoDB is disabled +#include +#include +#include "common/ephemeris.hpp" +#include "common/rtcmEncoder.hpp" +#include "enums.h" + +struct SSRMeta; +struct KFState; +struct GTime; + +inline SsrOutMap +mongoReadOrbClk(GTime referenceTime, SSRMeta& ssrMeta, int masterIod, E_Sys targetSys) +{ + return {}; +} + +inline SsrCBMap mongoReadCodeBias(SSRMeta& ssrMeta, int masterIod, E_Sys targetSys) +{ + return {}; +} + +inline SsrPBMap mongoReadPhaseBias(SSRMeta& ssrMeta, int masterIod, E_Sys targetSys) +{ + return {}; +} + +inline Eph mongoReadEphemeris(GTime targetTime, SatSys Sat, RtcmMessageType rtcmMessCode) +{ + return {}; +} + +inline Geph mongoReadGloEphemeris(GTime targetTime, SatSys Sat) +{ + return {}; +} + +inline SSRAtm mongoReadIGSIonosphere(GTime time, const SSRMeta& ssrMeta, int masterIod) +{ + return {}; +} + +inline SSRAtm mongoReadCmpAtmosphere(GTime time, SSRMeta ssrMeta) +{ + return {}; +} + +inline void mongoReadFilter( + KFState& kfState, + GTime time = GTime::noTime(), + const std::vector& types = {}, + const std::string& Sat = "", + const std::string& str = "" +) +{ + // No-op when MongoDB is disabled +} + +#endif // ENABLE_MONGODB diff --git a/src/cpp/common/mongoWrite.cpp b/src/cpp/common/mongoWrite.cpp index 5ec843276..6c5369cb1 100644 --- a/src/cpp/common/mongoWrite.cpp +++ b/src/cpp/common/mongoWrite.cpp @@ -1,1416 +1,1653 @@ - // #pragma GCC optimize ("O0") - -#include "observations.hpp" -#include "rtcmEncoder.hpp" -#include "coordinates.hpp" -#include "mongoWrite.hpp" -#include "GNSSambres.hpp" -#include "orbitProp.hpp" -#include "rtcmTrace.hpp" -#include "acsConfig.hpp" -#include "ionoModel.hpp" -#include "satStat.hpp" -#include "biases.hpp" -#include "common.hpp" -#include "mongo.hpp" - - +#include "common/mongoWrite.hpp" #include #include - #include - #include #include +#include "ambres/GNSSambres.hpp" +#include "common/acsConfig.hpp" +#include "common/biases.hpp" +#include "common/common.hpp" +#include "common/mongo.hpp" +#include "common/observations.hpp" +#include "common/rtcmEncoder.hpp" +#include "common/rtcmTrace.hpp" +#include "common/satStat.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/orbitProp.hpp" +using bsoncxx::builder::basic::kvp; using bsoncxx::builder::stream::close_array; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::finalize; using bsoncxx::builder::stream::open_array; using bsoncxx::builder::stream::open_document; -using bsoncxx::builder::basic::kvp; using bsoncxx::types::b_date; - struct DBEntry { - map> stringMap; - map> timeMap; - map> doubleMap; - map> intMap; - map> vectorMap; - map, bool>> doubleArrayMap; - map, bool>> boolArrayMap; + map> stringMap; + map> timeMap; + map> doubleMap; + map> intMap; + map> vectorMap; + map, bool>> doubleArrayMap; + map, bool>> boolArrayMap; }; struct QueuedMongo { - std::optional< KFState> kfState_optl; - std::optional< KFMeas> kfMeas_optl; - vector dbEntryList; + std::optional kfState_optl; + std::optional kfMeas_optl; + vector dbEntryList; - E_MongoType mongoType; - vector instances; - MongoStatesOptions mongoStatesOpts; + E_MongoType mongoType; + vector instances; + MongoStatesOptions mongoStatesOpts; - string suffix; + string suffix; - GTime time; + GTime time; }; void mongoOutput( - vector& dbEntryList, - bool queue, - vector instances, - string collection); + vector& dbEntryList, + bool queue, + vector instances, + string collection +); -list mongoQueue; -std::mutex mongoQueueMutex; -bool mongoQueueRunning = false; +list mongoQueue; +std::mutex mongoQueueMutex; +bool mongoQueueRunning = false; void mongoQueueRun() { - BOOST_LOG_TRIVIAL(debug) << "Running mongo queue thread"; - - while (1) - { - list localQueue; - //Guarding while we splice the shared queue to the local queue, which should be quick - { - lock_guard guard(mongoQueueMutex); - // We break when the queue is empty so the thread ends and we stop burning cycles - // It will be restarted when more messages come in - if (mongoQueue.empty()) - { - break; - } - - BOOST_LOG_TRIVIAL(debug) << "Mongo Queue: Splicing " << mongoQueue.size() << " entries for processing"; - localQueue.splice(localQueue.end(), mongoQueue); - } - BOOST_LOG_TRIVIAL(debug) << "Mongo Queue: Processing " << localQueue.size() << " entries"; - - vector traceJsons; - for (auto& object : localQueue) - { - switch (object.mongoType) - { - case E_MongoType::STATES_AVAILABLE: mongoStatesAvailable(object.kfState_optl->time, object.mongoStatesOpts); - case E_MongoType::STATES: mongoStates ( *object.kfState_optl, object.mongoStatesOpts); break; - case E_MongoType::RESIDUALS: mongoMeasResiduals (object.time, *object.kfMeas_optl, false, object.suffix); break; - case E_MongoType::TRACE: traceJsons.push_back(std::move(object.suffix)); break; - case E_MongoType::LIST: mongoOutput (object.dbEntryList, false, object.instances, object.suffix); break; - } - } - mongoTrace(traceJsons, false); - } - - lock_guard guard(mongoQueueMutex); - mongoQueueRunning = false; + BOOST_LOG_TRIVIAL(debug) << "Running mongo queue thread"; + + while (1) + { + list localQueue; + // Guarding while we splice the shared queue to the local queue, which should be quick + { + lock_guard guard(mongoQueueMutex); + // We break when the queue is empty so the thread ends and we stop burning cycles + // It will be restarted when more messages come in + if (mongoQueue.empty()) + { + break; + } + + BOOST_LOG_TRIVIAL(debug) + << "Mongo Queue: Splicing " << mongoQueue.size() << " entries for processing"; + localQueue.splice(localQueue.end(), mongoQueue); + } + BOOST_LOG_TRIVIAL(debug) << "Mongo Queue: Processing " << localQueue.size() << " entries"; + + vector traceJsons; + for (auto& object : localQueue) + { + switch (object.mongoType) + { + case E_MongoType::STATES_AVAILABLE: + mongoStatesAvailable(object.kfState_optl->time, object.mongoStatesOpts); + case E_MongoType::STATES: + mongoStates(*object.kfState_optl, object.mongoStatesOpts); + break; + case E_MongoType::RESIDUALS: + mongoMeasResiduals(object.time, *object.kfMeas_optl, false, object.suffix); + break; + case E_MongoType::TRACE: + traceJsons.push_back(std::move(object.suffix)); + break; + case E_MongoType::LIST: + mongoOutput(object.dbEntryList, false, object.instances, object.suffix); + break; + } + } + mongoTrace(traceJsons, false); + } + + lock_guard guard(mongoQueueMutex); + mongoQueueRunning = false; } -void queueMongo( - QueuedMongo& object) ///< Object to output +void queueMongo(QueuedMongo& object) ///< Object to output { - BOOST_LOG_TRIVIAL(debug) << "Queueing mongo: " << object.mongoType; + BOOST_LOG_TRIVIAL(debug) << "Queueing mongo: " << object.mongoType; - lock_guard guard(mongoQueueMutex); + lock_guard guard(mongoQueueMutex); - mongoQueue.push_back(std::move(object)); + mongoQueue.push_back(std::move(object)); - if (mongoQueueRunning == false) - { - mongoQueueRunning = true; + if (mongoQueueRunning == false) + { + mongoQueueRunning = true; - std::thread(mongoQueueRun).detach(); - } + std::thread(mongoQueueRun).detach(); + } } -b_date bDate( - const GTime& time) +b_date bDate(const GTime& time) { - int fractionalMilliseconds = (time.bigTime - (long int) time.bigTime) * 1000; + int fractionalMilliseconds = (time.bigTime - (long int)time.bigTime) * 1000; - auto stdTime = std::chrono::system_clock::from_time_t((time_t)((PTime)time).bigTime); + auto stdTime = std::chrono::system_clock::from_time_t((time_t)((PTime)time).bigTime); - stdTime += std::chrono::milliseconds(fractionalMilliseconds); + stdTime += std::chrono::milliseconds(fractionalMilliseconds); - return b_date(stdTime); + return b_date(stdTime); } -void mongoTestStat( - KFState& kfState, - TestStatistics& testStatistics) +void mongoTestStat(KFState& kfState, TestStatistics& testStatistics) { - auto instances = mongoInstances(acsConfig.mongoOpts.output_test_stats); - - if (instances.empty()) - { - return; - } - - map entries; - entries["StatsSumOfSquaresPre" ] = testStatistics.sumOfSquaresPre; - entries["StatsAverageRatioPre" ] = testStatistics.averageRatioPre; - entries["StatsSumOfSquaresPost" ] = testStatistics.sumOfSquaresPost; - entries["StatsAverageRatioPost" ] = testStatistics.averageRatioPost; - entries["StatsChiSquare" ] = testStatistics.chiSq; - entries["StatsChiSquareThreshold" ] = testStatistics.qc; - entries["StatsDegreeOfFreedom" ] = testStatistics.dof; - entries["StatsChiSquarePerDOF" ] = testStatistics.chiSqPerDof; - - for (auto instance : instances) - { - auto mongo_ptr = mongo_ptr_arr[instance]; - - if (mongo_ptr == nullptr) - continue; - - auto& mongo = *mongo_ptr; - - auto& config = acsConfig.mongoOpts[instance]; - - getMongoCollection(mongo, STATES_DB); - - mongocxx::options::bulk_write bulk_opts; - bulk_opts.ordered(false); - - auto bulk = coll.create_bulk_write(bulk_opts); - - bool update = false; - - for (auto& [state, value] : entries) - { - bsoncxx::builder::stream::document doc{}; - doc << REMOTE_EPOCH << bDate(kfState.time) - << MONGO_STR << kfState.id + config.suffix - << MONGO_SAT << "" + config.suffix - << MONGO_STATE << state - << MONGO_X << value; - - bsoncxx::document::value doc_val = doc << finalize; - bulk.append(mongocxx::model::insert_one(doc_val.view())); - update = true; - } - - if (update) - { - bulk.execute(); - } - } + auto instances = mongoInstances(acsConfig.mongoOpts.output_test_stats); + + if (instances.empty()) + { + return; + } + + map entries; + entries["StatsSumOfSquaresLsq"] = testStatistics.sumOfSquaresLsq; + entries["StatsAverageRatioLsq"] = testStatistics.averageRatioLsq; + entries["StatsSumOfSquaresPre"] = testStatistics.sumOfSquaresPre; + entries["StatsAverageRatioPre"] = testStatistics.averageRatioPre; + entries["StatsSumOfSquaresPost"] = testStatistics.sumOfSquaresPost; + entries["StatsAverageRatioPost"] = testStatistics.averageRatioPost; + entries["StatsChiSquare"] = testStatistics.chiSq; + entries["StatsChiSquareThreshold"] = testStatistics.qc; + entries["StatsDegreeOfFreedom"] = testStatistics.dof; + entries["StatsChiSquarePerDOF"] = testStatistics.chiSqPerDof; + + for (auto instance : instances) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; + + if (mongo_ptr == nullptr) + continue; + + auto& mongo = *mongo_ptr; + + auto& config = acsConfig.mongoOpts[static_cast(instance)]; + + getMongoCollection(mongo, Constants::Mongo::STATES_DB); + + mongocxx::options::bulk_write bulk_opts; + bulk_opts.ordered(false); + + auto bulk = coll.create_bulk_write(bulk_opts); + + bool update = false; + + for (auto& [state, value] : entries) + { + bsoncxx::builder::stream::document doc{}; + doc << REMOTE_EPOCH << bDate(kfState.time) << toString(Constants::Mongo::STR_VAR) + << kfState.id + config.suffix << toString(Constants::Mongo::SAT_VAR) + << "" + config.suffix << toString(Constants::Mongo::STATE_DB) << state + << toString(Constants::Mongo::X_VAR) << value; + + bsoncxx::document::value doc_val = doc << finalize; + bulk.append(mongocxx::model::insert_one(doc_val.view())); + update = true; + } + + if (update) + { + bulk.execute(); + } + } } -void mongoTrace( - const vector& jsons, - bool queue) +void mongoTrace(const vector& jsons, bool queue) { - auto instances = mongoInstances(acsConfig.mongoOpts.output_trace); - if (instances.empty()) - { - return; - } + auto instances = mongoInstances(acsConfig.mongoOpts.output_trace); + if (instances.empty()) + { + return; + } - if (queue) - { - for (auto& json : jsons) - { - QueuedMongo queueEntry; - queueEntry.suffix = json; - queueEntry.mongoType = E_MongoType::TRACE; + if (queue) + { + for (auto& json : jsons) + { + QueuedMongo queueEntry; + queueEntry.suffix = json; + queueEntry.mongoType = E_MongoType::TRACE; - queueMongo(queueEntry); - } - return; - } + queueMongo(queueEntry); + } + return; + } - for (auto instance : instances) - { - auto mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : instances) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; + auto& mongo = *mongo_ptr; - getMongoCollection(mongo, MONGO_TRACE); + getMongoCollection(mongo, Constants::Mongo::TRACE_DB); - mongocxx::options::bulk_write bulk_opts; - bulk_opts.ordered(false); + mongocxx::options::bulk_write bulk_opts; + bulk_opts.ordered(false); - auto bulk = coll.create_bulk_write(bulk_opts); - for (auto& json : jsons) - { - auto doc = bsoncxx::from_json(json); + auto bulk = coll.create_bulk_write(bulk_opts); + for (auto& json : jsons) + { + auto doc = bsoncxx::from_json(json); - bulk.append(mongocxx::model::insert_one(doc.view())); - } + bulk.append(mongocxx::model::insert_one(doc.view())); + } - bulk.execute(); - } + bulk.execute(); + } } -void mongoOutputConfig( - string& config) +void mongoOutputConfig(string& config) { - auto instances = mongoInstances(acsConfig.mongoOpts.output_config); - if (instances.empty()) - { - return; - } + auto instances = mongoInstances(acsConfig.mongoOpts.output_config); + if (instances.empty()) + { + return; + } - for (auto instance : instances) - { - auto mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : instances) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; + auto& mongo = *mongo_ptr; - getMongoCollection(mongo, MONGO_CONFIG); + getMongoCollection(mongo, Constants::Mongo::CONFIG_DB); - mongocxx::options::bulk_write bulk_opts; - bulk_opts.ordered(false); + mongocxx::options::bulk_write bulk_opts; + bulk_opts.ordered(false); - auto bulk = coll.create_bulk_write(bulk_opts); + auto bulk = coll.create_bulk_write(bulk_opts); - try - { - auto doc = bsoncxx::from_json(config); + try + { + auto doc = bsoncxx::from_json(config); - bulk.append(mongocxx::model::insert_one(doc.view())); + bulk.append(mongocxx::model::insert_one(doc.view())); - bulk.execute(); - } - catch (...) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Could not output config to mongo, likely due to empty entries in yaml."; - } - } + bulk.execute(); + } + catch (...) + { + BOOST_LOG_TRIVIAL(warning) + << "Could not output config to mongo, likely due to empty entries in yaml."; + } + } } -void mongoMeasSatStat( - ReceiverMap& receiverMap) +void mongoMeasSatStat(ReceiverMap& receiverMap) { - auto instances = mongoInstances(acsConfig.mongoOpts.output_measurements); - if (instances.empty()) - { - return; - } + auto instances = mongoInstances(acsConfig.mongoOpts.output_measurements); + if (instances.empty()) + { + return; + } - for (auto instance : instances) - { - auto mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : instances) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; + auto& mongo = *mongo_ptr; - auto& config = acsConfig.mongoOpts[instance]; + auto& config = acsConfig.mongoOpts[static_cast(instance)]; - getMongoCollection(mongo, MONGO_GEOMETRY); + getMongoCollection(mongo, Constants::Mongo::GEOMETRY_DB); - mongocxx::options::bulk_write bulk_opts; - bulk_opts.ordered(false); + mongocxx::options::bulk_write bulk_opts; + bulk_opts.ordered(false); - auto bulk = coll.create_bulk_write(bulk_opts); + auto bulk = coll.create_bulk_write(bulk_opts); - bool update = false; + bool update = false; - for (auto& [id, rec] : receiverMap) - { - for (auto& obs_ptr : rec.obsList) - { - auto& obs = *obs_ptr; + for (auto& [id, rec] : receiverMap) + { + for (auto& obs_ptr : rec.obsList) + { + auto& obs = *obs_ptr; - try - { - auto& satPos = dynamic_cast(obs); + try + { + auto& satPos = dynamic_cast(obs); - if (obs.exclude) - continue; + if (obs.exclude) + continue; - if (satPos.satStat_ptr == nullptr) - continue; + if (satPos.satStat_ptr == nullptr) + continue; - SatStat& satStat = *satPos.satStat_ptr; + SatStat& satStat = *satPos.satStat_ptr; - bsoncxx::builder::stream::document doc{}; - doc << MONGO_EPOCH << bDate(tsync) - << MONGO_STR << obs.mount - << MONGO_SAT << satPos.Sat.id() - << MONGO_SERIES << formatSeries(config.suffix) - << MONGO_AZIMUTH << satStat.az * R2D - << MONGO_ELEVATION << satStat.el * R2D - << MONGO_NADIR << satStat.nadir * R2D; + bsoncxx::builder::stream::document doc{}; + doc << toString(Constants::Mongo::EPOCH_VAR) << bDate(tsync) + << toString(Constants::Mongo::STR_VAR) << obs.mount + << toString(Constants::Mongo::SAT_VAR) << satPos.Sat.id() + << toString(Constants::Mongo::SERIES_VAR) << formatSeries(config.suffix) + << toString(Constants::Mongo::AZIMUTH_VAR) << satStat.az * R2D + << toString(Constants::Mongo::ELEVATION_VAR) << satStat.el * R2D + << toString(Constants::Mongo::NADIR_VAR) << satStat.nadir * R2D; - bsoncxx::document::value doc_val = doc << finalize; - bulk.append(mongocxx::model::insert_one(doc_val.view())); - update = true; - } - catch (...) - { - } - } - } + bsoncxx::document::value doc_val = doc << finalize; + bulk.append(mongocxx::model::insert_one(doc_val.view())); + update = true; + } + catch (...) + { + } + } + } - if (update) - bulk.execute(); - } + if (update) + bulk.execute(); + } } void mongoMeasResiduals( - const GTime& time, - KFMeas& kfMeas, - bool queue, - string suffix, - int beg, - int num) + const GTime& time, + KFMeas& kfMeas, + bool queue, + string suffix, + int beg, + int num +) { - auto instances = mongoInstances(acsConfig.mongoOpts.output_measurements); - - if (instances.empty()) - { - return; - } - - if (queue) - { - QueuedMongo queueEntry; - queueEntry.kfMeas_optl = kfMeas; - queueEntry.suffix = suffix; - queueEntry.time = time; - queueEntry.mongoType = E_MongoType::RESIDUALS; + auto instances = mongoInstances(acsConfig.mongoOpts.output_measurements); - queueMongo(queueEntry); + if (instances.empty()) + { + return; + } - return; - } + if (queue) + { + QueuedMongo queueEntry; + queueEntry.kfMeas_optl = kfMeas; + queueEntry.suffix = suffix; + queueEntry.time = time; + queueEntry.mongoType = E_MongoType::RESIDUALS; - for (auto instance : instances) - { - auto mongo_ptr = mongo_ptr_arr[instance]; + queueMongo(queueEntry); - if (mongo_ptr == nullptr) - continue; + return; + } - auto& mongo = *mongo_ptr; + for (auto instance : instances) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - auto& config = acsConfig.mongoOpts[instance]; + if (mongo_ptr == nullptr) + continue; - getMongoCollection(mongo, MONGO_MEASUREMENTS); + auto& mongo = *mongo_ptr; - mongocxx::options::bulk_write bulk_opts; - bulk_opts.ordered(false); + auto& config = acsConfig.mongoOpts[static_cast(instance)]; - auto bulk = coll.create_bulk_write(bulk_opts); + getMongoCollection(mongo, Constants::Mongo::MEASUREMENTS_DB); - bool update = false; + mongocxx::options::bulk_write bulk_opts; + bulk_opts.ordered(false); - if (num < 0) - { - num = kfMeas.obsKeys.size(); - } + auto bulk = coll.create_bulk_write(bulk_opts); - map, vector> lookup; + bool update = false; - map indexSeries; - map indexLabel; - map indexSite; - map indexSat; + if (num < 0) + { + num = kfMeas.obsKeys.size(); + } - for (int i = beg; i < beg + num; i++) - { - KFKey& obsKey = kfMeas.obsKeys[i]; + map, vector> lookup; - string commentString; - if (obsKey.comment.empty() == false) - commentString = obsKey.comment + "-"; + map indexSeries; + map indexLabel; + map indexSite; + map indexSat; - string name = commentString + std::to_string(obsKey.num); + for (int i = beg; i < beg + num; i++) + { + KFKey& obsKey = kfMeas.obsKeys[i]; - lookup[{obsKey.str, obsKey.Sat.id()}].push_back(i); - } + string commentString; + if (obsKey.comment.empty() == false) + commentString = obsKey.comment + "-"; - for (auto& [description, index] : lookup) - { - bsoncxx::builder::stream::document doc{}; - auto& [site, sat] = description; - auto series = formatSeries(config.suffix + suffix); - doc << MONGO_EPOCH << bDate(time) - << MONGO_STR << site - << MONGO_SAT << sat - << MONGO_SERIES << series; + string name = + commentString + std::to_string(obsKey.num); // Eugene: convert num to code? - indexSeries [series] = true; - indexSite [site] = true; - indexSat [sat] = true; + lookup[{obsKey.str, obsKey.Sat.id()}].push_back(i); + } - for (int& i : index) - { - KFKey& obsKey = kfMeas.obsKeys[i]; + for (auto& [description, index] : lookup) + { + bsoncxx::builder::stream::document doc{}; + auto& [site, sat] = description; + auto series = formatSeries(config.suffix + suffix); + doc << toString(Constants::Mongo::EPOCH_VAR) << bDate(time) + << toString(Constants::Mongo::STR_VAR) << site + << toString(Constants::Mongo::SAT_VAR) << sat + << toString(Constants::Mongo::SERIES_VAR) << series; - string commentString; - if (obsKey.comment.empty() == false) - commentString = obsKey.comment + "-"; + indexSeries[series] = true; + indexSite[site] = true; + indexSat[sat] = true; - string name = commentString + std::to_string(obsKey.num); + for (int& i : index) + { + KFKey& obsKey = kfMeas.obsKeys[i]; - doc << name + "-Prefit" << kfMeas.V (i) - << name + "-Postfit" << kfMeas.VV (i) - << name + "-Sigma" << sqrt( kfMeas.R (i,i)); + string commentString; + if (obsKey.comment.empty() == false) + commentString = obsKey.comment + "-"; - indexLabel[name + "-Prefit"] = true; - indexLabel[name + "-Postfit"] = true; - indexLabel[name + "-Sigma"] = true; + string name = + commentString + std::to_string(obsKey.num); // Eugene: convert num to code? - if ( (instance & acsConfig.mongoOpts.output_components) == +E_Mongo::NONE - ||kfMeas.componentsMaps.empty()) - { - continue; - } + doc << name + "-Prefit" << kfMeas.V(i) << name + "-Postfit" << kfMeas.VV(i) + << name + "-Sigma" << sqrt(kfMeas.R(i, i)); - auto& componentsMap = kfMeas.componentsMaps[i]; + indexLabel[name + "-Prefit"] = true; + indexLabel[name + "-Postfit"] = true; + indexLabel[name + "-Sigma"] = true; - double cumulative = 0; + if ((static_cast(instance) & + static_cast(acsConfig.mongoOpts.output_components)) == + static_cast(E_Mongo::NONE) || + kfMeas.componentsMaps.empty()) + { + continue; + } - for (auto& [component, details] : componentsMap) - { - auto& [value, desc, var] = details; + auto& componentsMap = kfMeas.componentsMaps[i]; - string label = name - + " " + KF::_from_integral_unchecked(obsKey.type)._to_string() - + " " + component._to_string(); + double cumulative = 0; - doc << label << value; + for (auto& [component, details] : componentsMap) + { + auto& [value, desc, var] = details; - indexLabel[label] = true; + string label = + name + " " + enum_to_string(obsKey.type) + " " + enum_to_string(component); - if (acsConfig.mongoOpts.output_cumulative == +E_Mongo::NONE) - { - continue; - } + doc << label << value; - cumulative += value; + indexLabel[label] = true; - string resLabel; - if (component._to_integral() >= 10) resLabel = (string) "RES-" + std::to_string(component._to_integral()); - else resLabel = (string) "RES-0" + std::to_string(component._to_integral()); + if (acsConfig.mongoOpts.output_cumulative == E_Mongo::NONE) + { + continue; + } - label = name - + " " + KF::_from_integral_unchecked(obsKey.type)._to_string() - + "_" + resLabel - + " " + component._to_string(); + cumulative += value; - doc << label << cumulative; + string resLabel; + if (static_cast(component) >= 10) + resLabel = (string) "RES-" + std::to_string(static_cast(component)); + else + resLabel = (string) "RES-0" + std::to_string(static_cast(component)); - indexLabel[label] = true; - } - } + label = name + " " + enum_to_string(obsKey.type) + "_" + resLabel + " " + + enum_to_string(component); - bsoncxx::document::value doc_val = doc << finalize; + doc << label << cumulative; - bulk.append(mongocxx::model::insert_one(doc_val.view())); + indexLabel[label] = true; + } + } - update = true; - } + bsoncxx::document::value doc_val = doc << finalize; - if (update) - { - bulk.execute(); + bulk.append(mongocxx::model::insert_one(doc_val.view())); - auto addIndices = [&](string name, map index) - { - mongocxx::options::update options; - options.upsert(true); + update = true; + } - auto eachDoc = document{}; + if (update) + { + bulk.execute(); - auto arrayDoc = eachDoc << "$each" << open_array; - for (auto &[indexName, unused]: index) - { - arrayDoc << indexName; - } - arrayDoc << close_array; + auto addIndices = [&](string name, map index) + { + mongocxx::options::update options; + options.upsert(true); - auto findDoc = document{} << MONGO_TYPE << name << finalize; - auto updateDoc = document{} << "$addToSet" << open_document << MONGO_VALUES << eachDoc << close_document << finalize; + auto eachDoc = document{}; + + auto arrayDoc = eachDoc << "$each" << open_array; + for (auto& [indexName, unused] : index) + { + arrayDoc << indexName; + } + arrayDoc << close_array; + + 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; - db[MONGO_CONTENT].update_one(findDoc.view(), updateDoc.view(), options); - }; + db[Constants::Mongo::CONTENT_DB] + .update_one(findDoc.view(), updateDoc.view(), options); + }; - addIndices(MONGO_MEASUREMENTS, indexLabel); - addIndices(MONGO_MEASUREMENTS MONGO_SERIES, indexSeries); - addIndices(MONGO_STR, indexSite); - addIndices(MONGO_SAT, indexSat); - } - } + addIndices(Constants::Mongo::MEASUREMENTS_DB, indexLabel); + addIndices( + toString(Constants::Mongo::MEASUREMENTS_DB) + + toString(Constants::Mongo::SERIES_VAR), + indexSeries + ); + addIndices(toString(Constants::Mongo::STR_VAR), indexSite); + addIndices(toString(Constants::Mongo::SAT_VAR), indexSat); + } + } } -void mongoStatesAvailable( - GTime time, - MongoStatesOptions opts) +void mongoStatesAvailable(GTime time, MongoStatesOptions opts) { - auto instances = mongoInstances(opts.instances); + auto instances = mongoInstances(opts.instances); - if (instances.empty()) - { - return; - } + if (instances.empty()) + { + return; + } - if (opts.queue) - { - opts.queue = false; + if (opts.queue) + { + opts.queue = false; - QueuedMongo queueEntry; - queueEntry.kfState_optl->time = time; - queueEntry.mongoType = E_MongoType::STATES_AVAILABLE; - queueEntry.mongoStatesOpts = opts; + QueuedMongo queueEntry; + queueEntry.kfState_optl->time = time; + queueEntry.mongoType = E_MongoType::STATES_AVAILABLE; + queueEntry.mongoStatesOpts = opts; - queueMongo(queueEntry); + queueMongo(queueEntry); - return; - } + return; + } - for (auto instance : instances) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; + for (auto instance : instances) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + if (mongo_ptr == nullptr) + continue; - auto& mongo = *mongo_ptr; - auto& config = acsConfig.mongoOpts[instance]; + auto& mongo = *mongo_ptr; + auto& config = acsConfig.mongoOpts[static_cast(instance)]; - auto c = mongo.pool.acquire(); - mongocxx::client& client = *c; - mongocxx::database db = client[mongo.database]; - mongocxx::collection coll = db[opts.collection]; + auto c = mongo.pool.acquire(); + mongocxx::client& client = *c; + mongocxx::database db = client[mongo.database]; + mongocxx::collection coll = db[opts.collection]; - auto findDoc = document{} - << MONGO_TYPE << MONGO_AVAILABLE - << MONGO_EPOCH << bDate(time) - << MONGO_UPDATED << bDate(opts.updated) - << finalize; + auto findDoc = document{} << toString(Constants::Mongo::TYPE_VAR) << MONGO_AVAILABLE + << toString(Constants::Mongo::EPOCH_VAR) << bDate(time) + << MONGO_UPDATED << bDate(opts.updated) << finalize; - db[STATES_DB].insert_one(findDoc.view()); - } + db[Constants::Mongo::STATES_DB].insert_one(findDoc.view()); + } } -void mongoStates( - KFState& kfState, - MongoStatesOptions opts) +void mongoStates(KFState& kfState, MongoStatesOptions opts) { - auto instances = mongoInstances(opts.instances); + auto instances = mongoInstances(opts.instances); - if (instances.empty()) - { - return; - } - - if (opts.queue) - { - opts.queue = false; - - QueuedMongo queueEntry; - queueEntry.kfState_optl = kfState; - queueEntry.mongoType = E_MongoType::STATES; - queueEntry.mongoStatesOpts = opts; - - queueMongo(queueEntry); - - return; - } - - for (auto instance : instances) - { - auto mongo_ptr = mongo_ptr_arr[instance]; - - if (mongo_ptr == nullptr) - continue; + if (instances.empty()) + { + return; + } + + if (opts.queue) + { + opts.queue = false; + + QueuedMongo queueEntry; + queueEntry.kfState_optl = kfState; + queueEntry.mongoType = E_MongoType::STATES; + queueEntry.mongoStatesOpts = opts; + + queueMongo(queueEntry); + + return; + } + + for (auto instance : instances) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; + + if (mongo_ptr == nullptr) + continue; + + auto& mongo = *mongo_ptr; + + auto& config = acsConfig.mongoOpts[static_cast(instance)]; + + getMongoCollection(mongo, opts.collection); + + mongocxx::options::bulk_write bulk_opts; + + bulk_opts.ordered(false); + + auto bulk = coll.create_bulk_write(bulk_opts); + + map, vector>> lookup; + map indexSeries; + map indexState; + map indexSite; + map indexSat; + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type == KF::ONE) + { + continue; + } + + lookup[{key.str, key.Sat.id(), enum_to_string(key.type)}].push_back({index, key.num}); + } + + vector documents; + + bool update = false; + + for (auto& [description, index] : lookup) + { + auto& [site, sat, state] = description; + + bsoncxx::builder::stream::document keydoc{}; + bsoncxx::builder::stream::document valdoc{}; + bsoncxx::builder::stream::document* val_ptr; + + if (opts.upsert) + { + val_ptr = &valdoc; + valdoc << "$set" << open_document; + } + else + { + val_ptr = &keydoc; + } + + auto& doc = *val_ptr; + string series = formatSeries(config.suffix + opts.suffix); + keydoc << toString(Constants::Mongo::EPOCH_VAR) << bDate(kfState.time) + << toString(Constants::Mongo::STR_VAR) << site + << toString(Constants::Mongo::SAT_VAR) << sat + << toString(Constants::Mongo::STATE_DB) << state + << toString(Constants::Mongo::SERIES_VAR) << series; + + indexSeries[series] = true; + indexState[state] = true; + indexSat[sat] = true; + indexSite[site] = true; + + if (opts.updated != GTime::noTime()) + { + doc << toString(MONGO_UPDATED) + << b_date{std::chrono::system_clock::from_time_t( + (time_t)((PTime)opts.updated).bigTime + )}; + } + + auto array_builder = doc << toString(Constants::Mongo::X_VAR) << open_array; + for (auto& [i, num] : index) + array_builder << kfState.x(i); + array_builder << close_array; + array_builder = doc << toString(Constants::Mongo::DX_VAR) << open_array; + for (auto& [i, num] : index) + array_builder << kfState.dx(i); + array_builder << close_array; + array_builder = doc << toString(Constants::Mongo::SIGMA_VAR) << open_array; + for (auto& [i, num] : index) + array_builder << sqrt(kfState.P(i, i)); + array_builder << close_array; + array_builder = doc << toString(Constants::Mongo::NUM_VAR) << open_array; + for (auto& [i, num] : index) + array_builder << num; + array_builder << close_array; + + if (static_cast(instance) & + static_cast(acsConfig.mongoOpts.output_state_covars)) + { + array_builder = doc << toString(Constants::Mongo::COVAR_VAR) << open_array; + for (auto& [i, numI] : index) + for (auto& [j, numJ] : index) + if (j > i) + array_builder << kfState.P(i, j); + array_builder << close_array; + } + + if (opts.upsert) + { + valdoc << close_document; + bulk.append(mongocxx::model::update_one(keydoc.view(), valdoc.view()).upsert(true)); + } + else + { + bulk.append(mongocxx::model::insert_one(keydoc.view())); + } + + update = true; + } + + if (update) + { + bulk.execute(); + + auto addIndices = [&](string name, map index) + { + mongocxx::options::update options; + options.upsert(true); + + auto eachDoc = document{}; + + auto arrayDoc = eachDoc << "$each" << open_array; + for (auto& [indexStr, unused] : index) + { + arrayDoc << indexStr; + } + arrayDoc << close_array; + + 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; + + db[Constants::Mongo::CONTENT_DB] + .update_one(findDoc.view(), updateDoc.view(), options); + }; + + if (opts.index) + { + addIndices(Constants::Mongo::STATE_DB, indexState); + addIndices( + toString(Constants::Mongo::STATE_DB) + toString(Constants::Mongo::SERIES_VAR), + indexSeries + ); + addIndices(toString(Constants::Mongo::STR_VAR), indexSite); + addIndices(toString(Constants::Mongo::SAT_VAR), indexSat); + } + } + } +} - auto& mongo = *mongo_ptr; +void mongoCull(GTime time) +{ + auto instances = mongoInstances(acsConfig.mongoOpts.cull_history); + if (instances.empty()) + { + return; + } - auto& config = acsConfig.mongoOpts[instance]; + for (auto instance : instances) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - getMongoCollection(mongo, opts.collection); + if (mongo_ptr == nullptr) + continue; - mongocxx::options::bulk_write bulk_opts; + auto& mongo = *mongo_ptr; - bulk_opts.ordered(false); + auto& config = acsConfig.mongoOpts[static_cast(instance)]; - auto bulk = coll.create_bulk_write(bulk_opts); + for (auto collection : {SSR_DB, REMOTE_DATA_DB}) + { + getMongoCollection(mongo, collection); - map, vector>> lookup; - map indexSeries; - map indexState; - map indexSite; - map indexSat; + b_date btime{std::chrono::system_clock::from_time_t( + (time_t)((PTime)(time - acsConfig.mongoOpts.min_cull_age)).bigTime + )}; - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type == KF::ONE) - { - continue; - } + // Remove all documents that match a condition. + auto docSys = document{} << SSR_EPOCH << open_document << "$lt" << btime + << close_document << finalize; - lookup[{key.str, key.Sat.id(), KF::_from_integral_unchecked(key.type)._to_string()}].push_back({index, key.num}); - } + coll.delete_many(docSys.view()); + } + } +} - vector documents; +document entryToDocument(DBEntry& entry, bool type) +{ + // builder::document builds an empty BSON document + document doc = {}; + + for (auto& [k, v] : entry.stringMap) + { + auto& [e, b] = v; + if (type == b) + doc << k << e; + } + for (auto& [k, v] : entry.intMap) + { + auto& [e, b] = v; + if (type == b) + doc << k << e; + } + for (auto& [k, v] : entry.doubleMap) + { + auto& [e, b] = v; + if (type == b) + doc << k << e; + } + for (auto& [k, v] : entry.timeMap) + { + auto& [e, b] = v; + if (type == b) + doc << k << b_date{std::chrono::system_clock::from_time_t((time_t)((PTime)e).bigTime)}; + } + for (auto& [k, v] : entry.vectorMap) + { + auto& [e, b] = v; + if (type == b) + for (int i = 0; i < 3; i++) + doc << k + std::to_string(i) << e[i]; + } + for (auto& [k, v] : entry.doubleArrayMap) + { + auto& [e, b] = v; + if (type == b) + { + auto builder = doc << k << open_array; + for (auto& a : e) + builder << a; + builder << close_array; + } + } + for (auto& [k, v] : entry.boolArrayMap) + { + auto& [e, b] = v; + if (type == b) + { + auto builder = doc << k << open_array; + for (auto& a : e) + builder << a; + builder << close_array; + } + } + + return doc; +} - bool update = false; +void mongoOutput( + vector& dbEntryList, + bool queue, + vector instances, + string collection +) +{ + if (queue) + { + QueuedMongo queueEntry; + queueEntry.dbEntryList = dbEntryList; + queueEntry.instances = instances; + queueEntry.suffix = collection; + queueEntry.mongoType = E_MongoType::LIST; - for (auto& [description, index] : lookup) - { - auto& [site, sat, state] = description; + queueMongo(queueEntry); - bsoncxx::builder::stream::document keydoc{}; - bsoncxx::builder::stream::document valdoc{}; - bsoncxx::builder::stream::document* val_ptr; + return; + } - if (opts.upsert) { val_ptr = &valdoc; valdoc << "$set" << open_document; } - else { val_ptr = &keydoc; } + for (auto instance : instances) + { + Mongo* mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - auto& doc = *val_ptr; - string series = formatSeries(config.suffix + opts.suffix); - keydoc << MONGO_EPOCH << bDate(kfState.time) - << MONGO_STR << site - << MONGO_SAT << sat - << MONGO_STATE << state - << MONGO_SERIES << series; + if (mongo_ptr == nullptr) + continue; - indexSeries [series] = true; - indexState [state] = true; - indexSat [sat] = true; - indexSite [site] = true; + auto& mongo = *mongo_ptr; - if (opts.updated != GTime::noTime()) - { - doc << MONGO_UPDATED << b_date{std::chrono::system_clock::from_time_t((time_t)((PTime)opts.updated).bigTime)}; - } + auto c = mongo.pool.acquire(); + mongocxx::client& client = *c; + mongocxx::database db = client[mongo.database]; + mongocxx::collection coll = db[collection]; - auto array_builder = doc << MONGO_X << open_array; for (auto& [i, num]: index) array_builder << kfState.x (i); array_builder << close_array; - array_builder = doc << MONGO_DX << open_array; for (auto& [i, num]: index) array_builder << kfState.dx (i); array_builder << close_array; - array_builder = doc << MONGO_SIGMA << open_array; for (auto& [i, num]: index) array_builder << sqrt( kfState.P (i,i)); array_builder << close_array; - array_builder = doc << MONGO_NUM << open_array; for (auto& [i, num]: index) array_builder << num; array_builder << close_array; + mongocxx::options::bulk_write bulk_opts; + bulk_opts.ordered(false); - if (instance & acsConfig.mongoOpts.output_state_covars) - { - array_builder = doc << MONGO_COVAR << open_array; for (auto& [i, numI]: index) - for (auto& [j, numJ]: index) - if (j > i) array_builder << kfState.P (i,j); array_builder << close_array; - } + auto bulk = coll.create_bulk_write(bulk_opts); - if (opts.upsert) { valdoc << close_document; bulk.append(mongocxx::model::update_one(keydoc.view(), valdoc.view()).upsert(true)); } - else { bulk.append(mongocxx::model::insert_one(keydoc.view())); } + bool update = false; - update = true; - } + for (auto& entry : dbEntryList) + { + // builder::document builds an empty BSON document + auto keys = entryToDocument(entry, true); + auto vals = entryToDocument(entry, false); - if (update) - { - bulk.execute(); + bsoncxx::builder::basic::document Vals = {}; - auto addIndices = [&](string name, map index) - { - mongocxx::options::update options; - options.upsert(true); + Vals.append(kvp("$set", vals)); - auto eachDoc = document{}; + // std::cout << "\n" << bsoncxx::to_json(keys.view()) << + // bsoncxx::to_json(Vals.view()) << "\n"; - auto arrayDoc = eachDoc << "$each" << open_array; - for (auto &[indexStr, unused]: index) - { - arrayDoc << indexStr; - } - arrayDoc << close_array; + mongocxx::model::update_one mongo_req{keys.view(), Vals.view()}; - auto findDoc = document{} << MONGO_TYPE << name << finalize; - auto updateDoc = document{} << "$addToSet" << open_document << MONGO_VALUES << eachDoc << close_document << finalize; + update = true; + mongo_req.upsert(true); + bulk.append(mongo_req); + } - db[MONGO_CONTENT].update_one(findDoc.view(), updateDoc.view(), options); - }; - - if (opts.index) - { - addIndices(MONGO_STATE, indexState); - addIndices(MONGO_STATE MONGO_SERIES, indexSeries); - addIndices(MONGO_STR, indexSite); - addIndices(MONGO_SAT, indexSat); - } - } - } + if (update) + bulk.execute(); + } } - -void mongoCull( - GTime time) +/** Write states generating SSR corrections to Mongo DB + */ +void prepareSsrStates( + Trace& trace, ///< Trace to output to + KFState& kfState, ///< Filter object to extract state elements from + KFState& ionState, ///< Filter object to extract state elements from + GTime time ///< Time of current epoch +) { - auto instances = mongoInstances(acsConfig.mongoOpts.cull_history); - if (instances.empty()) - { - return; - } - - for (auto instance : instances) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; - - if (mongo_ptr == nullptr) - continue; - - auto& mongo = *mongo_ptr; - - auto& config = acsConfig.mongoOpts[instance]; - - for (auto collection: {SSR_DB, REMOTE_DATA_DB}) - { - getMongoCollection(mongo, collection); - - b_date btime{std::chrono::system_clock::from_time_t((time_t)((PTime)(time - acsConfig.mongoOpts.min_cull_age)).bigTime)}; - - // Remove all documents that match a condition. - auto docSys = document{} << SSR_EPOCH - << open_document - << "$lt" << btime - << close_document - << finalize; - - coll.delete_many(docSys.view()); - } - } + auto instances = mongoInstances(acsConfig.mongoOpts.output_ssr_precursors); + + if (instances.empty()) + { + return; + } + + BOOST_LOG_TRIVIAL(info) << "Calculating SSR message precursors\n"; + + vector dbEntryList; + + time.bigTime = + (long int)(time.bigTime + 0.5); // time tags in mongo will be rounded up to whole sec + + for (E_Sys sys : magic_enum::enum_values()) + { + string sysName = boost::algorithm::to_lower_copy((string)enum_to_string(sys)); + + if (acsConfig.process_sys[sys] == false) + continue; + + auto sats = getSysSats(sys); + + // Calculate orbits + clocks at tsync & tsync+udi + for (auto& Sat : sats) + { + // Create a dummy observation + GObs obs; + obs.Sat = Sat; + + auto& satNav = nav.satNavMap[obs.Sat]; + SatStat satStatDummy; + + obs.satStat_ptr = &satStatDummy; + obs.satNav_ptr = &satNav; + + GTime teph = time; + + // Clock and orbit corrections (and predicted corrections) + for (int tpredict = 0; tpredict <= acsConfig.ssrOpts.prediction_duration; + tpredict += acsConfig.ssrOpts.prediction_interval) + { + GTime pTime = time + tpredict; + bool pass = true; + + pass = true; + pass &= satclk( + nullStream, + pTime, + pTime, + obs, + acsConfig.ssrOpts.clock_sources, + nav, + &kfState + ); + pass &= satpos( + nullStream, + pTime, + pTime, + obs, + acsConfig.ssrOpts.ephemeris_sources, + E_OffsetType::APC, + nav, + &kfState + ); // todo aaron, ssra streams expect common_sat_pco to be true + if (obs.satClk == INVALID_CLOCK_VALUE) + { + pass = false; + } + + // precise clock + double precClkVal = obs.satClk * CLIGHT; + + if (pass == false) + { + BOOST_LOG_TRIVIAL(info) << Sat.id() << " failed ssrprecise prediction.\n"; + + continue; + } + + if (obs.iodeClk != obs.iodePos) + { + BOOST_LOG_TRIVIAL(info) << Sat.id() << " IODE clock" << obs.iodeClk + << " mismatch IODE orbit " << obs.iodePos << ".\n"; + + continue; + } + + Vector3d precPos = obs.rSatApc; + Vector3d precVel = obs.satVel; + double posVar = obs.posVar; + + // broadcast values, must come after precise values to associate the correct IODE + // (when SSR in use) + pass &= satClkBroadcast(nullStream, pTime, pTime, obs, nav, obs.iodeClk); + pass &= satPosBroadcast(nullStream, pTime, pTime, obs, nav, obs.iodePos); + if (pass == false) + { + BOOST_LOG_TRIVIAL(info) << Sat.id() << " failed broadcast predictions.\n"; + + continue; + } + + int iodeClk = obs.iodeClk; + int iodePos = obs.iodePos; + // int iodcrc = ? + Vector3d brdcPos = obs.rSatApc; + Vector3d brdcVel = obs.satVel; + double brdcClkVal = obs.satClk * CLIGHT; + + // output the valid entries + { + DBEntry entry; + entry.stringMap[SSR_DATA] = {SSR_CLOCK, true}; + entry.stringMap[SSR_SAT] = {Sat.id(), true}; + entry.timeMap[SSR_EPOCH] = {pTime, true}; + + entry.doubleMap[SSR_CLOCK SSR_BRDC] = {brdcClkVal, false}; + entry.doubleMap[SSR_CLOCK SSR_PREC] = {precClkVal, false}; + entry.timeMap[SSR_UPDATED] = {time, false}; + entry.intMap[SSR_IODE] = {iodeClk, false}; + + dbEntryList.push_back(entry); + } + + { + DBEntry entry; + entry.stringMap[SSR_DATA] = {SSR_EPHEMERIS, true}; + entry.stringMap[SSR_SAT] = {Sat.id(), true}; + entry.timeMap[SSR_EPOCH] = {pTime, true}; + + entry.vectorMap[SSR_POS SSR_BRDC] = {brdcPos, false}; + entry.vectorMap[SSR_VEL SSR_BRDC] = {brdcVel, false}; + entry.vectorMap[SSR_POS SSR_PREC] = {precPos, false}; + entry.vectorMap[SSR_VEL SSR_PREC] = {precVel, false}; + entry.doubleMap[SSR_VAR] = {posVar, false}; + entry.timeMap[SSR_UPDATED] = {time, false}; + entry.intMap[SSR_IODE] = {iodePos, false}; + + dbEntryList.push_back(entry); + } + } + + E_Source source = acsConfig.ssrOpts.code_bias_sources.front(); + switch (source) + { + case E_Source::PRECISE: + { + for (auto& obsCode : acsConfig.code_priorities[Sat.sys]) + { + double bias = 0; + double bvar = 0; + bool pass = getBias(trace, time, Sat.id(), Sat, obsCode, CODE, bias, bvar); + if (pass == false) + { + continue; + } + + DBEntry entry; + entry.stringMap[SSR_DATA] = {SSR_CODE_BIAS, true}; + entry.stringMap[SSR_SAT] = {Sat.id(), true}; + entry.timeMap[SSR_EPOCH] = {time, true}; + entry.stringMap[SSR_OBSCODE] = {enum_to_string(obsCode), true}; + + entry.doubleMap[SSR_BIAS] = {bias, false}; + entry.doubleMap[SSR_VAR] = {bvar, false}; + entry.timeMap[SSR_UPDATED] = {time, false}; + + dbEntryList.push_back(entry); + } + + break; + } + + case E_Source::SSR: + { + auto it = satNav.receivedSSR.ssrCodeBias_map.upper_bound(time); + if (it == satNav.receivedSSR.ssrCodeBias_map.end()) + { + break; + } + + auto& [dummy, ssrCodeBias] = *it; + for (auto& [obsCode, biasVar] : ssrCodeBias.obsCodeBiasMap) + { + DBEntry entry; + entry.stringMap[SSR_DATA] = {SSR_CODE_BIAS, true}; + entry.stringMap[SSR_SAT] = {Sat.id(), true}; + entry.timeMap[SSR_EPOCH] = {ssrCodeBias.t0, true}; + entry.stringMap[SSR_OBSCODE] = {enum_to_string(obsCode), true}; + + entry.doubleMap[SSR_BIAS] = {biasVar.bias, false}; + entry.doubleMap[SSR_VAR] = {biasVar.var, false}; + entry.timeMap[SSR_UPDATED] = {time, false}; + + dbEntryList.push_back(entry); + } + + break; + } + + case E_Source::KALMAN: + { + for (auto& obsCode : acsConfig.code_priorities[Sat.sys]) + { + double bias = 0; + double bvar = 0; + + bool pass = queryBiasOutput( + trace, + time, + kfState, + ionState, + Sat, + "", + obsCode, + bias, + bvar, + CODE + ); + if (pass == false) + { + continue; + } + + DBEntry entry; + entry.stringMap[SSR_DATA] = {SSR_CODE_BIAS, true}; + entry.stringMap[SSR_SAT] = {Sat.id(), true}; + entry.timeMap[SSR_EPOCH] = {time, true}; + entry.stringMap[SSR_OBSCODE] = {enum_to_string(obsCode), true}; + + entry.doubleMap[SSR_BIAS] = {bias, false}; + entry.doubleMap[SSR_VAR] = {bvar, false}; + entry.timeMap[SSR_UPDATED] = {time, false}; + + dbEntryList.push_back(entry); + } + + break; + } + } + + source = acsConfig.ssrOpts.phase_bias_sources.front(); + switch (source) + { + case E_Source::PRECISE: + { + for (auto& obsCode : acsConfig.code_priorities[Sat.sys]) + { + double bias = 0; + double bvar = 0; + bool pass = getBias(trace, time, Sat.id(), Sat, obsCode, PHAS, bias, bvar); + if (pass == false) + { + continue; + } + + DBEntry entry; + entry.stringMap[SSR_DATA] = {SSR_PHAS_BIAS, true}; + entry.stringMap[SSR_SAT] = {Sat.id(), true}; + entry.timeMap[SSR_EPOCH] = {time, true}; + entry.stringMap[SSR_OBSCODE] = {enum_to_string(obsCode), true}; + + entry.doubleMap[SSR_BIAS] = {bias, false}; + entry.doubleMap[SSR_VAR] = {bvar, false}; + entry.timeMap[SSR_UPDATED] = {time, false}; + + entry.intMap["dispBiasConistInd"] = {0, false}; + entry.intMap["MWConistInd"] = {1, false}; + entry.doubleMap["yawAngle"] = { + 0, + false + }; /* To Do: reflect internal yaw calculation here */ + entry.doubleMap["yawRate"] = { + 0, + false + }; /* To Do: reflect internal yaw calculation here */ + entry.intMap["signalIntInd"] = {1, false}; + entry.intMap["signalWLIntInd"] = {2, false}; + entry.intMap["signalDisconCnt"] = {0, false}; + + dbEntryList.push_back(entry); + } + + break; + } + + case E_Source::SSR: + { + auto it = satNav.receivedSSR.ssrPhasBias_map.upper_bound(time); + if (it == satNav.receivedSSR.ssrPhasBias_map.end()) + { + break; + } + auto& [dummy, ssrPhasBias] = *it; + + for (auto& [obsCode, biasVar] : ssrPhasBias.obsCodeBiasMap) + { + DBEntry entry; + entry.stringMap[SSR_DATA] = {SSR_PHAS_BIAS, true}; + entry.stringMap[SSR_SAT] = {Sat.id(), true}; + entry.timeMap[SSR_EPOCH] = {ssrPhasBias.t0, true}; + entry.stringMap[SSR_OBSCODE] = {enum_to_string(obsCode), true}; + + entry.doubleMap[SSR_BIAS] = {biasVar.bias, false}; + entry.doubleMap[SSR_VAR] = {biasVar.var, false}; + entry.timeMap[SSR_UPDATED] = {time, false}; + + auto& ssrPhase = ssrPhasBias.ssrPhase; + auto& ssrPhaseChs = ssrPhasBias.ssrPhaseChs; + entry.intMap["dispBiasConistInd"] = {ssrPhase.dispBiasConistInd, false}; + entry.intMap["MWConistInd"] = {ssrPhase.MWConistInd, false}; + entry.doubleMap["yawAngle"] = {ssrPhase.yawAngle, false}; + entry.doubleMap["yawRate"] = {ssrPhase.yawRate, false}; + entry.intMap["signalIntInd"] = {ssrPhaseChs[obsCode].signalIntInd, false}; + entry.intMap["signalWLIntInd"] = { + ssrPhaseChs[obsCode].signalWLIntInd, + false + }; + entry.intMap["signalDisconCnt"] = { + ssrPhaseChs[obsCode].signalDisconCnt, + false + }; + + dbEntryList.push_back(entry); + } + + break; + } + + case E_Source::KALMAN: + { + double bias; + double bvar; + + for (auto& obsCode : acsConfig.code_priorities[sys]) + if (queryBiasOutput( + trace, + time, + kfState, + ionState, + Sat, + "", + obsCode, + bias, + bvar, + PHAS + )) + { + DBEntry entry; + entry.stringMap[SSR_DATA] = {SSR_PHAS_BIAS, true}; + entry.stringMap[SSR_SAT] = {Sat.id(), true}; + entry.timeMap[SSR_EPOCH] = {time, true}; + entry.stringMap[SSR_OBSCODE] = {enum_to_string(obsCode), true}; + + entry.doubleMap[SSR_BIAS] = {bias, false}; + entry.doubleMap[SSR_VAR] = {bvar, false}; + entry.timeMap[SSR_UPDATED] = {time, false}; + + entry.intMap["dispBiasConistInd"] = {0, false}; + entry.intMap["MWConistInd"] = {1, false}; + entry.doubleMap["yawAngle"] = { + 0, + false + }; /* To Do: reflect internal yaw calculation here */ + entry.doubleMap["yawRate"] = { + 0, + false + }; /* To Do: reflect internal yaw calculation here */ + entry.intMap["signalIntInd"] = {1, false}; + entry.intMap["signalWLIntInd"] = {2, false}; + entry.intMap["signalDisconCnt"] = {0, false}; + + dbEntryList.push_back(entry); + } + + break; + } + } + } + } + + E_Source atmSource = acsConfig.ssrOpts.atmosphere_sources.front(); + switch (atmSource) + { + case E_Source::SSR: + case E_Source::KALMAN: + { + // IGS SSR Ionosphere + bool igsIonoDetected = false; + auto it = nav.ssrAtm.atmosGlobalMap.lower_bound(time); + if (it != nav.ssrAtm.atmosGlobalMap.end()) + { + auto& [t0, atmGlob] = *it; + + double dTime = fabs((atmGlob.time - time).to_double()); + + if (dTime < acsConfig.ssrInOpts.global_vtec_valid_time && + atmGlob.layers.empty() == false) + { + igsIonoDetected = true; + } + } + + if (igsIonoDetected) + { + auto it = nav.ssrAtm.atmosGlobalMap.lower_bound(time); + auto& [t0, atmGlob] = *it; + int nbasis = 0; + for (auto& [hind, laydata] : atmGlob.layers) + for (auto& [bind, basdata] : laydata.sphHarmonic) + { + DBEntry entry; + entry.stringMap[SSR_DATA] = {IGS_ION_ENTRY, true}; + entry.timeMap[SSR_EPOCH] = {atmGlob.time, true}; + entry.intMap[SSR_ION_IND] = std::make_tuple(nbasis++, true); + + entry.intMap[IGS_ION_HGT] = std::make_tuple(basdata.layer, false); + entry.intMap[IGS_ION_DEG] = std::make_tuple(basdata.degree, false); + entry.intMap[IGS_ION_ORD] = std::make_tuple(basdata.order, false); + entry.intMap[IGS_ION_PAR] = + std::make_tuple(static_cast(basdata.trigType), false); + entry.doubleMap[IGS_ION_VAL] = std::make_tuple(basdata.value, false); + + dbEntryList.push_back(entry); + } + + { + DBEntry entry; + entry.stringMap[SSR_DATA] = {IGS_ION_META, true}; + entry.timeMap[SSR_EPOCH] = {atmGlob.time, true}; + + entry.intMap[IGS_ION_NLAY] = {atmGlob.numberLayers, false}; + entry.intMap[IGS_ION_NBAS] = {nbasis, false}; + entry.doubleMap[IGS_ION_QLTY] = {atmGlob.vtecQuality, false}; + for (auto& [layer, laydata] : atmGlob.layers) + { + string hghStr = "Height_" + std::to_string(layer); + entry.doubleMap[hghStr] = {laydata.height, false}; + } + dbEntryList.push_back(entry); + } + } + + // CMP SSR Atmosphere + for (auto& [regInd, regData] : nav.ssrAtm.atmosRegionsMap) + { + GTime stecTime = regData.stecUpdateTime; + map stecFound; + int sInd = 0; + for (auto& [sat, satData] : regData.stecData) + { + if (satData.find(stecTime) == satData.end()) + continue; + + stecFound[sInd] = sat; + sInd++; + } + + if (stecFound.empty()) + continue; + + double dTime = fabs((stecTime - time).to_double()); + if (dTime > acsConfig.ssrInOpts.local_stec_valid_time) + continue; + + // Region Metadata + { + DBEntry regMT; + regMT.stringMap[SSR_DATA] = {CMP_ATM_META, true}; + regMT.intMap["RegionID"] = {regInd, true}; + + regMT.intMap["RegionIOD"] = {regData.regionDefIOD, false}; + regMT.doubleMap["minLat"] = {regData.minLatDeg, false}; + regMT.doubleMap["maxLat"] = {regData.maxLatDeg, false}; + regMT.doubleMap["intLat"] = {regData.intLatDeg, false}; + regMT.doubleMap["minLon"] = {regData.minLonDeg, false}; + regMT.doubleMap["maxLon"] = {regData.maxLonDeg, false}; + regMT.doubleMap["intLon"] = {regData.intLonDeg, false}; + + regMT.intMap["gridType"] = {regData.gridType, false}; + regMT.intMap["tropPoly"] = {regData.tropPolySize, false}; + regMT.intMap["ionoPoly"] = {regData.ionoPolySize, false}; + regMT.intMap["tropGrid"] = {regData.tropGrid ? 1 : 0, false}; + regMT.intMap["ionoGrid"] = {regData.ionoGrid ? 1 : 0, false}; + + regMT.doubleMap["grdLat_0"] = {regData.gridLatDeg[0], false}; + regMT.doubleMap["grdLon_0"] = {regData.gridLonDeg[0], false}; + + if (regData.gridType == 0) + { + regMT.intMap["gridNum"] = {regData.gridLatDeg.size(), false}; + string keyStr; + for (int i = 1; i < regData.gridLatDeg.size(); i++) + { + keyStr = "grdLat_" + std::to_string(i); + regMT.doubleMap[keyStr] = {regData.gridLatDeg[i], false}; + + keyStr = "grdLon_" + std::to_string(i); + regMT.doubleMap[keyStr] = {regData.gridLonDeg[i], false}; + } + } + dbEntryList.push_back(regMT); + } + + // Troposphere entry + { + auto trop_ptr = regData.tropData.begin(); + if (trop_ptr != regData.tropData.end()) + { + GTime trpTime = trop_ptr->first; + auto& trpData = trop_ptr->second; + + DBEntry trpDT; + trpDT.stringMap[SSR_DATA] = {CMP_TRP_ENTRY, true}; + trpDT.intMap["RegionID"] = {regInd, true}; + trpDT.timeMap[SSR_EPOCH] = {trpTime, true}; + + trpDT.doubleMap["trpAcc"] = {trpData.sigma, false}; + for (int i = 0; i < regData.ionoPolySize; i++) + { + string keyStr = "tropPoly_" + std::to_string(i); + double polyVal = -9999; + if (trpData.polyDry.find(i) == trpData.polyDry.end()) + polyVal = trpData.polyDry[i]; + trpDT.doubleMap[keyStr] = {polyVal, false}; + } + if (regData.tropGrid) + for (auto& [ind, lat] : regData.gridLatDeg) + { + string keyStr = "tropDry_" + std::to_string(ind); + double tropDry = -9999; + if (trpData.gridDry.find(ind) == trpData.gridDry.end()) + tropDry = trpData.gridDry[ind]; + trpDT.doubleMap[keyStr] = {tropDry, false}; + + keyStr = "tropWet_" + std::to_string(ind); + double tropWet = -9999; + if (trpData.gridWet.find(ind) == trpData.gridWet.end()) + tropWet = trpData.gridWet[ind]; + trpDT.doubleMap[keyStr] = {tropWet, false}; + } + } + } + + // Ionosphere entries + { + // Ionosphere Metadata + DBEntry ionMT; + ionMT.stringMap[SSR_DATA] = {CMP_ION_META, true}; + ionMT.intMap["RegionID"] = {regInd, true}; + ionMT.timeMap[SSR_EPOCH] = {stecTime, true}; + + ionMT.intMap["satNumb"] = {stecFound.size(), false}; + for (auto& [satInd, sat] : stecFound) + { + string keyStr = "regSat_" + std::to_string(satInd); + ionMT.stringMap[keyStr] = {sat.id(), false}; + + // Ionosphere Data + DBEntry ionDT; + ionDT.stringMap[SSR_DATA] = {CMP_ION_ENTRY, true}; + ionDT.intMap["RegionID"] = {regInd, true}; + ionDT.stringMap[SSR_SAT] = {sat.id(), true}; + ionDT.timeMap[SSR_EPOCH] = {stecTime, true}; + + auto& stecData = regData.stecData[sat][stecTime]; + ionDT.doubleMap["ionAcc"] = {stecData.sigma, false}; + + for (int i = 0; i < regData.ionoPolySize; i++) + { + string keyStr = "ionoPoly_" + std::to_string(i); + double polyVal = -9999; + if (stecData.poly.find(i) != stecData.poly.end()) + polyVal = stecData.poly[i]; + ionDT.doubleMap[keyStr] = {polyVal, false}; + } + if (regData.ionoGrid) + for (auto& [ind, lat] : regData.gridLatDeg) + { + string keyStr = "ionoGrid_" + std::to_string(ind); + double gridVal = -9999; + if (stecData.grid.find(ind) != stecData.grid.end()) + gridVal = stecData.grid[ind]; + ionDT.doubleMap[keyStr] = {gridVal, false}; + } + dbEntryList.push_back(ionDT); + } + dbEntryList.push_back(ionMT); + } + } + } + } + + mongoOutput(dbEntryList, acsConfig.mongoOpts.queue_outputs, instances, SSR_DB); } -document entryToDocument( - DBEntry& entry, - bool type) +/// add editing infomation to mongo +/// @todo slow version, need to do some bulk writting. +void mongoEditing( + const std::string& sat, + const std::string& site, + const GTime& time, + const std::string& type, + const std::string& signal, + const int& values, + const std::string& message +) { - // builder::document builds an empty BSON document - document doc = {}; - - for (auto&[k,v]:entry.stringMap) {auto& [e,b] = v; if (type == b) doc << k << e;} - for (auto&[k,v]:entry.intMap) {auto& [e,b] = v; if (type == b) doc << k << e;} - for (auto&[k,v]:entry.doubleMap) {auto& [e,b] = v; if (type == b) doc << k << e;} - for (auto&[k,v]:entry.timeMap) {auto& [e,b] = v; if (type == b) doc << k << b_date{std::chrono::system_clock::from_time_t((time_t)((PTime)e).bigTime)};} - for (auto&[k,v]:entry.vectorMap) {auto& [e,b] = v; if (type == b) for (int i=0;i<3;i++) doc << k + std::to_string(i) << e[i];} - for (auto&[k,v]:entry.doubleArrayMap) {auto& [e,b] = v; if (type == b) { auto builder = doc << k << open_array; for (auto& a : e) builder << a; builder << close_array; }} - for (auto&[k,v]:entry.boolArrayMap) {auto& [e,b] = v; if (type == b) { auto builder = doc << k << open_array; for (auto& a : e) builder << a; builder << close_array; }} - - return doc; -} + auto instances = mongoInstances(acsConfig.mongoOpts.output_editing); + if (instances.empty()) + { + return; + } -void mongoOutput( - vector& dbEntryList, - bool queue, - vector instances, - string collection) -{ - if (queue) - { - QueuedMongo queueEntry; - queueEntry.dbEntryList = dbEntryList; - queueEntry.instances = instances; - queueEntry.suffix = collection; - queueEntry.mongoType = E_MongoType::LIST; + for (auto instance : instances) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - queueMongo(queueEntry); + if (mongo_ptr == nullptr) + continue; - return; - } + auto& mongo = *mongo_ptr; - for (auto instance : instances) - { - Mongo* mongo_ptr = mongo_ptr_arr[instance]; + auto& config = acsConfig.mongoOpts[static_cast(instance)]; - if (mongo_ptr == nullptr) - continue; + getMongoCollection(mongo, toString(Constants::Mongo::EDITING_DB)); - auto& mongo = *mongo_ptr; + bsoncxx::builder::stream::document doc{}; + doc << "Sat" << sat << "Site" << site << "Epoch" << bDate(time) << "Type" << type + << "Signal" << signal << "Values" << values; - auto c = mongo.pool.acquire(); - mongocxx::client& client = *c; - mongocxx::database db = client[mongo.database]; - mongocxx::collection coll = db[collection]; + if (!message.empty()) + { + doc << "message" << message; + } - mongocxx::options::bulk_write bulk_opts; - bulk_opts.ordered(false); + bsoncxx::document::value doc_val = doc << finalize; - auto bulk = coll.create_bulk_write(bulk_opts); + coll.insert_one(doc_val.view()); + } +} - bool update = false; +void mongoEditing( + const std::string& sat, + const std::string& site, + const GTime& time, + const std::string& type, + const std::string& signal, + const double& values, + const std::string& message +) +{ + auto instances = mongoInstances(acsConfig.mongoOpts.output_editing); - for (auto& entry : dbEntryList) - { - // builder::document builds an empty BSON document - auto keys = entryToDocument(entry, true); - auto vals = entryToDocument(entry, false); + if (instances.empty()) + { + return; + } - bsoncxx::builder::basic::document Vals = {}; + for (auto instance : instances) + { + auto mongo_ptr = mongo_ptr_arr[static_cast(instance)]; - Vals.append(kvp("$set", vals)); + if (mongo_ptr == nullptr) + continue; - // std::cout << "\n" << bsoncxx::to_json(keys.view()) << bsoncxx::to_json(Vals.view()) << "\n"; + auto& mongo = *mongo_ptr; - mongocxx::model::update_one mongo_req{keys.view(), Vals.view()}; + auto& config = acsConfig.mongoOpts[static_cast(instance)]; - update = true; - mongo_req.upsert(true); - bulk.append(mongo_req); - } + getMongoCollection(mongo, toString(Constants::Mongo::EDITING_DB)); - if (update) - bulk.execute(); - } -} + bsoncxx::builder::stream::document doc{}; + doc << "Sat" << sat << "Site" << site << "Epoch" << bDate(time) << "Type" << type + << "Signal" << signal << "Values" << values; -/** Write states generating SSR corrections to Mongo DB -*/ -void prepareSsrStates( - Trace& trace, ///< Trace to output to - KFState& kfState, ///< Filter object to extract state elements from - KFState& ionState, ///< Filter object to extract state elements from - GTime time) ///< Time of current epoch -{ - auto instances = mongoInstances(acsConfig.mongoOpts.output_ssr_precursors); - - if (instances.empty()) - { - return; - } + if (!message.empty()) + { + doc << "message" << message; + } - BOOST_LOG_TRIVIAL(info) - << "Calculating SSR message precursors\n"; - - vector dbEntryList; - - time.bigTime = (long int) (time.bigTime + 0.5); // time tags in mongo will be rounded up to whole sec - - for (E_Sys sys : E_Sys::_values()) - { - string sysName = boost::algorithm::to_lower_copy((string) sys._to_string()); - - if (acsConfig.process_sys[sys] == false) - continue; - - auto sats = getSysSats(sys); - - // Calculate orbits + clocks at tsync & tsync+udi - for (auto& Sat : sats) - { - // Create a dummy observation - GObs obs; - obs.Sat = Sat; - - auto& satNav = nav.satNavMap[obs.Sat]; - SatStat satStatDummy; - - obs.satStat_ptr = &satStatDummy; - obs.satNav_ptr = &satNav; - - GTime teph = time; - - // Clock and orbit corrections (and predicted corrections) - for (int tpredict = 0; tpredict <= acsConfig.ssrOpts.prediction_duration; tpredict += acsConfig.ssrOpts.prediction_interval) - { - GTime pTime = time + tpredict; - bool pass = true; - - pass = true; - pass &= satclk(nullStream, pTime, pTime, obs, acsConfig.ssrOpts.clock_sources, nav, &kfState); - pass &= satpos(nullStream, pTime, pTime, obs, acsConfig.ssrOpts.ephemeris_sources, E_OffsetType::APC, nav, &kfState); //todo aaron, ssra streams expect common_sat_pco to be true - if (obs.satClk == INVALID_CLOCK_VALUE) - { - pass = false; - } - - //precise clock - double precClkVal = obs.satClk * CLIGHT; - - if (pass == false) - { - BOOST_LOG_TRIVIAL(info) << Sat.id() << " failed ssrprecise prediction.\n"; - - continue; - } - - if (obs.iodeClk != obs.iodePos) - { - BOOST_LOG_TRIVIAL(info) - << Sat.id() << " IODE clock" << obs.iodeClk << " mismatch IODE orbit " << obs.iodePos << ".\n"; - - continue; - } - - Vector3d precPos = obs.rSatApc; - Vector3d precVel = obs.satVel; - double posVar = obs.posVar; - - //broadcast values, must come after precise values to associate the correct IODE (when SSR in use) - pass &= satClkBroadcast(nullStream, pTime, pTime, obs, nav, obs.iodeClk); - pass &= satPosBroadcast(nullStream, pTime, pTime, obs, nav, obs.iodePos); - if (pass == false) - { - BOOST_LOG_TRIVIAL(info) - << Sat.id() << " failed broadcast predictions.\n"; - - continue; - } - - int iodeClk = obs.iodeClk; - int iodePos = obs.iodePos; -// int iodcrc = ? - Vector3d brdcPos = obs.rSatApc; - Vector3d brdcVel = obs.satVel; - double brdcClkVal = obs.satClk * CLIGHT; - - //output the valid entries - { - DBEntry entry; - entry.stringMap [SSR_DATA ] = {SSR_CLOCK, true}; - entry.stringMap [SSR_SAT ] = {Sat.id(), true}; - entry.timeMap [SSR_EPOCH ] = {pTime, true}; - - entry.doubleMap [SSR_CLOCK SSR_BRDC ] = {brdcClkVal, false}; - entry.doubleMap [SSR_CLOCK SSR_PREC ] = {precClkVal, false}; - entry.timeMap [SSR_UPDATED ] = {time, false}; - entry.intMap [SSR_IODE ] = {iodeClk, false}; - - dbEntryList.push_back(entry); - } - - { - DBEntry entry; - entry.stringMap [SSR_DATA ] = {SSR_EPHEMERIS, true}; - entry.stringMap [SSR_SAT ] = {Sat.id(), true}; - entry.timeMap [SSR_EPOCH ] = {pTime, true}; - - entry.vectorMap [SSR_POS SSR_BRDC ] = {brdcPos, false}; - entry.vectorMap [SSR_VEL SSR_BRDC ] = {brdcVel, false}; - entry.vectorMap [SSR_POS SSR_PREC ] = {precPos, false}; - entry.vectorMap [SSR_VEL SSR_PREC ] = {precVel, false}; - entry.doubleMap [SSR_VAR ] = {posVar, false}; - entry.timeMap [SSR_UPDATED ] = {time, false}; - entry.intMap [SSR_IODE ] = {iodePos, false}; - - dbEntryList.push_back(entry); - } - } - - E_Source source = acsConfig.ssrOpts.code_bias_sources.front(); - switch (source) - { - case E_Source::PRECISE: - { - for (auto& obsCode : acsConfig.code_priorities[Sat.sys]) - { - double bias = 0; - double bvar = 0; - bool pass = getBias(trace, time, Sat.id(), Sat, obsCode, CODE, bias, bvar); - if (pass == false) - { - continue; - } - - DBEntry entry; - entry.stringMap [SSR_DATA ] = {SSR_CODE_BIAS, true}; - entry.stringMap [SSR_SAT ] = {Sat.id(), true}; - entry.timeMap [SSR_EPOCH ] = {time, true}; - entry.stringMap [SSR_OBSCODE ] = {obsCode._to_string(), true}; - - entry.doubleMap [SSR_BIAS ] = {bias, false}; - entry.doubleMap [SSR_VAR ] = {bvar, false}; - entry.timeMap [SSR_UPDATED ] = {time, false}; - - dbEntryList.push_back(entry); - } - - break; - } - - case E_Source::SSR: - { - auto it = satNav.receivedSSR.ssrCodeBias_map.upper_bound(time); - if (it == satNav.receivedSSR.ssrCodeBias_map.end()) - { - break; - } - - auto& [dummy, ssrCodeBias] = *it; - for (auto& [obsCode, biasVar] : ssrCodeBias.obsCodeBiasMap) - { - DBEntry entry; - entry.stringMap [SSR_DATA ] = {SSR_CODE_BIAS, true}; - entry.stringMap [SSR_SAT ] = {Sat.id(), true}; - entry.timeMap [SSR_EPOCH ] = {ssrCodeBias.t0, true}; - entry.stringMap [SSR_OBSCODE ] = {obsCode._to_string(), true}; - - entry.doubleMap [SSR_BIAS ] = {biasVar.bias, false}; - entry.doubleMap [SSR_VAR ] = {biasVar.var, false}; - entry.timeMap [SSR_UPDATED ] = {time, false}; - - dbEntryList.push_back(entry); - } - - break; - } - - case E_Source::KALMAN: - { - for (auto& obsCode : acsConfig.code_priorities[Sat.sys]) - { - double bias = 0; - double bvar = 0; - - bool pass = queryBiasOutput(trace, time, kfState, ionState, Sat,"", obsCode, bias, bvar, CODE); - if (pass == false) - { - continue; - } - - DBEntry entry; - entry.stringMap [SSR_DATA ] = {SSR_CODE_BIAS, true}; - entry.stringMap [SSR_SAT ] = {Sat.id(), true}; - entry.timeMap [SSR_EPOCH ] = {time, true}; - entry.stringMap [SSR_OBSCODE ] = {obsCode._to_string(), true}; - - entry.doubleMap [SSR_BIAS ] = {bias, false}; - entry.doubleMap [SSR_VAR ] = {bvar, false}; - entry.timeMap [SSR_UPDATED ] = {time, false}; - - dbEntryList.push_back(entry); - } - - break; - } - } - - source = acsConfig.ssrOpts.phase_bias_sources.front(); - switch (source) - { - case E_Source::PRECISE: - { - for (auto& obsCode : acsConfig.code_priorities[Sat.sys]) - { - double bias = 0; - double bvar = 0; - bool pass = getBias(trace, time, Sat.id(), Sat, obsCode, PHAS, bias, bvar); - if (pass == false) - { - continue; - } - - DBEntry entry; - entry.stringMap [SSR_DATA ] = {SSR_PHAS_BIAS, true}; - entry.stringMap [SSR_SAT ] = {Sat.id(), true}; - entry.timeMap [SSR_EPOCH ] = {time, true}; - entry.stringMap [SSR_OBSCODE ] = {obsCode._to_string(), true}; - - entry.doubleMap [SSR_BIAS ] = {bias, false}; - entry.doubleMap [SSR_VAR ] = {bvar, false}; - entry.timeMap [SSR_UPDATED ] = {time, false}; - - entry.intMap ["dispBiasConistInd"] = {0, false}; - entry.intMap ["MWConistInd" ] = {1, false}; - entry.doubleMap ["yawAngle" ] = {0, false}; /* To Do: reflect internal yaw calculation here */ - entry.doubleMap ["yawRate" ] = {0, false}; /* To Do: reflect internal yaw calculation here */ - entry.intMap ["signalIntInd" ] = {1, false}; - entry.intMap ["signalWLIntInd" ] = {2, false}; - entry.intMap ["signalDisconCnt" ] = {0, false}; - - dbEntryList.push_back(entry); - } - - break; - } - - case E_Source::SSR: - { - auto it = satNav.receivedSSR.ssrPhasBias_map.upper_bound(time); - if (it == satNav.receivedSSR.ssrPhasBias_map.end()) - { - break; - } - auto& [dummy, ssrPhasBias] = *it; - - for (auto& [obsCode, biasVar] : ssrPhasBias.obsCodeBiasMap) - { - DBEntry entry; - entry.stringMap [SSR_DATA ] = {SSR_PHAS_BIAS, true}; - entry.stringMap [SSR_SAT ] = {Sat.id(), true}; - entry.timeMap [SSR_EPOCH ] = {ssrPhasBias.t0, true}; - entry.stringMap [SSR_OBSCODE ] = {obsCode._to_string(), true}; - - entry.doubleMap [SSR_BIAS ] = {biasVar.bias, false}; - entry.doubleMap [SSR_VAR ] = {biasVar.var, false}; - entry.timeMap [SSR_UPDATED ] = {time, false}; - - auto& ssrPhase = ssrPhasBias.ssrPhase; - auto& ssrPhaseChs = ssrPhasBias.ssrPhaseChs; - entry.intMap ["dispBiasConistInd"] = {ssrPhase.dispBiasConistInd, false}; - entry.intMap ["MWConistInd" ] = {ssrPhase.MWConistInd, false}; - entry.doubleMap ["yawAngle" ] = {ssrPhase.yawAngle, false}; - entry.doubleMap ["yawRate" ] = {ssrPhase.yawRate, false}; - entry.intMap ["signalIntInd" ] = {ssrPhaseChs[obsCode].signalIntInd, false}; - entry.intMap ["signalWLIntInd" ] = {ssrPhaseChs[obsCode].signalWLIntInd, false}; - entry.intMap ["signalDisconCnt" ] = {ssrPhaseChs[obsCode].signalDisconCnt, false}; - - dbEntryList.push_back(entry); - } - - break; - } - - case E_Source::KALMAN: - { - double bias; - double bvar; - - for (auto& obsCode : acsConfig.code_priorities[sys]) - if (queryBiasOutput(trace, time, kfState, ionState, Sat, "", obsCode, bias, bvar, PHAS)) - { - DBEntry entry; - entry.stringMap [SSR_DATA ] = {SSR_PHAS_BIAS, true}; - entry.stringMap [SSR_SAT ] = {Sat.id(), true}; - entry.timeMap [SSR_EPOCH ] = {time, true}; - entry.stringMap [SSR_OBSCODE ] = {obsCode._to_string(), true}; - - entry.doubleMap [SSR_BIAS ] = {bias, false}; - entry.doubleMap [SSR_VAR ] = {bvar, false}; - entry.timeMap [SSR_UPDATED ] = {time, false}; - - entry.intMap ["dispBiasConistInd"] = {0, false}; - entry.intMap ["MWConistInd" ] = {1, false}; - entry.doubleMap ["yawAngle" ] = {0, false}; /* To Do: reflect internal yaw calculation here */ - entry.doubleMap ["yawRate" ] = {0, false}; /* To Do: reflect internal yaw calculation here */ - entry.intMap ["signalIntInd" ] = {1, false}; - entry.intMap ["signalWLIntInd" ] = {2, false}; - entry.intMap ["signalDisconCnt" ] = {0, false}; - - dbEntryList.push_back(entry); - } - - break; - } - } - } - } - - E_Source atmSource = acsConfig.ssrOpts.atmosphere_sources.front(); - switch (atmSource) - { - case E_Source::SSR: - case E_Source::KALMAN: - { - // IGS SSR Ionosphere - bool igsIonoDetected = false; - auto it = nav.ssrAtm.atmosGlobalMap.lower_bound(time); - if (it != nav.ssrAtm.atmosGlobalMap.end()) - { - auto& [t0, atmGlob] = *it; - - double dTime = fabs((atmGlob.time - time).to_double()); - - if (dTime < acsConfig.ssrInOpts.global_vtec_valid_time - && atmGlob.layers.empty() == false) - { - igsIonoDetected = true; - } - } - - if (igsIonoDetected) - { - auto it = nav.ssrAtm.atmosGlobalMap.lower_bound(time); - auto& [t0, atmGlob] = *it; - int nbasis = 0; - for (auto& [hind, laydata]: atmGlob.layers) - for (auto& [bind, basdata]: laydata.sphHarmonic) - { - DBEntry entry; - entry.stringMap [SSR_DATA ] = {IGS_ION_ENTRY, true}; - entry.timeMap [SSR_EPOCH ] = {atmGlob.time, true}; - entry.intMap [SSR_ION_IND ] = {nbasis++, true}; - - entry.intMap [IGS_ION_HGT ] = {basdata.layer, false}; - entry.intMap [IGS_ION_DEG ] = {basdata.degree, false}; - entry.intMap [IGS_ION_ORD ] = {basdata.order, false}; - entry.intMap [IGS_ION_PAR ] = {basdata.trigType, false}; - entry.doubleMap [IGS_ION_VAL ] = {basdata.value, false}; - - dbEntryList.push_back(entry); - } - - { - DBEntry entry; - entry.stringMap [SSR_DATA ] = {IGS_ION_META, true}; - entry.timeMap [SSR_EPOCH ] = {atmGlob.time, true}; - - entry.intMap [IGS_ION_NLAY ] = {atmGlob.numberLayers, false}; - entry.intMap [IGS_ION_NBAS ] = {nbasis, false}; - entry.doubleMap [IGS_ION_QLTY ] = {atmGlob.vtecQuality, false}; - for (auto& [layer, laydata]: atmGlob.layers) - { - string hghStr = "Height_"+std::to_string(layer); - entry.doubleMap [hghStr ] = {laydata.height, false}; - } - dbEntryList.push_back(entry); - } - } - - // CMP SSR Atmosphere - for (auto& [regInd, regData] : nav.ssrAtm.atmosRegionsMap) - { - GTime stecTime = regData.stecUpdateTime; - map stecFound; - int sInd = 0; - for (auto& [sat,satData] : regData.stecData) - { - if (satData.find(stecTime) == satData.end()) - continue; - - stecFound[sInd] = sat; - sInd++; - } - - if (stecFound.empty()) - continue; - - double dTime = fabs((stecTime - time).to_double()); - if (dTime > acsConfig.ssrInOpts.local_stec_valid_time) - continue; - - // Region Metadata - { - DBEntry regMT; - regMT.stringMap [SSR_DATA ] = {CMP_ATM_META, true}; - regMT.intMap ["RegionID" ] = {regInd, true}; - - regMT.intMap ["RegionIOD" ] = {regData.regionDefIOD, false}; - regMT.doubleMap ["minLat" ] = {regData.minLatDeg, false}; - regMT.doubleMap ["maxLat" ] = {regData.maxLatDeg, false}; - regMT.doubleMap ["intLat" ] = {regData.intLatDeg, false}; - regMT.doubleMap ["minLon" ] = {regData.minLonDeg, false}; - regMT.doubleMap ["maxLon" ] = {regData.maxLonDeg, false}; - regMT.doubleMap ["intLon" ] = {regData.intLonDeg, false}; - - regMT.intMap ["gridType" ] = {regData.gridType, false}; - regMT.intMap ["tropPoly" ] = {regData.tropPolySize, false}; - regMT.intMap ["ionoPoly" ] = {regData.ionoPolySize, false}; - regMT.intMap ["tropGrid" ] = {regData.tropGrid ? 1 : 0, false}; - regMT.intMap ["ionoGrid" ] = {regData.ionoGrid ? 1 : 0, false}; - - regMT.doubleMap ["grdLat_0" ] = {regData.gridLatDeg[0], false}; - regMT.doubleMap ["grdLon_0" ] = {regData.gridLonDeg[0], false}; - - if (regData.gridType==0) - { - regMT.intMap ["gridNum" ] = {regData.gridLatDeg.size(), false}; - string keyStr; - for (int i = 1; i < regData.gridLatDeg.size(); i++) - { - keyStr = "grdLat_" + std::to_string(i); - regMT.doubleMap [keyStr] = {regData.gridLatDeg[i], false}; - - keyStr = "grdLon_" + std::to_string(i); - regMT.doubleMap [keyStr] = {regData.gridLonDeg[i], false}; - } - } - dbEntryList.push_back(regMT); - } - - // Troposphere entry - { - auto trop_ptr = regData.tropData.begin(); - if (trop_ptr != regData.tropData.end()) - { - GTime trpTime = trop_ptr->first; - auto& trpData = trop_ptr->second; - - DBEntry trpDT; - trpDT.stringMap [SSR_DATA ] = {CMP_TRP_ENTRY, true}; - trpDT.intMap ["RegionID" ] = {regInd, true}; - trpDT.timeMap [SSR_EPOCH ] = {trpTime, true}; - - trpDT.doubleMap ["trpAcc" ] = {trpData.sigma, false}; - for (int i=0; i +#include "common/gTime.hpp" +#include "common/mongo.hpp" +#include "common/satSys.hpp" +#include "common/trace.hpp" using std::set; -#include "satSys.hpp" -#include "mongo.hpp" -#include "trace.hpp" -#include "gTime.hpp" - - struct MongoOptions; struct ReceiverMap; struct OrbitState; @@ -19,61 +18,160 @@ struct KFMeas; struct Geph; struct Eph; - struct TestStatistics { - int numMeas = 0; - double sumOfSquaresPre = 0; - double sumOfSquaresPost = 0; - double averageRatioPre = 0; - double averageRatioPost = 0; - double chiSq = 0; - double dof = 0; - double chiSqPerDof = 0; - double qc = 0; + int numMeas = 0; + double sumOfSquaresLsq = 0; + double sumOfSquaresPre = 0; + double sumOfSquaresPost = 0; + double averageRatioLsq = 0; + double averageRatioPre = 0; + double averageRatioPost = 0; + double chiSq = 0; + double dof = 0; + double chiSqPerDof = 0; + double qc = 0; }; void mongoMeasResiduals( - const GTime& time, - KFMeas& kfMeas, - bool queue = false, - string suffix = "", - int beg = 0, - int num = -1); + const GTime& time, + KFMeas& kfMeas, + bool queue = false, + string suffix = "", + int beg = 0, + int num = -1 +); -void mongoTrace( - const vector& jsons, - bool queue = false); +void mongoTrace(const vector& jsons, bool queue = false); -void mongoOutputConfig( - string& config); +void mongoOutputConfig(string& config); struct MongoStatesOptions { - string suffix; - string collection = STATES_DB; - E_Mongo instances; - bool force = false; - bool upsert = false; - bool queue = false; - bool index = true; - GTime updated; + string suffix; + string collection = Constants::Mongo::STATES_DB; + E_Mongo instances; + bool force = false; + bool upsert = false; + bool queue = false; + bool index = true; + GTime updated; }; -void mongoStatesAvailable( - GTime time, - MongoStatesOptions opts = {}); +void mongoStatesAvailable(GTime time, MongoStatesOptions opts = {}); + +void mongoStates(KFState& kfState, MongoStatesOptions opts = {}); + +void mongoMeasSatStat(ReceiverMap& receiverMap); + +void mongoTestStat(KFState& kfState, TestStatistics& statistics); + +void mongoCull(GTime time); -void mongoStates( - KFState& kfState, - MongoStatesOptions opts = {}); +void mongoEditing( + const std::string& sat, + const std::string& site, + const GTime& time, + const std::string& type, + const std::string& signal, + const double& values, + const std::string& message = "" +); -void mongoMeasSatStat( - ReceiverMap& receiverMap); +#else // !ENABLE_MONGODB + +// Stub declarations when MongoDB is disabled +#include +#include +#include "common/gTime.hpp" +#include "common/mongo.hpp" // For mongoooo() and other mongo functions +#include "enums.h" + +struct ReceiverMap; +struct KFState; +struct KFMeas; + +struct TestStatistics +{ + int numMeas = 0; + double sumOfSquaresLsq = 0; + double sumOfSquaresPre = 0; + double sumOfSquaresPost = 0; + double averageRatioLsq = 0; + double averageRatioPre = 0; + double averageRatioPost = 0; + double chiSq = 0; + double dof = 0; + double chiSqPerDof = 0; + double qc = 0; +}; -void mongoTestStat( - KFState& kfState, - TestStatistics& statistics); +struct MongoStatesOptions +{ + std::string suffix; + std::string collection = "States"; + E_Mongo instances; + bool force = false; + bool upsert = false; + bool queue = false; + bool index = true; + GTime updated; +}; + +inline void mongoMeasResiduals( + const GTime& time, + KFMeas& kfMeas, + bool queue = false, + std::string suffix = "", + int beg = 0, + int num = -1 +) +{ +} + +inline void mongoTrace(const std::vector& jsons, bool queue = false) {} + +inline void mongoOutputConfig(std::string& config) {} + +inline void mongoStatesAvailable(GTime time, MongoStatesOptions opts = {}) {} + +inline void mongoStates(KFState& kfState, MongoStatesOptions opts = {}) {} + +inline void mongoMeasSatStat(ReceiverMap& receiverMap) {} + +inline void mongoTestStat(KFState& kfState, TestStatistics& statistics) {} + +inline void mongoCull(GTime time) {} + +inline void mongoEditing( + const std::string& sat, + const std::string& site, + const GTime& time, + const std::string& type, + const std::string& signal, + const int& values, + const std::string& message = "" +) +{ +} + +inline void mongoEditing( + const std::string& sat, + const std::string& site, + const GTime& time, + const std::string& type, + const std::string& signal, + const double& values, + const std::string& message = "" +) +{ +} + +// Stub for prepareSsrStates which is declared in ssr.hpp but defined in mongoWrite.cpp +#include "common/trace.hpp" // For Trace typedef +inline void prepareSsrStates(Trace& trace, KFState& kfState, KFState& ionState, GTime time) +{ + // No-op when MongoDB is disabled +} -void mongoCull( - GTime time); +#endif // ENABLE_MONGODB diff --git a/src/cpp/common/navigation.hpp b/src/cpp/common/navigation.hpp index f12ee7a70..17ea031c9 100644 --- a/src/cpp/common/navigation.hpp +++ b/src/cpp/common/navigation.hpp @@ -1,128 +1,134 @@ - #pragma once #include #include #include #include - -#include #include #include +#include +#include "common/antenna.hpp" +#include "common/attitude.hpp" +#include "common/azElMapData.hpp" +#include "common/enums.h" +#include "common/ephemeris.hpp" +#include "common/erp.hpp" +#include "common/gTime.hpp" +#include "common/orbits.hpp" +#include "common/satSys.hpp" +#include "common/ssr.hpp" +#include "common/trace.hpp" +#include "sbas/sbas.hpp" -using std::string; using std::map; using std::set; - -#include "azElMapData.hpp" -#include "ephemeris.hpp" -#include "attitude.hpp" -#include "antenna.hpp" -#include "orbits.hpp" -#include "satSys.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" -#include "ssr.hpp" -#include "erp.hpp" - -#define MAXDTE 900.0 /* max time difference to ephem time (s) */ +using std::string; struct TECPoint { - double data = 0; ///< TEC grid data (tecu) - double rms = 0; ///< RMS values (tecu) + double data = 0; ///< TEC grid data (tecu) + double rms = 0; ///< RMS values (tecu) }; struct TEC { - /// TEC grid type - GTime time; ///< epoch time (GPST) - int ndata[3]; ///< TEC grid data size {nlat,nlon,nhgt} - double rb; ///< earth radius (km) - double lats[3]; ///< latitude start/interval (deg) - double lons[3]; ///< longitude start/interval (deg) - double hgts[3]; ///< heights start/interval (km) - vector tecPointVector; + /// TEC grid type + GTime time; ///< epoch time (GPST) + int ndata[3]; ///< TEC grid data size {nlat,nlon,nhgt} + double rb; ///< earth radius (km) + double lats[3]; ///< latitude start/interval (deg) + double lons[3]; ///< longitude start/interval (deg) + double hgts[3]; ///< heights start/interval (km) + vector tecPointVector; }; struct SatNav { - map lamMap; + map lamMap; + + SSRMaps receivedSSR; ///< SSR corrections + SBASMaps currentSBAS; ///< current SBAS correction - SSRMaps receivedSSR; ///< SSR corrections + VectorEci aprioriPos; ///< Inertial satellite position at epoch time + double aprioriClk = 0; ///< Apriori clock value at epoch time - VectorEci aprioriPos; ///< Inertial satellite position at epoch time - double aprioriClk = 0; ///< Apriori clock value at epoch time + AttStatus attStatus = {}; ///< Persistent data for attitude model - AttStatus attStatus = {}; ///< Persistent data for attitude model + string id; + string traceFilename; + string jsonTraceFilename; - string id; - string traceFilename; - string jsonTraceFilename; + Vector3d antBoresight = {0, 0, 1}; + Vector3d antAzimuth = {0, 1, 0}; - Vector3d antBoresight = {0,0,1}; - Vector3d antAzimuth = {0,1,0}; + SatPos satPos0; ///< Satellite position when propagated to nominal time - SatPos satPos0; ///< Satellite position when propagated to nominal time + int satelliteErrorEpochs = 0; + int satelliteErrorCount = 0; }; /** navigation data type */ struct Navigation { - //these are interpolated between, dont need greater - map> pephMap; ///< precise ephemeris - map> pclkMap; ///< precise clock - map> attMapMap; ///< attitudes - - map, std::greater>> dragMap; - map, std::greater>> reflectorMap; - map>>>> pcoMap; - map>>>> pcvMap; - - map>>> ephMap; ///< GPS/QZS/GAL/BDS ephemeris - map>>> gephMap; ///< GLONASS ephemeris - map>>> sephMap; ///< SBAS ephemeris - map>>> cephMap; ///< GPS/QZS/BDS CNVX ephemeris - map>>> ionMap; ///< ION messages - map>>> stoMap; ///< STO messages - map>>> eopMap; ///< EOP messages - map> tecMap; ///< tec grid data - map>> svnMap; ///< Sat SVNs - - map blocktypeMap; ///< svn blocktypes - - map satNavMap; - SSRAtm ssrAtm; - - ERP erp; /* earth rotation parameters */ - int leaps = -1; /* leap seconds (s) */ - char glo_fcn[NSATGLO+1]; /* glonass frequency channel number + 8 */ - double glo_cpbias[4]; /* glonass code-phase bias {1C,1P,2C,2P} (m) */ - - - struct jpl_eph_data* jplEph_ptr = nullptr; - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - ar & ephMap; - ar & satNavMap; - } + // these are interpolated between, dont need greater + map> pephMap; ///< precise ephemeris + map> pclkMap; ///< precise clock + map> attMapMap; ///< attitudes + + map, std::greater>> dragMap; + map, std::greater>> reflectorMap; + map>>>> + pcoMap; + map>>>> pcvMap; + + map>>> + ephMap; ///< GPS/QZS/GAL/BDS ephemeris + map>>> + gephMap; ///< GLONASS ephemeris + map>>> + sephMap; ///< SBAS ephemeris + map>>> + cephMap; ///< GPS/QZS/BDS CNVX ephemeris + map>>> ionMap; ///< ION messages + map>>> + stoMap; ///< STO messages + map>>> eopMap; ///< EOP messages + map> tecMap; ///< tec grid data + map>> svnMap; ///< Sat SVNs + + map blocktypeMap; ///< svn blocktypes + + map gloFreqMap; ///< glonass frequency channel numbers + map satNavMap; + SSRAtm ssrAtm; + SBASIono sbsIono; ///< current SBAS Ionosphere map + + ERP erp; /* earth rotation parameters */ + int leaps = -1; /* leap seconds (s) */ + double glo_cpbias[4]; /* glonass code-phase bias {1C,1P,2C,2P} (m) */ + + double pclkInterval = 900; + + struct jpl_eph_data* jplEph_ptr = nullptr; + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + ar & ephMap; + ar & satNavMap; + } }; - namespace boost::serialization { - template - void serialize(ARCHIVE& ar, Navigation& nav) - { -// ar & nav.ephMap; - } +template +void serialize(ARCHIVE& ar, Navigation& nav) +{ + // ar & nav.ephMap; } - +} // namespace boost::serialization extern map defNavMsgType; -extern Navigation nav; +extern Navigation nav; diff --git a/src/cpp/common/ntripBroadcast.cpp b/src/cpp/common/ntripBroadcast.cpp index e6f71e829..292f3c4a9 100644 --- a/src/cpp/common/ntripBroadcast.cpp +++ b/src/cpp/common/ntripBroadcast.cpp @@ -1,626 +1,805 @@ - // #pragma GCC optimize ("O0") -#include -#include - +#include "common/ntripBroadcast.hpp" +#include #include - -using std::chrono::system_clock; -using std::chrono::time_point; - -using bsoncxx::builder::basic::kvp; - -#include "ntripBroadcast.hpp" -#include "mongoRead.hpp" -#include "otherSSR.hpp" -#include "fileLog.hpp" -#include "gTime.hpp" - - -NtripBroadcaster ntripBroadcaster; +#include "common/fileLog.hpp" +#include "common/gTime.hpp" +#include "common/mongoRead.hpp" +#include "other_ssr/otherSSR.hpp" namespace bp = boost::asio::placeholders; - using boost::posix_time::ptime; using boost::posix_time::time_duration; +using std::chrono::system_clock; +using std::chrono::time_point; + +NtripBroadcaster ntripBroadcaster; void debugSSR(GTime t0, GTime targetTime, E_Sys sys, SsrOutMap& ssrOutMap); void NtripBroadcaster::startBroadcast() { - TcpSocket::startClients(); + TcpSocket::startClients(); } void NtripBroadcaster::stopBroadcast() { - for (auto [label, outStream] : ntripUploadStreams) - { - outStream->disconnect(); - } + for (auto [label, outStream] : ntripUploadStreams) + { + outStream->disconnect(); + } - ntripUploadStreams.clear(); + ntripUploadStreams.clear(); } -void NtripUploader::serverResponse( - unsigned int status_code, - string http_version) +void NtripUploader::serverResponse(unsigned int status_code, string http_version) { - if (acsConfig.output_ntrip_log == false) - return; + if (acsConfig.output_ntrip_log == false) + return; - std::ofstream logStream(networkTraceFilename, std::ofstream::app); + std::ofstream logStream(networkTraceFilename, std::ofstream::app); - if (!logStream) - { - BOOST_LOG_TRIVIAL(warning) << "Error opening log file.\n"; - return; - } + if (!logStream) + { + BOOST_LOG_TRIVIAL(warning) << "Error opening log file.\n"; + return; + } - GTime time = timeGet(); + GTime time = timeGet(); - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("label", __FUNCTION__)); - doc.append(kvp("Stream", url.path.substr(1, url.path.length()))); - doc.append(kvp("Time", time.to_string())); - doc.append(kvp("ServerStatus", (int)status_code)); - doc.append(kvp("VersionHTTP", http_version)); + boost::json::object doc; + doc["label"] = __FUNCTION__; + doc["Stream"] = url.path.substr(1, url.path.length()); + doc["Time"] = time.to_string(); + doc["ServerStatus"] = static_cast(status_code); + doc["VersionHTTP"] = http_version; - logStream << bsoncxx::to_json(doc) << "\n"; + logStream << boost::json::serialize(doc) << "\n"; } - -void NtripUploader::writeHandler( - const boost::system::error_code& err) +void NtripUploader::writeHandler(const boost::system::error_code& err) { - if (err) - { - outMessages.consume(outMessages.size()); - outMessagesMtx.unlock(); + if (err) + { + outMessages.consume(outMessages.size()); + outMessagesMtx.unlock(); - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } - onChunkSentStatistics(); + onChunkSentStatistics(); - outMessagesMtx.unlock(); + outMessagesMtx.unlock(); } -void NtripUploader::messageTimeoutHandler( - const boost::system::error_code& err) +void NtripUploader::messageTimeoutHandler(const boost::system::error_code& err) { - // BOOST_LOG_TRIVIAL(debug) << "started " << __FUNCTION__ << "\n"; - if (err) - { - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } - - //fire this callback again in the future - { - sendTimer.expires_from_now(boost::posix_time::milliseconds(500)); // check uploader twice a second to account for aliasing - sendTimer.async_wait(boost::bind(&NtripUploader::messageTimeoutHandler, this, bp::error)); - } - - SSRMeta ssrMeta; - SsrOutMap ssrOutMap; - - GTime latestTime = timeGet(); - - if (latestTime == GTime::noTime()) - return; - - GTime targetTime = latestTime.floorTime(1); - - if (targetTime == previousTargetTime) - { - //already did this epoch - return; - } - - BOOST_LOG_TRIVIAL(debug) << "SSR OUT Targeting epoch: " << targetTime.to_string() << "\n"; - - ssrMeta.receivedTime = targetTime; // for rtcmTrace (debugging) - ssrMeta.multipleMessage = 1; // We assume there will be more messages. - - if (streamConfig.itrf_datum) ssrMeta.referenceDatum = 0; // Orbit corrections, 0 - ITRF, 1 - Regional. - else ssrMeta.referenceDatum = 1; - - ssrMeta.provider = streamConfig.provider_id; - ssrMeta.solution = streamConfig.solution_id; - - for (auto [messCode, msgOpts] : streamConfig.rtcmMsgOptsMap) - { - int updateInterval = msgOpts.udi; - - if ( messCode == +RtcmMessageType::IGS_SSR - || messCode == +RtcmMessageType::COMPACT_SSR) - { - updateInterval = 1; - } - - if ( updateInterval == 0 - ||((long int)targetTime) % updateInterval != 0) - { - continue; - } - - int udiIndex = getUdiIndex(updateInterval); - - ssrMeta.updateIntIndex = udiIndex; - - if (ssrMeta.updateIntIndex == -1) - BOOST_LOG_TRIVIAL(error) << "Error: ssrMeta.updateIntIndex is not valid :" << ssrMeta.updateIntIndex << ")."; - - if (messCode == streamConfig.rtcmMsgOptsMap.rbegin()->first) - ssrMeta.multipleMessage = 0; - - E_Sys sys = rtcmMessageSystemMap[messCode]; - - if (sys == +E_Sys::NONE) - { - BOOST_LOG_TRIVIAL(error) << "Error: invalid message code system :" << messCode; - continue; - } - - if (sys == +E_Sys::GLO) { RTod tod = targetTime; ssrMeta.epochTime1s = (int)tod; } - else if (sys == +E_Sys::BDS) { BTow tow = targetTime; ssrMeta.epochTime1s = (int)tow; } - else { GTow tow = targetTime; ssrMeta.epochTime1s = (int)tow; } - - GTime t0; - if (ssrMeta.updateIntIndex == 0) t0 = targetTime; - else t0 = targetTime + updateInterval / 2.0; - - BOOST_LOG_TRIVIAL(debug) << "SSR message type: " << messCode._to_string() << ", udi: " << updateInterval; - switch (messCode) - { - case +RtcmMessageType::GPS_SSR_PHASE_BIAS: - case +RtcmMessageType::GLO_SSR_PHASE_BIAS: - case +RtcmMessageType::GAL_SSR_PHASE_BIAS: - case +RtcmMessageType::QZS_SSR_PHASE_BIAS: - case +RtcmMessageType::BDS_SSR_PHASE_BIAS: - case +RtcmMessageType::SBS_SSR_PHASE_BIAS: - { - auto ssrPBMap = mongoReadPhaseBias(ssrMeta, masterIod, sys); - - auto buffer = encodeSsrPhase(ssrPBMap, messCode); - bool write = encodeWriteMessageToBuffer(buffer); - - if (write == false) - { - std::cout << "RtcmMessageType::" << messCode._to_string() << " was not written" << "\n"; - } - - break; - } - case +RtcmMessageType::GPS_SSR_CODE_BIAS: - case +RtcmMessageType::GLO_SSR_CODE_BIAS: - case +RtcmMessageType::GAL_SSR_CODE_BIAS: - case +RtcmMessageType::QZS_SSR_CODE_BIAS: - case +RtcmMessageType::BDS_SSR_CODE_BIAS: - case +RtcmMessageType::SBS_SSR_CODE_BIAS: - { - auto ssrCBMap = mongoReadCodeBias(ssrMeta, masterIod, sys); - - auto buffer = encodeSsrCode(ssrCBMap, messCode); - bool write = encodeWriteMessageToBuffer(buffer); - - if (write == false) - { - std::cout << "RtcmMessageType::" << messCode._to_string() << " was not written" << "\n"; - } - - break; - } - case +RtcmMessageType::GPS_SSR_COMB_CORR: - case +RtcmMessageType::GLO_SSR_COMB_CORR: - case +RtcmMessageType::GAL_SSR_COMB_CORR: - case +RtcmMessageType::QZS_SSR_COMB_CORR: - case +RtcmMessageType::BDS_SSR_COMB_CORR: - case +RtcmMessageType::SBS_SSR_COMB_CORR: - case +RtcmMessageType::GPS_SSR_ORB_CORR: - case +RtcmMessageType::GLO_SSR_ORB_CORR: - case +RtcmMessageType::GAL_SSR_ORB_CORR: - case +RtcmMessageType::QZS_SSR_ORB_CORR: - case +RtcmMessageType::BDS_SSR_ORB_CORR: - case +RtcmMessageType::SBS_SSR_ORB_CORR: - case +RtcmMessageType::GPS_SSR_CLK_CORR: - case +RtcmMessageType::GLO_SSR_CLK_CORR: - case +RtcmMessageType::GAL_SSR_CLK_CORR: - case +RtcmMessageType::QZS_SSR_CLK_CORR: - case +RtcmMessageType::BDS_SSR_CLK_CORR: - case +RtcmMessageType::SBS_SSR_CLK_CORR: - case +RtcmMessageType::GPS_SSR_HR_CLK_CORR: - case +RtcmMessageType::GLO_SSR_HR_CLK_CORR: - case +RtcmMessageType::GAL_SSR_HR_CLK_CORR: - case +RtcmMessageType::QZS_SSR_HR_CLK_CORR: - case +RtcmMessageType::BDS_SSR_HR_CLK_CORR: - case +RtcmMessageType::SBS_SSR_HR_CLK_CORR: - { - ssrOutMap = mongoReadOrbClk(t0, ssrMeta, masterIod, sys); - - calculateSsrComb(t0, updateInterval, ssrMeta, masterIod, ssrOutMap); - - auto buffer = encodeSsrOrbClk(ssrOutMap, messCode); - bool write = encodeWriteMessageToBuffer(buffer); - - if (traceLevel > 5) - { -// debugSSR(t0, targetTime, sys, ssrOutMap); - - if (write == false) - { - std::cout << "RtcmMessageType::" << messCode._to_string() << " was not written" << "\n"; - } - } - - break; - } - case +RtcmMessageType::GPS_SSR_URA: - case +RtcmMessageType::GLO_SSR_URA: - case +RtcmMessageType::GAL_SSR_URA: - case +RtcmMessageType::QZS_SSR_URA: - case +RtcmMessageType::BDS_SSR_URA: - case +RtcmMessageType::SBS_SSR_URA: - { - auto buffer = encodeSsrUra(ssrOutMap, messCode); - bool write = encodeWriteMessageToBuffer(buffer); - - if (write == false) - { - std::cout << "RtcmMessageType::" << messCode._to_string() << " was not written" << "\n"; - } - - break; - } - case +RtcmMessageType::GPS_EPHEMERIS: - case +RtcmMessageType::BDS_EPHEMERIS: - case +RtcmMessageType::QZS_EPHEMERIS: - case +RtcmMessageType::GAL_FNAV_EPHEMERIS: - case +RtcmMessageType::GAL_INAV_EPHEMERIS: - { - bool write = false; - - for (auto& sat : getSysSats(sys)) - { - auto eph = mongoReadEphemeris(targetTime, sat, messCode); - - if (eph.toe == GTime::noTime()) - continue; - - auto buffer = encodeEphemeris(eph, messCode); - write |= encodeWriteMessageToBuffer(buffer); - } - - if (write == false) - { - std::cout << "RtcmMessageType::" << messCode._to_string() << " was not written" << "\n"; - } - - break; - } - case +RtcmMessageType::GLO_EPHEMERIS: - { - bool write = false; - - for (auto& sat : getSysSats(sys)) - { - auto geph = mongoReadGloEphemeris(targetTime, sat); - - if (geph.toe == GTime::noTime()) - continue; - - auto buffer = encodeEphemeris(geph, messCode); - write |= encodeWriteMessageToBuffer(buffer); - } - - if (write == false) - { - std::cout << "RtcmMessageType::" << messCode._to_string() << " was not written" << "\n"; - } - - break; - } - - case +RtcmMessageType::IGS_SSR: - { - SSRAtm ssrAtm; - map> ssrOutMaps; - map ssrCodMaps; - map ssrPhsMaps; - map ssrUraMaps; - - IgsSSRSubtype lastSubType = IgsSSRSubtype::NONE; - map approvedMessages; - - BOOST_LOG_TRIVIAL(debug) << "IGS SSR: "; - - E_Sys sys; - for (auto [subType, subUdi] : msgOpts.igs_udi) - { - BOOST_LOG_TRIVIAL(debug) << "message type: " << subType._to_string() << ", udi: " << subUdi; - - if ( subUdi == 0 - || ((long int)targetTime) % subUdi != 0) - { - continue; - } - int subUdiIndex = getUdiIndex(subUdi); - ssrMeta.updateIntIndex = subUdiIndex; - if (ssrMeta.updateIntIndex == 0) t0 = targetTime; - else t0 = targetTime + subUdi/2.0; - - auto group = IGS_SSR_group(subType, sys); - switch (group) - { - case 1: - case 2: - case 3: - case 4: - { - auto sysOutMap = mongoReadOrbClk(t0, ssrMeta, masterIod, sys); - - calculateSsrComb(t0, subUdi, ssrMeta, masterIod, sysOutMap); - - if (sysOutMap.empty() == false) - { - lastSubType = subType; - ssrOutMaps[sys] = sysOutMap; - approvedMessages[subType] = true; - } - break; - } - case 5: - { - auto sysCBMap = mongoReadCodeBias(ssrMeta, masterIod, sys); - - if (sysCBMap.empty() == false) - { - lastSubType = subType; - ssrCodMaps[sys] = sysCBMap; - approvedMessages[subType] = true; - } - break; - } - case 6: - { - auto sysPBMap = mongoReadPhaseBias(ssrMeta, masterIod, sys); - - if (sysPBMap.empty() == false) - { - lastSubType = subType; - ssrPhsMaps[sys] = sysPBMap; - approvedMessages[subType] = true; - } - break; - } - case 7: - { - // auto sysUraMap = mongoReadUra(t0, ssrMeta, masterIod, sys); // Eugene: use sysOutMap? - // if (sysUraMap.empty() == false) - // { - // lastSubType = subType; - // ssrUraMaps[sys] = sysUraMap; - // approvedMessages[subType] = true; - // } - break; - } - case 8: - { - ssrAtm = mongoReadIGSIonosphere(targetTime, ssrMeta, masterIod); - - if (ssrAtm.atmosGlobalMap.empty() == false) - { - lastSubType = subType; - approvedMessages[subType] = true; - } - break; - } - } - } - - for (auto [subType, approved] : approvedMessages) - { - bool last = (lastSubType == subType); - - switch (IGS_SSR_group(subType, sys)) - { - case 1: {auto buffer = encodeIGS_ORB(ssrOutMaps[sys], sys, last); encodeWriteMessageToBuffer(buffer); break;} - case 2: {auto buffer = encodeIGS_CLK(ssrOutMaps[sys], sys, last); encodeWriteMessageToBuffer(buffer); break;} - case 3: {auto buffer = encodeIGS_CMB(ssrOutMaps[sys], sys, last); encodeWriteMessageToBuffer(buffer); break;} - case 4: {auto buffer = encodeIGS_HRC(ssrOutMaps[sys], sys, last); encodeWriteMessageToBuffer(buffer); break;} - case 5: {auto buffer = encodeIGS_COD(ssrCodMaps[sys], sys, last); encodeWriteMessageToBuffer(buffer); break;} - case 6: {auto buffer = encodeIGS_PHS(ssrPhsMaps[sys], sys, last); encodeWriteMessageToBuffer(buffer); break;} - case 7: {auto buffer = encodeIGS_URA(ssrUraMaps[sys], sys, last); encodeWriteMessageToBuffer(buffer); break;} - case 8: {auto buffer = encodeIGS_ATM(ssrAtm, last); encodeWriteMessageToBuffer(buffer); break;} - } - } - break; - } - - case +RtcmMessageType::COMPACT_SSR: - { - SSRAtm ssrAtm; - map ssrOutMap; - map ssrCodMap; - map ssrPhsMap; - - map approvedMessages; - - bool new_mask = false; - for (auto [subType, subUdi] : msgOpts.comp_udi) - { - if ( subUdi == 0 ) - continue; - if (((long int)targetTime) % subUdi != 0) - { - if (!new_mask - && subType != +CompactSSRSubtype::SRV) - { - continue; - } - } - - int subUdiIndex = 0; - if (subType != +CompactSSRSubtype::SRV) - subUdiIndex = getUdiIndex(subUdi); - - ssrMeta.updateIntIndex = subUdiIndex; - if (ssrMeta.updateIntIndex == 0) t0 = targetTime; - else t0 = targetTime + subUdi / 2.0; - - switch (subType) - { - case +CompactSSRSubtype::MSK: - new_mask = true; - approvedMessages[subType] = subUdiIndex; - break; - case +CompactSSRSubtype::ORB: - case +CompactSSRSubtype::CLK: - case +CompactSSRSubtype::CMB: - case +CompactSSRSubtype::URA: - for (auto [sys, proc] : acsConfig.process_sys) //todo aaron, this is all just copying stuff from one map to another - if (proc) - { - auto sysOutMap = mongoReadOrbClk(t0, ssrMeta, masterIod, sys); for (auto [sat, data] : sysOutMap) { ssrOutMap[sat] = data; approvedMessages[subType] = subUdiIndex; } - } - calculateSsrComb(t0, subUdi, ssrMeta, masterIod, ssrOutMap); - break; - case +CompactSSRSubtype::COD: - for (auto [sys, proc] : acsConfig.process_sys) - if (proc) - { - auto sysCBMap = mongoReadCodeBias(ssrMeta, masterIod, sys); for (auto [sat, data] : sysCBMap) { ssrCodMap[sat] = data; approvedMessages[subType] = subUdiIndex; } - } - break; - case +CompactSSRSubtype::PHS: - for (auto [sys, proc] : acsConfig.process_sys) - if (proc) - { - auto sysPBMap = mongoReadPhaseBias(ssrMeta, masterIod, sys); for (auto [sat, data] : sysPBMap) { ssrPhsMap[sat] = data; approvedMessages[subType] = subUdiIndex; } - } - break; - case +CompactSSRSubtype::BIA: - for (auto [sys, proc] : acsConfig.process_sys) - if (proc) - { - auto sysCBMap = mongoReadCodeBias (ssrMeta, masterIod, sys); for (auto [sat, data] : sysCBMap) { ssrCodMap[sat] = data; approvedMessages[subType] = subUdiIndex; } - auto sysPBMap = mongoReadPhaseBias (ssrMeta, masterIod, sys); for (auto [sat, data] : sysPBMap) { ssrPhsMap[sat] = data; approvedMessages[subType] = subUdiIndex; } - } - break; - case +CompactSSRSubtype::TEC: - case +CompactSSRSubtype::GRD: - case +CompactSSRSubtype::ATM: - case +CompactSSRSubtype::SRV: - { - ssrAtm = mongoReadCmpAtmosphere(targetTime, ssrMeta); - - if (ssrAtm.atmosRegionsMap.empty() == false) - approvedMessages[subType] = subUdiIndex; - - break; - } - // default: - // BOOST_LOG_TRIVIAL(error) << "Error, attempting to upload incorrect compacr SSR type: " << subType.to_integral << "\n"; - - } - } - - if (approvedMessages.empty()) - break; - - CompactSSRSubtype lastSubType = CompactSSRSubtype::NONE; - - for (auto [subType, udi] : approvedMessages) - { - if ( subType == +CompactSSRSubtype::SRV - || subType == +CompactSSRSubtype::MSK) - { - continue; - } - - lastSubType = subType; - } - - int lastReg = -1; - if ( lastSubType == +CompactSSRSubtype::GRD - || lastSubType == +CompactSSRSubtype::TEC - || lastSubType == +CompactSSRSubtype::ATM) - for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) - lastReg = regId; - - for (auto [subType, udi] : approvedMessages) - { - bool last = (lastSubType == subType); - - switch (subType) - { - case +CompactSSRSubtype::SRV: {auto buffer = encodecompactSRV( ssrAtm); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::MSK: {auto buffer = encodecompactMSK( ssrOutMap, ssrCodMap, ssrPhsMap, ssrAtm, udi); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::ORB: {auto buffer = encodecompactORB( ssrOutMap, udi, last); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::CLK: {auto buffer = encodecompactCLK( ssrOutMap, udi, last); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::CMB: {auto buffer = encodecompactCMB( ssrOutMap, udi, last); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::URA: {auto buffer = encodecompactURA( ssrOutMap, udi, last); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::COD: {auto buffer = encodecompactCOD( ssrCodMap, udi, last); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::PHS: {auto buffer = encodecompactPHS( ssrPhsMap, udi, last); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::BIA: {auto buffer = encodecompactBIA( ssrCodMap, ssrPhsMap, udi, last); encodeWriteMessageToBuffer(buffer); break;} - case +CompactSSRSubtype::TEC: for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) {auto buffer = encodecompactTEC(ssrAtm.ssrMeta, regId, regData, udi, regId==lastReg); encodeWriteMessageToBuffer(buffer); } break; - case +CompactSSRSubtype::GRD: for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) {auto buffer = encodecompactGRD(ssrAtm.ssrMeta, regId, regData, udi, regId==lastReg); encodeWriteMessageToBuffer(buffer); } break; - case +CompactSSRSubtype::ATM: for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) {auto buffer = encodecompactATM(ssrAtm.ssrMeta, regId, regData, udi, regId==lastReg); encodeWriteMessageToBuffer(buffer); } break; - } - } - break; - } - default: - BOOST_LOG_TRIVIAL(error) << "Error, attempting to upload incorrect message type: " << messCode << "\n"; - } - } - - std::stringstream messStream; - encodeWriteMessages(messStream); - - messStream.seekg(0, messStream.end); - int length = messStream.tellg(); - messStream.seekg(0, messStream.beg); - - BOOST_LOG_TRIVIAL(debug) << "Called " << __FUNCTION__ << " MessageLength : " << length << "\n"; - if (length != 0) - { - vector data; - data.resize(length); - - outMessagesMtx.lock(); - std::ostream chunkedStream(&outMessages); - chunkedStream << std::uppercase << std::hex << length << "\r\n"; - - messStream .read (&data[0], length); - chunkedStream .write (&data[0], length); - chunkedStream << "\r\n"; - - if (url.protocol == "https") { boost::asio::async_write(*_sslsocket, outMessages, boost::bind(&NtripUploader::writeHandler, this, bp::error));} - else { boost::asio::async_write(*_socket, outMessages, boost::bind(&NtripUploader::writeHandler, this, bp::error));} - - previousTargetTime = targetTime; - } + // BOOST_LOG_TRIVIAL(debug) << "started " << __FUNCTION__ << "\n"; + if (err) + { + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } + + // fire this callback again in the future + { + sendTimer.expires_from_now( + boost::posix_time::milliseconds(500) + ); // check uploader twice a second to account for aliasing + sendTimer.async_wait(boost::bind(&NtripUploader::messageTimeoutHandler, this, bp::error)); + } + + SSRMeta ssrMeta; + SsrOutMap ssrOutMap; + + GTime latestTime = timeGet(); + + if (latestTime == GTime::noTime()) + return; + + GTime targetTime = latestTime.floorTime(1); + + if (targetTime == previousTargetTime) + { + // already did this epoch + return; + } + + BOOST_LOG_TRIVIAL(debug) << "SSR OUT Targeting epoch: " << targetTime.to_string() << "\n"; + + ssrMeta.receivedTime = targetTime; // for rtcmTrace (debugging) + ssrMeta.multipleMessage = 1; // We assume there will be more messages. + + if (streamConfig.itrf_datum) + ssrMeta.referenceDatum = 0; // Orbit corrections, 0 - ITRF, 1 - Regional. + else + ssrMeta.referenceDatum = 1; + + ssrMeta.provider = streamConfig.provider_id; + ssrMeta.solution = streamConfig.solution_id; + + for (auto [messCode, msgOpts] : streamConfig.rtcmMsgOptsMap) + { + int updateInterval = msgOpts.udi; + + if (messCode == RtcmMessageType::IGS_SSR || messCode == RtcmMessageType::COMPACT_SSR) + { + updateInterval = 1; + } + + if (updateInterval == 0 || ((long int)targetTime) % updateInterval != 0) + { + continue; + } + + int udiIndex = getUdiIndex(updateInterval); + + ssrMeta.updateIntIndex = udiIndex; + + if (ssrMeta.updateIntIndex == -1) + BOOST_LOG_TRIVIAL(error) + << "ssrMeta.updateIntIndex is not valid :" << ssrMeta.updateIntIndex << ")."; + + if (messCode == streamConfig.rtcmMsgOptsMap.rbegin()->first) + ssrMeta.multipleMessage = 0; + + E_Sys sys = rtcmMessageSystemMap[messCode]; + + if (sys == E_Sys::NONE) + { + BOOST_LOG_TRIVIAL(error) << "Invalid message code system :" << messCode; + continue; + } + + if (sys == E_Sys::GLO) + { + RTod tod = targetTime; + ssrMeta.epochTime1s = (int)tod; + } + else if (sys == E_Sys::BDS) + { + BTow tow = targetTime; + ssrMeta.epochTime1s = (int)tow; + } + else + { + GTow tow = targetTime; + ssrMeta.epochTime1s = (int)tow; + } + + GTime t0; + if (ssrMeta.updateIntIndex == 0) + t0 = targetTime; + else + t0 = targetTime + updateInterval / 2.0; + + BOOST_LOG_TRIVIAL(debug) << "SSR message type: " << enum_to_string(messCode) + << ", udi: " << updateInterval; + switch (messCode) + { + case RtcmMessageType::GPS_SSR_PHASE_BIAS: + case RtcmMessageType::GLO_SSR_PHASE_BIAS: + case RtcmMessageType::GAL_SSR_PHASE_BIAS: + case RtcmMessageType::QZS_SSR_PHASE_BIAS: + case RtcmMessageType::BDS_SSR_PHASE_BIAS: + case RtcmMessageType::SBS_SSR_PHASE_BIAS: + { + auto ssrPBMap = mongoReadPhaseBias(ssrMeta, masterIod, sys); + + auto buffer = encodeSsrPhase(ssrPBMap, messCode); + bool write = encodeWriteMessageToBuffer(buffer); + + if (write == false) + { + std::cout << "RtcmMessageType::" << enum_to_string(messCode) + << " was not written" + << "\n"; + } + + break; + } + case RtcmMessageType::GPS_SSR_CODE_BIAS: + case RtcmMessageType::GLO_SSR_CODE_BIAS: + case RtcmMessageType::GAL_SSR_CODE_BIAS: + case RtcmMessageType::QZS_SSR_CODE_BIAS: + case RtcmMessageType::BDS_SSR_CODE_BIAS: + case RtcmMessageType::SBS_SSR_CODE_BIAS: + { + auto ssrCBMap = mongoReadCodeBias(ssrMeta, masterIod, sys); + + auto buffer = encodeSsrCode(ssrCBMap, messCode); + bool write = encodeWriteMessageToBuffer(buffer); + + if (write == false) + { + std::cout << "RtcmMessageType::" << enum_to_string(messCode) + << " was not written" + << "\n"; + } + + break; + } + case RtcmMessageType::GPS_SSR_COMB_CORR: + case RtcmMessageType::GLO_SSR_COMB_CORR: + case RtcmMessageType::GAL_SSR_COMB_CORR: + case RtcmMessageType::QZS_SSR_COMB_CORR: + case RtcmMessageType::BDS_SSR_COMB_CORR: + case RtcmMessageType::SBS_SSR_COMB_CORR: + case RtcmMessageType::GPS_SSR_ORB_CORR: + case RtcmMessageType::GLO_SSR_ORB_CORR: + case RtcmMessageType::GAL_SSR_ORB_CORR: + case RtcmMessageType::QZS_SSR_ORB_CORR: + case RtcmMessageType::BDS_SSR_ORB_CORR: + case RtcmMessageType::SBS_SSR_ORB_CORR: + case RtcmMessageType::GPS_SSR_CLK_CORR: + case RtcmMessageType::GLO_SSR_CLK_CORR: + case RtcmMessageType::GAL_SSR_CLK_CORR: + case RtcmMessageType::QZS_SSR_CLK_CORR: + case RtcmMessageType::BDS_SSR_CLK_CORR: + case RtcmMessageType::SBS_SSR_CLK_CORR: + case RtcmMessageType::GPS_SSR_HR_CLK_CORR: + case RtcmMessageType::GLO_SSR_HR_CLK_CORR: + case RtcmMessageType::GAL_SSR_HR_CLK_CORR: + case RtcmMessageType::QZS_SSR_HR_CLK_CORR: + case RtcmMessageType::BDS_SSR_HR_CLK_CORR: + case RtcmMessageType::SBS_SSR_HR_CLK_CORR: + { + ssrOutMap = mongoReadOrbClk(t0, ssrMeta, masterIod, sys); + + calculateSsrComb(t0, updateInterval, ssrMeta, masterIod, ssrOutMap); + + auto buffer = encodeSsrOrbClk(ssrOutMap, messCode); + bool write = encodeWriteMessageToBuffer(buffer); + + if (traceLevel > 5) + { + // debugSSR(t0, targetTime, sys, ssrOutMap); + + if (write == false) + { + std::cout << "RtcmMessageType::" << enum_to_string(messCode) + << " was not written" << "\n"; + } + } + + break; + } + case RtcmMessageType::GPS_SSR_URA: + case RtcmMessageType::GLO_SSR_URA: + case RtcmMessageType::GAL_SSR_URA: + case RtcmMessageType::QZS_SSR_URA: + case RtcmMessageType::BDS_SSR_URA: + case RtcmMessageType::SBS_SSR_URA: + { + auto buffer = encodeSsrUra(ssrOutMap, messCode); + bool write = encodeWriteMessageToBuffer(buffer); + + if (write == false) + { + std::cout << "RtcmMessageType::" << enum_to_string(messCode) + << " was not written" + << "\n"; + } + + break; + } + case RtcmMessageType::GPS_EPHEMERIS: + case RtcmMessageType::BDS_EPHEMERIS: + case RtcmMessageType::QZS_EPHEMERIS: + case RtcmMessageType::GAL_FNAV_EPHEMERIS: + case RtcmMessageType::GAL_INAV_EPHEMERIS: + { + bool write = false; + + for (auto& sat : getSysSats(sys)) + { + auto eph = mongoReadEphemeris(targetTime, sat, messCode); + + if (eph.toe == GTime::noTime()) + continue; + + auto buffer = encodeEphemeris(eph, messCode); + write |= encodeWriteMessageToBuffer(buffer); + } + + if (write == false) + { + std::cout << "RtcmMessageType::" << enum_to_string(messCode) + << " was not written" + << "\n"; + } + + break; + } + case RtcmMessageType::GLO_EPHEMERIS: + { + bool write = false; + + for (auto& sat : getSysSats(sys)) + { + auto geph = mongoReadGloEphemeris(targetTime, sat); + + if (geph.toe == GTime::noTime()) + continue; + + auto buffer = encodeEphemeris(geph, messCode); + write |= encodeWriteMessageToBuffer(buffer); + } + + if (write == false) + { + std::cout << "RtcmMessageType::" << enum_to_string(messCode) + << " was not written" + << "\n"; + } + + break; + } + + case RtcmMessageType::IGS_SSR: + { + SSRAtm ssrAtm; + map> ssrOutMaps; + map ssrCodMaps; + map ssrPhsMaps; + map ssrUraMaps; + + IgsSSRSubtype lastSubType = IgsSSRSubtype::NONE; + map approvedMessages; + + BOOST_LOG_TRIVIAL(debug) << "IGS SSR: "; + + E_Sys sys; + for (auto [subType, subUdi] : msgOpts.igs_udi) + { + BOOST_LOG_TRIVIAL(debug) + << "message type: " << enum_to_string(subType) << ", udi: " << subUdi; + + if (subUdi == 0 || ((long int)targetTime) % subUdi != 0) + { + continue; + } + int subUdiIndex = getUdiIndex(subUdi); + ssrMeta.updateIntIndex = subUdiIndex; + if (ssrMeta.updateIntIndex == 0) + t0 = targetTime; + else + t0 = targetTime + subUdi / 2.0; + + auto group = IGS_SSR_group(subType, sys); + switch (group) + { + case IgsSSRSubtype::GROUP_ORB: + case IgsSSRSubtype::GROUP_CLK: + case IgsSSRSubtype::GROUP_CMB: + case IgsSSRSubtype::GROUP_HRC: + { + auto sysOutMap = mongoReadOrbClk(t0, ssrMeta, masterIod, sys); + + calculateSsrComb(t0, subUdi, ssrMeta, masterIod, sysOutMap); + + if (sysOutMap.empty() == false) + { + lastSubType = subType; + ssrOutMaps[sys] = sysOutMap; + approvedMessages[subType] = true; + } + break; + } + case IgsSSRSubtype::GROUP_COD: + { + auto sysCBMap = mongoReadCodeBias(ssrMeta, masterIod, sys); + + if (sysCBMap.empty() == false) + { + lastSubType = subType; + ssrCodMaps[sys] = sysCBMap; + approvedMessages[subType] = true; + } + break; + } + case IgsSSRSubtype::GROUP_PHS: + { + auto sysPBMap = mongoReadPhaseBias(ssrMeta, masterIod, sys); + + if (sysPBMap.empty() == false) + { + lastSubType = subType; + ssrPhsMaps[sys] = sysPBMap; + approvedMessages[subType] = true; + } + break; + } + case IgsSSRSubtype::GROUP_URA: + { + // auto sysUraMap = mongoReadUra(t0, ssrMeta, masterIod, sys); // + // Eugene: use sysOutMap? if (sysUraMap.empty() == false) + // { + // lastSubType = subType; + // ssrUraMaps[sys] = sysUraMap; + // approvedMessages[subType] = true; + // } + break; + } + case IgsSSRSubtype::GROUP_ION: + { + ssrAtm = mongoReadIGSIonosphere(targetTime, ssrMeta, masterIod); + + if (ssrAtm.atmosGlobalMap.empty() == false) + { + lastSubType = subType; + approvedMessages[subType] = true; + } + break; + } + } + } + + for (auto [subType, approved] : approvedMessages) + { + bool last = (lastSubType == subType); + + switch (IGS_SSR_group(subType, sys)) + { + case IgsSSRSubtype::GROUP_ORB: + { + auto buffer = encodeIGS_ORB(ssrOutMaps[sys], sys, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case IgsSSRSubtype::GROUP_CLK: + { + auto buffer = encodeIGS_CLK(ssrOutMaps[sys], sys, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case IgsSSRSubtype::GROUP_CMB: + { + auto buffer = encodeIGS_CMB(ssrOutMaps[sys], sys, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case IgsSSRSubtype::GROUP_HRC: + { + auto buffer = encodeIGS_HRC(ssrOutMaps[sys], sys, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case IgsSSRSubtype::GROUP_COD: + { + auto buffer = encodeIGS_COD(ssrCodMaps[sys], sys, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case IgsSSRSubtype::GROUP_PHS: + { + auto buffer = encodeIGS_PHS(ssrPhsMaps[sys], sys, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case IgsSSRSubtype::GROUP_URA: + { + auto buffer = encodeIGS_URA(ssrUraMaps[sys], sys, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case IgsSSRSubtype::GROUP_ION: + { + auto buffer = encodeIGS_ATM(ssrAtm, last); + encodeWriteMessageToBuffer(buffer); + break; + } + } + } + break; + } + + case RtcmMessageType::COMPACT_SSR: + { + SSRAtm ssrAtm; + map ssrOutMap; + map ssrCodMap; + map ssrPhsMap; + + map approvedMessages; + + bool new_mask = false; + for (auto [subType, subUdi] : msgOpts.comp_udi) + { + if (subUdi == 0) + continue; + if (((long int)targetTime) % subUdi != 0) + { + if (!new_mask && subType != CompactSSRSubtype::SRV) + { + continue; + } + } + + int subUdiIndex = 0; + if (subType != CompactSSRSubtype::SRV) + subUdiIndex = getUdiIndex(subUdi); + + ssrMeta.updateIntIndex = subUdiIndex; + if (ssrMeta.updateIntIndex == 0) + t0 = targetTime; + else + t0 = targetTime + subUdi / 2.0; + + switch (subType) + { + case CompactSSRSubtype::MSK: + new_mask = true; + approvedMessages[subType] = subUdiIndex; + break; + case CompactSSRSubtype::ORB: + case CompactSSRSubtype::CLK: + case CompactSSRSubtype::CMB: + case CompactSSRSubtype::URA: + for (auto [sys, proc] : + acsConfig.process_sys) // todo aaron, this is all just copying + // stuff from one map to another + if (proc) + { + auto sysOutMap = mongoReadOrbClk(t0, ssrMeta, masterIod, sys); + for (auto [sat, data] : sysOutMap) + { + ssrOutMap[sat] = data; + approvedMessages[subType] = subUdiIndex; + } + } + calculateSsrComb(t0, subUdi, ssrMeta, masterIod, ssrOutMap); + break; + case CompactSSRSubtype::COD: + for (auto [sys, proc] : acsConfig.process_sys) + if (proc) + { + auto sysCBMap = mongoReadCodeBias(ssrMeta, masterIod, sys); + for (auto [sat, data] : sysCBMap) + { + ssrCodMap[sat] = data; + approvedMessages[subType] = subUdiIndex; + } + } + break; + case CompactSSRSubtype::PHS: + for (auto [sys, proc] : acsConfig.process_sys) + if (proc) + { + auto sysPBMap = mongoReadPhaseBias(ssrMeta, masterIod, sys); + for (auto [sat, data] : sysPBMap) + { + ssrPhsMap[sat] = data; + approvedMessages[subType] = subUdiIndex; + } + } + break; + case CompactSSRSubtype::BIA: + for (auto [sys, proc] : acsConfig.process_sys) + if (proc) + { + auto sysCBMap = mongoReadCodeBias(ssrMeta, masterIod, sys); + for (auto [sat, data] : sysCBMap) + { + ssrCodMap[sat] = data; + approvedMessages[subType] = subUdiIndex; + } + auto sysPBMap = mongoReadPhaseBias(ssrMeta, masterIod, sys); + for (auto [sat, data] : sysPBMap) + { + ssrPhsMap[sat] = data; + approvedMessages[subType] = subUdiIndex; + } + } + break; + case CompactSSRSubtype::TEC: + case CompactSSRSubtype::GRD: + case CompactSSRSubtype::ATM: + case CompactSSRSubtype::SRV: + { + ssrAtm = mongoReadCmpAtmosphere(targetTime, ssrMeta); + + if (ssrAtm.atmosRegionsMap.empty() == false) + approvedMessages[subType] = subUdiIndex; + + break; + } + // default: + // BOOST_LOG_TRIVIAL(error) << "Attempting to upload incorrect + // compacr SSR type: " + // << subType.to_integral << "\n"; + } + } + + if (approvedMessages.empty()) + break; + + CompactSSRSubtype lastSubType = CompactSSRSubtype::NONE; + + for (auto [subType, udi] : approvedMessages) + { + if (subType == CompactSSRSubtype::SRV || subType == CompactSSRSubtype::MSK) + { + continue; + } + + lastSubType = subType; + } + + int lastReg = -1; + if (lastSubType == CompactSSRSubtype::GRD || + lastSubType == CompactSSRSubtype::TEC || lastSubType == CompactSSRSubtype::ATM) + for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) + lastReg = regId; + + for (auto [subType, udi] : approvedMessages) + { + bool last = (lastSubType == subType); + + switch (subType) + { + case CompactSSRSubtype::SRV: + { + auto buffer = encodecompactSRV(ssrAtm); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::MSK: + { + auto buffer = + encodecompactMSK(ssrOutMap, ssrCodMap, ssrPhsMap, ssrAtm, udi); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::ORB: + { + auto buffer = encodecompactORB(ssrOutMap, udi, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::CLK: + { + auto buffer = encodecompactCLK(ssrOutMap, udi, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::CMB: + { + auto buffer = encodecompactCMB(ssrOutMap, udi, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::URA: + { + auto buffer = encodecompactURA(ssrOutMap, udi, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::COD: + { + auto buffer = encodecompactCOD(ssrCodMap, udi, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::PHS: + { + auto buffer = encodecompactPHS(ssrPhsMap, udi, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::BIA: + { + auto buffer = encodecompactBIA(ssrCodMap, ssrPhsMap, udi, last); + encodeWriteMessageToBuffer(buffer); + break; + } + case CompactSSRSubtype::TEC: + for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) + { + auto buffer = encodecompactTEC( + ssrAtm.ssrMeta, + regId, + regData, + udi, + regId == lastReg + ); + encodeWriteMessageToBuffer(buffer); + } + break; + case CompactSSRSubtype::GRD: + for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) + { + auto buffer = encodecompactGRD( + ssrAtm.ssrMeta, + regId, + regData, + udi, + regId == lastReg + ); + encodeWriteMessageToBuffer(buffer); + } + break; + case CompactSSRSubtype::ATM: + for (auto& [regId, regData] : ssrAtm.atmosRegionsMap) + { + auto buffer = encodecompactATM( + ssrAtm.ssrMeta, + regId, + regData, + udi, + regId == lastReg + ); + encodeWriteMessageToBuffer(buffer); + } + break; + } + } + break; + } + default: + BOOST_LOG_TRIVIAL(error) + << "Attempting to upload incorrect message type: " << messCode << "\n"; + } + } + + std::stringstream messStream; + encodeWriteMessages(messStream); + + messStream.seekg(0, messStream.end); + int length = messStream.tellg(); + messStream.seekg(0, messStream.beg); + + BOOST_LOG_TRIVIAL(debug) << "Called " << __FUNCTION__ << " MessageLength : " << length << "\n"; + if (length != 0) + { + vector data; + data.resize(length); + + outMessagesMtx.lock(); + std::ostream chunkedStream(&outMessages); + chunkedStream << std::uppercase << std::hex << length << "\r\n"; + + messStream.read(&data[0], length); + chunkedStream.write(&data[0], length); + chunkedStream << "\r\n"; + + if (url.protocol == "https") + { + boost::asio::async_write( + *_sslsocket, + outMessages, + boost::bind(&NtripUploader::writeHandler, this, bp::error) + ); + } + else + { + boost::asio::async_write( + *_socket, + outMessages, + boost::bind(&NtripUploader::writeHandler, this, bp::error) + ); + } + + previousTargetTime = targetTime; + } } void NtripUploader::startBroadcast() { - // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " Starting Send Loop.\n"; + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " Starting Send Loop.\n"; - sendTimer.expires_from_now(boost::posix_time::seconds(1)); - sendTimer.async_wait(boost::bind(&NtripUploader::messageTimeoutHandler, this, bp::error)); + sendTimer.expires_from_now(boost::posix_time::seconds(1)); + sendTimer.async_wait(boost::bind(&NtripUploader::messageTimeoutHandler, this, bp::error)); } void NtripUploader::connected() { - // BOOST_LOG_TRIVIAL(info) << "Uploader connected.\n"; + // BOOST_LOG_TRIVIAL(info) << "Uploader connected.\n"; - // Although there should be no downloading attempting to download monitors the socket connection. - startRead(true); + // Although there should be no downloading attempting to download monitors the socket + // connection. + startRead(true); - startBroadcast(); + startBroadcast(); } - - - diff --git a/src/cpp/common/ntripBroadcast.hpp b/src/cpp/common/ntripBroadcast.hpp index 0c418d090..424502922 100644 --- a/src/cpp/common/ntripBroadcast.hpp +++ b/src/cpp/common/ntripBroadcast.hpp @@ -1,101 +1,88 @@ - #pragma once - #include +#include "common/acsConfig.hpp" +#include "common/enums.h" +#include "common/navigation.hpp" +#include "common/ntripTrace.hpp" +#include "common/rtcmEncoder.hpp" +#include "common/streamNtrip.hpp" +#include "common/tcpSocket.hpp" + +struct NtripUploader : NtripResponder, RtcmEncoder +{ + boost::posix_time::ptime timeNextMessage; -#include "streamNtrip.hpp" -#include "rtcmEncoder.hpp" -#include "ntripTrace.hpp" -#include "navigation.hpp" -#include "tcpSocket.hpp" -#include "acsConfig.hpp" -#include "enums.h" + // This mutex ensures that the main thread and the io_service thread do + // not alter the outMessages buffer at the same time. + std::mutex outMessagesMtx; + boost::asio::streambuf outMessages; + boost::asio::deadline_timer sendTimer; + int numberChunksSent = 0; + GTime previousTargetTime; ///< Time to prevent aliasing -struct NtripUploader : TcpSocket, RtcmEncoder -{ - boost::posix_time::ptime timeNextMessage; - - // This mutex ensures that the main thread and the io_service thread do - // not alter the outMessages buffer at the same time. - std::mutex outMessagesMtx; - boost::asio::streambuf outMessages; - boost::asio::deadline_timer sendTimer; - int numberChunksSent = 0; - - GTime previousTargetTime; ///< Time to prevent aliasing - - string ntripStr; - string id = "NtripUploader"; - - SsrBroadcast streamConfig; - - NtripUploader( - const string& url_str) - : TcpSocket(url_str), - sendTimer(ioService) - { - if (url.path.empty()) - { - BOOST_LOG_TRIVIAL(error) << "Error: Ntrip uploader created with empty url"; - - return; - } - - rtcmMountpoint = url.path.substr(1); // remove '/' - - if (acsConfig.output_encoded_rtcm_json) - { - rtcmTraceFilename = acsConfig.encoded_rtcm_json_filename; - } - - std::stringstream requestStream; - requestStream << "POST " << url.path << " HTTP/1.1" << "\r\n"; - requestStream << "Host: " << url.host << "\r\n"; - requestStream << "Ntrip-Version: Ntrip/2.0" << "\r\n"; - if (!url.user.empty()) - { - requestStream << "Authorization: Basic " - << Base64::encode(string(url.user + ":" + url.pass)) << "\r\n"; - } - requestStream << "User-Agent: NTRIP ACS/1.0" << "\r\n"; - if (!ntripStr.empty()) requestStream << "Ntrip-STR: " << ntripStr << "\r\n"; - requestStream << "Connection: keep-alive" << "\r\n"; - requestStream << "Transfer-Encoding: chunked" << "\r\n"; - requestStream << "\r\n"; - - requestString = requestStream.str(); - - connect(); - }; - - void startBroadcast(); - - void connected() - override; - - void messageTimeoutHandler( - const boost::system::error_code& err); - - void writeHandler( - const boost::system::error_code& err); - - void serverResponse( - unsigned int status_code, - string http_version); - - void getJsonNetworkStatistics( - GTime now); + string ntripStr; + string id = "NtripUploader"; + + SsrBroadcast streamConfig; + + NtripUploader(const string& url_str) : NtripResponder(url_str), sendTimer(ioContext) + { + if (url.path.empty()) + { + BOOST_LOG_TRIVIAL(error) << "Ntrip uploader created with empty url"; + + return; + } + + rtcmMountpoint = url.path.substr(1); // remove '/' + + if (acsConfig.output_encoded_rtcm_json) + { + rtcmTraceFilename = acsConfig.encoded_rtcm_json_filename; + } + + std::stringstream requestStream; + requestStream << "POST " << url.path << " HTTP/1.1" << "\r\n"; + requestStream << "Host: " << url.host << "\r\n"; + requestStream << "Ntrip-Version: Ntrip/2.0" << "\r\n"; + if (!url.user.empty()) + { + requestStream << "Authorization: Basic " + << Base64::encode(string(url.user + ":" + url.pass)) << "\r\n"; + } + requestStream << "User-Agent: NTRIP ACS/1.0" << "\r\n"; + if (!ntripStr.empty()) + requestStream << "Ntrip-STR: " << ntripStr << "\r\n"; + requestStream << "Connection: keep-alive" << "\r\n"; + requestStream << "Transfer-Encoding: chunked" << "\r\n"; + requestStream << "\r\n"; + + requestString = requestStream.str(); + + connect(); + }; + + void startBroadcast(); + + void connected() override; + + void messageTimeoutHandler(const boost::system::error_code& err); + + void writeHandler(const boost::system::error_code& err); + + void serverResponse(unsigned int status_code, string http_version) override; + + void getJsonNetworkStatistics(GTime now); }; struct NtripBroadcaster { - void startBroadcast(); - void stopBroadcast(); + void startBroadcast(); + void stopBroadcast(); - map> ntripUploadStreams; + map> ntripUploadStreams; }; -extern NtripBroadcaster ntripBroadcaster; - +extern NtripBroadcaster ntripBroadcaster; diff --git a/src/cpp/common/ntripTrace.cpp b/src/cpp/common/ntripTrace.cpp index 2a93f1693..ee14eb9c8 100644 --- a/src/cpp/common/ntripTrace.cpp +++ b/src/cpp/common/ntripTrace.cpp @@ -1,217 +1,207 @@ - // #pragma GCC optimize ("O0") -#include -#include - -using bsoncxx::builder::basic::kvp; -using bsoncxx::types::b_date; - - +#include "common/ntripTrace.hpp" +#include #include #include +#include "common/acsConfig.hpp" +#include "common/common.hpp" +using std::string; using std::chrono::system_clock; using std::chrono::time_point; -using std::string; - -#include "ntripTrace.hpp" -#include "acsConfig.hpp" -#include "common.hpp" -void NetworkStatistics::onErrorStatistics( - const boost::system::error_code& err, - string operation) +void NetworkStatistics::onErrorStatistics(const boost::system::error_code& err, string operation) { - if (networkTraceFilename.empty()) - { - return; - } - - - std::ofstream fout(networkTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - connectCount++; - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("StreamName", streamName )); - doc.append(kvp("MessageType", "Error" )); - doc.append(kvp("BoostSysErrCode", err.value() )); - doc.append(kvp("Error", err.message() )); - doc.append(kvp("SocketOperation", operation )); - doc.append(kvp("Time", b_date {std::chrono::system_clock::now()} )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (networkTraceFilename.empty()) + { + return; + } + + std::ofstream fout(networkTraceFilename, std::ios::app); + if (!fout) + { + std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; + return; + } + + connectCount++; + + boost::json::object doc = {}; + doc["StreamName"] = streamName; + doc["MessageType"] = "Error"; + doc["BoostSysErrCode"] = err.value(); + doc["Error"] = err.message(); + doc["SocketOperation"] = operation; + doc["Time"] = timeGet().to_string(); + + fout << boost::json::serialize(doc) << "\n"; } void NetworkStatistics::onConnectedStatistics() { - if (networkTraceFilename.empty()) - { - return; - } - - std::ofstream fout(networkTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - connectCount++; - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("StreamName", streamName )); - doc.append(kvp("MessageType", "Connected" )); - doc.append(kvp("Time", b_date {std::chrono::system_clock::now()} )); - doc.append(kvp("ConnectCount", connectCount )); - doc.append(kvp("DisonnectCount", disconnectCount )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (networkTraceFilename.empty()) + { + return; + } + + std::ofstream fout(networkTraceFilename, std::ios::app); + if (!fout) + { + std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; + return; + } + + connectCount++; + + boost::json::object doc = {}; + doc["StreamName"] = streamName; + doc["MessageType"] = "Connected"; + doc["Time"] = timeGet().to_string(); + doc["ConnectCount"] = connectCount; + doc["DisonnectCount"] = disconnectCount; + + fout << boost::json::serialize(doc) << "\n"; } void NetworkStatistics::onDisconnectedStatistics() { - if (networkTraceFilename.empty()) - { - return; - } - - std::ofstream fout(networkTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - disconnectCount++; - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("StreamName", streamName )); - doc.append(kvp("MessageType", "Disconnected" )); - doc.append(kvp("Time", b_date {std::chrono::system_clock::now()} )); - doc.append(kvp("ConnectCount", connectCount )); - doc.append(kvp("DisonnectCount", disconnectCount )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (networkTraceFilename.empty()) + { + return; + } + + std::ofstream fout(networkTraceFilename, std::ios::app); + if (!fout) + { + std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; + return; + } + + disconnectCount++; + + boost::json::object doc = {}; + doc["StreamName"] = streamName; + doc["MessageType"] = "Disconnected"; + doc["Time"] = timeGet().to_string(); + doc["ConnectCount"] = connectCount; + doc["DisonnectCount"] = disconnectCount; + + fout << boost::json::serialize(doc) << "\n"; } void NetworkStatistics::onChunkSentStatistics() { - if (networkTraceFilename.empty()) - { - return; - } - - std::ofstream fout(networkTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - chunksSent++; - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("StreamName", streamName )); - doc.append(kvp("MessageType", "ChunkSent" )); - doc.append(kvp("Time", b_date {std::chrono::system_clock::now()} )); - doc.append(kvp("ChunksSent", chunksSent )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (networkTraceFilename.empty()) + { + return; + } + + std::ofstream fout(networkTraceFilename, std::ios::app); + if (!fout) + { + std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; + return; + } + + chunksSent++; + + boost::json::object doc = {}; + doc["StreamName"] = streamName; + doc["MessageType"] = "ChunkSent"; + doc["Time"] = timeGet().to_string(); + doc["ChunksSent"] = chunksSent; + + fout << boost::json::serialize(doc) << "\n"; } void NetworkStatistics::onChunkReceivedStatistics() { - if (networkTraceFilename.empty()) - { - return; - } - - if (traceLevel < 5) - return; - - std::ofstream fout(networkTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - chunksReceived++; - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("StreamName", streamName )); - doc.append(kvp("MessageType", "ChunkReceived" )); - doc.append(kvp("Time", b_date {std::chrono::system_clock::now()} )); - doc.append(kvp("ChunksReceived", chunksReceived )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (networkTraceFilename.empty()) + { + return; + } + + if (traceLevel < 5) + return; + + std::ofstream fout(networkTraceFilename, std::ios::app); + if (!fout) + { + std::cout << "Error opening " << networkTraceFilename << " in " << __FUNCTION__ << "\n"; + return; + } + + chunksReceived++; + + boost::json::object doc = {}; + doc["StreamName"] = streamName; + doc["MessageType"] = "ChunkReceived"; + doc["Time"] = timeGet().to_string(); + doc["ChunksReceived"] = chunksReceived; + + fout << boost::json::serialize(doc) << "\n"; } - -string NetworkStatistics::getNetworkStatistics( - GTime now, - string label) +string NetworkStatistics::getNetworkStatistics(GTime now, string label) { - bsoncxx::builder::basic::document doc = {}; - - doc.append(kvp("label", label)); - doc.append(kvp("Stream", streamName)); -// doc.append(kvp("Epoch", std::put_time(std::localtime(&now.time), "%F %X"))); -// doc.append(kvp("Start", std::put_time(std::localtime(&startTime.time), "%F %X"))); -// doc.append(kvp("Finish", std::put_time(std::localtime(&endTime.time), "%F %X"))); - doc.append(kvp("Network", acsConfig.analysis_agency)); - doc.append(kvp("Downloading", true)); - - double totalTime = (timeGet() - startTime).to_double(); - -// double connRatio; -// if (disconnectionCount == 0 -// && numberChunks > 0) -// { -// connRatio = 1; -// } -// else -// { -// if (totalTime.total_milliseconds() == 0) -// connRatio = 0; -// else -// connRatio = (double)connectedDuration.total_milliseconds() / (double)totalTime.total_milliseconds(); -// } -// -// -// double meanReconn = 0; -// if (disconnectionCount != 0) -// meanReconn = (double)disconnectedDuration.total_milliseconds() / (60.0 * 1000.0 * disconnectionCount); -// -// doc.append(kvp("Disconnects", disconnectionCount)); -// doc.append(kvp("MeanDowntime", meanReconn)); -// doc.append(kvp("ConnectedRatio", connRatio)); - -// double chunkRatio = 0; -// -// if (numberChunks != 0) -// chunkRatio = (double)numberErroredChunks / (double)(numberChunks); -// -// doc.append(kvp("Chunks", numberChunks)); -// doc.append(kvp("ChunkErrors", numberErroredChunks)); -// doc.append(kvp("ChunkErrorRatio", chunkRatio)); - - return bsoncxx::to_json(doc); + boost::json::object doc = {}; + + doc["label"] = label; + doc["Stream"] = streamName; + // doc.append(kvp("Epoch", std::put_time(std::localtime(&now.time), "%F + // %X"))); doc.append(kvp("Start", std::put_time(std::localtime(&startTime.time), + // "%F %X"))); doc.append(kvp("Finish", std::put_time(std::localtime(&endTime.time), + // "%F %X"))); + doc["Network"] = acsConfig.analysis_agency; + doc["Downloading"] = true; + + double totalTime = (timeGet() - startTime).to_double(); + + // double connRatio; + // if (disconnectionCount == 0 + // && numberChunks > 0) + // { + // connRatio = 1; + // } + // else + // { + // if (totalTime.total_milliseconds() == 0) + // connRatio = 0; + // else + // connRatio = (double)connectedDuration.total_milliseconds() / + // (double)totalTime.total_milliseconds(); + // } + // + // + // double meanReconn = 0; + // if (disconnectionCount != 0) + // meanReconn = (double)disconnectedDuration.total_milliseconds() / (60.0 * 1000.0 * + // disconnectionCount); + // + // doc["Disconnects"] = disconnectionCount; + // doc["MeanDowntime"] = meanReconn; + // doc["ConnectedRatio"] = connRatio; + + // double chunkRatio = 0; + // + // if (numberChunks != 0) + // chunkRatio = (double)numberErroredChunks / (double)(numberChunks); + // + // doc["Chunks"] = numberChunks; + // doc["ChunkErrors"] = numberErroredChunks; + // doc.append(kvp("ChunkErrorRatio", chunkRatio)); + + return boost::json::serialize(doc); } // void NetworkStatistics::printNetworkStatistics( // Trace& trace) // { // std::stringstream traceStr; -// traceStr << "Start : " << std::put_time(std::localtime(&startTime.time), "%F %X") << "\n"; -// traceStr << "Finish : " << std::put_time(std::localtime(&endTime.time), "%F %X") << "\n"; +// traceStr << "Start : " << std::put_time(std::localtime(&startTime.time), "%F %X") +// << "\n"; traceStr << "Finish : " << std::put_time(std::localtime(&endTime.time), +// "%F %X") << "\n"; // // double totalTime = timeGet() - startTime; // @@ -226,12 +216,14 @@ string NetworkStatistics::getNetworkStatistics( // if (totalTime.total_milliseconds() == 0) // connRatio = 0; // else -// connRatio = (double) connectedDuration.total_milliseconds() / (double)totalTime.total_milliseconds(); +// connRatio = (double) connectedDuration.total_milliseconds() / +// (double)totalTime.total_milliseconds(); // } // // double meanReconn = 0; // if (disconnectionCount != 0) -// meanReconn = (double)disconnectedDuration.total_milliseconds() / (60.0 * 1000.0 * disconnectionCount); +// meanReconn = (double)disconnectedDuration.total_milliseconds() / (60.0 * 1000.0 * +// disconnectionCount); // // traceStr << "Disconnects : " << disconnectionCount << "\n"; // traceStr << "MeanDowntime : " << meanReconn << "\n"; @@ -257,5 +249,3 @@ string NetworkStatistics::getNetworkStatistics( // while (std::getline(traceStr, messLine)) // tracepde(0, trace, (messLine + "\n").c_str()); // } - - diff --git a/src/cpp/common/ntripTrace.hpp b/src/cpp/common/ntripTrace.hpp index e9c9a2c57..74f691348 100644 --- a/src/cpp/common/ntripTrace.hpp +++ b/src/cpp/common/ntripTrace.hpp @@ -1,51 +1,43 @@ - #pragma once -#include "satSys.hpp" -#include "gTime.hpp" -#include "enums.h" - +#include +#include +#include #include #include #include -#include +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/satSys.hpp" using std::string; using std::vector; -#include -#include - struct NetworkStatistics { - string streamName; - GTime startTime; - GTime endTime; + string streamName; + GTime startTime; + GTime endTime; - string networkTraceFilename; + string networkTraceFilename; - int connectCount = 0; - int disconnectCount = 0; - int chunksSent = 0; - int chunksReceived = 0; + int connectCount = 0; + int disconnectCount = 0; + int chunksSent = 0; + int chunksReceived = 0; - boost::posix_time::time_duration connectedDuration = boost::posix_time::hours(0); - boost::posix_time::time_duration disconnectedDuration = boost::posix_time::hours(0); + boost::posix_time::time_duration connectedDuration = boost::posix_time::hours(0); + boost::posix_time::time_duration disconnectedDuration = boost::posix_time::hours(0); - string getNetworkStatistics( - GTime now, - string label); + string getNetworkStatistics(GTime now, string label); - void onConnectedStatistics(); + void onConnectedStatistics(); - void onDisconnectedStatistics(); + void onDisconnectedStatistics(); - void onChunkSentStatistics(); + void onChunkSentStatistics(); - void onChunkReceivedStatistics(); + void onChunkReceivedStatistics(); - void onErrorStatistics( - const boost::system::error_code& err, - string operation); + void onErrorStatistics(const boost::system::error_code& err, string operation); }; - diff --git a/src/cpp/common/observations.hpp b/src/cpp/common/observations.hpp index 9b309d446..a71005d90 100644 --- a/src/cpp/common/observations.hpp +++ b/src/cpp/common/observations.hpp @@ -1,28 +1,23 @@ - #pragma once -#include -#include -#include #include #include +#include +#include +#include +#include "3rdparty/slr/crd.h" +#include "common/algebra.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/satSys.hpp" +using std::list; using std::make_shared; +using std::map; using std::shared_ptr; -using std::vector; using std::string; -using std::list; -using std::map; - -#include "eigenIncluder.hpp" -#include "algebra.hpp" -#include "satSys.hpp" -#include "gTime.hpp" -#include "enums.h" -#include "crd.h" - - - +using std::vector; struct GObs; struct PObs; @@ -31,396 +26,374 @@ struct LObs; struct ObsMeta { - ObsMeta() : exclude(0) - { - - } - - union - { - const unsigned int exclude = 0; - struct - { - unsigned excludeElevation : 1; - unsigned excludeEclipse : 1; - unsigned excludeSystem : 1; - unsigned excludeOutlier : 1; - unsigned excludeBadSPP : 1; - unsigned excludeConfig : 1; - unsigned excludeSVH : 1; - unsigned excludeBadRange : 1; - unsigned excludeDataHandling : 1; - unsigned excludeCom : 1; - unsigned excludeBadFlags : 1; - unsigned excludeAlert : 1; - }; - }; + ObsMeta() : exclude(0) {} + + union + { + const unsigned int exclude = 0; + struct + { + unsigned excludeElevation : 1; + unsigned excludeEclipse : 1; + unsigned excludeSystem : 1; + unsigned excludeOutlier : 1; + unsigned excludeBadSPP : 1; + unsigned excludeConfig : 1; + unsigned excludeSVH : 1; + unsigned excludeBadRange : 1; + unsigned excludeDataHandling : 1; + unsigned excludeCom : 1; + unsigned excludeBadFlags : 1; + unsigned excludeAlert : 1; + }; + }; }; struct Observation : ObsMeta { - GTime time = {}; ///< Receiver sampling time (GPST) - string mount; ///< ID of the receiver that generated the observation - double ephVar = 0; + GTime time = {}; ///< Receiver sampling time (GPST) + string mount; ///< ID of the receiver that generated the observation + double ephVar = 0; - virtual ~Observation() = default; + virtual ~Observation() = default; -protected: - GObs* gObs_ptr = nullptr; - PObs* pObs_ptr = nullptr; - FObs* fObs_ptr = nullptr; - LObs* lObs_ptr = nullptr; + protected: + GObs* gObs_ptr = nullptr; + PObs* pObs_ptr = nullptr; + FObs* fObs_ptr = nullptr; + LObs* lObs_ptr = nullptr; }; -/** Raw observation data from a receiver for a single frequency. Not to be modified by processing functions -*/ +/** Raw observation data from a receiver for a single frequency. Not to be modified by processing + * functions + */ struct RawSig { - E_ObsCode code = E_ObsCode::NONE; ///< Reported code type - double L = 0; ///< Carrier phase (cycles) - double P = 0; ///< Pseudorange (meters) - double D = 0; ///< Doppler - bool LLI = false; ///< Loss of lock indicator - double snr = 0; ///< Signal to Noise ratio (dB-Hz) - - bool invalid = false; - - bool operator < (const RawSig& b) const - { - return (code < b.code); - } + E_ObsCode code = E_ObsCode::NONE; ///< Reported code type + double L = 0; ///< Carrier phase (cycles) + double P = 0; ///< Pseudorange (meters) + double D = 0; ///< Doppler + bool LLI = false; ///< Loss of lock indicator + double snr = 0; ///< Signal to Noise ratio (dB-Hz) + + bool invalid = false; // Eugene: not used? + + bool operator<(const RawSig& b) const { return (code < b.code); } }; /** Per signal data that is calculated from the raw signals. -*/ + */ struct Sig : RawSig { - Sig() - { - - } - - Sig(RawSig& raw) : RawSig(raw) - { - - } + Sig() {} - double codeVar = 0; ///< Variance of code measurement - double phasVar = 0; ///< Variance of phase measurement + Sig(RawSig& raw) : RawSig(raw) {} - double biases [NUM_MEAS_TYPES] = {std::nan("")}; - double biasVars[NUM_MEAS_TYPES] = {}; + double codeVar = 0; ///< Variance of code measurement + double phasVar = 0; ///< Variance of phase measurement }; - - struct IonoPP { - double latDeg = 0; - double lonDeg = 0; - double slantFactor = 1; + double latDeg = 0; + double lonDeg = 0; + double slantFactor = 1; }; struct IonoObs { - IonoObs() : ionExclude(0) - { - - } - - double stecToDelay; - int stecType = 0; - double stecVal; - double stecVar; - int stecCodeCombo; - - SatSys ionoSat; //todo aaron, remove when possible - - map ippMap; - - union - { - unsigned int ionExclude; - struct - { - unsigned ionExcludeElevation : 1; - unsigned ionExcludeCode : 1; - unsigned ionExcludeLC : 1; - unsigned ionExcludeRange : 1; - }; - }; + IonoObs() : ionExclude(0) {} + + double stecToDelay; + int stecType = 0; + double stecVal; + double stecVar; + int stecCodeCombo; + + SatSys ionoSat; // todo aaron, remove when possible + + map ippMap; + + union + { + unsigned int ionExclude; + struct + { + unsigned ionExcludeElevation : 1; + unsigned ionExcludeCode : 1; + unsigned ionExcludeLC : 1; + unsigned ionExcludeRange : 1; + }; + }; }; -//forward declarations for pointers below +// forward declarations for pointers below struct SatNav; struct SatStat; struct Receiver; - /** Observation metadata and data derived from it. -* All processing relevant for a single rec:sat:epoch should be stored here. -* For data that should persist across epochs: use SatStat. -**/ + * All processing relevant for a single rec:sat:epoch should be stored here. + * For data that should persist across epochs: use SatStat. + **/ struct GObsMeta : IonoObs { - Receiver* rec_ptr = nullptr; - - double sppCodeResidual = 0; ///< Residuals of code - double tropSlant = 0; ///< Troposphere slant delay - double tropSlantVar = 0; ///< Troposphere slant delay variance + Receiver* rec_ptr = nullptr; + double sppCodeResidual = 0; ///< Residuals of code + double tropSlant = 0; ///< Troposphere slant delay + double tropSlantVar = 0; ///< Troposphere slant delay variance }; /** Satellite position data - for determining and storing satellite positions/clocks -*/ + */ struct SatPos { - SatPos() : failure(0) - { - - } - - GTime posTime; - SatSys Sat = {}; ///> Satellite ID (system, prn) - SatNav* satNav_ptr = nullptr; ///< Pointer to a navigation object for this satellite - SatStat* satStat_ptr = nullptr; ///< Pointer to a status object for this satellite - - E_Source posSource = E_Source::NONE; - E_Source clkSource = E_Source::NONE; - - VectorEcef rSatCom; ///< ECEF based vector of satellite - VectorEcef rSatApc; ///< ECEF based vector of satellite - VectorEcef satVel; ///< ECEF based vector of satellite velocity - VectorEci rSatEciDt; ///< ECI based vector of satellite at transmission time - VectorEci vSatEciDt; ///< ECI based vector of satellite velocity at transmission time - VectorEci rSatEci0; ///< ECI based vector of satellite at nominal epoch - VectorEci vSatEci0; ///< ECI based vector of satellite velocity at nominal epoch - - double posVar = 0; ///< Variance of ephemeris derived values - - double satClk = 0; - double satClkVel = 0; - double satClkVar = 0; - - bool sppValid = 0; ///< Valid satellite flag - - int iodeClk = -1; ///< Issue of data ephemeris - int iodePos = -1; ///< Issue of data ephemeris - bool ephPosValid = false; - bool ephClkValid = false; - - double tof = 0; ///< Estimated time of flight - - union - { - const unsigned int failure = 0; - struct - { - unsigned failureExclude : 1; - unsigned failureNoSatPos : 1; - unsigned failureNoSatClock : 1; - unsigned failureNoPseudorange : 1; - unsigned failureIodeConsistency : 1; - unsigned failureBroadcastEph : 1; - unsigned failureSSRFail : 1; - unsigned failureSsrPosEmpty : 1; - unsigned failureSsrClkEmpty : 1; - unsigned failureSsrPosTime : 1; - unsigned failureSsrClkTime : 1; - unsigned failureSsrPosMag : 1; - unsigned failureSsrClkMag : 1; - unsigned failureSsrPosUdi : 1; - unsigned failureSsrClkUdi : 1; - unsigned failureGeodist : 1; - unsigned failureRSat : 1; - unsigned failureElevation : 1; - unsigned failurePrange : 1; - unsigned failureIonocorr : 1; - }; - }; + SatPos() : failure(0) {} + + GTime posTime; + SatSys Sat = {}; ///> Satellite ID (system, prn) + SatNav* satNav_ptr = nullptr; ///< Pointer to a navigation object for this satellite + SatStat* satStat_ptr = nullptr; ///< Pointer to a status object for this satellite + + E_Source posSource = E_Source::NONE; + E_Source clkSource = E_Source::NONE; + + VectorEcef rSatCom; ///< ECEF based vector of satellite + VectorEcef rSatApc; ///< ECEF based vector of satellite + VectorEcef satVel; ///< ECEF based vector of satellite velocity + VectorEci rSatEciDt; ///< ECI based vector of satellite at transmission time + VectorEci vSatEciDt; ///< ECI based vector of satellite velocity at transmission time + VectorEci rSatEci0; ///< ECI based vector of satellite at nominal epoch + VectorEci vSatEci0; ///< ECI based vector of satellite velocity at nominal epoch + + double posVar = 0; ///< Satellite position variances (m^2) + + double satClk = 0; ///< Satellite clock bias (s) + double satClkVel = 0; ///< Satellite clock rate (s/s) + double satClkVar = 0; ///< Satellite clock variance (s^2) + + bool sppValid = 0; ///< Valid satellite flag + + int iodeClk = -1; ///< Issue of data ephemeris + int iodePos = -1; ///< Issue of data ephemeris + bool ephPosValid = false; + bool ephClkValid = false; + + double tof = 0; ///< Estimated time of flight + + union + { + const unsigned int failure = 0; + struct + { + unsigned failureExclude : 1; + unsigned failureNoSatPos : 1; + unsigned failureNoSatClock : 1; + unsigned failureNoPseudorange : 1; + unsigned failureIodeConsistency : 1; + unsigned failureBroadcastEph : 1; + unsigned failureSSRFail : 1; + unsigned failureSsrPosEmpty : 1; + unsigned failureSsrClkEmpty : 1; + unsigned failureSsrPosTime : 1; + unsigned failureSsrClkTime : 1; + unsigned failureSsrPosMag : 1; + unsigned failureSsrClkMag : 1; + unsigned failureSsrPosUdi : 1; + unsigned failureSsrClkUdi : 1; + unsigned failureGeodist : 1; + unsigned failureRSat : 1; + unsigned failureElevation : 1; + unsigned failurePrange : 1; + }; + }; }; /** Raw observation data from a receiver. Not to be modified by processing functions -*/ + */ struct GObs : Observation, GObsMeta, SatPos { - map sigs; ///> Map of signals available in this observation (one per frequency only) - map> sigsLists; ///> Map of all signals available in this observation (may include multiple per frequency, eg L1X, L1C) + map + sigs; ///> Map of signals available in this observation (one per frequency only) + map> + sigsLists; ///> Map of all signals available in this observation (may include multiple per + /// frequency, eg L1X, L1C) + // Do not replace the list with a vector, it causes issues in rt - operator shared_ptr() - { - auto pointer = make_shared(*this); + operator shared_ptr() + { + auto pointer = make_shared(*this); - pointer->gObs_ptr = pointer.get(); + pointer->gObs_ptr = pointer.get(); - return pointer; - } + return pointer; + } - virtual ~GObs() = default; + virtual ~GObs() = default; }; +void obsVariance(GObs& obs); + struct PObs : Observation { - SatSys Sat = {}; ///> Satellite ID (system, prn) - Vector3d pos = Vector3d::Zero(); - Vector3d vel = Vector3d::Zero(); + SatSys Sat = {}; ///> Satellite ID (system, prn) + Vector3d pos = Vector3d::Zero(); + Vector3d vel = Vector3d::Zero(); + SatNav* satNav_ptr = nullptr; ///< Pointer to a navigation object for this satellite - operator shared_ptr() - { - auto pointer = make_shared(*this); + operator shared_ptr() + { + auto pointer = make_shared(*this); - pointer->pObs_ptr = pointer.get(); + pointer->pObs_ptr = pointer.get(); - return pointer; - } + return pointer; + } - virtual ~PObs() = default; + virtual ~PObs() = default; }; struct FObs : Observation { - KFState obsState; + KFState obsState; - operator shared_ptr() - { - auto pointer = make_shared(*this); + operator shared_ptr() + { + auto pointer = make_shared(*this); - pointer->fObs_ptr = pointer.get(); + pointer->fObs_ptr = pointer.get(); - return pointer; - } + return pointer; + } - virtual ~FObs() = default; + virtual ~FObs() = default; }; - /** List of observations for an epoch -*/ + */ struct ObsList : vector> { - ObsList& operator+=(const ObsList& right) - { - this->insert(this->end(), right.begin(), right.end()); - return *this; - } + ObsList& operator+=(const ObsList& right) + { + this->insert(this->end(), right.begin(), right.end()); + return *this; + } }; - //======================================================================================================== /* -#Mission Name SP3c Code ILRS ID NORAD Altitude [km] Inclination [deg] Tracking Status -# (PRN) From To - GPS-MET L02 9501703 23547 740 740 69.900 Off +#Mission Name SP3c Code ILRS ID NORAD Altitude [km] Inclination [deg] Tracking +Status # (PRN) From To GPS-MET L02 9501703 +23547 740 740 69.900 Off */ //======================================================================================================== struct SatIdentity { - string satName; // Mission Name - string satId; // SP3c Code (PRN) - int ilrsId; // ILRS ID - int noradId; // NORAD - double altitude[2]; // [km], 0: min, 1: max - double inclination; // [deg] - bool tracking; // Tracking Status + string satName; // Mission Name + string satId; // SP3c Code (PRN) + int ilrsId; // ILRS ID + int noradId; // NORAD + double altitude[2]; // [km], 0: min, 1: max + double inclination; // [deg] + bool tracking; // Tracking Status }; -void readSatId(string filepath); +void readSatId(string filepath); -extern map satIdMap;// index by ILRS ID +extern map satIdMap; // index by ILRS ID /** SLR normal point data from a session within a CRD file -*/ + */ struct CrdSession { - CrdH1 h1; - CrdH2 h2; - CrdH3 h3; - CrdH4 h4; - CrdH5 h5; - CrdC0 c0; - CrdC1 c1; - CrdC2 c2; - CrdC3 c3; - CrdC4 c4; - CrdC5 c5; - CrdC6 c6; - CrdC7 c7; - vector d10; - vector d11; - vector d12; - vector d20; - vector d21; - vector d30; - vector d40; - vector d41; - vector d42; - vector d50; - vector d60; - vector d00; - - union - { - unsigned int read = 0; - struct - { - unsigned readH1 : 1; - unsigned readH2 : 1; - unsigned readH3 : 1; - unsigned readH4 : 1; - unsigned readH5 : 1; - unsigned readC0 : 1; - unsigned readC1 : 1; - unsigned readC2 : 1; - unsigned readC3 : 1; - unsigned readC4 : 1; - unsigned readC5 : 1; - unsigned readC6 : 1; - unsigned readC7 : 1; - }; - }; + CrdH1 h1; + CrdH2 h2; + CrdH3 h3; + CrdH4 h4; + CrdH5 h5; + CrdC0 c0; + CrdC1 c1; + CrdC2 c2; + CrdC3 c3; + CrdC4 c4; + CrdC5 c5; + CrdC6 c6; + CrdC7 c7; + vector d10; + vector d11; + vector d12; + vector d20; + vector d21; + vector d30; + vector d40; + vector d41; + vector d42; + vector d50; + vector d60; + vector d00; + + union + { + unsigned int read = 0; + struct + { + unsigned readH1 : 1; + unsigned readH2 : 1; + unsigned readH3 : 1; + unsigned readH4 : 1; + unsigned readH5 : 1; + unsigned readC0 : 1; + unsigned readC1 : 1; + unsigned readC2 : 1; + unsigned readC3 : 1; + unsigned readC4 : 1; + unsigned readC5 : 1; + unsigned readC6 : 1; + unsigned readC7 : 1; + }; + }; }; - vector readCrdFile(string filepath); struct LObsMeta { - // Rec data - string recName = {}; - int recCdpId = 0; - - // Sat data - string satName = {}; - int ilrsId = 0; - string cosparId = {}; + // Rec data + string recName = {}; + int recCdpId = 0; + + // Sat data + string satName = {}; + int ilrsId = 0; + string cosparId = {}; }; struct LObs : Observation, LObsMeta, SatPos { - // Obs data - E_CrdEpochEvent epochEvent = E_CrdEpochEvent::NONE; - GTime timeTx = {}; - double twoWayTimeOfFlight = 0; + // Obs data + E_CrdEpochEvent epochEvent = E_CrdEpochEvent::NONE; + GTime timeTx = {}; + double twoWayTimeOfFlight = 0; - double pressure = 0; // hPa (mbar) - double temperature = 0; // K - double humidity = 0; // 0.00-1.00 - double wavelengthNm = 0; // nm + double pressure = 0; // hPa (mbar) + double temperature = 0; // K + double humidity = 0; // 0.00-1.00 + double wavelengthNm = 0; // nm - double rangeBias = 0; // m - double timeBias = 0; // s - double pressureBias = 0; // hPa (mbar) - double humidityBias = 0; // 0.00-1.00 + double rangeBias = 0; // m + double timeBias = 0; // s + double pressureBias = 0; // hPa (mbar) + double humidityBias = 0; // 0.00-1.00 - operator shared_ptr() - { - auto pointer = make_shared(*this); + operator shared_ptr() + { + auto pointer = make_shared(*this); - pointer->lObs_ptr = pointer.get(); + pointer->lObs_ptr = pointer.get(); - return pointer; - } + return pointer; + } - virtual ~LObs() = default; + virtual ~LObs() = default; }; - diff --git a/src/cpp/common/orbex.cpp b/src/cpp/common/orbex.cpp index 6c1f360f9..6628e922f 100644 --- a/src/cpp/common/orbex.cpp +++ b/src/cpp/common/orbex.cpp @@ -1,383 +1,377 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -FileType OBX__() -{ - -} - +#include +#include +#include #include #include +#include "architectureDocs.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/navigation.hpp" -using std::string; using std::ifstream; using std::ofstream; +using std::string; -#include -#include -#include - -#include "eigenIncluder.hpp" -#include "navigation.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "enums.h" - +FileType OBX__() {} /** Satellite code to satellite system -*/ + */ E_Sys code2sys(char code); /** Read and check the two header lines from an orbex file -*/ + */ int readOrbexHeader( - ifstream& fileStream, ///< Stream to read content from - double& ver) ///< ORBEX version + ifstream& fileStream, ///< Stream to read content from + double& ver ///< ORBEX version +) { - ver = 0; + ver = 0; - string line; + string line; - // first header line - std::getline(fileStream, line); + // first header line + std::getline(fileStream, line); - if (fileStream.eof()) - { - BOOST_LOG_TRIVIAL(error) << "Empty file"; - return 1; - } + if (fileStream.eof()) + { + BOOST_LOG_TRIVIAL(error) << "Empty file"; + return 1; + } - // verify document type - if (line.substr(0, 7) != "%=ORBEX") - { - BOOST_LOG_TRIVIAL(error) << "Not an Orbex file"; - return 2; - } + // verify document type + if (line.substr(0, 7) != "%=ORBEX") + { + BOOST_LOG_TRIVIAL(error) << "Not an Orbex file"; + return 2; + } - char* buff = &line[0]; - ver = str2num(buff, 8, 5); + char* buff = &line[0]; + ver = str2num(buff, 8, 5); - // second header line - std::getline(fileStream, line); + // second header line + std::getline(fileStream, line); - if (line.substr(0, 2) != "%%") - { - BOOST_LOG_TRIVIAL(error) << "Incorrect format"; - return 3; - } + if (line.substr(0, 2) != "%%") + { + BOOST_LOG_TRIVIAL(error) << "Incorrect format"; + return 3; + } - return 0; + return 0; } /** Read necessary information, e.g. time system & frame type, from the FILE/DESCRIPTION block -* Note: Only TIME_SYSTEM and FRAME_TYPE are read currently -*/ + * Note: Only TIME_SYSTEM and FRAME_TYPE are read currently + */ bool readOrbexFileDesc( - ifstream& fileStream, ///< Stream to read content from - E_TimeSys& tsys, ///< Time system - E_ObxFrame& frame) ///< Frame type + ifstream& fileStream, ///< Stream to read content from + E_TimeSys& tsys, ///< Time system + E_ObxFrame& frame ///< Frame type +) { - tsys = E_TimeSys::NONE; - frame = E_ObxFrame::OTHER; - - while (fileStream) - { - string line; - - getline(fileStream, line); - - if (line[0] == ' ') - { - vector split; - boost::trim(line); - boost::algorithm::split(split, line, boost::algorithm::is_space(), boost::token_compress_on); - - if (split[0] == "TIME_SYSTEM") - { - string timeSysStr = split[1]; - - if (timeSysStr == "GPS") tsys = E_TimeSys::GPST; - else if (timeSysStr == "UTC") tsys = E_TimeSys::UTC; - else if (timeSysStr == "TAI") tsys = E_TimeSys::TAI; - else if (timeSysStr == "GAL") tsys = E_TimeSys::GST; - else if (timeSysStr == "GLO") tsys = E_TimeSys::GLONASST; - else if (timeSysStr == "TT" ) tsys = E_TimeSys::TT; - else - { - BOOST_LOG_TRIVIAL(error) - << "Unknown Orbex time system: " << timeSysStr; - return false; - } - - // currently only GPST and UTC are supported - if ( tsys != +E_TimeSys::GPST - &&tsys != +E_TimeSys::UTC) - { - BOOST_LOG_TRIVIAL(error) - << "Unsupported time system: " << timeSysStr; - return false; - } - } - else if (split[0] == "FRAME_TYPE") - { - string frameTypeStr = split[1]; - - try - { - frame = E_ObxFrame::_from_string(frameTypeStr.c_str()); - } - catch (...) - { - BOOST_LOG_TRIVIAL(debug) - << "Unknown Orbex frame type: " << frameTypeStr; - } - - if ( frame != +E_ObxFrame::ECEF - &&frame != +E_ObxFrame::ECI) - { - BOOST_LOG_TRIVIAL(error) - << "Unsupported Orbex frame type: " << frameTypeStr; - return false; - } - } - } - else if (line[0] == '-') - { - // end of block - string closure = "-FILE/DESCRIPTION"; - if (line != closure) - { - BOOST_LOG_TRIVIAL(error) - << "Incorrect block closure line encountered: " - << line << " != " << closure; - return false; - } - else - { - return true; - } - } - } - - return false; + tsys = E_TimeSys::NONE; + frame = E_ObxFrame::OTHER; + + while (fileStream) + { + string line; + + getline(fileStream, line); + + if (line[0] == ' ') + { + vector split; + boost::trim(line); + boost::algorithm::split( + split, + line, + boost::algorithm::is_space(), + boost::token_compress_on + ); + + if (split[0] == "TIME_SYSTEM") + { + string timeSysStr = split[1]; + + if (timeSysStr == "GPS") + tsys = E_TimeSys::GPST; + else if (timeSysStr == "UTC") + tsys = E_TimeSys::UTC; + else if (timeSysStr == "TAI") + tsys = E_TimeSys::TAI; + else if (timeSysStr == "GAL") + tsys = E_TimeSys::GST; + else if (timeSysStr == "GLO") + tsys = E_TimeSys::GLONASST; + else if (timeSysStr == "TT") + tsys = E_TimeSys::TT; + else + { + BOOST_LOG_TRIVIAL(error) << "Unknown Orbex time system: " << timeSysStr; + return false; + } + + // currently only GPST and UTC are supported + if (tsys != E_TimeSys::GPST && tsys != E_TimeSys::UTC) + { + BOOST_LOG_TRIVIAL(error) << "Unsupported time system: " << timeSysStr; + return false; + } + } + else if (split[0] == "FRAME_TYPE") + { + string frameTypeStr = split[1]; + + try + { + frame = string_to_enum(frameTypeStr.c_str()); + } + catch (...) + { + BOOST_LOG_TRIVIAL(debug) << "Unknown Orbex frame type: " << frameTypeStr; + } + + if (frame != E_ObxFrame::ECEF && frame != E_ObxFrame::ECI) + { + BOOST_LOG_TRIVIAL(error) << "Unsupported Orbex frame type: " << frameTypeStr; + return false; + } + } + } + else if (line[0] == '-') + { + // end of block + string closure = "-FILE/DESCRIPTION"; + if (line != closure) + { + BOOST_LOG_TRIVIAL(error) + << "Incorrect block closure line encountered: " << line << " != " << closure; + return false; + } + else + { + return true; + } + } + } + + return false; } /** Read SATELLITE/ID_AND_DESCRIPTION block -* Note: This function is currently not fully implemented, satellite ID's are read from EPHEMERIS/DATA block -*/ -bool readOrbexSatId( - ifstream& fileStream) ///< Stream to read content from + * Note: This function is currently not fully implemented, satellite ID's are read from + * EPHEMERIS/DATA block + */ +bool readOrbexSatId(ifstream& fileStream) ///< Stream to read content from { - while (fileStream) - { - string line; - - getline(fileStream, line); - - if (line[0] == '-') - { - // end of block - string closure = "-SATELLITE/ID_AND_DESCRIPTION"; - if (line != closure) - { - BOOST_LOG_TRIVIAL(error) - << "Incorrect block closure line encountered: " - << line << " != " << closure; - return false; - } - else - { - return true; - } - } - } - - return false; + while (fileStream) + { + string line; + + getline(fileStream, line); + + if (line[0] == '-') + { + // end of block + string closure = "-SATELLITE/ID_AND_DESCRIPTION"; + if (line != closure) + { + BOOST_LOG_TRIVIAL(error) + << "Incorrect block closure line encountered: " << line << " != " << closure; + return false; + } + else + { + return true; + } + } + } + + return false; } /** Read EPHEMERIS/DATA block -* Note: Only ATT record type is supported currently -*/ + * Note: Only ATT record type is supported currently + */ bool readOrbexEph( - ifstream& fileStream, ///< Stream to read content from - Navigation& nav, ///< Navigation data - E_TimeSys tsys = E_TimeSys::GPST, ///< Time system - E_ObxFrame frame = E_ObxFrame::ECEF) ///< Frame type + ifstream& fileStream, ///< Stream to read content from + Navigation& nav, ///< Navigation data + E_TimeSys tsys = E_TimeSys::GPST, ///< Time system + E_ObxFrame frame = E_ObxFrame::ECEF ///< Frame type +) { - GTime time = {}; - int nsat = 0; - - static int index = 0; // keep track of file number - index++; - - while (fileStream) - { - string line; - - getline(fileStream, line); - - char* buff = &line[0]; - - if ( line[0] == '#' - &&line[1] == '#') - { - bool error = str2time(buff, 3, 32, time, tsys); - if (error) - { - BOOST_LOG_TRIVIAL(error) - << "Invalid epoch line in Orbex file: " << line; - return false; - } - - nsat = str2num(buff, 36, 3); - - index++; - } - else if (line[0] == ' ') - { - if (nsat == 0) - { - BOOST_LOG_TRIVIAL(error) - << "Epoch line invalid or not found before data records"; - return false; - } - - string recType = line.substr(1,3); - if (recType == "ATT") - { - string id = line.substr(5); - id = id.substr(0, id.find(' ')); - - Att att = {}; - att.time = time; - att.index = index; - att.id = id; - att.frame = frame; - - int nRec = (int)str2num(buff, 22, 1); - if (nRec != 4) - { - BOOST_LOG_TRIVIAL(error) - << "Invalid number of data columns: " << nRec; - return false; - } - - double val[4]; - int found = sscanf(buff+24, "%lf %lf %lf %lf", &val[0], &val[1], &val[2], &val[3]); - - if (found < 4) - { - continue; - } - - att.q.w() = val[0]; - att.q.x() = val[1]; - att.q.y() = val[2]; - att.q.z() = val[3]; - - if (abs(att.q.norm() - 1) > 1E-6) - { - BOOST_LOG_TRIVIAL(warning) - << "The quaternion is not approximately unit norm"; - continue; - } - - att.q.normalize(); - - nav.attMapMap[att.id][att.time] = att; - } - // other record types to be added here, e.g. - /* - else if (recType == "PCS") - { - ... - } - */ - else - { - BOOST_LOG_TRIVIAL(error) - << "Unsupported record type: " << recType; - return false; - } - } - else if (line[0] == '-') - { - // end of block - string closure = "-EPHEMERIS/DATA"; - if (line != closure) - { - BOOST_LOG_TRIVIAL(error) - << "Incorrect block closure line encountered: " - << line << " != " << closure; - return false; - } - else - { - return true; - } - } - } - - return false; + GTime time = {}; + int nsat = 0; + + static int index = 0; // keep track of file number + index++; + + while (fileStream) + { + string line; + + getline(fileStream, line); + + char* buff = &line[0]; + + if (line[0] == '#' && line[1] == '#') + { + bool error = str2time(buff, 3, 32, time, tsys); + if (error) + { + BOOST_LOG_TRIVIAL(error) << "Invalid epoch line in Orbex file: " << line; + return false; + } + + nsat = str2num(buff, 36, 3); + + index++; + } + else if (line[0] == ' ') + { + if (nsat == 0) + { + BOOST_LOG_TRIVIAL(error) << "Epoch line invalid or not found before data records"; + return false; + } + + string recType = line.substr(1, 3); + if (recType == "ATT") + { + string id = line.substr(5); + id = id.substr(0, id.find(' ')); + + Att att = {}; + att.time = time; + att.index = index; + att.id = id; + att.frame = frame; + + int nRec = (int)str2num(buff, 22, 1); + if (nRec != 4) + { + BOOST_LOG_TRIVIAL(error) << "Invalid number of data columns: " << nRec; + return false; + } + + double val[4]; + int found = + sscanf(buff + 24, "%lf %lf %lf %lf", &val[0], &val[1], &val[2], &val[3]); + + if (found < 4) + { + continue; + } + + att.q.w() = val[0]; + att.q.x() = val[1]; + att.q.y() = val[2]; + att.q.z() = val[3]; + + if (abs(att.q.norm() - 1) > 1E-6) + { + BOOST_LOG_TRIVIAL(warning) << "The quaternion is not approximately unit norm"; + continue; + } + + att.q.normalize(); + + nav.attMapMap[att.id][att.time] = att; + } + // other record types to be added here, e.g. + /* + else if (recType == "PCS") + { + ... + } + */ + else + { + BOOST_LOG_TRIVIAL(error) << "Unsupported record type: " << recType; + return false; + } + } + else if (line[0] == '-') + { + // end of block + string closure = "-EPHEMERIS/DATA"; + if (line != closure) + { + BOOST_LOG_TRIVIAL(error) + << "Incorrect block closure line encountered: " << line << " != " << closure; + return false; + } + else + { + return true; + } + } + } + + return false; } /** Read an ORBEX file into navigation data struct -*/ -void readOrbex( - string filepath, ///< File path to output file - Navigation& nav) ///< Navigation data + */ +void readOrbex( + string filepath, ///< File path to output file + Navigation& nav ///< Navigation data +) { - ifstream fileStream(filepath); - if (!fileStream) - { - BOOST_LOG_TRIVIAL(error) - << "Orbex file open error " << filepath; - - return; - } - - // header lines - each ORBEX file must begin with the two header lines - double ver; - int failure = readOrbexHeader(fileStream, ver); - - if (failure) - { - BOOST_LOG_TRIVIAL(error) - << "Error reading Orbex header lines"; - return; - } - - E_TimeSys tsys = E_TimeSys::NONE; - E_ObxFrame frame = E_ObxFrame::OTHER; - - while (fileStream) - { - string line; - getline(fileStream, line); - line = line.substr(0, line.find(' ')); - if (line.back() == '\r') - line.pop_back(); - bool pass = true; - - if (!fileStream) - { - // unexpected end of file - BOOST_LOG_TRIVIAL(error) - << "Closure line not found before end of Orbex file " << filepath; - break; - } - else if (line == "+FILE/DESCRIPTION") pass = readOrbexFileDesc(fileStream, tsys, frame ); - else if (line == "+SATELLITE/ID_AND_DESCRIPTION") pass = readOrbexSatId (fileStream ); - else if (line == "+EPHEMERIS/DATA") pass = readOrbexEph (fileStream, nav, tsys, frame ); - else if (line == "%END_ORBEX") return; // end of file - - if (pass == false) - { - BOOST_LOG_TRIVIAL(error) - << "Error reading Orbex " << line << " block"; - return; - } - } + ifstream fileStream(filepath); + if (!fileStream) + { + BOOST_LOG_TRIVIAL(error) << "Orbex file open error " << filepath; + + return; + } + + // header lines - each ORBEX file must begin with the two header lines + double ver; + int failure = readOrbexHeader(fileStream, ver); + + if (failure) + { + BOOST_LOG_TRIVIAL(error) << "Error reading Orbex header lines"; + return; + } + + E_TimeSys tsys = E_TimeSys::NONE; + E_ObxFrame frame = E_ObxFrame::OTHER; + + while (fileStream) + { + string line; + getline(fileStream, line); + line = line.substr(0, line.find(' ')); + if (line.back() == '\r') + line.pop_back(); + bool pass = true; + + if (!fileStream) + { + // unexpected end of file + BOOST_LOG_TRIVIAL(error) + << "Closure line not found before end of Orbex file " << filepath; + break; + } + else if (line == "+FILE/DESCRIPTION") + pass = readOrbexFileDesc(fileStream, tsys, frame); + else if (line == "+SATELLITE/ID_AND_DESCRIPTION") + pass = readOrbexSatId(fileStream); + else if (line == "+EPHEMERIS/DATA") + pass = readOrbexEph(fileStream, nav, tsys, frame); + else if (line == "%END_ORBEX") + return; // end of file + + if (pass == false) + { + BOOST_LOG_TRIVIAL(error) << "Error reading Orbex " << line << " block"; + return; + } + } } diff --git a/src/cpp/common/orbexWrite.cpp b/src/cpp/common/orbexWrite.cpp index 2026cdf36..b09a46add 100644 --- a/src/cpp/common/orbexWrite.cpp +++ b/src/cpp/common/orbexWrite.cpp @@ -1,432 +1,600 @@ - // #pragma GCC optimize ("O0") - +#include "common/orbexWrite.hpp" #include - - -#include "eigenIncluder.hpp" -#include "navigation.hpp" -#include "orbexWrite.hpp" -#include "ephemeris.hpp" -#include "acsConfig.hpp" -#include "enums.h" -#include "ppp.hpp" - -#define ORBEX_VER 0.09 -#define NO_PV_STD 99999.9 -#define NO_CLK 9999999.9999999 -#define NO_CLK_STD 9999999.999 +#include "common/acsConfig.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/ephemeris.hpp" +#include "common/navigation.hpp" +#include "pea/ppp.hpp" + +constexpr double ORBEX_VER = 0.09; +constexpr double NO_PV_STD = 99999.9; +constexpr double NO_CLK = 9999999.9999999; +constexpr double NO_CLK_STD = 9999999.999; /** ORBEX entry to write out -*/ + */ struct OrbexEntry { - SatSys Sat = {}; ///< Satellite ID - Vector3d pos = Vector3d::Zero(); ///< Satellite position (m) - Vector3d vel = Vector3d::Zero(); ///< Satellite velocity (m/s) - double clk = NO_CLK; ///< Satellite clock (microsecond) & clock-rate (nanosecond/s) - double clkVel = NO_CLK; - Vector3d posStd = Vector3d::Ones() * NO_PV_STD; ///< Satellite position std (mm) - Vector3d velStd = Vector3d::Ones() * NO_PV_STD; ///< Satellite velocity std (micrometer/s) - double clkStd = NO_CLK_STD; ///< Satellite clock (picosecond) - double clkVelStd = NO_CLK_STD; ///< Satellite clock-rate std (femtosecond/s) - Quaterniond q = {0, 0, 0, 0}; ///< Satellite attitude in quaternions - - // other record types, e.g. correlation coefficients, to be added in the future + SatSys Sat = {}; ///< Satellite ID + Vector3d pos = Vector3d::Zero(); ///< Satellite position (m) + Vector3d vel = Vector3d::Zero(); ///< Satellite velocity (m/s) + double clk = NO_CLK; ///< Satellite clock (microsecond) & clock-rate (nanosecond/s) + double clkVel = NO_CLK; + Vector3d posStd = Vector3d::Ones() * NO_PV_STD; ///< Satellite position std (mm) + Vector3d velStd = Vector3d::Ones() * NO_PV_STD; ///< Satellite velocity std (micrometer/s) + double clkStd = NO_CLK_STD; ///< Satellite clock (picosecond) + double clkVelStd = NO_CLK_STD; ///< Satellite clock-rate std (femtosecond/s) + Quaterniond q = {0, 0, 0, 0}; ///< Satellite attitude in quaternions + + // other record types, e.g. correlation coefficients, to be added in the future }; -OrbexFileData orbexCombinedFileData; ///< Combined file editing information for ORBEX writing +OrbexFileData orbexCombinedFileData; ///< Combined file editing information for ORBEX writing -/** Write ORBEX header lines and header blocks including FILE/DESCRIPTION and SATELLITE/ID_AND_DESCRIPTION block -*/ +/** Write ORBEX header lines and header blocks including FILE/DESCRIPTION and + * SATELLITE/ID_AND_DESCRIPTION block + */ void writeOrbexHeader( - std::fstream& orbexStream, ///< Output stream - GTime time, ///< Epoch time (GPST) - map& outSys, ///< Systems to include in file - OrbexFileData& outFileDat) ///< File editing information for ORBEX writing + std::fstream& orbexStream, ///< Output stream + GTime time, ///< Epoch time (GPST) + map& outSys, ///< Systems to include in file + OrbexFileData& outFileDat ///< File editing information for ORBEX writing +) { - GEpoch ep = time; - - tracepdeex(0, orbexStream, "%%=ORBEX %5.2f\n", ORBEX_VER); - tracepdeex(0, orbexStream, "%%%%\n"); - - tracepdeex(0, orbexStream, "+FILE/DESCRIPTION\n"); - tracepdeex(0, orbexStream, " DESCRIPTION %s\n", "Satellite attitude quaternions"); - tracepdeex(0, orbexStream, " CREATED_BY %s %s\n", acsConfig.analysis_agency.c_str(), acsConfig.analysis_software.c_str()); - tracepdeex(0, orbexStream, " CREATION_DATE %s\n", timeGet().to_string(0).c_str()); - tracepdeex(0, orbexStream, " INPUT_DATA %s\n", ""); - tracepdeex(0, orbexStream, " CONTACT %s\n", "npi@ga.gov.au"); - tracepdeex(0, orbexStream, " TIME_SYSTEM %s\n", "GPS"); - tracepdeex(0, orbexStream, " START_TIME %4.0f %2.0f %2.0f %2.0f %2.0f %15.12f\n", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - outFileDat.headerTimePos = orbexStream.tellp(); - - tracepdeex(0, orbexStream, " END_TIME %4.0f %2.0f %2.0f %2.0f %2.0f %15.12f\n", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - tracepdeex(0, orbexStream, " EPOCH_INTERVAL %9.3f\n", acsConfig.orbex_output_interval); - tracepdeex(0, orbexStream, " COORD_SYSTEM %s\n", "IGS14"); - tracepdeex(0, orbexStream, " FRAME_TYPE %s\n", "ECEF"); - tracepdeex(0, orbexStream, " ORBIT_TYPE %s\n", ""); - tracepdeex(0, orbexStream, " LIST_OF_REC_TYPES "); - for (auto& recType : acsConfig.orbex_record_types) - { - tracepdeex(0, orbexStream, " %s", recType._to_string()); - } - tracepdeex(0, orbexStream, "\n"); - - tracepdeex(0, orbexStream, "-FILE/DESCRIPTION\n"); - - for (auto sys : {E_Sys::GPS, E_Sys::GLO, E_Sys::GAL, E_Sys::BDS}) - { - if (outSys[sys]) - for (auto Sat : getSysSats(sys)) - { - outFileDat.satList[Sat] = false; - } - } - - Block block(orbexStream, "SATELLITE/ID_AND_DESCRIPTION"); - - outFileDat.satListPos = orbexStream.tellp(); - - for (auto& [sat, dummy] : outFileDat.satList) - { - tracepdeex(0, orbexStream, "* \n"); // placeholders for satellite list - } - - // optional blocks to be added in the future (not yet available in existing ORBEX files) + GEpoch ep = time; + + tracepdeex(0, orbexStream, "%%=ORBEX %5.2f\n", ORBEX_VER); + tracepdeex(0, orbexStream, "%%%%\n"); + + tracepdeex(0, orbexStream, "+FILE/DESCRIPTION\n"); + tracepdeex(0, orbexStream, " DESCRIPTION %s\n", "Satellite attitude quaternions"); + tracepdeex( + 0, + orbexStream, + " CREATED_BY %s %s\n", + acsConfig.analysis_agency.c_str(), + acsConfig.analysis_software.c_str() + ); + tracepdeex(0, orbexStream, " CREATION_DATE %s\n", timeGet().to_string(0).c_str()); + tracepdeex(0, orbexStream, " INPUT_DATA %s\n", ""); + tracepdeex(0, orbexStream, " CONTACT %s\n", "npi@ga.gov.au"); + tracepdeex(0, orbexStream, " TIME_SYSTEM %s\n", "GPS"); + tracepdeex( + 0, + orbexStream, + " START_TIME %4.0f %2.0f %2.0f %2.0f %2.0f %15.12f\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + outFileDat.headerTimePos = orbexStream.tellp(); + + tracepdeex( + 0, + orbexStream, + " END_TIME %4.0f %2.0f %2.0f %2.0f %2.0f %15.12f\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + tracepdeex(0, orbexStream, " EPOCH_INTERVAL %9.3f\n", acsConfig.orbex_output_interval); + tracepdeex(0, orbexStream, " COORD_SYSTEM %s\n", acsConfig.reference_system); + tracepdeex(0, orbexStream, " FRAME_TYPE %s\n", "ECEF"); + tracepdeex(0, orbexStream, " ORBIT_TYPE %s\n", ""); + tracepdeex(0, orbexStream, " LIST_OF_REC_TYPES "); + for (auto& recType : acsConfig.orbex_record_types) + { + tracepdeex(0, orbexStream, " %s", enum_to_string(recType)); + } + tracepdeex(0, orbexStream, "\n"); + + tracepdeex(0, orbexStream, "-FILE/DESCRIPTION\n"); + + for (auto sys : {E_Sys::GPS, E_Sys::GLO, E_Sys::GAL, E_Sys::BDS}) + { + if (outSys[sys]) + for (auto Sat : getSysSats(sys)) + { + outFileDat.satList[Sat] = false; + } + } + + Block block(orbexStream, "SATELLITE/ID_AND_DESCRIPTION"); + + outFileDat.satListPos = orbexStream.tellp(); + + for (auto& [sat, dummy] : outFileDat.satList) + { + tracepdeex(0, orbexStream, "* \n"); // placeholders for satellite list + } + + // optional blocks to be added in the future (not yet available in existing ORBEX files) } /** Write PCS or VCS records -*/ + */ bool writePVCS( - std::fstream& orbexStream, ///< Output stream - OrbexEntry& entry, ///< ORBEX entry to write out - E_OrbexRecord recType) ///< Record type + std::fstream& orbexStream, ///< Output stream + OrbexEntry& entry, ///< ORBEX entry to write out + E_OrbexRecord recType ///< Record type +) { - Vector3d pv = Vector3d::Zero(); // satellite position or velocity - Vector3d pvStd = Vector3d::Zero(); // satellite position or velocity std - double clk = 0; // satellite clock or clock-rate - double clkStd = 0; // satellite clock or clock-rate std - - if (recType == +E_OrbexRecord::PCS) { pv = entry.pos; pvStd = entry.posStd; clk = entry.clk; clkStd = entry.clkStd; } - else if (recType == +E_OrbexRecord::VCS) { pv = entry.vel; pvStd = entry.velStd; clk = entry.clkVel; clkStd = entry.clkVelStd; } - else - { - return false; - } - - if (pv.isZero()) - return false; // skip if no position entries - - if (fabs(clk) >= NO_CLK) clk = NO_CLK; - if (pvStd.x() >= NO_PV_STD) pvStd.x() = NO_PV_STD; - if (pvStd.y() >= NO_PV_STD) pvStd.y() = NO_PV_STD; - if (pvStd.z() >= NO_PV_STD) pvStd.z() = NO_PV_STD; - if (clkStd >= NO_CLK_STD) clkStd = NO_CLK_STD; - - - // satellite flags not available at the moment - int nRec = 8; - tracepdeex(0, orbexStream, " %s %s %d %16.4f %16.4f %16.4f %16.7f %7.1f %7.1f %7.1f %11.3f\n", - recType, - entry.Sat.id().c_str(), - nRec, - pv.x(), - pv.y(), - pv.z(), - clk, - pvStd.x(), - pvStd.y(), - pvStd.z(), - clkStd); - - return true; + Vector3d pv = Vector3d::Zero(); // satellite position or velocity + Vector3d pvStd = Vector3d::Zero(); // satellite position or velocity std + double clk = 0; // satellite clock or clock-rate + double clkStd = 0; // satellite clock or clock-rate std + + if (recType == E_OrbexRecord::PCS) + { + pv = entry.pos; + pvStd = entry.posStd; + clk = entry.clk; + clkStd = entry.clkStd; + } + else if (recType == E_OrbexRecord::VCS) + { + pv = entry.vel; + pvStd = entry.velStd; + clk = entry.clkVel; + clkStd = entry.clkVelStd; + } + else + { + return false; + } + + if (pv.isZero()) + return false; // skip if no position entries + + if (fabs(clk) >= NO_CLK) + clk = NO_CLK; + if (pvStd.x() >= NO_PV_STD) + pvStd.x() = NO_PV_STD; + if (pvStd.y() >= NO_PV_STD) + pvStd.y() = NO_PV_STD; + if (pvStd.z() >= NO_PV_STD) + pvStd.z() = NO_PV_STD; + if (clkStd >= NO_CLK_STD) + clkStd = NO_CLK_STD; + + // satellite flags not available at the moment + int nRec = 8; + tracepdeex( + 0, + orbexStream, + " %s %s %d %16.4f %16.4f %16.4f %16.7f %7.1f %7.1f %7.1f %11.3f\n", + recType, + entry.Sat.id().c_str(), + nRec, + pv.x(), + pv.y(), + pv.z(), + clk, + pvStd.x(), + pvStd.y(), + pvStd.z(), + clkStd + ); + + return true; } /** Write POS or VEL records -*/ + */ bool writePV( - std::fstream& orbexStream, ///< Output stream - OrbexEntry& entry, ///< ORBEX entry to write out - E_OrbexRecord recType) ///< Record type + std::fstream& orbexStream, ///< Output stream + OrbexEntry& entry, ///< ORBEX entry to write out + E_OrbexRecord recType ///< Record type +) { - Vector3d pv = Vector3d::Zero(); // satellite position or velocity - - if (recType == +E_OrbexRecord::POS) pv = entry.pos; - else if (recType == +E_OrbexRecord::VEL) pv = entry.vel; - else - { - return false; - } - - if (pv.isZero()) - { - return false; // skip if no position entries - } - - int nRec = 3; - tracepdeex(0, orbexStream, " %s %s %d %16.4f %16.4f %16.4f\n", - recType, - entry.Sat.id().c_str(), - nRec, - pv.x(), - pv.y(), - pv.z()); - - return true; + Vector3d pv = Vector3d::Zero(); // satellite position or velocity + + if (recType == E_OrbexRecord::POS) + pv = entry.pos; + else if (recType == E_OrbexRecord::VEL) + pv = entry.vel; + else + { + return false; + } + + if (pv.isZero()) + { + return false; // skip if no position entries + } + + int nRec = 3; + tracepdeex( + 0, + orbexStream, + " %s %s %d %16.4f %16.4f %16.4f\n", + recType, + entry.Sat.id().c_str(), + nRec, + pv.x(), + pv.y(), + pv.z() + ); + + return true; } /** Write CLK or CRT records -*/ + */ bool writeClk( - std::fstream& orbexStream, ///< Output stream - OrbexEntry& entry, ///< ORBEX entry to write out - E_OrbexRecord recType) ///< Record type + std::fstream& orbexStream, ///< Output stream + OrbexEntry& entry, ///< ORBEX entry to write out + E_OrbexRecord recType ///< Record type +) { - double clk = 0; // satellite clock or clock-rate - - if (recType == +E_OrbexRecord::CLK) clk = entry.clk; - else if (recType == +E_OrbexRecord::CRT) clk = entry.clkVel; - else - { - return false; - } - - if (fabs(clk) > NO_CLK) - return false; - - int nRec = 1; - tracepdeex(0, orbexStream, " %s %s %d %16.7f\n", - recType, - entry.Sat.id().c_str(), - nRec, - clk); - - return true; + double clk = 0; // satellite clock or clock-rate + + if (recType == E_OrbexRecord::CLK) + clk = entry.clk; + else if (recType == E_OrbexRecord::CRT) + clk = entry.clkVel; + else + { + return false; + } + + if (fabs(clk) > NO_CLK) + return false; + + int nRec = 1; + tracepdeex( + 0, + orbexStream, + " %s %s %d %16.7f\n", + recType, + entry.Sat.id().c_str(), + nRec, + clk + ); + + return true; } /** Write ATT records -*/ + */ bool writeAtt( - std::fstream& orbexStream, ///< Output stream - OrbexEntry& entry, ///< ORBEX entry to write out - E_OrbexRecord recType) ///< Record type + std::fstream& orbexStream, ///< Output stream + OrbexEntry& entry, ///< ORBEX entry to write out + E_OrbexRecord recType ///< Record type +) { - if (recType != +E_OrbexRecord::ATT) return false; - if (entry.q.norm() == 0) return false; - - int nRec = 4; - tracepdeex(0, orbexStream, " %s %s %d %19.16f %19.16f %19.16f %19.16f\n", - recType, - entry.Sat.id().c_str(), - nRec, - entry.q.w(), - entry.q.x(), - entry.q.y(), - entry.q.z()); - - return true; + if (recType != E_OrbexRecord::ATT) + return false; + if (entry.q.norm() == 0) + return false; + + int nRec = 4; + tracepdeex( + 0, + orbexStream, + " %s %s %d %19.16f %19.16f %19.16f %19.16f\n", + recType, + entry.Sat.id().c_str(), + nRec, + entry.q.w(), + entry.q.x(), + entry.q.y(), + entry.q.z() + ); + + return true; } /** Write EPHEMERIS/DATA block and update END_TIME line -*/ + */ void updateOrbexBody( - string& filename, ///< File path to output file - map& entryList, ///< List of data to print - GTime time, ///< Epoch time (GPST) - map& outSys, ///< Systems to include in file - OrbexFileData& outFileDat) ///< Current file editing information + string& filename, ///< File path to output file + map& entryList, ///< List of data to print + GTime time, ///< Epoch time (GPST) + map& outSys, ///< Systems to include in file + OrbexFileData& outFileDat ///< Current file editing information +) { - GEpoch ep = time; - - // first create if non existing - { - std::fstream maker(filename, std::ios::app); - } - std::fstream orbexStream(filename); - - if (!orbexStream) - { - BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for Orbex file."; - return; - } - - orbexStream.seekp(0, std::ios::end); - - long endFilePos = orbexStream.tellp(); - - if (endFilePos == 0) - { - writeOrbexHeader(orbexStream, time, outSys, outFileDat); - - tracepdeex(0, orbexStream, "+EPHEMERIS/DATA\n"); - tracepdeex(0, orbexStream, "*PCS ID_ FLAGS_ N __X______(m)____ ______Y__(m)____ ______Z___(m)___ _SVCLK___(usec)_ _STD_X_ _STD_Y_ _STD_Z_ ____STD_CLK\n"); - tracepdeex(0, orbexStream, "*VCS ID_ FLAGS_ N __VX_____(m/s)__ ______VY_(m/s)__ ______VZ__(m/s)_ _CLKRATE_(ns/s)_ _STD_VX _STD_VY _STD_VZ ____STD_CLK\n"); - tracepdeex(0, orbexStream, "*POS ID_ FLAGS_ N __X______(m)____ ______Y__(m)____ ______Z___(m)___\n"); - tracepdeex(0, orbexStream, "*VEL ID_ FLAGS_ N __VX_____(m/s)__ ______VY_(m/s)__ ______VZ__(m/s)_\n"); - tracepdeex(0, orbexStream, "*CLK ID_ FLAGS_ N __SVCLK__(usec)_\n"); - tracepdeex(0, orbexStream, "*CRT ID_ FLAGS_ N __SVCLKR_(ns/s)_\n"); - tracepdeex(0, orbexStream, "*ATT ID_ FLAGS_ N __q0_______________ ___q1______________ ____q2_____________ ____q3_____________\n"); - tracepdeex(0, orbexStream, "*ATT RECORDS: TRANSFORMATION FROM TERRESTRIAL FRAME COORDINATES (T) TO SAT. BODY FRAME ONES (B) SUCH AS\n"); - tracepdeex(0, orbexStream, "* (0,B) = q.(0,T).trans(q)\n"); - - //tracepdeex(0, orbexStream, "*CPC ID_ FLAGS_ N ______xy_corr____ ______xz_corr____ ______xc_corr____ ______yz_corr____ ______yc_corr____ ______zc_corr____\n"); - //tracepdeex(0, orbexStream, "*CVC ID_ FLAGS_ N ____vxvy_corr____ ____vxvz_corr____ ____vxvc_corr____ ____vyvz_corr____ ____vyvc_corr____ ____vzvc_corr____\n"); - } - else - { - orbexStream.seekp(outFileDat.headerTimePos); - - tracepdeex(0, orbexStream, " END_TIME %4.0f %2.0f %2.0f %2.0f %2.0f %15.12f\n", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - orbexStream.seekp(outFileDat.endDataPos); - } - - tracepdeex(0, orbexStream, "## %4.0f %2.0f %2.0f %2.0f %2.0f %15.12f", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - long numSatPos = orbexStream.tellp(); - - int nsat = 0; - tracepdeex(0, orbexStream, " %3d\n", nsat); - - for (auto& [sat, isIncluded] : outFileDat.satList) - { - auto it = entryList.find(sat); - if (it == entryList.end()) - continue; - - auto& [key, entry] = *it; - isIncluded = false; - for (auto& recType : acsConfig.orbex_record_types) - switch (recType) - { - case E_OrbexRecord::PCS: { isIncluded |= writePVCS (orbexStream, entry, recType); break; } - case E_OrbexRecord::VCS: { isIncluded |= writePVCS (orbexStream, entry, recType); break; } - case E_OrbexRecord::CPC: { break; } // to be added if needed in the future - case E_OrbexRecord::CVC: { break; } // to be added if needed in the future - case E_OrbexRecord::POS: { isIncluded |= writePV (orbexStream, entry, recType); break; } - case E_OrbexRecord::VEL: { isIncluded |= writePV (orbexStream, entry, recType); break; } - case E_OrbexRecord::CLK: { isIncluded |= writeClk (orbexStream, entry, recType); break; } - case E_OrbexRecord::CRT: { isIncluded |= writeClk (orbexStream, entry, recType); break; } - case E_OrbexRecord::ATT: { isIncluded |= writeAtt (orbexStream, entry, recType); break; } - } - - if (isIncluded) - { - nsat++; - } - } - - outFileDat.endDataPos = orbexStream.tellp(); - - tracepdeex(0, orbexStream, "-EPHEMERIS/DATA\n"); - tracepdeex(0, orbexStream, "%%END_ORBEX\n"); - - orbexStream.seekp(numSatPos); - - tracepdeex(0, orbexStream, " %3d\n", nsat); - - orbexStream.seekp(outFileDat.satListPos); - - for (auto& [sat, isIncluded] : outFileDat.satList) - { - if (isIncluded) - { - tracepdeex(0, orbexStream, " %3s\n", sat.id().c_str()); - } - } + GEpoch ep = time; + + // first create if non existing + { + std::fstream maker(filename, std::ios::app); + } + std::fstream orbexStream(filename); + + if (!orbexStream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for Orbex file."; + return; + } + + orbexStream.seekp(0, std::ios::end); + + long endFilePos = orbexStream.tellp(); + + if (endFilePos == 0) + { + writeOrbexHeader(orbexStream, time, outSys, outFileDat); + + tracepdeex(0, orbexStream, "+EPHEMERIS/DATA\n"); + tracepdeex( + 0, + orbexStream, + "*PCS ID_ FLAGS_ N __X______(m)____ ______Y__(m)____ ______Z___(m)___ " + "_SVCLK___(usec)_ _STD_X_ " + "_STD_Y_ _STD_Z_ ____STD_CLK\n" + ); + tracepdeex( + 0, + orbexStream, + "*VCS ID_ FLAGS_ N __VX_____(m/s)__ ______VY_(m/s)__ ______VZ__(m/s)_ " + "_CLKRATE_(ns/s)_ _STD_VX " + "_STD_VY _STD_VZ ____STD_CLK\n" + ); + tracepdeex( + 0, + orbexStream, + "*POS ID_ FLAGS_ N __X______(m)____ ______Y__(m)____ ______Z___(m)___\n" + ); + tracepdeex( + 0, + orbexStream, + "*VEL ID_ FLAGS_ N __VX_____(m/s)__ ______VY_(m/s)__ ______VZ__(m/s)_\n" + ); + tracepdeex(0, orbexStream, "*CLK ID_ FLAGS_ N __SVCLK__(usec)_\n"); + tracepdeex(0, orbexStream, "*CRT ID_ FLAGS_ N __SVCLKR_(ns/s)_\n"); + tracepdeex( + 0, + orbexStream, + "*ATT ID_ FLAGS_ N __q0_______________ ___q1______________ ____q2_____________ " + "____q3_____________\n" + ); + tracepdeex( + 0, + orbexStream, + "*ATT RECORDS: TRANSFORMATION FROM TERRESTRIAL FRAME COORDINATES (T) TO SAT. BODY " + "FRAME ONES (B) SUCH AS\n" + ); + tracepdeex(0, orbexStream, "* (0,B) = q.(0,T).trans(q)\n"); + + // tracepdeex(0, orbexStream, "*CPC ID_ FLAGS_ N ______xy_corr____ ______xz_corr____ + // ______xc_corr____ + // ______yz_corr____ ______yc_corr____ ______zc_corr____\n"); tracepdeex(0, orbexStream, + // "*CVC ID_ FLAGS_ N + // ____vxvy_corr____ ____vxvz_corr____ ____vxvc_corr____ ____vyvz_corr____ ____vyvc_corr____ + // ____vzvc_corr____\n"); + } + else + { + orbexStream.seekp(outFileDat.headerTimePos); + + tracepdeex( + 0, + orbexStream, + " END_TIME %4.0f %2.0f %2.0f %2.0f %2.0f %15.12f\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + orbexStream.seekp(outFileDat.endDataPos); + } + + tracepdeex( + 0, + orbexStream, + "## %4.0f %2.0f %2.0f %2.0f %2.0f %15.12f", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + long numSatPos = orbexStream.tellp(); + + int nsat = 0; + tracepdeex(0, orbexStream, " %3d\n", nsat); + + for (auto& [sat, isIncluded] : outFileDat.satList) + { + auto it = entryList.find(sat); + if (it == entryList.end()) + continue; + + auto& [key, entry] = *it; + isIncluded = false; + for (auto& recType : acsConfig.orbex_record_types) + switch (recType) + { + case E_OrbexRecord::PCS: + { + isIncluded |= writePVCS(orbexStream, entry, recType); + break; + } + case E_OrbexRecord::VCS: + { + isIncluded |= writePVCS(orbexStream, entry, recType); + break; + } + case E_OrbexRecord::CPC: + { + break; + } // to be added if needed in the future + case E_OrbexRecord::CVC: + { + break; + } // to be added if needed in the future + case E_OrbexRecord::POS: + { + isIncluded |= writePV(orbexStream, entry, recType); + break; + } + case E_OrbexRecord::VEL: + { + isIncluded |= writePV(orbexStream, entry, recType); + break; + } + case E_OrbexRecord::CLK: + { + isIncluded |= writeClk(orbexStream, entry, recType); + break; + } + case E_OrbexRecord::CRT: + { + isIncluded |= writeClk(orbexStream, entry, recType); + break; + } + case E_OrbexRecord::ATT: + { + isIncluded |= writeAtt(orbexStream, entry, recType); + break; + } + } + + if (isIncluded) + { + nsat++; + } + } + + outFileDat.endDataPos = orbexStream.tellp(); + + tracepdeex(0, orbexStream, "-EPHEMERIS/DATA\n"); + tracepdeex(0, orbexStream, "%%END_ORBEX\n"); + + orbexStream.seekp(numSatPos); + + tracepdeex(0, orbexStream, " %3d\n", nsat); + + orbexStream.seekp(outFileDat.satListPos); + + for (auto& [sat, isIncluded] : outFileDat.satList) + { + if (isIncluded) + { + tracepdeex(0, orbexStream, " %3s\n", sat.id().c_str()); + } + } } -/** Retrieve satellite orbits, clocks and attitudes for all included systems and write out to an ORBEX file -*/ +/** Retrieve satellite orbits, clocks and attitudes for all included systems and write out to an + * ORBEX file + */ void writeSysSetOrbex( - string filename, ///< File path to output file - GTime time, ///< Epoch time (GPST) - map& outSys, ///< Systems to include in file - OrbexFileData& outFileDat, ///< Current file editing information - vector orbDataSrcs, ///< Data source for satellite positions & velocities - vector clkDataSrcs, ///< Data source for satellite clocks - vector attDataSrcs, ///< Data source for satellite attitudes - KFState* kfState_ptr) ///< Pointer to a kalman filter to take values from + string filename, ///< File path to output file + GTime time, ///< Epoch time (GPST) + map& outSys, ///< Systems to include in file + OrbexFileData& outFileDat, ///< Current file editing information + vector orbDataSrcs, ///< Data source for satellite positions & velocities + vector clkDataSrcs, ///< Data source for satellite clocks + vector attDataSrcs, ///< Data source for satellite attitudes + KFState& kfState ///< kalman filter to take values from +) { - map entryList; - - for (auto& [Sat, satNav] : nav.satNavMap) - { - if (outSys[Sat.sys] == false) - continue; - - OrbexEntry entry; - entry.Sat = Sat; - - // Create a dummy observation - GObs obs; - obs.Sat = Sat; - obs.time = time; - obs.satNav_ptr = &nav.satNavMap[Sat]; - - GTime teph = time; - - // satellite orbit - position and velocity + satellite clock (for PCS and VCS record only) - bool orbPass = true; - orbPass &= satclk(nullStream, time, time, obs, clkDataSrcs, nav, kfState_ptr); - orbPass &= satpos(nullStream, time, time, obs, orbDataSrcs, E_OffsetType::COM, nav, kfState_ptr); - - if (orbPass) - { - entry.pos = obs.rSatCom; - entry.vel = obs.satVel; - entry.clk = obs.satClk * 1E6; // microsecond - entry.clkVel = obs.satClkVel * 1E9; // nanosecond - } - - // satellite attitude - bool attPass = false; - Quaterniond quat; - if (orbPass) - { - updateSatYaw(obs, obs.satNav_ptr->attStatus); - attPass = satQuat(obs, attDataSrcs, quat); - } - - if (attPass) - { - if (quat.w() < 0) // convention to have +ve w - { - quat.w() *= -1; - quat.x() *= -1; - quat.y() *= -1; - quat.z() *= -1; - } - - entry.q = quat.conjugate(); // satQuat() returns transformation from body to ECEF, but Orbex req's ECEF to body - i.e. quat.conjugate() - } - - if ( orbPass - ||attPass) - { - entryList[Sat] = entry; - } - } - - updateOrbexBody(filename, entryList, time, outSys, outFileDat); + map entryList; + + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (outSys[Sat.sys] == false) + continue; + + OrbexEntry entry; + entry.Sat = Sat; + + // Create a dummy observation + GObs obs; + obs.Sat = Sat; + obs.time = time; + obs.satNav_ptr = &nav.satNavMap[Sat]; + + GTime teph = time; + + // satellite orbit - position and velocity + satellite clock (for PCS and VCS record only) + bool orbPass = true; + orbPass &= satclk(nullStream, time, time, obs, clkDataSrcs, nav, &kfState); + orbPass &= + satpos(nullStream, time, time, obs, orbDataSrcs, E_OffsetType::COM, nav, &kfState); + + if (orbPass) + { + entry.pos = obs.rSatCom; + entry.vel = obs.satVel; + entry.clk = obs.satClk * 1E6; // microsecond + entry.clkVel = obs.satClkVel * 1E9; // nanosecond + } + + // satellite attitude + bool attPass = false; + Quaterniond quat; + if (orbPass) + { + updateSatYaw(obs, obs.satNav_ptr->attStatus); + attPass = satQuat(obs, attDataSrcs, quat); + } + + if (attPass) + { + if (quat.w() < 0) // convention to have +ve w + { + quat.w() *= -1; + quat.x() *= -1; + quat.y() *= -1; + quat.z() *= -1; + } + + entry.q = quat.conjugate(); // satQuat() returns transformation from body to ECEF, but + // Orbex req's ECEF to body - i.e. quat.conjugate() + } + + if (orbPass || attPass) + { + entryList[Sat] = entry; + } + } + + updateOrbexBody(filename, entryList, time, outSys, outFileDat); } /** Output ORBEX files -*/ + */ void outputOrbex( - string filename, ///< File to write to - GTime time, ///< Epoch time (GPST) - vector orbDataSrcs, ///< Data source for satellite positions & velocities - vector clkDataSrcs, ///< Data source for satellite clocks - vector attDataSrcs, ///< Data source for satellite attitudes - KFState* kfState_ptr) ///< Pointer to a kalman filter to take values from + string filename, ///< File to write to + GTime time, ///< Epoch time (GPST) + KFState& kfState, ///< Kalman filter to take values from + vector orbDataSrcs, ///< Data source for satellite positions & velocities + vector clkDataSrcs, ///< Data source for satellite clocks + vector attDataSrcs ///< Data source for satellite attitudes +) { - auto sysFilenames = getSysOutputFilenames(filename, time); - - for (auto [filename, sysMap] : sysFilenames) - { - writeSysSetOrbex(filename, time, sysMap, orbexCombinedFileData, orbDataSrcs, clkDataSrcs, attDataSrcs, kfState_ptr); - } + auto sysFilenames = getSysOutputFilenames(filename, time); + + for (auto [filename, sysMap] : sysFilenames) + { + writeSysSetOrbex( + filename, + time, + sysMap, + orbexCombinedFileData, + orbDataSrcs, + clkDataSrcs, + attDataSrcs, + kfState + ); + } } diff --git a/src/cpp/common/orbexWrite.hpp b/src/cpp/common/orbexWrite.hpp index e60ac43aa..3b3a09a6c 100644 --- a/src/cpp/common/orbexWrite.hpp +++ b/src/cpp/common/orbexWrite.hpp @@ -1,42 +1,31 @@ #pragma once -#include #include +#include +#include "common/rinexObsWrite.hpp" +#include "enums.h" -using std::string; using std::map; - -#include "rinexObsWrite.hpp" +using std::string; struct GTime; -class E_Source; struct KFState; /** File editing information for ORBEX writing -*/ + */ struct OrbexFileData { - map satList; ///< List of satellites included in the ORBEX file - long headerTimePos = 0; ///< Position of put pointer for END_TIME line - long satListPos = 0; ///< Position of put pointer for beginning of satellite list - long endDataPos = 0; ///< Position of put pointer for end of EPHEMERIS/DATA block + map satList; ///< List of satellites included in the ORBEX file + long headerTimePos = 0; ///< Position of put pointer for END_TIME line + long satListPos = 0; ///< Position of put pointer for beginning of satellite list + long endDataPos = 0; ///< Position of put pointer for end of EPHEMERIS/DATA block }; -void writeSysSetOrbex( - string filename, - GTime time, - map& outSys, - OrbexFileData& outFileDat, - vector orbDataSrcs, - vector clkDataSrcs, - vector attDataSrcs, - KFState* kfState_ptr = nullptr); - void outputOrbex( - string filename, - GTime time, - vector orbDataSrcs, - vector clkDataSrcs, - vector attDataSrcs, - KFState* kfState_ptr = nullptr); - + string filename, + GTime time, + KFState& kfState, + vector orbDataSrcs, + vector clkDataSrcs, + vector attDataSrcs +); diff --git a/src/cpp/common/orbits.cpp b/src/cpp/common/orbits.cpp index 4088001d0..918049ba3 100644 --- a/src/cpp/common/orbits.cpp +++ b/src/cpp/common/orbits.cpp @@ -1,470 +1,467 @@ - // #pragma GCC optimize ("O0") +///< max number of iteration of Kelpler +#include "common/orbits.hpp" #include -#include -#include #include -#include -#include #include +#include +#include +#include +#include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/ephPrecise.hpp" +#include "common/navigation.hpp" +#include "common/satSys.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/orbitProp.hpp" +#include "pea/peaCommitStrings.hpp" +using std::string; using std::chrono::system_clock; using std::chrono::time_point; -using std::string; - - -#include "peaCommitStrings.hpp" -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "ephPrecise.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "orbitProp.hpp" -#include "algebra.hpp" -#include "orbits.hpp" -#include "satSys.hpp" -#include "common.hpp" -#include "trace.hpp" -#include "enums.h" +constexpr double RTOL_KEPLER = 1E-14; ///< relative tolerance for Kepler equation +constexpr int MAX_ITER_KEPLER = 30; -#define RTOL_KEPLER 1E-14 ///< relative tolerance for Kepler equation -#define MAX_ITER_KEPLER 30 ///< max number of iteration of Kelpler - - -bool inertial2Keplers( - Trace& trace, - const VectorEci& r, - const VectorEci& v, - Vector6d& keplers) +bool inertial2Keplers(Trace& trace, const VectorEci& r, const VectorEci& v, Vector6d& keplers) { - Vector3d e_r = r.normalized(); - - //Calculate orbital momentum vector (perpendicular to both position and velocity) - Vector3d L = r.cross(v); - - //Obtain the eccentricity vector - Vector3d e = v.cross(L) / GM_Earth - e_r; - - double L_x = L(0); - double L_y = L(1); - double L_z = L(2); - - L.normalize(); - - //Determine the vector n pointing towards the ascending node (point on arc crossing xy plane?) - Vector3d n0 = Vector3d(0,0,1).cross(L); - if (n0.norm() < 0.0001) - { - trace << "\n fixingKKKKKKK"; - n0 = Vector3d(1,0,0); - } - n0.normalize(); - - //get another handy vector - Vector3d n1 = L.cross(n0).normalized(); - - double e_X = e.dot(n0); - double e_Y = e.dot(n1); - - //Determine the orbit eccentricity, which is simply the magnitude of the eccentricity vector e, - double e_ = e.norm(); - - if (e_ < 0.000001) - { - //if its too small, point it at a valid place and use a normalised version of it. - e = n0; - trace << "\n fixingBBB"; - } - e.normalize(); - - //Determine the true anomaly (angle between e and r) - double nu; - if (r.dot(v) >= 0) nu = + acos(e.dot(e_r)); - else nu = 2*PI - acos(e.dot(e_r)); - - //Determine the eccentric anomaly - double E = 2 * atan2( sqrt(1-e_) * sin(nu/2), - sqrt(1+e_) * cos(nu/2)); - - - //Compute the mean anomaly with help of Kepler’s Equation from the eccentric anomaly E and the eccentricity e - double M = E - e_ * sin(E); - - bool error = false; - if (isnan(nu)) { std::cout << "nu is nan\n"; error = true; } - if (isnan(e_)) { std::cout << "e_ is nan\n"; error = true; } - if (isnan(E)) { std::cout << "E is nan\n"; error = true; } - if (isnan(M)) { std::cout << "M is nan\n"; error = true; } - - if (error) - { - std::cout << "\n" << "n0 " << n0.transpose(); - std::cout << "\te " << e.transpose(); - std::cout << "\tn1 " << n1.transpose(); - std::cout << "\tnu " << nu; - std::cout << "\tE " << E; - std::cout << "\tM " << M; - - return false; - } - - keplers(KEPLER::LX) = L_x; - keplers(KEPLER::LY) = L_y; - keplers(KEPLER::LZ) = L_z; - keplers(KEPLER::EU) = e_X; - keplers(KEPLER::EV) = e_Y; - keplers(KEPLER::M ) = M; - - return true; + Vector3d e_r = r.normalized(); + + // Calculate orbital momentum vector (perpendicular to both position and velocity) + Vector3d L = r.cross(v); + + // Obtain the eccentricity vector + Vector3d e = v.cross(L) / GM_Earth - e_r; + + double L_x = L(0); + double L_y = L(1); + double L_z = L(2); + + L.normalize(); + + // Determine the vector n pointing towards the ascending node (point on arc crossing xy plane?) + Vector3d n0 = Vector3d(0, 0, 1).cross(L); + if (n0.norm() < 0.0001) + { + trace << "\n fixingKKKKKKK"; + n0 = Vector3d(1, 0, 0); + } + n0.normalize(); + + // get another handy vector + Vector3d n1 = L.cross(n0).normalized(); + + double e_X = e.dot(n0); + double e_Y = e.dot(n1); + + // Determine the orbit eccentricity, which is simply the magnitude of the eccentricity vector e, + double e_ = e.norm(); + + if (e_ < 0.000001) + { + // if its too small, point it at a valid place and use a normalised version of it. + e = n0; + trace << "\n fixingBBB"; + } + e.normalize(); + + // Determine the true anomaly (angle between e and r) + double nu; + if (r.dot(v) >= 0) + nu = +acos(e.dot(e_r)); + else + nu = 2 * PI - acos(e.dot(e_r)); + + // Determine the eccentric anomaly + double E = 2 * atan2(sqrt(1 - e_) * sin(nu / 2), sqrt(1 + e_) * cos(nu / 2)); + + // Compute the mean anomaly with help of Kepler’s Equation from the eccentric anomaly E and the + // eccentricity e + double M = E - e_ * sin(E); + + bool error = false; + if (isnan(nu)) + { + std::cout << "nu is nan\n"; + error = true; + } + if (isnan(e_)) + { + std::cout << "e_ is nan\n"; + error = true; + } + if (isnan(E)) + { + std::cout << "E is nan\n"; + error = true; + } + if (isnan(M)) + { + std::cout << "M is nan\n"; + error = true; + } + + if (error) + { + std::cout << "\n" + << "n0 " << n0.transpose(); + std::cout << "\te " << e.transpose(); + std::cout << "\tn1 " << n1.transpose(); + std::cout << "\tnu " << nu; + std::cout << "\tE " << E; + std::cout << "\tM " << M; + + return false; + } + + keplers(static_cast(KEPLER::LX)) = L_x; + keplers(static_cast(KEPLER::LY)) = L_y; + keplers(static_cast(KEPLER::LZ)) = L_z; + keplers(static_cast(KEPLER::EU)) = e_X; + keplers(static_cast(KEPLER::EV)) = e_Y; + keplers(static_cast(KEPLER::M)) = M; + + return true; } - -VectorEci keplers2Inertial( - Trace& trace, - const Vector6d& keplers0) +VectorEci keplers2Inertial(Trace& trace, const Vector6d& keplers0) { - Vector3d L = Vector3d::Zero(); - Vector2d eee = Vector2d::Zero(); - - L[0] = keplers0[KEPLER::LX]; - L[1] = keplers0[KEPLER::LY]; - L[2] = keplers0[KEPLER::LZ]; - eee[0] = keplers0[KEPLER::EU]; - eee[1] = keplers0[KEPLER::EV]; - double M = keplers0[KEPLER::M ]; - - - double e_ = eee.norm(); - - - double E = M; - double Eprev = 0; - int n; - for (n = 0; n < MAX_ITER_KEPLER; n++) - { -// std::cout << "\nE: " << n << " " << E << " " << Eprev; - Eprev = E; - E -= (E - e_ * sin(E) - M) / (1 - e_ * cos(E)); - - if (fabs(E - Eprev) < RTOL_KEPLER) - { - break; - } - } - - if (n >= MAX_ITER_KEPLER) - { - std::cout << "iteratios"; - - return VectorEci(); - } - - - double nu = 2 * atan2( sqrt(1 + e_) * sin(E/2), - sqrt(1 - e_) * cos(E/2)); - - double r = L.squaredNorm() / GM_Earth / (1 + e_ * cos(nu)); - - L.normalize(); - - //Determine the vector n pointing towards the ascending node (point on arc crossing xy plane?) - Vector3d n0 = Vector3d(0,0,1).cross(L); - if (n0.norm() < 0.0001) - { - trace << "\n fixingKKKKKKK"; - n0 = Vector3d(1,0,0); - } - n0.normalize(); - - - //get another handy vector - Vector3d n1 = L.cross(n0).normalized(); - - Vector3d e = eee[0] * n0 - + eee[1] * n1; - - if (e.norm() < 0.000001) - { - //if its too small, point it at a valid place and use a normalised version of it. - e = n0; - trace << "\n fixingBBB"; - } - e.normalize(); - - double x = r * cos(nu); - double y = r * sin(nu); - - double cos_w = n0.dot(e); - double cos_i = L(2); - - double cos_W = n0(0); - double sin_W = n0(1); //dont unify, needs sign - - if (cos_w > +1) cos_w = +1; - else if (cos_w < -1) cos_w = -1; - if (cos_i > +1) cos_i = +1; - else if (cos_i < -1) cos_i = -1; - - - double sin_w = sqrt(1 - SQR(cos_w)); - double sin_i = sqrt(1 - SQR(cos_i)); - - if (n0.dot(e.cross(L)) < 0) - { - sin_w *= -1; - } - -// std::cout << "\tcosw " << cos_w; -// std::cout << "\tcosi " << cos_i; -// std::cout << "\tcosW " << cos_W; -// std::cout << "\tsinw " << sin_w; -// std::cout << "\tsini " << sin_i; -// std::cout << "\tsinW " << sin_W; - - if (isnan(nu)) { std::cout << "nu is NAN\n"; } - if (isnan(r)) { std::cout << "r is NAN\n"; } - if (isnan(sin_w)) { std::cout << "sin_w is NAN\n"; } - if (isnan(sin_i)) { std::cout << "sin_i is NAN\n"; } - if (isnan(sin_W)) { std::cout << "sin_W is NAN\n"; } - - VectorEci rSat; - rSat.x() = x * (cos_w * cos_W - sin_w * cos_i * sin_W ) - y * (cos_w * cos_i * sin_W + sin_w * cos_W); - rSat.y() = x * (cos_w * sin_W + sin_w * cos_i * cos_W ) + y * (cos_w * cos_i * cos_W - sin_w * sin_W); - rSat.z() = x * ( + sin_w * sin_i ) + y * (cos_w * sin_i); - - return rSat; -// std::cout << "\te " << e.transpose(); -// std::cout << "\n" << "n0 " << n0.transpose(); -// std::cout << "\tn1 " << n1.transpose(); - -// std::cout << "\tnu " << nu; - -// std::cout << "\tE " << E; -// std::cout << "\tM " << M; - -// double A = r / (1 - e_ * cos(E)); -// -// dM = sqrt(GM_Earth /A/A/A); + Vector3d L = Vector3d::Zero(); + Vector2d eee = Vector2d::Zero(); + + L[0] = keplers0[static_cast(KEPLER::LX)]; + L[1] = keplers0[static_cast(KEPLER::LY)]; + L[2] = keplers0[static_cast(KEPLER::LZ)]; + eee[0] = keplers0[static_cast(KEPLER::EU)]; + eee[1] = keplers0[static_cast(KEPLER::EV)]; + double M = keplers0[static_cast(KEPLER::M)]; + + double e_ = eee.norm(); + + double E = M; + double Eprev = 0; + int n; + for (n = 0; n < MAX_ITER_KEPLER; n++) + { + // std::cout << "\nE: " << n << " " << E << " " << Eprev; + Eprev = E; + E -= (E - e_ * sin(E) - M) / (1 - e_ * cos(E)); + + if (fabs(E - Eprev) < RTOL_KEPLER) + { + break; + } + } + + if (n >= MAX_ITER_KEPLER) + { + std::cout << "iteratios"; + + return VectorEci(); + } + + double nu = 2 * atan2(sqrt(1 + e_) * sin(E / 2), sqrt(1 - e_) * cos(E / 2)); + + double r = L.squaredNorm() / GM_Earth / (1 + e_ * cos(nu)); + + L.normalize(); + + // Determine the vector n pointing towards the ascending node (point on arc crossing xy plane?) + Vector3d n0 = Vector3d(0, 0, 1).cross(L); + if (n0.norm() < 0.0001) + { + trace << "\n fixingKKKKKKK"; + n0 = Vector3d(1, 0, 0); + } + n0.normalize(); + + // get another handy vector + Vector3d n1 = L.cross(n0).normalized(); + + Vector3d e = eee[0] * n0 + eee[1] * n1; + + if (e.norm() < 0.000001) + { + // if its too small, point it at a valid place and use a normalised version of it. + e = n0; + trace << "\n fixingBBB"; + } + e.normalize(); + + double x = r * cos(nu); + double y = r * sin(nu); + + double cos_w = n0.dot(e); + double cos_i = L(2); + + double cos_W = n0(0); + double sin_W = n0(1); // dont unify, needs sign + + if (cos_w > +1) + cos_w = +1; + else if (cos_w < -1) + cos_w = -1; + if (cos_i > +1) + cos_i = +1; + else if (cos_i < -1) + cos_i = -1; + + double sin_w = sqrt(1 - SQR(cos_w)); + double sin_i = sqrt(1 - SQR(cos_i)); + + if (n0.dot(e.cross(L)) < 0) + { + sin_w *= -1; + } + + // std::cout << "\tcosw " << cos_w; + // std::cout << "\tcosi " << cos_i; + // std::cout << "\tcosW " << cos_W; + // std::cout << "\tsinw " << sin_w; + // std::cout << "\tsini " << sin_i; + // std::cout << "\tsinW " << sin_W; + + if (isnan(nu)) + { + std::cout << "nu is NAN\n"; + } + if (isnan(r)) + { + std::cout << "r is NAN\n"; + } + if (isnan(sin_w)) + { + std::cout << "sin_w is NAN\n"; + } + if (isnan(sin_i)) + { + std::cout << "sin_i is NAN\n"; + } + if (isnan(sin_W)) + { + std::cout << "sin_W is NAN\n"; + } + + VectorEci rSat; + rSat.x() = + x * (cos_w * cos_W - sin_w * cos_i * sin_W) - y * (cos_w * cos_i * sin_W + sin_w * cos_W); + rSat.y() = + x * (cos_w * sin_W + sin_w * cos_i * cos_W) + y * (cos_w * cos_i * cos_W - sin_w * sin_W); + rSat.z() = x * (+sin_w * sin_i) + y * (cos_w * sin_i); + + return rSat; + // std::cout << "\te " << e.transpose(); + // std::cout << "\n" << "n0 " << n0.transpose(); + // std::cout << "\tn1 " << n1.transpose(); + + // std::cout << "\tnu " << nu; + + // std::cout << "\tE " << E; + // std::cout << "\tM " << M; + + // double A = r / (1 - e_ * cos(E)); + // + // dM = sqrt(GM_Earth /A/A/A); } -void getKeplerPartials( - Trace& trace, - VectorXd& keplers0, - MatrixXd& partials) +void getKeplerPartials(Trace& trace, VectorXd& keplers0, MatrixXd& partials) { - partials = MatrixXd::Zero(3, 6); + partials = MatrixXd::Zero(3, 6); - double deltas[6] = - { - 1, 1, 1, 0.00001, 0.00001, PI/180/1000 - }; + double deltas[6] = {1, 1, 1, 0.00001, 0.00001, PI / 180 / 1000}; - //get base position - VectorEci pos0 = keplers2Inertial(trace, keplers0); + // get base position + VectorEci pos0 = keplers2Inertial(trace, keplers0); - for (int i = 0; i < 6; i++) - { - VectorXd keplers1 = keplers0; + for (int i = 0; i < 6; i++) + { + VectorXd keplers1 = keplers0; - keplers1[i] += deltas[i]; + keplers1[i] += deltas[i]; - VectorEci pos1 = keplers2Inertial(trace, keplers1); + VectorEci pos1 = keplers2Inertial(trace, keplers1); - partials.col(i) = (pos1 - pos0) / deltas[i]; - } + partials.col(i) = (pos1 - pos0) / deltas[i]; + } } - -void getKeplerInversePartials( - Trace& trace, - VectorEci& pos, - VectorEci& vel, - MatrixXd& partials) +void getKeplerInversePartials(Trace& trace, VectorEci& pos, VectorEci& vel, MatrixXd& partials) { - double deltas[6] = - { - 20000, 20000, 20000, 10, 10, 10 - }; + double deltas[6] = {20000, 20000, 20000, 10, 10, 10}; - partials = MatrixXd::Zero(6, 6); + partials = MatrixXd::Zero(6, 6); - //get base keplers - Vector6d keplers0; - bool pass = inertial2Keplers(std::cout, pos, vel, keplers0); + // get base keplers + Vector6d keplers0; + bool pass = inertial2Keplers(std::cout, pos, vel, keplers0); - for (int i = 0; i < 6; i++) - { - VectorEci pos1 = pos; - VectorEci vel1 = vel; + for (int i = 0; i < 6; i++) + { + VectorEci pos1 = pos; + VectorEci vel1 = vel; - if (i < 3) pos1[i] += deltas[i]; - else vel1[i-3] += deltas[i]; + if (i < 3) + pos1[i] += deltas[i]; + else + vel1[i - 3] += deltas[i]; - Vector6d keplers1; - pass = inertial2Keplers(std::cout, pos1, vel1, keplers1); + Vector6d keplers1; + pass = inertial2Keplers(std::cout, pos1, vel1, keplers1); - partials.col(i) = (keplers1 - keplers0) / deltas[i]; - } + partials.col(i) = (keplers1 - keplers0) / deltas[i]; + } } VectorEci propagateEllipse( - Trace& trace, - GTime time, - double dt, - const VectorEci& rSat, - const VectorEci& vSat, - SatPos& satPos, - bool j2) + Trace& trace, + GTime time, + double dt, + const VectorEci& rSat, + const VectorEci& vSat, + SatPos& satPos, + bool j2 +) { - ERPValues erpv = getErp(nav.erp, time); - - FrameSwapper frameSwapper(time + dt, erpv); + ERPValues erpv = getErp(nav.erp, time); - auto& ecef = satPos.rSatCom; - auto& vSatEcef = satPos.satVel; + FrameSwapper frameSwapper(time + dt, erpv); - if (dt == 0) - { - ecef = frameSwapper(rSat, &vSat, &vSatEcef); + auto& ecef = satPos.rSatCom; + auto& vSatEcef = satPos.satVel; - return rSat; - } + if (dt == 0) + { + ecef = frameSwapper(rSat, &vSat, &vSatEcef); - Vector6d keplers0; + return rSat; + } - bool pass = inertial2Keplers(trace, rSat, vSat, keplers0); + Vector6d keplers0; - if (pass == false) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Failed to determine keplers for " << satPos.Sat.id() << " , " - << rSat.transpose().format(heavyFmt) - << vSat.transpose().format(heavyFmt); + bool pass = inertial2Keplers(trace, rSat, vSat, keplers0); - VectorEci newPos = rSat - + vSat * dt; + if (pass == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Failed to determine keplers for " << satPos.Sat.id() << " , " + << rSat.transpose().format(heavyFmt) << vSat.transpose().format(heavyFmt); - ecef = frameSwapper(newPos, &vSat, &vSatEcef); + VectorEci newPos = rSat + vSat * dt; - return newPos; - } + ecef = frameSwapper(newPos, &vSat, &vSatEcef); + return newPos; + } - Vector3d L = Vector3d( - keplers0[KEPLER::LX], - keplers0[KEPLER::LY], - keplers0[KEPLER::LZ] - ); + Vector3d L = Vector3d( + keplers0[static_cast(KEPLER::LX)], + keplers0[static_cast(KEPLER::LY)], + keplers0[static_cast(KEPLER::LZ)] + ); - double h = L.norm(); + double h = L.norm(); - Vector2d E = Vector2d( - keplers0[KEPLER::EU], - keplers0[KEPLER::EV] - ); + Vector2d E = + Vector2d(keplers0[static_cast(KEPLER::EU)], keplers0[static_cast(KEPLER::EV)]); - double e = E.norm(); + double e = E.norm(); - double n = SQR(GM_Earth) / pow(h / sqrt(1 - SQR(e)), 3); //2.82 + double n = SQR(GM_Earth) / pow(h / sqrt(1 - SQR(e)), 3); // 2.82 - keplers0[KEPLER::M] += n * dt; + keplers0[static_cast(KEPLER::M)] += n * dt; - VectorEci newPos1 = keplers2Inertial(trace, keplers0); + VectorEci newPos1 = keplers2Inertial(trace, keplers0); - if (j2) - { - double J2 = 0.00108263; + if (j2) + { + double J2 = 0.00108263; - VectorEci a; + VectorEci a; - for (auto& rSat : {rSat, newPos1}) - { - double R = rSat.norm(); + for (auto& rSat : {rSat, newPos1}) + { + double R = rSat.norm(); - double x = rSat.x(); - double y = rSat.y(); - double z = rSat.z(); + double x = rSat.x(); + double y = rSat.y(); + double z = rSat.z(); - double commonTerm = 5 * SQR(z/R); + double commonTerm = 5 * SQR(z / R); - Vector3d vec = Vector3d( - x * (commonTerm - 1), - y * (commonTerm - 1), - z * (commonTerm - 3) - ); + Vector3d vec = + Vector3d(x * (commonTerm - 1), y * (commonTerm - 1), z * (commonTerm - 3)); - a += 1.5 * J2 * GM_Earth * SQR(RE_MEAN) / (R * R * R * R * R) * vec; - } + a += 1.5 * J2 * GM_Earth * SQR(RE_MEAN) / (R * R * R * R * R) * vec; + } - a /= 2; + a /= 2; - newPos1 += 0.5 * a * SQR(dt); - } + newPos1 += 0.5 * a * SQR(dt); + } - double dtVel = 1e-4; + double dtVel = 1e-4; - keplers0[KEPLER::M] += n * dtVel; + keplers0[static_cast(KEPLER::M)] += n * dtVel; - VectorEci velEci; - if (1) - { - VectorEci newPos2 = keplers2Inertial(trace, keplers0); + VectorEci velEci; + if (1) + { + VectorEci newPos2 = keplers2Inertial(trace, keplers0); - velEci = ((Vector3d) newPos2 - newPos1) / dtVel; - } + velEci = ((Vector3d)newPos2 - newPos1) / dtVel; + } -// std::cout << "\nrSatInertial: " << newPos.transpose(); + // std::cout << "\nrSatInertial: " << newPos.transpose(); - ecef = frameSwapper(newPos1, &velEci, &vSatEcef); + ecef = frameSwapper(newPos1, &velEci, &vSatEcef); - return newPos1; + return newPos1; } -VectorEci propagateFull( - Trace& trace, - GTime time, - double dt, - VectorEci& rSat, - VectorEci& vSat, - SatPos& satPos) +VectorEci +propagateFull(Trace& trace, GTime time, double dt, VectorEci& rSat, VectorEci& vSat, SatPos& satPos) { - ERPValues erpv = getErp(nav.erp, time + dt); + ERPValues erpv = getErp(nav.erp, time + dt); - FrameSwapper frameSwapper(time + dt, erpv); + FrameSwapper frameSwapper(time + dt, erpv); - auto& ecef = satPos.rSatCom; - auto& vSatEcef = satPos.satVel; + auto& ecef = satPos.rSatCom; + auto& vSatEcef = satPos.satVel; - if (dt == 0) - { - ecef = frameSwapper(rSat, &vSat, &vSatEcef); + if (dt == 0) + { + ecef = frameSwapper(rSat, &vSat, &vSatEcef); - return rSat; - } + return rSat; + } - OrbitState orbit; - orbit.Sat = satPos.Sat; - orbit.pos = rSat; - orbit.vel = vSat; - orbit.posVelSTM = MatrixXd::Identity(6, 6); + OrbitState orbit; + orbit.Sat = satPos.Sat; + orbit.pos = rSat; + orbit.vel = vSat; + orbit.posVelSTM = MatrixXd::Identity(6, 6); - Orbits orbits; - orbits.push_back(orbit); + Orbits orbits; + orbits.push_back(orbit); - OrbitIntegrator integrator; - integrator.timeInit = time; + OrbitIntegrator integrator; + integrator.timeInit = time; - integrateOrbits(integrator, orbits, dt, acsConfig.propagationOptions.integrator_time_step); + integrateOrbits(integrator, orbits, dt, acsConfig.propagationOptions.integrator_time_step); - VectorEci newPos; - VectorEci velEci; - newPos = orbits[0].pos; - velEci = orbits[0].vel; + VectorEci newPos; + VectorEci velEci; + newPos = orbits[0].pos; + velEci = orbits[0].vel; - ecef = frameSwapper(newPos, &velEci, &vSatEcef); + ecef = frameSwapper(newPos, &velEci, &vSatEcef); - return newPos; + return newPos; } diff --git a/src/cpp/common/orbits.hpp b/src/cpp/common/orbits.hpp index 4a921b098..ee963f1f8 100644 --- a/src/cpp/common/orbits.hpp +++ b/src/cpp/common/orbits.hpp @@ -1,33 +1,29 @@ - #pragma once +#include "common/eigenIncluder.hpp" +#include "common/trace.hpp" -#include "eigenIncluder.hpp" -#include "trace.hpp" +struct SatPos; -VectorEci keplers2Inertial( - Trace& trace, - const Vector6d& keplers0); +VectorEci keplers2Inertial(Trace& trace, const Vector6d& keplers0); -bool inertial2Keplers( - Trace& trace, - const VectorEci& r, - const VectorEci& v, - Vector6d& keplers); +bool inertial2Keplers(Trace& trace, const VectorEci& r, const VectorEci& v, Vector6d& keplers); VectorEci propagateEllipse( - Trace& trace, - GTime time, - double dt, - const VectorEci& rSat, - const VectorEci& vSat, - SatPos& satPos, - bool j2 = false); + Trace& trace, + GTime time, + double dt, + const VectorEci& rSat, + const VectorEci& vSat, + SatPos& satPos, + bool j2 = false +); VectorEci propagateFull( - Trace& trace, - GTime time, - double dt, - VectorEci& rSat, - VectorEci& vSat, - SatPos& satPos); + Trace& trace, + GTime time, + double dt, + VectorEci& rSat, + VectorEci& vSat, + SatPos& satPos +); diff --git a/src/cpp/common/packetStatistics.hpp b/src/cpp/common/packetStatistics.hpp index 7bfefbd63..215c471c7 100644 --- a/src/cpp/common/packetStatistics.hpp +++ b/src/cpp/common/packetStatistics.hpp @@ -1,107 +1,99 @@ - #pragma once -#include "trace.hpp" +#include "common/trace.hpp" struct PacketStatistics { - long int numPreambleFound = 0; - long int numFramesFailedCRC = 0; - long int numFramesPassCRC = 0; - long int numFramesDecoded = 0; - long int numNonMessBytes = 0; - long int numMessagesLatency = 0; - double totalLatency = 0; - - void printPacketStatistics( - Trace& trace) - { - std::stringstream traceStr; - // traceStr << "Start : " << std::put_time(std::localtime(&startTime.time), "%F %X") << "\n"; - // traceStr << "Finish : " << std::put_time(std::localtime(&endTime.time), "%F %X") << "\n"; - -// double totalTime = timeGet() - startTime; - - traceStr << "ExtraBytes : " << numNonMessBytes << "\n"; - traceStr << "FailCrc : " << numFramesFailedCRC << "\n"; - traceStr << "PassedCrc : " << numFramesPassCRC << "\n"; - traceStr << "Decoded : " << numFramesDecoded << "\n"; - traceStr << "Preamble : " << numPreambleFound << "\n"; - - double FailedToPreambleRatio = 0; - if (numPreambleFound != 0) - FailedToPreambleRatio = (double)numFramesFailedCRC / (double)numPreambleFound; - traceStr << "FailedCrcToPreambleRatio : " << FailedToPreambleRatio << "\n"; - - if (numMessagesLatency != 0) - { - double meanLatency = totalLatency / numMessagesLatency; - traceStr << "meanLatency : " << meanLatency << "\n"; - } - else - { - traceStr << "meanLatency : " << 0.0 << "\n"; - } - - string messLine; - while (std::getline(traceStr, messLine)) - tracepdeex(0, trace, (messLine + "\n").c_str()); - } - - void checksumFailure( - string id = "") - { - numFramesFailedCRC++; - - std::stringstream message; - message << "\n CRC Failure - " << id; - message << "\n Number Fail CRC : " << numFramesFailedCRC; - message << "\n Number Passed CRC : " << numFramesPassCRC; - message << "\n Number Decoded : " << numFramesDecoded; - message << "\n Number Preamble : " << numPreambleFound; - message << "\n Number Unknown : " << numNonMessBytes; - message << "\n"; - - std::cout << message.str(); -// messageRtcmLog(message.str()); - } - - - void checksumSuccess( - unsigned int crcRead = 0) - { - numFramesPassCRC++; - -// printf("\n CRC pass: %02x %02x %02x", -// ((char*)&crcRead)[2], -// ((char*)&crcRead)[1], -// ((char*)&crcRead)[0]); - } - - void nonFrameByteFound( - unsigned char c) - { -// printf(".%02x", c); - - numNonMessBytes++; - } - - void preambleFound() - { - numPreambleFound++; - - if (numNonMessBytes) - { - std::stringstream message; - message << "Extra Bytes, size : " << numNonMessBytes; -// messageRtcmLog(message.str()); - } - - numNonMessBytes = 0; - } - - void frameDecoded() - { - numFramesDecoded++; - } + long int numPreambleFound = 0; + long int numFramesFailedCRC = 0; + long int numFramesPassCRC = 0; + long int numFramesDecoded = 0; + long int numNonMessBytes = 0; + long int numMessagesLatency = 0; + double totalLatency = 0; + + void printPacketStatistics(Trace& trace) + { + std::stringstream traceStr; + // traceStr << "Start : " << std::put_time(std::localtime(&startTime.time), "%F + // %X") << "\n"; traceStr << "Finish : " << + // std::put_time(std::localtime(&endTime.time), "%F %X") << "\n"; + + // double totalTime = timeGet() - startTime; + + traceStr << "ExtraBytes : " << numNonMessBytes << "\n"; + traceStr << "FailCrc : " << numFramesFailedCRC << "\n"; + traceStr << "PassedCrc : " << numFramesPassCRC << "\n"; + traceStr << "Decoded : " << numFramesDecoded << "\n"; + traceStr << "Preamble : " << numPreambleFound << "\n"; + + double FailedToPreambleRatio = 0; + if (numPreambleFound != 0) + FailedToPreambleRatio = (double)numFramesFailedCRC / (double)numPreambleFound; + traceStr << "FailedCrcToPreambleRatio : " << FailedToPreambleRatio << "\n"; + + if (numMessagesLatency != 0) + { + double meanLatency = totalLatency / numMessagesLatency; + traceStr << "meanLatency : " << meanLatency << "\n"; + } + else + { + traceStr << "meanLatency : " << 0.0 << "\n"; + } + + string messLine; + while (std::getline(traceStr, messLine)) + tracepdeex(0, trace, (messLine + "\n").c_str()); + } + + void checksumFailure(string id = "") + { + numFramesFailedCRC++; + + std::stringstream message; + message << "\n CRC Failure - " << id; + message << "\n Number Fail CRC : " << numFramesFailedCRC; + message << "\n Number Passed CRC : " << numFramesPassCRC; + message << "\n Number Decoded : " << numFramesDecoded; + message << "\n Number Preamble : " << numPreambleFound; + message << "\n Number Unknown : " << numNonMessBytes; + message << "\n"; + + std::cout << message.str(); + // messageRtcmLog(message.str()); + } + + void checksumSuccess(unsigned int crcRead = 0) + { + numFramesPassCRC++; + + // printf("\n CRC pass: %02x %02x %02x", + // ((char*)&crcRead)[2], + // ((char*)&crcRead)[1], + // ((char*)&crcRead)[0]); + } + + void nonFrameByteFound(unsigned char c) + { + // printf(".%02x", c); + + numNonMessBytes++; + } + + void preambleFound() + { + numPreambleFound++; + + if (numNonMessBytes) + { + std::stringstream message; + message << "Extra Bytes, size : " << numNonMessBytes; + // messageRtcmLog(message.str()); + } + + numNonMessBytes = 0; + } + + void frameDecoded() { numFramesDecoded++; } }; diff --git a/src/cpp/common/platformCompat.hpp b/src/cpp/common/platformCompat.hpp new file mode 100644 index 000000000..718e43c95 --- /dev/null +++ b/src/cpp/common/platformCompat.hpp @@ -0,0 +1,92 @@ +#pragma once + +/** + * @file platformCompat.hpp + * @brief Cross-platform compatibility definitions for Windows and POSIX systems + * + * This header provides platform-specific abstractions for: + * - File I/O operations + * - Socket operations + * - Threading primitives + * - Time functions + */ + +// Windows-specific definitions +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include +#include + +// POSIX compatibility macros for Windows +#ifndef O_RDWR +#define O_RDWR _O_RDWR +#endif +#ifndef O_NONBLOCK +#define O_NONBLOCK 0 // Windows doesn't have direct equivalent +#endif + +// Function name compatibility +#define strcasecmp _stricmp +#define strncasecmp _strnicmp +#define popen _popen +#define pclose _pclose +#define fileno _fileno + +// Socket compatibility +typedef int socklen_t; + +// POSIX systems (Linux, macOS, etc.) +#else +#include +#include +#include +#include +#include +#include +#include +#include + +// Windows compatibility for POSIX +#define INVALID_HANDLE_VALUE -1 +#define SOCKET int +#define INVALID_SOCKET -1 +#define SOCKET_ERROR -1 +#define closesocket close + +typedef void* HANDLE; +#endif + +// Common cross-platform utilities +namespace PlatformCompat +{ +/** + * @brief Get the last system error code + * @return Error code (errno on POSIX, GetLastError() on Windows) + */ +inline int GetLastError() +{ +#ifdef _WIN32 + return ::GetLastError(); +#else + return errno; +#endif +} + +/** + * @brief Sleep for specified milliseconds + * @param milliseconds Duration to sleep + */ +inline void SleepMs(unsigned int milliseconds) +{ +#ifdef _WIN32 + Sleep(milliseconds); +#else + usleep(milliseconds * 1000); +#endif +} +} // namespace PlatformCompat diff --git a/src/cpp/common/pos.cpp b/src/cpp/common/pos.cpp index e6a2899c1..50951ba08 100644 --- a/src/cpp/common/pos.cpp +++ b/src/cpp/common/pos.cpp @@ -1,27 +1,19 @@ - -#include "architectureDocs.hpp" - -FileType POS__() -{ - -} - #include - -#include "coordinates.hpp" -#include "constants.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "trace.hpp" - -#include #include +#include +#include "architectureDocs.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/gTime.hpp" +#include "common/receiver.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" -using std::string; using std::map; +using std::string; +FileType POS__() {} string posHeader = R"POSHEADER(PBO Station Position Time Series. Reference Frame : IGS20 Format Version: 2.0.0 @@ -58,182 +50,162 @@ End Field Description *YYYY-MM-DDTHH:MM:SS.SSS YYYY.YYYYYYYYY X Y Z Sx Sy Sz Rxy Rxz Ryz NLat Elong Height dN dE dU Sn Se Su Rne Rnu Reu soln )POSHEADER"; -map posAprioriValue; +map posAprioriValue; -void replacePlaceholder( - string& str, - const string& placeholder, - const string& value) +void replacePlaceholder(string& str, const string& placeholder, const string& value) { - size_t pos = str.find(placeholder); + size_t pos = str.find(placeholder); - if (pos != string::npos) - { - str.replace(pos, placeholder.length(), value); - } + if (pos != string::npos) + { + str.replace(pos, placeholder.length(), value); + } } void formatAndReplace( - string& header, - const string& placeholder, - double value, - int precision, - int width) + string& header, + const string& placeholder, + double value, + int precision, + int width +) { - std::ostringstream oss; - oss << std::fixed << std::setprecision(precision) << std::setw(width) << value; - replacePlaceholder(header, placeholder, oss.str()); + std::ostringstream oss; + oss << std::fixed << std::setprecision(precision) << std::setw(width) << value; + replacePlaceholder(header, placeholder, oss.str()); } - template -void formatAndOutput( - std::ostream& output, - T value, - int precision, - int width) +void formatAndOutput(std::ostream& output, T value, int precision, int width) { - std::ostringstream oss; - oss << std::fixed << std::setprecision(precision) << std::setw(width) << value; - output << " " << oss.str(); + std::ostringstream oss; + oss << std::fixed << std::setprecision(precision) << std::setw(width) << value; + output << " " << oss.str(); } - -void writePOSHeader( - Trace& output, - string name, - GTime time) +void writePOSHeader(Trace& output, string name, GTime time) { - if (name.empty()) - { - name = "Track"; - } - - VectorEcef aprEcef = posAprioriValue[name]; - VectorPos aprPos = ecef2pos(aprEcef); - string header = posHeader; - - replacePlaceholder(header, "{ID}", name); - replacePlaceholder(header, "{FirstEpoch}", time.to_ISOstring(2)); - - formatAndReplace(header, "{POSX}", aprEcef.x(), 6, 13); - formatAndReplace(header, "{POSY}", aprEcef.y(), 6, 13); - formatAndReplace(header, "{POSZ}", aprEcef.z(), 6, 13); - formatAndReplace(header, "{POSN}", aprPos.latDeg(), 12, 14); - formatAndReplace(header, "{POSE}", aprPos.lonDeg(), 12, 14); - formatAndReplace(header, "{POSU}", aprPos.hgt(), 9, 12); - - output << header; + if (name.empty()) + { + name = "Track"; + } + + VectorEcef aprEcef = posAprioriValue[name]; + VectorPos aprPos = ecef2pos(aprEcef); + string header = posHeader; + + replacePlaceholder(header, "{ID}", name); + replacePlaceholder(header, "{FirstEpoch}", time.to_ISOstring(2)); + + formatAndReplace(header, "{POSX}", aprEcef.x(), 6, 13); + formatAndReplace(header, "{POSY}", aprEcef.y(), 6, 13); + formatAndReplace(header, "{POSZ}", aprEcef.z(), 6, 13); + formatAndReplace(header, "{POSN}", aprPos.latDeg(), 12, 14); + formatAndReplace(header, "{POSE}", aprPos.lonDeg(), 12, 14); + formatAndReplace(header, "{POSU}", aprPos.hgt(), 9, 12); + + output << header; } - -void writePOSEntry( - Trace& output, - Receiver& rec, - KFState& kfState) +void writePOSEntry(Trace& output, Receiver& rec, KFState& kfState) { - VectorEcef apriori = rec.aprioriPos; - VectorEcef xyz = apriori; - Matrix3d vcv; - - for (auto& [kfKey, index] : kfState.kfIndexMap) - { - if ( kfKey.type != KF::REC_POS - ||kfKey.str != rec.id) - { - continue; - } - - xyz[kfKey.num] = kfState.x(index); - - for (auto& [kfKey2, index2] : kfState.kfIndexMap) - { - if ( kfKey2.type != KF::REC_POS - ||kfKey2.str != rec.id) - { - continue; - } - - vcv(kfKey.num, kfKey2.num) = kfState.P(index, index2); - } - } - - VectorPos pos = ecef2pos(xyz); - VectorEcef aprEcef = posAprioriValue[rec.id]; - VectorPos aprPos = ecef2pos(posAprioriValue[rec.id]); - - VectorEcef diff = xyz - aprEcef; - Matrix3d E; - pos2enu(aprPos, E.data()); - Vector3d diffEnu = E * diff; - Matrix3d vcvEnu = E * vcv * E.transpose(); - Vector3d var; - Vector3d varEnu; - - for (int i = 0; i < 3; i++) - { - var [i] = SQRT(vcv (i,i)); - varEnu [i] = SQRT(vcvEnu (i,i)); - } - - for (int i = 0; i < 3; i++) - for (int j = i; j < 3; j++) - { - vcvEnu (i,j) /= (varEnu [i] * varEnu[j]); - vcv (i,j) /= (var [i] * var [j]); - } - - output << " " << kfState.time.to_ISOstring(3); - formatAndOutput(output, kfState.time.to_decYear(), 9, 14); - formatAndOutput(output, xyz.x(), 5, 11); - formatAndOutput(output, xyz.y(), 5, 11); - formatAndOutput(output, xyz.z(), 5, 11); - formatAndOutput(output, var.x(), 5, 9); - formatAndOutput(output, var.y(), 5, 9); - formatAndOutput(output, var.z(), 5, 9); - formatAndOutput(output, vcv(0,1), 3, 7); - formatAndOutput(output, vcv(0,2), 3, 7); - formatAndOutput(output, vcv(1,2), 3, 7); - formatAndOutput(output, pos.latDeg(), 10, 15); - formatAndOutput(output, pos.lonDeg(), 10, 15); - formatAndOutput(output, pos.hgt(), 5, 11); - formatAndOutput(output, diffEnu.y(), 5, 11); - formatAndOutput(output, diffEnu.x(), 5, 11); - formatAndOutput(output, diffEnu.z(), 5, 11); - formatAndOutput(output, varEnu(1), 5, 9); - formatAndOutput(output, varEnu(0), 5, 9); - formatAndOutput(output, varEnu(2), 5, 9); - formatAndOutput(output, vcvEnu(0,1), 3, 7); - formatAndOutput(output, vcvEnu(1,2), 3, 7); - formatAndOutput(output, vcvEnu(0,2), 3, 7); - output << " ginan"; //Placeholder for solution type. - output << "\n"; + VectorEcef apriori = rec.aprioriPos; + VectorEcef xyz = apriori; + Matrix3d vcv; + + for (auto& [kfKey, index] : kfState.kfIndexMap) + { + if (kfKey.type != KF::REC_POS || kfKey.str != rec.id) + { + continue; + } + + xyz[kfKey.num] = kfState.x(index); + + for (auto& [kfKey2, index2] : kfState.kfIndexMap) + { + if (kfKey2.type != KF::REC_POS || kfKey2.str != rec.id) + { + continue; + } + + vcv(kfKey.num, kfKey2.num) = kfState.P(index, index2); + } + } + + VectorPos pos = ecef2pos(xyz); + VectorEcef aprEcef = posAprioriValue[rec.id]; + VectorPos aprPos = ecef2pos(posAprioriValue[rec.id]); + + VectorEcef diff = xyz - aprEcef; + Matrix3d E; + pos2enu(aprPos, E.data()); + Vector3d diffEnu = E * diff; + Matrix3d vcvEnu = E * vcv * E.transpose(); + Vector3d var; + Vector3d varEnu; + + for (int i = 0; i < 3; i++) + { + var[i] = SQRT(vcv(i, i)); + varEnu[i] = SQRT(vcvEnu(i, i)); + } + + for (int i = 0; i < 3; i++) + for (int j = i; j < 3; j++) + { + vcvEnu(i, j) /= (varEnu[i] * varEnu[j]); + vcv(i, j) /= (var[i] * var[j]); + } + + output << " " << kfState.time.to_ISOstring(3); + formatAndOutput(output, kfState.time.to_decYear(), 9, 14); + formatAndOutput(output, xyz.x(), 5, 11); + formatAndOutput(output, xyz.y(), 5, 11); + formatAndOutput(output, xyz.z(), 5, 11); + formatAndOutput(output, var.x(), 5, 9); + formatAndOutput(output, var.y(), 5, 9); + formatAndOutput(output, var.z(), 5, 9); + formatAndOutput(output, vcv(0, 1), 3, 7); + formatAndOutput(output, vcv(0, 2), 3, 7); + formatAndOutput(output, vcv(1, 2), 3, 7); + formatAndOutput(output, pos.latDeg(), 10, 15); + formatAndOutput(output, pos.lonDeg(), 10, 15); + formatAndOutput(output, pos.hgt(), 5, 11); + formatAndOutput(output, diffEnu.y(), 5, 11); + formatAndOutput(output, diffEnu.x(), 5, 11); + formatAndOutput(output, diffEnu.z(), 5, 11); + formatAndOutput(output, varEnu(1), 5, 9); + formatAndOutput(output, varEnu(0), 5, 9); + formatAndOutput(output, varEnu(2), 5, 9); + formatAndOutput(output, vcvEnu(0, 1), 3, 7); + formatAndOutput(output, vcvEnu(1, 2), 3, 7); + formatAndOutput(output, vcvEnu(0, 2), 3, 7); + output << " ginan"; // Placeholder for solution type. + output << "\n"; } -void writePOS( - string filename, - KFState& kfState, - Receiver& rec) +void writePOS(string filename, KFState& kfState, Receiver& rec) { - std::ofstream output(filename, std::fstream::in | std::fstream::out); - if (!output) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Error opening POS file '" << filename << "'"; - return; - } - - output.seekp(0, output.end); // seek to end of file - - if (output.tellp() == 0) - { - if (posAprioriValue.find(rec.id) == posAprioriValue.end()) - { - VectorEcef apriori = rec.aprioriPos; - posAprioriValue[rec.id] = apriori; - } - - writePOSHeader(output, rec.id, kfState.time); - } - - writePOSEntry(output, rec, kfState); + std::ofstream output(filename, std::fstream::out | std::fstream::app); + if (!output.is_open()) + { + BOOST_LOG_TRIVIAL(warning) << "Error opening POS file '" << filename; + return; + } + + output.seekp(0, output.end); // seek to end of file + + if (output.tellp() == 0) + { + if (posAprioriValue.find(rec.id) == posAprioriValue.end()) + { + VectorEcef apriori = rec.aprioriPos; + posAprioriValue[rec.id] = apriori; + } + + writePOSHeader(output, rec.id, kfState.time); + } + + writePOSEntry(output, rec, kfState); } diff --git a/src/cpp/common/pos.hpp b/src/cpp/common/pos.hpp index 2e14ef233..26eaa5f5d 100644 --- a/src/cpp/common/pos.hpp +++ b/src/cpp/common/pos.hpp @@ -1,10 +1,4 @@ - #pragma once - struct KFState; struct Receiver; - -void writePOS( - string filename, - KFState& kfState, - Receiver& rec); +void writePOS(string filename, KFState& kfState, Receiver& rec); \ No newline at end of file diff --git a/src/cpp/common/receiver.cpp b/src/cpp/common/receiver.cpp index 26ed67e70..6794bac86 100644 --- a/src/cpp/common/receiver.cpp +++ b/src/cpp/common/receiver.cpp @@ -1,51 +1,78 @@ +#include "common/receiver.hpp" +#include +#include "common/sinex.hpp" +#include "common/streamParser.hpp" +#include "common/streamRinex.hpp" +#include "common/streamRtcm.hpp" -#include "receiver.hpp" -#include "sinex.hpp" -#include "tides.hpp" +SinexSiteId dummySiteid; +SinexReceiver dummyReceiver; +SinexAntenna dummyAntenna; +SinexSiteEcc dummySiteEcc; -SinexSiteId dummySiteid; -SinexReceiver dummyReceiver; -SinexAntenna dummyAntenna; -SinexSiteEcc dummySiteEcc; +SinexSatIdentity dummySinexSatIdentity; +SinexSatEcc dummySinexSatEcc; -SinexSatIdentity dummySinexSatIdentity; -SinexSatEcc dummySinexSatEcc; +ReceiverMap receiverMap; +void extractTrackedSignals(Receiver& rec, Parser& parser, ObsList* obsList) +{ + // Extract tracked signals from RINEX header or accumulate from RTCM observations + // This enables receiver-specific signal tracking mode detection + string parserType = parser.parserType(); + if (parserType == "RinexParser" && rec.trackedSignals.empty()) + { + auto& rinexParser = static_cast(parser); + for (auto& [sys, codeTypeMap] : rinexParser.sysCodeTypes) + { + vector signals; + for (auto& [idx, codeType] : codeTypeMap) + { + if (codeType.code != E_ObsCode::NONE) + { + signals.push_back(codeType.code); + } + } + rec.trackedSignals[sys] = signals; + } + } + else if (parserType == "RtcmParser" && obsList != nullptr) + { + // For RTCM streams, accumulate tracked signals from observations + // This builds up the signal list dynamically as we receive data -ReceiverMap receiverMap; + // Extract signals from the provided observation list + for (auto& obs : only(*obsList)) + { + E_Sys sys = obs.Sat.sys; + set signalSet; -void initialiseStation( - string id, - Receiver& rec) -{ - if (rec.id.empty() == false) - { - //already initialised - return; - } - - BOOST_LOG_TRIVIAL(info) - << "Initialising station " << id; - - rec.id = id; - - auto loadBlq = [&](vector files, E_LoadingType type) - { - bool found = false; - for (auto& blqfile : acsConfig.ocean_tide_loading_blq_files) - { - found |= readBlq(blqfile, rec, E_LoadingType::OCEAN); - } - - if (found == false) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: No " << type._to_string() << " BLQ for " << id; - } - }; - - loadBlq(acsConfig.ocean_tide_loading_blq_files, E_LoadingType::OCEAN); - loadBlq(acsConfig.atmos_tide_loading_blq_files, E_LoadingType::ATMOSPHERIC); + // If we already have signals for this system, start with those + if (rec.trackedSignals.count(sys)) + { + signalSet.insert(rec.trackedSignals[sys].begin(), rec.trackedSignals[sys].end()); + } + + // Add signals from this observation + for (auto& [ftype, sigsList] : obs.sigsLists) + { + for (auto& sig : sigsList) + { + if (sig.code != E_ObsCode::NONE) + { + signalSet.insert(sig.code); + } + } + } + + // Update the tracked signals with the expanded set + rec.trackedSignals[sys] = vector(signalSet.begin(), signalSet.end()); + } + } + else + { + // Signal extraction not supported for other parser types (UBX, Custom, etc.) + } } diff --git a/src/cpp/common/receiver.hpp b/src/cpp/common/receiver.hpp index 9dd362103..17e27eeda 100644 --- a/src/cpp/common/receiver.hpp +++ b/src/cpp/common/receiver.hpp @@ -1,85 +1,70 @@ - #pragma once -#include "eigenIncluder.hpp" -#include "observations.hpp" -#include "attitude.hpp" -#include "satStat.hpp" -#include "common.hpp" -#include "cache.hpp" -#include "gTime.hpp" -#include "ppp.hpp" +#include "common/attitude.hpp" +#include "common/cache.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/observations.hpp" +#include "common/satStat.hpp" +#include "pea/ppp.hpp" + +struct Parser; /** Solution of user mode processing functinos -*/ + */ struct Solution { - /* solution type */ - GTime sppTime; ///< time (GPST) - map dtRec_m; ///< receiver clock bias to time systems (m) - map dtRec_m_pppp_old; ///< previous receiver clock bias to time systems (m) - E_Solution status; ///< solution status - int numMeas = 0; ///< number of valid satellites - KFState sppState; ///< SPP filter object - Dops dops; ///< dilution of precision (GDOP,PDOP,HDOP,VDOP) - VectorEcef sppRRec; ///< Position vector from spp + GTime sppTime; ///< time (GPST) + KFState sppState; ///< SPP filter object + VectorEcef sppPos; ///< Position vector from SPP + double sppClk = 0; ///< receiver clock/system biases to receiver reference system (m) + E_Sys clkRefSys = E_Sys::NONE; ///< receiver clock reference system + double sppPppClkOffset = 0; ///< SPP to PPP offset of receiver clock + bool clkAdjustReady = false; ///< if SPP to PPP offset is ready + E_Solution status = E_Solution::NONE; ///< solution status + int numMeas = 0; ///< number of valid measurements used to estimate solution + Dops dops; ///< dilution of precision (GDOP,PDOP,HDOP,VDOP) + double horzPL = -1; ///< Horizontal Protection Level for SBAS + double vertPL = -1; ///< Vertical Protection Level for SBAS }; struct RinexStation { - string id; ///< marker name - string marker; ///< marker number - string antDesc; ///< antenna descriptor - string antSerial; ///< antenna serial number - string recType; ///< receiver type descriptor - string recFWVersion; ///< receiver firmware version - string recSerial; ///< receiver serial number - Vector3d del = Vector3d::Zero(); ///< antenna position delta (e/n/u or x/y/z) (m) - Vector3d pos = Vector3d::Zero(); + string id; ///< marker name + string marker; ///< marker number + string antDesc; ///< antenna descriptor + string antSerial; ///< antenna serial number + string recType; ///< receiver type descriptor + string recFWVersion; ///< receiver firmware version + string recSerial; ///< receiver serial number + Vector3d del = Vector3d::Zero(); ///< antenna position delta (e/n/u or x/y/z) (m) + Vector3d pos = Vector3d::Zero(); }; - struct ReceiverLogs { - PTime firstEpoch = GTime::noTime(); - PTime lastEpoch = GTime::noTime(); - int epochCount = 0; - int obsCount = 0; - int slipCount = 0; - map codeCount; - map satCount; - - int receiverErrorEpochs = 0; - int receiverErrorCount = 0; -}; - - -/** Structure of ocean/atmospheric tide loading displacements in amplitude and phase -*/ -struct TidalDisplacement -{ - VectorEnu amplitude; - VectorEnu phase; -}; - -/** Map of ocean/atmospheric tide loading displacements -*/ -struct TideMap : map -{ - + PTime firstEpoch = GTime::noTime(); + PTime lastEpoch = GTime::noTime(); + int epochCount = 0; + int obsCount = 0; + int slipCount = 0; + map codeCount; + map satCount; + + int receiverErrorEpochs = 0; + int receiverErrorCount = 0; }; struct Rtk { - Solution sol; ///< RTK solution - string antennaType; - string receiverType; - string antennaId; - map satStatMap; - TideMap otlDisplacement; ///< ocean tide loading parameters - TideMap atlDisplacement; ///< atmospheric tide loading parameters - VectorEnu antDelta; ///< antenna delta {rov_e,rov_n,rov_u} - AttStatus attStatus; + Solution sol; ///< RTK solution + string antennaType; + string receiverType; + string antennaId; + map satStatMap; + VectorEnu antDelta; ///< antenna delta {rov_e,rov_n,rov_u} + AttStatus attStatus; }; struct SinexSiteId; @@ -87,91 +72,94 @@ struct SinexReceiver; struct SinexAntenna; struct SinexSiteEcc; -extern SinexSiteId dummySiteid; -extern SinexReceiver dummyReceiver; -extern SinexAntenna dummyAntenna; -extern SinexSiteEcc dummySiteEcc; +extern SinexSiteId dummySiteid; +extern SinexReceiver dummyReceiver; +extern SinexAntenna dummyAntenna; +extern SinexSiteEcc dummySiteEcc; struct SinexRecData { - SinexSiteId* id_ptr = &dummySiteid; - SinexReceiver* rec_ptr = &dummyReceiver; - SinexAntenna* ant_ptr = &dummyAntenna; - SinexSiteEcc* ecc_ptr = &dummySiteEcc; - - UYds start; - UYds stop = UYds(-1,-1,-1); - - bool primary = false; ///< this position estimate is considered to come from a primary source - VectorEcef pos; - VectorEcef var; - VectorEcef vel; - GTime refEpoch = {}; + SinexSiteId* id_ptr = &dummySiteid; + SinexReceiver* rec_ptr = &dummyReceiver; + SinexAntenna* ant_ptr = &dummyAntenna; + SinexSiteEcc* ecc_ptr = &dummySiteEcc; + + UYds start; + UYds stop = UYds(-1, -1, -1); + + bool primary = false; ///< this position estimate is considered to come from a primary source + VectorEcef pos; + VectorEcef var; + VectorEcef vel; + GTime refEpoch = {}; }; /** Object to maintain receiver station data -*/ + */ struct Receiver : ReceiverLogs, Rtk { - bool isPseudoRec = false; - bool invalid = false; - SinexRecData snx; ///< Antenna information - - map metaDataMap; - ObsList obsList; ///< Observations available for this station at this epoch - string id; ///< Unique name for this station (4 characters) - - bool primaryApriori = false; - UYds aprioriTime; - double aprioriClk = 0; - double aprioriClkVar = 0; - Vector3d aprioriPos = Vector3d::Zero(); ///< station position (ecef) (m) - Matrix3d aprioriVar = Matrix3d::Zero(); - Vector3d minconApriori = Vector3d::Zero(); - - VectorPos pos; - - bool ready = false; - - Vector3d antBoresight = {0,0,1}; - Vector3d antAzimuth = {0,1,0}; - - string traceFilename; - string jsonTraceFilename; - - map savedSlips; - - union - { - const unsigned int failure = 0; - struct - { - unsigned failureSinex : 1; - unsigned failureAprioriPos : 1; - unsigned failureEccentricity : 1; - unsigned failureAntenna : 1; - }; - }; - Cache> pppTideCache; - Cache> pppEopCache; + bool isPseudoRec = false; + bool invalid = false; + SinexRecData snx; ///< Antenna information + + map metaDataMap; + + ObsList obsList; ///< Observations available for this station at this epoch + string id; ///< Unique name for this station (4 characters) + string source; ///< Source of most recently synchronised data + + bool primaryApriori = false; + UYds aprioriTime; + double aprioriClk = 0; ///< receiver clock bias (m) + double aprioriClkVar = 0; ///< receiver clock variance (m^2) + Vector3d aprioriPos = Vector3d::Zero(); ///< receiver position (ecef) (m) + Matrix3d aprioriPosVar = Matrix3d::Zero(); ///< receiver position variances (m^2) + Vector3d minconApriori = Vector3d::Zero(); + + VectorPos pos; + + bool ready = false; + + Vector3d antBoresight = {0, 0, 1}; + Vector3d antAzimuth = {0, 1, 0}; + + string traceFilename; + string jsonTraceFilename; + string sppOutputFile; + + map savedSlips; + + // Receiver-specific signal tracking capabilities from RINEX header + map> trackedSignals; + + union + { + const unsigned int failure = 0; + struct + { + unsigned failureSinex : 1; + unsigned failureAprioriPos : 1; + unsigned failureEccentricity : 1; + unsigned failureAntenna : 1; + }; + }; + Cache> pppTideCache; + Cache> pppEopCache; }; struct ReceiverMap : map { - }; -extern ReceiverMap receiverMap; +extern ReceiverMap receiverMap; + +void extractTrackedSignals(Receiver& rec, Parser& parser, ObsList* obsList = nullptr); struct Network { - string traceFilename; - string jsonTraceFilename; - string id = "Network"; + string traceFilename; + string jsonTraceFilename; + string id = "Network"; - KFState kfState = {}; + KFState kfState = {}; }; - -void initialiseStation( - string id, - Receiver& rec); diff --git a/src/cpp/common/rinex.cpp b/src/cpp/common/rinex.cpp index 630847b9b..6007fad22 100644 --- a/src/cpp/common/rinex.cpp +++ b/src/cpp/common/rinex.cpp @@ -1,1896 +1,2717 @@ - +/** + * @file rinex.cpp + * @brief Implementation of RINEX file format processing and observation handling + * + * This file implements comprehensive RINEX (Receiver Independent Exchange Format) + * file processing capabilities including: + * - RINEX 2.x and 3.x observation data parsing + * - Navigation data extraction + * - Station information processing + * - Observation type conversion and mapping + * - Phase observation priority resolution + * - Robust error handling and validation + * + * The implementation follows SOLID principles with single-responsibility functions + * and a staging pattern for robust data processing. + * + * @author Geoscience Australia + * @date 2024 + * @version 1.0 + */ // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -FileType CLK__() -{ - -} - -FileType RNX__() -{ - -} - +#include "common/rinex.hpp" #include - #include +#include "architectureDocs.hpp" +#include "common/biases.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/rinexNavWrite.hpp" +#include "common/trace.hpp" using std::string; -#include "rinexNavWrite.hpp" -#include "navigation.hpp" -#include "constants.hpp" -#include "receiver.hpp" -#include "common.hpp" -#include "biases.hpp" -#include "gTime.hpp" -#include "rinex.hpp" -#include "trace.hpp" -#include "enum.h" - -#define MAXPOSHEAD 1024 ///< max head line position -#define MINFREQ_GLO -7 ///< min frequency number glonass -#define MAXFREQ_GLO 13 ///< max frequency number glonass - -BETTER_ENUM(E_EphType, short int, - NONE, ///< NONE for unknown - EPH, ///< GPS/QZS LNAV, GAL IFNV, BDS D1D2 Ephemeris - GEPH, ///< GLO Ephemeris - SEPH, ///< SBAS Ephemeris - CEPH, ///< GPS/QZS/BDS CNVX Ephemeris - STO, ///< STO message - EOP, ///< EOP message - ION) ///< ION message - -/** Default navigation massage type for RINEX 3 and 2 -*/ -map defNavMsgType = -{ - {E_Sys::GPS, E_NavMsgType::LNAV}, - {E_Sys::GLO, E_NavMsgType::FDMA}, - {E_Sys::GAL, E_NavMsgType::IFNV}, - {E_Sys::BDS, E_NavMsgType::D1D2}, - {E_Sys::QZS, E_NavMsgType::LNAV}, - {E_Sys::IRN, E_NavMsgType::LNAV}, - {E_Sys::SBS, E_NavMsgType::SBAS} +FileType CLK__() {} + +FileType RNX__() {} + +constexpr int MAXPOSHEAD = 1024; ///< max head line position +constexpr int MINFREQ_GLO = -7; ///< min frequency number glonass +constexpr int MAXFREQ_GLO = 13; ///< max frequency number glonass + +/** + * @brief Default navigation message types by GNSS system + * + * Provides default navigation message type mappings for each GNSS system + * when processing RINEX 2.x and 3.x navigation files. Used to initialize + * ephemeris structures with appropriate message types. + * + * @note These defaults may be overridden by explicit message type indicators + */ +map defNavMsgType = { + {E_Sys::GPS, E_NavMsgType::LNAV}, + {E_Sys::GLO, E_NavMsgType::FDMA}, + {E_Sys::GAL, E_NavMsgType::IFNV}, + {E_Sys::BDS, E_NavMsgType::D1D2}, + {E_Sys::QZS, E_NavMsgType::LNAV}, + {E_Sys::IRN, E_NavMsgType::LNAV}, + {E_Sys::SBS, E_NavMsgType::SBAS} }; -/** Set string without tail space -*/ -void setstr(char *dst, const char *src, int n) +// Set string without trailing spaces +void setstr(char* dst, const char* src, int n) { - char *p = dst; - const char *q = src; + char* p = dst; + const char* q = src; - while (*q && q < src + n) *p++ = *q++; + while (*q && q < src + n) + *p++ = *q++; - *p-- = '\0'; + *p-- = '\0'; - while (p >= dst && *p == ' ') *p-- = '\0'; + while (p >= dst && *p == ' ') + *p-- = '\0'; } -/** Decode obs header -*/ +// Decode RINEX observation file header void decodeObsH( - std::istream& inputStream, - string& line, - double ver, - E_TimeSys& tsys, - map>& sysCodeTypes, - Navigation& nav, - RinexStation& rnxRec) + std::istream& inputStream, + string& line, + double ver, + E_TimeSys& tsys, + map>& sysCodeTypes, + Navigation& nav, + RinexStation& rnxRec +) { - double del[3]; - int prn; - int fcn; - const char *p; - char* buff = &line[0]; - char* label = buff + 60; - -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver; - - if (strstr(label, "MARKER NAME" )) - { - if (rnxRec.id.empty()) - { - rnxRec.id .assign(buff, 4); - } - } - else if (strstr(label, "MARKER NUMBER" )) - { - rnxRec.marker .assign(buff, 20); - - } -// else if (strstr(label,"MARKER TYPE" )) ; // ver.3 -// else if (strstr(label,"OBSERVER / AGENCY" )) ; - else if (strstr(label, "REC # / TYPE / VERS" )) - { - rnxRec.recSerial .assign(buff, 20); - rnxRec.recType .assign(buff + 20, 20); - rnxRec.recFWVersion .assign(buff + 40, 20); - - } - else if (strstr(label, "ANT # / TYPE" )) - { - rnxRec.antSerial .assign(buff, 20); - rnxRec.antDesc .assign(buff + 20, 20); - - } - else if (strstr(label, "APPROX POSITION XYZ" )) - { - for (int i = 0, j = 0; i < 3; i++, j += 14) - rnxRec.pos[i] = str2num(buff, j, 14); - - } - else if (strstr(label, "ANTENNA: DELTA H/E/N")) - { - for (int i = 0, j = 0; i < 3; i++, j += 14) - del[i] = str2num(buff, j, 14); - - rnxRec.del[2] = del[0]; // h - rnxRec.del[0] = del[1]; // e - rnxRec.del[1] = del[2]; // n - - } -// else if (strstr(label,"ANTENNA: DELTA X/Y/Z")) ; // opt ver.3 -// else if (strstr(label,"ANTENNA: PHASECENTER")) ; // opt ver.3 -// else if (strstr(label,"ANTENNA: B.SIGHT XYZ")) ; // opt ver.3 -// else if (strstr(label,"ANTENNA: ZERODIR AZI")) ; // opt ver.3 -// else if (strstr(label,"ANTENNA: ZERODIR XYZ")) ; // opt ver.3 -// else if (strstr(label,"CENTER OF MASS: XYZ" )) ; // opt ver.3 - else if (strstr(label, "SYS / # / OBS TYPES" )) - { - // ver.3 - //get system from code letter - char code[] = "x00"; - code[0] = buff[0]; - - SatSys Sat(code); - - if (Sat.sys == +E_Sys::NONE) - { - BOOST_LOG_TRIVIAL(debug) - << "invalid system code: sys=" << code[0]; - - return; - } - - int n = (int) str2num(buff, 3, 3); - - for (int j = 0, k = 7; j < n; j++, k += 4) - { - if (k > 58) - { - //more on the next line - - if (!std::getline(inputStream, line)) - break; - - buff = &line[0]; - k = 7; - } - - CodeType codeType; - codeType.type = buff[k]; - - char code[] = "Lxx"; - code[1] = buff[k + 1]; - code[2] = buff[k + 2]; - if ( (Sat.sys == +E_Sys::BDS) - &&(code[1] == '1') - &&(ver == 3.02)) - { - // change beidou B1 code: 3.02 draft -> 3.02 - code[1] = '2'; - } - try - { - codeType.code = E_ObsCode::_from_string(code); - } - catch (...) - { - BOOST_LOG_TRIVIAL(debug) - << "invalid obs code: " << code; - } - - sysCodeTypes[Sat.sys][j] = codeType; - } - - // if unknown code in ver.3, set default code -// for (auto& codeType : sysCodeTypes[Sat.sys]) -// { -// if (tobs[i][j][2]) -// continue; -// -// if (!(p = strchr(frqcodes, tobs[i][j][1]))) -// continue; -// - // default codes for unknown code -// const char *defcodes[] = -// { -// "CWX ", // GPS: L125___ -// "CC ", // GLO: L12____ -// "X XXXX", // GAL: L1_5678 -// "CXXX ", // QZS: L1256__ -// "C X ", // SBS: L1_5___ -// "X XX " // BDS: L1__67_ -// }; -// tobs[i][j][2] = defcodes[i][(int)(p - frqcodes)]; -// -// BOOST_LOG_TRIVIAL(debug) -// << "set default for unknown code: sys=" << buff[0] -// << " code=" << tobs[i][j]; -// } - } -// else if (strstr(label,"WAVELENGTH FACT L1/2")) ; // opt ver.2 - else if (strstr(label, "# / TYPES OF OBSERV" )) - { - // ver.2 - - int n = (int)str2num(buff, 0, 6); - - for (int i = 0, j = 10; i < n; i++, j += 6) - { - if (j > 58) - { - //go onto new line - if (!std::getline(inputStream, line)) - break; - - buff = (char*) line.c_str(); - - j = 10; - } - - if (ver <= 2.99) - { - char obsCode2str[3] = {}; - setstr(obsCode2str, buff + j, 2); - - //save the type char before cleaning the string - char typeChar = obsCode2str[0]; - - for (E_Sys sys : E_Sys::_values()) - { - auto& recOpts = acsConfig.getRecOpts(rnxRec.id, {SatSys(sys, 0).sysName()}); - - map* conversionMap_ptr; - if ( typeChar != 'C' - &&typeChar != 'P') - { - obsCode2str[0] = 'L'; - conversionMap_ptr = &recOpts.rinex23Conv.phasConv; // ie use Phase Conversions for phase, doppler etc. - } - else - { - conversionMap_ptr = &recOpts.rinex23Conv.codeConv; - } - - auto& conversionMap = *conversionMap_ptr; - - CodeType codeType; - - try - { - E_ObsCode2 obsCode2 = E_ObsCode2::_from_string(obsCode2str); - E_ObsCode obsCode = conversionMap[obsCode2]; - codeType.code = obsCode; - codeType.type = typeChar; - } - catch (...) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Unknown code in rinex file: " << obsCode2str; - } - - sysCodeTypes[sys][i] = codeType; - } - } - } - //*tobs[0][nt]='\0'; - } -// else if (strstr(label, "SIGNAL STRENGTH UNIT")) ; // opt ver.3 -// else if (strstr(label, "INTERVAL" )) ; // opt - else if (strstr(label, "TIME OF FIRST OBS" )) - { - if (!strncmp(buff+48, "GPS", 3)) tsys = E_TimeSys::GPST; - else if (!strncmp(buff+48, "GLO", 3)) tsys = E_TimeSys::UTC; - else if (!strncmp(buff+48, "GAL", 3)) tsys = E_TimeSys::GST; - else if (!strncmp(buff+48, "QZS", 3)) tsys = E_TimeSys::QZSST; // ver.3.02 - else if (!strncmp(buff+48, "BDT", 3)) tsys = E_TimeSys::BDT; // ver.3.02 - } -// else if (strstr(label, "TIME OF LAST OBS" )) ; // opt -// else if (strstr(label, "RCV CLOCK OFFS APPL" )) ; // opt -// else if (strstr(label, "SYS / DCBS APPLIED" )) ; // opt ver.3 -// else if (strstr(label, "SYS / PCVS APPLIED" )) ; // opt ver.3 -// else if (strstr(label, "SYS / SCALE FACTOR" )) ; // opt ver.3 -// else if (strstr(label, "SYS / PHASE SHIFTS" )) ; // ver.3.01 - else if (strstr(label, "GLONASS SLOT / FRQ #")) - { - // ver.3.02 - p = buff + 4; - for (int i = 0; i < 8; i++, p += 8) - { - if (sscanf(p, "R%2d %2d", &prn, &fcn) < 2) - continue; - - if (1 <= prn - &&prn <= NSATGLO) - { - nav.glo_fcn[prn - 1] = fcn + 8; - } - - } - } - else if (strstr(label, "GLONASS COD/PHS/BIS" )) - { - // ver.3.02 - p = buff; - for (int i = 0; i < 4; i++, p += 13) - { - if (strncmp(p + 1, "C1C", 3)) nav.glo_cpbias[0] = str2num(p, 5, 8); - else if (strncmp(p + 1, "C1P", 3)) nav.glo_cpbias[1] = str2num(p, 5, 8); - else if (strncmp(p + 1, "C2C", 3)) nav.glo_cpbias[2] = str2num(p, 5, 8); - else if (strncmp(p + 1, "C2P", 3)) nav.glo_cpbias[3] = str2num(p, 5, 8); - } - } - else if (strstr(label, "LEAP SECONDS" )) - { - // This would be GPS-UTC, and NOT optional as of RINEX 4 - nav.leaps = (int)str2num(buff, 0, 6); - } -// else if (strstr(label, "# OF SALTELLITES" )) ; // opt -// else if (strstr(label, "PRN / # OF OBS" )) ; // opt + double del[3]; + int prn; + int fcn; + const char* p; + char* buff = &line[0]; + char* label = buff + 60; + + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver; + + if (strstr(label, "MARKER NAME")) + { + if (rnxRec.id.empty()) + { + rnxRec.id.assign(buff, 4); + } + } + else if (strstr(label, "MARKER NUMBER")) + { + rnxRec.marker.assign(buff, 20); + } + // else if (strstr(label,"MARKER TYPE" )) ; // ver.3 + // else if (strstr(label,"OBSERVER / AGENCY" )) ; + else if (strstr(label, "REC # / TYPE / VERS")) + { + rnxRec.recSerial.assign(buff, 20); + rnxRec.recType.assign(buff + 20, 20); + rnxRec.recFWVersion.assign(buff + 40, 20); + } + else if (strstr(label, "ANT # / TYPE")) + { + rnxRec.antSerial.assign(buff, 20); + rnxRec.antDesc.assign(buff + 20, 20); + } + else if (strstr(label, "APPROX POSITION XYZ")) + { + for (int i = 0, j = 0; i < 3; i++, j += 14) + rnxRec.pos[i] = str2num(buff, j, 14); + } + else if (strstr(label, "ANTENNA: DELTA H/E/N")) + { + for (int i = 0, j = 0; i < 3; i++, j += 14) + del[i] = str2num(buff, j, 14); + + rnxRec.del[2] = del[0]; // h + rnxRec.del[0] = del[1]; // e + rnxRec.del[1] = del[2]; // n + } + // else if (strstr(label,"ANTENNA: DELTA X/Y/Z")) ; // opt ver.3 + // else if (strstr(label,"ANTENNA: PHASECENTER")) ; // opt ver.3 + // else if (strstr(label,"ANTENNA: B.SIGHT XYZ")) ; // opt ver.3 + // else if (strstr(label,"ANTENNA: ZERODIR AZI")) ; // opt ver.3 + // else if (strstr(label,"ANTENNA: ZERODIR XYZ")) ; // opt ver.3 + // else if (strstr(label,"CENTER OF MASS: XYZ" )) ; // opt ver.3 + else if (strstr(label, "SYS / # / OBS TYPES")) + { + // RINEX 3: Parse system-specific observation types + // Example: "G 16 C1C L1C D1C S1C C2S L2S D2S S2S C2W L2W D2W S2W C5Q" + + // get system from code letter + char code[] = "x00"; + code[0] = buff[0]; + + SatSys Sat(code); + + if (Sat.sys == E_Sys::NONE) + { + BOOST_LOG_TRIVIAL(debug) << "invalid system code: sys=" << code[0]; + + return; + } + + int n = (int)str2num(buff, 3, 3); + + BOOST_LOG_TRIVIAL(debug) << "RINEX3 processing " << n << " observation types for system " + << code[0]; + + for (int j = 0, k = 7; j < n; j++, k += 4) + { + if (k > 58) + { + // more on the next line + if (!std::getline(inputStream, line)) + break; + + buff = &line[0]; + k = 7; + } + + CodeType codeType; + codeType.type = buff[k]; + + // Extract 3-character observation code for RINEX 3 + char obsCode3str[] = "Lxx"; + obsCode3str[1] = buff[k + 1]; + obsCode3str[2] = buff[k + 2]; + + // Handle BeiDou B1 code special case for version 3.02 + if ((Sat.sys == E_Sys::BDS) && (obsCode3str[1] == '1') && (ver == 3.02)) + { + // change beidou B1 code: 3.02 draft -> 3.02 + obsCode3str[1] = '2'; + } + + try + { + // For RINEX 3, directly store the 3-character code in the code field + codeType.code = string_to_enum(obsCode3str); + + // Leave code2 as NONE for RINEX 3 since we have the full 3-character code + codeType.code2 = E_ObsCode2::NONE; + + BOOST_LOG_TRIVIAL(debug) + << "RINEX3 stored code: " << obsCode3str << " -> " + << enum_to_string(codeType.code) << " for system " << enum_to_string(Sat.sys); + } + catch (...) + { + BOOST_LOG_TRIVIAL(debug) << "invalid RINEX3 obs code: " << obsCode3str; + + codeType.code = E_ObsCode::NONE; + codeType.code2 = E_ObsCode2::NONE; + } + + sysCodeTypes[Sat.sys][j] = codeType; + } + + // if unknown code in ver.3, set default code + // for (auto& codeType : sysCodeTypes[Sat.sys]) + // { + // if (tobs[i][j][2]) + // continue; + // + // if (!(p = strchr(frqcodes, tobs[i][j][1]))) + // continue; + // + // // default codes for unknown code + // const char *defcodes[] = + // { + // "CWX ", // GPS: L125___ + // "CC ", // GLO: L12____ + // "X XXXX", // GAL: L1_5678 + // "CXXX ", // QZS: L1256__ + // "C X ", // SBS: L1_5___ + // "X XX " // BDS: L1__67_ + // }; + // tobs[i][j][2] = defcodes[i][(int)(p - frqcodes)]; + // + // BOOST_LOG_TRIVIAL(debug) + // << "set default for unknown code: sys=" << buff[0] + // << " code=" << tobs[i][j]; + // } + } + // else if (strstr(label,"WAVELENGTH FACT L1/2")) ; // opt ver.2 + else if (strstr(label, "# / TYPES OF OBSERV")) + { + // RINEX 2: Parse global observation types (applies to all systems) + // Example: " 10 C1 L1 S1 C2 P2 L2 S2 C5 L5" + + int n = (int)str2num(buff, 0, 6); + + BOOST_LOG_TRIVIAL(debug) << "RINEX2 processing " << n << " observation types"; + + for (int i = 0, j = 10; i < n; i++, j += 6) + { + if (j > 58) + { + // go onto new line + if (!std::getline(inputStream, line)) + break; + + buff = (char*)line.c_str(); + j = 10; + } + + if (ver <= 2.99) + { + // Extract 2-character observation code for RINEX 2 + char obsCode2str[3] = {}; + setstr(obsCode2str, buff + j, 2); + + // save the type char before processing + char typeChar = obsCode2str[0]; + + BOOST_LOG_TRIVIAL(debug) << "RINEX2 processing obs type: " << obsCode2str; + + // Process for all satellite systems since RINEX 2 doesn't specify per-system + for (E_Sys sys : enum_values()) + { + auto& recOpts = acsConfig.getRecOpts(rnxRec.id, {SatSys(sys, 0).sysName()}); + + CodeType codeType; + codeType.type = typeChar; + try + { + // For RINEX 2, primarily use code2 to store the original 2-character codes + BOOST_LOG_TRIVIAL(debug) << "RINEX2 converting code: " << obsCode2str + << " for system " << enum_to_string(sys); + E_ObsCode2 obsCode2 = string_to_enum(obsCode2str); + codeType.code2 = obsCode2; + + BOOST_LOG_TRIVIAL(debug) + << "RINEX2 stored code2: " << obsCode2str << " -> " + << enum_to_string(obsCode2) << " for system " << enum_to_string(sys); + + // Keep code as NONE for RINEX 2 - let downstream processing handle + // conversion if needed + codeType.code = E_ObsCode::NONE; + } + catch (...) + { + BOOST_LOG_TRIVIAL(warning) + << rnxRec.id << " Unknown RINEX2 code: " << obsCode2str; + + codeType.code2 = E_ObsCode2::NONE; + codeType.code = E_ObsCode::NONE; + } + + sysCodeTypes[sys][i] = codeType; + } + } + } + //*tobs[0][nt]='\0'; + } + // else if (strstr(label, "SIGNAL STRENGTH UNIT")) ; // opt ver.3 + // else if (strstr(label, "INTERVAL" )) ; // opt + else if (strstr(label, "TIME OF FIRST OBS")) + { + if (!strncmp(buff + 48, "GPS", 3)) + tsys = E_TimeSys::GPST; + else if (!strncmp(buff + 48, "GLO", 3)) + tsys = E_TimeSys::UTC; + else if (!strncmp(buff + 48, "GAL", 3)) + tsys = E_TimeSys::GST; + else if (!strncmp(buff + 48, "QZS", 3)) + tsys = E_TimeSys::QZSST; // ver.3.02 + else if (!strncmp(buff + 48, "BDT", 3)) + tsys = E_TimeSys::BDT; // ver.3.02 + } + // else if (strstr(label, "TIME OF LAST OBS" )) ; // opt + // else if (strstr(label, "RCV CLOCK OFFS APPL" )) ; // opt + // else if (strstr(label, "SYS / DCBS APPLIED" )) ; // opt ver.3 + // else if (strstr(label, "SYS / PCVS APPLIED" )) ; // opt ver.3 + // else if (strstr(label, "SYS / SCALE FACTOR" )) ; // opt ver.3 + // else if (strstr(label, "SYS / PHASE SHIFTS" )) ; // ver.3.01 + else if (strstr(label, "GLONASS SLOT / FRQ #")) + { + // ver.3.02 + p = buff + 4; + for (int i = 0; i < 8; i++, p += 7) + { + if (sscanf(p, "R%2d %2d", &prn, &fcn) < 2) + continue; + + SatSys Sat(E_Sys::GLO, prn); + + nav.gloFreqMap[Sat] = fcn; + } + } + else if (strstr(label, "GLONASS COD/PHS/BIS")) + { + // ver.3.02 + p = buff; + for (int i = 0; i < 4; i++, p += 13) + { + if (strncmp(p + 1, "C1C", 3)) + nav.glo_cpbias[0] = str2num(p, 5, 8); + else if (strncmp(p + 1, "C1P", 3)) + nav.glo_cpbias[1] = str2num(p, 5, 8); + else if (strncmp(p + 1, "C2C", 3)) + nav.glo_cpbias[2] = str2num(p, 5, 8); + else if (strncmp(p + 1, "C2P", 3)) + nav.glo_cpbias[3] = str2num(p, 5, 8); + } + } + else if (strstr(label, "LEAP SECONDS")) + { + // This would be GPS-UTC, and NOT optional as of RINEX 4 + nav.leaps = (int)str2num(buff, 0, 6); + } + // else if (strstr(label, "# OF SALTELLITES" )) ; // opt + // else if (strstr(label, "PRN / # OF OBS" )) ; // opt } -/** Decode nav header -*/ +// Decode RINEX navigation file header void decodeNavH( - string& line, ///< Line to decode - E_Sys sys, ///< GNSS system - Navigation& nav) ///< Navigation data + string& line, ///< Line to decode + E_Sys sys, ///< GNSS system + Navigation& nav ///< Navigation data +) { - char* buff = &line[0]; - char* label = buff + 60; - -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; - - if (strstr(label, "ION ALPHA" )) - { - // opt ver.2 - E_NavMsgType type = defNavMsgType[sys]; - GTime time = {}; - - ION& ionEntry = nav.ionMap[sys][type][time]; - - ionEntry.type = type; - ionEntry.Sat.sys = sys; - ionEntry.ttm = time; - - for (int i = 0, j = 2; i < 4; i++, j += 12) - ionEntry.vals[i] = str2num(buff, j, 12); - } - else if (strstr(label, "ION BETA" )) - { - // opt ver.2 - E_NavMsgType type = defNavMsgType[sys]; - GTime time = {}; - - ION& ionEntry = nav.ionMap[sys][type][time]; - - ionEntry.type = type; - ionEntry.Sat.sys = sys; - ionEntry.ttm = time; - - for (int i = 0, j = 2; i < 4; i++, j += 12) - ionEntry.vals[i + 4] = str2num(buff, j, 12); - } - else if (strstr(label, "DELTA-UTC: A0,A1,T,W")) - { - // opt ver.2 - E_NavMsgType type = defNavMsgType[sys]; - E_StoCode code = E_StoCode::NONE; - switch (sys) - { - case E_Sys::GPS: code = E_StoCode::GPUT; break; - case E_Sys::QZS: code = E_StoCode::QZUT; break; - case E_Sys::GAL: code = E_StoCode::GAUT; break; - } - - GTow tow = str2num(buff, 31, 9); - GWeek week = (int) str2num(buff, 40, 9); - GTime time(week, tow); - - STO& stoEntry = nav.stoMap[code][type][time]; - - stoEntry.type = type; - stoEntry.Sat.sys = sys; - stoEntry.tot = time; - stoEntry.code = code; - - stoEntry.A0 = str2num(buff, 3, 19); - stoEntry.A1 = str2num(buff, 22, 19); - stoEntry.A2 = 0; - } - else if (strstr(label, "IONOSPHERIC CORR" )) - { - // opt ver.3 - char sysStr[4] = ""; - strncpy(sysStr,buff,3); - sys = E_Sys::_from_string(sysStr); - E_NavMsgType type = defNavMsgType[sys]; - GTime time = {}; - - ION& ionEntry = nav.ionMap[sys][type][time]; - - ionEntry.type = type; - ionEntry.Sat.sys = sys; - ionEntry.Sat.prn = str2num(buff, 55, 3); - ionEntry.ttm = time; - - if ( buff[3] == 'A' - ||buff[3] == ' ') - { - for (int i = 0, j = 5; i < 4; i++, j += 12) - ionEntry.vals[i] = str2num(buff, j, 12); - } - else if ( buff[3] == 'B') - { - for (int i = 0, j = 5; i < 4; i++, j += 12) - ionEntry.vals[i + 4] = str2num(buff, j, 12); - } - } - else if (strstr(label, "TIME SYSTEM CORR" )) - { - // opt ver.3 - char codeStr[5] = ""; - strncpy(codeStr, buff, 4); - E_StoCode code = E_StoCode::_from_string(codeStr); - - char id[8] = ""; - strncpy(id, buff + 51, 5); - SatSys Sat = SatSys(id); - - if (Sat.sys == +E_Sys::NONE) - { - switch (code) - { - case E_StoCode::GPUT : Sat.sys = E_Sys::GPS; break; - case E_StoCode::GLUT : Sat.sys = E_Sys::GLO; break; - case E_StoCode::GAUT : Sat.sys = E_Sys::GAL; break; - case E_StoCode::BDUT : Sat.sys = E_Sys::BDS; break; - case E_StoCode::QZUT : Sat.sys = E_Sys::QZS; break; - case E_StoCode::SBUT : Sat.sys = E_Sys::SBS; break; - case E_StoCode::GAGP : Sat.sys = E_Sys::GAL; break; - case E_StoCode::QZGP : Sat.sys = E_Sys::QZS; break; - } - } - // UTC ID skipped - - E_NavMsgType type = defNavMsgType[Sat.sys]; - - double sec = str2num(buff, 38, 7); - double week = str2num(buff, 45, 5); - GTime time = {}; - if (Sat.sys != +E_Sys::BDS) { time = GTime(GWeek(week), GTow(sec)); } - else { time = GTime(BWeek(week), BTow(sec)); } - - STO& stoEntry = nav.stoMap[code][type][time]; - - stoEntry.type = type; - stoEntry.Sat = Sat; - stoEntry.tot = time; - stoEntry.ttm = time; - stoEntry.code = code; - - stoEntry.A0 = str2num(buff, 5, 17); - stoEntry.A1 = str2num(buff, 22, 16); - stoEntry.A2 = 0.0; - } - else if (strstr(label, "LEAP SECONDS" )) - { - // opt - nav.leaps=(int)str2num(buff, 0, 6); - } + char* buff = &line[0]; + char* label = buff + 60; + + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + + if (strstr(label, "ION ALPHA")) + { + // opt ver.2 + E_NavMsgType type = defNavMsgType[sys]; + GTime time = {}; + + ION& ionEntry = nav.ionMap[sys][type][time]; + + ionEntry.type = type; + ionEntry.Sat.sys = sys; + ionEntry.ttm = time; + + for (int i = 0, j = 2; i < 4; i++, j += 12) + ionEntry.vals[i] = str2num(buff, j, 12); + } + else if (strstr(label, "ION BETA")) + { + // opt ver.2 + E_NavMsgType type = defNavMsgType[sys]; + GTime time = {}; + + ION& ionEntry = nav.ionMap[sys][type][time]; + + ionEntry.type = type; + ionEntry.Sat.sys = sys; + ionEntry.ttm = time; + + for (int i = 0, j = 2; i < 4; i++, j += 12) + ionEntry.vals[i + 4] = str2num(buff, j, 12); + } + else if (strstr(label, "DELTA-UTC: A0,A1,T,W")) + { + // opt ver.2 + E_NavMsgType type = defNavMsgType[sys]; + E_StoCode code = E_StoCode::NONE; + switch (sys) + { + case E_Sys::GPS: + code = E_StoCode::GPUT; + break; + case E_Sys::QZS: + code = E_StoCode::QZUT; + break; + case E_Sys::GAL: + code = E_StoCode::GAUT; + break; + } + + GTow tow = str2num(buff, 31, 9); + GWeek week = (int)str2num(buff, 40, 9); + GTime time(week, tow); + + STO& stoEntry = nav.stoMap[code][type][time]; + + stoEntry.type = type; + stoEntry.Sat.sys = sys; + stoEntry.tot = time; + stoEntry.code = code; + + stoEntry.A0 = str2num(buff, 3, 19); + stoEntry.A1 = str2num(buff, 22, 19); + stoEntry.A2 = 0; + } + else if (strstr(label, "IONOSPHERIC CORR")) + { + // opt ver.3 + char sysStr[4] = ""; + strncpy(sysStr, buff, 3); + sys = string_to_enum(sysStr); + E_NavMsgType type = defNavMsgType[sys]; + GTime time = {}; + + ION& ionEntry = nav.ionMap[sys][type][time]; + + ionEntry.type = type; + ionEntry.Sat.sys = sys; + ionEntry.Sat.prn = str2num(buff, 55, 3); + ionEntry.ttm = time; + + if (buff[3] == 'A' || buff[3] == ' ') + { + for (int i = 0, j = 5; i < 4; i++, j += 12) + ionEntry.vals[i] = str2num(buff, j, 12); + } + else if (buff[3] == 'B') + { + for (int i = 0, j = 5; i < 4; i++, j += 12) + ionEntry.vals[i + 4] = str2num(buff, j, 12); + } + } + else if (strstr(label, "TIME SYSTEM CORR")) + { + // opt ver.3 + char codeStr[5] = ""; + strncpy(codeStr, buff, 4); + E_StoCode code = string_to_enum(codeStr); + + char id[8] = ""; + strncpy(id, buff + 51, 5); + SatSys Sat = SatSys(id); + + if (Sat.sys == E_Sys::NONE) + { + switch (code) + { + case E_StoCode::GPUT: + Sat.sys = E_Sys::GPS; + break; + case E_StoCode::GLUT: + Sat.sys = E_Sys::GLO; + break; + case E_StoCode::GAUT: + Sat.sys = E_Sys::GAL; + break; + case E_StoCode::BDUT: + Sat.sys = E_Sys::BDS; + break; + case E_StoCode::QZUT: + Sat.sys = E_Sys::QZS; + break; + case E_StoCode::SBUT: + Sat.sys = E_Sys::SBS; + break; + case E_StoCode::GAGP: + Sat.sys = E_Sys::GAL; + break; + case E_StoCode::QZGP: + Sat.sys = E_Sys::QZS; + break; + } + } + // UTC ID skipped + + E_NavMsgType type = defNavMsgType[Sat.sys]; + + double sec = str2num(buff, 38, 7); + double week = str2num(buff, 45, 5); + GTime time = {}; + if (Sat.sys != E_Sys::BDS) + { + time = GTime(GWeek(week), GTow(sec)); + } + else + { + time = GTime(BWeek(week), BTow(sec)); + } + + STO& stoEntry = nav.stoMap[code][type][time]; + + stoEntry.type = type; + stoEntry.Sat = Sat; + stoEntry.tot = time; + stoEntry.ttm = time; + stoEntry.code = code; + + stoEntry.A0 = str2num(buff, 5, 17); + stoEntry.A1 = str2num(buff, 22, 16); + stoEntry.A2 = 0.0; + } + else if (strstr(label, "LEAP SECONDS")) + { + // opt + nav.leaps = (int)str2num(buff, 0, 6); + } } -/** Decode gnav header -*/ -void decodeGnavH( - string& line, - Navigation& nav) +// Decode GLONASS navigation file header +void decodeGnavH(string& line, Navigation& nav) { - char* buff = &line[0]; - char* label = buff + 60; - -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; - - if (strstr(label, "CORR TO SYTEM TIME" )) ; // opt - else if (strstr(label, "LEAP SECONDS" )) - { - // opt - nav.leaps=(int)str2num(buff, 0, 6); - } + char* buff = &line[0]; + char* label = buff + 60; + + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + + if (strstr(label, "CORR TO SYTEM TIME")) + ; // opt + else if (strstr(label, "LEAP SECONDS")) + { + // opt + nav.leaps = (int)str2num(buff, 0, 6); + } } -/** Decode geo nav header -*/ -void decodeHnavH( - string& line, - Navigation& nav) +// Decode SBAS/geostationary navigation file header +void decodeHnavH(string& line, Navigation& nav) { - char* buff = &line[0]; - char* label = buff + 60; - -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; - - if (strstr(label, "CORR TO SYTEM TIME" )) ; // opt - else if (strstr(label, "D-UTC A0,A1,T,W,S,U" )) ; // opt - else if (strstr(label, "LEAP SECONDS" )) - { - // opt - nav.leaps= (int)str2num(buff, 0, 6); - } + char* buff = &line[0]; + char* label = buff + 60; + + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + + if (strstr(label, "CORR TO SYTEM TIME")) + ; // opt + else if (strstr(label, "D-UTC A0,A1,T,W,S,U")) + ; // opt + else if (strstr(label, "LEAP SECONDS")) + { + // opt + nav.leaps = (int)str2num(buff, 0, 6); + } } -/** Read rinex header -*/ +// Read RINEX file header section int readRnxH( - std::istream& inputStream, - double& ver, - char& type, - E_Sys& sys, - E_TimeSys& tsys, - map>& sysCodeTypes, - Navigation& nav, - RinexStation& rnxRec) + std::istream& inputStream, + double& ver, + char& type, + E_Sys& sys, + E_TimeSys& tsys, + map>& sysCodeTypes, + Navigation& nav, + RinexStation& rnxRec +) { - string line; - int i = 0; - int block = 0; - -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; - - ver = 2.10; - type = ' '; - sys = E_Sys::GPS; - tsys = E_TimeSys::GPST; - - char sysChar = '\0'; - int typeOffset = 20; - int sysCharOffset = 40; - - while (std::getline(inputStream, line)) - { - char* buff = &line[0]; - char* label = buff + 60; - - if (line.length() <= 60) - { - continue; - } - else if (strstr(label, "RINEX VERSION / TYPE")) - { - ver = str2num(buff, 0, 9); - - type = buff[typeOffset]; - - sysChar = buff[sysCharOffset]; - - // possible error in generation by one manufacturer. This hack gets around it - if(ver == 3.04 && type == ' ') - { - typeOffset += 1; - sysCharOffset += 2; - type = buff[typeOffset]; - - sysChar = buff[sysCharOffset]; - } - - // satellite system - switch (sysChar) - { - case ' ': - case 'G': sys = E_Sys::GPS; tsys = E_TimeSys::GPST; break; - case 'R': sys = E_Sys::GLO; tsys = E_TimeSys::UTC; break; - case 'E': sys = E_Sys::GAL; tsys = E_TimeSys::GST; break; // v.2.12 - case 'S': sys = E_Sys::SBS; tsys = E_TimeSys::GPST; break; - case 'J': sys = E_Sys::QZS; tsys = E_TimeSys::QZSST; break; // v.3.02 - case 'C': sys = E_Sys::BDS; tsys = E_TimeSys::BDT; break; // v.2.12 - case 'M': sys = E_Sys::NONE; tsys = E_TimeSys::GPST; break; // mixed - default : - BOOST_LOG_TRIVIAL(debug) - << "unsupported satellite system: " << sysChar; - - break; - } - continue; - } - else if (strstr(label, "PGM / RUN BY / DATE")) - continue; - else if (strstr(label, "COMMENT")) - { - // read cnes wl satellite fractional bias - if ( strstr(buff, "WIDELANE SATELLITE FRACTIONAL BIASES") - ||strstr(buff, "WIDELANE SATELLITE FRACTIONNAL BIASES")) - { - block = 1; - } - if (strstr(buff, "->")) - { - //may be a conversion line, test - - char sysChar; - char r3[4] = {}; - char r2[3] = {}; - char comment[81]; - int num = sscanf(buff, " %c %3c -> %2c %80s", &sysChar, r3, r2, comment); - - - if ( num == 4 - &&(string)comment == "COMMENT") - { - try - { - E_Sys sys = SatSys::sysFromChar(sysChar); - - char code = r3[0]; - r3[0] = 'L'; - auto obs2 = E_ObsCode2 ::_from_string_nocase(r2); - auto obs3 = E_ObsCode ::_from_string_nocase(r3); - - auto& recOpts = acsConfig.getRecOpts(rnxRec.id, {SatSys(sys, 0).sysName()}); - - auto& codeMap = recOpts.rinex23Conv.codeConv; - auto& phasMap = recOpts.rinex23Conv.phasConv; - - if (r2[0] == 'C' || r2[0] == 'P') codeMap[obs2] = obs3; - else phasMap[obs2] = obs3; - } - catch (...) - { - - } - } - } - else if (block) - { - //ignore reported widelane biases - - // double bias; - // SatSys Sat; - // - // // cnes/cls grg clock - // if ( !strncmp(buff, "WL", 2) - // &&(Sat = SatSys(buff + 3), Sat) - // && sscanf(buff+40, "%lf", &bias) == 1) - // { - // nav.satNavMap[Sat].wlbias = bias; - // } - // // cnes ppp-wizard clock - // else if ((Sat = SatSys(buff + 1), Sat) - // &&sscanf(buff+6, "%lf", &bias) == 1) - // { - // nav.satNavMap[Sat].wlbias = bias; - // } - } - continue; - } - // file type - switch (type) - { - case 'O': decodeObsH(inputStream, line, ver, tsys, sysCodeTypes, nav, rnxRec); break; - case 'N': decodeNavH ( line, sys, nav); break; // GPS (ver.2) or mixed (ver.3) - case 'G': decodeGnavH ( line, nav); break; - case 'H': decodeHnavH ( line, nav); break; - case 'J': decodeNavH ( line, E_Sys::QZS, nav); break; // extension - case 'E': //fallthrough - case 'L': decodeNavH ( line, E_Sys::GAL, nav); break; // extension - } - if (strstr(label, "END OF HEADER")) - return 1; - - if (++i >= MAXPOSHEAD - &&type == ' ') - { - break; // no rinex file - } - } - return 0; + string line; + int i = 0; + int block = 0; + + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + + ver = 2.10; + type = ' '; + sys = E_Sys::GPS; + tsys = E_TimeSys::GPST; + + char sysChar = '\0'; + int typeOffset = 20; + int sysCharOffset = 40; + + while (std::getline(inputStream, line)) + { + char* buff = &line[0]; + char* label = buff + 60; + + if (line.length() <= 60) + { + continue; + } + else if (strstr(label, "RINEX VERSION / TYPE")) + { + ver = str2num(buff, 0, 9); + + type = buff[typeOffset]; + + sysChar = buff[sysCharOffset]; + + // possible error in generation by one manufacturer. This hack gets around it + if (ver == 3.04 && type == ' ') + { + typeOffset += 1; + sysCharOffset += 2; + type = buff[typeOffset]; + + sysChar = buff[sysCharOffset]; + } + + // satellite system + switch (sysChar) + { + case ' ': + case 'G': + sys = E_Sys::GPS; + tsys = E_TimeSys::GPST; + break; + case 'R': + sys = E_Sys::GLO; + tsys = E_TimeSys::UTC; + break; + case 'E': + sys = E_Sys::GAL; + tsys = E_TimeSys::GST; + break; // v.2.12 + case 'S': + sys = E_Sys::SBS; + tsys = E_TimeSys::GPST; + break; + case 'J': + sys = E_Sys::QZS; + tsys = E_TimeSys::QZSST; + break; // v.3.02 + case 'C': + sys = E_Sys::BDS; + tsys = E_TimeSys::BDT; + break; // v.2.12 + case 'M': + sys = E_Sys::NONE; + tsys = E_TimeSys::GPST; + break; // mixed + default: + BOOST_LOG_TRIVIAL(debug) << "unsupported satellite system: " << sysChar; + + break; + } + continue; + } + else if (strstr(label, "PGM / RUN BY / DATE")) + continue; + else if (strstr(label, "COMMENT")) + { + // read cnes wl satellite fractional bias + if (strstr(buff, "WIDELANE SATELLITE FRACTIONAL BIASES") || + strstr(buff, "WIDELANE SATELLITE FRACTIONNAL BIASES")) + { + block = 1; + } + if (strstr(buff, "->")) + { + // may be a conversion line, test + // @todo: not sure what this is about... + char sysChar; + char r3[4] = {}; + char r2[3] = {}; + char comment[81]; + int num = sscanf(buff, " %c %3c -> %2c %80s", &sysChar, r3, r2, comment); + + if (num == 4 && (string)comment == "COMMENT") + { + try + { + E_Sys sys = SatSys::sysFromChar(sysChar); + + char code = r3[0]; + r3[0] = 'L'; + auto obs2 = string_to_enum_nocase(r2); + auto obs3 = string_to_enum_nocase(r3); + + auto& recOpts = acsConfig.getRecOpts(rnxRec.id, {SatSys(sys, 0).sysName()}); + + auto& codeMap = recOpts.rinex23Conv.codeConv; + auto& phasMap = recOpts.rinex23Conv.phasConv; + + if (r2[0] == 'C' || r2[0] == 'P') + { + codeMap[obs2] = obs3; + } + else + { + // For phase conversions, create a single-element vector + phasMap[obs2].push_back(obs3); + } + } + catch (...) + { + } + } + } + else if (block) + { + // ignore reported widelane biases + + // double bias; + // SatSys Sat; + // + // // cnes/cls grg clock + // if ( !strncmp(buff, "WL", 2) + // &&(Sat = SatSys(buff + 3), Sat) + // && sscanf(buff+40, "%lf", &bias) == 1) + // { + // nav.satNavMap[Sat].wlbias = bias; + // } + // // cnes ppp-wizard clock + // else if ((Sat = SatSys(buff + 1), Sat) + // &&sscanf(buff+6, "%lf", &bias) == 1) + // { + // nav.satNavMap[Sat].wlbias = bias; + // } + } + continue; + } + // file type + switch (type) + { + case 'O': + decodeObsH(inputStream, line, ver, tsys, sysCodeTypes, nav, rnxRec); + break; + case 'N': + decodeNavH(line, sys, nav); + break; // GPS (ver.2) or mixed (ver.3) + case 'G': + decodeGnavH(line, nav); + break; + case 'H': + decodeHnavH(line, nav); + break; + case 'J': + decodeNavH(line, E_Sys::QZS, nav); + break; // extension + case 'E': // fallthrough + case 'L': + decodeNavH(line, E_Sys::GAL, nav); + break; // extension + } + if (strstr(label, "END OF HEADER")) + { + return 1; + } + + if (++i >= MAXPOSHEAD && type == ' ') + { + break; // no rinex file + } + } + return 0; } -/** Decode obs epoch -*/ +// Decode observation epoch header int decodeObsEpoch( - std::istream& inputStream, - string& line, - double ver, - E_TimeSys tsys, - GTime& time, - int& flag, - vector& sats) + std::istream& inputStream, + string& line, + double ver, + E_TimeSys tsys, + GTime& time, + int& flag, + vector& sats +) +{ + int n = 0; + char* buff = &line[0]; + + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver; + + if (ver <= 2.99) + { + // ver.2 + n = (int)str2num(buff, 29, 3); + if (n <= 0) + return 0; + + // epoch flag: 3:new site,4:header info,5:external event + flag = (int)str2num(buff, 28, 1); + + if (flag >= 3 && flag <= 5) + { + return n; + } + + bool error = str2time(buff, 0, 26, time, tsys); + if (error) + { + BOOST_LOG_TRIVIAL(debug) << "rinex obs invalid epoch: epoch=" << buff; + + return 0; + } + + for (int i = 0, j = 32; i < n; i++, j += 3) + { + if (j >= 68) + { + // more on the next line + if (!std::getline(inputStream, line)) + break; + + buff = &line[0]; + + j = 32; + } + + char id[4] = {}; + strncpy(id, buff + j, 3); + sats.push_back(SatSys(id)); + } + } + else + { + // ver.3 + n = (int)str2num(buff, 32, 3); + if (n <= 0) + { + return 0; + } + + flag = (int)str2num(buff, 31, 1); + + if (flag >= 3 && flag <= 5) + return n; + + if (buff[0] != '>' || str2time(buff, 1, 28, time, tsys)) + { + BOOST_LOG_TRIVIAL(debug) << "rinex obs invalid epoch: epoch=" << buff; + return 0; + } + } + + // BOOST_LOG_TRIVIAL(debug) + // << "__FUNCTION__: time=" << time.to_string(3) + // << " flag=" << flag; + + return n; +} + +/** Decode RINEX 2 observation data + */ +int decodeObsDataRinex2( + std::istream& inputStream, + string& line, + map>& sysCodeTypes, + GObs& obs, + SatSys& v2SatSys, + RinexStation& rnxRec +) { - int n = 0; - char* buff = &line[0]; - -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver; - - if (ver <= 2.99) - { - // ver.2 - n = (int) str2num(buff, 29, 3); - if (n <= 0) - return 0; - - // epoch flag: 3:new site,4:header info,5:external event - flag = (int) str2num(buff, 28, 1); - - if ( flag >= 3 - &&flag <= 5) - { - return n; - } - - bool error = str2time(buff, 0, 26, time, tsys); - if (error) - { - BOOST_LOG_TRIVIAL(debug) - << "rinex obs invalid epoch: epoch=" << buff; - - return 0; - } - - for (int i = 0, j = 32; i < n; i++, j += 3) - { - if (j >= 68) - { - //more on the next line - if (!std::getline(inputStream, line)) - break; - - buff = &line[0]; - - j = 32; - } - - char id[4] = {}; - strncpy(id, buff + j, 3); - sats.push_back(SatSys(id)); - } - } - else - { - // ver.3 - n = (int) str2num(buff, 32, 3); - if (n <= 0) - { - return 0; - } - - flag = (int) str2num(buff, 31, 1); - - if ( flag >= 3 - &&flag <= 5) - return n; - - if ( buff[0] != '>' - ||str2time(buff, 1, 28, time, tsys)) - { - BOOST_LOG_TRIVIAL(debug) - << "rinex obs invalid epoch: epoch=" << buff; - return 0; - } - } - -// BOOST_LOG_TRIVIAL(debug) -// << "__FUNCTION__: time=" << time.to_string(3) -// << " flag=" << flag; - - return n; + char* buff = &line[0]; + int stat = 1; + + // RINEX 2: Use satellite from epoch header + obs.Sat = v2SatSys; + + if (!obs.Sat) + { + BOOST_LOG_TRIVIAL(debug) << "decodeObsDataRinex2: unsupported sat"; + stat = 0; + } + + if (!stat) + return 0; + + // Defensive check for valid satellite system before accessing sysCodeTypes + if (obs.Sat.sys == E_Sys::NONE || static_cast(obs.Sat.sys) < 0) + { + BOOST_LOG_TRIVIAL(error) << "RINEX2: Invalid satellite system: " + << enum_to_string(obs.Sat.sys); + return 0; + } + + // Check if the system exists in sysCodeTypes map + if (sysCodeTypes.find(obs.Sat.sys) == sysCodeTypes.end()) + { + BOOST_LOG_TRIVIAL(error) << "RINEX2: System " << enum_to_string(obs.Sat.sys) + << " not found in sysCodeTypes"; + return 0; + } + + auto& codeTypes = sysCodeTypes[obs.Sat.sys]; + int j = 0; // RINEX 2 starts at position 0 + + BOOST_LOG_TRIVIAL(debug) << "RINEX2: About to process satellite " << obs.Sat.id() << " with " + << codeTypes.size() << " code types, line size: " << line.size(); + + // Check for valid line buffer + if (line.empty() || line.size() < 16) + { + BOOST_LOG_TRIVIAL(error) << "RINEX2: Invalid or too short line buffer (size: " + << line.size() << ")"; + return 0; + } + + BOOST_LOG_TRIVIAL(debug) << "RINEX2: Accessing receiver options for " << rnxRec.id; + + // Additional safety check for acsConfig + try + { + auto testSys = SatSys(obs.Sat.sys, 0); + BOOST_LOG_TRIVIAL(debug) << "RINEX2: Created test SatSys: " << testSys.sysName(); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "RINEX2: Error creating SatSys: " << e.what(); + throw; + } + + auto& recOpts = acsConfig.getRecOpts(rnxRec.id, {SatSys(obs.Sat.sys, 0).sysName()}); + BOOST_LOG_TRIVIAL(debug) << "RINEX2: Got receiver options, accessing conversion maps"; + auto& codeMap = recOpts.rinex23Conv.codeConv; + auto& phasMap = recOpts.rinex23Conv.phasConv; + BOOST_LOG_TRIVIAL(debug) << "RINEX2: Successfully accessed conversion maps, starting staging"; + + // Stage 1: Collect all observations, storing priority arrays for phase observations + ObservationStaging staging; + BOOST_LOG_TRIVIAL(debug) << "RINEX2: Processing observations with priority staging"; + if (line.size() < 80) + line.append(80 - line.size(), ' '); // Ensure line is at least 80 characters + + for (auto& [index, codeType] : codeTypes) + { + // RINEX 2: Check for line continuation + if (j >= 80) + { + if (!std::getline(inputStream, line)) + break; + if (line.size() < 80) + line.append(80 - line.size(), ' '); // Ensure line is at least 80 characters + + // Validate new line + if (line.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "RINEX2: Empty continuation line"; + break; + } + buff = &line[0]; + j = 0; + } + + // check if codeType.code2 is in codeMap or phasMap(inside an array) + + E_ObsCode effectiveCode = E_ObsCode::NONE; + vector priorityCodes; + bool isPhaseObservation = false; + + // Determine which map to use based on observation type + if (codeType.type == 'C' || codeType.type == 'P') + { + // Code/Pseudorange observations - use codeMap + auto it = codeMap.find(codeType.code2); + if (it != codeMap.end()) + { + effectiveCode = it->second; + BOOST_LOG_TRIVIAL(debug) << "RINEX2: Found code2 " << enum_to_string(codeType.code2) + << " in codeMap -> " << enum_to_string(effectiveCode); + } + else + { + BOOST_LOG_TRIVIAL(warning) << "RINEX2: code2 " << enum_to_string(codeType.code2) + << " not found in codeMap for type " << codeType.type + << ", sat=" << obs.Sat.id(); + } + } + else if (codeType.type == 'L') + { + // Phase observations - store priority array for later resolution + auto it = phasMap.find(codeType.code2); + if (it != phasMap.end() && !it->second.empty()) + { + priorityCodes = it->second; + effectiveCode = priorityCodes[0]; // Temporary for frequency lookup + isPhaseObservation = true; + BOOST_LOG_TRIVIAL(debug) << "RINEX2: Found phase priorities for " + << enum_to_string(codeType.code2) << " -> [" << [&]() + { + string codes; + for (size_t i = 0; i < priorityCodes.size(); ++i) + { + if (i > 0) + codes += ","; + codes += enum_to_string(priorityCodes[i]); + } + return codes; + }() << "]"; + } + else + { + BOOST_LOG_TRIVIAL(warning) << "RINEX2: code2 " << enum_to_string(codeType.code2) + << " not found in phasMap for type " << codeType.type + << ", sat=" << obs.Sat.id(); + } + } + + E_FType ft = code2Freq[obs.Sat.sys][effectiveCode]; + + // Parse observation values + ObservationValues obsValues = parseObservationValues(buff, j); + + // Stage the observation - use appropriate staging method + if (isPhaseObservation) + { + stagePhaseObservation( + staging, + codeType.type, + priorityCodes, + ft, + obsValues.value, + obsValues.lli + ); + } + else + { + stageObservation( + staging, + codeType.type, + effectiveCode, + ft, + obsValues.value, + obsValues.lli + ); + } + j += 16; + // } + } + + // Stage 2: Validate all staged observations with conflict resolution + ValidationReport report = validateStagedObservationsDetailed(staging, obs.Sat); + if (!report.passed) + { + BOOST_LOG_TRIVIAL(warning) + << "RINEX2: " << rnxRec.id << " Validation failed for satellite " << obs.Sat.id() + << " (valid: " << report.validObservations << "/" << report.totalObservations << ")"; + return 0; + } + + // Stage 3: Commit all validated observations to final structure + commitStagedObservations(staging, obs, codeMap); + + // // Debug: Show final committed observations with L, P, and LLI values + BOOST_LOG_TRIVIAL(debug) << "Final committed observations for " << obs.Sat.id() << ":"; + for (const auto& [fType, sigsList] : obs.sigsLists) + { + for (const auto& sig : sigsList) + { + BOOST_LOG_TRIVIAL(debug) + << " " << enum_to_string(sig.code) << ": L=" << std::setprecision(16) << sig.L + << ", P=" << std::setprecision(16) << sig.P + << ", LLI=" << static_cast(sig.LLI); + } + } + + return 1; } -/** Decode obs data -*/ +/** Decode RINEX 3 observation data + */ +int decodeObsDataRinex3( + std::istream& inputStream, + string& line, + map>& sysCodeTypes, + GObs& obs, + RinexStation& rnxRec +) +{ + char satid[8] = ""; + char* buff = &line[0]; + int stat = 1; + + // RINEX 3: Extract satellite ID from observation line + strncpy(satid, buff, 3); + obs.Sat = SatSys(satid); + + if (!obs.Sat) + { + BOOST_LOG_TRIVIAL(debug) << "decodeObsDataRinex3: unsupported sat sat=" << satid; + stat = 0; + } + + if (!stat) + return 0; + + auto& codeTypes = sysCodeTypes[obs.Sat.sys]; + int j = 3; // RINEX 3 starts after 3-character satellite ID + + // Stage 1: Collect all observations into staging area + ObservationStaging staging; + + for (auto& [index, codeType] : codeTypes) + { + E_FType ft = code2Freq[obs.Sat.sys][codeType.code]; + + // Parse observation values using SRP helper function + ObservationValues obsValues = parseObservationValues(buff, j); + + // Stage the observation instead of immediately committing + stageObservation(staging, codeType.type, codeType.code, ft, obsValues.value, obsValues.lli); + + j += 16; + } + + // Stage 2: Validate all staged observations with conflict resolution + ValidationReport report = validateStagedObservationsDetailed(staging, obs.Sat); + if (!report.passed) + { + BOOST_LOG_TRIVIAL(warning) + << "RINEX3: Validation failed for satellite " << obs.Sat.id() + << " (valid: " << report.validObservations << "/" << report.totalObservations << ")"; + return 0; + } + + // Stage 3: Commit all validated observations to final structure + // RINEX 3 doesn't use code conversion maps, so provide empty map + map emptyCodeMap; + commitStagedObservations(staging, obs, emptyCodeMap); + + return 1; +} + +/** Decode obs data (dispatcher function) + */ int decodeObsData( - std::istream& inputStream, - string& line, - double ver, - map>& sysCodeTypes, - GObs& obs, - SatSys& v2SatSys) + std::istream& inputStream, + string& line, + double ver, + map>& sysCodeTypes, + GObs& obs, + SatSys& v2SatSys, + RinexStation& rnxRec +) { - char satid[8] = ""; - int stat = 1; - char* buff = &line[0]; - -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver; - - if (ver > 2.99) - { - // ver.3 - strncpy(satid, buff, 3); - obs.Sat = SatSys(satid); - } - else - { - obs.Sat = v2SatSys; - } - - if (!obs.Sat) - { - BOOST_LOG_TRIVIAL(debug) - << "decodeObsdata: unsupported sat sat=" << satid; - - stat = 0; - } - - auto& codeTypes = sysCodeTypes[obs.Sat.sys]; - - int j; - if (ver <= 2.99) j = 0; - else j = 3; - - if (!stat) - return 0; - - for (auto& [index, codeType] : codeTypes) - { - if ( ver <= 2.99 - &&j >= 80) - { - // ver.2 - if (!std::getline(inputStream, line)) - break; - buff = &line[0]; - j = 0; - } - - E_FType ft = code2Freq[obs.Sat.sys][codeType.code]; - - RawSig* rawSig = nullptr; - auto& sigList = obs.sigsLists[ft]; - - for (auto& sig : sigList) - { - if (sig.code == codeType.code) - { - rawSig = &sig; - break; - } - } - - if (rawSig == nullptr) - { - RawSig raw; - raw.code = codeType.code; - - sigList.push_back(raw); - rawSig = &sigList.back(); - } - - double val = str2num(buff, j, 14); - double lli = str2num(buff, j + 14, 1); - lli = (unsigned char) lli & 0x03; - - RawSig& sig = *rawSig; - if (val) - switch (codeType.type) - { - case 'P': //fallthrough - case 'C': sig.P = val; break; - case 'L': sig.L = val; sig.LLI = lli; break; - case 'D': sig.D = val; break; - case 'S': sig.snr = val; break; - } - - j += 16; - } - -// BOOST_LOG_TRIVIAL(debug) -// << "decodeObsdata: time=" << obs.time.to_string() -// << " sat=" << obs.Sat.id(); - - return 1; + if (ver <= 2.99) + { + return decodeObsDataRinex2(inputStream, line, sysCodeTypes, obs, v2SatSys, rnxRec); + } + else + { + return decodeObsDataRinex3(inputStream, line, sysCodeTypes, obs, rnxRec); + } } -/** Read rinex obs data body -*/ +// Read RINEX observation data body int readRnxObsB( - std::istream& inputStream, - double ver, - E_TimeSys tsys, - map>& sysCodeTypes, - int& flag, - ObsList& obsList) + std::istream& inputStream, + double ver, + E_TimeSys tsys, + map>& sysCodeTypes, + int& flag, + ObsList& obsList, + RinexStation& rnxRec +) { - GTime time = {}; - int i = 0; - int nSats = 0; //cant replace with sats.size() - vector sats; - - // read record - string line; - std::streampos pos; - while (pos = inputStream.tellg(), std::getline(inputStream, line)) - { - // decode obs epoch - if (i == 0) - { - nSats = decodeObsEpoch(inputStream, line, ver, tsys, time, flag, sats); - if (nSats <= 0) - { - continue; - } - } - else if (line[0] == '>') - { - BOOST_LOG_TRIVIAL(warning) << "Warning: unexpected end of epoch in rinex file at " << time; - inputStream.seekg(pos); - return obsList.size(); - } - else if ( flag <= 2 - ||flag == 6) - { - GObs rawObs = {}; - - rawObs.time = time; - - // decode obs data - bool pass = decodeObsData(inputStream, line, ver, sysCodeTypes, rawObs, sats[i-1]); - if (pass) - { - // save obs data - obsList.push_back((shared_ptr)rawObs); - } - } - - i++; - - if (i > nSats) - return obsList.size(); - } - - return -1; + GTime time = {}; + int i = 0; + int nSats = 0; // cant replace with sats.size() + vector sats; + + // read record + string line; + std::streampos pos; + while (pos = inputStream.tellg(), std::getline(inputStream, line)) + { + // decode obs epoch + if (i == 0) + { + nSats = decodeObsEpoch(inputStream, line, ver, tsys, time, flag, sats); + if (nSats <= 0) + { + continue; + } + } + else if (line[0] == '>') + { + BOOST_LOG_TRIVIAL(warning) << "Unexpected end of epoch in rinex file at " << time; + inputStream.seekg(pos); + return obsList.size(); + } + else if (flag <= 2 || flag == 6) + { + GObs rawObs = {}; + + rawObs.time = time; + bool pass = false; + // decode obs data + if (ver <= 2.99) + { + // RINEX 2: Pass satellite from epoch header + SatSys v2SatSys = sats[i - 1]; + bool pass = + decodeObsData(inputStream, line, ver, sysCodeTypes, rawObs, v2SatSys, rnxRec); + if (pass) + { + // save obs data + obsList.push_back((shared_ptr)rawObs); + } + } + else + { + // RINEX 3: Satellite ID is in observation line + SatSys dummySatSys; // Not used in RINEX 3 + bool pass = decodeObsData( + inputStream, + line, + ver, + sysCodeTypes, + rawObs, + dummySatSys, + rnxRec + ); + if (pass) + { + // save obs data + obsList.push_back((shared_ptr)rawObs); + } + } + + if (pass) + { + // save obs data + obsList.push_back((shared_ptr)rawObs); + } + } + + i++; + + if (i > nSats) + return obsList.size(); + } + + return -1; } -/** Read rinex obs -*/ +// Read complete RINEX observation file int readRnxObs( - std::istream& inputStream, - double ver, - E_TimeSys tsys, - map>& sysCodeTypes, - ObsList& obsList, - RinexStation& rnxRec) + std::istream& inputStream, + double ver, + E_TimeSys tsys, + map>& sysCodeTypes, + ObsList& obsList, + RinexStation& rnxRec +) { - int flag = 0; - int stat = 0; + int flag = 0; + int stat = 0; -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver << " tsys=" << tsys; + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver << " tsys=" << tsys; - // read rinex obs data body - int n = readRnxObsB(inputStream, ver, tsys, sysCodeTypes, flag, obsList); + // read rinex obs data body + int n = readRnxObsB(inputStream, ver, tsys, sysCodeTypes, flag, obsList, rnxRec); - if (n >= 0) - stat = 1; + if (n >= 0) + stat = 1; - return stat; + return stat; } -/** Decode ephemeris -*/ -int decodeEph( - double ver, - SatSys Sat, - GTime toc, - vector& data, - Eph& eph) +// Decode GPS/Galileo/QZS/BeiDou ephemeris +int decodeEph(double ver, SatSys Sat, GTime toc, vector& data, Eph& eph) { -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver << " sat=" << Sat.id(); - - int sys = Sat.sys; - - if ( sys != +E_Sys::GPS - &&sys != +E_Sys::GAL - &&sys != +E_Sys::QZS - &&sys != +E_Sys::BDS) - { - BOOST_LOG_TRIVIAL(debug) - << "ephemeris error: invalid satellite sat=" << Sat.id(); - - return 0; - } - - eph.type = defNavMsgType[Sat.sys]; - eph.Sat = Sat; - eph.toc = toc; - - eph.f0 = data[0]; - eph.f1 = data[1]; - eph.f2 = data[2]; - eph.crs = data[ 4]; - eph.deln = data[ 5]; - eph.M0 = data[ 6]; - eph.cuc = data[ 7]; - eph.e = data[ 8]; - eph.cus = data[ 9]; - eph.sqrtA = data[10]; eph.A = SQR(eph.sqrtA); - eph.toes = data[11]; // toe (s) in gps/bdt week - eph.cic = data[12]; - eph.OMG0 = data[13]; - eph.cis = data[14]; - eph.i0 = data[15]; - eph.crc = data[16]; - eph.omg = data[17]; - eph.OMGd = data[18]; - eph.idot = data[19]; - eph.week =(int)data[21]; // gps/bdt week - eph.ttms = data[27]; - - if ( sys == +E_Sys::GPS - ||sys == +E_Sys::QZS) - { - eph.iode =(int)data[ 3]; // IODE - eph.iodc =(int)data[26]; // IODC - eph.toe = GTime(GTow(eph.toes), eph.toc); - eph.ttm = GTime(GTow(eph.ttms), eph.toc); - - eph.code =(int) data[20]; // GPS: codes on L2 ch - eph.svh =(E_Svh) data[24]; // sv health - eph.sva =uraToSva( data[23]); // ura (m->index) - eph.flag =(int) data[22]; // GPS: L2 P data flag - - eph.tgd[0] = data[25]; // TGD - - if (sys == +E_Sys::GPS) { eph.fit = data[28]; } // fit interval in hours for GPS - else if (sys == +E_Sys::QZS) { eph.fitFlag = data[28]; eph.fit = eph.fitFlag?0.0:2.0; } // fit interval flag for QZS - - if (acsConfig.use_tgd_bias) - decomposeTGDBias(Sat, eph.tgd[0]); - } - else if ( sys == +E_Sys::GAL) - { - // GAL ver.3 - eph.iode =(int)data[ 3]; // IODnav - eph.toe = GTime(GTow(eph.toes), eph.toc); - eph.ttm = GTime(GTow(eph.ttms), eph.toc); - - eph.code =(int)data[20]; // data sources - // bit 0 set: I/NAV E1-B - // bit 1 set: F/NAV E5a-I - // bit 2 set: I/NAV E5b-I - // bit 8 set: af0-af2 toc are for E5a.E1 - // bit 9 set: af0-af2 toc are for E5b.E1 - unsigned short iNavMask = 0x0005; - unsigned short fNavMask = 0x0002; - if (eph.code & iNavMask) eph.type = E_NavMsgType::INAV; - else if (eph.code & fNavMask) eph.type = E_NavMsgType::FNAV; - - eph.svh =(E_Svh)data[24]; // sv health - // bit 0: E1B DVS - // bit 1-2: E1B HS - // bit 3: E5a DVS - // bit 4-5: E5a HS - // bit 6: E5b DVS - // bit 7-8: E5b HS - eph.sva =sisaToSva(data[23]); - - eph.tgd[0]= data[25]; // BGD E5a/E1 - eph.tgd[1]= data[26]; // BGD E5b/E1 - - if (acsConfig.use_tgd_bias) - decomposeBGDBias(Sat, eph.tgd[0], eph.tgd[1]); - } - else if ( sys == +E_Sys::BDS) - { - // BeiDou v.3.02 - if (Sat.prn > 5 && Sat.prn < 59) eph.type = E_NavMsgType::D1; // MEO/IGSO - else eph.type = E_NavMsgType::D2; // GEO, prn range may change in the future*/ - - eph.tocs = BTow(toc); - eph.aode =(int)data[ 3]; // AODE - eph.aodc =(int)data[28]; // AODC - eph.iode = int(eph.tocs / 720) % 240; - eph.iodc = eph.iode + 256 * int(eph.tocs / 172800) % 4; - eph.toe = GTime(BTow(eph.toes), eph.toc); - eph.ttm = GTime(BTow(eph.ttms), eph.toc); - - eph.svh =(E_Svh)data[24]; // satH1 - eph.sva =uraToSva(data[23]); // ura (m->index) - - eph.tgd[0] = data[25]; // TGD1 B1/B3 - eph.tgd[1] = data[26]; // TGD2 B2/B3 - } - - if ( eph.iode < 0 - ||eph.iode > 1023) - { - BOOST_LOG_TRIVIAL(debug) - << "rinex nav invalid: sat=" << Sat.id() << " iode=" << eph.iode; - } - - if ( eph.iodc < 0 - ||eph.iodc > 1023) - { - BOOST_LOG_TRIVIAL(debug) - << "rinex nav invalid: sat=" << Sat.id() << " iodc=" << eph.iodc; - } - return 1; + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver << " sat=" << Sat.id(); + + E_Sys sys = Sat.sys; + + if (sys != E_Sys::GPS && sys != E_Sys::GAL && sys != E_Sys::QZS && sys != E_Sys::BDS) + { + BOOST_LOG_TRIVIAL(debug) << "ephemeris error: invalid satellite sat=" << Sat.id(); + + return 0; + } + + double deltaTime = (toc - toc.floorTime(7200)).to_double(); + if (sys == E_Sys::GPS && deltaTime > 60 && deltaTime < (7200 - 60)) + { + // Skip decoding bad ephemeris (being off for more than 1 minute from 2-hour modulo epochs) + return 0; + } + + eph.type = defNavMsgType[Sat.sys]; + eph.Sat = Sat; + eph.toc = toc; + + eph.f0 = data[0]; + eph.f1 = data[1]; + eph.f2 = data[2]; + eph.crs = data[4]; + eph.deln = data[5]; + eph.M0 = data[6]; + eph.cuc = data[7]; + eph.e = data[8]; + eph.cus = data[9]; + eph.sqrtA = data[10]; + eph.A = SQR(eph.sqrtA); + eph.toes = data[11]; // toe (s) in gps/bdt week + eph.cic = data[12]; + eph.OMG0 = data[13]; + eph.cis = data[14]; + eph.i0 = data[15]; + eph.crc = data[16]; + eph.omg = data[17]; + eph.OMGd = data[18]; + eph.idot = data[19]; + eph.week = (int)data[21]; // gps/bdt week + eph.ttms = data[27]; + + if (sys == E_Sys::GPS || sys == E_Sys::QZS) + { + eph.iode = (int)data[3]; // IODE + eph.iodc = (int)data[26]; // IODC + eph.toe = GTime(GTow(eph.toes), eph.toc); + eph.ttm = GTime(GTow(eph.ttms), eph.toc); + + eph.code = (int)data[20]; // GPS: codes on L2 ch + eph.svh = (E_Svh)data[24]; // sv health + eph.sva = uraToSva(data[23]); // ura (m->index) + eph.ura[0] = data[23]; + eph.flag = (int)data[22]; // GPS: L2 P data flag + + eph.tgd[0] = data[25]; // TGD + + if (sys == E_Sys::GPS) + { + eph.fit = data[28]; + } // fit interval in hours for GPS + else if (sys == E_Sys::QZS) + { + eph.fitFlag = data[28]; + eph.fit = eph.fitFlag ? 0.0 : 2.0; + } // fit interval flag for QZS + + if (acsConfig.use_tgd_bias) + decomposeTGDBias(Sat, eph.tgd[0]); + } + else if (sys == E_Sys::GAL) + { + // GAL ver.3 + eph.iode = (int)data[3]; // IODnav + eph.toe = GTime(GTow(eph.toes), eph.toc); + eph.ttm = GTime(GTow(eph.ttms), eph.toc); + + eph.code = (int)data[20]; // data sources + // bit 0 set: I/NAV E1-B + // bit 1 set: F/NAV E5a-I + // bit 2 set: I/NAV E5b-I + // bit 8 set: af0-af2 toc are for E5a.E1 + // bit 9 set: af0-af2 toc are for E5b.E1 + unsigned short iNavMask = 0x0005; + unsigned short fNavMask = 0x0002; + if (eph.code & iNavMask) + eph.type = E_NavMsgType::INAV; + else if (eph.code & fNavMask) + eph.type = E_NavMsgType::FNAV; + + eph.svh = (E_Svh)data[24]; // sv health + // bit 0: E1B DVS + // bit 1-2: E1B HS + // bit 3: E5a DVS + // bit 4-5: E5a HS + // bit 6: E5b DVS + // bit 7-8: E5b HS + eph.sva = sisaToSva(data[23]); + eph.ura[0] = data[23]; + + eph.tgd[0] = data[25]; // BGD E5a/E1 + eph.tgd[1] = data[26]; // BGD E5b/E1 + + if (acsConfig.use_tgd_bias) + decomposeBGDBias(Sat, eph.tgd[0], eph.tgd[1]); + } + else if (sys == E_Sys::BDS) + { + // BeiDou v.3.02 + if (Sat.prn > 5 && Sat.prn < 59) + eph.type = E_NavMsgType::D1; // MEO/IGSO + else + eph.type = E_NavMsgType::D2; // GEO, prn range may change in the future*/ + + eph.tocs = BTow(toc); + eph.aode = (int)data[3]; // AODE + eph.aodc = (int)data[28]; // AODC + eph.iode = int(eph.tocs / 720) % 240; + eph.iodc = eph.iode + 256 * int(eph.tocs / 172800) % 4; + eph.toe = GTime(BTow(eph.toes), eph.toc); + eph.ttm = GTime(BTow(eph.ttms), eph.toc); + + eph.svh = (E_Svh)data[24]; // satH1 + eph.sva = uraToSva(data[23]); // ura (m->index) + eph.ura[0] = data[23]; + + eph.tgd[0] = data[25]; // TGD1 B1/B3 + eph.tgd[1] = data[26]; // TGD2 B2/B3 + } + + if (eph.iode < 0 || eph.iode > 1023) + { + BOOST_LOG_TRIVIAL(debug) << "rinex nav invalid: sat=" << Sat.id() << " iode=" << eph.iode; + } + + if (eph.iodc < 0 || eph.iodc > 1023) + { + BOOST_LOG_TRIVIAL(debug) << "rinex nav invalid: sat=" << Sat.id() << " iodc=" << eph.iodc; + } + return 1; } -/** Decode glonass ephemeris -*/ +// Decode GLONASS ephemeris parameters int decodeGeph( - double ver, ///< RINEX version - SatSys Sat, ///< Satellite ID - GTime toc, ///< Time of clock - vector& data, ///< Data to decode - Geph& geph) ///< Glonass ephemeris + double ver, ///< RINEX version + SatSys Sat, ///< Satellite ID + GTime toc, ///< Time of clock + vector& data, ///< Data to decode + Geph& geph ///< Glonass ephemeris +) { - double tow; + double tow; -// BOOST_LOG_TRIVIAL(debug) -// << "decodeGeph: ver=" << ver << " sat=" << Sat.id(); + // BOOST_LOG_TRIVIAL(debug) + // << "decodeGeph: ver=" << ver << " sat=" << Sat.id(); - if (Sat.sys != +E_Sys::GLO) - { - BOOST_LOG_TRIVIAL(debug) - << "glonass ephemeris error: invalid satellite sat=" << Sat.id(); + if (Sat.sys != E_Sys::GLO) + { + BOOST_LOG_TRIVIAL(debug) << "glonass ephemeris error: invalid satellite sat=" << Sat.id(); - return 0; - } + return 0; + } - geph.type = defNavMsgType[Sat.sys]; - geph.Sat = Sat; + geph.type = defNavMsgType[Sat.sys]; + geph.Sat = Sat; - RTod toes = int(RTod(toc) + 450.0) / 900 * 900.0; - geph.toe = GTime(toes, toc); + RTod toes = int(RTod(toc) + 450.0) / 900 * 900.0; + geph.toe = GTime(toes, toc); - geph.tofs = data[2]; // UTC - geph.tof = GTime(RTod(geph.tofs + 10800.0), toc); + geph.tofs = data[2]; // UTC + geph.tof = GTime(RTod(geph.tofs + 10800.0), toc); - geph.iode = (int)toes / 900; + geph.iode = (int)toes / 900; - geph.taun = -data[0]; // -taun -> +taun - geph.gammaN = data[1]; // +gamman + geph.taun = -data[0]; // -taun -> +taun + geph.gammaN = data[1]; // +gamman - for (int i = 0; i < 3; i++) - { - geph.pos[i] = data[3 + i*4] * 1E3; - geph.vel[i] = data[4 + i*4] * 1E3; - geph.acc[i] = data[5 + i*4] * 1E3; - } + for (int i = 0; i < 3; i++) + { + geph.pos[i] = data[3 + i * 4] * 1E3; + geph.vel[i] = data[4 + i * 4] * 1E3; + geph.acc[i] = data[5 + i * 4] * 1E3; + } - geph.svh = (E_Svh) data[6]; - geph.frq = (int) data[10]; - geph.age = (int) data[14]; + geph.svh = (E_Svh)data[6]; + geph.frq = (int)data[10]; + geph.age = (int)data[14]; - if (ver >= 3.05) - { - // todo Eugene: additional records from version 3.05 and on - } + if (ver >= 3.05) + { + // todo Eugene: additional records from version 3.05 and on + } - // some receiver output >128 for minus frequency number - if (geph.frq > 128) - geph.frq -= 256; + // some receiver output >128 for minus frequency number + if (geph.frq > 128) + geph.frq -= 256; - if ( geph.frq < MINFREQ_GLO - ||geph.frq > MAXFREQ_GLO) - { - BOOST_LOG_TRIVIAL(debug) - << "rinex gnav invalid freq: sat=" << Sat << " fn=" << geph.frq; - } - return 1; + if (geph.frq < MINFREQ_GLO || geph.frq > MAXFREQ_GLO) + { + BOOST_LOG_TRIVIAL(debug) << "rinex gnav invalid freq: sat=" << Sat << " fn=" << geph.frq; + } + return 1; } -/** Decode geo ephemeris -*/ -int decodeSeph( - double ver, - SatSys Sat, - GTime toc, - vector& data, - Seph& seph) +// Decode SBAS/geostationary satellite ephemeris +int decodeSeph(double ver, SatSys Sat, GTime toc, vector& data, Seph& seph) { -// BOOST_LOG_TRIVIAL(debug) -// << "decodeSeph: ver=" << ver << " sat=" << Sat.id(); - - if (Sat.sys != +E_Sys::SBS) - { - BOOST_LOG_TRIVIAL(debug) - << "geo ephemeris error: invalid satellite sat=" << Sat.id(); + // BOOST_LOG_TRIVIAL(debug) + // << "decodeSeph: ver=" << ver << " sat=" << Sat.id(); - return 0; - } + if (Sat.sys != E_Sys::SBS) + { + BOOST_LOG_TRIVIAL(debug) << "geo ephemeris error: invalid satellite sat=" << Sat.id(); - seph.type = defNavMsgType[Sat.sys]; - seph.Sat = Sat; - seph.t0 = toc; + return 0; + } - seph.tofs = data[2]; - seph.tof = GTime(GTow(seph.tofs), seph.t0); + seph.type = defNavMsgType[Sat.sys]; + seph.Sat = Sat; + seph.t0 = toc; - seph.af0 = data[0]; - seph.af1 = data[1]; + seph.tofs = data[2]; + seph.tof = GTime(GTow(seph.tofs), seph.t0); - for (int i = 0; i < 3; i++) - { - seph.pos[i] = data[3 + i*4] * 1E3; - seph.vel[i] = data[4 + i*4] * 1E3; - seph.acc[i] = data[5 + i*4] * 1E3; - } + seph.af0 = data[0]; + seph.af1 = data[1]; - seph.svh = (E_Svh)data[6]; - seph.sva = uraToSva(data[10]); + for (int i = 0; i < 3; i++) + { + seph.pos[i] = data[3 + i * 4] * 1E3; + seph.vel[i] = data[4 + i * 4] * 1E3; + seph.acc[i] = data[5 + i * 4] * 1E3; + } - return 1; + seph.svh = (E_Svh)data[6]; + seph.sva = uraToSva(data[10]); + seph.ura = data[10]; + return 1; } -/** Decode CNVX ephemeris -*/ +// Decode CNVX (Civil Navigation) ephemeris int decodeCeph( - double ver, ///< RINEX version - SatSys Sat, ///< Satellite ID - E_NavMsgType type, ///< Navigation message type - GTime toc, ///< Time of clock - vector& data, ///< Data to decode - Ceph& ceph) ///< CNVX ephemeris + double ver, ///< RINEX version + SatSys Sat, ///< Satellite ID + E_NavMsgType type, ///< Navigation message type + GTime toc, ///< Time of clock + vector& data, ///< Data to decode + Ceph& ceph ///< CNVX ephemeris +) { -// BOOST_LOG_TRIVIAL(debug) -// << "decodeCeph: ver=" << ver << " sat=" << Sat.id(); - - if (ver < 4.0) - { - BOOST_LOG_TRIVIAL(debug) - << "ephemeris error: invalid RINEX version=" << ver; - - return -1; - } - - if ( type != +E_NavMsgType::CNAV - &&type != +E_NavMsgType::CNV1 - &&type != +E_NavMsgType::CNV2 - &&type != +E_NavMsgType::CNV3) - { - BOOST_LOG_TRIVIAL(debug) - << "ephemeris error: invalid message type=" << type._to_string(); - - return 0; - } - - int sys = Sat.sys; - - if ( sys != +E_Sys::GPS - &&sys != +E_Sys::QZS - &&sys != +E_Sys::BDS) - { - BOOST_LOG_TRIVIAL(debug) - << "ephemeris error: invalid satellite sat=" << Sat.id(); - - return 0; - } - - ceph.Sat = Sat; - ceph.type = type; - ceph.toc = toc; - - ceph.f0 = data[0]; - ceph.f1 = data[1]; - ceph.f2 = data[2]; - ceph.Adot = data[3]; - ceph.crs = data[4]; - ceph.deln = data[5]; - ceph.M0 = data[6]; - ceph.cuc = data[7]; - ceph.e = data[8]; - ceph.cus = data[9]; - ceph.A = SQR(data[10]); - ceph.cic = data[12]; - ceph.OMG0 = data[13]; - ceph.cis = data[14]; - ceph.i0 = data[15]; - ceph.crc = data[16]; - ceph.omg = data[17]; - ceph.OMGd = data[18]; - ceph.idot = data[19]; - ceph.dn0d = data[20]; - - if ( sys == +E_Sys::GPS - ||sys == +E_Sys::QZS) - { - ceph.toe = ceph.toc; - ceph.toes = GTow(ceph.toe); - - ceph.ura[0] = data[21]; - ceph.ura[1] = data[22]; - ceph.ura[2] = data[26]; - ceph.ura[3] = data[23]; - - ceph.svh = (E_Svh)data[24]; // sv health - - ceph.tgd[0] = data[25]; // TGD - - ceph.isc[0] = data[27]; - ceph.isc[1] = data[28]; - ceph.isc[2] = data[29]; - ceph.isc[3] = data[30]; - - if (type == +E_NavMsgType::CNAV) - { - ceph.ttms = data[31]; - ceph.ttm = GTime(GTow(ceph.ttms), ceph.toc); - ceph.wnop = (int)data[32]; - } - else if (type == +E_NavMsgType::CNV2) - { - ceph.isc[4] = data[31]; - ceph.isc[5] = data[32]; - - ceph.ttms = data[35]; - ceph.ttm = GTime(GTow(ceph.ttms), ceph.toc); - ceph.wnop = (int)data[36]; - } - - ceph.tops = data[11]; // top (s) in seconds - ceph.top = GTime(GTow(ceph.tops), ceph.toc); - } - else if ( sys == +E_Sys::BDS) - { - // BeiDou v.4.00 - - ceph.orb = E_SatType::_from_integral(data[21]); - - ceph.sis[0] = data[23]; - ceph.sis[1] = data[24]; - ceph.sis[2] = data[25]; - ceph.sis[3] = data[26]; - - if ( type == +E_NavMsgType::CNV1 - ||type == +E_NavMsgType::CNV2) - { - ceph.isc[0] = data[27]; - ceph.isc[1] = data[28]; - - ceph.tgd[0] = data[29]; // TGD_B1Cp - ceph.tgd[1] = data[30]; // TGD_B2ap - - ceph.sis[4] = data[31]; - - ceph.svh = (E_Svh)data[32]; // sv health - ceph.flag = (int)data[33]; // integrity flag - ceph.iodc = (int)data[34]; // IODC - ceph.iode = (int)data[38]; // IODE - - ceph.ttms = data[35]; - ceph.ttm = GTime(BTow(ceph.ttms), ceph.toc); - } - else if (type == +E_NavMsgType::CNV3) - { - ceph.sis[4] = data[27]; - ceph.svh = (E_Svh)data[28]; // sv health - ceph.flag = (int)data[29]; // integrity flag - ceph.tgd[2] = data[30]; // TGD_B2ap - - ceph.ttms = data[31]; - ceph.ttm = GTime(BTow(ceph.ttms), ceph.toc); - } - - ceph.toes = data[11]; // top (s) in seconds - ceph.tops = data[22]; // top (s) in seconds - ceph.toe = GTime(BTow(ceph.toes), ceph.toc); - ceph.top = GTime(BTow(ceph.tops), ceph.toc); - } - - if ( ceph.iode < 0 - ||ceph.iode > 1023) - { - BOOST_LOG_TRIVIAL(debug) - << "rinex nav invalid: sat=" << Sat.id() << " iode=" << ceph.iode; - } - - if ( ceph.iodc < 0 - ||ceph.iodc > 1023) - { - BOOST_LOG_TRIVIAL(debug) - << "rinex nav invalid: sat=" << Sat.id() << " iodc=" << ceph.iodc; - } - - return 1; + // BOOST_LOG_TRIVIAL(debug) + // << "decodeCeph: ver=" << ver << " sat=" << Sat.id(); + + if (ver < 4.0) + { + BOOST_LOG_TRIVIAL(debug) << "ephemeris error: invalid RINEX version=" << ver; + + return -1; + } + + if (type != E_NavMsgType::CNAV && type != E_NavMsgType::CNV1 && type != E_NavMsgType::CNV2 && + type != E_NavMsgType::CNV3) + { + BOOST_LOG_TRIVIAL(debug) << "ephemeris error: invalid message type=" + << enum_to_string(type); + + return 0; + } + + E_Sys sys = Sat.sys; + + if (sys != E_Sys::GPS && sys != E_Sys::QZS && sys != E_Sys::BDS) + { + BOOST_LOG_TRIVIAL(debug) << "ephemeris error: invalid satellite sat=" << Sat.id(); + + return 0; + } + + ceph.Sat = Sat; + ceph.type = type; + ceph.toc = toc; + + ceph.f0 = data[0]; + ceph.f1 = data[1]; + ceph.f2 = data[2]; + ceph.Adot = data[3]; + ceph.crs = data[4]; + ceph.deln = data[5]; + ceph.M0 = data[6]; + ceph.cuc = data[7]; + ceph.e = data[8]; + ceph.cus = data[9]; + ceph.A = SQR(data[10]); + ceph.cic = data[12]; + ceph.OMG0 = data[13]; + ceph.cis = data[14]; + ceph.i0 = data[15]; + ceph.crc = data[16]; + ceph.omg = data[17]; + ceph.OMGd = data[18]; + ceph.idot = data[19]; + ceph.dn0d = data[20]; + + if (sys == E_Sys::GPS || sys == E_Sys::QZS) + { + ceph.toe = ceph.toc; + ceph.toes = GTow(ceph.toe); + + ceph.ura[0] = data[21]; + ceph.ura[1] = data[22]; + ceph.ura[2] = data[26]; + ceph.ura[3] = data[23]; + + ceph.svh = (E_Svh)data[24]; // sv health + + ceph.tgd[0] = data[25]; // TGD + + ceph.isc[0] = data[27]; + ceph.isc[1] = data[28]; + ceph.isc[2] = data[29]; + ceph.isc[3] = data[30]; + + if (type == E_NavMsgType::CNAV) + { + ceph.ttms = data[31]; + ceph.ttm = GTime(GTow(ceph.ttms), ceph.toc); + ceph.wnop = (int)data[32]; + } + else if (type == E_NavMsgType::CNV2) + { + ceph.isc[4] = data[31]; + ceph.isc[5] = data[32]; + + ceph.ttms = data[35]; + ceph.ttm = GTime(GTow(ceph.ttms), ceph.toc); + ceph.wnop = (int)data[36]; + } + + ceph.tops = data[11]; // top (s) in seconds + ceph.top = GTime(GTow(ceph.tops), ceph.toc); + } + else if (sys == E_Sys::BDS) + { + // BeiDou v.4.00 + + ceph.orb = int_to_enum(data[21]); + + ceph.sis[0] = data[23]; + ceph.sis[1] = data[24]; + ceph.sis[2] = data[25]; + ceph.sis[3] = data[26]; + + if (type == E_NavMsgType::CNV1 || type == E_NavMsgType::CNV2) + { + ceph.isc[0] = data[27]; + ceph.isc[1] = data[28]; + + ceph.tgd[0] = data[29]; // TGD_B1Cp + ceph.tgd[1] = data[30]; // TGD_B2ap + + ceph.sis[4] = data[31]; + + ceph.svh = (E_Svh)data[32]; // sv health + ceph.flag = (int)data[33]; // integrity flag + ceph.iodc = (int)data[34]; // IODC + ceph.iode = (int)data[38]; // IODE + + ceph.ttms = data[35]; + ceph.ttm = GTime(BTow(ceph.ttms), ceph.toc); + } + else if (type == E_NavMsgType::CNV3) + { + ceph.sis[4] = data[27]; + ceph.svh = (E_Svh)data[28]; // sv health + ceph.flag = (int)data[29]; // integrity flag + ceph.tgd[2] = data[30]; // TGD_B2ap + + ceph.ttms = data[31]; + ceph.ttm = GTime(BTow(ceph.ttms), ceph.toc); + } + + ceph.toes = data[11]; // top (s) in seconds + ceph.tops = data[22]; // top (s) in seconds + ceph.toe = GTime(BTow(ceph.toes), ceph.toc); + ceph.top = GTime(BTow(ceph.tops), ceph.toc); + } + + if (ceph.iode < 0 || ceph.iode > 1023) + { + BOOST_LOG_TRIVIAL(debug) << "rinex nav invalid: sat=" << Sat.id() << " iode=" << ceph.iode; + } + + if (ceph.iodc < 0 || ceph.iodc > 1023) + { + BOOST_LOG_TRIVIAL(debug) << "rinex nav invalid: sat=" << Sat.id() << " iodc=" << ceph.iodc; + } + + return 1; } -/** Decode STO message -*/ -int decodeSto( - double ver, - SatSys Sat, - E_NavMsgType type, - GTime toc, - vector& data, - STO& sto) +// Decode System Time Offset message +int decodeSto(double ver, SatSys Sat, E_NavMsgType type, GTime toc, vector& data, STO& sto) { - if (ver < 4.0) - { - BOOST_LOG_TRIVIAL(debug) - << "ephemeris error: invalid RINEX version=" << ver; + if (ver < 4.0) + { + BOOST_LOG_TRIVIAL(debug) << "ephemeris error: invalid RINEX version=" << ver; - return -1; - } + return -1; + } - int sys = Sat.sys; + E_Sys sys = Sat.sys; - sto.Sat = Sat; - sto.type = type; - sto.tot = toc; + sto.Sat = Sat; + sto.type = type; + sto.tot = toc; - sto.code = E_StoCode ::_from_integral(data[0]); - sto.sid = E_SbasId ::_from_integral(data[1]); - sto.uid = E_UtcId ::_from_integral(data[2]); + sto.code = int_to_enum(data[0]); + sto.sid = int_to_enum(data[1]); + sto.uid = int_to_enum(data[2]); - sto.ttms = data[3]; + sto.ttms = data[3]; - sto.A0 = data[4]; - sto.A1 = data[5]; - sto.A2 = data[6]; + sto.A0 = data[4]; + sto.A1 = data[5]; + sto.A2 = data[6]; - if (sys != +E_Sys::BDS) { sto.ttm = GTime(GWeek(sto.tot), GTow(sto.ttms));} - else { sto.ttm = GTime(BWeek(sto.tot), BTow(sto.ttms));} + if (sys != E_Sys::BDS) + { + sto.ttm = GTime(GWeek(sto.tot), GTow(sto.ttms)); + } + else + { + sto.ttm = GTime(BWeek(sto.tot), BTow(sto.ttms)); + } - return 1; + return 1; } -/** Decode EOP message -*/ -int decodeEop( - double ver, - SatSys Sat, - E_NavMsgType type, - GTime toc, - vector& data, - EOP& eop) +// Decode Earth Orientation Parameters message +int decodeEop(double ver, SatSys Sat, E_NavMsgType type, GTime toc, vector& data, EOP& eop) { - if (ver < 4.0) - { - BOOST_LOG_TRIVIAL(debug) - << "ephemeris error: invalid RINEX version=" << ver; - - return -1; - } - - int sys = Sat.sys; - - eop.Sat = Sat; - eop.type = type; - eop.teop = toc; - - eop.xp = data[0] * AS2R; - eop.xpr = data[1] * AS2R; - eop.xprr = data[2] * AS2R; - eop.yp = data[4] * AS2R; - eop.ypr = data[5] * AS2R; - eop.yprr = data[6] * AS2R; - eop.ttms = data[7]; - eop.dut1 = data[8]; - eop.dur = data[9]; - eop.durr = data[10]; - - if (sys != +E_Sys::BDS) { eop.ttm = GTime(GWeek(eop.teop), GTow(eop.ttms));} - else { eop.ttm = GTime(BWeek(eop.teop), BTow(eop.ttms));} - - return 1; + if (ver < 4.0) + { + BOOST_LOG_TRIVIAL(debug) << "ephemeris error: invalid RINEX version=" << ver; + + return -1; + } + + E_Sys sys = Sat.sys; + + eop.Sat = Sat; + eop.type = type; + eop.teop = toc; + + eop.xp = data[0] * AS2R; + eop.xpr = data[1] * AS2R; + eop.xprr = data[2] * AS2R; + eop.yp = data[4] * AS2R; + eop.ypr = data[5] * AS2R; + eop.yprr = data[6] * AS2R; + eop.ttms = data[7]; + eop.dut1 = data[8]; + eop.dur = data[9]; + eop.durr = data[10]; + + if (sys != E_Sys::BDS) + { + eop.ttm = GTime(GWeek(eop.teop), GTow(eop.ttms)); + } + else + { + eop.ttm = GTime(BWeek(eop.teop), BTow(eop.ttms)); + } + + return 1; } -/** Decode ION message -*/ -int decodeIon( - double ver, - SatSys Sat, - E_NavMsgType type, - GTime toc, - vector& data, - ION& ion) +// Decode ionospheric parameters message +int decodeIon(double ver, SatSys Sat, E_NavMsgType type, GTime toc, vector& data, ION& ion) { - if (ver < 4.0) - { - BOOST_LOG_TRIVIAL(debug) - << "ephemeris error: invalid RINEX version=" << ver; - - return -1; - } - - int sys = Sat.sys; - - ion.Sat = Sat; - ion.type = type; - ion.ttm = toc; - - if ( sys == +E_Sys::GAL - &&type == +E_NavMsgType::IFNV) - { - ion.ai0 = data[0]; - ion.ai1 = data[1]; - ion.ai2 = data[2]; - - ion.flag = (int)data[3]; - } - else if ( sys == +E_Sys::BDS - &&type == +E_NavMsgType::CNVX) - { - ion.alpha1 = data[0]; - ion.alpha2 = data[1]; - ion.alpha3 = data[2]; - ion.alpha4 = data[3]; - ion.alpha5 = data[4]; - ion.alpha6 = data[5]; - ion.alpha7 = data[6]; - ion.alpha8 = data[7]; - ion.alpha9 = data[8]; - } - else if ( type == +E_NavMsgType::LNAV - ||type == +E_NavMsgType::D1D2 - ||type == +E_NavMsgType::CNVX) - { - ion.a0 = data[0]; - ion.a1 = data[1]; - ion.a2 = data[2]; - ion.a3 = data[3]; - ion.b0 = data[4]; - ion.b1 = data[5]; - ion.b2 = data[6]; - ion.b3 = data[7]; - - ion.code = (int)data[8]; - - if (ion.code == 1) // QZS Japan area coefficients are currently skipped - return 0; - } - - return 1; + if (ver < 4.0) + { + BOOST_LOG_TRIVIAL(debug) << "ephemeris error: invalid RINEX version=" << ver; + + return -1; + } + + E_Sys sys = Sat.sys; + + ion.Sat = Sat; + ion.type = type; + ion.ttm = toc; + + if (sys == E_Sys::GAL && type == E_NavMsgType::IFNV) + { + ion.ai0 = data[0]; + ion.ai1 = data[1]; + ion.ai2 = data[2]; + + ion.flag = (int)data[3]; + } + else if (sys == E_Sys::BDS && type == E_NavMsgType::CNVX) + { + ion.alpha1 = data[0]; + ion.alpha2 = data[1]; + ion.alpha3 = data[2]; + ion.alpha4 = data[3]; + ion.alpha5 = data[4]; + ion.alpha6 = data[5]; + ion.alpha7 = data[6]; + ion.alpha8 = data[7]; + ion.alpha9 = data[8]; + } + else if (type == E_NavMsgType::LNAV || type == E_NavMsgType::D1D2 || type == E_NavMsgType::CNVX) + { + ion.a0 = data[0]; + ion.a1 = data[1]; + ion.a2 = data[2]; + ion.a3 = data[3]; + ion.b0 = data[4]; + ion.b1 = data[5]; + ion.b2 = data[6]; + ion.b3 = data[7]; + + ion.code = (int)data[8]; + + if (ion.code == 1) // QZS Japan area coefficients are currently skipped + return 0; + } + + return 1; } -/** Read rinex navigation data body -*/ +// Read RINEX navigation data body int readRnxNavB( - std::istream& inputStream, ///< Input stream to read - double ver, ///< RINEX version - E_Sys sys, ///< Satellite system - E_EphType& type, ///< Ephemeris type (output) - Eph& eph, ///< GPS Ephemeris - Geph& geph, ///< Glonass ephemeris - Seph& seph, ///< Geo ephemeris - Ceph& ceph, ///< CNVX ephemeris - STO& sto, ///< System time offset data - EOP& eop, ///< EOP data - ION& ion) ///< Ionosphere data + std::istream& inputStream, ///< Input stream to read + double ver, ///< RINEX version + E_Sys sys, ///< Satellite system + E_EphType& type, ///< Ephemeris type (output) + Eph& eph, ///< GPS Ephemeris + Geph& geph, ///< Glonass ephemeris + Seph& seph, ///< Geo ephemeris + Ceph& ceph, ///< CNVX ephemeris + STO& sto, ///< System time offset data + EOP& eop, ///< EOP data + ION& ion ///< Ionosphere data +) { - GTime toc; - vector data; - int sp = 3; - string line; - char id[8] = ""; - char *p; - -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver << " sys=" << sys; - - SatSys Sat = {}; - E_NavRecType recType = E_NavRecType::NONE; - E_NavMsgType msgType = E_NavMsgType::NONE; - - while (std::getline(inputStream, line)) - { - char* buff = &line[0]; - - if (data.empty()) - { - // decode message type field - if ( ver >= 4.0 - &&buff[0] == '>') - { - // ver.4 - char typeStr[5] = ""; - strncpy(typeStr, buff+2, 3); - recType = E_NavRecType::_from_string(typeStr); - - strncpy(id, buff + 6, 3); - Sat=SatSys(id); - sys=Sat.sys; - - strncpy(typeStr, buff + 10, 4); - std::replace(typeStr, typeStr + 4, ' ', '\0'); - msgType = E_NavMsgType::_from_string(typeStr); - - continue; - } - - // decode satellite field - if ( ver >= 3.0 - ||sys == +E_Sys::GAL - ||sys == +E_Sys::QZS) - { - // ver.3 or GAL/QZS - strncpy(id, buff, 3); - sp = 4; - if (ver < 4.0) // satellite id included in message type field in ver.4 - { - Sat=SatSys(id); - if (ver >= 3.0) - sys = Sat.sys; - } - } - else - { - Sat.sys = sys; - Sat.prn = str2num(buff, 0, 2); - } - - E_TimeSys tsys = E_TimeSys::GPST; - switch (sys) - { - case E_Sys::GPS: tsys = E_TimeSys::GPST; break; - case E_Sys::GLO: tsys = E_TimeSys::UTC; break; - case E_Sys::GAL: tsys = E_TimeSys::GST; break; - case E_Sys::BDS: tsys = E_TimeSys::BDT; break; - case E_Sys::QZS: tsys = E_TimeSys::QZSST; break; - case E_Sys::SBS: tsys = E_TimeSys::GPST; break; - default: tsys = E_TimeSys::GPST; break; - } - - // decode toc field - bool error = str2time(buff+sp, 0, 19, toc, tsys); - if (error == true) - { -// BOOST_LOG_TRIVIAL(debug) -// << "rinex nav toc error: " << buff; - - return 0; - } - - if (recType == +E_NavRecType::STO) - { - // decode STO code, SBAS ID & UTC ID for STO message - char code[19] = ""; - strncpy(code, buff + 24, 18); - std::replace(code, code + 18, ' ', '\0'); - data.push_back(E_StoCode::_from_string(code)); - - strncpy(code, buff + 43, 18); - std::replace(code, code + 18, '-', '_' ); - std::replace(code, code + 18, ' ', '\0'); - data.push_back(*(E_SbasId::_from_string_nothrow(code))); //code may be empty - - strncpy(code, buff + 62, 18); - std::replace(code, code + 18, '(', '_' ); - std::replace(code, code + 18, ')', '\0'); - std::replace(code, code + 18, ' ', '\0'); - data.push_back(*(E_UtcId::_from_string_nothrow(code))); //code may be empty - } - else - { - // decode data fields - p = buff+sp+19; - for (int j = 0; j < 3; j++, p += 19) - { - data.push_back(str2num(p, 0, 19)); - } - } - - if (recType == +E_NavRecType::NONE) recType = E_NavRecType::EPH; - if (msgType == +E_NavMsgType::NONE) msgType = defNavMsgType[sys]; - } - else - { - // decode data fields - p = buff+sp; - for (int j = 0; j < 4; j++, p += 19) - { - data.push_back(str2num(p, 0, 19)); - } - // decode ephemeris - if (recType == +E_NavRecType::EPH) - { - switch (msgType) - { - case E_NavMsgType::CNAV: //fallthrough - case E_NavMsgType::CNV3: { if (data.size() >= 35) { type = E_EphType::CEPH; return decodeCeph(ver, Sat, msgType, toc, data, ceph); } break; } - case E_NavMsgType::CNV1: //fallthrough - case E_NavMsgType::CNV2: { if (data.size() >= 39) { type = E_EphType::CEPH; return decodeCeph(ver, Sat, msgType, toc, data, ceph); } break; } - case E_NavMsgType::FDMA: { if (data.size() >= 15) { type = E_EphType::GEPH; return decodeGeph(ver, Sat, toc, data, geph); } break; } // todo Eugene: additional records from version 3.05 and on - case E_NavMsgType::SBAS: { if (data.size() >= 15) { type = E_EphType::SEPH; return decodeSeph(ver, Sat, toc, data, seph); } break; } - default: { if (data.size() >= 31) { type = E_EphType:: EPH; return decodeEph (ver, Sat, toc, data, eph); } break; } - } - } - else if (recType == +E_NavRecType::STO) { if (data.size() >= 7) { type = E_EphType:: STO; return decodeSto (ver, Sat, msgType, toc, data, sto); } } - else if (recType == +E_NavRecType::EOP) { if (data.size() >= 11) { type = E_EphType:: EOP; return decodeEop (ver, Sat, msgType, toc, data, eop); } } - else if (recType == +E_NavRecType::ION) - { - switch (sys) - { - case E_Sys::GAL: { if (data.size() >= 7) { type = E_EphType:: ION; return decodeIon (ver, Sat, msgType, toc, data, ion); } break; } - default: { if (data.size() >= 11) { type = E_EphType:: ION; return decodeIon (ver, Sat, msgType, toc, data, ion); } break; } - } - } - else - return -1; - } - } - return -1; + GTime toc; + vector data; + int sp = 3; + string line; + char id[8] = ""; + char* p; + + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver << " sys=" << sys; + + SatSys Sat = {}; + E_NavRecType recType = E_NavRecType::NONE; + E_NavMsgType msgType = E_NavMsgType::NONE; + + while (std::getline(inputStream, line)) + { + char* buff = &line[0]; + + if (data.empty()) + { + // decode message type field + if (ver >= 4.0 && buff[0] == '>') + { + // ver.4 + char typeStr[5] = ""; + strncpy(typeStr, buff + 2, 3); + recType = string_to_enum(typeStr); + + strncpy(id, buff + 6, 3); + Sat = SatSys(id); + sys = Sat.sys; + + strncpy(typeStr, buff + 10, 4); + std::replace(typeStr, typeStr + 4, ' ', '\0'); + msgType = string_to_enum(typeStr); + + continue; + } + + // decode satellite field + if (ver >= 3.0 || sys == E_Sys::GAL || sys == E_Sys::QZS) + { + // ver.3 or GAL/QZS + strncpy(id, buff, 3); + sp = 4; + if (ver < 4.0) // satellite id included in message type field in ver.4 + { + Sat = SatSys(id); + if (ver >= 3.0) + sys = Sat.sys; + } + } + else + { + Sat.sys = sys; + Sat.prn = str2num(buff, 0, 2); + } + + E_TimeSys tsys = E_TimeSys::GPST; + switch (sys) + { + case E_Sys::GPS: + tsys = E_TimeSys::GPST; + break; + case E_Sys::GLO: + tsys = E_TimeSys::UTC; + break; + case E_Sys::GAL: + tsys = E_TimeSys::GST; + break; + case E_Sys::BDS: + tsys = E_TimeSys::BDT; + break; + case E_Sys::QZS: + tsys = E_TimeSys::QZSST; + break; + case E_Sys::SBS: + tsys = E_TimeSys::GPST; + break; + default: + tsys = E_TimeSys::GPST; + break; + } + + // decode toc field + bool error = str2time(buff + sp, 0, 19, toc, tsys); + if (error == true) + { + // BOOST_LOG_TRIVIAL(debug) + // << "rinex nav toc error: " << buff; + + return 0; + } + + if (recType == E_NavRecType::STO) + { + // decode STO code, SBAS ID & UTC ID for STO message + char code[19] = ""; + strncpy(code, buff + 24, 18); + std::replace(code, code + 18, ' ', '\0'); + data.push_back(static_cast(string_to_enum(code))); + + strncpy(code, buff + 43, 18); + std::replace(code, code + 18, '-', '_'); + std::replace(code, code + 18, ' ', '\0'); + auto sbasId = string_to_enum_opt(code); + data.push_back(static_cast(sbasId.value_or(E_SbasId{}))); // code may be empty + + strncpy(code, buff + 62, 18); + std::replace(code, code + 18, '(', '_'); + std::replace(code, code + 18, ')', '\0'); + std::replace(code, code + 18, ' ', '\0'); + auto utcId = string_to_enum_opt(code); + data.push_back(static_cast(utcId.value_or(E_UtcId{}))); // code may be empty + } + else + { + // decode data fields + p = buff + sp + 19; + for (int j = 0; j < 3; j++, p += 19) + { + data.push_back(str2num(p, 0, 19)); + } + } + + if (recType == E_NavRecType::NONE) + recType = E_NavRecType::EPH; + if (msgType == E_NavMsgType::NONE) + msgType = defNavMsgType[sys]; + } + else + { + // decode data fields + p = buff + sp; + for (int j = 0; j < 4; j++, p += 19) + { + data.push_back(str2num(p, 0, 19)); + } + // decode ephemeris + if (recType == E_NavRecType::EPH) + { + switch (msgType) + { + case E_NavMsgType::CNAV: // fallthrough + case E_NavMsgType::CNV3: + { + if (data.size() >= 35) + { + type = E_EphType::CEPH; + return decodeCeph(ver, Sat, msgType, toc, data, ceph); + } + break; + } + case E_NavMsgType::CNV1: // fallthrough + case E_NavMsgType::CNV2: + { + if (data.size() >= 39) + { + type = E_EphType::CEPH; + return decodeCeph(ver, Sat, msgType, toc, data, ceph); + } + break; + } + case E_NavMsgType::FDMA: + { + if (data.size() >= 15) + { + type = E_EphType::GEPH; + return decodeGeph(ver, Sat, toc, data, geph); + } + break; + } // todo Eugene: additional records from version 3.05 and on + case E_NavMsgType::SBAS: + { + if (data.size() >= 15) + { + type = E_EphType::SEPH; + return decodeSeph(ver, Sat, toc, data, seph); + } + break; + } + default: + { + if (data.size() >= 31) + { + type = E_EphType::EPH; + return decodeEph(ver, Sat, toc, data, eph); + } + break; + } + } + } + else if (recType == E_NavRecType::STO) + { + if (data.size() >= 7) + { + type = E_EphType::STO; + return decodeSto(ver, Sat, msgType, toc, data, sto); + } + } + else if (recType == E_NavRecType::EOP) + { + if (data.size() >= 11) + { + type = E_EphType::EOP; + return decodeEop(ver, Sat, msgType, toc, data, eop); + } + } + else if (recType == E_NavRecType::ION) + { + switch (sys) + { + case E_Sys::GAL: + { + if (data.size() >= 7) + { + type = E_EphType::ION; + return decodeIon(ver, Sat, msgType, toc, data, ion); + } + break; + } + default: + { + if (data.size() >= 11) + { + type = E_EphType::ION; + return decodeIon(ver, Sat, msgType, toc, data, ion); + } + break; + } + } + } + else + return -1; + } + } + return -1; } -/** Read rinex nav/gnav/geo nav -*/ +// Read complete RINEX navigation file int readRnxNav( - std::istream& inputStream, ///< Input stream to read - double ver, ///< RINEX version - E_Sys sys, ///< Satellite system - Navigation& nav) ///< Navigation object + std::istream& inputStream, ///< Input stream to read + double ver, ///< RINEX version + E_Sys sys, ///< Satellite system + Navigation& nav ///< Navigation object +) { -// BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver << " sys=" << sys; - - // read rinex navigation data body - while (1) - { - // initialise each time to avoid incomplete overwriting - Eph eph = {}; - Geph geph = {}; - Seph seph = {}; - Ceph ceph = {}; - STO sto = {}; - EOP eop = {}; - ION ion = {}; - - E_EphType type; - - int stat = readRnxNavB(inputStream, ver, sys, type, eph, geph, seph, ceph, sto, eop, ion); - - if (stat < 0) - { - break; - } - else if (stat > 0) - { - // add ephemeris to navigation data - switch (type) - { - case E_EphType::EPH: nav.ephMap [eph. Sat] [eph. type] [eph. toe] = eph; break; - case E_EphType::GEPH: nav.gephMap [geph.Sat] [geph.type] [geph.toe] = geph; break; - case E_EphType::SEPH: nav.sephMap [seph.Sat] [seph.type] [seph.t0 ] = seph; break; - case E_EphType::CEPH: nav.cephMap [ceph.Sat] [ceph.type] [ceph.toe] = ceph; break; - case E_EphType::STO: nav.stoMap [sto.code] [sto. type] [sto. tot] = sto; break; - case E_EphType::EOP: nav.eopMap [eop.Sat.sys] [eop. type] [eop.teop] = eop; break; - case E_EphType::ION: nav.ionMap [ion.Sat.sys] [ion. type] [ion. ttm] = ion; break; - default: continue; - } - } - } - - for (auto& [sys, eopSysMap] : nav.eopMap) - for (auto& [type, eopList] : eopSysMap) - { - map erpMap; - - for (auto& [time, eop] : eopList) - { - ERPValues erpv; - erpv.time = eop.teop; - erpv.xp = eop.xp; - erpv.yp = eop.yp; - erpv.ut1Utc = eop.dut1; - erpv.xpr = eop.xpr; - erpv.ypr = eop.ypr; - erpMap[erpv.time] = erpv; - } - - if (!erpMap.empty()) - nav.erp.erpMaps.push_back(erpMap); - } - - return ( nav. ephMap.empty() == false - ||nav.gephMap.empty() == false - ||nav.sephMap.empty() == false - ||nav.cephMap.empty() == false - ||nav. stoMap.empty() == false - ||nav. eopMap.empty() == false - ||nav. ionMap.empty() == false); + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ver=" << ver << " sys=" << sys; + + // read rinex navigation data body + while (1) + { + // initialise each time to avoid incomplete overwriting + Eph eph = {}; + Geph geph = {}; + Seph seph = {}; + Ceph ceph = {}; + STO sto = {}; + EOP eop = {}; + ION ion = {}; + + E_EphType type; + + int stat = readRnxNavB(inputStream, ver, sys, type, eph, geph, seph, ceph, sto, eop, ion); + + if (stat < 0) + { + break; + } + else if (stat > 0) + { + // add ephemeris to navigation data + switch (type) + { + case E_EphType::EPH: + nav.ephMap[eph.Sat][eph.type][eph.toe] = eph; + break; + case E_EphType::GEPH: + nav.gephMap[geph.Sat][geph.type][geph.toe] = geph; + break; + case E_EphType::SEPH: + nav.sephMap[seph.Sat][seph.type][seph.t0] = seph; + break; + case E_EphType::CEPH: + nav.cephMap[ceph.Sat][ceph.type][ceph.toe] = ceph; + break; + case E_EphType::STO: + nav.stoMap[sto.code][sto.type][sto.tot] = sto; + break; + case E_EphType::EOP: + nav.eopMap[eop.Sat.sys][eop.type][eop.teop] = eop; + break; + case E_EphType::ION: + nav.ionMap[ion.Sat.sys][ion.type][ion.ttm] = ion; + break; + default: + continue; + } + } + } + + for (auto& [sys, eopSysMap] : nav.eopMap) + for (auto& [type, eopList] : eopSysMap) + { + map erpMap; + + for (auto& [time, eop] : eopList) + { + ERPValues erpv; + erpv.time = eop.teop; + erpv.xp = eop.xp; + erpv.yp = eop.yp; + erpv.ut1Utc = eop.dut1; + erpv.xpr = eop.xpr; + erpv.ypr = eop.ypr; + erpMap[erpv.time] = erpv; + } + + if (!erpMap.empty()) + nav.erp.erpMaps.push_back(erpMap); + } + + return ( + nav.ephMap.empty() == false || nav.gephMap.empty() == false || + nav.sephMap.empty() == false || nav.cephMap.empty() == false || + nav.stoMap.empty() == false || nav.eopMap.empty() == false || nav.ionMap.empty() == false + ); } -/** Read rinex clock -*/ -int readRnxClk( - std::istream& inputStream, - double ver, - Navigation& nav) +// Read RINEX clock file +int readRnxClk(std::istream& inputStream, double ver, Navigation& nav) { -// trace(3,"readrnxclk: index=%d\n", index); - - static int index = 0; - index++; - string line; - - typedef struct - { - short offset; - short length; - } ClkStruct; - - - ClkStruct typ = { 0, 2}; - ClkStruct as = { 3, 3}; - ClkStruct ar = { 3, 4}; - ClkStruct tim = { 8, 26}; - ClkStruct clk = {40, 19}; - ClkStruct std = {60, 19}; - - // special case for 3.04 rnx with 9 char AR names - if (ver == 3.04) - { - ar.length += 5; - tim.offset += 5; - clk.offset += 5; - std.offset += 5; - } - - while (std::getline(inputStream, line)) - { - char* buff = &line[0]; - - GTime time; - if (str2time(buff, tim.offset, tim.length, time)) - { -// trace(2,"rinex clk invalid epoch: %34.34s\n", buff); - continue; - } - - string type(buff + typ.offset, typ.length); - - string idString; - if (type == "AS") { idString.assign(buff + as.offset, as.length); } - else if (type == "AR") { idString.assign(buff + ar.offset, ar.length); } - else continue; - - Pclk preciseClock = {}; - - preciseClock.clk = str2num(buff, clk.offset, clk.length); - preciseClock.clkStd = str2num(buff, std.offset, std.length); - preciseClock.clkIndex = index; - - nav.pclkMap[idString][time] = preciseClock; - } - - return nav.pclkMap.size() > 0; + // trace(3,"readrnxclk: index=%d\n", index); + + static int index = 0; + index++; + string line; + + typedef struct + { + short offset; + short length; + } ClkStruct; + + ClkStruct typ = {0, 2}; + ClkStruct as = {3, 3}; + ClkStruct ar = {3, 4}; + ClkStruct tim = {8, 26}; + ClkStruct clk = {40, 19}; + ClkStruct std = {60, 19}; + + // special case for 3.04 rnx with 9 char AR names + if (ver == 3.04) + { + ar.length += 5; + tim.offset += 5; + clk.offset += 5; + std.offset += 5; + } + + GTime time0; + while (std::getline(inputStream, line)) + { + char* buff = &line[0]; + + GTime time; + if (str2time(buff, tim.offset, tim.length, time)) + { + // trace(2,"rinex clk invalid epoch: %34.34s\n", buff); + continue; + } + + string type(buff + typ.offset, typ.length); + + string idString; + if (type == "AS") + { + idString.assign(buff + as.offset, as.length); + } + else if (type == "AR") + { + idString.assign(buff + ar.offset, ar.length); + } + else + continue; + + Pclk preciseClock = {}; + + preciseClock.clk = str2num(buff, clk.offset, clk.length); + preciseClock.clkStd = str2num(buff, std.offset, std.length); + preciseClock.clkIndex = index; + + nav.pclkMap[idString][time] = preciseClock; + + // Use minimum delta time between epochs as the data interval + double dt = (time - time0).to_double(); + if (dt > 0 && dt < nav.pclkInterval) + { + nav.pclkInterval = dt; + } + + time0 = time; + } + + return nav.pclkMap.size() > 0; } -/** Read rinex file -*/ +// Read RINEX file with automatic type detection int readRnx( - std::istream& inputStream, - char& type, - ObsList& obsList, - Navigation& nav, - RinexStation& rnxRec, - double& ver, - E_Sys& sys, - E_TimeSys& tsys, - map>& sysCodeTypes) + std::istream& inputStream, + char& type, + ObsList& obsList, + Navigation& nav, + RinexStation& rnxRec, + double& ver, + E_Sys& sys, + E_TimeSys& tsys, + map>& sysCodeTypes +) { - if (inputStream.tellg() == 0) - { - // read rinex header if at beginning of file - readRnxH(inputStream, ver, type, sys, tsys, sysCodeTypes, nav, rnxRec); - } - - // read rinex body - switch (type) - { - case 'O': return readRnxObs(inputStream, ver, tsys, sysCodeTypes, obsList, rnxRec); - case 'N': return readRnxNav(inputStream, ver, sys, nav); - case 'G': return readRnxNav(inputStream, ver, E_Sys::GLO, nav); - case 'H': return readRnxNav(inputStream, ver, E_Sys::SBS, nav); - case 'J': return readRnxNav(inputStream, ver, E_Sys::QZS, nav); // extension - case 'L': return readRnxNav(inputStream, ver, E_Sys::GAL, nav); // extension - case 'C': return readRnxClk(inputStream, ver, nav); - } - - BOOST_LOG_TRIVIAL(debug) - << "unsupported rinex type ver=" << ver << " type=" << type; - - return 0; + if (inputStream.tellg() == 0) + { + // read rinex header if at beginning of file + readRnxH(inputStream, ver, type, sys, tsys, sysCodeTypes, nav, rnxRec); + } + + // read rinex body + switch (type) + { + case 'O': + return readRnxObs(inputStream, ver, tsys, sysCodeTypes, obsList, rnxRec); + case 'N': + return readRnxNav(inputStream, ver, sys, nav); + case 'G': + return readRnxNav(inputStream, ver, E_Sys::GLO, nav); + case 'H': + return readRnxNav(inputStream, ver, E_Sys::SBS, nav); + case 'J': + return readRnxNav(inputStream, ver, E_Sys::QZS, nav); // extension + case 'L': + return readRnxNav(inputStream, ver, E_Sys::GAL, nav); // extension + case 'C': + return readRnxClk(inputStream, ver, nav); + } + + BOOST_LOG_TRIVIAL(debug) << "unsupported rinex type ver=" << ver << " type=" << type; + + return 0; } +// Helper functions for common RINEX observation processing (SRP compliance) + +/** + * @brief Parse observation values from RINEX formatted text + * + * Extracts numerical observation value and Loss of Lock Indicator from + * a RINEX formatted line at the specified position. Includes comprehensive + * bounds checking and error handling for malformed input data. + * + * RINEX observation format: + * - 14 characters: observation value (right-justified, decimal point optional) + * - 1 character: Loss of Lock Indicator (0-3) + * - 1 character: Signal strength (optional, not currently processed) + * + * @param buff Character buffer containing RINEX observation line + * @param position Starting position in buffer (0-based index) + * + * @return ObservationValues Structure containing parsed value and LLI + * Returns zeros if parsing fails or position is out of bounds + * + * @note Includes debug output for development/troubleshooting + * @note LLI bits are masked to extract only relevant flags (bits 0-1) + * + * @warning Function assumes RINEX standard 16-character field width + */ +ObservationValues parseObservationValues(char* buff, int position) +{ + ObservationValues result; + + // Add bounds checking to prevent buffer overflow + if (!buff) + { + BOOST_LOG_TRIVIAL(error) << "parseObservationValues: null buffer pointer"; + result.value = 0.0; + result.lli = 0.0; + return result; + } + + // Basic bounds check - RINEX observation fields are 14 chars + 1 char LLI + size_t bufferLength = strlen(buff); + result.value = str2num(buff, position, 14); + result.lli = str2num(buff, position + 14, 1); + result.lli = (unsigned char)result.lli & 0x03; // Extract LLI bits + return result; +} + +/** + * @brief Assign parsed values to appropriate RawSig fields + * + * Routes observation values to the correct field in a RawSig structure + * based on the observation type character. Implements type-safe assignment + * with validation to prevent data corruption. + * + * Observation type mapping: + * - 'C', 'P': Pseudorange/code observations -> signal.P + * - 'L': Carrier phase observations -> signal.L (with LLI) + * - 'D': Doppler observations -> signal.D + * - 'S': Signal-to-noise ratio -> signal.snr + * + * @param signal Reference to RawSig structure to modify + * @param observationType Single character observation type identifier + * @param value Numerical observation value to assign + * @param lli Loss of Lock Indicator (only used for phase observations) + * + * @note Only assigns non-zero values to prevent overwriting existing data + * @note LLI is only assigned for phase ('L') observations + */ +void assignObservationValue(RawSig& signal, char observationType, double value, double lli) +{ + if (value) // Only assign if value is valid (non-zero) + { + switch (observationType) + { + case 'P': // fallthrough + case 'C': + signal.P = value; + break; // Pseudorange (Code/Pulse) + case 'L': + signal.L = value; + signal.LLI = lli; + break; // Carrier Phase + case 'D': + signal.D = value; + break; // Doppler + case 'S': + signal.snr = value; + break; // Signal-to-Noise Ratio + } + } +} + +/** + * @brief Stage observation for later processing and validation + * + * Adds an observation to the staging area with complete metadata for later + * processing. Used for observations that can be immediately resolved without + * requiring priority-based selection logic. + * + * The staging pattern provides several benefits: + * - Deferred processing allows validation before commitment + * - Conflict detection and resolution + * - Consistent handling of all observation types + * - Enhanced debugging and logging capabilities + * + * @param staging Reference to staging container map + * @param obsType Single character observation type ('C', 'L', 'P', 'D', 'S') + * @param obsCode Resolved RINEX 3 observation code (e.g., L1C, C1W) + * @param frequency Frequency type enumeration (F1, F2, F5, etc.) + * @param value Numerical observation value + * @param lli Loss of Lock Indicator (0-3) + * + * @note Creates composite key from obsType + frequency + obsCode for uniqueness + * @note Logs staging operation for debugging purposes + * + * @see stagePhaseObservation() for priority-based phase observations + * @see ObservationKey for key structure details + */ +void stageObservation( + ObservationStaging& staging, + char obsType, + E_ObsCode obsCode, + E_FType frequency, + double value, + double lli +) +{ + ObservationKey key = {obsType, frequency, obsCode}; + + StagedObservation staged; + staged.value = value; + staged.lli = lli; + staged.obsCode = obsCode; + staged.frequency = frequency; + staged.isValid = (value != 0.0); // Consider non-zero values as valid + + staging[key] = staged; + + BOOST_LOG_TRIVIAL(debug) << "Staged observation: type=" << obsType + << " code=" << enum_to_string(obsCode) << " freq=" << (int)frequency + << " value=" << value; +} + +/** + * @brief Stage phase observation with priority resolution support + * + * Stages a phase observation that requires priority-based code resolution. + * Unlike regular observations, phase observations in RINEX 2 can map to + * multiple possible RINEX 3 codes, requiring selection based on available + * code observations. + * + * Priority resolution example: + * - Configuration: L1 -> [L1W, L1C] (try L1W first, then L1C) + * - If P1 has data -> L1W is available -> use L1W for L1 phase + * - If P1 is zero but C1 has data -> use L1C for L1 phase + * + * @param staging Reference to staging container map + * @param obsType Single character observation type (typically 'L') + * @param priorityCodes Vector of observation codes in priority order + * @param frequency Frequency type enumeration + * @param value Numerical phase observation value + * @param lli Loss of Lock Indicator + * + * @note Resolution occurs during commitStagedObservations() when code data is available + * @note Uses first priority code as temporary key for staging + * @note Logs priority array for debugging purposes + * + * @see commitStagedObservations() for priority resolution implementation + */ +void stagePhaseObservation( + ObservationStaging& staging, + char obsType, + const vector& priorityCodes, + E_FType frequency, + double value, + double lli +) +{ + // Use the first priority code as the key, but store the full priority array + E_ObsCode keyCode = E_ObsCode::NONE; + if (!priorityCodes.empty()) + { + keyCode = priorityCodes[0]; + } + ObservationKey key = {obsType, frequency, keyCode}; + + StagedObservation staged; + staged.value = value; + staged.lli = lli; + staged.obsCode = keyCode; + staged.frequency = frequency; + staged.isValid = (value != 0.0); + staged.priorityCodes = priorityCodes; + staged.isPhaseWithPriority = true; + + staging[key] = staged; + + BOOST_LOG_TRIVIAL(debug) << "Staged phase observation with priority: type=" << obsType + << " priority_codes=[" << + [&]() + { + string codes; + for (size_t i = 0; i < priorityCodes.size(); ++i) + { + if (i > 0) + codes += ","; + codes += enum_to_string(priorityCodes[i]); + } + return codes; + }() << "]" << " freq=" << (int)frequency + << " value=" << value; +} + +/** + * @brief Commit staged observations with phase priority resolution + * + * Processes all staged observations and transfers them to the final GObs + * structure. Implements sophisticated two-pass algorithm for phase observation + * priority resolution: + * + * Pass 1: Commit code observations and track available codes + * - Process all non-phase observations (C, P, D, S types) + * - Build set of available observation codes + * - Create RawSig entries in appropriate frequency lists + * + * Pass 2: Resolve and commit phase observations + * - For each phase observation with priority array + * - Find first available code from priority list + * - Fallback to first priority if none available + * - Commit resolved phase observation + * + * @param staging Container of staged observations to process + * @param obs Reference to output GObs structure to populate + * @param codeMap RINEX 2->3 code conversion map (for reference, not used in current impl) + * + * @note Phase resolution depends on code observations being processed first + * @note Extensive debug logging for troubleshooting priority resolution + * @note Creates RawSig entries using findOrCreateSignal() helper + * + * @see findOrCreateSignal(), assignObservationValue() + */ +void commitStagedObservations( + const ObservationStaging& staging, + GObs& obs, + const map& codeMap +) +{ + // Step 1: Collect all committed code observations to determine availability + set availableCodes; + + // Step 2: First pass - commit all non-phase observations and collect available codes + BOOST_LOG_TRIVIAL(debug) << "Phase priority: Starting first pass - collecting available codes"; + for (const auto& [key, staged] : staging) + { + if (!staged.isValid || staged.isPhaseWithPriority) + continue; + + auto& sigList = obs.sigsLists[staged.frequency]; + RawSig* rawSig = findOrCreateSignal(sigList, staged.obsCode); + + // Commit the staged observation + assignObservationValue(*rawSig, key.obsType, staged.value, staged.lli); + + // Track available codes for phase priority resolution + availableCodes.insert(staged.obsCode); + + BOOST_LOG_TRIVIAL(debug) << "Committed code observation: type=" << key.obsType + << " code=" << enum_to_string(staged.obsCode) + << " freq=" << (int)staged.frequency; + } + + BOOST_LOG_TRIVIAL(debug) << "Phase priority: Available codes: [" << [&]() + { + string codes; + for (const auto& code : availableCodes) + { + if (!codes.empty()) + codes += ","; + codes += enum_to_string(code); + } + return codes; + }() << "]"; + + // Step 3: Second pass - resolve and commit phase observations with priority + BOOST_LOG_TRIVIAL(debug) << "Phase priority: Starting second pass - resolving phase priorities"; + // this part was initially `for (const auto& [key, staged] : staging)`. However it seems to be + // an issue with clang in openMP sections + + for (const auto& stage : staging) + { + auto& key = stage.first; + auto& staged = stage.second; + if (!staged.isValid || !staged.isPhaseWithPriority) + continue; + + BOOST_LOG_TRIVIAL(debug) << "Phase priority: Processing phase " << key.obsType + << " with priorities [" << [&]() + { + string codes; + for (const auto& code : staged.priorityCodes) + { + if (!codes.empty()) + codes += ","; + codes += enum_to_string(code); + } + return codes; + }() << "]"; + + // Resolve priority: find first available code from priority list + E_ObsCode resolvedCode = E_ObsCode::NONE; + for (const auto& priorityCode : staged.priorityCodes) + { + BOOST_LOG_TRIVIAL(debug) << "Phase priority: Checking if " + << enum_to_string(priorityCode) << " is available"; + if (availableCodes.find(priorityCode) != availableCodes.end()) + { + resolvedCode = priorityCode; + BOOST_LOG_TRIVIAL(debug) << "Phase priority resolved: " << key.obsType << " -> " + << enum_to_string(resolvedCode) << " (available)"; + break; + } + else + { + BOOST_LOG_TRIVIAL(debug) + << "Phase priority: " << enum_to_string(priorityCode) << " not available"; + } + } + + // Fallback to first priority if none available + if (resolvedCode == E_ObsCode::NONE && !staged.priorityCodes.empty()) + { + resolvedCode = staged.priorityCodes[0]; + BOOST_LOG_TRIVIAL(warning) << "Phase priority fallback: " << key.obsType << " -> " + << enum_to_string(resolvedCode) << " (no available codes)"; + } + + if (resolvedCode != E_ObsCode::NONE) + { + auto& sigList = obs.sigsLists[staged.frequency]; + RawSig* rawSig = findOrCreateSignal(sigList, resolvedCode); + + // Commit the resolved phase observation + assignObservationValue(*rawSig, key.obsType, staged.value, staged.lli); + + BOOST_LOG_TRIVIAL(debug) + << "Committed phase observation: type=" << key.obsType + << " code=" << enum_to_string(resolvedCode) << " freq=" << (int)staged.frequency; + } + else + { + BOOST_LOG_TRIVIAL(error) << "Failed to resolve phase observation: " << key.obsType; + } + } +} + +/** Validate staged observations before committing + * Single Responsibility: Quality assurance for staged data + */ +bool validateStagedObservations(const ObservationStaging& staging, const SatSys& satellite) +{ + int validCount = 0; + int totalCount = 0; + + for (const auto& [obsKey, stagedObs] : staging) + { + totalCount++; + if (stagedObs.isValid) + validCount++; + } + + // Require at least one valid observation + bool isValid = (validCount > 0); + + BOOST_LOG_TRIVIAL(debug) << "Validation for " << satellite.id() << ": " << validCount << "/" + << totalCount << " valid observations" << " -> " + << (isValid ? "PASS" : "FAIL"); + + return isValid; +} + +/** Advanced validation and conflict resolution for staged observations + * Single Responsibility: Data quality assurance and conflict handling + */ +void resolveObservationConflicts(ObservationStaging& staging) +{ + // With the new composite key structure (type + frequency + code), + // each observation should be unique, so no conflicts should occur. + // This function is kept for future extensions or edge cases. + + BOOST_LOG_TRIVIAL(debug) << "Conflict resolution: " << staging.size() + << " unique observations (no conflicts expected with composite keys)"; +} + +/** Enhanced validation with detailed statistics + * Single Responsibility: Comprehensive data quality assessment + */ +ValidationReport validateStagedObservationsDetailed( + ObservationStaging& staging, // Note: non-const to allow conflict resolution + const SatSys& satellite +) +{ + ValidationReport report; + + // Resolve conflicts first (should be no-op with composite keys) + resolveObservationConflicts(staging); + + // Collect statistics + for (const auto& [key, staged] : staging) + { + report.observationCounts[key.obsType]++; + report.totalObservations++; + if (staged.isValid) + report.validObservations++; + } + + // Apply validation rules + report.passed = (report.validObservations > 0); // Require at least one valid observation + + BOOST_LOG_TRIVIAL(debug) << "Validation report for " << satellite.id() << ": " + << report.validObservations << "/" << report.totalObservations + << " valid observations - " << (report.passed ? "PASSED" : "FAILED"); + + return report; +} diff --git a/src/cpp/common/rinex.hpp b/src/cpp/common/rinex.hpp index df0307ca9..0d0328011 100644 --- a/src/cpp/common/rinex.hpp +++ b/src/cpp/common/rinex.hpp @@ -1,30 +1,868 @@ - +/** + * @file rinex.hpp + * @brief RINEX file format processing and observation handling + * + * This file contains functions and structures for reading and processing RINEX + * (Receiver Independent Exchange Format) files, including observation data, + * navigation data, and station information. Supports both RINEX 2.x and 3.x formats + * with comprehensive error handling and validation. + * + * Key features: + * - RINEX 2.x and 3.x format support + * - Observation type conversion and mapping + * - Staging pattern for robust data processing + * - Phase observation priority resolution + * - Comprehensive validation and error handling + * + * @author Geoscience Australia + * @date 2024 + * @version 1.0 + */ #pragma once #include +#include +#include +#include +#include "common/enums.h" +#include "common/observations.hpp" /// @todo try to remove this dependency (trying minimising header dependencies) -#include "enums.h" +using std::map; +using std::string; +using std::vector; struct RinexStation; struct Navigation; struct ObsList; +struct GObs; +struct SatSys; +struct GTime; + +// Forward declarations for ephemeris and navigation message types +struct Eph; // GPS/Galileo/QZS/BeiDou ephemeris +struct Geph; // GLONASS ephemeris +struct Seph; // SBAS ephemeris +struct Ceph; // CNVX ephemeris +struct STO; // System Time Offset +struct EOP; // Earth Orientation Parameters +struct ION; // Ionospheric parameters +/** + * @brief Ephemeris type enumeration for RINEX navigation data + * + * Defines the various types of ephemeris and auxiliary data that can be + * found in RINEX navigation files. Used for parsing and processing + * navigation messages from different GNSS systems. + * + * @note Values correspond to RINEX navigation message types + */ +enum class E_EphType : short int +{ + NONE, ///< Unknown or uninitialized ephemeris type + EPH, ///< GPS/QZS LNAV, GAL IFNV, BDS D1D2 Ephemeris + GEPH, ///< GLONASS Ephemeris (frequency division) + SEPH, ///< SBAS Ephemeris (geostationary satellites) + CEPH, ///< GPS/QZS/BDS CNVX Ephemeris (civil navigation) + STO, ///< System Time Offset message + EOP, ///< Earth Orientation Parameters message + ION ///< Ionospheric parameters message +}; +/** + * @brief RINEX observation code type structure + * + * Encapsulates observation code information for both RINEX 2.x and 3.x formats. + * Provides helper methods to determine format compatibility and effective codes. + * + * RINEX 2 uses 2-character codes (e.g., L1, C1, P1) + * RINEX 3 uses 3-character codes (e.g., L1C, C1W, L2X) + * + * @note One of code or code2 should be set, but not necessarily both + */ struct CodeType { - char type = 0; - E_ObsCode code = E_ObsCode::NONE; + char type = 0; ///< Single character observation type identifier ('C', 'L', 'P', 'D', 'S') + E_ObsCode code = + E_ObsCode::NONE; ///< RINEX 3 style observation code (3-character codes like L1C) + E_ObsCode2 code2 = + E_ObsCode2::NONE; ///< RINEX 2 style observation code (2-character codes like L1) + + /** + * @brief Get the effective observation code for processing + * + * Returns the RINEX 3 style code if available, otherwise attempts conversion + * from RINEX 2 style code. Used to normalize observation codes across formats. + * + * @return E_ObsCode The effective observation code, or NONE if unavailable + * + * @note Future enhancement: implement conversion logic from code2 to code + */ + E_ObsCode getEffectiveCode() const + { + if (code != E_ObsCode::NONE) + { + return code; + } + + // For RINEX 2, would need conversion logic here + // This is where conversion from code2 to code would happen if needed + return E_ObsCode::NONE; + } + + /** + * @brief Check if this represents a RINEX 2 style observation code + * + * @return true if code2 is set and code is not set + * @return false otherwise + */ + bool isRinex2Style() const { return (code2 != E_ObsCode2::NONE) && (code == E_ObsCode::NONE); } + + /** + * @brief Check if this represents a RINEX 3 style observation code + * + * @return true if code is set (regardless of code2 status) + * @return false if code is not set + */ + bool isRinex3Style() const { return (code != E_ObsCode::NONE); } }; +/** + * @brief Decode RINEX 2.x observation data from input stream + * + * Processes observation data lines from RINEX 2.x format files. Handles satellite + * identification from epoch header and maps observation types using system-specific + * code type configurations. + * + * @param inputStream Input stream containing RINEX data + * @param line Current observation data line to process + * @param sysCodeTypes System-specific observation code type mappings + * @param obs Output observation structure to populate + * @param v2SatSys Satellite system identifier from RINEX 2 epoch header + * @param rnxRec RINEX station information and configuration + * + * @return int Processing status (1 = success, 0 = failure) + * + * @note RINEX 2 satellite ID comes from epoch header, not observation line + * @see decodeObsDataRinex3() for RINEX 3.x equivalent + */ +int decodeObsDataRinex2( + std::istream& inputStream, + string& line, + map>& sysCodeTypes, + GObs& obs, + SatSys& v2SatSys, + RinexStation& rnxRec +); + +/** + * @brief Decode RINEX 3.x observation data from input stream + * + * Processes observation data lines from RINEX 3.x format files. Extracts satellite + * identification from observation line and uses staging pattern for robust data + * processing and validation. + * + * @param inputStream Input stream containing RINEX data + * @param line Current observation data line to process + * @param sysCodeTypes System-specific observation code type mappings + * @param obs Output observation structure to populate + * @param rnxRec RINEX station information and configuration + * + * @return int Processing status (1 = success, 0 = failure) + * + * @note RINEX 3 satellite ID is embedded in each observation line + * @note Uses staging pattern for conflict detection and resolution + * @see decodeObsDataRinex2() for RINEX 2.x equivalent + */ +int decodeObsDataRinex3( + std::istream& inputStream, + string& line, + map>& sysCodeTypes, + GObs& obs, + RinexStation& rnxRec +); + +/** + * @brief Read and parse complete RINEX file + * + * Main entry point for RINEX file processing. Automatically detects file type + * (observation, navigation, etc.) and version, then dispatches to appropriate + * processing functions. + * + * @param inputStream Input stream containing RINEX file data + * @param type Output parameter for detected file type ('O', 'N', 'G', etc.) + * @param obsList Output list of parsed observations + * @param nav Output navigation data structure + * @param rnxRec Output station information and metadata + * @param ver Output RINEX version number + * @param sys Output primary GNSS system + * @param tsys Output time system used in file + * @param sysCodeTypes Output observation code type mappings by system + * + * @return int Processing status (positive = success, negative = error) + * + * @note Automatically handles both RINEX 2.x and 3.x formats + * @note File type detection is based on header information + */ int readRnx( - std::istream& inputStream, - char& type, - ObsList& obsList, - Navigation& nav, - RinexStation& rnxRec, - double& ver, - E_Sys& sys, - E_TimeSys& tsys, - map>& sysCodeTypes); - -string rinexSysDesc( - E_Sys sys); + std::istream& inputStream, + char& type, + ObsList& obsList, + Navigation& nav, + RinexStation& rnxRec, + double& ver, + E_Sys& sys, + E_TimeSys& tsys, + map>& sysCodeTypes +); + +/** + * @brief Get human-readable description of GNSS system + * + * Converts GNSS system enumeration to descriptive string for logging + * and user interface purposes. + * + * @param sys GNSS system enumeration value + * @return string Human-readable system description + * + * @note Used primarily for diagnostic output and error messages + */ +string rinexSysDesc(E_Sys sys); + +// RINEX Header Processing Functions + +/** + * @brief Set string without trailing spaces + * + * Copies source string to destination buffer while removing trailing spaces. + * Ensures proper null termination and prevents buffer overflow. + * + * @param dst Destination character buffer + * @param src Source string to copy + * @param n Maximum number of characters to copy + * + * @warning Assumes dst buffer is large enough to hold result + */ +void setstr(char* dst, const char* src, int n); + +/** + * @brief Decode RINEX observation file header + * + * Parses header lines from RINEX observation files, extracting station information, + * observation types, antenna details, and system-specific configurations. + * + * @param inputStream Input stream containing RINEX header data + * @param line Current header line being processed + * @param ver RINEX version number (2.x or 3.x) + * @param tsys Time system used in file (GPS, UTC, etc.) + * @param sysCodeTypes Output map of observation code types by system + * @param nav Navigation data structure (for ionospheric parameters) + * @param rnxRec Output structure for station information and metadata + */ +void decodeObsH( + std::istream& inputStream, + string& line, + double ver, + E_TimeSys& tsys, + map>& sysCodeTypes, + Navigation& nav, + RinexStation& rnxRec +); + +/** + * @brief Decode RINEX navigation file header + * + * Parses header lines from RINEX navigation files, extracting ionospheric + * parameters, time system corrections, and other auxiliary navigation data. + * + * @param line Header line to decode + * @param sys GNSS system identifier (GPS, GLO, GAL, BDS, etc.) + * @param nav Navigation data structure to populate + */ +void decodeNavH(string& line, E_Sys sys, Navigation& nav); + +/** + * @brief Decode GLONASS navigation file header + * + * Processes header lines specific to GLONASS navigation files, including + * system time corrections and GLONASS-specific parameters. + * + * @param line Header line to decode + * @param nav Navigation data structure to populate + */ +void decodeGnavH(string& line, Navigation& nav); + +/** + * @brief Decode SBAS/Geostationary navigation file header + * + * Processes header lines specific to SBAS and geostationary satellite + * navigation files with unique orbital parameters. + * + * @param line Header line to decode + * @param nav Navigation data structure to populate + */ +void decodeHnavH(string& line, Navigation& nav); + +/** + * @brief Read RINEX file header section + * + * Reads and parses the complete header section with automatic file type + * and version detection. + * + * @param inputStream Input stream containing RINEX file data + * @param ver Output RINEX version number + * @param type Output file type character ('O', 'N', 'G', etc.) + * @param sys Output primary GNSS system + * @param tsys Output time system + * @param sysCodeTypes Output observation code type mappings + * @param nav Navigation data structure + * @param rnxRec Station information structure + * @return int Processing status (0 = success, negative = error) + */ +int readRnxH( + std::istream& inputStream, + double& ver, + char& type, + E_Sys& sys, + E_TimeSys& tsys, + map>& sysCodeTypes, + Navigation& nav, + RinexStation& rnxRec +); + +// RINEX Observation Processing Functions + +/** + * @brief Decode observation epoch header information + * + * Parses epoch header lines to extract time stamps, epoch flags, and satellite lists. + * Handles both RINEX 2.x and 3.x epoch formats. + * + * @param inputStream Input stream for reading continuation lines + * @param line Current epoch header line + * @param ver RINEX version number + * @param tsys Time system for time stamp interpretation + * @param time Output time stamp for this epoch + * @param flag Output epoch flag (0=OK, 1=power failure, etc.) + * @param sats Output vector of satellites in this epoch + * @return int Number of satellites in epoch, or negative for error + */ +int decodeObsEpoch( + std::istream& inputStream, + string& line, + double ver, + E_TimeSys tsys, + GTime& time, + int& flag, + vector& sats +); + +/** + * @brief Read RINEX observation data body section + * + * Processes observation epochs and individual satellite observations with + * data validation and error recovery. + * + * @param inputStream Input stream containing observation data + * @param ver RINEX version number + * @param tsys Time system + * @param sysCodeTypes Observation code type mappings + * @param flag Output epoch flag from last processed epoch + * @param obsList Output list of parsed observations + * @param rnxRec Station information and configuration + * @return int Number of epochs processed, or negative for error + */ +int readRnxObsB( + std::istream& inputStream, + double ver, + E_TimeSys tsys, + map>& sysCodeTypes, + int& flag, + ObsList& obsList, + RinexStation& rnxRec +); + +/** + * @brief Read complete RINEX observation file + * + * Main entry point for processing RINEX observation files with header + * and data section coordination. + * + * @param inputStream Input stream containing complete observation file + * @param ver RINEX version number + * @param tsys Time system used in file + * @param sysCodeTypes Observation code type mappings + * @param obsList Output list of all parsed observations + * @param rnxRec Station information and metadata + * @return int Processing status (positive = success, negative = error) + */ +int readRnxObs( + std::istream& inputStream, + double ver, + E_TimeSys tsys, + map>& sysCodeTypes, + ObsList& obsList, + RinexStation& rnxRec +); + +// RINEX Navigation/Ephemeris Processing Functions + +/** + * @brief Decode GPS/Galileo/QZS/BeiDou ephemeris parameters + * + * Parses Keplerian orbital elements from RINEX navigation data and converts + * to standardized ephemeris structure. + * + * @param ver RINEX version number + * @param Sat Satellite system identifier + * @param toc Time of clock reference epoch + * @param data Vector of decoded navigation parameters + * @param eph Output ephemeris structure + * @return int Processing status (1 = success, 0 = error) + */ +int decodeEph(double ver, SatSys Sat, GTime toc, vector& data, Eph& eph); + +/** + * @brief Decode GLONASS ephemeris parameters + * + * Parses GLONASS navigation message parameters using FDMA orbital + * representation with position/velocity state vectors. + * + * @param ver RINEX version number + * @param Sat GLONASS satellite identifier + * @param toc Time of clock reference epoch + * @param data Vector of GLONASS navigation parameters + * @param geph Output GLONASS ephemeris structure + * @return int Processing status (1 = success, 0 = error) + */ +int decodeGeph(double ver, SatSys Sat, GTime toc, vector& data, Geph& geph); + +/** + * @brief Decode SBAS/geostationary satellite ephemeris + * + * Parses ephemeris parameters for SBAS satellites using simplified + * geostationary orbital models. + * + * @param ver RINEX version number + * @param Sat SBAS satellite identifier + * @param toc Time of clock reference epoch + * @param data Vector of SBAS navigation parameters + * @param seph Output SBAS ephemeris structure + * @return int Processing status (1 = success, 0 = error) + */ +int decodeSeph(double ver, SatSys Sat, GTime toc, vector& data, Seph& seph); + +/** + * @brief Decode CNVX (Civil Navigation) ephemeris parameters + * + * Processes ephemeris data from civil navigation messages for modernized + * GNSS signals with enhanced accuracy and integrity. + * + * @param ver RINEX version number + * @param Sat Satellite identifier + * @param type Navigation message type + * @param toc Time of clock reference epoch + * @param data Vector of CNVX navigation parameters + * @param ceph Output CNVX ephemeris structure + * @return int Processing status (1 = success, 0 = error) + */ +int decodeCeph( + double ver, + SatSys Sat, + E_NavMsgType type, + GTime toc, + vector& data, + Ceph& ceph +); + +/** + * @brief Decode System Time Offset (STO) message + * + * Processes system time offset parameters for inter-system time conversions. + * + * @param ver RINEX version number + * @param Sat Reference satellite system + * @param type Navigation message type + * @param toc Reference time epoch + * @param data Vector of STO parameters + * @param sto Output system time offset structure + * @return int Processing status (1 = success, 0 = error) + */ +int decodeSto(double ver, SatSys Sat, E_NavMsgType type, GTime toc, vector& data, STO& sto); + +/** + * @brief Decode Earth Orientation Parameters (EOP) message + * + * Processes Earth orientation parameters for precise coordinate transformations. + * + * @param ver RINEX version number + * @param Sat Reference satellite system + * @param type Navigation message type + * @param toc Reference time epoch + * @param data Vector of EOP parameters + * @param eop Output Earth orientation parameters + * @return int Processing status (1 = success, 0 = error) + */ +int decodeEop(double ver, SatSys Sat, E_NavMsgType type, GTime toc, vector& data, EOP& eop); + +/** + * @brief Decode ionospheric parameters (ION) message + * + * Processes ionospheric delay model parameters for single-frequency correction. + * + * @param ver RINEX version number + * @param Sat Reference satellite system + * @param type Navigation message type + * @param toc Reference time epoch + * @param data Vector of ionospheric model parameters + * @param ion Output ionospheric parameters + * @return int Processing status (1 = success, 0 = error) + */ +int decodeIon(double ver, SatSys Sat, E_NavMsgType type, GTime toc, vector& data, ION& ion); + +/** + * @brief Read RINEX navigation data body section + * + * Parses navigation file data section with automatic message type detection + * and dispatching to appropriate decoders. + * + * @param inputStream Input stream containing navigation data + * @param ver RINEX version number + * @param sys Primary GNSS system + * @param type Output ephemeris type detected + * @param eph Output GPS/GAL/QZS/BDS ephemeris + * @param geph Output GLONASS ephemeris + * @param seph Output SBAS ephemeris + * @param ceph Output CNVX ephemeris + * @param sto Output system time offset + * @param eop Output Earth orientation parameters + * @param ion Output ionospheric parameters + * @return int Processing status (1 = success, 0 = end, negative = error) + */ +int readRnxNavB( + std::istream& inputStream, + double ver, + E_Sys sys, + E_EphType& type, + Eph& eph, + Geph& geph, + Seph& seph, + Ceph& ceph, + STO& sto, + EOP& eop, + ION& ion +); + +/** + * @brief Read complete RINEX navigation file + * + * Processes complete navigation files including GPS, GLONASS, Galileo, + * BeiDou, and mixed-constellation files. + * + * @param inputStream Input stream containing navigation file + * @param ver RINEX version number + * @param sys Primary GNSS system + * @param nav Output navigation data structure + * @return int Processing status (positive = records, negative = error) + */ +int readRnxNav(std::istream& inputStream, double ver, E_Sys sys, Navigation& nav); + +/** + * @brief Read RINEX clock file + * + * Processes RINEX clock files containing high-precision satellite and + * station clock corrections. + * + * @param inputStream Input stream containing clock file data + * @param ver RINEX version number + * @param nav Navigation data structure for clock corrections + * @return int Processing status (positive = records, negative = error) + */ +int readRnxClk(std::istream& inputStream, double ver, Navigation& nav); + +/** + * @brief Container for parsed RINEX observation values + * + * Holds a single observation value along with its Loss of Lock Indicator (LLI). + * Used as intermediate storage during RINEX parsing operations. + * + * @note LLI bits indicate phase break events and cycle slip detection + */ +struct ObservationValues +{ + double value; ///< Numerical observation value (pseudorange, phase, doppler, SNR) + double lli; ///< Loss of Lock Indicator (0-3, phase observations only) +}; + +/** + * @brief Find existing signal or create new one in signal list + * + * Template function that searches for a signal with matching observation code + * in the provided signal list. Creates and appends a new signal if not found. + * + * @tparam SigList Type of signal list container (e.g., std::vector) + * @param sigList Reference to signal list to search/modify + * @param obsCode Observation code to find or create + * + * @return RawSig* Pointer to existing or newly created signal + * + * @note Template design allows use with different signal list types + * @note Always returns valid pointer - creates new entry if needed + */ +template +RawSig* findOrCreateSignal(SigList& sigList, E_ObsCode obsCode) +{ + // Find existing signal + for (auto& sig : sigList) + { + if (sig.code == obsCode) + { + return &sig; + } + } + + // Create new signal if not found + RawSig raw; + raw.code = obsCode; + sigList.push_back(raw); + return &sigList.back(); +} + +/** + * @brief Parse observation values from RINEX formatted text buffer + * + * Extracts numerical observation value and Loss of Lock Indicator from + * RINEX formatted line at specified position with comprehensive bounds checking. + * + * @param buff Character buffer containing RINEX observation line + * @param position Starting position in buffer (0-based index) + * + * @return ObservationValues Structure containing parsed value and LLI + * + * @note RINEX format: 14 chars value + 1 char LLI + 1 char signal strength + * @note Returns zeros if parsing fails or position is out of bounds + */ +ObservationValues parseObservationValues(char* buff, int position); + +/** + * @brief Assign parsed observation value to appropriate signal field + * + * Routes observation values to correct RawSig field based on observation type. + * Provides type-safe assignment with validation to prevent data corruption. + * + * @param signal Reference to RawSig structure to modify + * @param observationType Single character observation type ('C', 'L', 'P', 'D', 'S') + * @param value Numerical observation value to assign + * @param lli Loss of Lock Indicator (used only for phase observations) + * + * @note Only assigns non-zero values to prevent overwriting existing data + * @note LLI assignment limited to phase ('L') observations + */ +void assignObservationValue(RawSig& signal, char observationType, double value, double lli); + +/** + * @brief Staged observation data for deferred processing + * + * Container for observation data that requires validation and conflict resolution + * before final commitment. Supports both direct observations and phase observations + * with priority-based code resolution. + * + * @note Used in staging pattern to enable robust data processing + */ +struct StagedObservation +{ + double value = 0.0; ///< Numerical observation value + double lli = 0.0; ///< Loss of Lock Indicator + E_ObsCode obsCode = E_ObsCode::NONE; ///< Primary observation code + E_FType frequency = E_FType::NONE; ///< Frequency type (F1, F2, F5, etc.) + bool isValid = false; ///< Validation status flag + + vector priorityCodes; ///< Priority-ordered codes for phase resolution + bool isPhaseWithPriority = false; ///< Flag indicating priority-based phase observation +}; + +/** + * @brief Composite key for staging observation map + * + * Uniquely identifies staged observations using observation type, frequency, + * and observation code. Implements comparison operator for use in std::map. + * + * @note Ensures unique identification of observations during staging + */ +struct ObservationKey +{ + char obsType; ///< Single character observation type ('C', 'L', 'P', 'D', 'S') + E_FType frequency; ///< Frequency enumeration (F1, F2, F5, etc.) + E_ObsCode obsCode; ///< Observation code enumeration + + /** + * @brief Comparison operator for std::map ordering + * + * Implements lexicographic ordering: obsType, then frequency, then obsCode. + * Required for use as key in std::map containers. + * + * @param other Other ObservationKey to compare against + * @return true if this key is less than other key + */ + bool operator<(const ObservationKey& other) const + { + if (obsType != other.obsType) + return obsType < other.obsType; + if (frequency != other.frequency) + return frequency < other.frequency; + return obsCode < other.obsCode; + } +}; + +/// Type alias for observation staging container +using ObservationStaging = std::map; + +/** + * @brief Stream output operator for ObservationKey + * + * Enables logging and debugging output for ObservationKey structures. + * Formats key components in human-readable form. + * + * @param os Output stream reference + * @param key ObservationKey to output + * @return std::ostream& Reference to output stream for chaining + */ +inline std::ostream& operator<<(std::ostream& os, const ObservationKey& key) +{ + os << "{obsType:" << key.obsType << ",freq:" << key.frequency + << ",obsCode:" << enum_to_string(key.obsCode) << "}"; + return os; +} + +/** + * @brief Stage regular observation for deferred processing + * + * Adds observation to staging area for later validation and commitment. + * Used for observations that can be directly resolved without priority logic. + * + * @param staging Reference to staging container map + * @param obsType Single character observation type ('C', 'L', 'P', 'D', 'S') + * @param obsCode Resolved observation code (e.g., L1C, C1W) + * @param frequency Frequency type enumeration + * @param value Numerical observation value + * @param lli Loss of Lock Indicator + * + * @note Creates composite key for unique identification + * @see stagePhaseObservation() for priority-based phase observations + */ +void stageObservation( + ObservationStaging& staging, + char obsType, + E_ObsCode obsCode, + E_FType frequency, + double value, + double lli +); + +/** + * @brief Commit all staged observations to final structure + * + * Processes staged observations with two-pass algorithm: commit code observations + * first to establish available codes, then resolve and commit phase observations + * using priority-based code selection. + * + * @param staging Container of staged observations to process + * @param obs Reference to output GObs structure to populate + * @param codeMap RINEX 2->3 code conversion map (for reference) + * + * @note Phase resolution depends on code observations being processed first + * @note Extensive debug logging for troubleshooting priority resolution + */ +void commitStagedObservations( + const ObservationStaging& staging, + GObs& obs, + const map& codeMap +); + +/** + * @brief Validate staged observations before commitment + * + * Performs basic validation checks on staged observation data to ensure + * data quality and consistency before final processing. + * + * @param staging Container of staged observations to validate + * @param satellite Satellite system for context-specific validation + * + * @return true if all validations pass + * @return false if validation failures detected + * + * @note Basic validation - see validateStagedObservationsDetailed() for comprehensive checks + */ +bool validateStagedObservations(const ObservationStaging& staging, const SatSys& satellite); + +/** + * @brief Comprehensive validation report for staged observations + * + * Contains detailed statistics and validation results for staged observation + * data. Provides metrics for data quality assessment and error diagnosis. + */ +struct ValidationReport +{ + int totalObservations = 0; ///< Total number of observations processed + int validObservations = 0; ///< Number of observations passing validation + int conflictingTypes = 0; ///< Number of observation type conflicts detected + std::map observationCounts; ///< Count of observations by type ('C', 'L', etc.) + bool passed = false; ///< Overall validation pass/fail status +}; + +/** + * @brief Resolve conflicts in staged observations + * + * Analyzes staged observations for conflicts and inconsistencies, applying + * resolution strategies to ensure data integrity. Modifies staging container + * to resolve detected conflicts. + * + * @param staging Reference to staging container (modified in-place) + * + * @note Implements conflict resolution strategies for overlapping observations + * @note May remove or modify staged observations to resolve conflicts + */ +void resolveObservationConflicts(ObservationStaging& staging); + +/** + * @brief Comprehensive validation with detailed reporting + * + * Performs thorough validation of staged observations with comprehensive + * statistics collection and detailed error reporting. Provides enhanced + * diagnostics for data quality assessment. + * + * @param staging Reference to staging container (may be modified for conflict resolution) + * @param satellite Satellite system for context-specific validation rules + * + * @return ValidationReport Detailed validation results and statistics + * + * @note More comprehensive than validateStagedObservations() + * @note May modify staging container during conflict resolution + */ +ValidationReport +validateStagedObservationsDetailed(ObservationStaging& staging, const SatSys& satellite); + +/** + * @brief Stage phase observation with priority-based code resolution + * + * Stages phase observation that requires priority-based code selection. + * Used when phase observation can map to multiple possible codes based on + * available code observations. + * + * @param staging Reference to staging container map + * @param obsType Single character observation type (typically 'L') + * @param priorityCodes Vector of observation codes in priority order + * @param frequency Frequency type enumeration + * @param value Numerical phase observation value + * @param lli Loss of Lock Indicator + * + * @note Code resolution occurs during commitStagedObservations() + * @note Uses first priority code as temporary staging key + * @see commitStagedObservations() for priority resolution implementation + */ +void stagePhaseObservation( + ObservationStaging& staging, + char obsType, + const vector& priorityCodes, + E_FType frequency, + double value, + double lli +); diff --git a/src/cpp/common/rinexClkWrite.cpp b/src/cpp/common/rinexClkWrite.cpp index 00aac380e..4977cca6a 100644 --- a/src/cpp/common/rinexClkWrite.cpp +++ b/src/cpp/common/rinexClkWrite.cpp @@ -1,469 +1,582 @@ - // #pragma GCC optimize ("O0") - +#include "common/rinexClkWrite.hpp" #include -#include #include +#include +#include "ambres/GNSSambres.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/ephPrecise.hpp" +#include "common/ephemeris.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/rinex.hpp" +#include "common/rinexNavWrite.hpp" +#include "common/rinexObsWrite.hpp" +#include "common/sinex.hpp" +#include "pea/inputsOutputs.hpp" using std::map; -#include "inputsOutputs.hpp" -#include "rinexNavWrite.hpp" -#include "rinexObsWrite.hpp" -#include "rinexClkWrite.hpp" -#include "ephPrecise.hpp" -#include "GNSSambres.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "ephemeris.hpp" -#include "constants.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "sinex.hpp" -#include "rinex.hpp" -#include "enums.h" +constexpr double VERSION = 3.00; /* macro defintions */ -#define VERSION 3.00 struct ClockEntry { - string id; // Either station of satellite. - string monid; // Monument identification, receiver. - Vector3d recPos = {}; // Receiver position. - double clock = 0; // Mean clock delta reference. - double sigma = 0; // Standard deviation. - bool isRec = true; // If true is receiver clock data. - vector clkIndices; + string id; // Either station of satellite. + string monid; // Monument identification, receiver. + Vector3d recPos = {}; // Receiver position. + double clock = 0; // Mean clock delta reference. + double sigma = 0; // Standard deviation. + bool isRec = true; // If true is receiver clock data. + vector clkIndices; }; struct ClockList : vector { - }; void outputRinexClocksBody( - string& filename, ///< Path to output file. - ClockList& clkEntryList, ///< List of data to print. - const GTime& time) ///< Epoch time. + string& filename, ///< Path to output file. + ClockList& clkEntryList, ///< List of data to print. + const GTime& time ///< Epoch time. +) { - std::ofstream clockFile(filename, std::ofstream::app); - - if (!clockFile) - { - BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for RINEX clock file."; - return; - } - - GEpoch ep = time; - - for (auto& clkEntry : clkEntryList) - { - string dataType; - if (clkEntry.isRec) dataType = "AR"; // Result for receiver clock. - else dataType = "AS"; // Result for satellite clock. - - int numData = 2; // Number of data values is 2, clock and sigma. - tracepdeex(0,clockFile,"%2s %-4s %4d%3d%3d%3d%3d%10.6f%3d %19.12E %19.12E\n", - dataType.c_str(), clkEntry.id.c_str(), - (int) ep[0], - (int) ep[1], - (int) ep[2], - (int) ep[3], - (int) ep[4], - ep[5], - numData, - clkEntry.clock, - clkEntry.sigma); - } + std::ofstream clockFile(filename, std::ofstream::app); + + if (!clockFile) + { + BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for RINEX clock file."; + return; + } + + GEpoch ep = time; + + for (auto& clkEntry : clkEntryList) + { + string dataType; + if (clkEntry.isRec) + dataType = "AR"; // Result for receiver clock. + else + dataType = "AS"; // Result for satellite clock. + + int numData = 2; // Number of data values is 2, clock and sigma. + tracepdeex( + 0, + clockFile, + "%2s %-4s %4d%3d%3d%3d%3d%10.6f%3d %19.12E %19.12E\n", + dataType.c_str(), + clkEntry.id.c_str(), + (int)ep[0], + (int)ep[1], + (int)ep[2], + (int)ep[3], + (int)ep[4], + ep[5], + numData, + clkEntry.clock, + clkEntry.sigma + ); + } } -void getKalmanSatClks( - ClockList& clkEntryList, - map& outSys, - KFState& kfState) +void getKalmanSatClks(ClockList& clkEntryList, map& outSys, KFState& kfState) { - map clockEntries; - - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( key.type != KF::SAT_CLOCK - ||outSys[key.Sat.sys] == false) - { - continue; - } - - double clk = 0; - double variance = 0; - kfState.getKFValue(key, clk, &variance); - - ClockEntry& clkEntry = clockEntries[key.Sat.id()]; - clkEntry.id = key.Sat.id(); - clkEntry.isRec = false; - - clkEntry.clkIndices.push_back(index); - } - - for (auto& [id, clkEntry] : clockEntries) - { - clkEntry.clock = kfState.x(clkEntry.clkIndices ).sum() / CLIGHT; - clkEntry.sigma = sqrt( kfState.P(clkEntry.clkIndices, clkEntry.clkIndices ).sum()) / CLIGHT; - - clkEntryList.push_back(clkEntry); - } + map clockEntries; + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::SAT_CLOCK || outSys[key.Sat.sys] == false) + { + continue; + } + + double clk = 0; + double variance = 0; + kfState.getKFValue(key, clk, &variance); + + ClockEntry& clkEntry = clockEntries[key.Sat.id()]; + clkEntry.id = key.Sat.id(); + clkEntry.isRec = false; + + clkEntry.clkIndices.push_back(index); + } + + for (auto& [id, clkEntry] : clockEntries) + { + clkEntry.clock = kfState.x(clkEntry.clkIndices).sum() / CLIGHT; + clkEntry.sigma = sqrt(kfState.P(clkEntry.clkIndices, clkEntry.clkIndices).sum()) / CLIGHT; + + clkEntryList.push_back(clkEntry); + } } -void getKalmanRecClks( - ClockList& clkEntryList, - ClockEntry& referenceRec, - KFState& kfState) +void getKalmanRecClks(ClockList& clkEntryList, ClockEntry& referenceRec, KFState& kfState) { - map clockEntries; - - SatSys firstSys; - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( ( key.type == KF::REC_SYS_BIAS - || key.type == KF::REC_CLOCK) - &&( firstSys == E_Sys::NONE - || firstSys == key.Sat)) - { - firstSys = key.Sat; - - double clk = 0; - double variance = 0; - kfState.getKFValue(key, clk, &variance); - - ClockEntry& clkEntry = clockEntries[key.str]; - clkEntry.id = key.str; - clkEntry.isRec = true; - - clkEntry.clkIndices.push_back(index); - - if (key.rec_ptr == nullptr) - { - BOOST_LOG_TRIVIAL(error) << "Error: Kalman RINEX file clock entry has no reference to receiver."; -// continue; - } - else - { - clkEntry.monid = key.rec_ptr->snx.id_ptr->domes; - clkEntry.recPos = key.rec_ptr->snx.pos; - } - } - - if ( key.type == KF::REC_SYS_BIAS - || key.type == KF::REC_CLOCK) - { - // Enter details for reference receiver if available. - referenceRec.id = key.str; - referenceRec.isRec = true; - - if (key.rec_ptr) - { - referenceRec.monid = key.rec_ptr->snx.id_ptr->domes; - referenceRec.recPos = key.rec_ptr->snx.pos; - } - } - } - - for (auto& [id, clkEntry] : clockEntries) - { - clkEntry.clock = kfState.x(clkEntry.clkIndices ).sum() / CLIGHT; - clkEntry.sigma = sqrt( kfState.P(clkEntry.clkIndices, clkEntry.clkIndices ).sum()) / CLIGHT; - - clkEntryList.push_back(clkEntry); - } + map clockEntries; + + SatSys firstSys; + for (auto& [key, index] : kfState.kfIndexMap) + { + if ((key.type == KF::REC_SYS_BIAS || key.type == KF::REC_CLOCK) && + (firstSys.sys == E_Sys::NONE || firstSys == key.Sat)) + { + firstSys = key.Sat; + + double clk = 0; + double variance = 0; + kfState.getKFValue(key, clk, &variance); + + ClockEntry& clkEntry = clockEntries[key.str]; + clkEntry.id = key.str; + clkEntry.isRec = true; + + clkEntry.clkIndices.push_back(index); + + if (key.rec_ptr == nullptr) + { + BOOST_LOG_TRIVIAL(error) + << "Kalman RINEX file clock entry has no reference to receiver."; + // continue; + } + else + { + clkEntry.monid = key.rec_ptr->snx.id_ptr->domes; + clkEntry.recPos = key.rec_ptr->snx.pos; + } + } + + if (key.type == KF::REC_SYS_BIAS || key.type == KF::REC_CLOCK) + { + // Enter details for reference receiver if available. + referenceRec.id = key.str; + referenceRec.isRec = true; + + if (key.rec_ptr) + { + referenceRec.monid = key.rec_ptr->snx.id_ptr->domes; + referenceRec.recPos = key.rec_ptr->snx.pos; + } + } + } + + for (auto& [id, clkEntry] : clockEntries) + { + clkEntry.clock = kfState.x(clkEntry.clkIndices).sum() / CLIGHT; + clkEntry.sigma = sqrt(kfState.P(clkEntry.clkIndices, clkEntry.clkIndices).sum()) / CLIGHT; + + clkEntryList.push_back(clkEntry); + } } -void getPreciseRecClks( - ClockList& clkEntryList, - ReceiverMap* receiverMap_ptr, - const GTime& time) +void getPreciseRecClks(ClockList& clkEntryList, ReceiverMap* receiverMap_ptr, const GTime& time) { - if (receiverMap_ptr == nullptr) - { - return; - } - - auto& receiverMap = *receiverMap_ptr; - - for (auto& [id, rec] : receiverMap) - { - double dt; - double variance; - int ret = pephclk(std::cout, time, rec.id, nav, dt, &variance); - if (ret != 1) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Receiver : " << rec.id - << ", precise clock entry not calculated."; - - continue; - } - - ClockEntry clkEntry; - clkEntry.id = rec.id; - clkEntry.clock = dt; - clkEntry.sigma = sqrt(variance); - clkEntry.isRec = true; - clkEntry.monid = rec.snx.id_ptr->domes; - clkEntry.recPos = rec.snx.pos; - - clkEntryList.push_back(clkEntry); - } + if (receiverMap_ptr == nullptr) + { + return; + } + + auto& receiverMap = *receiverMap_ptr; + + for (auto& [id, rec] : receiverMap) + { + double dt; + double variance; + int ret = pephclk(std::cout, time, rec.id, nav, dt, &variance); + if (ret != 1) + { + BOOST_LOG_TRIVIAL(warning) + << "Receiver: " << rec.id << ", precise clock entry not calculated."; + + continue; + } + + ClockEntry clkEntry; + clkEntry.id = rec.id; + clkEntry.clock = dt; + clkEntry.sigma = sqrt(variance); + clkEntry.isRec = true; + clkEntry.monid = rec.snx.id_ptr->domes; + clkEntry.recPos = rec.snx.pos; + + clkEntryList.push_back(clkEntry); + } } void getSatClksFromEph( - ClockList& clkEntryList, - const GTime& time, - map& outSys, - vector ephType) + ClockList& clkEntryList, + const GTime& time, + map& outSys, + vector ephType +) { - for (auto& [Sat, satNav] : nav.satNavMap) - { - if (outSys[Sat.sys] == false) - continue; - - // Create a dummy observation - GObs obs; - obs.Sat = Sat; - obs.satNav_ptr = &nav.satNavMap[Sat]; // for satpos_ssr() - - bool pass = true; - pass &= satclk(nullStream, time, time, obs, ephType, nav); - pass &= satpos(nullStream, time, time, obs, ephType, E_OffsetType::COM, nav); //use both for now to get ssr clocks if required - if (pass == false) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Satellite : " << Sat.id() - << ", clock entry not calculated."; - - continue; - } - - ClockEntry clkEntry; - clkEntry.id = Sat.id(); - clkEntry.clock = obs.satClk; - clkEntry.sigma = sqrt(obs.satClkVar); - clkEntry.isRec = false; - - clkEntryList.push_back(clkEntry); - } + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (outSys[Sat.sys] == false) + continue; + + // Create a dummy observation + GObs obs; + obs.Sat = Sat; + obs.satNav_ptr = &nav.satNavMap[Sat]; // for satpos_ssr() + + bool pass = true; + pass &= satclk(nullStream, time, time, obs, ephType, nav); + pass &= satpos( + nullStream, + time, + time, + obs, + ephType, + E_OffsetType::COM, + nav + ); // use both for now to get ssr + // clocks if required + if (pass == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Satellite: " << Sat.id() << ", clock entry not calculated."; + + continue; + } + + ClockEntry clkEntry; + clkEntry.id = Sat.id(); + clkEntry.clock = obs.satClk; + clkEntry.sigma = sqrt(obs.satClkVar) / CLIGHT; + clkEntry.isRec = false; + + clkEntryList.push_back(clkEntry); + } } void outputRinexClocksHeader( - string& filename, ///< Path of tile to output to - ClockList& clkEntryList, ///< List of clock values to output - ClockEntry& referenceRec, ///< Entry for the reference receiver - map& sysMap, ///< Options to enable outputting of specific systems - GTime time) ///< Epoch time + string& filename, ///< Path of tile to output to + ClockList& clkEntryList, ///< List of clock values to output + ClockEntry& referenceRec, ///< Entry for the reference receiver + map& sysMap, ///< Options to enable outputting of specific systems + GTime time ///< Epoch time +) { - std::ofstream clockFile(filename, std::ofstream::app); - - if (!clockFile) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Error opening " << filename << " for RINEX clock file."; - return; - } - - auto pos = clockFile.tellp(); - if (pos != 0) - return; - - - string sysDesc; - if (sysMap.size() == 1) sysDesc = rinexSysDesc(sysMap.begin()->first); - else sysDesc = rinexSysDesc(E_Sys::COMB); - - string clkRefStation = referenceRec.id; - - int numRecs = 0; - for (auto clkEntry : clkEntryList) - { - if (clkEntry.isRec) - { - numRecs++; - } - } - - tracepdeex(0, clockFile, "%9.2f%-11s%-20s%-20s%-20s\n", - VERSION, - "", - "C", - sysDesc.c_str(), - "RINEX VERSION / TYPE"); - - GEpoch ep = time; - - tracepdeex(0,clockFile,"%-20s%-20s%4d%02d%02d %02d%02d%02d %4s%s\n", - acsConfig.analysis_software .c_str(), - acsConfig.analysis_agency .c_str(), - (int)ep[0], - (int)ep[1], - (int)ep[2], - (int)ep[3], - (int)ep[4], - (int)ep[5], - "LCL","PGM / RUN BY / DATE"); - - tracepdeex(0,clockFile,"%-60s%s\n","", "SYS / # / OBS TYPES"); - tracepdeex(0,clockFile,"%-60s%s\n","", "TIME SYSTEM ID"); - tracepdeex(0,clockFile,"%6d %2s %2s%-42s%s\n", 2,"AS","AR","", "# / TYPES OF DATA"); - tracepdeex(0,clockFile,"%-60s%s\n","", "STATION NAME / NUM"); - tracepdeex(0,clockFile,"%-60s%s\n","", "STATION CLK REF"); - tracepdeex(0,clockFile,"%-3s %-55s%s\n", acsConfig.analysis_agency.c_str(), acsConfig.analysis_centre.c_str(), "ANALYSIS CENTER"); - tracepdeex(0,clockFile,"%6d%54s%s\n",1,"", "# OF CLK REF"); - - // Note clkRefStation can be a zero length string. - tracepdeex(0,clockFile,"%-4s %-20s%35s%s\n", "", clkRefStation.c_str(),"", "ANALYSIS CLK REF"); - tracepdeex(0,clockFile,"%6d %-50s%s\n", numRecs, "IGS14", "# OF SOLN STA / TRF"); - // MM This line causes the clock combination software to crash to removing - //tracepdeex(0,clockFile,"%-60s%s\n",acsConfig.rinex_comment, "COMMENT"); - - /* output receiver id and coordinates */ - - for (auto& clkEntry : clkEntryList) - { - if (clkEntry.isRec == false) - { - continue; - } - - string idStr = clkEntry.id .substr(0,4); - string monuid = clkEntry.monid .substr(0,20); - - tracepdeex(0,clockFile,"%-4s ",idStr.c_str()); - tracepdeex(0,clockFile,"%-20s",monuid.c_str()); - tracepdeex(0,clockFile,"%11.0f %11.0f %11.0f%s\n", - clkEntry.recPos(0) * 1000, - clkEntry.recPos(1) * 1000, - clkEntry.recPos(2) * 1000, - "SOLN STA NAME / NUM"); - } - - int num_sats = 0; - if (sysMap[E_Sys::GPS]) num_sats += NSATGPS; - if (sysMap[E_Sys::GLO]) num_sats += NSATGLO; - if (sysMap[E_Sys::GAL]) num_sats += NSATGAL; - if (sysMap[E_Sys::BDS]) num_sats += NSATBDS; - if (sysMap[E_Sys::QZS]) num_sats += NSATQZS; - - - /* output satellite PRN*/ - int k = 0; - tracepdeex(0,clockFile,"%6d%54s%s\n",num_sats,"","# OF SOLN SATS"); - if (sysMap[E_Sys::GPS]) for (int prn = 1; prn <= NSATGPS; prn++) {k++; SatSys s(E_Sys::GPS,prn); tracepdeex(0,clockFile,"%3s ", s.id().c_str()); if (k % 15 == 0) tracepdeex(0,clockFile,"%s\n","PRN LIST");} - if (sysMap[E_Sys::GLO]) for (int prn = 1; prn <= NSATGLO; prn++) {k++; SatSys s(E_Sys::GLO,prn); tracepdeex(0,clockFile,"%3s ", s.id().c_str()); if (k % 15 == 0) tracepdeex(0,clockFile,"%s\n","PRN LIST");} - if (sysMap[E_Sys::GAL]) for (int prn = 1; prn <= NSATGAL; prn++) {k++; SatSys s(E_Sys::GAL,prn); tracepdeex(0,clockFile,"%3s ", s.id().c_str()); if (k % 15 == 0) tracepdeex(0,clockFile,"%s\n","PRN LIST");} - if (sysMap[E_Sys::BDS]) for (int prn = 1; prn <= NSATBDS; prn++) {k++; SatSys s(E_Sys::BDS,prn); tracepdeex(0,clockFile,"%3s ", s.id().c_str()); if (k % 15 == 0) tracepdeex(0,clockFile,"%s\n","PRN LIST");} - if (sysMap[E_Sys::QZS]) for (int prn = 1; prn <= NSATQZS; prn++) {k++; SatSys s(E_Sys::QZS,prn); tracepdeex(0,clockFile,"%3s ", s.id().c_str()); if (k % 15 == 0) tracepdeex(0,clockFile,"%s\n","PRN LIST");} - /*finish the line*/ while (k % 15 != 0) {k++; tracepdeex(0,clockFile,"%3s ", ""); if (k % 15 == 0) tracepdeex(0,clockFile,"%s\n","PRN LIST");} - - tracepdeex(0,clockFile,"%-60s%s\n","","END OF HEADER"); + std::ofstream clockFile(filename, std::ofstream::app); + + if (!clockFile) + { + BOOST_LOG_TRIVIAL(warning) << "Error opening " << filename << " for RINEX clock file."; + return; + } + + auto pos = clockFile.tellp(); + if (pos != 0) + return; + + string sysDesc; + if (sysMap.size() == 1) + sysDesc = rinexSysDesc(sysMap.begin()->first); + else + sysDesc = rinexSysDesc(E_Sys::COMB); + + string clkRefStation = referenceRec.id; + + int numRecs = 0; + for (auto clkEntry : clkEntryList) + { + if (clkEntry.isRec) + { + numRecs++; + } + } + + tracepdeex( + 0, + clockFile, + "%9.2f%-11s%-20s%-20s%-20s\n", + VERSION, + "", + "C", + sysDesc.c_str(), + "RINEX VERSION / TYPE" + ); + + GEpoch ep = time; + + tracepdeex( + 0, + clockFile, + "%-20s%-20s%4d%02d%02d %02d%02d%02d %4s%s\n", + acsConfig.analysis_software.c_str(), + acsConfig.analysis_agency.c_str(), + (int)ep[0], + (int)ep[1], + (int)ep[2], + (int)ep[3], + (int)ep[4], + (int)ep[5], + "LCL", + "PGM / RUN BY / DATE" + ); + + tracepdeex(0, clockFile, "%-60s%s\n", "", "SYS / # / OBS TYPES"); + tracepdeex(0, clockFile, "%-60s%s\n", "GPS", "TIME SYSTEM ID"); + tracepdeex(0, clockFile, "%6d %2s %2s%-42s%s\n", 2, "AS", "AR", "", "# / TYPES OF DATA"); + tracepdeex(0, clockFile, "%-60s%s\n", "", "STATION NAME / NUM"); + tracepdeex(0, clockFile, "%-60s%s\n", "", "STATION CLK REF"); + tracepdeex( + 0, + clockFile, + "%-3s %-55s%s\n", + acsConfig.analysis_agency.c_str(), + acsConfig.analysis_centre.c_str(), + "ANALYSIS CENTER" + ); + tracepdeex(0, clockFile, "%6d%54s%s\n", 1, "", "# OF CLK REF"); + + // Note clkRefStation can be a zero length string. + tracepdeex( + 0, + clockFile, + "%-4s %-20s%35s%s\n", + "", + clkRefStation.c_str(), + "", + "ANALYSIS CLK REF" + ); + tracepdeex( + 0, + clockFile, + "%6d %-50s%s\n", + numRecs, + acsConfig.reference_system, + "# OF SOLN STA / TRF" + ); + // MM This line causes the clock combination software to crash to removing + // tracepdeex(0,clockFile,"%-60s%s\n",acsConfig.rinex_comment, "COMMENT"); + + /* output receiver id and coordinates */ + + for (auto& clkEntry : clkEntryList) + { + if (clkEntry.isRec == false) + { + continue; + } + + string idStr = clkEntry.id.substr(0, 4); + string monuid = clkEntry.monid.substr(0, 20); + + tracepdeex(0, clockFile, "%-4s ", idStr.c_str()); + tracepdeex(0, clockFile, "%-20s", monuid.c_str()); + tracepdeex( + 0, + clockFile, + "%11.0f %11.0f %11.0f%s\n", + clkEntry.recPos(0) * 1000, + clkEntry.recPos(1) * 1000, + clkEntry.recPos(2) * 1000, + "SOLN STA NAME / NUM" + ); + } + + int num_sats = 0; + if (sysMap[E_Sys::GPS]) + num_sats += NSATGPS; + if (sysMap[E_Sys::GLO]) + num_sats += NSATGLO; + if (sysMap[E_Sys::GAL]) + num_sats += NSATGAL; + if (sysMap[E_Sys::BDS]) + num_sats += NSATBDS; + if (sysMap[E_Sys::QZS]) + num_sats += NSATQZS; + + /* output satellite PRN*/ + int k = 0; + tracepdeex(0, clockFile, "%6d%54s%s\n", num_sats, "", "# OF SOLN SATS"); + if (sysMap[E_Sys::GPS]) + for (int prn = 1; prn <= NSATGPS; prn++) + { + k++; + SatSys s(E_Sys::GPS, prn); + tracepdeex(0, clockFile, "%3s ", s.id().c_str()); + if (k % 15 == 0) + tracepdeex(0, clockFile, "%s\n", "PRN LIST"); + } + if (sysMap[E_Sys::GLO]) + for (int prn = 1; prn <= NSATGLO; prn++) + { + k++; + SatSys s(E_Sys::GLO, prn); + tracepdeex(0, clockFile, "%3s ", s.id().c_str()); + if (k % 15 == 0) + tracepdeex(0, clockFile, "%s\n", "PRN LIST"); + } + if (sysMap[E_Sys::GAL]) + for (int prn = 1; prn <= NSATGAL; prn++) + { + k++; + SatSys s(E_Sys::GAL, prn); + tracepdeex(0, clockFile, "%3s ", s.id().c_str()); + if (k % 15 == 0) + tracepdeex(0, clockFile, "%s\n", "PRN LIST"); + } + if (sysMap[E_Sys::BDS]) + for (int prn = 1; prn <= NSATBDS; prn++) + { + k++; + SatSys s(E_Sys::BDS, prn); + tracepdeex(0, clockFile, "%3s ", s.id().c_str()); + if (k % 15 == 0) + tracepdeex(0, clockFile, "%s\n", "PRN LIST"); + } + if (sysMap[E_Sys::QZS]) + for (int prn = 1; prn <= NSATQZS; prn++) + { + k++; + SatSys s(E_Sys::QZS, prn); + tracepdeex(0, clockFile, "%3s ", s.id().c_str()); + if (k % 15 == 0) + tracepdeex(0, clockFile, "%s\n", "PRN LIST"); + } + /*finish the line*/ while (k % 15 != 0) + { + k++; + tracepdeex(0, clockFile, "%3s ", ""); + if (k % 15 == 0) + tracepdeex(0, clockFile, "%s\n", "PRN LIST"); + } + + tracepdeex(0, clockFile, "%-60s%s\n", "", "END OF HEADER"); } - void outputClocksSet( - string filename, - vector clkDataRecSrcs, - vector clkDataSatSrcs, - const GTime& time, - map& outSys, - KFState& kfState, - ReceiverMap* receiverMap_ptr) + string filename, + vector clkDataRecSrcs, + vector clkDataSatSrcs, + const GTime& time, + map& outSys, + KFState& kfState, + ReceiverMap* receiverMap_ptr +) { - ClockList clkEntryList; - ClockEntry referenceRec; - referenceRec.isRec = false; - - switch (clkDataSatSrcs.front()) //todo aaron, remove this function - { - case +E_Source::NONE: break; - case +E_Source::KALMAN: getKalmanSatClks(clkEntryList, outSys, kfState); break; - case +E_Source::PRECISE: //fallthrough - case +E_Source::BROADCAST: //fallthrough - case +E_Source::SSR: getSatClksFromEph(clkEntryList, time, outSys, clkDataSatSrcs); break; - default: BOOST_LOG_TRIVIAL(error) << "Error: Unknown / Undefined clock data source."; return; - } - - switch (clkDataRecSrcs.front()) - { - case +E_Source::NONE: break; - case +E_Source::KALMAN: getKalmanRecClks(clkEntryList, referenceRec, kfState); break; - case +E_Source::PRECISE: getPreciseRecClks(clkEntryList, receiverMap_ptr, time); break; - case +E_Source::SSR: //fallthrough - case +E_Source::BROADCAST: //fallthrough - default: BOOST_LOG_TRIVIAL(error) << "Error: Printing receiver clocks for " << clkDataRecSrcs.front()._to_string() << " not implemented."; return; - } - - outputRinexClocksHeader (filename, clkEntryList, referenceRec, outSys, time); - outputRinexClocksBody (filename, clkEntryList, time); + ClockList clkEntryList; + ClockEntry referenceRec; + referenceRec.isRec = false; + + switch (clkDataSatSrcs.front()) // todo aaron, remove this function + { + case E_Source::NONE: + break; + case E_Source::KALMAN: + getKalmanSatClks(clkEntryList, outSys, kfState); + break; + case E_Source::PRECISE: // fallthrough + case E_Source::BROADCAST: // fallthrough + case E_Source::SSR: + getSatClksFromEph(clkEntryList, time, outSys, clkDataSatSrcs); + break; + default: + BOOST_LOG_TRIVIAL(error) << "Unknown / Undefined clock data source."; + return; + } + + switch (clkDataRecSrcs.front()) + { + case E_Source::NONE: + break; + case E_Source::KALMAN: + getKalmanRecClks(clkEntryList, referenceRec, kfState); + break; + case E_Source::PRECISE: + getPreciseRecClks(clkEntryList, receiverMap_ptr, time); + break; + case E_Source::SSR: // fallthrough + case E_Source::BROADCAST: // fallthrough + default: + BOOST_LOG_TRIVIAL(error) + << "Printing receiver clocks for " << enum_to_string(clkDataRecSrcs.front()) + << " not implemented."; + return; + } + + outputRinexClocksHeader(filename, clkEntryList, referenceRec, outSys, time); + outputRinexClocksBody(filename, clkEntryList, time); } -map> getSysOutputFilenames( - string filename, - GTime logtime, - bool replaceSys, - string id) +map> +getSysOutputFilenames(string filename, GTime logtime, bool replaceSys, string id) { - logtime = logtime.floorTime(acsConfig.rotate_period); + logtime = logtime.floorTime(acsConfig.rotate_period); - boost::posix_time::ptime logptime = boost::posix_time::from_time_t((time_t)((PTime)logtime).bigTime); + boost::posix_time::ptime logptime = + boost::posix_time::from_time_t((time_t)((PTime)logtime).bigTime); - if (logtime == GTime::noTime()) - { - logptime = boost::posix_time::not_a_date_time; - } + if (logtime == GTime::noTime()) + { + logptime = boost::posix_time::not_a_date_time; + } - replaceString(filename, "", id); - replaceTimes (filename, logptime); + replaceString(filename, "", id); + replaceTimes(filename, logptime); - map> fileOutputSysMap; + map> fileOutputSysMap; - if (replaceSys == false) - { - fileOutputSysMap[filename][E_Sys::NONE] = true; + if (replaceSys == false) + { + fileOutputSysMap[filename][E_Sys::NONE] = true; - return fileOutputSysMap; - } + return fileOutputSysMap; + } - for (auto& [sys, output] : acsConfig.process_sys) - { - if (output == false) - continue; + for (auto& [sys, output] : acsConfig.process_sys) + { + if (output == false) + continue; - SatSys t_Sat = SatSys(sys, 0); + SatSys t_Sat = SatSys(sys, 0); - string sysChar; - if (acsConfig.split_sys) sysChar = string(1, t_Sat.sysChar()); - else sysChar = "M"; + string sysChar; + if (acsConfig.split_sys) + sysChar = string(1, t_Sat.sysChar()); + else + sysChar = "M"; - if (sysChar == "-") - continue; + if (sysChar == "-") + continue; - string sysFilename = filename; - replaceString(sysFilename, "", sysChar); + string sysFilename = filename; + replaceString(sysFilename, "", sysChar); - fileOutputSysMap[sysFilename][sys] = true; - } + fileOutputSysMap[sysFilename][sys] = true; + } - return fileOutputSysMap; + return fileOutputSysMap; } void outputClocks( - string filename, - const GTime& time, - vector clkDataRecSrcs, - vector clkDataSatSrcs, - KFState& kfState, - ReceiverMap* receiverMap_ptr) + string filename, + const GTime& time, + KFState& kfState, + vector clkDataRecSrcs, + vector clkDataSatSrcs, + ReceiverMap* receiverMap_ptr +) { - auto filenameSysMap = getSysOutputFilenames(filename, time); - - for (auto [sysFilename, sysMap] : filenameSysMap) - { - outputClocksSet(sysFilename, clkDataRecSrcs, clkDataSatSrcs, time, sysMap, kfState, receiverMap_ptr); - } + auto filenameSysMap = getSysOutputFilenames(filename, time); + + for (auto [sysFilename, sysMap] : filenameSysMap) + { + outputClocksSet( + sysFilename, + clkDataRecSrcs, + clkDataSatSrcs, + time, + sysMap, + kfState, + receiverMap_ptr + ); + } } diff --git a/src/cpp/common/rinexClkWrite.hpp b/src/cpp/common/rinexClkWrite.hpp index 7c32d97c2..87610ce1c 100644 --- a/src/cpp/common/rinexClkWrite.hpp +++ b/src/cpp/common/rinexClkWrite.hpp @@ -1,24 +1,23 @@ - #pragma once -#include -#include #include +#include +#include +#include "common/enums.h" -using std::vector; -using std::string; using std::map; - -#include "enums.h" +using std::string; +using std::vector; struct GTime; +struct KFState; struct ReceiverMap; -class E_Source; void outputClocks( - string filename, - const GTime& time, - vector clkDataRecSrcs, - vector clkDataSatSrcs, - KFState& kfState, - ReceiverMap* receiverMap_ptr = nullptr); + string filename, + const GTime& time, + KFState& kfState, + vector clkDataRecSrcs, + vector clkDataSatSrcs, + ReceiverMap* receiverMap_ptr = nullptr +); diff --git a/src/cpp/common/rinexNavWrite.cpp b/src/cpp/common/rinexNavWrite.cpp index 1c7c45314..be3e0e5a7 100644 --- a/src/cpp/common/rinexNavWrite.cpp +++ b/src/cpp/common/rinexNavWrite.cpp @@ -1,940 +1,1173 @@ - // #pragma GCC optimize ("O0") -#include - +#include "common/rinexNavWrite.hpp" #include - -#include "rinexNavWrite.hpp" -#include "rinexObsWrite.hpp" -#include "rinexClkWrite.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "common.hpp" -#include "rinex.hpp" -#include "trace.hpp" - - +#include +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/navigation.hpp" +#include "common/rinex.hpp" +#include "common/rinexClkWrite.hpp" +#include "common/rinexObsWrite.hpp" +#include "common/trace.hpp" struct RinexNavFileOutput { - map sysMap; - map last_iode; ///< Used to track last ephemeris. + map sysMap; + map last_iode; ///< Used to track last ephemeris. }; map filenameNavFileDataMap; - -map utcIdStr = -{ - {E_UtcId::NONE, "" }, - {E_UtcId::UTC_USNO, "UTC(USNO)" }, - {E_UtcId::UTC_SU, "UTC(SU)" }, - {E_UtcId::UTCGAL, "UTCGAL" }, - {E_UtcId::UTC_NTSC, "UTC(NTSC)" }, - {E_UtcId::UTC_NICT, "UTC(NICT)" }, - {E_UtcId::UTC_NPLI, "UTC(NPLI)" }, - {E_UtcId::UTCIRN, "UTCIRN" }, - {E_UtcId::UTC_OP, "UTC(OP)" }, - {E_UtcId::UTC_NIST, "UTC(NIST)" }, +map utcIdStr = { + {E_UtcId::NONE, ""}, + {E_UtcId::UTC_USNO, "UTC(USNO)"}, + {E_UtcId::UTC_SU, "UTC(SU)"}, + {E_UtcId::UTCGAL, "UTCGAL"}, + {E_UtcId::UTC_NTSC, "UTC(NTSC)"}, + {E_UtcId::UTC_NICT, "UTC(NICT)"}, + {E_UtcId::UTC_NPLI, "UTC(NPLI)"}, + {E_UtcId::UTCIRN, "UTCIRN"}, + {E_UtcId::UTC_OP, "UTC(OP)"}, + {E_UtcId::UTC_NIST, "UTC(NIST)"}, }; -void outputNavRinexEph( - Eph& eph, - Trace& trace, - const double rnxver) +void outputNavRinexEph(Eph& eph, Trace& trace, const double rnxver) { - string formatStr = "% 15.12fE%+03d"; - - if (rnxver >= 4.0) - { - tracepdeex(0, trace, "> EPH %-3s %-4s", eph.Sat.id().c_str(), eph.type); - trace << "\n"; - } - - auto sys = eph.Sat.sys; - - E_TimeSys tsys = E_TimeSys::GPST; - switch (sys) - { - case E_Sys::GPS: tsys = E_TimeSys::GPST; break; - case E_Sys::GAL: tsys = E_TimeSys::GST; break; - case E_Sys::BDS: tsys = E_TimeSys::BDT; break; - case E_Sys::QZS: tsys = E_TimeSys::QZSST; break; - case E_Sys::SBS: tsys = E_TimeSys::GPST; break; - default: tsys = E_TimeSys::GPST; break; - } - - double ep[6] = {0}; - time2epoch(eph.toc, ep, tsys); - - tracepdeex(0, trace, "%-3s %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", eph.Sat.id().c_str(), ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - traceFormatedFloat(trace, eph.f0, formatStr); - traceFormatedFloat(trace, eph.f1, formatStr); - traceFormatedFloat(trace, eph.f2, formatStr); - trace << "\n"; - - trace << " "; - if (sys != +E_Sys::BDS) { traceFormatedFloat(trace, eph.iode, formatStr); } /* GPS/QZS: IODE, GAL: IODnav */ - else { traceFormatedFloat(trace, eph.aode, formatStr); } /* BDS: AODE */ - - traceFormatedFloat(trace, eph.crs, formatStr); - traceFormatedFloat(trace, eph.deln, formatStr); - traceFormatedFloat(trace, eph.M0, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, eph.cuc, formatStr); - traceFormatedFloat(trace, eph.e, formatStr); - traceFormatedFloat(trace, eph.cus, formatStr); - traceFormatedFloat(trace, sqrt(eph.A), formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, eph.toes, formatStr); - traceFormatedFloat(trace, eph.cic, formatStr); - traceFormatedFloat(trace, eph.OMG0, formatStr); - traceFormatedFloat(trace, eph.cis, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, eph.i0, formatStr); - traceFormatedFloat(trace, eph.crc, formatStr); - traceFormatedFloat(trace, eph.omg, formatStr); - traceFormatedFloat(trace, eph.OMGd, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, eph.idot, formatStr); - traceFormatedFloat(trace, eph.code, formatStr); - traceFormatedFloat(trace, eph.week, formatStr); /* GPS/QZS: GPS week, GAL: GAL week, BDS: BDT week */ - traceFormatedFloat(trace, eph.flag, formatStr); - trace << "\n"; - - trace << " "; - if (sys == +E_Sys::GAL) { traceFormatedFloat(trace, svaToSisa (eph.sva), formatStr); } - else { traceFormatedFloat(trace, svaToUra (eph.sva), formatStr); } - - traceFormatedFloat(trace, eph.svh, formatStr); - traceFormatedFloat(trace, eph.tgd[0], formatStr); /* GPS/QZS:TGD, GAL:BGD E5a/E1, BDS: TGD1 B1/B3 */ - - if ( sys == +E_Sys::GAL - || sys == +E_Sys::BDS) { traceFormatedFloat(trace, eph.tgd[1], formatStr); } /* GAL:BGD E5b/E1, BDS: TGD2 B2/B3 */ - else { traceFormatedFloat(trace, eph.iodc, formatStr); } /* GPS/QZS:IODC */ - trace << "\n"; - - trace << " "; - if (sys != +E_Sys::BDS) { traceFormatedFloat(trace, GTow(eph.ttm), formatStr); } - else { traceFormatedFloat(trace, BTow(eph.ttm), formatStr); } - - if (sys == +E_Sys::GPS) { traceFormatedFloat(trace, eph.fit, formatStr); } /* fit interval in hours for GPS */ - else if (sys == +E_Sys::QZS) { traceFormatedFloat(trace, eph.fitFlag, formatStr); } /* fit interval flag for QZS */ - else if (sys == +E_Sys::BDS) { traceFormatedFloat(trace, eph.aodc, formatStr); } /* BDS: AODC */ - else { traceFormatedFloat(trace, 0, formatStr); } /* spare */ - trace << "\n"; + string formatStr = "% 15.12fE%+03d"; + + if (rnxver >= 4.0) + { + tracepdeex(0, trace, "> EPH %-3s %-4s", eph.Sat.id().c_str(), eph.type); + trace << "\n"; + } + + auto sys = eph.Sat.sys; + + E_TimeSys tsys = E_TimeSys::GPST; + switch (sys) + { + case E_Sys::GPS: + tsys = E_TimeSys::GPST; + break; + case E_Sys::GAL: + tsys = E_TimeSys::GST; + break; + case E_Sys::BDS: + tsys = E_TimeSys::BDT; + break; + case E_Sys::QZS: + tsys = E_TimeSys::QZSST; + break; + case E_Sys::SBS: + tsys = E_TimeSys::GPST; + break; + default: + tsys = E_TimeSys::GPST; + break; + } + + double ep[6] = {0}; + time2epoch(eph.toc, ep, tsys); + + tracepdeex( + 0, + trace, + "%-3s %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", + eph.Sat.id().c_str(), + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + traceFormatedFloat(trace, eph.f0, formatStr); + traceFormatedFloat(trace, eph.f1, formatStr); + traceFormatedFloat(trace, eph.f2, formatStr); + trace << "\n"; + + trace << " "; + if (sys != E_Sys::BDS) + { + traceFormatedFloat(trace, eph.iode, formatStr); + } /* GPS/QZS: IODE, GAL: IODnav */ + else + { + traceFormatedFloat(trace, eph.aode, formatStr); + } /* BDS: AODE */ + + traceFormatedFloat(trace, eph.crs, formatStr); + traceFormatedFloat(trace, eph.deln, formatStr); + traceFormatedFloat(trace, eph.M0, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, eph.cuc, formatStr); + traceFormatedFloat(trace, eph.e, formatStr); + traceFormatedFloat(trace, eph.cus, formatStr); + traceFormatedFloat(trace, sqrt(eph.A), formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, eph.toes, formatStr); + traceFormatedFloat(trace, eph.cic, formatStr); + traceFormatedFloat(trace, eph.OMG0, formatStr); + traceFormatedFloat(trace, eph.cis, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, eph.i0, formatStr); + traceFormatedFloat(trace, eph.crc, formatStr); + traceFormatedFloat(trace, eph.omg, formatStr); + traceFormatedFloat(trace, eph.OMGd, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, eph.idot, formatStr); + traceFormatedFloat(trace, eph.code, formatStr); + traceFormatedFloat( + trace, + eph.week, + formatStr + ); /* GPS/QZS: GPS week, GAL: GAL week, BDS: BDT week */ + traceFormatedFloat(trace, eph.flag, formatStr); + trace << "\n"; + + trace << " "; + if (sys == E_Sys::GAL) + { + traceFormatedFloat(trace, svaToSisa(eph.sva), formatStr); + } + else + { + traceFormatedFloat(trace, svaToUra(eph.sva), formatStr); + } + + traceFormatedFloat(trace, eph.svh, formatStr); + traceFormatedFloat( + trace, + eph.tgd[0], + formatStr + ); /* GPS/QZS:TGD, GAL:BGD E5a/E1, BDS: TGD1 B1/B3 */ + + if (sys == E_Sys::GAL || sys == E_Sys::BDS) + { + traceFormatedFloat(trace, eph.tgd[1], formatStr); + } /* GAL:BGD E5b/E1, BDS: TGD2 B2/B3 */ + else + { + traceFormatedFloat(trace, eph.iodc, formatStr); + } /* GPS/QZS:IODC */ + trace << "\n"; + + trace << " "; + if (sys != E_Sys::BDS) + { + traceFormatedFloat(trace, GTow(eph.ttm), formatStr); + } + else + { + traceFormatedFloat(trace, BTow(eph.ttm), formatStr); + } + + if (sys == E_Sys::GPS) + { + traceFormatedFloat(trace, eph.fit, formatStr); + } /* fit interval in hours for GPS */ + else if (sys == E_Sys::QZS) + { + traceFormatedFloat(trace, eph.fitFlag, formatStr); + } /* fit interval flag for QZS */ + else if (sys == E_Sys::BDS) + { + traceFormatedFloat(trace, eph.aodc, formatStr); + } /* BDS: AODC */ + else + { + traceFormatedFloat(trace, 0, formatStr); + } /* spare */ + trace << "\n"; } - -void outputNavRinexGeph( - Geph& geph, - Trace& trace, - const double rnxver) +void outputNavRinexGeph(Geph& geph, Trace& trace, const double rnxver) { - string formatStr = "% 15.12fE%+03d"; - - if (rnxver >= 4.0) - { - tracepdeex(0, trace, "> EPH %-3s %-4s", geph.Sat.id().c_str(), geph.type); - trace << "\n"; - } - - if (geph.Sat.sys != +E_Sys::GLO) - { - BOOST_LOG_TRIVIAL(error) << "Error output Geph to RINEX incorrect system type"; - return; - } - - UtcTime utcTime; - GTime fakeGTime; - - utcTime = geph.tof; - fakeGTime.bigTime = utcTime.bigTime; - double tof = GTow(fakeGTime); - - double ep[6] = {0}; - time2epoch(geph.toe, ep, E_TimeSys::UTC); - - tracepdeex(0, trace, "%-3s %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", geph.Sat.id().c_str(), ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - traceFormatedFloat(trace,-geph.taun, formatStr); // -taun - traceFormatedFloat(trace, geph.gammaN, formatStr); - traceFormatedFloat(trace, tof, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, geph.pos[0] / 1E3, formatStr); - traceFormatedFloat(trace, geph.vel[0] / 1E3, formatStr); - traceFormatedFloat(trace, geph.acc[0] / 1E3, formatStr); - traceFormatedFloat(trace, geph.svh, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, geph.pos[1] / 1E3, formatStr); - traceFormatedFloat(trace, geph.vel[1] / 1E3, formatStr); - traceFormatedFloat(trace, geph.acc[1] / 1E3, formatStr); - traceFormatedFloat(trace, geph.frq, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, geph.pos[2] / 1E3, formatStr); - traceFormatedFloat(trace, geph.vel[2] / 1E3, formatStr); - traceFormatedFloat(trace, geph.acc[2] / 1E3, formatStr); - traceFormatedFloat(trace, geph.age, formatStr); - trace << "\n"; - - if (rnxver >= 3.05) - { - // todo Eugene: additional records from version 3.05 and on - } + string formatStr = "% 15.12fE%+03d"; + + if (rnxver >= 4.0) + { + tracepdeex(0, trace, "> EPH %-3s %-4s", geph.Sat.id().c_str(), geph.type); + trace << "\n"; + } + + if (geph.Sat.sys != E_Sys::GLO) + { + BOOST_LOG_TRIVIAL(error) << "Error output Geph to RINEX incorrect system type"; + return; + } + + UtcTime utcTime; + GTime fakeGTime; + + utcTime = geph.tof; + fakeGTime.bigTime = utcTime.bigTime; + double tof = GTow(fakeGTime); + + double ep[6] = {0}; + time2epoch(geph.toe, ep, E_TimeSys::UTC); + + tracepdeex( + 0, + trace, + "%-3s %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", + geph.Sat.id().c_str(), + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + traceFormatedFloat(trace, -geph.taun, formatStr); // -taun + traceFormatedFloat(trace, geph.gammaN, formatStr); + traceFormatedFloat(trace, tof, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, geph.pos[0] / 1E3, formatStr); + traceFormatedFloat(trace, geph.vel[0] / 1E3, formatStr); + traceFormatedFloat(trace, geph.acc[0] / 1E3, formatStr); + traceFormatedFloat(trace, geph.svh, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, geph.pos[1] / 1E3, formatStr); + traceFormatedFloat(trace, geph.vel[1] / 1E3, formatStr); + traceFormatedFloat(trace, geph.acc[1] / 1E3, formatStr); + traceFormatedFloat(trace, geph.frq, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, geph.pos[2] / 1E3, formatStr); + traceFormatedFloat(trace, geph.vel[2] / 1E3, formatStr); + traceFormatedFloat(trace, geph.acc[2] / 1E3, formatStr); + traceFormatedFloat(trace, geph.age, formatStr); + trace << "\n"; + + if (rnxver >= 3.05) + { + // todo Eugene: additional records from version 3.05 and on + } } - -void outputNavRinexCeph( - Ceph& ceph, - Trace& trace, - const double rnxver) +void outputNavRinexCeph(Ceph& ceph, Trace& trace, const double rnxver) { - string formatStr = "% 15.12fE%+03d"; - - if (rnxver < 4.0) - { - BOOST_LOG_TRIVIAL(error) << "Error output Ceph to RINEX incorrect RINEX version=" << rnxver; - return; - } - - tracepdeex(0, trace, "> EPH %-3s %-4s", ceph.Sat.id().c_str(), ceph.type); - trace << "\n"; - - auto sys = ceph.Sat.sys; - auto type = ceph.type; - - E_TimeSys tsys = E_TimeSys::GPST; - switch (sys) - { - case E_Sys::GPS: tsys = E_TimeSys::GPST; break; - case E_Sys::GAL: tsys = E_TimeSys::GST; break; - case E_Sys::BDS: tsys = E_TimeSys::BDT; break; - case E_Sys::QZS: tsys = E_TimeSys::QZSST; break; - case E_Sys::SBS: tsys = E_TimeSys::GPST; break; - default: tsys = E_TimeSys::GPST; break; - } - - double ep[6] = {0}; - time2epoch(ceph.toc, ep, tsys); - - tracepdeex(0, trace, "%-3s %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", ceph.Sat.id().c_str(), ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - traceFormatedFloat(trace, ceph.f0, formatStr); - traceFormatedFloat(trace, ceph.f1, formatStr); - traceFormatedFloat(trace, ceph.f2, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ceph.Adot, formatStr); - traceFormatedFloat(trace, ceph.crs, formatStr); - traceFormatedFloat(trace, ceph.deln, formatStr); - traceFormatedFloat(trace, ceph.M0, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ceph.cuc, formatStr); - traceFormatedFloat(trace, ceph.e, formatStr); - traceFormatedFloat(trace, ceph.cus, formatStr); - traceFormatedFloat(trace, sqrt(ceph.A), formatStr); - trace << "\n"; - - trace << " "; - if (sys != +E_Sys::BDS) { traceFormatedFloat(trace, ceph.tops, formatStr); } - else { traceFormatedFloat(trace, ceph.toes, formatStr); } - - traceFormatedFloat(trace, ceph.cic, formatStr); - traceFormatedFloat(trace, ceph.OMG0, formatStr); - traceFormatedFloat(trace, ceph.cis, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ceph.i0, formatStr); - traceFormatedFloat(trace, ceph.crc, formatStr); - traceFormatedFloat(trace, ceph.omg, formatStr); - traceFormatedFloat(trace, ceph.OMGd, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ceph.idot, formatStr); - traceFormatedFloat(trace, ceph.dn0d, formatStr); - - if (sys != +E_Sys::BDS) - { - traceFormatedFloat(trace, ceph.ura[0], formatStr); - traceFormatedFloat(trace, ceph.ura[1], formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ceph.ura[3], formatStr); - traceFormatedFloat(trace, ceph.svh, formatStr); - traceFormatedFloat(trace, ceph.tgd[0], formatStr); - traceFormatedFloat(trace, ceph.ura[2], formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ceph.isc[0], formatStr); - traceFormatedFloat(trace, ceph.isc[1], formatStr); - traceFormatedFloat(trace, ceph.isc[2], formatStr); - traceFormatedFloat(trace, ceph.isc[3], formatStr); - trace << "\n"; - - - if (type==+E_NavMsgType::CNAV) - { - trace << " "; - traceFormatedFloat(trace, GTow(ceph.ttm), formatStr); - traceFormatedFloat(trace, ceph.wnop, formatStr); - trace << " "; - trace << " "; - trace << "\n"; - } - else if (type==+E_NavMsgType::CNV2) - { - trace << " "; - traceFormatedFloat(trace, ceph.isc[4], formatStr); - traceFormatedFloat(trace, ceph.isc[5], formatStr); - trace << " "; - trace << " "; - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, GTow(ceph.ttm), formatStr); - traceFormatedFloat(trace, ceph.wnop, formatStr); - trace << " "; - trace << " "; - trace << "\n"; - } - } - else - { - traceFormatedFloat(trace, ceph.orb, formatStr); - traceFormatedFloat(trace, ceph.tops, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ceph.sis[0], formatStr); - traceFormatedFloat(trace, ceph.sis[1], formatStr); - traceFormatedFloat(trace, ceph.sis[2], formatStr); - traceFormatedFloat(trace, ceph.sis[3], formatStr); - trace << "\n"; - - if ( type==+E_NavMsgType::CNV1 - ||type==+E_NavMsgType::CNV2) - { - trace << " "; - if (type==+E_NavMsgType::CNV1) - { - traceFormatedFloat(trace, ceph.isc[0], formatStr); - trace << " "; - } - else if (type==+E_NavMsgType::CNV2) - { - trace << " "; - traceFormatedFloat(trace, ceph.isc[1], formatStr); - } - traceFormatedFloat(trace, ceph.tgd[0], formatStr); - traceFormatedFloat(trace, ceph.tgd[1], formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ceph.sis[4], formatStr); - traceFormatedFloat(trace, ceph.svh, formatStr); - traceFormatedFloat(trace, ceph.flag, formatStr); - traceFormatedFloat(trace, ceph.iodc, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, BTow(ceph.ttm), formatStr); - trace << " "; - trace << " "; - traceFormatedFloat(trace, ceph.iode, formatStr); - trace << "\n"; - } - else if (type==+E_NavMsgType::CNV3) - { - trace << " "; - traceFormatedFloat(trace, ceph.sis[4], formatStr); - traceFormatedFloat(trace, ceph.svh, formatStr); - traceFormatedFloat(trace, ceph.flag, formatStr); - traceFormatedFloat(trace, ceph.tgd[2], formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, BTow(ceph.ttm), formatStr); - trace << " "; - trace << " "; - trace << " "; - trace << "\n"; - } - } + string formatStr = "% 15.12fE%+03d"; + + if (rnxver < 4.0) + { + BOOST_LOG_TRIVIAL(error) << "Error output Ceph to RINEX incorrect RINEX version=" << rnxver; + return; + } + + tracepdeex(0, trace, "> EPH %-3s %-4s", ceph.Sat.id().c_str(), ceph.type); + trace << "\n"; + + auto sys = ceph.Sat.sys; + auto type = ceph.type; + + E_TimeSys tsys = E_TimeSys::GPST; + switch (sys) + { + case E_Sys::GPS: + tsys = E_TimeSys::GPST; + break; + case E_Sys::GAL: + tsys = E_TimeSys::GST; + break; + case E_Sys::BDS: + tsys = E_TimeSys::BDT; + break; + case E_Sys::QZS: + tsys = E_TimeSys::QZSST; + break; + case E_Sys::SBS: + tsys = E_TimeSys::GPST; + break; + default: + tsys = E_TimeSys::GPST; + break; + } + + double ep[6] = {0}; + time2epoch(ceph.toc, ep, tsys); + + tracepdeex( + 0, + trace, + "%-3s %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", + ceph.Sat.id().c_str(), + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + traceFormatedFloat(trace, ceph.f0, formatStr); + traceFormatedFloat(trace, ceph.f1, formatStr); + traceFormatedFloat(trace, ceph.f2, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ceph.Adot, formatStr); + traceFormatedFloat(trace, ceph.crs, formatStr); + traceFormatedFloat(trace, ceph.deln, formatStr); + traceFormatedFloat(trace, ceph.M0, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ceph.cuc, formatStr); + traceFormatedFloat(trace, ceph.e, formatStr); + traceFormatedFloat(trace, ceph.cus, formatStr); + traceFormatedFloat(trace, sqrt(ceph.A), formatStr); + trace << "\n"; + + trace << " "; + if (sys != E_Sys::BDS) + { + traceFormatedFloat(trace, ceph.tops, formatStr); + } + else + { + traceFormatedFloat(trace, ceph.toes, formatStr); + } + + traceFormatedFloat(trace, ceph.cic, formatStr); + traceFormatedFloat(trace, ceph.OMG0, formatStr); + traceFormatedFloat(trace, ceph.cis, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ceph.i0, formatStr); + traceFormatedFloat(trace, ceph.crc, formatStr); + traceFormatedFloat(trace, ceph.omg, formatStr); + traceFormatedFloat(trace, ceph.OMGd, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ceph.idot, formatStr); + traceFormatedFloat(trace, ceph.dn0d, formatStr); + + if (sys != E_Sys::BDS) + { + traceFormatedFloat(trace, ceph.ura[0], formatStr); + traceFormatedFloat(trace, ceph.ura[1], formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ceph.ura[3], formatStr); + traceFormatedFloat(trace, ceph.svh, formatStr); + traceFormatedFloat(trace, ceph.tgd[0], formatStr); + traceFormatedFloat(trace, ceph.ura[2], formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ceph.isc[0], formatStr); + traceFormatedFloat(trace, ceph.isc[1], formatStr); + traceFormatedFloat(trace, ceph.isc[2], formatStr); + traceFormatedFloat(trace, ceph.isc[3], formatStr); + trace << "\n"; + + if (type == E_NavMsgType::CNAV) + { + trace << " "; + traceFormatedFloat(trace, GTow(ceph.ttm), formatStr); + traceFormatedFloat(trace, ceph.wnop, formatStr); + trace << " "; + trace << " "; + trace << "\n"; + } + else if (type == E_NavMsgType::CNV2) + { + trace << " "; + traceFormatedFloat(trace, ceph.isc[4], formatStr); + traceFormatedFloat(trace, ceph.isc[5], formatStr); + trace << " "; + trace << " "; + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, GTow(ceph.ttm), formatStr); + traceFormatedFloat(trace, ceph.wnop, formatStr); + trace << " "; + trace << " "; + trace << "\n"; + } + } + else + { + traceFormatedFloat(trace, static_cast(ceph.orb), formatStr); + traceFormatedFloat(trace, ceph.tops, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ceph.sis[0], formatStr); + traceFormatedFloat(trace, ceph.sis[1], formatStr); + traceFormatedFloat(trace, ceph.sis[2], formatStr); + traceFormatedFloat(trace, ceph.sis[3], formatStr); + trace << "\n"; + + if (type == E_NavMsgType::CNV1 || type == E_NavMsgType::CNV2) + { + trace << " "; + if (type == E_NavMsgType::CNV1) + { + traceFormatedFloat(trace, ceph.isc[0], formatStr); + trace << " "; + } + else if (type == E_NavMsgType::CNV2) + { + trace << " "; + traceFormatedFloat(trace, ceph.isc[1], formatStr); + } + traceFormatedFloat(trace, ceph.tgd[0], formatStr); + traceFormatedFloat(trace, ceph.tgd[1], formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ceph.sis[4], formatStr); + traceFormatedFloat(trace, ceph.svh, formatStr); + traceFormatedFloat(trace, ceph.flag, formatStr); + traceFormatedFloat(trace, ceph.iodc, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, BTow(ceph.ttm), formatStr); + trace << " "; + trace << " "; + traceFormatedFloat(trace, ceph.iode, formatStr); + trace << "\n"; + } + else if (type == E_NavMsgType::CNV3) + { + trace << " "; + traceFormatedFloat(trace, ceph.sis[4], formatStr); + traceFormatedFloat(trace, ceph.svh, formatStr); + traceFormatedFloat(trace, ceph.flag, formatStr); + traceFormatedFloat(trace, ceph.tgd[2], formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, BTow(ceph.ttm), formatStr); + trace << " "; + trace << " "; + trace << " "; + trace << "\n"; + } + } } - -void outputNavRinexSTO( - STO& sto, - Trace& trace, - const double rnxver) +void outputNavRinexSTO(STO& sto, Trace& trace, const double rnxver) { - string formatStr = "% 15.12fE%+03d"; - - if (rnxver < 4.0) - { - BOOST_LOG_TRIVIAL(error) << "Error output STO to RINEX incorrect RINEX version=" << rnxver; - return; - } - - if (sto.Sat.prn > 0) tracepdeex(0, trace, "> STO %-3s %-4s", sto.Sat.id().c_str(), sto.type); - else tracepdeex(0, trace, "> STO %-3c %-4s", sto.Sat.sysChar(), sto.type); - trace << "\n"; - - auto sys = sto.Sat.sys; - - E_TimeSys tsys = E_TimeSys::GPST; - switch (sys) - { - case E_Sys::GPS: tsys = E_TimeSys::GPST; break; - case E_Sys::GLO: tsys = E_TimeSys::UTC; break; - case E_Sys::GAL: tsys = E_TimeSys::GST; break; - case E_Sys::BDS: tsys = E_TimeSys::BDT; break; - case E_Sys::QZS: tsys = E_TimeSys::QZSST; break; - case E_Sys::SBS: tsys = E_TimeSys::GPST; break; - default: tsys = E_TimeSys::GPST; break; - } - - double ep[6] = {0}; - time2epoch(sto.tot, ep, tsys); - - tracepdeex(0, trace, " %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - string sbasId; - if (sto.sid != +E_SbasId::NONE) - { - sbasId = sto.sid._to_string(); - std::replace(sbasId.begin(), sbasId.end(), '_', '-'); - } - tracepdeex(0, trace, " %-18s %-18s %-18s", sto.code._to_string(), sbasId.c_str(), utcIdStr[sto.uid]); - trace << "\n"; - - double ttm = 0; - int week = 0; - int weekRef = 0; - if (sys != +E_Sys::BDS) { ttm = GTow(sto.ttm); week = GWeek(sto.ttm); weekRef = GWeek(sto.tot); } - else { ttm = BTow(sto.ttm); week = BWeek(sto.ttm); weekRef = BWeek(sto.tot); } - - trace << " "; - traceFormatedFloat(trace, ttm + (week - weekRef) * 604800.0, formatStr); /* align ttm to tot week */ - traceFormatedFloat(trace, sto.A0, formatStr); - traceFormatedFloat(trace, sto.A1, formatStr); - traceFormatedFloat(trace, sto.A2, formatStr); - trace << "\n"; + string formatStr = "% 15.12fE%+03d"; + + if (rnxver < 4.0) + { + BOOST_LOG_TRIVIAL(error) << "Error output STO to RINEX incorrect RINEX version=" << rnxver; + return; + } + + if (sto.Sat.prn > 0) + tracepdeex(0, trace, "> STO %-3s %-4s", sto.Sat.id().c_str(), sto.type); + else + tracepdeex(0, trace, "> STO %-3c %-4s", sto.Sat.sysChar(), sto.type); + trace << "\n"; + + auto sys = sto.Sat.sys; + + E_TimeSys tsys = E_TimeSys::GPST; + switch (sys) + { + case E_Sys::GPS: + tsys = E_TimeSys::GPST; + break; + case E_Sys::GLO: + tsys = E_TimeSys::UTC; + break; + case E_Sys::GAL: + tsys = E_TimeSys::GST; + break; + case E_Sys::BDS: + tsys = E_TimeSys::BDT; + break; + case E_Sys::QZS: + tsys = E_TimeSys::QZSST; + break; + case E_Sys::SBS: + tsys = E_TimeSys::GPST; + break; + default: + tsys = E_TimeSys::GPST; + break; + } + + double ep[6] = {0}; + time2epoch(sto.tot, ep, tsys); + + tracepdeex( + 0, + trace, + " %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + string sbasId; + if (sto.sid != E_SbasId::NONE) + { + sbasId = enum_to_string(sto.sid); + std::replace(sbasId.begin(), sbasId.end(), '_', '-'); + } + tracepdeex( + 0, + trace, + " %-18s %-18s %-18s", + enum_to_string(sto.code).c_str(), + sbasId.c_str(), + utcIdStr[sto.uid] + ); + trace << "\n"; + + double ttm = 0; + int week = 0; + int weekRef = 0; + if (sys != E_Sys::BDS) + { + ttm = GTow(sto.ttm); + week = GWeek(sto.ttm); + weekRef = GWeek(sto.tot); + } + else + { + ttm = BTow(sto.ttm); + week = BWeek(sto.ttm); + weekRef = BWeek(sto.tot); + } + + trace << " "; + traceFormatedFloat( + trace, + ttm + (week - weekRef) * 604800.0, + formatStr + ); /* align ttm to tot week */ + traceFormatedFloat(trace, sto.A0, formatStr); + traceFormatedFloat(trace, sto.A1, formatStr); + traceFormatedFloat(trace, sto.A2, formatStr); + trace << "\n"; } - -void outputNavRinexEOP( - EOP& eop, - Trace& trace, - const double rnxver) +void outputNavRinexEOP(EOP& eop, Trace& trace, const double rnxver) { - string formatStr = "% 15.12fE%+03d"; - - if (rnxver < 4.0) - { - BOOST_LOG_TRIVIAL(error) << "Error output EOP to RINEX incorrect RINEX version=" << rnxver; - return; - } - - if (eop.Sat.prn > 0) tracepdeex(0, trace, "> EOP %-3s %-4s", eop.Sat.id().c_str(), eop.type); - else tracepdeex(0, trace, "> EOP %-3c %-4s", eop.Sat.sysChar(), eop.type); - trace << "\n"; - - auto sys = eop.Sat.sys; - - E_TimeSys tsys = E_TimeSys::GPST; - switch (sys) - { - case E_Sys::GPS: tsys = E_TimeSys::GPST; break; - case E_Sys::GLO: tsys = E_TimeSys::UTC; break; - case E_Sys::GAL: tsys = E_TimeSys::GST; break; - case E_Sys::BDS: tsys = E_TimeSys::BDT; break; - case E_Sys::QZS: tsys = E_TimeSys::QZSST; break; - case E_Sys::SBS: tsys = E_TimeSys::GPST; break; - default: tsys = E_TimeSys::GPST; break; - } - - double ep[6] = {0}; - time2epoch(eop.teop, ep, tsys); - - tracepdeex(0, trace, " %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - traceFormatedFloat(trace, eop.xp * R2AS, formatStr); - traceFormatedFloat(trace, eop.xpr * R2AS, formatStr); - traceFormatedFloat(trace, eop.xprr * R2AS, formatStr); - trace << "\n"; - - trace << " "; - trace << " "; - traceFormatedFloat(trace, eop.yp * R2AS, formatStr); - traceFormatedFloat(trace, eop.ypr * R2AS, formatStr); - traceFormatedFloat(trace, eop.yprr * R2AS, formatStr); - trace << "\n"; - - double ttm = 0; - int week = 0; - int weekRef = 0; - if (sys != +E_Sys::BDS) { ttm = GTow(eop.ttm); week = GWeek(eop.ttm); weekRef = GWeek(eop.teop); } - else { ttm = BTow(eop.ttm); week = BWeek(eop.ttm); weekRef = BWeek(eop.teop); } - - trace << " "; - traceFormatedFloat(trace, ttm + (week - weekRef) * 604800.0, formatStr); /* align ttm to teop week */ - traceFormatedFloat(trace, eop.dut1, formatStr); - traceFormatedFloat(trace, eop.dur, formatStr); - traceFormatedFloat(trace, eop.durr, formatStr); - trace << "\n"; + string formatStr = "% 15.12fE%+03d"; + + if (rnxver < 4.0) + { + BOOST_LOG_TRIVIAL(error) << "Error output EOP to RINEX incorrect RINEX version=" << rnxver; + return; + } + + if (eop.Sat.prn > 0) + tracepdeex(0, trace, "> EOP %-3s %-4s", eop.Sat.id().c_str(), eop.type); + else + tracepdeex(0, trace, "> EOP %-3c %-4s", eop.Sat.sysChar(), eop.type); + trace << "\n"; + + auto sys = eop.Sat.sys; + + E_TimeSys tsys = E_TimeSys::GPST; + switch (sys) + { + case E_Sys::GPS: + tsys = E_TimeSys::GPST; + break; + case E_Sys::GLO: + tsys = E_TimeSys::UTC; + break; + case E_Sys::GAL: + tsys = E_TimeSys::GST; + break; + case E_Sys::BDS: + tsys = E_TimeSys::BDT; + break; + case E_Sys::QZS: + tsys = E_TimeSys::QZSST; + break; + case E_Sys::SBS: + tsys = E_TimeSys::GPST; + break; + default: + tsys = E_TimeSys::GPST; + break; + } + + double ep[6] = {0}; + time2epoch(eop.teop, ep, tsys); + + tracepdeex( + 0, + trace, + " %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + traceFormatedFloat(trace, eop.xp * R2AS, formatStr); + traceFormatedFloat(trace, eop.xpr * R2AS, formatStr); + traceFormatedFloat(trace, eop.xprr * R2AS, formatStr); + trace << "\n"; + + trace << " "; + trace << " "; + traceFormatedFloat(trace, eop.yp * R2AS, formatStr); + traceFormatedFloat(trace, eop.ypr * R2AS, formatStr); + traceFormatedFloat(trace, eop.yprr * R2AS, formatStr); + trace << "\n"; + + double ttm = 0; + int week = 0; + int weekRef = 0; + if (sys != E_Sys::BDS) + { + ttm = GTow(eop.ttm); + week = GWeek(eop.ttm); + weekRef = GWeek(eop.teop); + } + else + { + ttm = BTow(eop.ttm); + week = BWeek(eop.ttm); + weekRef = BWeek(eop.teop); + } + + trace << " "; + traceFormatedFloat( + trace, + ttm + (week - weekRef) * 604800.0, + formatStr + ); /* align ttm to teop week */ + traceFormatedFloat(trace, eop.dut1, formatStr); + traceFormatedFloat(trace, eop.dur, formatStr); + traceFormatedFloat(trace, eop.durr, formatStr); + trace << "\n"; } - -void outputNavRinexION( - ION& ion, - Trace& trace, - const double rnxver) +void outputNavRinexION(ION& ion, Trace& trace, const double rnxver) { - string formatStr = "% 15.12fE%+03d"; - - if (rnxver < 4.0) - { - BOOST_LOG_TRIVIAL(error) << "Error output ION to RINEX incorrect RINEX version=" << rnxver; - return; - } - - if (ion.Sat.prn > 0) tracepdeex(0, trace, "> ION %-3s %-4s", ion.Sat.id().c_str(), ion.type); - else tracepdeex(0, trace, "> ION %-3c %-4s", ion.Sat.sysChar(), ion.type); - trace << "\n"; - - auto sys = ion.Sat.sys; - auto type = ion.type; - - E_TimeSys tsys = E_TimeSys::GPST; - switch (sys) - { - case E_Sys::GPS: tsys = E_TimeSys::GPST; break; - case E_Sys::GLO: tsys = E_TimeSys::UTC; break; - case E_Sys::GAL: tsys = E_TimeSys::GST; break; - case E_Sys::BDS: tsys = E_TimeSys::BDT; break; - case E_Sys::QZS: tsys = E_TimeSys::QZSST; break; - case E_Sys::SBS: tsys = E_TimeSys::GPST; break; - default: tsys = E_TimeSys::GPST; break; - } - - double ep[6] = {0}; - time2epoch(ion.ttm, ep, tsys); - - tracepdeex(0, trace, " %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - if (sys==+E_Sys::GAL) - { - traceFormatedFloat(trace, ion.ai0, formatStr); - traceFormatedFloat(trace, ion.ai1, formatStr); - traceFormatedFloat(trace, ion.ai2, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ion.flag, formatStr); - trace << " "; - trace << " "; - trace << " "; - trace << "\n"; - } - else if ( sys == +E_Sys::BDS - &&type == +E_NavMsgType::CNVX) - { - traceFormatedFloat(trace, ion.alpha1, formatStr); - traceFormatedFloat(trace, ion.alpha2, formatStr); - traceFormatedFloat(trace, ion.alpha3, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ion.alpha4, formatStr); - traceFormatedFloat(trace, ion.alpha5, formatStr); - traceFormatedFloat(trace, ion.alpha6, formatStr); - traceFormatedFloat(trace, ion.alpha7, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ion.alpha8, formatStr); - traceFormatedFloat(trace, ion.alpha9, formatStr); - trace << " "; - trace << " "; - trace << "\n"; - } - else if ( type==+E_NavMsgType::LNAV - ||type==+E_NavMsgType::D1D2 - ||type==+E_NavMsgType::CNVX) - { - traceFormatedFloat(trace, ion.a0, formatStr); - traceFormatedFloat(trace, ion.a1, formatStr); - traceFormatedFloat(trace, ion.a2, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ion.a3, formatStr); - traceFormatedFloat(trace, ion.b0, formatStr); - traceFormatedFloat(trace, ion.b1, formatStr); - traceFormatedFloat(trace, ion.b2, formatStr); - trace << "\n"; - - trace << " "; - traceFormatedFloat(trace, ion.b3, formatStr); - traceFormatedFloat(trace, ion.code, formatStr); - trace << " "; - trace << " "; - trace << "\n"; - } + string formatStr = "% 15.12fE%+03d"; + + if (rnxver < 4.0) + { + BOOST_LOG_TRIVIAL(error) << "Error output ION to RINEX incorrect RINEX version=" << rnxver; + return; + } + + if (ion.Sat.prn > 0) + tracepdeex(0, trace, "> ION %-3s %-4s", ion.Sat.id().c_str(), ion.type); + else + tracepdeex(0, trace, "> ION %-3c %-4s", ion.Sat.sysChar(), ion.type); + trace << "\n"; + + auto sys = ion.Sat.sys; + auto type = ion.type; + + E_TimeSys tsys = E_TimeSys::GPST; + switch (sys) + { + case E_Sys::GPS: + tsys = E_TimeSys::GPST; + break; + case E_Sys::GLO: + tsys = E_TimeSys::UTC; + break; + case E_Sys::GAL: + tsys = E_TimeSys::GST; + break; + case E_Sys::BDS: + tsys = E_TimeSys::BDT; + break; + case E_Sys::QZS: + tsys = E_TimeSys::QZSST; + break; + case E_Sys::SBS: + tsys = E_TimeSys::GPST; + break; + default: + tsys = E_TimeSys::GPST; + break; + } + + double ep[6] = {0}; + time2epoch(ion.ttm, ep, tsys); + + tracepdeex( + 0, + trace, + " %04.0f %02.0f %02.0f %02.0f %02.0f %02.0f", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + if (sys == E_Sys::GAL) + { + traceFormatedFloat(trace, ion.ai0, formatStr); + traceFormatedFloat(trace, ion.ai1, formatStr); + traceFormatedFloat(trace, ion.ai2, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ion.flag, formatStr); + trace << " "; + trace << " "; + trace << " "; + trace << "\n"; + } + else if (sys == E_Sys::BDS && type == E_NavMsgType::CNVX) + { + traceFormatedFloat(trace, ion.alpha1, formatStr); + traceFormatedFloat(trace, ion.alpha2, formatStr); + traceFormatedFloat(trace, ion.alpha3, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ion.alpha4, formatStr); + traceFormatedFloat(trace, ion.alpha5, formatStr); + traceFormatedFloat(trace, ion.alpha6, formatStr); + traceFormatedFloat(trace, ion.alpha7, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ion.alpha8, formatStr); + traceFormatedFloat(trace, ion.alpha9, formatStr); + trace << " "; + trace << " "; + trace << "\n"; + } + else if (type == E_NavMsgType::LNAV || type == E_NavMsgType::D1D2 || type == E_NavMsgType::CNVX) + { + traceFormatedFloat(trace, ion.a0, formatStr); + traceFormatedFloat(trace, ion.a1, formatStr); + traceFormatedFloat(trace, ion.a2, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ion.a3, formatStr); + traceFormatedFloat(trace, ion.b0, formatStr); + traceFormatedFloat(trace, ion.b1, formatStr); + traceFormatedFloat(trace, ion.b2, formatStr); + trace << "\n"; + + trace << " "; + traceFormatedFloat(trace, ion.b3, formatStr); + traceFormatedFloat(trace, ion.code, formatStr); + trace << " "; + trace << " "; + trace << "\n"; + } } - -void outputNavRinexBody( - RinexNavFileOutput& outFileData, - Trace& rinexStream, - const double rnxver) +void outputNavRinexBody(RinexNavFileOutput& outFileData, Trace& rinexStream, const double rnxver) { - for (auto& [sys, output] : outFileData.sysMap) - { - if (output == false) - continue; - - // EOP/ION messages - // STO messages ignored - if (rnxver >= 4) - { - E_NavMsgType type = defNavMsgType[sys]; - - auto* ion_ptr = seleph(std::cout, tsync, sys, type, nav); if (ion_ptr != nullptr) outputNavRinexION(*ion_ptr, rinexStream, rnxver); - auto* eop_ptr = seleph(std::cout, tsync, sys, type, nav); if (eop_ptr != nullptr) outputNavRinexEOP(*eop_ptr, rinexStream, rnxver); - } - - for (auto& [Sat, satNav] : nav.satNavMap) - { - if (Sat.sys != sys) - continue; - - E_NavMsgType nvtyp = acsConfig.used_nav_types[Sat.sys]; - - if (sys == +E_Sys::GLO) - { - auto geph_ptr = seleph(std::cout, tsync, Sat, nvtyp, ANY_IODE, nav); - - if (geph_ptr == nullptr) - continue; - - auto& geph = *geph_ptr; - - if ( outFileData.last_iode.find(Sat) == outFileData.last_iode.end() - ||geph.iode != outFileData.last_iode[Sat]) - { - outFileData.last_iode[Sat] = geph.iode; - outputNavRinexGeph(geph, rinexStream, rnxver); - } - - continue; - } - else if (sys == +E_Sys::SBS) - { - //optional to do (probably not useful): Seph writing - // Seph* seph_ptr = seleph(std::cout, tsync, sat, ANY_IODE, nav); - - // if (seph_ptr == nullptr) - // continue; - - // if ( outFileData.last_iode.find(sat) == outFileData.last_iode.end() - // ||seph_ptr->iode != outFileData.last_iode[sat]) - // { - // outFileData.last_iode[sat] = seph_ptr->iode; - // outputNavRinexSeph(*seph_ptr, rinexStream, rnxver); - // } - - continue; - } - else - { - auto eph_ptr = seleph(std::cout, tsync, Sat, nvtyp, ANY_IODE, nav); - - if (eph_ptr == nullptr) - continue; - - // Note iode can be zero, checking the map makes a zero entry as well. - if ( outFileData.last_iode.find(Sat) == outFileData.last_iode.end() - || eph_ptr->iode != outFileData.last_iode[Sat]) - { - outFileData.last_iode[Sat] = eph_ptr->iode; - outputNavRinexEph(*eph_ptr, rinexStream, rnxver); - } - - continue; - } - } - } + for (auto& [sys, output] : outFileData.sysMap) + { + if (output == false) + continue; + + // EOP/ION messages + // STO messages ignored + if (rnxver >= 4) + { + E_NavMsgType type = defNavMsgType[sys]; + + auto* ion_ptr = seleph(std::cout, tsync, sys, type, nav); + if (ion_ptr != nullptr) + outputNavRinexION(*ion_ptr, rinexStream, rnxver); + auto* eop_ptr = seleph(std::cout, tsync, sys, type, nav); + if (eop_ptr != nullptr) + outputNavRinexEOP(*eop_ptr, rinexStream, rnxver); + } + + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (Sat.sys != sys) + continue; + + E_NavMsgType nvtyp = acsConfig.used_nav_types[Sat.sys]; + + if (sys == E_Sys::GLO) + { + auto geph_ptr = seleph(std::cout, tsync, Sat, nvtyp, ANY_IODE, nav); + + if (geph_ptr == nullptr) + continue; + + auto& geph = *geph_ptr; + + if (outFileData.last_iode.find(Sat) == outFileData.last_iode.end() || + geph.iode != outFileData.last_iode[Sat]) + { + outFileData.last_iode[Sat] = geph.iode; + outputNavRinexGeph(geph, rinexStream, rnxver); + } + + continue; + } + else if (sys == E_Sys::SBS) + { + // optional to do (probably not useful): Seph writing + // Seph* seph_ptr = seleph(std::cout, tsync, sat, ANY_IODE, nav); + + // if (seph_ptr == nullptr) + // continue; + + // if ( outFileData.last_iode.find(sat) == outFileData.last_iode.end() + // ||seph_ptr->iode != outFileData.last_iode[sat]) + // { + // outFileData.last_iode[sat] = seph_ptr->iode; + // outputNavRinexSeph(*seph_ptr, rinexStream, rnxver); + // } + + continue; + } + else + { + auto eph_ptr = seleph(std::cout, tsync, Sat, nvtyp, ANY_IODE, nav); + + if (eph_ptr == nullptr) + continue; + + // Note iode can be zero, checking the map makes a zero entry as well. + if (outFileData.last_iode.find(Sat) == outFileData.last_iode.end() || + eph_ptr->iode != outFileData.last_iode[Sat]) + { + outFileData.last_iode[Sat] = eph_ptr->iode; + outputNavRinexEph(*eph_ptr, rinexStream, rnxver); + } + + continue; + } + } + } } - -void rinexNavHeader( - map& sysMap, - Trace& rinexStream, - const double rnxver) +void rinexNavHeader(map& sysMap, Trace& rinexStream, const double rnxver) { - string prog = "PEA v2"; - string runby = "Geoscience Australia"; - - UtcTime now = timeGet(); - - string timeDate = now.to_string(0); - boost::replace_all(timeDate, "-", ""); - boost::replace_all(timeDate, ":", ""); - timeDate += " UTC"; - - string sysDesc; - if (sysMap.size() == 1) sysDesc = rinexSysDesc(sysMap.begin()->first); - else sysDesc = rinexSysDesc(E_Sys::COMB); - - tracepdeex(0, rinexStream, "%9.2f%-11s%-20s%-20s%-20s\n", - rnxver, - "", - "N: GNSS NAV DATA", - sysDesc.c_str(), - "RINEX VERSION / TYPE"); - - tracepdeex(0, rinexStream, "%-20.20s%-20.20s%-20.20s%-20s\n", - prog.c_str(), - runby.c_str(), - timeDate.c_str(), - "PGM / RUN BY / DATE"); - - if (int(rnxver) == 3) - { - for (auto& [sys, output] : sysMap) - { - if (output == false) - { - continue; - } - - double* ion_arr1 = nullptr; - double* ion_arr2 = nullptr; - string ion_str1; - string ion_str2; - - E_NavMsgType type = defNavMsgType[sys]; - - auto& ionList = nav.ionMap[sys][type]; - - for (auto& [dummy, ion] : ionList) - { - switch (sys) - { - case E_Sys::GPS: - case E_Sys::QZS: - case E_Sys::BDS: ion_arr1 = &ion.vals[0]; ion_str1 = sys._to_string(); ion_str1 += 'A'; - ion_arr2 = &ion.vals[4]; ion_str2 = sys._to_string(); ion_str2 += 'B'; break; - case E_Sys::GAL: ion_arr1 = &ion.vals[0]; ion_str1 = sys._to_string(); ion_str1 += ' '; break; - default: - continue; - } - - if (ion_arr1) - tracepdeex(0, rinexStream, "%s %12.4E%12.4E%12.4E%12.4E%7s%-20s\n", - ion_str1.c_str(), - ion_arr1[0], - ion_arr1[1], - ion_arr1[2], - ion_arr1[3], "", "IONOSPHERIC CORR"); - - if (ion_arr2) - tracepdeex(0, rinexStream, "%s %12.4E%12.4E%12.4E%12.4E%7s%-20s\n", - ion_str2.c_str(), - ion_arr2[0], - ion_arr2[1], - ion_arr2[2], - ion_arr2[3], "", "IONOSPHERIC CORR"); - } - } - - for (auto& [sys, output] : sysMap) - { - if (output == false) - continue; - - E_StoCode code1 = E_StoCode::NONE; - E_StoCode code2 = E_StoCode::NONE; - E_NavMsgType type = defNavMsgType[sys]; - - switch (sys) - { - case E_Sys::GPS: code1 = E_StoCode::GPUT; break; - case E_Sys::GLO: code1 = E_StoCode::GLUT; code2 = E_StoCode::GLGP; break; - case E_Sys::GAL: code1 = E_StoCode::GAUT; code2 = E_StoCode::GAGP; break; - case E_Sys::QZS: code1 = E_StoCode::QZUT; code2 = E_StoCode::QZGP; break; - case E_Sys::BDS: code1 = E_StoCode::BDUT; break; - - default: - continue; - } - - for (auto& code : {code1, code2}) - { - auto& stoList = nav.stoMap[code][type]; - - for (auto& [dummy, sto] : stoList) - { - int week; - double tow; - if (sys != +E_Sys::BDS) { tow = GTow(sto.tot); week = GWeek(sto.tot); } - else { tow = BTow(sto.tot); week = BWeek(sto.tot); } - - tracepdeex(0, rinexStream, "%s %17.10E%16.9E%7.0f%5.0f %-5s %-2s %-20s\n", - code._to_string(), - sto.A0, - sto.A1, - tow, - week, "", "", "TIME SYSTEM CORR"); - } - } - } - } - - tracepdeex(0, rinexStream, "%6d%54s%-20s\n", nav.leaps, "", "LEAP SECONDS"); - tracepdeex(0, rinexStream, "%60s%-20s\n", "", "END OF HEADER"); + UtcTime now = timeGet(); + + string timeDate = now.to_string(0); + boost::replace_all(timeDate, "-", ""); + boost::replace_all(timeDate, ":", ""); + timeDate += " UTC"; + + string sysDesc; + if (sysMap.size() == 1) + sysDesc = rinexSysDesc(sysMap.begin()->first); + else + sysDesc = rinexSysDesc(E_Sys::COMB); + + tracepdeex( + 0, + rinexStream, + "%9.2f%-11s%-20s%-20s%-20s\n", + rnxver, + "", + "N: GNSS NAV DATA", + sysDesc.c_str(), + "RINEX VERSION / TYPE" + ); + + tracepdeex( + 0, + rinexStream, + "%-20.20s%-20.20s%-20.20s%-20s\n", + acsConfig.analysis_software.c_str(), + acsConfig.analysis_centre.c_str(), + timeDate.c_str(), + "PGM / RUN BY / DATE" + ); + + if (int(rnxver) == 3) + { + for (auto& [sys, output] : sysMap) + { + if (output == false) + { + continue; + } + + double* ion_arr1 = nullptr; + double* ion_arr2 = nullptr; + string ion_str1; + string ion_str2; + + E_NavMsgType type = defNavMsgType[sys]; + + auto& ionList = nav.ionMap[sys][type]; + + for (auto& [dummy, ion] : ionList) + { + switch (sys) + { + case E_Sys::GPS: + case E_Sys::QZS: + case E_Sys::BDS: + ion_arr1 = &ion.vals[0]; + ion_str1 = enum_to_string(sys); + ion_str1 += 'A'; + ion_arr2 = &ion.vals[4]; + ion_str2 = enum_to_string(sys); + ion_str2 += 'B'; + break; + case E_Sys::GAL: + ion_arr1 = &ion.vals[0]; + ion_str1 = enum_to_string(sys); + ion_str1 += ' '; + break; + default: + continue; + } + + if (ion_arr1) + tracepdeex( + 0, + rinexStream, + "%s %12.4E%12.4E%12.4E%12.4E%7s%-20s\n", + ion_str1.c_str(), + ion_arr1[0], + ion_arr1[1], + ion_arr1[2], + ion_arr1[3], + "", + "IONOSPHERIC CORR" + ); + + if (ion_arr2) + tracepdeex( + 0, + rinexStream, + "%s %12.4E%12.4E%12.4E%12.4E%7s%-20s\n", + ion_str2.c_str(), + ion_arr2[0], + ion_arr2[1], + ion_arr2[2], + ion_arr2[3], + "", + "IONOSPHERIC CORR" + ); + } + } + + for (auto& [sys, output] : sysMap) + { + if (output == false) + continue; + + E_StoCode code1 = E_StoCode::NONE; + E_StoCode code2 = E_StoCode::NONE; + E_NavMsgType type = defNavMsgType[sys]; + + switch (sys) + { + case E_Sys::GPS: + code1 = E_StoCode::GPUT; + break; + case E_Sys::GLO: + code1 = E_StoCode::GLUT; + code2 = E_StoCode::GLGP; + break; + case E_Sys::GAL: + code1 = E_StoCode::GAUT; + code2 = E_StoCode::GAGP; + break; + case E_Sys::QZS: + code1 = E_StoCode::QZUT; + code2 = E_StoCode::QZGP; + break; + case E_Sys::BDS: + code1 = E_StoCode::BDUT; + break; + + default: + continue; + } + + for (auto& code : {code1, code2}) + { + auto& stoList = nav.stoMap[code][type]; + + for (auto& [dummy, sto] : stoList) + { + int week; + double tow; + if (sys != E_Sys::BDS) + { + tow = GTow(sto.tot); + week = GWeek(sto.tot); + } + else + { + tow = BTow(sto.tot); + week = BWeek(sto.tot); + } + + tracepdeex( + 0, + rinexStream, + "%s %17.10E%16.9E%7.0f%5.0f %-5s %-2s %-20s\n", + enum_to_string(code), + sto.A0, + sto.A1, + tow, + week, + "", + "", + "TIME SYSTEM CORR" + ); + } + } + } + } + + tracepdeex(0, rinexStream, "%6d%54s%-20s\n", nav.leaps, "", "LEAP SECONDS"); + tracepdeex(0, rinexStream, "%60s%-20s\n", "", "END OF HEADER"); } void writeRinexNav(const double rnxver) { - auto filenameSysMap = getSysOutputFilenames(acsConfig.rinex_nav_filename, tsync); + auto filenameSysMap = getSysOutputFilenames(acsConfig.rinex_nav_filename, tsync); - for (auto [filename, sysMap] : filenameSysMap) - { - auto& fileData = filenameNavFileDataMap[filename]; + for (auto [filename, sysMap] : filenameSysMap) + { + auto& fileData = filenameNavFileDataMap[filename]; - std::ofstream rinexStream(filename, std::ofstream::app); - if (!rinexStream) - { - BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for writing rinex nav"; - return; - } + std::ofstream rinexStream(filename, std::ofstream::app); + if (!rinexStream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for writing rinex nav"; + return; + } - if (rinexStream.tellp() == 0) - rinexNavHeader(sysMap, rinexStream, rnxver); + if (rinexStream.tellp() == 0) + rinexNavHeader(sysMap, rinexStream, rnxver); - fileData.sysMap = sysMap; + fileData.sysMap = sysMap; - outputNavRinexBody(fileData, rinexStream, rnxver); - } + outputNavRinexBody(fileData, rinexStream, rnxver); + } } -void outputNavRinexBodyAll( - Trace& rinexStream, - const double rnxver) +void outputNavRinexBodyAll(Trace& rinexStream, const double rnxver) { - if (rnxver >= 4) - { - for (auto& [code, stoCodeMap] : nav.stoMap) - for (auto& [type, stoList] : stoCodeMap) - for (auto it = stoList.rbegin(); it != stoList.rend(); it++) - { - auto& [key, value] = *it; - - outputNavRinexSTO(value, rinexStream, rnxver); - } - - for (auto& [sys, eopSysMap] : nav.eopMap) - for (auto& [type, eopList] : eopSysMap) - for (auto it = eopList.rbegin(); it != eopList.rend(); it++) - { - auto& [key, value] = *it; - - outputNavRinexEOP(value, rinexStream, rnxver); - } - - for (auto& [sys, ionSysMap] : nav.ionMap) - for (auto& [type, ionList] : ionSysMap) - for (auto it = ionList.rbegin(); it != ionList.rend(); it++) - { - auto& [key, value] = *it; - - outputNavRinexION(value, rinexStream, rnxver); - } - } - - for (auto& [satId, navList] : nav.ephMap) - for (auto& [nvtyp, ephList] : navList) - for (auto it = ephList.rbegin(); it != ephList.rend(); it++) - { - auto& [key, value] = *it; - - outputNavRinexEph(value, rinexStream, rnxver); - } - - for (auto& [satId, navList] : nav.gephMap) - for (auto& [nvtyp, gephList] : navList) - for (auto it = gephList.rbegin(); it != gephList.rend(); it++) - { - auto& [key, value] = *it; - - outputNavRinexGeph(value, rinexStream, rnxver); - } - - // for (auto& [satId, sephList] : nav.sephMap) - // for (auto it = sephList.rbegin(); it != sephList.rend(); it++) - // { -// auto& [key, value] = *it; - - // outputNavRinexSeph(ritSeph->second, rinexStream, rnxver); - // } - - for (auto& [satId, cephSatMap] : nav.cephMap) - for (auto& [type, cephList] : cephSatMap) - for (auto it = cephList.rbegin(); it != cephList.rend(); it++) - { - auto& [key, value] = *it; - - outputNavRinexCeph(value, rinexStream, rnxver); - } + if (rnxver >= 4) + { + for (auto& [code, stoCodeMap] : nav.stoMap) + for (auto& [type, stoList] : stoCodeMap) + for (auto it = stoList.rbegin(); it != stoList.rend(); it++) + { + auto& [key, value] = *it; + + outputNavRinexSTO(value, rinexStream, rnxver); + } + + for (auto& [sys, eopSysMap] : nav.eopMap) + for (auto& [type, eopList] : eopSysMap) + for (auto it = eopList.rbegin(); it != eopList.rend(); it++) + { + auto& [key, value] = *it; + + outputNavRinexEOP(value, rinexStream, rnxver); + } + + for (auto& [sys, ionSysMap] : nav.ionMap) + for (auto& [type, ionList] : ionSysMap) + for (auto it = ionList.rbegin(); it != ionList.rend(); it++) + { + auto& [key, value] = *it; + + outputNavRinexION(value, rinexStream, rnxver); + } + } + + for (auto& [satId, navList] : nav.ephMap) + for (auto& [nvtyp, ephList] : navList) + for (auto it = ephList.rbegin(); it != ephList.rend(); it++) + { + auto& [key, value] = *it; + + outputNavRinexEph(value, rinexStream, rnxver); + } + + for (auto& [satId, navList] : nav.gephMap) + for (auto& [nvtyp, gephList] : navList) + for (auto it = gephList.rbegin(); it != gephList.rend(); it++) + { + auto& [key, value] = *it; + + outputNavRinexGeph(value, rinexStream, rnxver); + } + + // for (auto& [satId, sephList] : nav.sephMap) + // for (auto it = sephList.rbegin(); it != sephList.rend(); it++) + // { + // auto& [key, value] = *it; + + // outputNavRinexSeph(ritSeph->second, rinexStream, rnxver); + // } + + for (auto& [satId, cephSatMap] : nav.cephMap) + for (auto& [type, cephList] : cephSatMap) + for (auto it = cephList.rbegin(); it != cephList.rend(); it++) + { + auto& [key, value] = *it; + + outputNavRinexCeph(value, rinexStream, rnxver); + } } void writeRinexNavAll(string filename, const double rnxver) { - map sysMap = - { - {E_Sys::GPS, true}, - {E_Sys::GLO, true}, - {E_Sys::GAL, true}, - {E_Sys::BDS, true}, - {E_Sys::QZS, true}, - {E_Sys::SBS, true} - }; - - std::ofstream rinexStream(filename, std::ofstream::app); - if (!rinexStream) - { - BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for writing rinex nav"; - return; - } - - if (rinexStream.tellp() == 0) - rinexNavHeader(sysMap, rinexStream, rnxver); - - - outputNavRinexBodyAll(rinexStream, rnxver); + map sysMap = { + {E_Sys::GPS, true}, + {E_Sys::GLO, true}, + {E_Sys::GAL, true}, + {E_Sys::BDS, true}, + {E_Sys::QZS, true}, + {E_Sys::SBS, true} + }; + + std::ofstream rinexStream(filename, std::ofstream::app); + if (!rinexStream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening " << filename << " for writing rinex nav"; + return; + } + + if (rinexStream.tellp() == 0) + rinexNavHeader(sysMap, rinexStream, rnxver); + + outputNavRinexBodyAll(rinexStream, rnxver); } diff --git a/src/cpp/common/rinexNavWrite.hpp b/src/cpp/common/rinexNavWrite.hpp index d62254f03..de2ecbdef 100644 --- a/src/cpp/common/rinexNavWrite.hpp +++ b/src/cpp/common/rinexNavWrite.hpp @@ -1,6 +1,2 @@ - #pragma once - - -void writeRinexNav( - const double rnxver = 3.05); +void writeRinexNav(const double rnxver = 3.05); \ No newline at end of file diff --git a/src/cpp/common/rinexObsWrite.cpp b/src/cpp/common/rinexObsWrite.cpp index d4af8b075..a1ac472b0 100644 --- a/src/cpp/common/rinexObsWrite.cpp +++ b/src/cpp/common/rinexObsWrite.cpp @@ -1,471 +1,544 @@ +#include "common/rinexObsWrite.hpp" #include +#include +#include +#include #include -#include -#include #include +#include +#include +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/rinexClkWrite.hpp" +#include "common/sinex.hpp" +#include "common/trace.hpp" -// #pragma GCC optimize ("O0") - -using std::vector; using std::pair; +using std::vector; -#include -#include -#include - -#include "rinexObsWrite.hpp" -#include "rinexClkWrite.hpp" -#include "observations.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "common.hpp" -#include "sinex.hpp" -#include "trace.hpp" +// #pragma GCC optimize ("O0") struct RinexOutput { - string sysDesc; - long headerObsPos = 0; - long headerTimePos = 0; + string sysDesc; + long headerObsPos = 0; + long headerTimePos = 0; - map>> codesPerSys; + map>> codesPerSys; }; map filenameObsFileDataMap; - -string rinexSysDesc( - E_Sys sys) +string rinexSysDesc(E_Sys sys) { - switch (sys) - { - case E_Sys::COMB: //fallthrough - case E_Sys::NONE: return "M: Mixed"; - case E_Sys::GPS: return "G: GPS"; - case E_Sys::GLO: return "R: GLONASS"; - case E_Sys::GAL: return "E: Galileo"; - case E_Sys::QZS: return "J: QZSS"; - case E_Sys::BDS: return "C: BDS"; - //case E_Sys::LEO: return 'L'; - case E_Sys::IRN: return "I: IRNSS"; - case E_Sys::SBS: return "S: SBAS payload"; - default: - BOOST_LOG_TRIVIAL(error) << "Error writing RINEX Navigation header unsupported system type."; - - } - return "M: Mixed"; + switch (sys) + { + case E_Sys::COMB: // fallthrough + case E_Sys::NONE: + return "M: Mixed"; + case E_Sys::GPS: + return "G: GPS"; + case E_Sys::GLO: + return "R: GLONASS"; + case E_Sys::GAL: + return "E: Galileo"; + case E_Sys::QZS: + return "J: QZSS"; + case E_Sys::BDS: + return "C: BDS"; + // case E_Sys::LEO: return 'L'; + case E_Sys::IRN: + return "I: IRNSS"; + case E_Sys::SBS: + return "S: SBAS payload"; + default: + BOOST_LOG_TRIVIAL(error) + << "Error writing RINEX Navigation header unsupported system type."; + } + return "M: Mixed"; } -void updateRinexObsHeader( - RinexOutput& rinexOutput, - std::fstream& rinexStream, - GTime& firstObsTime) +void updateRinexObsHeader(RinexOutput& rinexOutput, std::fstream& rinexStream, GTime& firstObsTime) { - rinexStream.seekp(rinexOutput.headerObsPos); - const char label[] = "SYS / # / OBS TYPES"; - - int numSysLines = 0; - for (auto& [sys, obsCodeDesc] : rinexOutput.codesPerSys) - { - auto dummySat = SatSys(sys, 0); - char sys_c = dummySat.sysChar(); - - if (sys_c == '-') - { - BOOST_LOG_TRIVIAL(error) << "Error: Writing RINEX file undefined system."; - return; - } - - tracepdeex(0, rinexStream, "%c %3d", sys_c, obsCodeDesc.size()); - - int obsCodeCnt = 0; - for (auto& [obsCode, obsDesc] : obsCodeDesc) - { - obsCodeCnt++; - auto obsDescStr = obsDesc._to_string(); - auto obsCodeStr = obsCode._to_string(); - char obsStr[4]; - obsStr[0] = obsDescStr[0]; - obsStr[1] = obsCodeStr[1]; - obsStr[2] = obsCodeStr[2]; - obsStr[3] = 0; - - if ( obsCodeCnt % 13 == 1 - &&obsCodeCnt != 1) - { - tracepdeex(0, rinexStream, " "); - } - - tracepdeex(0, rinexStream, " %3s", obsStr); - - if (obsCodeCnt % 13 == 0) - { - // After 13 observations make a new line. - tracepdeex(0, rinexStream, " %-20s\n", label); - numSysLines++; - } - } - - if (obsCodeCnt % 13 != 0) - { - // less than 13 entries and a new line is required. - while (obsCodeCnt % 13 != 0) - { - obsCodeCnt++; - tracepdeex(0, rinexStream, " %3s", ""); - } - - tracepdeex(0, rinexStream, " %-20s\n", label); - numSysLines++; - } - } - - while (numSysLines < 2 * E_Sys::_size()) - { - //add some lines to be filled in later to allow for the maximum number expected - tracepdeex(0, rinexStream, "%-60.60s%-20s\n", "", "COMMENT"); - numSysLines++; - } - - if (rinexOutput.headerTimePos == 0) - { - string timeSysStr = "GPS"; // PEA internal time is GPS. - - GEpoch ep = firstObsTime; - - tracepdeex(0, rinexStream, "%10.3f%50s%-20s\n", - acsConfig.epoch_interval, - "", - "INTERVAL"); - - tracepdeex(0, rinexStream, " %04.0f%6.0f%6.0f%6.0f%6.0f%13.7f %-12s%-20s\n", - ep[0], - ep[1], - ep[2], - ep[3], - ep[4], - ep[5], - timeSysStr, - "TIME OF FIRST OBS"); - - rinexOutput.headerTimePos = rinexStream.tellp(); - - //output dummy entry to be overwritten - tracepdeex(0, rinexStream, " %04.0f%6.0f%6.0f%6.0f%6.0f%13.7f %-12s%-20s\n", - ep[0], - ep[1], - ep[2], - ep[3], - ep[4], - ep[5], - timeSysStr, - "TIME OF LAST OBS"); - } + rinexStream.seekp(rinexOutput.headerObsPos); + const char label[] = "SYS / # / OBS TYPES"; + + int numSysLines = 0; + for (auto& [sys, obsCodeDesc] : rinexOutput.codesPerSys) + { + auto dummySat = SatSys(sys, 0); + char sys_c = dummySat.sysChar(); + + if (sys_c == '-') + { + BOOST_LOG_TRIVIAL(error) << "Writing RINEX file undefined system."; + return; + } + + tracepdeex(0, rinexStream, "%c %3d", sys_c, obsCodeDesc.size()); + + int obsCodeCnt = 0; + for (auto& [obsCode, obsDesc] : obsCodeDesc) + { + obsCodeCnt++; + auto obsDescStr = enum_to_string(obsDesc); + auto obsCodeStr = enum_to_string(obsCode); + char obsStr[4]; + obsStr[0] = obsDescStr[0]; + obsStr[1] = obsCodeStr[1]; + obsStr[2] = obsCodeStr[2]; + obsStr[3] = 0; + + if (obsCodeCnt % 13 == 1 && obsCodeCnt != 1) + { + tracepdeex(0, rinexStream, " "); + } + + tracepdeex(0, rinexStream, " %3s", obsStr); + + if (obsCodeCnt % 13 == 0) + { + // After 13 observations make a new line. + tracepdeex(0, rinexStream, " %-20s\n", label); + numSysLines++; + } + } + + if (obsCodeCnt % 13 != 0) + { + // less than 13 entries and a new line is required. + while (obsCodeCnt % 13 != 0) + { + obsCodeCnt++; + tracepdeex(0, rinexStream, " %3s", ""); + } + + tracepdeex(0, rinexStream, " %-20s\n", label); + numSysLines++; + } + } + + while (numSysLines < 2 * enum_count()) + { + // add some lines to be filled in later to allow for the maximum number expected + tracepdeex(0, rinexStream, "%-60.60s%-20s\n", "", "COMMENT"); + numSysLines++; + } + + if (rinexOutput.headerTimePos == 0) + { + string timeSysStr = "GPS"; // PEA internal time is GPS. + + GEpoch ep = firstObsTime; + + tracepdeex(0, rinexStream, "%10.3f%50s%-20s\n", acsConfig.epoch_interval, "", "INTERVAL"); + + tracepdeex( + 0, + rinexStream, + " %04.0f%6.0f%6.0f%6.0f%6.0f%13.7f %-12s%-20s\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5], + timeSysStr, + "TIME OF FIRST OBS" + ); + + rinexOutput.headerTimePos = rinexStream.tellp(); + + // output dummy entry to be overwritten + tracepdeex( + 0, + rinexStream, + " %04.0f%6.0f%6.0f%6.0f%6.0f%13.7f %-12s%-20s\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5], + timeSysStr, + "TIME OF LAST OBS" + ); + } } void writeRinexObsHeader( - RinexOutput& fileData, - SinexRecData& snx, - std::fstream& rinexStream, - GTime& firstObsTime, - const double rnxver) + RinexOutput& fileData, + Receiver& rec, + std::fstream& rinexStream, + GTime& firstObsTime, + const double rnxver +) { - fileData.headerTimePos = 0; - - // Write the RINEX header. - UtcTime now = timeGet(); - - string timeDate = now.to_string(0); - boost::replace_all(timeDate, "-", ""); - boost::replace_all(timeDate, ":", ""); - timeDate += " UTC"; - - string prog = "PEA v2"; - string runby = "Geoscience Australia"; - - tracepdeex(0, rinexStream, "%9.2f%-11s%-20s%-20s%-20s\n", - rnxver, - "", - "OBSERVATION DATA", - fileData.sysDesc, - "RINEX VERSION / TYPE"); - - tracepdeex(0, rinexStream, "%-20.20s%-20.20s%-20.20s%-20s\n", - prog, - runby, - timeDate.c_str(), - "PGM / RUN BY / DATE"); - - tracepdeex(0, rinexStream, "%-60.60s%-20s\n", - snx.id_ptr->sitecode, - "MARKER NAME"); - - tracepdeex(0, rinexStream, "%-20.20s%-40.40s%-20s\n", - snx.id_ptr->domes, - "", - "MARKER NUMBER"); - - //TODO Add marker type as RINEX version is greater than 2.99 - //tracepdeex(0,rinexStream,"%-20.20s%-40.40s%-20s\n",rinexOutput.snx.,"","MARKER TYPE"); - - tracepdeex(0, rinexStream, "%-20.20s%-40.40s%-20s\n", - "", - acsConfig.analysis_centre, - "OBSERVER / AGENCY"); - - tracepdeex(0, rinexStream, "%-20.20s%-20.20s%-20.20s%-20s\n", - snx.rec_ptr->sn, - snx.rec_ptr->type, - snx.rec_ptr->firm, - "REC # / TYPE / VERS"); - - tracepdeex(0, rinexStream, "%-20.20s%-20.20s%-20.20s%-20s\n", - snx.ant_ptr->sn, - snx.ant_ptr->type, - "", - "ANT # / TYPE"); - - tracepdeex(0, rinexStream, "%14.4f%14.4f%14.4f%-18s%-20s\n", - snx.pos.x(), - snx.pos.y(), - snx.pos.z(), - "", - "APPROX POSITION XYZ"); - - tracepdeex(0, rinexStream, "%14.4f%14.4f%14.4f%-18s%-20s\n", - snx.ecc_ptr->ecc[2], - snx.ecc_ptr->ecc[1], - snx.ecc_ptr->ecc[0], - "", - "ANTENNA: DELTA H/E/N"); - - fileData.headerObsPos = rinexStream.tellp(); - - updateRinexObsHeader(fileData, rinexStream, firstObsTime); - - tracepdeex(0, rinexStream, "%-60.60s%-20s\n", - "", - "END OF HEADER"); + fileData.headerTimePos = 0; + + // Write the RINEX header. + UtcTime now = timeGet(); + + string timeDate = now.to_string(0); + boost::replace_all(timeDate, "-", ""); + boost::replace_all(timeDate, ":", ""); + timeDate += " UTC"; + + auto& snx = rec.snx; + + tracepdeex( + 0, + rinexStream, + "%9.2f%-11s%-20s%-20s%-20s\n", + rnxver, + "", + "OBSERVATION DATA", + fileData.sysDesc, + "RINEX VERSION / TYPE" + ); + + tracepdeex( + 0, + rinexStream, + "%-20.20s%-20.20s%-20.20s%-20s\n", + acsConfig.analysis_software.c_str(), + acsConfig.analysis_centre.c_str(), + timeDate.c_str(), + "PGM / RUN BY / DATE" + ); + + tracepdeex(0, rinexStream, "%-60.60s%-20s\n", rec.id.c_str(), "MARKER NAME"); + + tracepdeex(0, rinexStream, "%-20.20s%-40.40s%-20s\n", snx.id_ptr->domes, "", "MARKER NUMBER"); + + // TODO Add marker type as RINEX version is greater than 2.99 + // tracepdeex(0,rinexStream,"%-20.20s%-40.40s%-20s\n",rinexOutput.snx.,"","MARKER TYPE"); + + tracepdeex( + 0, + rinexStream, + "%-20.20s%-40.40s%-20s\n", + "", + acsConfig.analysis_centre, + "OBSERVER / AGENCY" + ); + + tracepdeex( + 0, + rinexStream, + "%-20.20s%-20.20s%-20.20s%-20s\n", + snx.rec_ptr->sn, + rec.receiverType.c_str(), + snx.rec_ptr->firm, + "REC # / TYPE / VERS" + ); + + tracepdeex( + 0, + rinexStream, + "%-20.20s%-20.20s%-20.20s%-20s\n", + snx.ant_ptr->sn, + rec.antennaType.c_str(), + "", + "ANT # / TYPE" + ); + + tracepdeex( + 0, + rinexStream, + "%14.4f%14.4f%14.4f%-18s%-20s\n", + rec.aprioriPos.x(), + rec.aprioriPos.y(), + rec.aprioriPos.z(), + "", + "APPROX POSITION XYZ" + ); + + tracepdeex( + 0, + rinexStream, + "%14.4f%14.4f%14.4f%-18s%-20s\n", + rec.antDelta[2], + rec.antDelta[0], + rec.antDelta[1], + "", + "ANTENNA: DELTA H/E/N" + ); + + fileData.headerObsPos = rinexStream.tellp(); + + updateRinexObsHeader(fileData, rinexStream, firstObsTime); + + tracepdeex(0, rinexStream, "%-60.60s%-20s\n", "", "END OF HEADER"); } void writeRinexObsBody( - RinexOutput& fileData, - std::fstream& rinexStream, - ObsList& obsList, - GTime& time, - map& sysMap) + RinexOutput& fileData, + std::fstream& rinexStream, + ObsList& obsList, + GTime& time, + map& sysMap +) { - GEpoch ep = time; - string timeSysStr = "GPS"; // PEA internal time is GPS. - - rinexStream.seekp(fileData.headerTimePos); - tracepdeex(0, rinexStream, " %04.0f%6.0f%6.0f%6.0f%6.0f%13.7f %-12s%-20s\n", - ep[0], - ep[1], - ep[2], - ep[3], - ep[4], - ep[5], - timeSysStr, - "TIME OF LAST OBS"); - - // Write the RINEX body. - rinexStream.seekp(0, std::ios::end); - - int count = 0; - for (auto& obs : only(obsList)) - { - if (sysMap[obs.Sat.sys] == false) - { - continue; - } - - count++; - } - - // flag epoch flag (0:ok,1:power failure,>1:event flag) - int flag = 0; - tracepdeex(0, rinexStream, "> %04.0f %02.0f %02.0f %02.0f %02.0f%11.7f %d%3d%21s\n", - ep[0], - ep[1], - ep[2], - ep[3], - ep[4], - ep[5], - flag, - count, - ""); - - for (auto& obs : only(obsList)) - { - if (sysMap[obs.Sat.sys] == false) - { - continue; - } - - tracepdeex(0, rinexStream, "%s", obs.Sat.id().c_str()); - - auto& obsCodeDesc = fileData.codesPerSys[obs.Sat.sys]; - - for (auto& [obsCode, obsDesc] : obsCodeDesc) - { - if (obsCode == +E_ObsCode::NONE) - continue; - - bool foundObsPair = false; - - for (auto& [ftype, sigList] : obs.sigsLists) - for (auto& sig : sigList) - { - if (sig.code != obsCode) - continue; - - // if it locates the E_ObsCode then it will always locate E_ObsDesc. - if (foundObsPair) - { - BOOST_LOG_TRIVIAL(error) << "Error: Writing RINEX file duplicated observation."; - break; - } - else - { - foundObsPair = true; - } - - int sn_rnx = std::min(std::max((int)std::round(sig.snr / 6.0), 1), 9); - - switch (obsDesc) - { - case E_ObsDesc::C: - //tracepdeex(0,rinexStream,"%14.3f %d",sig.P,sSI); - if (sig.P == 0) tracepdeex(0, rinexStream, "%14.3s ", ""); - else tracepdeex(0, rinexStream, "%14.3f ", sig.P); - break; - - case E_ObsDesc::L: - if (sig.L == 0) tracepdeex(0, rinexStream, "%14.3s ", ""); - else tracepdeex(0, rinexStream, "%14.3f%d%d", sig.L, (unsigned int)sig.LLI, sn_rnx); - break; - - case E_ObsDesc::D: - if (sig.D == 0) tracepdeex(0, rinexStream, "%14.3s ", ""); - else tracepdeex(0, rinexStream, "%14.3f ", sig.D); - break; - - case E_ObsDesc::S: - if (sig.snr == 0) tracepdeex(0, rinexStream, "%14.3s ", ""); - else tracepdeex(0, rinexStream, "%14.3f ", sig.snr); - break; - - default: - BOOST_LOG_TRIVIAL(error) << "Error: Writing RINEX unknown/unused observation code."; - break; - } - } - - if (foundObsPair == false) - { - // Observation code and description not in observation. - tracepdeex(0, rinexStream, "%14.3s ", ""); - } - } - rinexStream << "\n"; - } + GEpoch ep = time; + string timeSysStr = "GPS"; // PEA internal time is GPS. + + rinexStream.seekp(fileData.headerTimePos); + tracepdeex( + 0, + rinexStream, + " %04.0f%6.0f%6.0f%6.0f%6.0f%13.7f %-12s%-20s\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5], + timeSysStr, + "TIME OF LAST OBS" + ); + + // Write the RINEX body. + rinexStream.seekp(0, std::ios::end); + + int count = 0; + for (auto& obs : only(obsList)) + { + if (sysMap[obs.Sat.sys] == false) + { + continue; + } + + count++; + } + + // flag epoch flag (0:ok,1:power failure,>1:event flag) + int flag = 0; + tracepdeex( + 0, + rinexStream, + "> %04.0f %02.0f %02.0f %02.0f %02.0f%11.7f %d%3d%21s\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5], + flag, + count, + "" + ); + + for (auto& obs : only(obsList)) + { + if (sysMap[obs.Sat.sys] == false) + { + continue; + } + + tracepdeex(0, rinexStream, "%s", obs.Sat.id().c_str()); + + auto& obsCodeDesc = fileData.codesPerSys[obs.Sat.sys]; + + for (auto& [obsCode, obsDesc] : obsCodeDesc) + { + if (obsCode == E_ObsCode::NONE) + continue; + + bool foundObsPair = false; + + for (auto& [ftype, sigList] : obs.sigsLists) + for (auto& sig : sigList) + { + if (sig.code != obsCode) + continue; + + // if it locates the E_ObsCode then it will always locate E_ObsDesc. + if (foundObsPair) + { + BOOST_LOG_TRIVIAL(error) << "Writing RINEX file duplicated observation."; + break; + } + else + { + foundObsPair = true; + } + + int sn_rnx = std::min(std::max((int)std::round(sig.snr / 6.0), 1), 9); + + switch (obsDesc) + { + case E_ObsDesc::C: + // tracepdeex(0,rinexStream,"%14.3f %d",sig.P,sSI); + if (sig.P == 0) + tracepdeex(0, rinexStream, "%14.3s ", ""); + else + tracepdeex(0, rinexStream, "%14.3f ", sig.P); + break; + + case E_ObsDesc::L: + if (sig.L == 0) + tracepdeex(0, rinexStream, "%14.3s ", ""); + else + tracepdeex( + 0, + rinexStream, + "%14.3f%d%d", + sig.L, + (unsigned int)sig.LLI, + sn_rnx + ); + break; + + case E_ObsDesc::D: + if (sig.D == 0) + tracepdeex(0, rinexStream, "%14.3s ", ""); + else + tracepdeex(0, rinexStream, "%14.3f ", sig.D); + break; + + case E_ObsDesc::S: + if (sig.snr == 0) + tracepdeex(0, rinexStream, "%14.3s ", ""); + else + tracepdeex(0, rinexStream, "%14.3f ", sig.snr); + break; + + default: + BOOST_LOG_TRIVIAL(error) + << "Writing RINEX unknown/unused observation code."; + break; + } + } + + if (foundObsPair == false) + { + // Observation code and description not in observation. + tracepdeex(0, rinexStream, "%14.3s ", ""); + } + } + rinexStream << "\n"; + } } bool updateRinexObsOutput( - RinexOutput& rinexOutput, ///< Information for writing file. - ObsList& obsList, ///< List of observation data - map sysMap) ///< Options to enable outputting specific systems + RinexOutput& rinexOutput, ///< Information for writing file. + ObsList& obsList, ///< List of observation data + map sysMap ///< Options to enable outputting specific systems +) { - bool foundNew = false; - for (auto& obs : only(obsList)) - { - E_Sys sys = obs.Sat.sys; - - if (sysMap[sys] == false) - { - continue; - } - - auto& codes = rinexOutput.codesPerSys[sys]; - - for (auto& [ftype, sigsList] : obs.sigsLists) - for (auto& sig : sigsList) - { - if (sig.code == +E_ObsCode::NONE) - continue; - - pair codePair; - auto& [code, type] = codePair; - - code = sig.code; - - code = sig.code; - type = E_ObsDesc::C; if (std::find(codes.begin(), codes.end(), codePair) == codes.end() &&sig.P != 0 &&acsConfig.rinex_obs_print_C_code) { foundNew = true; codes.push_back(codePair); } - type = E_ObsDesc::L; if (std::find(codes.begin(), codes.end(), codePair) == codes.end() &&sig.L != 0 &&acsConfig.rinex_obs_print_L_code) { foundNew = true; codes.push_back(codePair); } - type = E_ObsDesc::D; if (std::find(codes.begin(), codes.end(), codePair) == codes.end() &&sig.D != 0 &&acsConfig.rinex_obs_print_D_code) { foundNew = true; codes.push_back(codePair); } - type = E_ObsDesc::S; if (std::find(codes.begin(), codes.end(), codePair) == codes.end() &&sig.snr != 0 &&acsConfig.rinex_obs_print_S_code) { foundNew = true; codes.push_back(codePair); } - } - - if (codes.size() == 0) - rinexOutput.codesPerSys.erase(rinexOutput.codesPerSys.find(sys)); - } - return foundNew; + bool foundNew = false; + for (auto& obs : only(obsList)) + { + E_Sys sys = obs.Sat.sys; + + if (sysMap[sys] == false) + { + continue; + } + + auto& codes = rinexOutput.codesPerSys[sys]; + + for (auto& [ftype, sigsList] : obs.sigsLists) + for (auto& sig : sigsList) + { + if (sig.code == E_ObsCode::NONE) + continue; + + pair codePair; + auto& [code, type] = codePair; + + code = sig.code; + + code = sig.code; + type = E_ObsDesc::C; + if (std::find(codes.begin(), codes.end(), codePair) == codes.end() && sig.P != 0 && + acsConfig.rinex_obs_print_C_code) + { + foundNew = true; + codes.push_back(codePair); + } + type = E_ObsDesc::L; + if (std::find(codes.begin(), codes.end(), codePair) == codes.end() && sig.L != 0 && + acsConfig.rinex_obs_print_L_code) + { + foundNew = true; + codes.push_back(codePair); + } + type = E_ObsDesc::D; + if (std::find(codes.begin(), codes.end(), codePair) == codes.end() && sig.D != 0 && + acsConfig.rinex_obs_print_D_code) + { + foundNew = true; + codes.push_back(codePair); + } + type = E_ObsDesc::S; + if (std::find(codes.begin(), codes.end(), codePair) == codes.end() && + sig.snr != 0 && acsConfig.rinex_obs_print_S_code) + { + foundNew = true; + codes.push_back(codePair); + } + } + + if (codes.size() == 0) + rinexOutput.codesPerSys.erase(rinexOutput.codesPerSys.find(sys)); + } + return foundNew; } - void writeRinexObsFile( - RinexOutput& fileData, - SinexRecData& snx, - string fileName, - ObsList& obsList, - GTime& time, - map sysMap, - const double rnxver) + RinexOutput& fileData, + Receiver& rec, + string fileName, + ObsList& obsList, + GTime& time, + map sysMap, + const double rnxver +) { - if (obsList.empty()) - return; - - std::fstream rinexStream(fileName); - rinexStream.seekp(0, std::ios::end); - long endFilePos = rinexStream.tellp(); - - if (endFilePos == 0) - { - fileData = {}; - - if (sysMap.size() == 1) fileData.sysDesc = rinexSysDesc(sysMap.begin()->first); - else fileData.sysDesc = rinexSysDesc(E_Sys::COMB); - - updateRinexObsOutput(fileData, obsList, sysMap); - writeRinexObsHeader(fileData, snx, rinexStream, time, rnxver); - } - else - { - bool newVals = updateRinexObsOutput(fileData, obsList, sysMap); - if (newVals) - updateRinexObsHeader(fileData, rinexStream, time); - } - writeRinexObsBody(fileData, rinexStream, obsList, time, sysMap); + if (obsList.empty()) + return; + + std::fstream rinexStream(fileName); + rinexStream.seekp(0, std::ios::end); + long endFilePos = rinexStream.tellp(); + + if (endFilePos == 0) + { + fileData = {}; + + if (sysMap.size() == 1) + fileData.sysDesc = rinexSysDesc(sysMap.begin()->first); + else + fileData.sysDesc = rinexSysDesc(E_Sys::COMB); + + updateRinexObsOutput(fileData, obsList, sysMap); + writeRinexObsHeader(fileData, rec, rinexStream, time, rnxver); + } + else + { + bool newVals = updateRinexObsOutput(fileData, obsList, sysMap); + if (newVals) + updateRinexObsHeader(fileData, rinexStream, time); + } + writeRinexObsBody(fileData, rinexStream, obsList, time, sysMap); } map rinexObsFilenameMap; -void writeRinexObs( - string& id, - SinexRecData& snx, - GTime& time, - ObsList& obsList, - const double rnxver) +void writeRinexObs(string& id, Receiver& rec, GTime& time, ObsList& obsList, const double rnxver) { - string filename = acsConfig.rinex_obs_filename; + string filename = acsConfig.rinex_obs_filename; - auto filenameSysMap = getSysOutputFilenames(filename, time, true, id); + auto filenameSysMap = getSysOutputFilenames(filename, time, true, id); - for (auto [filename, sysMap] : filenameSysMap) - { - auto& fileData = filenameObsFileDataMap[filename]; + for (auto [filename, sysMap] : filenameSysMap) + { + auto& fileData = filenameObsFileDataMap[filename]; - writeRinexObsFile(fileData, snx, filename, obsList, time, sysMap, rnxver); - } + writeRinexObsFile(fileData, rec, filename, obsList, time, sysMap, rnxver); + } } diff --git a/src/cpp/common/rinexObsWrite.hpp b/src/cpp/common/rinexObsWrite.hpp index ce4887114..ebe2f898a 100644 --- a/src/cpp/common/rinexObsWrite.hpp +++ b/src/cpp/common/rinexObsWrite.hpp @@ -1,32 +1,25 @@ - #pragma once - #include #include -#include -#include #include - +#include +#include +#include "common/observations.hpp" using std::string; -#include "observations.hpp" - -struct SinexRecData; +struct Receiver; void writeRinexObs( - string& id, - SinexRecData& snx, - GTime& time, - ObsList& obsList, - const double rnxver = 3.05); + string& id, + Receiver& snx, + GTime& time, + ObsList& obsList, + const double rnxver = 3.05 +); -map> getSysOutputFilenames( - string filename, - GTime logtime, - bool replaceSys = true, - string id = ""); +map> +getSysOutputFilenames(string filename, GTime logtime, bool replaceSys = true, string id = ""); extern map rinexObsFilenameMap; - diff --git a/src/cpp/common/rtcmDecoder.cpp b/src/cpp/common/rtcmDecoder.cpp index a29509da6..63547a09b 100644 --- a/src/cpp/common/rtcmDecoder.cpp +++ b/src/cpp/common/rtcmDecoder.cpp @@ -1,1754 +1,1961 @@ - - // #pragma GCC optimize ("O0") +#include "common/rtcmDecoder.hpp" #include "architectureDocs.hpp" - -FileType RTCM__() -{ - -} - -FileType SSR__() -{ - -} - -#include "rtcmEncoder.hpp" -#include "rtcmDecoder.hpp" -#include "streamRtcm.hpp" -#include "mongoWrite.hpp" -#include "acsConfig.hpp" -#include "otherSSR.hpp" -#include "biases.hpp" -#include "gTime.hpp" -#include "enums.h" - -double RtcmDecoder::rtcmDeltaTime = 0; - -map RtcmDecoder::receivedTimeMap; - -map carrierFrequency = -{ - {F1, FREQ1}, - {F2, FREQ2}, - {F5, FREQ5}, - {F6, FREQ6}, - {F7, FREQ7}, - {F8, FREQ8}, - {G1, FREQ1_GLO}, - {G2, FREQ2_GLO}, - {G3, FREQ3_GLO}, - {G4, FREQ4_GLO}, - {G6, FREQ6_GLO}, - {B1, FREQ1_CMP}, - {B3, FREQ3_CMP} +#include "common/acsConfig.hpp" +#include "common/biases.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/mongoWrite.hpp" +#include "common/rtcmEncoder.hpp" +#include "common/streamRtcm.hpp" +#include "other_ssr/otherSSR.hpp" + +FileType RTCM__() {} + +FileType SSR__() {} + +double RtcmDecoder::rtcmDeltaTime = 0; + +map RtcmDecoder::receivedTimeMap; + +map carrierFrequency = { + {F1, FREQ1}, + {F2, FREQ2}, + {F5, FREQ5}, + {F6, FREQ6}, + {F7, FREQ7}, + {F8, FREQ8}, + {G1, FREQ1_GLO}, + {G2, FREQ2_GLO}, + {G3, FREQ3_GLO}, + {G4, FREQ4_GLO}, + {G6, FREQ6_GLO}, + {B1, FREQ1_CMP}, + {B3, FREQ3_CMP} }; - GTime RtcmDecoder::rtcmTime() { - GTime time; + GTime time; - if (rtcmTimestampTime != GTime::noTime()) time = rtcmTimestampTime; - else if (tsync != GTime::noTime()) time = tsync; - // todo Eugene: gps nav - else time = timeGet(); + if (rtcmTimestampTime != GTime::noTime()) + time = rtcmTimestampTime; + else if (tsync != GTime::noTime()) + time = tsync; + // todo Eugene: gps nav + else + time = timeGet(); - return time; + return time; } /** adjust GPS week number according to RTCM time */ -int RtcmDecoder::adjGpsWeek( - int week) ///< not-adjusted GPS week number +int RtcmDecoder::adjGpsWeek(int week) ///< not-adjusted GPS week number { - GWeek nowWeek = rtcmTime(); + GWeek nowWeek = rtcmTime(); - int dWeeks = nowWeek - week; - int roundDWeeks = (dWeeks + 512) / 1024 * 1024; + int dWeeks = nowWeek - week; + int roundDWeeks = (dWeeks + 512) / 1024 * 1024; - return (week + roundDWeeks); + return (week + roundDWeeks); } /** adjust GST week number according to RTCM time */ -int RtcmDecoder::adjGstWeek( - int week) ///< not-adjusted GST week number +int RtcmDecoder::adjGstWeek(int week) ///< not-adjusted GST week number { - GWeek nowWeek = rtcmTime(); + GWeek nowWeek = rtcmTime(); - int dWeeks = nowWeek - week; - int roundDWeeks = (dWeeks + 2048) / 4096 * 4096; + int dWeeks = nowWeek - week; + int roundDWeeks = (dWeeks + 2048) / 4096 * 4096; - return (week + roundDWeeks); + return (week + roundDWeeks); } /** adjust BDT week number according to RTCM time */ -int RtcmDecoder::adjBdtWeek( - int week) ///< not-adjusted BDT week number +int RtcmDecoder::adjBdtWeek(int week) ///< not-adjusted BDT week number { - BWeek nowWeek = rtcmTime(); + BWeek nowWeek = rtcmTime(); - int dWeeks = nowWeek - week; - int roundDWeeks = (dWeeks + 4096) / 8192 * 8192; + int dWeeks = nowWeek - week; + int roundDWeeks = (dWeeks + 4096) / 8192 * 8192; - return (week + roundDWeeks); + return (week + roundDWeeks); } -E_RTCMSubmessage RtcmDecoder::decodeCustomId( - vector& message) +E_RTCMSubmessage RtcmDecoder::decodeCustomId(vector& message) { - int i = 0; + int i = 0; - int messageNumber = getbituInc(message, i, 12); - int customMessageNumber = getbituInc(message, i, 8); + int messageNumber = getbituInc(message, i, 12); + int customMessageNumber = getbituInc(message, i, 8); - E_RTCMSubmessage customType = E_RTCMSubmessage::_from_integral(customMessageNumber); + E_RTCMSubmessage customType = int_to_enum(customMessageNumber); - return customType; + return customType; } -GTime RtcmDecoder::decodeCustomTimestamp( - vector& data) +GTime RtcmDecoder::decodeCustomTimestamp(vector& data) { - int i = 0; + int i = 0; - int messageNumber = getbituInc(data, i, 12); - int customMessageNumber = getbituInc(data, i, 8); + int messageNumber = getbituInc(data, i, 12); + int customMessageNumber = getbituInc(data, i, 8); - E_RTCMSubmessage customType = E_RTCMSubmessage::_from_integral(customMessageNumber); + E_RTCMSubmessage customType = int_to_enum(customMessageNumber); - GTime time; + GTime time; - int reserved = getbituInc(data, i, 4); + int reserved = getbituInc(data, i, 4); - unsigned int chunk1 = getbituInc(data, i, 32); - unsigned int chunk2 = getbituInc(data, i, 32); + unsigned int chunk1 = getbituInc(data, i, 32); + unsigned int chunk2 = getbituInc(data, i, 32); - long int milliseconds = ((unsigned long int) chunk1) - + (((unsigned long int) chunk2) << 32); + long int milliseconds = ((unsigned long int)chunk1) + (((unsigned long int)chunk2) << 32); - time.bigTime = milliseconds / 1000.0; + time.bigTime = milliseconds / 1000.0; - return time; + return time; } - /** decode RTCM SSR messages */ -void RtcmDecoder::decodeSSR( - vector& data) ///< stream data +void RtcmDecoder::decodeSSR(vector& data) ///< stream data { - int i = 0; - - int messageNumber = getbituInc(data, i, 12); - -// std::cout << "SSR message received: " << messageNumber << "\n"; - - RtcmMessageType messCode; - try - { - messCode = RtcmMessageType::_from_integral(messageNumber); - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised message in " << __FUNCTION__; - return; - } - - string messCodeStr = messCode._to_string(); - string messTypeStr = messCodeStr.substr(8); - - E_Sys sys = rtcmMessageSystemMap[messCode]; - - if (sys == +E_Sys::NONE) - { - BOOST_LOG_TRIVIAL(error) << "Error: invalid message code system :" << messCode; - return; - } - - if ( sys != +E_Sys::GPS - &&sys != +E_Sys::GLO - &&sys != +E_Sys::GAL - &&sys != +E_Sys::QZS - &&sys != +E_Sys::SBS - &&sys != +E_Sys::BDS) - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised message in " << __FUNCTION__; - } - - // if (sys == +E_Sys::BDS) - // { - // printf ("\nRTCM %4d: ", messageNumber); - // for (auto& dat : data) printf("%02X",dat); - // printf ("\n"); - // } - - int ne = 0; - int ns = 0; - int np = 0; - int ni = 0; - int nj = 0; - switch (sys) - { - case E_Sys::GPS: ne = 20; ns = 6; np = 6; ni = 8; nj = 0; break; - case E_Sys::GLO: ne = 17; ns = 6; np = 5; ni = 8; nj = 0; break; - case E_Sys::GAL: ne = 20; ns = 6; np = 6; ni = 10; nj = 0; break; - case E_Sys::QZS: ne = 20; ns = 4; np = 4; ni = 8; nj = 0; break; - case E_Sys::BDS: ne = 20; ns = 6; np = 6; ni = 8; nj = 10; break; - case E_Sys::SBS: ne = 20; ns = 6; np = 6; ni = 9; nj = 0; break; - default: BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system in " << __FUNCTION__; return; - } - - int epochTime1s = getbituInc(data, i, ne); - int updateIntIndex = getbituInc(data, i, 4); - int multipleMessage = getbituInc(data, i, 1); - - int ssrUpdateInterval = updateInterval[updateIntIndex]; - - double referenceTime; - if (updateIntIndex == 0) referenceTime = epochTime1s; - else referenceTime = epochTime1s + ssrUpdateInterval / 2.0; - - GTime nearTime = rtcmTime(); - - GTime receivedTime; - GTime t0; - if (sys == +E_Sys::GLO) - { - receivedTime = GTime(RTod(epochTime1s), nearTime); - t0 = GTime(RTod(referenceTime), nearTime); - } - else if (sys == +E_Sys::BDS) - { - receivedTime = GTime(BTow(epochTime1s), nearTime); - t0 = GTime(BTow(referenceTime), nearTime); - } - else - { - receivedTime = GTime(GTow(epochTime1s), nearTime); - t0 = GTime(GTow(referenceTime), nearTime); - } - -// std::cout << "SSR message received: " << messCode << "\n"; - - unsigned int referenceDatum = 0; - if ( messTypeStr == "ORB_CORR" - ||messTypeStr == "COMB_CORR") - { - referenceDatum = getbituInc(data, i, 1); - } - - unsigned int iod = getbituInc(data, i, 4); - unsigned int provider = getbituInc(data, i, 16); - unsigned int solution = getbituInc(data, i, 4); - - unsigned int dispBiasConistInd = 0; - unsigned int MWConistInd = 0; - if ( messTypeStr == "PHASE_BIAS") - { - dispBiasConistInd = getbituInc(data, i, 1); - MWConistInd = getbituInc(data, i, 1); - } - - unsigned int numSats = getbituInc(data, i, ns); - - // SRR variables for encoding and decoding. - SSRMeta ssrMeta; - ssrMeta.epochTime1s = epochTime1s; - ssrMeta.receivedTime = receivedTime; - ssrMeta.updateIntIndex = updateIntIndex; - ssrMeta.multipleMessage = multipleMessage; - ssrMeta.referenceDatum = referenceDatum; - ssrMeta.provider = provider; - ssrMeta.solution = solution; - ssrMeta.numSats = numSats; - - bool failure = false; - - for (int sat = 0; sat < numSats; sat++) - { - unsigned int satId = getbituInc(data, i, np); - - SatSys Sat(sys, satId); - - auto& ssr = nav.satNavMap[Sat].receivedSSR; - - if ( messTypeStr == "ORB_CORR" - ||messTypeStr == "COMB_CORR") - { - SSREph ssrEph; - ssrEph.ssrMeta = ssrMeta; - ssrEph.t0 = t0; - ssrEph.udi = ssrUpdateInterval; - ssrEph.iod = iod; - - ssrEph.iodcrc = getbituInc(data, i, nj); - ssrEph.iode = getbituInc(data, i, ni); - ssrEph.deph[0] = getbitsInc(data, i, 22) * 0.1e-3; // Position, radial, along track, cross track. - ssrEph.deph[1] = getbitsInc(data, i, 20) * 0.4e-3; - ssrEph.deph[2] = getbitsInc(data, i, 20) * 0.4e-3; - ssrEph.ddeph[0] = getbitsInc(data, i, 21) * 0.001e-3; // Velocity - ssrEph.ddeph[1] = getbitsInc(data, i, 19) * 0.004e-3; - ssrEph.ddeph[2] = getbitsInc(data, i, 19) * 0.004e-3; - - tracepdeex(5, std::cout, "\n#RTCM_SSR ORBITS %s %s %4d %10.3f %10.3f %10.3f %d ", Sat.id().c_str(), ssrEph.t0.to_string().c_str(), ssrEph.iode, ssrEph.deph[0], ssrEph.deph[1], ssrEph.deph[2], iod); - - ssr.ssrEph_map[receivedTime] = ssrEph; - - if (acsConfig.output_decoded_rtcm_json) - traceSsrEph(messCode, Sat, ssrEph); - } - - if ( messTypeStr == "CLK_CORR" - ||messTypeStr == "COMB_CORR") - { - SSRClk ssrClk; - ssrClk.ssrMeta = ssrMeta; - ssrClk.t0 = t0; - ssrClk.udi = ssrUpdateInterval; - ssrClk.iod = iod; - - // C = C_0 + C_1(t-t_0)+C_2(t-t_0)^2 where C is a correction in meters. - // C gets converted into a time correction for futher calculations. - - ssrClk.dclk[0] = getbitsInc(data, i, 22) * 0.1e-3; - ssrClk.dclk[1] = getbitsInc(data, i, 21) * 0.001e-3; - ssrClk.dclk[2] = getbitsInc(data, i, 27) * 0.00002e-3; - - tracepdeex(5, std::cout, "\n#RTCM_SSR CLOCKS %s %s %10.3f %10.3f %10.3f %d", Sat.id().c_str(), ssrClk.t0.to_string().c_str(), ssrClk.dclk[0], ssrClk.dclk[1], ssrClk.dclk[2], iod); - - ssr.ssrClk_map[receivedTime] = ssrClk; - - if (acsConfig.output_decoded_rtcm_json) - traceSsrClk(messCode, Sat, ssrClk); - } - - if (messTypeStr == "URA") - { - //std::cout << "Received SSR URA Message.\n"; - - SSRUra ssrUra; - ssrUra.ssrMeta = ssrMeta; - ssrUra.t0 = t0; - ssrUra.udi = ssrUpdateInterval; - ssrUra.iod = iod; - - int uraClassValue = getbituInc(data, i, 6); - ssrUra.ura = uraSsr[uraClassValue]; - - // This is the total User Range Accuracy calculated from all the SSR. - // TODO: Check implementation, RTCM manual DF389. - - ssr.ssrUra_map[receivedTime] = ssrUra; - - if (acsConfig.output_decoded_rtcm_json) - traceSsrUra(messCode, Sat, ssrUra); - } - - if (messTypeStr == "HR_CLK_CORR") - { - SSRHRClk ssrHRClk; - ssrHRClk.ssrMeta = ssrMeta; - ssrHRClk.t0 = t0; - ssrHRClk.udi = ssrUpdateInterval; - ssrHRClk.iod = iod; - - ssrHRClk.hrclk = getbitsInc(data, i, 22) * 0.1e-3; - - ssr.ssrHRClk_map[receivedTime] = ssrHRClk; - - if (acsConfig.output_decoded_rtcm_json) - traceSsrHRClk(messCode, Sat, ssrHRClk); - } - - if (messTypeStr == "CODE_BIAS") - { - SSRCodeBias ssrBiasCode; - ssrBiasCode.ssrMeta = ssrMeta; - ssrBiasCode.t0 = t0; - ssrBiasCode.udi = ssrUpdateInterval; - ssrBiasCode.iod = iod; - - ssrBiasCode.nbias = getbituInc(data, i, 5); - - BiasEntry entry; - string id; - if (Sat.sys == +E_Sys::GLO) id = Sat.id() + ":" + Sat.id(); - else id = Sat.id() + ":" + Sat.sysChar(); - - entry.measType = CODE; - entry.Sat = Sat; - entry.tini = t0; - entry.tfin = entry.tini + acsConfig.ssrInOpts.code_bias_valid_time; - entry.source = "ssr"; - - for (int k = 0; k < ssrBiasCode.nbias && i + 19 <= data.size() * 8; k++) - { - int rtcmCode = getbituInc(data, i, 5); - double bias = getbitsIncScale(data, i, 14, 0.01, &failure); - - try - { - E_ObsCode obsCode; - if (sys == +E_Sys::GPS) { obsCode = mCodes_gps.right.at(rtcmCode); } - else if (sys == +E_Sys::GLO) { obsCode = mCodes_glo.right.at(rtcmCode); } - else if (sys == +E_Sys::GAL) { obsCode = mCodes_gal.right.at(rtcmCode); } - else if (sys == +E_Sys::QZS) { obsCode = mCodes_qzs.right.at(rtcmCode); } - else if (sys == +E_Sys::BDS) { obsCode = mCodes_bds.right.at(rtcmCode); } - else if (sys == +E_Sys::SBS) { obsCode = mCodes_sbs.right.at(rtcmCode); } - else - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system in " << __FUNCTION__; - continue; - } - - ssrBiasCode.obsCodeBiasMap[obsCode].bias = bias; //todo aaron missing var - - if (acsConfig.output_decoded_rtcm_json) - traceSsrCodeBias(messCode, Sat, obsCode, ssrBiasCode); - - entry.cod1 = obsCode; - entry.cod2 = E_ObsCode::NONE; - entry.bias = bias; - entry.var = 0; - entry.slop = 0; - entry.slpv = 0; - - pushBiasEntry(id, entry); - tracepdeex(5, std::cout, "\n#RTCM_SSR CODBIA for %s %s: %.4f", Sat.id().c_str(), obsCode._to_string(), bias); - } - - catch (std::exception& e) - { - // BOOST_LOG_TRIVIAL(error) << "Error, Decoding SSR Message unknown RTCM code : " << rtcmCode << " for " << rtcmMountPoint << " : " << messCode; - // BOOST_LOG_TRIVIAL(error) << "Code bias for " << Sat.id() << " rtcmCode: " << rtcmCode << ": " << bias; - continue; - } - } - - ssr.ssrCodeBias_map[receivedTime] = ssrBiasCode; - } - - if ( messTypeStr == "PHASE_BIAS") - { - SSRPhasBias ssrBiasPhas; - SSRPhase ssrPhase; - ssrBiasPhas.ssrMeta = ssrMeta; - ssrBiasPhas.t0 = t0; - ssrBiasPhas.udi = ssrUpdateInterval; - ssrBiasPhas.iod = iod; - - ssrPhase.dispBiasConistInd = dispBiasConistInd; - ssrPhase.MWConistInd = MWConistInd; - - ssrBiasPhas.nbias = getbituInc (data, i, 5); - ssrPhase.yawAngle = getbituIncScale (data, i, 9, 1/256.0 *SC2RAD); - ssrPhase.yawRate = getbitsIncScale (data, i, 8, 1/8192.0 *SC2RAD, &failure); - - ssrBiasPhas.ssrPhase = ssrPhase; - - BiasEntry entry; - string id; - if (Sat.sys == +E_Sys::GLO) id = Sat.id() + ":" + Sat.id(); - else id = Sat.id() + ":" + Sat.sysChar(); - - entry.measType = PHAS; - entry.Sat = Sat; - entry.tini = t0; - entry.tfin = entry.tini + acsConfig.ssrInOpts.phase_bias_valid_time; - entry.source = "ssr"; - - for (int k = 0; k < ssrBiasPhas.nbias && i + 32 <= data.size() * 8; k++) - { - SSRPhaseCh ssrPhaseCh; - unsigned int rtcmCode = getbituInc(data, i, 5); - ssrPhaseCh.signalIntInd = getbituInc(data, i, 1); - ssrPhaseCh.signalWLIntInd = getbituInc(data, i, 2); - ssrPhaseCh.signalDisconCnt = getbituInc(data, i, 4); - double bias = getbitsIncScale(data, i, 20, 0.0001, &failure); - - try - { - E_ObsCode obsCode; - if (sys == +E_Sys::GPS) { obsCode = mCodes_gps.right.at(rtcmCode); } - else if (sys == +E_Sys::GLO) { obsCode = mCodes_glo.right.at(rtcmCode); } - else if (sys == +E_Sys::GAL) { obsCode = mCodes_gal.right.at(rtcmCode); } - else if (sys == +E_Sys::QZS) { obsCode = mCodes_qzs.right.at(rtcmCode); } - else if (sys == +E_Sys::BDS) { obsCode = mCodes_bds.right.at(rtcmCode); } - else if (sys == +E_Sys::SBS) { obsCode = mCodes_sbs.right.at(rtcmCode); } - else - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system in " << __FUNCTION__; - continue; - } - - ssrBiasPhas.obsCodeBiasMap [obsCode].bias = bias; // offset meters due to satellite rotation. //todo aaron missing var - ssrBiasPhas.ssrPhaseChs [obsCode] = ssrPhaseCh; - - if (acsConfig.output_decoded_rtcm_json) - traceSsrPhasBias(messCode, Sat, obsCode, ssrBiasPhas); - - entry.cod1 = obsCode; - entry.cod2 = E_ObsCode::NONE; - entry.bias = bias; - entry.var = 0; - entry.slop = 0; - entry.slpv = 0; - - pushBiasEntry(id, entry); - tracepdeex(5, std::cout, "\n#RTCM_SSR PHSBIA for %s %s: %.4f", Sat.id().c_str(), obsCode._to_string(), bias); - } - catch (std::exception& e) - { - // BOOST_LOG_TRIVIAL(error) << "Error, Decoding SSR Message unknown RTCM code : " << rtcmCode << " for " << rtcmMountPoint << " : " << messCode; - continue; - } - } - - ssr.ssrPhasBias_map[receivedTime] = ssrBiasPhas; - } - } + int i = 0; + + int messageNumber = getbituInc(data, i, 12); + + // std::cout << "SSR message received: " << messageNumber << "\n"; + + RtcmMessageType messCode; + try + { + messCode = messageNumberToRtcmType(messageNumber); + } + catch (...) + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised message in " << __FUNCTION__; + return; + } + + string messCodeStr = enum_to_string(messCode); + string messTypeStr = messCodeStr.substr(8); + + E_Sys sys = rtcmMessageSystemMap[messCode]; + + if (sys == E_Sys::NONE) + { + BOOST_LOG_TRIVIAL(error) << "Invalid message code system :" << messCode; + return; + } + + if (sys != E_Sys::GPS && sys != E_Sys::GLO && sys != E_Sys::GAL && sys != E_Sys::QZS && + sys != E_Sys::SBS && sys != E_Sys::BDS) + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised message in " << __FUNCTION__; + } + + // if (sys == E_Sys::BDS) + // { + // printf ("\nRTCM %4d: ", messageNumber); + // for (auto& dat : data) printf("%02X",dat); + // printf ("\n"); + // } + + int ne = 0; + int ns = 0; + int np = 0; + int ni = 0; + int nj = 0; + switch (sys) + { + case E_Sys::GPS: + ne = 20; + ns = 6; + np = 6; + ni = 8; + nj = 0; + break; + case E_Sys::GLO: + ne = 17; + ns = 6; + np = 5; + ni = 8; + nj = 0; + break; + case E_Sys::GAL: + ne = 20; + ns = 6; + np = 6; + ni = 10; + nj = 0; + break; + case E_Sys::QZS: + ne = 20; + ns = 4; + np = 4; + ni = 8; + nj = 0; + break; + case E_Sys::BDS: + ne = 20; + ns = 6; + np = 6; + ni = 8; + nj = 10; + break; + case E_Sys::SBS: + ne = 20; + ns = 6; + np = 6; + ni = 9; + nj = 0; + break; + default: + BOOST_LOG_TRIVIAL(error) << "Unrecognised system in " << __FUNCTION__; + return; + } + + int epochTime1s = getbituInc(data, i, ne); + int updateIntIndex = getbituInc(data, i, 4); + int multipleMessage = getbituInc(data, i, 1); + + int ssrUpdateInterval = updateInterval[updateIntIndex]; + + double referenceTime; + if (updateIntIndex == 0) + referenceTime = epochTime1s; + else + referenceTime = epochTime1s + ssrUpdateInterval / 2.0; + + GTime nearTime = rtcmTime(); + + GTime receivedTime; + GTime t0; + if (sys == E_Sys::GLO) + { + receivedTime = GTime(RTod(epochTime1s), nearTime); + t0 = GTime(RTod(referenceTime), nearTime); + } + else if (sys == E_Sys::BDS) + { + receivedTime = GTime(BTow(epochTime1s), nearTime); + t0 = GTime(BTow(referenceTime), nearTime); + } + else + { + receivedTime = GTime(GTow(epochTime1s), nearTime); + t0 = GTime(GTow(referenceTime), nearTime); + } + + // std::cout << "SSR message received: " << messCode << "\n"; + + unsigned int referenceDatum = 0; + if (messTypeStr == "ORB_CORR" || messTypeStr == "COMB_CORR") + { + referenceDatum = getbituInc(data, i, 1); + } + + unsigned int iod = getbituInc(data, i, 4); + unsigned int provider = getbituInc(data, i, 16); + unsigned int solution = getbituInc(data, i, 4); + + unsigned int dispBiasConistInd = 0; + unsigned int MWConistInd = 0; + if (messTypeStr == "PHASE_BIAS") + { + dispBiasConistInd = getbituInc(data, i, 1); + MWConistInd = getbituInc(data, i, 1); + } + + unsigned int numSats = getbituInc(data, i, ns); + + // SRR variables for encoding and decoding. + SSRMeta ssrMeta; + ssrMeta.epochTime1s = epochTime1s; + ssrMeta.receivedTime = receivedTime; + ssrMeta.updateIntIndex = updateIntIndex; + ssrMeta.multipleMessage = multipleMessage; + ssrMeta.referenceDatum = referenceDatum; + ssrMeta.provider = provider; + ssrMeta.solution = solution; + ssrMeta.numSats = numSats; + + bool failure = false; + + for (int sat = 0; sat < numSats; sat++) + { + unsigned int satId = getbituInc(data, i, np); + + SatSys Sat(sys, satId); + + auto& ssr = nav.satNavMap[Sat].receivedSSR; + + if (messTypeStr == "ORB_CORR" || messTypeStr == "COMB_CORR") + { + SSREph ssrEph; + ssrEph.ssrMeta = ssrMeta; + ssrEph.t0 = t0; + ssrEph.udi = ssrUpdateInterval; + ssrEph.iod = iod; + + ssrEph.iodcrc = getbituInc(data, i, nj); + ssrEph.iode = getbituInc(data, i, ni); + ssrEph.deph[0] = + getbitsInc(data, i, 22) * 0.1e-3; // Position, radial, along track, cross track. + ssrEph.deph[1] = getbitsInc(data, i, 20) * 0.4e-3; + ssrEph.deph[2] = getbitsInc(data, i, 20) * 0.4e-3; + ssrEph.ddeph[0] = getbitsInc(data, i, 21) * 0.001e-3; // Velocity + ssrEph.ddeph[1] = getbitsInc(data, i, 19) * 0.004e-3; + ssrEph.ddeph[2] = getbitsInc(data, i, 19) * 0.004e-3; + + tracepdeex( + 5, + std::cout, + "\n#RTCM_SSR ORBITS %s %s %4d %10.3f %10.3f %10.3f %d ", + Sat.id().c_str(), + ssrEph.t0.to_string().c_str(), + ssrEph.iode, + ssrEph.deph[0], + ssrEph.deph[1], + ssrEph.deph[2], + iod + ); + + ssr.ssrEph_map[receivedTime] = ssrEph; + + if (acsConfig.output_decoded_rtcm_json) + traceSsrEph(messCode, Sat, ssrEph); + } + + if (messTypeStr == "CLK_CORR" || messTypeStr == "COMB_CORR") + { + SSRClk ssrClk; + ssrClk.ssrMeta = ssrMeta; + ssrClk.t0 = t0; + ssrClk.udi = ssrUpdateInterval; + ssrClk.iod = iod; + + // C = C_0 + C_1(t-t_0)+C_2(t-t_0)^2 where C is a correction in meters. + // C gets converted into a time correction for futher calculations. + + ssrClk.dclk[0] = getbitsInc(data, i, 22) * 0.1e-3; + ssrClk.dclk[1] = getbitsInc(data, i, 21) * 0.001e-3; + ssrClk.dclk[2] = getbitsInc(data, i, 27) * 0.00002e-3; + + tracepdeex( + 5, + std::cout, + "\n#RTCM_SSR CLOCKS %s %s %10.3f %10.3f %10.3f %d", + Sat.id().c_str(), + ssrClk.t0.to_string().c_str(), + ssrClk.dclk[0], + ssrClk.dclk[1], + ssrClk.dclk[2], + iod + ); + + ssr.ssrClk_map[receivedTime] = ssrClk; + + if (acsConfig.output_decoded_rtcm_json) + traceSsrClk(messCode, Sat, ssrClk); + } + + if (messTypeStr == "URA") + { + // std::cout << "Received SSR URA Message.\n"; + + SSRUra ssrUra; + ssrUra.ssrMeta = ssrMeta; + ssrUra.t0 = t0; + ssrUra.udi = ssrUpdateInterval; + ssrUra.iod = iod; + + int uraClassValue = getbituInc(data, i, 6); + ssrUra.ura = uraSsr[uraClassValue]; + + // This is the total User Range Accuracy calculated from all the SSR. + // TODO: Check implementation, RTCM manual DF389. + + ssr.ssrUra_map[receivedTime] = ssrUra; + + if (acsConfig.output_decoded_rtcm_json) + traceSsrUra(messCode, Sat, ssrUra); + } + + if (messTypeStr == "HR_CLK_CORR") + { + SSRHRClk ssrHRClk; + ssrHRClk.ssrMeta = ssrMeta; + ssrHRClk.t0 = t0; + ssrHRClk.udi = ssrUpdateInterval; + ssrHRClk.iod = iod; + + ssrHRClk.hrclk = getbitsInc(data, i, 22) * 0.1e-3; + + ssr.ssrHRClk_map[receivedTime] = ssrHRClk; + + if (acsConfig.output_decoded_rtcm_json) + traceSsrHRClk(messCode, Sat, ssrHRClk); + } + + if (messTypeStr == "CODE_BIAS") + { + SSRCodeBias ssrBiasCode; + ssrBiasCode.ssrMeta = ssrMeta; + ssrBiasCode.t0 = t0; + ssrBiasCode.udi = ssrUpdateInterval; + ssrBiasCode.iod = iod; + + ssrBiasCode.nbias = getbituInc(data, i, 5); + + BiasEntry entry; + string id; + if (Sat.sys == E_Sys::GLO) + id = Sat.id() + ":" + Sat.id(); + else + id = Sat.id() + ":" + Sat.sysChar(); + + entry.measType = CODE; + entry.Sat = Sat; + entry.tini = t0; + entry.tfin = entry.tini + acsConfig.ssrInOpts.code_bias_valid_time; + entry.source = "ssr"; + + for (int k = 0; k < ssrBiasCode.nbias && i + 19 <= data.size() * 8; k++) + { + int rtcmCode = getbituInc(data, i, 5); + double bias = getbitsIncScale(data, i, 14, 0.01, &failure); + + try + { + E_ObsCode obsCode; + if (sys == E_Sys::GPS) + { + obsCode = mCodes_gps.right.at(rtcmCode); + } + else if (sys == E_Sys::GLO) + { + obsCode = mCodes_glo.right.at(rtcmCode); + } + else if (sys == E_Sys::GAL) + { + obsCode = mCodes_gal.right.at(rtcmCode); + } + else if (sys == E_Sys::QZS) + { + obsCode = mCodes_qzs.right.at(rtcmCode); + } + else if (sys == E_Sys::BDS) + { + obsCode = mCodes_bds.right.at(rtcmCode); + } + else if (sys == E_Sys::SBS) + { + obsCode = mCodes_sbs.right.at(rtcmCode); + } + else + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised system in " << __FUNCTION__; + continue; + } + + ssrBiasCode.obsCodeBiasMap[obsCode].bias = bias; // todo aaron missing var + + if (acsConfig.output_decoded_rtcm_json) + traceSsrCodeBias(messCode, Sat, obsCode, ssrBiasCode); + + entry.cod1 = obsCode; + entry.cod2 = E_ObsCode::NONE; + entry.bias = bias; + entry.var = 0; + entry.slop = 0; + entry.slpv = 0; + + updateRefTime(entry); + pushBiasEntry(id, entry); + tracepdeex( + 5, + std::cout, + "\n#RTCM_SSR CODBIA for %s %s: %.4f", + Sat.id().c_str(), + enum_to_string(obsCode), + bias + ); + } + + catch (std::exception& e) + { + // BOOST_LOG_TRIVIAL(error) << "Decoding SSR Message unknown RTCM code : + // " << rtcmCode << " for " << rtcmMountPoint << " : " << messCode; + // BOOST_LOG_TRIVIAL(error) << "Code bias for " << Sat.id() << " rtcmCode: " << + // rtcmCode << ": " << bias; + continue; + } + } + + ssr.ssrCodeBias_map[receivedTime] = ssrBiasCode; + } + + if (messTypeStr == "PHASE_BIAS") + { + SSRPhasBias ssrBiasPhas; + SSRPhase ssrPhase; + ssrBiasPhas.ssrMeta = ssrMeta; + ssrBiasPhas.t0 = t0; + ssrBiasPhas.udi = ssrUpdateInterval; + ssrBiasPhas.iod = iod; + + ssrPhase.dispBiasConistInd = dispBiasConistInd; + ssrPhase.MWConistInd = MWConistInd; + + ssrBiasPhas.nbias = getbituInc(data, i, 5); + ssrPhase.yawAngle = getbituIncScale(data, i, 9, 1 / 256.0 * SC2RAD); + ssrPhase.yawRate = getbitsIncScale(data, i, 8, 1 / 8192.0 * SC2RAD, &failure); + + ssrBiasPhas.ssrPhase = ssrPhase; + + BiasEntry entry; + string id; + if (Sat.sys == E_Sys::GLO) + id = Sat.id() + ":" + Sat.id(); + else + id = Sat.id() + ":" + Sat.sysChar(); + + entry.measType = PHAS; + entry.Sat = Sat; + entry.tini = t0; + entry.tfin = entry.tini + acsConfig.ssrInOpts.phase_bias_valid_time; + entry.source = "ssr"; + + for (int k = 0; k < ssrBiasPhas.nbias && i + 32 <= data.size() * 8; k++) + { + SSRPhaseCh ssrPhaseCh; + unsigned int rtcmCode = getbituInc(data, i, 5); + ssrPhaseCh.signalIntInd = getbituInc(data, i, 1); + ssrPhaseCh.signalWLIntInd = getbituInc(data, i, 2); + ssrPhaseCh.signalDisconCnt = getbituInc(data, i, 4); + double bias = getbitsIncScale(data, i, 20, 0.0001, &failure); + + try + { + E_ObsCode obsCode; + if (sys == E_Sys::GPS) + { + obsCode = mCodes_gps.right.at(rtcmCode); + } + else if (sys == E_Sys::GLO) + { + obsCode = mCodes_glo.right.at(rtcmCode); + } + else if (sys == E_Sys::GAL) + { + obsCode = mCodes_gal.right.at(rtcmCode); + } + else if (sys == E_Sys::QZS) + { + obsCode = mCodes_qzs.right.at(rtcmCode); + } + else if (sys == E_Sys::BDS) + { + obsCode = mCodes_bds.right.at(rtcmCode); + } + else if (sys == E_Sys::SBS) + { + obsCode = mCodes_sbs.right.at(rtcmCode); + } + else + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised system in " << __FUNCTION__; + continue; + } + + ssrBiasPhas.obsCodeBiasMap[obsCode].bias = + bias; // offset meters due to satellite rotation. //todo aaron missing var + ssrBiasPhas.ssrPhaseChs[obsCode] = ssrPhaseCh; + + if (acsConfig.output_decoded_rtcm_json) + traceSsrPhasBias(messCode, Sat, obsCode, ssrBiasPhas); + + entry.cod1 = obsCode; + entry.cod2 = E_ObsCode::NONE; + entry.bias = bias; + entry.var = 0; + entry.slop = 0; + entry.slpv = 0; + + updateRefTime(entry); + pushBiasEntry(id, entry); + tracepdeex( + 5, + std::cout, + "\n#RTCM_SSR PHSBIA for %s %s: %.4f", + Sat.id().c_str(), + enum_to_string(obsCode), + bias + ); + } + catch (std::exception& e) + { + // BOOST_LOG_TRIVIAL(error) << "Decoding SSR Message unknown RTCM code : + // " << rtcmCode << " for " << rtcmMountPoint << " : " << messCode; + continue; + } + } + + ssr.ssrPhasBias_map[receivedTime] = ssrBiasPhas; + } + } } - /** decode RTCM navigation messages */ -void RtcmDecoder::decodeEphemeris( - vector& data) ///< stream data +void RtcmDecoder::decodeEphemeris(vector& data) ///< stream data { - Eph eph = {}; - Geph geph = {}; - int i = 0; - - int messageNumber = getbituInc(data, i, 12); - - RtcmMessageType messCode; - try - { - messCode = RtcmMessageType::_from_integral(messageNumber); - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised message in " << __FUNCTION__; - return; - } - - E_Sys sys = E_Sys::NONE; - switch (messageNumber) - { - case RtcmMessageType::GPS_EPHEMERIS: sys = E_Sys::GPS; break; - case RtcmMessageType::GLO_EPHEMERIS: sys = E_Sys::GLO; break; - case RtcmMessageType::BDS_EPHEMERIS: sys = E_Sys::BDS; break; - case RtcmMessageType::QZS_EPHEMERIS: sys = E_Sys::QZS; break; - case RtcmMessageType::GAL_FNAV_EPHEMERIS: //fallthrough - case RtcmMessageType::GAL_INAV_EPHEMERIS: sys = E_Sys::GAL; break; - default: - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised message in " << __FUNCTION__; - return; - } - } - - bool failure = false; - - if (sys == +E_Sys::GPS) - { - if (i + 488-12 > data.size() * 8) - { - BOOST_LOG_TRIVIAL(error) << "Error: rtcm3 1019 length error: len=" << data.size(); - return; - } - - eph.type = E_NavMsgType::LNAV; - - int prn = getbituInc(data, i, 6); - eph.weekRollOver= getbituInc(data, i, 10); eph.week = adjGpsWeek(eph.weekRollOver); // rolled-over week -> full week number - eph.sva = getbituInc(data, i, 4); eph.ura[0] = svaToUra(eph.sva); - eph.code = getbituInc(data, i, 2); - eph.idot = getbitsInc(data, i, 14)*P2_43*SC2RAD; - eph.iode = getbituInc(data, i, 8); - eph.tocs = getbituInc(data, i, 16)*16.0; - eph.f2 = getbitsInc(data, i, 8)*P2_55; - eph.f1 = getbitsInc(data, i, 16)*P2_43; - eph.f0 = getbitsInc(data, i, 22)*P2_31; - eph.iodc = getbituInc(data, i, 10); - 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, 16)*16.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, 8)*P2_31; - int svh = getbituInc(data, i, 6); eph.svh = (E_Svh)svh; - eph.flag = getbituInc(data, i, 1); - eph.fitFlag = getbituInc(data, i, 1); eph.fit = eph.fitFlag?0.0:4.0; // 0:4hr,1:>4hr - - if (prn >= 40) - { - sys = E_Sys::SBS; - prn += 80; - } - eph.Sat = SatSys(sys, prn); - - GTime nearTime = rtcmTime(); - - eph.ttm = nearTime; - eph.toe = GTime(GTow(eph.toes), nearTime); - eph.toc = GTime(GTow(eph.tocs), nearTime); - - if (acsConfig.use_tgd_bias) - decomposeTGDBias(eph.Sat, eph.tgd[0]); - } - else if (sys == +E_Sys::GLO) - { - if (i + 360-12 > data.size() * 8) - { - BOOST_LOG_TRIVIAL(error) << "Error: rtcm3 1020 length error: len=" << data.size(); - return; - } - - geph.type = E_NavMsgType::FDMA; - - int prn = getbituInc(data, i, 6); - geph.frq = getbituInc(data, i, 5)-7; i += 4; // skip DF104, 105, 106 (almanac health, P1) - geph.tk_hour = getbituInc(data, i, 5); - geph.tk_min = getbituInc(data, i, 6); - geph.tk_sec = getbituInc(data, i, 1)*30; - int svh = getbituInc(data, i, 1); geph.svh = (E_Svh)svh; - - int dummy = getbituInc(data, i, 1); // skip DF109 (P2) - geph.tb = getbituInc(data, i, 7); geph.iode = geph.tb; - geph.vel[0] = getbitgInc(data, i, 24)*P2_20*1E3; - geph.pos[0] = getbitgInc(data, i, 27)*P2_11*1E3; - geph.acc[0] = getbitgInc(data, i, 5)*P2_30*1E3; - geph.vel[1] = getbitgInc(data, i, 24)*P2_20*1E3; - geph.pos[1] = getbitgInc(data, i, 27)*P2_11*1E3; - geph.acc[1] = getbitgInc(data, i, 5)*P2_30*1E3; - geph.vel[2] = getbitgInc(data, i, 24)*P2_20*1E3; - geph.pos[2] = getbitgInc(data, i, 27)*P2_11*1E3; - geph.acc[2] = getbitgInc(data, i, 5)*P2_30*1E3; - dummy = getbituInc(data, i, 1); // skip DF120 (P3) - geph.gammaN = getbitgInc(data, i, 11)*P2_40; - dummy = getbituInc(data, i, 3); // skip DF122, 123 (P, ln) - geph.taun = getbitgInc(data, i, 22)*P2_30; - geph.dtaun = getbitgInc(data, i, 5)*P2_30; - geph.age = getbituInc(data, i, 5); - dummy = getbituInc(data, i, 5); // skip DF127, 128 (P4, FT) - geph.NT = getbituInc(data, i, 11); // GLONASS-M only, may be arbitrary value - geph.glonassM = getbituInc(data, i, 2); // if GLONASS-M data feilds valid - geph.moreData = getbituInc(data, i, 1); // availability of additional data - i += 43; // skip DF132, 133 (NA, tauc) - geph.N4 = getbituInc(data, i, 5); // additional data and GLONASS-M only, may be arbitrary value - - geph.Sat = SatSys(sys, prn); - - RTod toes = geph.tb * 15 * 60; - - RTod tofs = geph.tk_hour * 60 * 60 - + geph.tk_min * 60 - + geph.tk_sec; - - GTime nearTime = rtcmTime(); - geph.toe = GTime(toes, nearTime); - geph.tof = GTime(tofs, nearTime); - } - else if (sys == +E_Sys::BDS) - { - if (i + 511-12 > data.size() * 8) - { - BOOST_LOG_TRIVIAL(error) << "Error: rtcm3 1042 length error: len=" << data.size(); - return; - } - - eph.type = E_NavMsgType::D1; - - int prn = getbituInc(data, i, 6); - eph.weekRollOver= getbituInc(data, i, 13); eph.week = adjBdtWeek(eph.weekRollOver); // rolled-over week -> full week number - eph.sva = getbituInc(data, i, 4); eph.ura[0] = svaToUra(eph.sva); - eph.idot = getbitsInc(data, i, 14)*P2_43*SC2RAD; - eph.aode = getbituInc(data, i, 5); - eph.tocs = getbituInc(data, i, 17)*8.0; - eph.f2 = getbitsInc(data, i, 11)*P2_66; - eph.f1 = getbitsInc(data, i, 22)*P2_50; - eph.f0 = getbitsInc(data, i, 24)*P2_33; - eph.aodc = getbituInc(data, i, 5); - eph.crs = getbitsInc(data, i, 18)*P2_6; - eph.deln = getbitsInc(data, i, 16)*P2_43*SC2RAD; - eph.M0 = getbitsInc(data, i, 32)*P2_31*SC2RAD; - eph.cuc = getbitsInc(data, i, 18)*P2_31; - eph.e = getbituInc(data, i, 32)*P2_33; - eph.cus = getbitsInc(data, i, 18)*P2_31; - eph.sqrtA = getbituInc(data, i, 32)*P2_19; eph.A = SQR(eph.sqrtA); - eph.toes = getbituInc(data, i, 17)*8.0; - eph.cic = getbitsInc(data, i, 18)*P2_31; - eph.OMG0 = getbitsInc(data, i, 32)*P2_31*SC2RAD; - eph.cis = getbitsInc(data, i, 18)*P2_31; - eph.i0 = getbitsInc(data, i, 32)*P2_31*SC2RAD; - eph.crc = getbitsInc(data, i, 18)*P2_6; - 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)*1E-10; - eph.tgd[1] = getbitsInc(data, i, 10)*1E-10; - int svh = getbituInc(data, i, 1); eph.svh = (E_Svh)svh; - - eph.Sat = SatSys(sys, prn); - - eph.iode = int(eph.tocs / 720) % 240; - eph.iodc = eph.iode + 256 * int(eph.tocs / 172800) % 4; - - GTime nearTime = rtcmTime(); - - eph.ttm = nearTime; - eph.toe = GTime(BTow(eph.toes), nearTime); - eph.toc = GTime(BTow(eph.tocs), nearTime); - } - else if (sys == +E_Sys::QZS) - { - if (i + 485-12 > data.size() * 8) - { - BOOST_LOG_TRIVIAL(error) << "Error: rtcm3 1044 length error: len=" << data.size(); - return; - } - - eph.type = E_NavMsgType::LNAV; - - int prn = getbituInc(data, i, 4); - eph.tocs = getbituInc(data, i, 16)*16.0; - eph.f2 = getbitsInc(data, i, 8)*P2_55; - eph.f1 = getbitsInc(data, i, 16)*P2_43; - eph.f0 = getbitsInc(data, i, 22)*P2_31; - eph.iode = getbituInc(data, i, 8); - 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, 16)*16.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.idot = getbitsInc(data, i, 14)*P2_43*SC2RAD; - eph.code = getbituInc(data, i, 2); - eph.weekRollOver= getbituInc(data, i, 10); eph.week = adjGpsWeek(eph.weekRollOver); // rolled-over week -> full week number - eph.sva = getbituInc(data, i, 4); eph.ura[0] = svaToUra(eph.sva); - int svh = getbituInc(data, i, 6); eph.svh = (E_Svh)svh; - eph.tgd[0] = getbitsInc(data, i, 8)*P2_31; - eph.iodc = getbituInc(data, i, 10); - eph.fitFlag = getbituInc(data, i, 1); eph.fit = eph.fitFlag?0.0:2.0; // 0:2hr,1:>2hr - - eph.Sat = SatSys(sys, prn); - - GTime nearTime = rtcmTime(); - - eph.ttm = nearTime; - eph.toe = GTime(GTow(eph.toes), nearTime); - eph.toc = GTime(GTow(eph.tocs), nearTime); - if (acsConfig.use_tgd_bias) - decomposeTGDBias(eph.Sat, eph.tgd[0]); - } - else if (sys == +E_Sys::GAL) - { - if (messageNumber == RtcmMessageType::GAL_FNAV_EPHEMERIS ) - { - if (i + 496-12 > data.size() * 8) - { - BOOST_LOG_TRIVIAL(error) << "Error: rtcm3 1045 length error: len=" << data.size(); - return; - } - - eph.type = E_NavMsgType::FNAV; - } - else if (messageNumber == RtcmMessageType::GAL_INAV_EPHEMERIS) - { - if (i + 504-12 > data.size() * 8) - { - BOOST_LOG_TRIVIAL(error) << "Error: rtcm3 1046 length error: len=" << data.size(); - return; - } - - eph.type = E_NavMsgType::INAV; - } - else - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised message for GAL in " << __FUNCTION__; - } - - 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; - - if (messageNumber == RtcmMessageType::GAL_FNAV_EPHEMERIS) - { - eph.e5a_hs = getbituInc(data, i, 2); // OSHS - eph.e5a_dvs = getbituInc(data, i, 1); // OSDVS - // eph.rsv = getbituInc(data, i, 7); - - int svh = (eph.e5a_hs << 4) - + (eph.e5a_dvs << 3); - eph.svh = (E_Svh)svh; - eph.code = (1<<1)+(1<<8); // data source = F/NAV+E5a - } - else if (messageNumber == RtcmMessageType::GAL_INAV_EPHEMERIS) - { - eph.tgd[1] = getbitsInc(data, i, 10)*P2_32; // E5b/E1 - eph.e5b_hs = getbituInc(data, i, 2); // E5b OSHS - eph.e5b_dvs = getbituInc(data, i, 1); // E5b OSDVS - eph.e1_hs = getbituInc(data, i, 2); // E1 OSHS - eph.e1_dvs = getbituInc(data, i, 1); // E1 OSDVS - - int svh = (eph.e5b_hs << 7) - + (eph.e5b_dvs << 6) - + (eph.e1_hs << 1) - + (eph.e1_dvs << 0); - eph.svh = (E_Svh)svh; - eph.code = (1<<0)+(1<<2)+(1<<9); // data source = I/NAV+E1+E5b - - if (acsConfig.use_tgd_bias) - decomposeBGDBias(eph.Sat, eph.tgd[0], eph.tgd[1]); - } - - eph.Sat = SatSys(sys, prn); - - GTime nearTime = rtcmTime(); - - eph.ttm = nearTime; - eph.toe = GTime(GTow(eph.toes), nearTime); - eph.toc = GTime(GTow(eph.tocs), nearTime); - } - else - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system in " << __FUNCTION__; - return; - } - - if ( sys == +E_Sys::GPS - ||sys == +E_Sys::GAL - ||sys == +E_Sys::BDS - ||sys == +E_Sys::QZS) - { - nav.ephMap[eph.Sat][eph.type][eph.toe] = eph; - - traceTrivialDebug("#RTCM_BRD EPHEMR %s %s %d", eph.Sat.id().c_str(), eph.toe.to_string().c_str(), eph.iode); - - if (acsConfig.output_decoded_rtcm_json) - traceBrdcEph(messCode, eph); - } - else if (sys == +E_Sys::GLO) - { - nav.gephMap[geph.Sat][geph.type][geph.toe] = geph; - - traceTrivialDebug("#RTCM_BRD EPHEMR %s %s %d", geph.Sat.id().c_str(), geph.toe.to_string().c_str(), geph.iode); - - if (acsConfig.output_decoded_rtcm_json) - traceBrdcEph(messCode, geph); - } - else - { - if (acsConfig.output_decoded_rtcm_json) - traceUnknown(); - } + Eph eph = {}; + Geph geph = {}; + int i = 0; + + int messageNumber = getbituInc(data, i, 12); + + RtcmMessageType messCode; + try + { + messCode = messageNumberToRtcmType(messageNumber); + } + catch (...) + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised message in " << __FUNCTION__; + return; + } + + E_Sys sys = E_Sys::NONE; + switch (messCode) + { + case RtcmMessageType::GPS_EPHEMERIS: + sys = E_Sys::GPS; + break; + case RtcmMessageType::GLO_EPHEMERIS: + sys = E_Sys::GLO; + break; + case RtcmMessageType::BDS_EPHEMERIS: + sys = E_Sys::BDS; + break; + case RtcmMessageType::QZS_EPHEMERIS: + sys = E_Sys::QZS; + break; + case RtcmMessageType::GAL_FNAV_EPHEMERIS: // fallthrough + case RtcmMessageType::GAL_INAV_EPHEMERIS: + sys = E_Sys::GAL; + break; + default: + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised message in " << __FUNCTION__; + return; + } + } + + bool failure = false; + + if (sys == E_Sys::GPS) + { + if (i + 488 - 12 > data.size() * 8) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1019 length error: len=" << data.size(); + return; + } + + eph.type = E_NavMsgType::LNAV; + + int prn = getbituInc(data, i, 6); + eph.weekRollOver = getbituInc(data, i, 10); + eph.week = adjGpsWeek(eph.weekRollOver); // rolled-over week -> full week number + eph.sva = getbituInc(data, i, 4); + eph.ura[0] = svaToUra(eph.sva); + eph.code = getbituInc(data, i, 2); + eph.idot = getbitsInc(data, i, 14) * P2_43 * SC2RAD; + eph.iode = getbituInc(data, i, 8); + eph.tocs = getbituInc(data, i, 16) * 16.0; + eph.f2 = getbitsInc(data, i, 8) * P2_55; + eph.f1 = getbitsInc(data, i, 16) * P2_43; + eph.f0 = getbitsInc(data, i, 22) * P2_31; + eph.iodc = getbituInc(data, i, 10); + 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, 16) * 16.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, 8) * P2_31; + int svh = getbituInc(data, i, 6); + eph.svh = (E_Svh)svh; + eph.flag = getbituInc(data, i, 1); + eph.fitFlag = getbituInc(data, i, 1); + eph.fit = eph.fitFlag ? 0.0 : 4.0; // 0:4hr,1:>4hr + + if (prn >= 40) + { + sys = E_Sys::SBS; + prn += 80; + } + eph.Sat = SatSys(sys, prn); + + GTime nearTime = rtcmTime(); + + eph.ttm = nearTime; + eph.toe = GTime(GTow(eph.toes), nearTime); + eph.toc = GTime(GTow(eph.tocs), nearTime); + + if (acsConfig.use_tgd_bias) + decomposeTGDBias(eph.Sat, eph.tgd[0]); + } + else if (sys == E_Sys::GLO) + { + if (i + 360 - 12 > data.size() * 8) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1020 length error: len=" << data.size(); + return; + } + + geph.type = E_NavMsgType::FDMA; + + int prn = getbituInc(data, i, 6); + geph.frq = getbituInc(data, i, 5) - 7; + i += 4; // skip DF104, 105, 106 (almanac health, P1) + geph.tk_hour = getbituInc(data, i, 5); + geph.tk_min = getbituInc(data, i, 6); + geph.tk_sec = getbituInc(data, i, 1) * 30; + int svh = getbituInc(data, i, 1); + geph.svh = (E_Svh)svh; + + int dummy = getbituInc(data, i, 1); // skip DF109 (P2) + geph.tb = getbituInc(data, i, 7); + geph.iode = geph.tb; + geph.vel[0] = getbitgInc(data, i, 24) * P2_20 * 1E3; + geph.pos[0] = getbitgInc(data, i, 27) * P2_11 * 1E3; + geph.acc[0] = getbitgInc(data, i, 5) * P2_30 * 1E3; + geph.vel[1] = getbitgInc(data, i, 24) * P2_20 * 1E3; + geph.pos[1] = getbitgInc(data, i, 27) * P2_11 * 1E3; + geph.acc[1] = getbitgInc(data, i, 5) * P2_30 * 1E3; + geph.vel[2] = getbitgInc(data, i, 24) * P2_20 * 1E3; + geph.pos[2] = getbitgInc(data, i, 27) * P2_11 * 1E3; + geph.acc[2] = getbitgInc(data, i, 5) * P2_30 * 1E3; + dummy = getbituInc(data, i, 1); // skip DF120 (P3) + geph.gammaN = getbitgInc(data, i, 11) * P2_40; + dummy = getbituInc(data, i, 3); // skip DF122, 123 (P, ln) + geph.taun = getbitgInc(data, i, 22) * P2_30; + geph.dtaun = getbitgInc(data, i, 5) * P2_30; + geph.age = getbituInc(data, i, 5); + dummy = getbituInc(data, i, 5); // skip DF127, 128 (P4, FT) + geph.NT = getbituInc(data, i, 11); // GLONASS-M only, may be arbitrary value + geph.glonassM = getbituInc(data, i, 2); // if GLONASS-M data feilds valid + geph.moreData = getbituInc(data, i, 1); // availability of additional data + i += 43; // skip DF132, 133 (NA, tauc) + geph.N4 = + getbituInc(data, i, 5); // additional data and GLONASS-M only, may be arbitrary value + + geph.Sat = SatSys(sys, prn); + + RTod toes = geph.tb * 15 * 60; + + RTod tofs = geph.tk_hour * 60 * 60 + geph.tk_min * 60 + geph.tk_sec; + + GTime nearTime = rtcmTime(); + geph.toe = GTime(toes, nearTime); + geph.tof = GTime(tofs, nearTime); + } + else if (sys == E_Sys::BDS) + { + if (i + 511 - 12 > data.size() * 8) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1042 length error: len=" << data.size(); + return; + } + + eph.type = E_NavMsgType::D1; + + int prn = getbituInc(data, i, 6); + eph.weekRollOver = getbituInc(data, i, 13); + eph.week = adjBdtWeek(eph.weekRollOver); // rolled-over week -> full week number + eph.sva = getbituInc(data, i, 4); + eph.ura[0] = svaToUra(eph.sva); + eph.idot = getbitsInc(data, i, 14) * P2_43 * SC2RAD; + eph.aode = getbituInc(data, i, 5); + eph.tocs = getbituInc(data, i, 17) * 8.0; + eph.f2 = getbitsInc(data, i, 11) * P2_66; + eph.f1 = getbitsInc(data, i, 22) * P2_50; + eph.f0 = getbitsInc(data, i, 24) * P2_33; + eph.aodc = getbituInc(data, i, 5); + eph.crs = getbitsInc(data, i, 18) * P2_6; + eph.deln = getbitsInc(data, i, 16) * P2_43 * SC2RAD; + eph.M0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; + eph.cuc = getbitsInc(data, i, 18) * P2_31; + eph.e = getbituInc(data, i, 32) * P2_33; + eph.cus = getbitsInc(data, i, 18) * P2_31; + eph.sqrtA = getbituInc(data, i, 32) * P2_19; + eph.A = SQR(eph.sqrtA); + eph.toes = getbituInc(data, i, 17) * 8.0; + eph.cic = getbitsInc(data, i, 18) * P2_31; + eph.OMG0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; + eph.cis = getbitsInc(data, i, 18) * P2_31; + eph.i0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; + eph.crc = getbitsInc(data, i, 18) * P2_6; + 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) * 1E-10; + eph.tgd[1] = getbitsInc(data, i, 10) * 1E-10; + int svh = getbituInc(data, i, 1); + eph.svh = (E_Svh)svh; + + eph.Sat = SatSys(sys, prn); + + eph.iode = int(eph.tocs / 720) % 240; + eph.iodc = eph.iode + 256 * int(eph.tocs / 172800) % 4; + + GTime nearTime = rtcmTime(); + + eph.ttm = nearTime; + eph.toe = GTime(BTow(eph.toes), nearTime); + eph.toc = GTime(BTow(eph.tocs), nearTime); + } + else if (sys == E_Sys::QZS) + { + if (i + 485 - 12 > data.size() * 8) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1044 length error: len=" << data.size(); + return; + } + + eph.type = E_NavMsgType::LNAV; + + int prn = getbituInc(data, i, 4); + eph.tocs = getbituInc(data, i, 16) * 16.0; + eph.f2 = getbitsInc(data, i, 8) * P2_55; + eph.f1 = getbitsInc(data, i, 16) * P2_43; + eph.f0 = getbitsInc(data, i, 22) * P2_31; + eph.iode = getbituInc(data, i, 8); + 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, 16) * 16.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.idot = getbitsInc(data, i, 14) * P2_43 * SC2RAD; + eph.code = getbituInc(data, i, 2); + eph.weekRollOver = getbituInc(data, i, 10); + eph.week = adjGpsWeek(eph.weekRollOver); // rolled-over week -> full week number + eph.sva = getbituInc(data, i, 4); + eph.ura[0] = svaToUra(eph.sva); + int svh = getbituInc(data, i, 6); + eph.svh = (E_Svh)svh; + eph.tgd[0] = getbitsInc(data, i, 8) * P2_31; + eph.iodc = getbituInc(data, i, 10); + eph.fitFlag = getbituInc(data, i, 1); + eph.fit = eph.fitFlag ? 0.0 : 2.0; // 0:2hr,1:>2hr + + eph.Sat = SatSys(sys, prn); + + GTime nearTime = rtcmTime(); + + eph.ttm = nearTime; + eph.toe = GTime(GTow(eph.toes), nearTime); + eph.toc = GTime(GTow(eph.tocs), nearTime); + if (acsConfig.use_tgd_bias) + decomposeTGDBias(eph.Sat, eph.tgd[0]); + } + else if (sys == E_Sys::GAL) + { + if (messCode == RtcmMessageType::GAL_FNAV_EPHEMERIS) + { + if (i + 496 - 12 > data.size() * 8) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1045 length error: len=" << data.size(); + return; + } + + eph.type = E_NavMsgType::FNAV; + } + else if (messCode == RtcmMessageType::GAL_INAV_EPHEMERIS) + { + if (i + 504 - 12 > data.size() * 8) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1046 length error: len=" << data.size(); + return; + } + + eph.type = E_NavMsgType::INAV; + } + else + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised message for GAL in " << __FUNCTION__; + } + + 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; + + if (messCode == RtcmMessageType::GAL_FNAV_EPHEMERIS) + { + eph.e5a_hs = getbituInc(data, i, 2); // OSHS + eph.e5a_dvs = getbituInc(data, i, 1); // OSDVS + // eph.rsv = getbituInc(data, i, 7); + + int svh = (eph.e5a_hs << 4) + (eph.e5a_dvs << 3); + eph.svh = (E_Svh)svh; + eph.code = (1 << 1) + (1 << 8); // data source = F/NAV+E5a + } + else if (messCode == RtcmMessageType::GAL_INAV_EPHEMERIS) + { + eph.tgd[1] = getbitsInc(data, i, 10) * P2_32; // E5b/E1 + eph.e5b_hs = getbituInc(data, i, 2); // E5b OSHS + eph.e5b_dvs = getbituInc(data, i, 1); // E5b OSDVS + eph.e1_hs = getbituInc(data, i, 2); // E1 OSHS + eph.e1_dvs = getbituInc(data, i, 1); // E1 OSDVS + + int svh = (eph.e5b_hs << 7) + (eph.e5b_dvs << 6) + (eph.e1_hs << 1) + (eph.e1_dvs << 0); + eph.svh = (E_Svh)svh; + eph.code = (1 << 0) + (1 << 2) + (1 << 9); // data source = I/NAV+E1+E5b + + if (acsConfig.use_tgd_bias) + decomposeBGDBias(eph.Sat, eph.tgd[0], eph.tgd[1]); + } + + eph.Sat = SatSys(sys, prn); + + GTime nearTime = rtcmTime(); + + eph.ttm = nearTime; + eph.toe = GTime(GTow(eph.toes), nearTime); + eph.toc = GTime(GTow(eph.tocs), nearTime); + } + else + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised system in " << __FUNCTION__; + return; + } + + if (sys == E_Sys::GPS || sys == E_Sys::GAL || sys == E_Sys::BDS || sys == E_Sys::QZS) + { + nav.ephMap[eph.Sat][eph.type][eph.toe] = eph; + + traceTrivialDebug( + "#RTCM_BRD EPHEMR %s %s %d", + eph.Sat.id().c_str(), + eph.toe.to_string().c_str(), + eph.iode + ); + + if (acsConfig.output_decoded_rtcm_json) + traceBrdcEph(messCode, eph); + } + else if (sys == E_Sys::GLO) + { + nav.gephMap[geph.Sat][geph.type][geph.toe] = geph; + + traceTrivialDebug( + "#RTCM_BRD EPHEMR %s %s %d", + geph.Sat.id().c_str(), + geph.toe.to_string().c_str(), + geph.iode + ); + + if (acsConfig.output_decoded_rtcm_json) + traceBrdcEph(messCode, geph); + } + else + { + if (acsConfig.output_decoded_rtcm_json) + traceUnknown(); + } } - // sys -> rtcm signal enum -> siginfo (sig enum,// From the RTCM spec... // - table 3.5-91 (GPS) // - table 3.5-96 (GLONASS) // - table 3.5-99 (GALILEO) // - table 3.5-105 (QZSS) // - table 3.5-108 (BEIDOU) -map> sysIdSignalMapMap = -{ - { E_Sys::GPS, - { - {2, {2, F1, E_ObsCode::L1C}}, - {3, {3, F1, E_ObsCode::L1P}}, - {4, {4, F1, E_ObsCode::L1W}}, - - {8, {8, F2, E_ObsCode::L2C}}, - {9, {9, F2, E_ObsCode::L2P}}, - {10, {10, F2, E_ObsCode::L2W}}, - {15, {15, F2, E_ObsCode::L2S}}, - {16, {16, F2, E_ObsCode::L2L}}, - {17, {17, F2, E_ObsCode::L2X}}, - - {22, {22, F5, E_ObsCode::L5I}}, - {23, {23, F5, E_ObsCode::L5Q}}, - {24, {24, F5, E_ObsCode::L5X}}, - - {30, {2, F1, E_ObsCode::L1S}}, - {31, {2, F1, E_ObsCode::L1L}}, - {32, {2, F1, E_ObsCode::L1X}} - } - }, - - { E_Sys::GLO, - { - {2, {2, G1, E_ObsCode::L1C}}, - {3, {3, G1, E_ObsCode::L1P}}, - - {8, {8, G2, E_ObsCode::L2C}}, - {9, {9, G2, E_ObsCode::L2P}} - } - }, - - { E_Sys::GAL, - { - {2, {2, F1, E_ObsCode::L1C}}, - {3, {3, F1, E_ObsCode::L1A}}, - {4, {4, F1, E_ObsCode::L1B}}, - {5, {5, F1, E_ObsCode::L1X}}, - {6, {6, F1, E_ObsCode::L1Z}}, - - {8, {8, F6, E_ObsCode::L6C}}, - {9, {9, F6, E_ObsCode::L6A}}, - {10, {10, F6, E_ObsCode::L6B}}, - {11, {11, F6, E_ObsCode::L6X}}, - {12, {12, F6, E_ObsCode::L6Z}}, - - {14, {14, F7, E_ObsCode::L7I}}, - {15, {15, F7, E_ObsCode::L7Q}}, - {16, {16, F7, E_ObsCode::L7X}}, - - {18, {18, F8, E_ObsCode::L8I}}, - {19, {19, F8, E_ObsCode::L8Q}}, - {20, {20, F8, E_ObsCode::L8X}}, - - {22, {22, F5, E_ObsCode::L5I}}, - {23, {23, F5, E_ObsCode::L5Q}}, - {24, {24, F5, E_ObsCode::L5X}} - } - }, - - { E_Sys::QZS, - { - {2, {2, F1, E_ObsCode::L1C}}, - - {9, {9, F6, E_ObsCode::L6S}}, - {10, {10, F6, E_ObsCode::L6L}}, - {11, {11, F6, E_ObsCode::L6X}}, - - {15, {15, F2, E_ObsCode::L2S}}, - {16, {16, F2, E_ObsCode::L2L}}, - {17, {17, F2, E_ObsCode::L2X}}, - - {22, {22, F5, E_ObsCode::L5I}}, - {23, {23, F5, E_ObsCode::L5Q}}, - {24, {24, F5, E_ObsCode::L5X}}, - - {30, {30, F1, E_ObsCode::L1S}}, - {31, {31, F1, E_ObsCode::L1L}}, - {32, {32, F1, E_ObsCode::L1X}} - } - }, - - { E_Sys::BDS, - { - {2, {2, B1, E_ObsCode::L2I}}, - {3, {3, B1, E_ObsCode::L2Q}}, - {4, {4, B1, E_ObsCode::L2X}}, - - {8, {8, B3, E_ObsCode::L6I}}, - {9, {9, B3, E_ObsCode::L6Q}}, - {10, {10, B3, E_ObsCode::L6X}}, - - {14, {14, F7, E_ObsCode::L7I}}, - {15, {15, F7, E_ObsCode::L7Q}}, - {16, {16, F7, E_ObsCode::L7X}}, - - {22, {22, F5, E_ObsCode::L5D}}, - {23, {23, F5, E_ObsCode::L5P}}, - {24, {24, F5, E_ObsCode::L5X}}, - {25, {25, F7, E_ObsCode::L7D}}, - - {30, {30, F1, E_ObsCode::L1D}}, - {31, {31, F1, E_ObsCode::L1P}}, - {32, {32, F1, E_ObsCode::L1X}} - } - } +map> sysIdSignalMapMap = { + {E_Sys::GPS, + {{2, {2, F1, E_ObsCode::L1C}}, + {3, {3, F1, E_ObsCode::L1P}}, + {4, {4, F1, E_ObsCode::L1W}}, + + {8, {8, F2, E_ObsCode::L2C}}, + {9, {9, F2, E_ObsCode::L2P}}, + {10, {10, F2, E_ObsCode::L2W}}, + {15, {15, F2, E_ObsCode::L2S}}, + {16, {16, F2, E_ObsCode::L2L}}, + {17, {17, F2, E_ObsCode::L2X}}, + + {22, {22, F5, E_ObsCode::L5I}}, + {23, {23, F5, E_ObsCode::L5Q}}, + {24, {24, F5, E_ObsCode::L5X}}, + + {30, {2, F1, E_ObsCode::L1S}}, + {31, {2, F1, E_ObsCode::L1L}}, + {32, {2, F1, E_ObsCode::L1X}}}}, + + {E_Sys::GLO, + {{2, {2, G1, E_ObsCode::L1C}}, + {3, {3, G1, E_ObsCode::L1P}}, + + {8, {8, G2, E_ObsCode::L2C}}, + {9, {9, G2, E_ObsCode::L2P}}}}, + + {E_Sys::GAL, + {{2, {2, F1, E_ObsCode::L1C}}, + {3, {3, F1, E_ObsCode::L1A}}, + {4, {4, F1, E_ObsCode::L1B}}, + {5, {5, F1, E_ObsCode::L1X}}, + {6, {6, F1, E_ObsCode::L1Z}}, + + {8, {8, F6, E_ObsCode::L6C}}, + {9, {9, F6, E_ObsCode::L6A}}, + {10, {10, F6, E_ObsCode::L6B}}, + {11, {11, F6, E_ObsCode::L6X}}, + {12, {12, F6, E_ObsCode::L6Z}}, + + {14, {14, F7, E_ObsCode::L7I}}, + {15, {15, F7, E_ObsCode::L7Q}}, + {16, {16, F7, E_ObsCode::L7X}}, + + {18, {18, F8, E_ObsCode::L8I}}, + {19, {19, F8, E_ObsCode::L8Q}}, + {20, {20, F8, E_ObsCode::L8X}}, + + {22, {22, F5, E_ObsCode::L5I}}, + {23, {23, F5, E_ObsCode::L5Q}}, + {24, {24, F5, E_ObsCode::L5X}}}}, + + {E_Sys::QZS, + {{2, {2, F1, E_ObsCode::L1C}}, + + {9, {9, F6, E_ObsCode::L6S}}, + {10, {10, F6, E_ObsCode::L6L}}, + {11, {11, F6, E_ObsCode::L6X}}, + + {15, {15, F2, E_ObsCode::L2S}}, + {16, {16, F2, E_ObsCode::L2L}}, + {17, {17, F2, E_ObsCode::L2X}}, + + {22, {22, F5, E_ObsCode::L5I}}, + {23, {23, F5, E_ObsCode::L5Q}}, + {24, {24, F5, E_ObsCode::L5X}}, + + {30, {30, F1, E_ObsCode::L1S}}, + {31, {31, F1, E_ObsCode::L1L}}, + {32, {32, F1, E_ObsCode::L1X}}}}, + + {E_Sys::BDS, + {{2, {2, B1, E_ObsCode::L2I}}, + {3, {3, B1, E_ObsCode::L2Q}}, + {4, {4, B1, E_ObsCode::L2X}}, + + {8, {8, B3, E_ObsCode::L6I}}, + {9, {9, B3, E_ObsCode::L6Q}}, + {10, {10, B3, E_ObsCode::L6X}}, + + {14, {14, F7, E_ObsCode::L7I}}, + {15, {15, F7, E_ObsCode::L7Q}}, + {16, {16, F7, E_ObsCode::L7X}}, + + {22, {22, F5, E_ObsCode::L5D}}, + {23, {23, F5, E_ObsCode::L5P}}, + {24, {24, F5, E_ObsCode::L5X}}, + {25, {25, F7, E_ObsCode::L7D}}, + + {30, {30, F1, E_ObsCode::L1D}}, + {31, {31, F1, E_ObsCode::L1P}}, + {32, {32, F1, E_ObsCode::L1X}}}} }; E_ObsCode RtcmDecoder::signal_to_code(E_Sys sys, uint8_t signal) { - auto it1 = sysIdSignalMapMap.find(sys); - if (it1 == sysIdSignalMapMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: unrecognised system in " << __FUNCTION__ << ": mountpoint=" << rtcmMountpoint << " sys=" << sys; + auto it1 = sysIdSignalMapMap.find(sys); + if (it1 == sysIdSignalMapMap.end()) + { + BOOST_LOG_TRIVIAL(warning) << "Unrecognised system in " << __FUNCTION__ + << ": mountpoint=" << rtcmMountpoint << " sys=" << sys; - return E_ObsCode::NONE; - } + return E_ObsCode::NONE; + } - auto& [dummy1, idSignalMap] = *it1; + auto& [dummy1, idSignalMap] = *it1; - auto it2 = idSignalMap.find(signal); - if (it2 == idSignalMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: unrecognised signal in " << __FUNCTION__ << ": mountpoint=" << rtcmMountpoint << " sys=" << sys << " signal=" << (int)signal; + auto it2 = idSignalMap.find(signal); + if (it2 == idSignalMap.end()) + { + BOOST_LOG_TRIVIAL(warning) + << "Unrecognised signal in " << __FUNCTION__ << ": mountpoint=" << rtcmMountpoint + << " sys=" << sys << " signal=" << (int)signal; - return E_ObsCode::NONE; - } + return E_ObsCode::NONE; + } - auto& [dummy2, sigInfo] = *it2; + auto& [dummy2, sigInfo] = *it2; - return sigInfo.obsCode; + return sigInfo.obsCode; } -const int defaultGloChannel[] = { 0, 1, -4, 5, 6, 1, -4, 5, 6, -2, -7, 0, -1, -2, -7, 0, -1, 4, -3, 3, 2, 4, -3, 3, 2 }; +const int defaultGloChannel[] = {0, 1, -4, 5, 6, 1, -4, 5, 6, -2, -7, 0, -1, + -2, -7, 0, -1, 4, -3, 3, 2, 4, -3, 3, 2}; -double lockTimeFromIndicator( - int indicator) +double lockTimeFromIndicator(int indicator) { - if (indicator == 0) - { - return 0; - } + if (indicator == 0) + { + return 0; + } - double lockTime = pow(2, 4 + indicator) / 1000.0; + double lockTime = pow(2, 4 + indicator) / 1000.0; - return lockTime; + return lockTime; } -double highResLockTimeFromIndicator( - int indicator) +double highResLockTimeFromIndicator(int indicator) { - int i = indicator; - double lockTime = 0; - if (i < 64) lockTime = 1 * i - 0; - else if (i < 96) lockTime = 2 * i - 64; - else if (i < 128) lockTime = 4 * i - 256; - else if (i < 160) lockTime = 8 * i - 768; - else if (i < 192) lockTime = 16 * i - 2048; - else if (i < 224) lockTime = 32 * i - 5120; - else if (i < 256) lockTime = 64 * i - 12288; - else if (i < 288) lockTime = 128 * i - 28672; - else if (i < 320) lockTime = 256 * i - 65536; - else if (i < 352) lockTime = 512 * i - 147456; - else if (i < 384) lockTime = 1024 * i - 327680; - else if (i < 416) lockTime = 2048 * i - 720896; - else if (i < 448) lockTime = 4096 * i - 1572864; - else if (i < 480) lockTime = 8192 * i - 3407872; - else if (i < 512) lockTime = 16384 * i - 7340032; - else if (i < 544) lockTime = 32768 * i - 15728640; - else if (i < 576) lockTime = 65536 * i - 33554432; - else if (i < 608) lockTime = 131072 * i - 71303168; - else if (i < 640) lockTime = 262144 * i - 150994944; - else if (i < 672) lockTime = 524288 * i - 318767104; - else if (i < 704) lockTime = 1048576 * i - 671088640; - else if (i== 704) lockTime = 2097152 * i - 1409286144; - else - BOOST_LOG_TRIVIAL(warning) << "Warning: High res lock time out of bounds: " << lockTime; - - lockTime /= 1000; - - return lockTime; + int i = indicator; + double lockTime = 0; + if (i < 64) + lockTime = 1 * i - 0; + else if (i < 96) + lockTime = 2 * i - 64; + else if (i < 128) + lockTime = 4 * i - 256; + else if (i < 160) + lockTime = 8 * i - 768; + else if (i < 192) + lockTime = 16 * i - 2048; + else if (i < 224) + lockTime = 32 * i - 5120; + else if (i < 256) + lockTime = 64 * i - 12288; + else if (i < 288) + lockTime = 128 * i - 28672; + else if (i < 320) + lockTime = 256 * i - 65536; + else if (i < 352) + lockTime = 512 * i - 147456; + else if (i < 384) + lockTime = 1024 * i - 327680; + else if (i < 416) + lockTime = 2048 * i - 720896; + else if (i < 448) + lockTime = 4096 * i - 1572864; + else if (i < 480) + lockTime = 8192 * i - 3407872; + else if (i < 512) + lockTime = 16384 * i - 7340032; + else if (i < 544) + lockTime = 32768 * i - 15728640; + else if (i < 576) + lockTime = 65536 * i - 33554432; + else if (i < 608) + lockTime = 131072 * i - 71303168; + else if (i < 640) + lockTime = 262144 * i - 150994944; + else if (i < 672) + lockTime = 524288 * i - 318767104; + else if (i < 704) + lockTime = 1048576 * i - 671088640; + else if (i == 704) + lockTime = 2097152 * i - 1409286144; + else + BOOST_LOG_TRIVIAL(warning) << "High res lock time out of bounds: " << lockTime; + + lockTime /= 1000; + + return lockTime; } -ObsList RtcmDecoder::decodeMSM( - vector& data) +ObsList RtcmDecoder::decodeMSM(vector& data) { - ObsList obsList; - int i = 0; - - int messageNumber = getbituInc(data, i, 12); - int reference_station_id = getbituInc(data, i, 12); - int epoch_time_ = getbituInc(data, i, 30); - int multiple_message = getbituInc(data, i, 1); - int issue_of_data_station = getbituInc(data, i, 3); - int reserved = getbituInc(data, i, 7); - int clock_steering_indicator = getbituInc(data, i, 2); - int external_clock_indicator = getbituInc(data, i, 2); - int smoothing_indicator = getbituInc(data, i, 1); - int smoothing_interval = getbituInc(data, i, 3); - - RtcmMessageType messCode; - try - { - messCode = RtcmMessageType::_from_integral(messageNumber); - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised message in " << __FUNCTION__; - return obsList; - } - - int msmtyp = messageNumber % 10; - int lsat = 0; - int lcell = 0; - switch (msmtyp) - { - case 4: lsat = 18; lcell = 48; break; - case 5: lsat = 36; lcell = 63; break; - case 6: lsat = 18; lcell = 65; break; - case 7: lsat = 36; lcell = 80; break; - default: BOOST_LOG_TRIVIAL(error) << "Error: unrecognised message in " << __FUNCTION__; return obsList; - } - - bool extrainfo = false; - if ( msmtyp == 5 - || msmtyp == 7) - { - extrainfo = true; - } - - int nbcd = 15; - int nbph = 22; - int nblk = 4; - int nbcn = 6; - double sccd = P2_24; - double scph = P2_29; - double scsn = 1; - if ( msmtyp == 6 - || msmtyp == 7) - { - nbcd = 20; sccd = P2_29; - nbph = 24; scph = P2_31; - nblk = 10; - nbcn = 10; scsn = 0.0625; - } - - int sysind = messageNumber / 10; // integer division is intentional - E_Sys rtcmsys = E_Sys::NONE; - - - GTime nearTime = rtcmTime(); - - double tow = epoch_time_ * 0.001; - GTime tobs; - - switch (sysind) - { - case 107: rtcmsys = E_Sys::GPS; tobs = GTime(GTow(tow), nearTime); break; - case 108: rtcmsys = E_Sys::GLO; /*see below*/ break; - case 109: rtcmsys = E_Sys::GAL; tobs = GTime(GTow(tow), nearTime); break; - case 111: rtcmsys = E_Sys::QZS; tobs = GTime(GTow(tow), nearTime); break; - case 112: rtcmsys = E_Sys::BDS; tobs = GTime(BTow(tow), nearTime); break; - default: BOOST_LOG_TRIVIAL(error) << "Error: unrecognised message in " << __FUNCTION__; return obsList; - } - - if (rtcmsys == +E_Sys::GLO) - { - int dowi = (epoch_time_ >> 27); - int todi = (epoch_time_ & 0x7FFFFFF); - - RTod tk = 0.001 * todi; - tobs = GTime(tk, nearTime); - } - - traceLatency(tobs); - - //create observations for satellites according to the mask - int nsat = 0; - for (int sat = 0; sat < 64; sat++) - { - bool mask = getbituInc(data, i, 1); - if (mask == false) - { - continue; - } - - GObs obs; - obs.Sat = SatSys(rtcmsys, sat + 1); - obs.time = tobs; - - obsList.push_back((shared_ptr)obs); - -// std::cout << obs.time << " " << obs.Sat.id() << "\n"; - - nsat++; - } - - //create a temporary list of signals - vector signalMaskList; - for (int sig = 0; sig < 32; sig++) - { - bool mask = getbituInc(data, i, 1); - if (mask) - { - int code = signal_to_code(rtcmsys, sig + 1); - signalMaskList.push_back(E_ObsCode::_from_integral(code)); - } - } - - //create a temporary list of signal pointers for simpler iteration later - map signalPointermap; - map cellSatellitemap; - int ncell = 0; - //create signals for observations according to existing observations, the list of signals, and the cell mask - for (auto& obs : only(obsList)) - for (auto& sigNum : signalMaskList) - { - bool mask = getbituInc(data, i, 1); - if (mask == false) - { - continue; - } - - Sig sig; - sig.code = sigNum; - - E_FType ft = FTYPE_NONE; - 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 - { - ft = code2Freq[rtcmsys][sig.code]; - } - else - { - BOOST_LOG_TRIVIAL(warning) << "Warning: unrecognised signal in " << __FUNCTION__ << ": mountpoint=" << rtcmMountpoint << " messageNumber=" << messageNumber << " signal=" << sig.code; - } - } - else - { - BOOST_LOG_TRIVIAL(warning) << "Warning: unrecognised system in " << __FUNCTION__ << ": mountpoint=" << rtcmMountpoint << "messageNumber=" << messageNumber; - } - - obs.sigsLists[ft].push_back(sig); - - Sig* pointer = &obs.sigsLists[ft].back(); - signalPointermap[ncell] = pointer; - cellSatellitemap[ncell] = obs.Sat; - ncell++; - } - - if (i + nsat * lsat + ncell * lcell > data.size() * 8) - { - BOOST_LOG_TRIVIAL(error) << "Error: rtcm3 " << messageNumber << " length error: len=" << data.size(); - return obsList; - } - - //get satellite specific data - needs to be in chunks - for (auto& obs : only(obsList)) - { - int ms_rough_range = getbituInc(data, i, 8); - if (ms_rough_range == 255) - { - obs.excludeBadRange = true; - - continue; - } - - for (auto& [ft, sigList] : obs.sigsLists) - for (auto& sig : sigList) - { - sig.P = ms_rough_range; - sig.L = ms_rough_range; - } - } - - map> GLOFreqShift; - if (extrainfo) - { - for (auto& obs : only(obsList)) - { - int extended_sat_info = getbituInc(data, i, 4); - - if (rtcmsys == +E_Sys::GLO) - { - GLOFreqShift[obs.Sat][G1] = DFRQ1_GLO * (extended_sat_info-7); - GLOFreqShift[obs.Sat][G2] = DFRQ2_GLO * (extended_sat_info-7); - } - } - } - else if(rtcmsys == +E_Sys::GLO) - { - for (auto& obs : only(obsList)) - { - short int prn = obs.Sat.prn; - - if ( prn > 24 - || prn < 1) - { - GLOFreqShift[obs.Sat][G1] = 0; - GLOFreqShift[obs.Sat][G2] = 0; - } - else - { - GLOFreqShift[obs.Sat][G1] = DFRQ1_GLO * defaultGloChannel[prn]; - GLOFreqShift[obs.Sat][G2] = DFRQ2_GLO * defaultGloChannel[prn]; - } - } - } - - for (auto& obs : only(obsList)) - { - int rough_range_modulo = getbituInc(data, i, 10); - - for (auto& [ft, sigList] : obs.sigsLists) - for (auto& sig : sigList) - { - sig.P += rough_range_modulo * P2_10; - sig.L += rough_range_modulo * P2_10; - } - } - - if (extrainfo) - for (auto& obs : only(obsList)) - { - bool failure = false; - double rough_doppler = getbitsIncScale(data, i, 14, 1, &failure); - if (failure) - { - continue; - } - - for (auto& [ft, sigList] : obs.sigsLists) - for (auto& sig : sigList) - { - sig.D = rough_doppler; - } - } - - //get signal specific data - for (auto& [indx, signalPointer] : signalPointermap) - { - Sig& sig = *signalPointer; - - bool failure = false; - double fine_pseudorange = getbitsIncScale(data, i, nbcd, sccd, &failure); - if (failure) - { - sig.invalid = true; - continue; - } - - sig.P += fine_pseudorange; - } - - for (auto& [indx, signalPointer] : signalPointermap) - { - Sig& sig = *signalPointer; - - bool failure = false; - double fine_phase_range = getbitsIncScale(data, i, nbph, scph, &failure); - if (failure) - { - sig.invalid = true; - - continue; - } - - sig.L += fine_phase_range; - } - - for (auto& [indx, signalPointer] : signalPointermap) - { - int lockTimeIndicator = getbituInc(data, i, nblk); - - double lockTime = 0; - if ( msmtyp <= 5) lockTime = lockTimeFromIndicator(lockTimeIndicator); - else if ( msmtyp == 6 - || msmtyp == 7) lockTime = highResLockTimeFromIndicator(lockTimeIndicator); - - Sig& sig = *signalPointer; - SatSys& Sat = cellSatellitemap[indx]; - - if (lockTime < acsConfig.epoch_interval) sig.LLI = true; - else sig.LLI = false; - } - - for (auto& [indx, signalPointer] : signalPointermap) - { - int half_cycle_ambiguity = getbituInc(data, i, 1); - - Sig& sig = *signalPointer; - - if (half_cycle_ambiguity > 0) - sig.LLI = true; - } - - for (auto& [indx, signalPointer] : signalPointermap) - { - double carrier_noise_ratio = getbituIncScale(data, i, nbcn, scsn); - - Sig& sig = *signalPointer; - sig.snr = carrier_noise_ratio; - } - - if (extrainfo) - for (auto& [indx, signalPointer] : signalPointermap) - { - bool failure = false; - double fine_doppler = getbitsIncScale(data, i, 15, 0.0001, &failure); - - if (failure) - continue; - - Sig& sig = *signalPointer; - - sig.D += fine_doppler; - } - - - //convert millisecond or m/s measurements to meters or cycles or Hz - for (auto& obs : only(obsList)) - for (auto& [ft, sigList] : obs.sigsLists) - for (auto& sig : sigList) - { - double freqcy = carrierFrequency[ft]; - if (rtcmsys == +E_Sys::GLO) - freqcy += GLOFreqShift[obs.Sat][ft]; - - sig.P *= CLIGHT / 1000; // ms -> metre - sig.L *= freqcy / 1000; // ms -> cycle - sig.D *= -freqcy / CLIGHT; // m/s -> Hz - - - traceTrivialDebug("#RTCM_MSM OBSERV %s %s %d %s %.4f %.4f", obs.time.to_string().c_str(), obs.Sat.id().c_str(), ft, sig.code._to_string(), sig.P, sig.L); - - if (acsConfig.output_decoded_rtcm_json) - traceMSM(messCode, obs.time, obs.Sat, sig); - } - - - return obsList; + ObsList obsList; + int i = 0; + + int messageNumber = getbituInc(data, i, 12); + int reference_station_id = getbituInc(data, i, 12); + int epoch_time_ = getbituInc(data, i, 30); + int multiple_message = getbituInc(data, i, 1); + int issue_of_data_station = getbituInc(data, i, 3); + int reserved = getbituInc(data, i, 7); + int clock_steering_indicator = getbituInc(data, i, 2); + int external_clock_indicator = getbituInc(data, i, 2); + int smoothing_indicator = getbituInc(data, i, 1); + int smoothing_interval = getbituInc(data, i, 3); + + RtcmMessageType messCode; + try + { + messCode = messageNumberToRtcmType(messageNumber); + } + catch (...) + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised message in " << __FUNCTION__; + return obsList; + } + + int msmtyp = messageNumber % 10; + int lsat = 0; + int lcell = 0; + switch (msmtyp) + { + case 4: + lsat = 18; + lcell = 48; + break; + case 5: + lsat = 36; + lcell = 63; + break; + case 6: + lsat = 18; + lcell = 65; + break; + case 7: + lsat = 36; + lcell = 80; + break; + default: + BOOST_LOG_TRIVIAL(error) << "Unrecognised message in " << __FUNCTION__; + return obsList; + } + + bool extrainfo = false; + if (msmtyp == 5 || msmtyp == 7) + { + extrainfo = true; + } + + int nbcd = 15; + int nbph = 22; + int nblk = 4; + int nbcn = 6; + double sccd = P2_24; + double scph = P2_29; + double scsn = 1; + if (msmtyp == 6 || msmtyp == 7) + { + nbcd = 20; + sccd = P2_29; + nbph = 24; + scph = P2_31; + nblk = 10; + nbcn = 10; + scsn = 0.0625; + } + + int sysind = messageNumber / 10; // integer division is intentional + E_Sys rtcmsys = E_Sys::NONE; + + GTime nearTime = rtcmTime(); + + double tow = epoch_time_ * 0.001; + GTime tobs; + + switch (sysind) + { + case 107: + rtcmsys = E_Sys::GPS; + tobs = GTime(GTow(tow), nearTime); + break; + case 108: + rtcmsys = E_Sys::GLO; /*see below*/ + break; + case 109: + rtcmsys = E_Sys::GAL; + tobs = GTime(GTow(tow), nearTime); + break; + case 111: + rtcmsys = E_Sys::QZS; + tobs = GTime(GTow(tow), nearTime); + break; + case 112: + rtcmsys = E_Sys::BDS; + tobs = GTime(BTow(tow), nearTime); + break; + default: + BOOST_LOG_TRIVIAL(error) << "Unrecognised message in " << __FUNCTION__; + return obsList; + } + + if (rtcmsys == E_Sys::GLO) + { + int dowi = (epoch_time_ >> 27); + int todi = (epoch_time_ & 0x7FFFFFF); + + RTod tk = 0.001 * todi; + tobs = GTime(tk, nearTime); + } + + traceLatency(tobs); + + // create observations for satellites according to the mask + int nsat = 0; + for (int sat = 0; sat < 64; sat++) + { + bool mask = getbituInc(data, i, 1); + if (mask == false) + { + continue; + } + + GObs obs; + obs.Sat = SatSys(rtcmsys, sat + 1); + obs.time = tobs; + + obsList.push_back((shared_ptr)obs); + + // std::cout << obs.time << " " << obs.Sat.id() << "\n"; + + nsat++; + } + + // create a temporary list of signals + vector signalMaskList; + for (int sig = 0; sig < 32; sig++) + { + bool mask = getbituInc(data, i, 1); + if (mask) + { + E_ObsCode code = signal_to_code(rtcmsys, sig + 1); + signalMaskList.push_back(code); + } + } + + // create a temporary list of signal pointers for simpler iteration later + map signalPointermap; + map cellSatellitemap; + int ncell = 0; + // create signals for observations according to existing observations, the list of signals, and + // the cell mask + for (auto& obs : only(obsList)) + for (auto& sigNum : signalMaskList) + { + bool mask = getbituInc(data, i, 1); + if (mask == false) + { + continue; + } + + Sig sig; + sig.code = sigNum; + + E_FType ft = NONE; + 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 + { + ft = code2Freq[rtcmsys][sig.code]; + } + else + { + BOOST_LOG_TRIVIAL(warning) + << "Unrecognised signal in " << __FUNCTION__ + << ": mountpoint=" << rtcmMountpoint << " messageNumber=" << messageNumber + << " signal=" << sig.code; + } + } + else + { + BOOST_LOG_TRIVIAL(warning) + << "Unrecognised system in " << __FUNCTION__ + << ": mountpoint=" << rtcmMountpoint << "messageNumber=" << messageNumber; + } + + obs.sigsLists[ft].push_back(sig); + + Sig* pointer = &obs.sigsLists[ft].back(); + signalPointermap[ncell] = pointer; + cellSatellitemap[ncell] = obs.Sat; + ncell++; + } + + if (i + nsat * lsat + ncell * lcell > data.size() * 8) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 " << messageNumber + << " length error: len=" << data.size(); + return obsList; + } + + // get satellite specific data - needs to be in chunks + for (auto& obs : only(obsList)) + { + int ms_rough_range = getbituInc(data, i, 8); + if (ms_rough_range == 255) + { + obs.excludeBadRange = true; + + continue; + } + + for (auto& [ft, sigList] : obs.sigsLists) + for (auto& sig : sigList) + { + sig.P = ms_rough_range; + sig.L = ms_rough_range; + } + } + + map> GLOFreqShift; + if (extrainfo) + { + for (auto& obs : only(obsList)) + { + int extended_sat_info = getbituInc(data, i, 4); + + if (rtcmsys == E_Sys::GLO) + { + GLOFreqShift[obs.Sat][G1] = DFRQ1_GLO * (extended_sat_info - 7); + GLOFreqShift[obs.Sat][G2] = DFRQ2_GLO * (extended_sat_info - 7); + } + } + } + else if (rtcmsys == E_Sys::GLO) + { + for (auto& obs : only(obsList)) + { + short int prn = obs.Sat.prn; + + if (prn > 24 || prn < 1) + { + GLOFreqShift[obs.Sat][G1] = 0; + GLOFreqShift[obs.Sat][G2] = 0; + } + else + { + GLOFreqShift[obs.Sat][G1] = DFRQ1_GLO * defaultGloChannel[prn]; + GLOFreqShift[obs.Sat][G2] = DFRQ2_GLO * defaultGloChannel[prn]; + } + } + } + + for (auto& obs : only(obsList)) + { + int rough_range_modulo = getbituInc(data, i, 10); + + for (auto& [ft, sigList] : obs.sigsLists) + for (auto& sig : sigList) + { + sig.P += rough_range_modulo * P2_10; + sig.L += rough_range_modulo * P2_10; + } + } + + if (extrainfo) + for (auto& obs : only(obsList)) + { + bool failure = false; + double rough_doppler = getbitsIncScale(data, i, 14, 1, &failure); + if (failure) + { + continue; + } + + for (auto& [ft, sigList] : obs.sigsLists) + for (auto& sig : sigList) + { + sig.D = rough_doppler; + } + } + + // get signal specific data + for (auto& [indx, signalPointer] : signalPointermap) + { + Sig& sig = *signalPointer; + + bool failure = false; + double fine_pseudorange = getbitsIncScale(data, i, nbcd, sccd, &failure); + if (failure) + { + sig.invalid = true; // Eugene: not used? + continue; + } + + sig.P += fine_pseudorange; + } + + for (auto& [indx, signalPointer] : signalPointermap) + { + Sig& sig = *signalPointer; + + bool failure = false; + double fine_phase_range = getbitsIncScale(data, i, nbph, scph, &failure); + if (failure) + { + sig.invalid = true; // Eugene: not used? + + continue; + } + + sig.L += fine_phase_range; + } + + for (auto& [indx, signalPointer] : signalPointermap) + { + int lockTimeIndicator = getbituInc(data, i, nblk); + + double lockTime = 0; + if (msmtyp <= 5) + lockTime = lockTimeFromIndicator(lockTimeIndicator); + else if (msmtyp == 6 || msmtyp == 7) + lockTime = highResLockTimeFromIndicator(lockTimeIndicator); + + Sig& sig = *signalPointer; + SatSys& Sat = cellSatellitemap[indx]; + + if (lockTime < acsConfig.epoch_interval) + sig.LLI = true; + else + sig.LLI = false; + } + + for (auto& [indx, signalPointer] : signalPointermap) + { + int half_cycle_ambiguity = getbituInc(data, i, 1); + + Sig& sig = *signalPointer; + + if (half_cycle_ambiguity > 0) + sig.LLI = true; + } + + for (auto& [indx, signalPointer] : signalPointermap) + { + double carrier_noise_ratio = getbituIncScale(data, i, nbcn, scsn); + + Sig& sig = *signalPointer; + sig.snr = carrier_noise_ratio; + } + + if (extrainfo) + for (auto& [indx, signalPointer] : signalPointermap) + { + bool failure = false; + double fine_doppler = getbitsIncScale(data, i, 15, 0.0001, &failure); + + if (failure) + continue; + + Sig& sig = *signalPointer; + + sig.D += fine_doppler; + } + + // convert millisecond or m/s measurements to meters or cycles or Hz + for (auto& obs : only(obsList)) + for (auto& [ft, sigList] : obs.sigsLists) + for (auto& sig : sigList) + { + double freqcy = carrierFrequency[ft]; + if (rtcmsys == E_Sys::GLO) + freqcy += GLOFreqShift[obs.Sat][ft]; + + sig.P *= CLIGHT / 1000; // ms -> metre + sig.L *= freqcy / 1000; // ms -> cycle + sig.D *= -freqcy / CLIGHT; // m/s -> Hz + + traceTrivialDebug( + "#RTCM_MSM OBSERV %s %s %d %s %.4f %.4f", + obs.time.to_string().c_str(), + obs.Sat.id().c_str(), + ft, + enum_to_string(sig.code).c_str(), + sig.P, + sig.L + ); + + if (acsConfig.output_decoded_rtcm_json) + traceMSM(messCode, obs.time, obs.Sat, sig); + } + + return obsList; } - void RtcmDecoder::traceLatency(GTime tobs) { - GTime now = timeGet(); + GTime now = timeGet(); - double latency = (now - tobs).to_double(); + double latency = (now - tobs).to_double(); - //std::cout << "traceLatency : " << latency << " seconds.\n"; - totalLatency += latency; - numMessagesLatency++; + // std::cout << "traceLatency : " << latency << " seconds.\n"; + totalLatency += latency; + numMessagesLatency++; } -E_ReturnType RtcmDecoder::decode( - vector& message) +E_ReturnType RtcmDecoder::decode(vector& message) { - E_ReturnType retVal = E_ReturnType::OK; - - int messageNumber = getbitu(message, 0, 12); - -// std::cout << "\n" << "Received " << RtcmMessageType::_from_integral(messageNumber)._to_string(); - - switch (messageNumber) - { - default: retVal = E_ReturnType::UNSUPPORTED; break; - - case +RtcmMessageType::CUSTOM: retVal = decodeCustom (message); break; - case +RtcmMessageType::GPS_EPHEMERIS: //fallthrough - case +RtcmMessageType::GLO_EPHEMERIS: //fallthrough - case +RtcmMessageType::BDS_EPHEMERIS: //fallthrough - case +RtcmMessageType::QZS_EPHEMERIS: //fallthrough - case +RtcmMessageType::GAL_INAV_EPHEMERIS: //fallthrough - case +RtcmMessageType::GAL_FNAV_EPHEMERIS: decodeEphemeris (message); break; - - case +RtcmMessageType::GPS_SSR_ORB_CORR: //fallthrough - case +RtcmMessageType::GPS_SSR_CLK_CORR: //fallthrough - case +RtcmMessageType::GPS_SSR_COMB_CORR: //fallthrough - case +RtcmMessageType::GPS_SSR_CODE_BIAS: //fallthrough - case +RtcmMessageType::GPS_SSR_PHASE_BIAS: //fallthrough - case +RtcmMessageType::GPS_SSR_URA: //fallthrough - case +RtcmMessageType::GPS_SSR_HR_CLK_CORR: //fallthrough - case +RtcmMessageType::GLO_SSR_ORB_CORR: //fallthrough - case +RtcmMessageType::GLO_SSR_CLK_CORR: //fallthrough - case +RtcmMessageType::GLO_SSR_COMB_CORR: //fallthrough - case +RtcmMessageType::GLO_SSR_CODE_BIAS: //fallthrough - case +RtcmMessageType::GLO_SSR_PHASE_BIAS: //fallthrough - case +RtcmMessageType::GLO_SSR_URA: //fallthrough - case +RtcmMessageType::GLO_SSR_HR_CLK_CORR: //fallthrough - case +RtcmMessageType::GAL_SSR_ORB_CORR: //fallthrough - case +RtcmMessageType::GAL_SSR_CLK_CORR: //fallthrough - case +RtcmMessageType::GAL_SSR_COMB_CORR: //fallthrough - case +RtcmMessageType::GAL_SSR_CODE_BIAS: //fallthrough - case +RtcmMessageType::GAL_SSR_PHASE_BIAS: //fallthrough - case +RtcmMessageType::GAL_SSR_URA: //fallthrough - case +RtcmMessageType::GAL_SSR_HR_CLK_CORR: //fallthrough - case +RtcmMessageType::QZS_SSR_ORB_CORR: //fallthrough - case +RtcmMessageType::QZS_SSR_CLK_CORR: //fallthrough - case +RtcmMessageType::QZS_SSR_COMB_CORR: //fallthrough - case +RtcmMessageType::QZS_SSR_CODE_BIAS: //fallthrough - case +RtcmMessageType::QZS_SSR_PHASE_BIAS: //fallthrough - case +RtcmMessageType::QZS_SSR_URA: //fallthrough - case +RtcmMessageType::QZS_SSR_HR_CLK_CORR: //fallthrough - case +RtcmMessageType::BDS_SSR_ORB_CORR: //fallthrough - case +RtcmMessageType::BDS_SSR_CLK_CORR: //fallthrough - case +RtcmMessageType::BDS_SSR_COMB_CORR: //fallthrough - case +RtcmMessageType::BDS_SSR_CODE_BIAS: //fallthrough - case +RtcmMessageType::BDS_SSR_PHASE_BIAS: //fallthrough - case +RtcmMessageType::BDS_SSR_URA: //fallthrough - case +RtcmMessageType::BDS_SSR_HR_CLK_CORR: //fallthrough - case +RtcmMessageType::SBS_SSR_ORB_CORR: //fallthrough - case +RtcmMessageType::SBS_SSR_CLK_CORR: //fallthrough - case +RtcmMessageType::SBS_SSR_COMB_CORR: //fallthrough - case +RtcmMessageType::SBS_SSR_CODE_BIAS: //fallthrough - case +RtcmMessageType::SBS_SSR_PHASE_BIAS: //fallthrough - case +RtcmMessageType::SBS_SSR_URA: //fallthrough - case +RtcmMessageType::SBS_SSR_HR_CLK_CORR: decodeSSR (message); break; - - case +RtcmMessageType::MSM4_GPS: //fallthrough - case +RtcmMessageType::MSM4_GLONASS: //fallthrough - case +RtcmMessageType::MSM4_GALILEO: //fallthrough - case +RtcmMessageType::MSM4_QZSS: //fallthrough - case +RtcmMessageType::MSM4_BEIDOU: //fallthrough - case +RtcmMessageType::MSM5_GPS: //fallthrough - case +RtcmMessageType::MSM5_GLONASS: //fallthrough - case +RtcmMessageType::MSM5_GALILEO: //fallthrough - case +RtcmMessageType::MSM5_QZSS: //fallthrough - case +RtcmMessageType::MSM5_BEIDOU: //fallthrough - case +RtcmMessageType::MSM6_GPS: //fallthrough - case +RtcmMessageType::MSM6_GLONASS: //fallthrough - case +RtcmMessageType::MSM6_GALILEO: //fallthrough - case +RtcmMessageType::MSM6_QZSS: //fallthrough - case +RtcmMessageType::MSM6_BEIDOU: //fallthrough - case +RtcmMessageType::MSM7_GPS: //fallthrough - case +RtcmMessageType::MSM7_GLONASS: //fallthrough - case +RtcmMessageType::MSM7_GALILEO: //fallthrough - case +RtcmMessageType::MSM7_QZSS: //fallthrough - case +RtcmMessageType::MSM7_BEIDOU: //fallthrough - { - ObsList obsList = decodeMSM(message); - - int i = 54; - int multimessage = getbituInc(message, i, 1); - -// 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 - { - //time delta, push the old list and start a new one - obsListList.push_back(std::move(superObsList)); - superObsList.clear(); - - retVal = E_ReturnType::GOT_OBS; - } - - //copy the new data into the new list - superObsList.insert(superObsList.end(), obsList.begin(), obsList.end()); - - if (multimessage == 0) - { - obsListList.push_back(std::move(superObsList)); - superObsList.clear(); - - retVal = E_ReturnType::GOT_OBS; - } - - if (superObsList.size() > 1000) - { - superObsList.clear(); - } - - break; - } - - case +RtcmMessageType::IGS_SSR: if (decodeigsSSR (message, rtcmTime()) == E_ReturnType::WAIT) retVal = E_ReturnType::WAIT; break; - case +RtcmMessageType::COMPACT_SSR: if (decodecompactSSR(message, rtcmTime()) == E_ReturnType::WAIT) retVal = E_ReturnType::WAIT; break; - } - - - if ( retVal == E_ReturnType::OK - || retVal == E_ReturnType::GOT_OBS) - { - frameDecoded(); - } - else if ( retVal == E_ReturnType::UNSUPPORTED) - { - if (acsConfig.output_decoded_rtcm_json) - traceUnknown(); - } - - return retVal; + E_ReturnType retVal = E_ReturnType::OK; + + int messageNumber = getbitu(message, 0, 12); + RtcmMessageType messCode = messageNumberToRtcmType(messageNumber); + + // std::cout << "\n" << "Received " << enum_to_string(messageNumberToRtcmType(messageNumber)); + + switch (messCode) + { + default: + retVal = E_ReturnType::UNSUPPORTED; + break; + + case RtcmMessageType::CUSTOM: + retVal = decodeCustom(message); + break; + case RtcmMessageType::GPS_EPHEMERIS: // fallthrough + case RtcmMessageType::GLO_EPHEMERIS: // fallthrough + case RtcmMessageType::BDS_EPHEMERIS: // fallthrough + case RtcmMessageType::QZS_EPHEMERIS: // fallthrough + case RtcmMessageType::GAL_INAV_EPHEMERIS: // fallthrough + case RtcmMessageType::GAL_FNAV_EPHEMERIS: + decodeEphemeris(message); + break; + + case RtcmMessageType::GPS_SSR_ORB_CORR: // fallthrough + case RtcmMessageType::GPS_SSR_CLK_CORR: // fallthrough + case RtcmMessageType::GPS_SSR_COMB_CORR: // fallthrough + case RtcmMessageType::GPS_SSR_CODE_BIAS: // fallthrough + case RtcmMessageType::GPS_SSR_PHASE_BIAS: // fallthrough + case RtcmMessageType::GPS_SSR_URA: // fallthrough + case RtcmMessageType::GPS_SSR_HR_CLK_CORR: // fallthrough + case RtcmMessageType::GLO_SSR_ORB_CORR: // fallthrough + case RtcmMessageType::GLO_SSR_CLK_CORR: // fallthrough + case RtcmMessageType::GLO_SSR_COMB_CORR: // fallthrough + case RtcmMessageType::GLO_SSR_CODE_BIAS: // fallthrough + case RtcmMessageType::GLO_SSR_PHASE_BIAS: // fallthrough + case RtcmMessageType::GLO_SSR_URA: // fallthrough + case RtcmMessageType::GLO_SSR_HR_CLK_CORR: // fallthrough + case RtcmMessageType::GAL_SSR_ORB_CORR: // fallthrough + case RtcmMessageType::GAL_SSR_CLK_CORR: // fallthrough + case RtcmMessageType::GAL_SSR_COMB_CORR: // fallthrough + case RtcmMessageType::GAL_SSR_CODE_BIAS: // fallthrough + case RtcmMessageType::GAL_SSR_PHASE_BIAS: // fallthrough + case RtcmMessageType::GAL_SSR_URA: // fallthrough + case RtcmMessageType::GAL_SSR_HR_CLK_CORR: // fallthrough + case RtcmMessageType::QZS_SSR_ORB_CORR: // fallthrough + case RtcmMessageType::QZS_SSR_CLK_CORR: // fallthrough + case RtcmMessageType::QZS_SSR_COMB_CORR: // fallthrough + case RtcmMessageType::QZS_SSR_CODE_BIAS: // fallthrough + case RtcmMessageType::QZS_SSR_PHASE_BIAS: // fallthrough + case RtcmMessageType::QZS_SSR_URA: // fallthrough + case RtcmMessageType::QZS_SSR_HR_CLK_CORR: // fallthrough + case RtcmMessageType::BDS_SSR_ORB_CORR: // fallthrough + case RtcmMessageType::BDS_SSR_CLK_CORR: // fallthrough + case RtcmMessageType::BDS_SSR_COMB_CORR: // fallthrough + case RtcmMessageType::BDS_SSR_CODE_BIAS: // fallthrough + case RtcmMessageType::BDS_SSR_PHASE_BIAS: // fallthrough + case RtcmMessageType::BDS_SSR_URA: // fallthrough + case RtcmMessageType::BDS_SSR_HR_CLK_CORR: // fallthrough + case RtcmMessageType::SBS_SSR_ORB_CORR: // fallthrough + case RtcmMessageType::SBS_SSR_CLK_CORR: // fallthrough + case RtcmMessageType::SBS_SSR_COMB_CORR: // fallthrough + case RtcmMessageType::SBS_SSR_CODE_BIAS: // fallthrough + case RtcmMessageType::SBS_SSR_PHASE_BIAS: // fallthrough + case RtcmMessageType::SBS_SSR_URA: // fallthrough + case RtcmMessageType::SBS_SSR_HR_CLK_CORR: + decodeSSR(message); + break; + + case RtcmMessageType::MSM4_GPS: // fallthrough + case RtcmMessageType::MSM4_GLONASS: // fallthrough + case RtcmMessageType::MSM4_GALILEO: // fallthrough + case RtcmMessageType::MSM4_QZSS: // fallthrough + case RtcmMessageType::MSM4_BEIDOU: // fallthrough + case RtcmMessageType::MSM5_GPS: // fallthrough + case RtcmMessageType::MSM5_GLONASS: // fallthrough + case RtcmMessageType::MSM5_GALILEO: // fallthrough + case RtcmMessageType::MSM5_QZSS: // fallthrough + case RtcmMessageType::MSM5_BEIDOU: // fallthrough + case RtcmMessageType::MSM6_GPS: // fallthrough + case RtcmMessageType::MSM6_GLONASS: // fallthrough + case RtcmMessageType::MSM6_GALILEO: // fallthrough + case RtcmMessageType::MSM6_QZSS: // fallthrough + case RtcmMessageType::MSM6_BEIDOU: // fallthrough + case RtcmMessageType::MSM7_GPS: // fallthrough + case RtcmMessageType::MSM7_GLONASS: // fallthrough + case RtcmMessageType::MSM7_GALILEO: // fallthrough + case RtcmMessageType::MSM7_QZSS: // fallthrough + case RtcmMessageType::MSM7_BEIDOU: // fallthrough + { + ObsList obsList = decodeMSM(message); + + int i = 54; + int multimessage = getbituInc(message, i, 1); + + // 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 + { + // time delta, push the old list and start a new one + obsListList.push_back(std::move(superObsList)); + superObsList.clear(); + + retVal = E_ReturnType::GOT_OBS; + } + + // copy the new data into the new list + superObsList.insert(superObsList.end(), obsList.begin(), obsList.end()); + + if (multimessage == 0) + { + obsListList.push_back(std::move(superObsList)); + superObsList.clear(); + + retVal = E_ReturnType::GOT_OBS; + } + + if (superObsList.size() > 1000) + { + superObsList.clear(); + } + + break; + } + + case RtcmMessageType::IGS_SSR: + if (decodeigsSSR(message, rtcmTime()) == E_ReturnType::WAIT) + retVal = E_ReturnType::WAIT; + break; + case RtcmMessageType::COMPACT_SSR: + if (decodecompactSSR(message, rtcmTime()) == E_ReturnType::WAIT) + retVal = E_ReturnType::WAIT; + break; + } + + if (retVal == E_ReturnType::OK || retVal == E_ReturnType::GOT_OBS) + { + frameDecoded(); + } + else if (retVal == E_ReturnType::UNSUPPORTED) + { + if (acsConfig.output_decoded_rtcm_json) + traceUnknown(); + } + + return retVal; } - /** extract unsigned bits from byte data -*/ + */ unsigned int getbitu( - const unsigned char* buff, ///< byte data - int pos, ///< bit position from start of data (bits) - int len) ///< bit length (bits) (len<=32) + 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; - for (int i = pos; i < pos+len; i++) - bits = (bits<<1) + ((buff[i/8]>>(7-i%8))&1u); + unsigned int bits = 0; + for (int i = pos; i < pos + len; i++) + bits = (bits << 1) + ((buff[i / 8] >> (7 - i % 8)) & 1u); - return bits; + return bits; } /** extract unsigned bits from RTCM messages -*/ + */ unsigned int getbitu( - vector& buff, ///< RTCM messages - int pos, ///< bit position from start of data (bits) - int len) ///< bit length (bits) (len<=32) + vector& buff, ///< RTCM messages + int pos, ///< bit position from start of data (bits) + int len ///< bit length (bits) (len<=32) +) { - return getbitu(buff.data(), pos, len); + return getbitu(buff.data(), pos, len); } /** extract signed bits from byte data -*/ + */ int 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 + 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); - - long int invalid = (1ul<<(len-1)); - - if (bits == invalid) - { -// std::cout << "warning: invalid number received on " << __FUNCTION__ << " " << invalid << " " << len << "\n"; - if (failure_ptr) - { - *failure_ptr = true; - } - } - - if ( len <= 0 - ||len >= 32 - ||!(bits&(1u<<(len-1)))) - { - return (int)bits; - } - return (int)(bits|(~0u<= 32 || !(bits & (1u << (len - 1)))) + { + return (int)bits; + } + return (int)(bits | (~0u << len)); /* extend sign */ } /** increasingly extract unsigned bits from byte data -*/ + */ unsigned int getbituInc( - const unsigned char* buff, ///< byte data - int& pos, ///< bit position from start of data (bits) - int len) ///< bit length (bits) (len<=32) + 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); - pos += len; - return ans; + unsigned int ans = getbitu(buff, pos, len); + pos += len; + return ans; } /** increasingly extract unsigned bits from RTCM messages -*/ + */ unsigned int getbituInc( - vector& buff, ///< byte data - int& pos, ///< bit position from start of data (bits) - int len) ///< bit length (bits) (len<=32) + vector& buff, ///< byte data + int& pos, ///< bit position from start of data (bits) + int len ///< bit length (bits) (len<=32) +) { - return getbituInc(buff.data(), pos, len); + return getbituInc(buff.data(), pos, len); } /** increasingly extract signed bits from byte data -*/ + */ int 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 + 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); - pos += len; - return ans; + int ans = getbits(buff, pos, len, failure_ptr); + pos += len; + return ans; } /** increasingly extract signed bits from RTCM messages -*/ + */ int getbitsInc( - vector& 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 + vector& 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 +) { - return getbitsInc(buff.data(), pos, len, failure_ptr); + return getbitsInc(buff.data(), pos, len, failure_ptr); } /** increasingly extract signed bits from RTCM messages with scale factor/resolution applied -*/ + */ double getbitsIncScale( - vector& buff, ///< byte data - int& pos, ///< bit position from start of data (bits) - int len, ///< bit length (bits) (len<=32) - double scale, ///< scale factor/resolution - bool* failure_ptr) ///< pointer for failure flag + vector& buff, ///< byte data + int& pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + double scale, ///< scale factor/resolution + bool* failure_ptr ///< pointer for failure flag +) { - return scale * getbitsInc(buff.data(), pos, len, failure_ptr); + return scale * getbitsInc(buff.data(), pos, len, failure_ptr); } /** increasingly extract unsigned bits from RTCM messages with scale factor/resolution applied -*/ + */ double getbituIncScale( - vector& buff, ///< byte data - int& pos, ///< bit position from start of data (bits) - int len, ///< bit length (bits) (len<=32) - double scale) ///< scale factor/resolution + vector& buff, ///< byte data + int& pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + double scale ///< scale factor/resolution +) { - return scale * getbituInc(buff.data(), pos, len); + return scale * getbituInc(buff.data(), pos, len); } /** extract sign-magnitude bits applied in GLO nav messages from byte data -*/ + */ int getbitg( - const unsigned char* buff, ///< byte data - int pos, ///< bit position from start of data (bits) - int len) ///< bit length (bits) (len<=32) + 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); + int 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( - const unsigned char* buff, ///< byte data - int& pos, ///< bit position from start of data (bits) - int len) ///< bit length (bits) (len<=32) + 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); - pos += len; - return ans; + int ans = getbitg(buff, pos, len); + pos += len; + return ans; } /** increasingly extract sign-magnitude bits applied in GLO nav messages from RTCM messages -*/ + */ int getbitgInc( - vector& buff, ///< byte data - int& pos, ///< bit position from start of data (bits) - int len) ///< bit length (bits) (len<=32) + vector& buff, ///< byte data + int& pos, ///< bit position from start of data (bits) + int len ///< bit length (bits) (len<=32) +) { - return getbitgInc(buff.data(), pos, len); + return getbitgInc(buff.data(), pos, len); } diff --git a/src/cpp/common/rtcmDecoder.hpp b/src/cpp/common/rtcmDecoder.hpp index f1e382efc..560f6520b 100644 --- a/src/cpp/common/rtcmDecoder.hpp +++ b/src/cpp/common/rtcmDecoder.hpp @@ -1,259 +1,219 @@ - #pragma once -#include "packetStatistics.hpp" -#include "observations.hpp" -#include "rtcmEncoder.hpp" -#include "rtcmTrace.hpp" -#include "streamObs.hpp" -#include "acsConfig.hpp" -#include "gTime.hpp" -#include "enums.h" +#include "common/acsConfig.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/observations.hpp" +#include "common/packetStatistics.hpp" +#include "common/rtcmEncoder.hpp" +#include "common/rtcmTrace.hpp" +#include "common/streamObs.hpp" struct SignalInfo { - uint8_t signal_id; - E_FType ftype; - E_ObsCode obsCode; + uint8_t signal_id; + E_FType ftype; + E_ObsCode obsCode; }; struct RtcmDecoder : RtcmTrace, ObsLister, PacketStatistics { - static double rtcmDeltaTime; ///< Common time used among all rtcmDecoders for delaying decoding when realtime is enabled - static map receivedTimeMap; - - GTime lastTimeStamp; - - GTime receivedTime; ///< Recent internal time from decoded rtcm messages - - ObsList superObsList; ///< List to accumulate observations from smaller lists which share a common time - - static uint16_t message_length( - char header[2]); - - static RtcmMessageType message_type( - const uint8_t message[]); - - int adjGpsWeek( - int week); - - int adjGstWeek( - int week); + static double rtcmDeltaTime; ///< Common time used among all rtcmDecoders for delaying decoding + ///< when realtime is enabled + static map receivedTimeMap; - int adjBdtWeek( - int week); + GTime lastTimeStamp; - void traceLatency(GTime gpsTime); + GTime receivedTime; ///< Recent internal time from decoded rtcm messages - constexpr static int updateInterval[16] = - { - 1, 2, 5, 10, 15, 30, 60, 120, 240, 300, 600, 900, 1800, 3600, 7200, 10800 - }; + ObsList superObsList; ///< List to accumulate observations from smaller lists which share a + ///< common time - GTime rtcmTimestampTime; - GWeek rtcmWeek = -1; + static uint16_t message_length(char header[2]); - E_ObsCode signal_to_code( - E_Sys sys, - uint8_t signal); + static RtcmMessageType message_type(const uint8_t message[]); - GTime rtcmTime(); + int adjGpsWeek(int week); - void decodeEphemeris( - vector& message); + int adjGstWeek(int week); - void decodeSSR( - vector& message); + int adjBdtWeek(int week); - GTime decodeCustomTimestamp( - vector& message); + void traceLatency(GTime gpsTime); - E_RTCMSubmessage decodeCustomId( - vector& message); + constexpr static int updateInterval[16] = + {1, 2, 5, 10, 15, 30, 60, 120, 240, 300, 600, 900, 1800, 3600, 7200, 10800}; - ObsList decodeMSM( - vector& message); + GTime rtcmTimestampTime; + GWeek rtcmWeek = -1; - string recordFilename; + E_ObsCode signal_to_code(E_Sys sys, uint8_t signal); - void recordFrame( - vector& data, - unsigned int crcRead) - { - if (recordFilename.empty()) - { - return; - } + GTime rtcmTime(); - std::ofstream ofs(recordFilename, std::ofstream::app); + void decodeEphemeris(vector& message); - if (!ofs) - { - return; - } + void decodeSSR(vector& message); - //Write the custom time stamp message. - RtcmEncoder encoder; - encoder.rtcmTraceFilename = rtcmTraceFilename; + GTime decodeCustomTimestamp(vector& message); - auto buffer = encoder.encodeTimeStampRTCM(); - bool write = encoder.encodeWriteMessageToBuffer(buffer); + E_RTCMSubmessage decodeCustomId(vector& message); - if (write) - { - encoder.encodeWriteMessages(ofs); - } + ObsList decodeMSM(vector& message); - //copy the message to the output file too - ofs.write((char *)data.data(), data.size()); - ofs.write((char *)&crcRead, 3); - } + string recordFilename; + void recordFrame(vector& data, unsigned int crcRead) + { + if (recordFilename.empty()) + { + return; + } + std::ofstream ofs(recordFilename, std::ofstream::app); - E_ReturnType decodeCustom( - vector& message) - { - E_RTCMSubmessage submessage = decodeCustomId(message); + if (!ofs) + { + return; + } - switch (submessage) - { - case (E_RTCMSubmessage::TIMESTAMP): - { - GTime timeStamp = decodeCustomTimestamp(message); + // Write the custom time stamp message. + RtcmEncoder encoder; + encoder.rtcmTraceFilename = rtcmTraceFilename; - rtcmTimestampTime = timeStamp; + auto buffer = encoder.encodeTimeStampRTCM(); + bool write = encoder.encodeWriteMessageToBuffer(buffer); - if (acsConfig.simulate_real_time) - { - //get the current time and compare it with the timestamp in the message - GTime now = timeGet(); + if (write) + { + encoder.encodeWriteMessages(ofs); + } - //find the delay between creation of the timestamp, and now - double thisDeltaTime = (now - timeStamp).to_double(); + // copy the message to the output file too + ofs.write((char*)data.data(), data.size()); + ofs.write((char*)&crcRead, 3); + } - //initialise the global rtcm delay if needed - if (rtcmDeltaTime == 0) - { - rtcmDeltaTime = thisDeltaTime; - } + E_ReturnType decodeCustom(vector& message) + { + E_RTCMSubmessage submessage = decodeCustomId(message); - //if the delay is shorter than the global, go back and wait until it is longer - if (thisDeltaTime < rtcmDeltaTime) - { - return E_ReturnType::WAIT; - } + switch (submessage) + { + case (E_RTCMSubmessage::TIMESTAMP): + { + GTime timeStamp = decodeCustomTimestamp(message); - if (acsConfig.output_decoded_rtcm_json) - traceTimestamp(timeStamp); + rtcmTimestampTime = timeStamp; - break; - } + if (acsConfig.simulate_real_time) + { + // get the current time and compare it with the timestamp in the message + GTime now = timeGet(); - if (1) - { - int& waitingStreams = receivedTimeMap[timeStamp]; + // find the delay between creation of the timestamp, and now + double thisDeltaTime = (now - timeStamp).to_double(); - if (lastTimeStamp == GTime::noTime()) - { - lastTimeStamp = timeStamp; - waitingStreams++; + // initialise the global rtcm delay if needed + if (rtcmDeltaTime == 0) + { + rtcmDeltaTime = thisDeltaTime; + } - return E_ReturnType::WAIT; - } + // if the delay is shorter than the global, go back and wait until it is longer + if (thisDeltaTime < rtcmDeltaTime) + { + return E_ReturnType::WAIT; + } - if (timeStamp != lastTimeStamp) - { - lastTimeStamp = timeStamp; - waitingStreams++; - } + if (acsConfig.output_decoded_rtcm_json) + traceTimestamp(timeStamp); + break; + } - auto& [firstTime, count] = *receivedTimeMap.begin(); + if (1) + { + int& waitingStreams = receivedTimeMap[timeStamp]; - if (timeStamp > firstTime) - { - return E_ReturnType::WAIT; - } + if (lastTimeStamp == GTime::noTime()) + { + lastTimeStamp = timeStamp; + waitingStreams++; - if (timeStamp < firstTime) - { - std::cout << "unexpected time here" << "\n"; - exit(1); - } + return E_ReturnType::WAIT; + } - //we are the head of the pack, decrement/remove, and process - waitingStreams--; + if (timeStamp != lastTimeStamp) + { + lastTimeStamp = timeStamp; + waitingStreams++; + } - if (waitingStreams <= 0) - { - receivedTimeMap.erase(timeStamp); - } - } + auto& [firstTime, count] = *receivedTimeMap.begin(); - if (acsConfig.output_decoded_rtcm_json) - traceTimestamp(timeStamp); - - break; - } - default: - { - if (acsConfig.output_decoded_rtcm_json) - traceUnknown(); - - break; - } - } - - return E_ReturnType::OK; - } - - E_ReturnType decode( - vector& message); + if (timeStamp > firstTime) + { + return E_ReturnType::WAIT; + } + if (timeStamp < firstTime) + { + std::cout << "unexpected time here" << "\n"; + exit(1); + } + + // we are the head of the pack, decrement/remove, and process + waitingStreams--; + + if (waitingStreams <= 0) + { + receivedTimeMap.erase(timeStamp); + } + } + + if (acsConfig.output_decoded_rtcm_json) + traceTimestamp(timeStamp); + + break; + } + default: + { + if (acsConfig.output_decoded_rtcm_json) + traceUnknown(); + + break; + } + } + + return E_ReturnType::OK; + } + + 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); -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); - - -int getbitg (const unsigned char *buff, int pos, int len); -int getbitgInc (const unsigned char *buff, int& pos, int len); - -int 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); -unsigned int getbitu( - vector& buff, - int pos, - int len); +int getbitgInc(vector& buff, int& pos, int len); -unsigned int getbituInc( - vector& buff, - int& pos, - int len); +unsigned int getbitu(vector& buff, int pos, int len); -int getbitsInc( - vector& buff, - int& pos, - int len, - bool* failure_ptr = nullptr); +unsigned int getbituInc(vector& buff, int& pos, int len); -double getbituIncScale( - vector& buff, - int& pos, - int len, - double scale); +int getbitsInc(vector& buff, int& pos, int len, bool* failure_ptr = nullptr); -double getbitsIncScale( - vector& buff, - int& pos, - int len, - double scale, - bool* failure_ptr = nullptr); +double getbituIncScale(vector& buff, int& pos, int len, double scale); +double getbitsIncScale( + vector& buff, + int& pos, + int len, + double scale, + bool* failure_ptr = nullptr +); diff --git a/src/cpp/common/rtcmEncoder.cpp b/src/cpp/common/rtcmEncoder.cpp index d569e0e0d..3a40ac112 100644 --- a/src/cpp/common/rtcmEncoder.cpp +++ b/src/cpp/common/rtcmEncoder.cpp @@ -1,1260 +1,1543 @@ - // #pragma GCC optimize ("O0") +#include "common/rtcmEncoder.hpp" #include - -#include "rtcmEncoder.hpp" -#include "navigation.hpp" -#include "ephemeris.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" +#include "common/acsConfig.hpp" +#include "common/constants.hpp" +#include "common/ephemeris.hpp" +#include "common/navigation.hpp" /** Convert SSR URA to URA_CLASS and URA_VALUE combination, with 3 Msb URA_CLASS and 3Lsb URA_VALUE -*/ + */ int uraToClassValue(double ura) { - int uraClassValue = 0; + int uraClassValue = 0; - for (uraClassValue = 0; uraClassValue < 64; uraClassValue++) - if (uraSsr[uraClassValue] >= ura) - break; + for (uraClassValue = 0; uraClassValue < 64; uraClassValue++) + if (uraSsr[uraClassValue] >= ura) + break; - return uraClassValue; + return uraClassValue; } void calculateSsrComb( - GTime referenceTime, - int udi, - SSRMeta& ssrMeta, - int masterIod, - SsrOutMap& ssrOutMap) + GTime referenceTime, + int udi, + SSRMeta& ssrMeta, + int masterIod, + SsrOutMap& ssrOutMap +) { - if (ssrOutMap.empty()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No suitable Ephemeris data available."; - return; - } - - map> commonClockOffsetsMap; // 0: bias; 1: bias rate - map> commonClockEpochsMap; // 0: 1st straddle pt epoch; 1: 2nd straddle pt epoch - - for (auto& [Sat, ssrOut] : ssrOutMap) - { - if (ssrOut.clkInput.vals[0].iode != ssrOut.ephInput.vals[0].iode) - { - tracepdeex(3, std::cout, "IODE mismatch between clock and ephemeris for %s\n", Sat.id().c_str()); - continue; - } - - auto& ssrEph = ssrOut.ssrEph; - auto& ssrClk = ssrOut.ssrClk; - auto& ssrUra = ssrOut.ssrUra; - auto& ssrEphInput = ssrOut.ephInput; - auto& ssrClkInput = ssrOut.clkInput; - - ssrEph.t0 = referenceTime; - ssrClk.t0 = referenceTime; - ssrUra.t0 = referenceTime; - ssrEph.udi = udi; - ssrClk.udi = udi; - ssrUra.udi = udi; - ssrEph.iod = masterIod; - ssrClk.iod = masterIod; - ssrUra.iod = masterIod; - - ssrEph.ssrMeta = ssrMeta; - ssrClk.ssrMeta = ssrMeta; - ssrUra.ssrMeta = ssrMeta; - - ssrEph.iode = ssrEphInput.vals[0].iode; - // ssrEph.iodcrc = ?? - - double clkCorrections[2]; - Vector3d posCorrections[2]; - double uras[2]; - - for (int i = 0; i < 2; i++) - { - posCorrections[i] = ssrEphInput.vals[i].brdcPos - - ssrEphInput.vals[i].precPos; - clkCorrections[i] = ssrClkInput.vals[i].brdcClk - - ssrClkInput.vals[i].precClk; - uras[i] = ephVarToUra(ssrEphInput.vals[i].ephVar); - } - - if (acsConfig.ssrOpts.extrapolate_corrections) // todo Eugene: check if ura can be interpolated - { - Vector3d diffRAC[2]; - double diffClock[2]; - double uraSsr[2]; - - for (int dt : {0, 1}) - { - double ephRatio = (ssrEph.t0 + dt - ssrEphInput.vals[0].time).to_double() / (ssrEphInput.vals[1].time - ssrEphInput.vals[0].time).to_double(); - double clkRatio = (ssrClk.t0 + dt - ssrClkInput.vals[0].time).to_double() / (ssrClkInput.vals[1].time - ssrClkInput.vals[0].time).to_double(); - - Vector3d posCorrection = posCorrections[0] + ephRatio * (posCorrections[1] - posCorrections[0]); - double clkCorrection = clkCorrections[0] + clkRatio * (clkCorrections[1] - clkCorrections[0]); - double ura = uras[0] + ephRatio * (uras[1] - uras[0]); - - Vector3d satPosition = ssrEphInput.vals[0].brdcPos + ephRatio * (ssrEphInput.vals[1].brdcPos - ssrEphInput.vals[0].brdcPos); - Vector3d satVelocity = ssrEphInput.vals[0].brdcVel + ephRatio * (ssrEphInput.vals[1].brdcVel - ssrEphInput.vals[0].brdcVel); - - Vector3d diffRac = ecef2rac(satPosition, satVelocity) * posCorrection; - - diffRAC [dt] = diffRac; - diffClock [dt] = -clkCorrection; - uraSsr [dt] = ura; - } - - ssrEph. deph = diffRAC[0]; - ssrEph.ddeph = diffRAC[1] - diffRAC[0]; - - ssrClk.dclk[0] = diffClock[0]; - ssrClk.dclk[1] = 0; //diffClock[1] - diffClock[0]; - ssrClk.dclk[2] = 0; // set to zero (not used) - - ssrUra.ura = uraSsr[0]; - - tracepdeex (6, std::cout, "\n RTCM_intp_Clk %s %s %8.3f", referenceTime.to_string().c_str(), Sat.id().c_str(), ssrClk.dclk[0]); - - } - else - { - ssrEph.deph = ecef2rac(ssrEphInput.vals[1].brdcPos, ssrEphInput.vals[1].brdcVel) * posCorrections[1]; - ssrClk.dclk[0] = -clkCorrections[1]; - ssrUra.ura = uras[1]; - tracepdeex (6, std::cout, "\n RTCM_last_Clk %s %s %8.4f", referenceTime.to_string().c_str(), Sat.id().c_str(), ssrClk.dclk[0]); - tracepdeex (6, std::cout, "\n RTCM_last_Eph %s %s %8.4f %8.4f %8.4f", referenceTime.to_string().c_str(), Sat.id().c_str(), ssrEph.deph[0], ssrEph.deph[1], ssrEph.deph[2]); - } - - //adjust all clock corrections so that they remain within the bounds of the outputs - E_Sys sys = Sat.sys; - if (commonClockOffsetsMap[sys][0] == 0) - { - commonClockOffsetsMap[sys][0] = ssrClk.dclk[0]; - commonClockOffsetsMap[sys][1] = ssrClk.dclk[1]; - commonClockEpochsMap [sys][0] = ssrClkInput.vals[0].time; - commonClockEpochsMap [sys][1] = ssrClkInput.vals[1].time; - } - if ( ssrClkInput.vals[0].time != commonClockEpochsMap[sys][0] - ||ssrClkInput.vals[1].time != commonClockEpochsMap[sys][1]) - { - continue; // commonClockOffset is lagging/leading this sat's clock, skip - } - ssrClk.dclk[0] -= commonClockOffsetsMap[sys][0]; - ssrClk.dclk[1] -= commonClockOffsetsMap[sys][1]; - } + if (ssrOutMap.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "No suitable Ephemeris data available."; + return; + } + + map> commonClockOffsetsMap; // 0: bias; 1: bias rate + map> + commonClockEpochsMap; // 0: 1st straddle pt epoch; 1: 2nd straddle pt epoch + + for (auto& [Sat, ssrOut] : ssrOutMap) + { + if (ssrOut.clkInput.vals[0].iode != ssrOut.ephInput.vals[0].iode) + { + tracepdeex( + 3, + std::cout, + "IODE mismatch between clock and ephemeris for %s\n", + Sat.id().c_str() + ); + continue; + } + + auto& ssrEph = ssrOut.ssrEph; + auto& ssrClk = ssrOut.ssrClk; + auto& ssrUra = ssrOut.ssrUra; + auto& ssrEphInput = ssrOut.ephInput; + auto& ssrClkInput = ssrOut.clkInput; + + ssrEph.t0 = referenceTime; + ssrClk.t0 = referenceTime; + ssrUra.t0 = referenceTime; + ssrEph.udi = udi; + ssrClk.udi = udi; + ssrUra.udi = udi; + ssrEph.iod = masterIod; + ssrClk.iod = masterIod; + ssrUra.iod = masterIod; + + ssrEph.ssrMeta = ssrMeta; + ssrClk.ssrMeta = ssrMeta; + ssrUra.ssrMeta = ssrMeta; + + ssrEph.iode = ssrEphInput.vals[0].iode; + // ssrEph.iodcrc = ?? + + double clkCorrections[2]; + Vector3d posCorrections[2]; + double uras[2]; + + for (int i = 0; i < 2; i++) + { + posCorrections[i] = ssrEphInput.vals[i].brdcPos - ssrEphInput.vals[i].precPos; + clkCorrections[i] = ssrClkInput.vals[i].brdcClk - ssrClkInput.vals[i].precClk; + uras[i] = ephVarToUra(ssrEphInput.vals[i].ephVar); + } + + if (acsConfig.ssrOpts + .extrapolate_corrections) // todo Eugene: check if ura can be interpolated + { + Vector3d diffRAC[2]; + double diffClock[2]; + double uraSsr[2]; + + for (int dt : {0, 1}) + { + double ephRatio = (ssrEph.t0 + dt - ssrEphInput.vals[0].time).to_double() / + (ssrEphInput.vals[1].time - ssrEphInput.vals[0].time).to_double(); + double clkRatio = (ssrClk.t0 + dt - ssrClkInput.vals[0].time).to_double() / + (ssrClkInput.vals[1].time - ssrClkInput.vals[0].time).to_double(); + + Vector3d posCorrection = + posCorrections[0] + ephRatio * (posCorrections[1] - posCorrections[0]); + double clkCorrection = + clkCorrections[0] + clkRatio * (clkCorrections[1] - clkCorrections[0]); + double ura = uras[0] + ephRatio * (uras[1] - uras[0]); + + Vector3d satPosition = + ssrEphInput.vals[0].brdcPos + + ephRatio * (ssrEphInput.vals[1].brdcPos - ssrEphInput.vals[0].brdcPos); + Vector3d satVelocity = + ssrEphInput.vals[0].brdcVel + + ephRatio * (ssrEphInput.vals[1].brdcVel - ssrEphInput.vals[0].brdcVel); + + Vector3d diffRac = ecef2rac(satPosition, satVelocity) * posCorrection; + + diffRAC[dt] = diffRac; + diffClock[dt] = -clkCorrection; + uraSsr[dt] = ura; + } + + ssrEph.deph = diffRAC[0]; + ssrEph.ddeph = diffRAC[1] - diffRAC[0]; + + ssrClk.dclk[0] = diffClock[0]; + ssrClk.dclk[1] = 0; // diffClock[1] - diffClock[0]; + ssrClk.dclk[2] = 0; // set to zero (not used) + + ssrUra.ura = uraSsr[0]; + + tracepdeex( + 6, + std::cout, + "\n RTCM_intp_Clk %s %s %8.3f", + referenceTime.to_string().c_str(), + Sat.id().c_str(), + ssrClk.dclk[0] + ); + } + else + { + ssrEph.deph = ecef2rac(ssrEphInput.vals[1].brdcPos, ssrEphInput.vals[1].brdcVel) * + posCorrections[1]; + ssrClk.dclk[0] = -clkCorrections[1]; + ssrUra.ura = uras[1]; + tracepdeex( + 6, + std::cout, + "\n RTCM_last_Clk %s %s %8.4f", + referenceTime.to_string().c_str(), + Sat.id().c_str(), + ssrClk.dclk[0] + ); + tracepdeex( + 6, + std::cout, + "\n RTCM_last_Eph %s %s %8.4f %8.4f %8.4f", + referenceTime.to_string().c_str(), + Sat.id().c_str(), + ssrEph.deph[0], + ssrEph.deph[1], + ssrEph.deph[2] + ); + } + + // adjust all clock corrections so that they remain within the bounds of the outputs + E_Sys sys = Sat.sys; + if (commonClockOffsetsMap[sys][0] == 0) + { + commonClockOffsetsMap[sys][0] = ssrClk.dclk[0]; + commonClockOffsetsMap[sys][1] = ssrClk.dclk[1]; + commonClockEpochsMap[sys][0] = ssrClkInput.vals[0].time; + commonClockEpochsMap[sys][1] = ssrClkInput.vals[1].time; + } + if (ssrClkInput.vals[0].time != commonClockEpochsMap[sys][0] || + ssrClkInput.vals[1].time != commonClockEpochsMap[sys][1]) + { + continue; // commonClockOffset is lagging/leading this sat's clock, skip + } + ssrClk.dclk[0] -= commonClockOffsetsMap[sys][0]; + ssrClk.dclk[1] -= commonClockOffsetsMap[sys][1]; + } } - int RtcmEncoder::getUdiIndex(int udi) { - for (int i = 0; i < 16; i++) // 16 from updateInterval[16] above - { - if (updateInterval[i] == udi) - { - return i; - } - } + for (int i = 0; i < 16; i++) // 16 from updateInterval[16] above + { + if (updateInterval[i] == udi) + { + return i; + } + } - BOOST_LOG_TRIVIAL(error) << "Error: udi is not valid :" << udi << ")."; + BOOST_LOG_TRIVIAL(error) << "UDI is not valid :" << udi << ")."; - return -1; + return -1; } -void RtcmEncoder::encodeWriteMessages( - std::ostream& outputStream) +void RtcmEncoder::encodeWriteMessages(std::ostream& outputStream) { - if (outputStream) - { - outputStream.write((const char*) &data[0], data.size()); - data.clear(); - } + if (outputStream) + { + outputStream.write((const char*)&data[0], data.size()); + data.clear(); + } } -bool RtcmEncoder::encodeWriteMessageToBuffer( - vector& buffer) +bool RtcmEncoder::encodeWriteMessageToBuffer(vector& buffer) { - int i = 0; - int messLength = buffer.size(); - - if (buffer.empty()) - { - return false; - } - - if (messLength > 1023) - { - BOOST_LOG_TRIVIAL(error) << "Error: message length exceeds the limit."; - return false; - } - -// unsigned char nbuf[messLength+6]; - vector newbuffer(messLength + 6); - unsigned char* nbuf = newbuffer.data(); - unsigned char* buf = buffer.data(); - - i = setbituInc(nbuf, i, 8, RTCM_PREAMBLE); - i = setbituInc(nbuf, i, 6, 0); - i = setbituInc(nbuf, i, 10, messLength); - - memcpy(nbuf + 3, buf, sizeof(uint8_t)* messLength); - i = i + messLength * 8; - - const unsigned char* bCrcBuf = (const unsigned char *)nbuf; - unsigned int crcCalc = crc24q(bCrcBuf, sizeof(char) * (messLength + 3)); - - unsigned char* bCrcCalc = (unsigned char*)&crcCalc; - unsigned int b1 = 0; - unsigned int b2 = 0; - unsigned int b3 = 0; - setbituInc((unsigned char *)&b1, 0, 8, *(bCrcCalc + 0)); - setbituInc((unsigned char *)&b2, 0, 8, *(bCrcCalc + 1)); - setbituInc((unsigned char *)&b3, 0, 8, *(bCrcCalc + 2)); - i = setbituInc(nbuf, i, 8, b3); - i = setbituInc(nbuf, i, 8, b2); - i = setbituInc(nbuf, i, 8, b1); - - data.insert(data.end(), &nbuf[0], &nbuf[messLength + 6]); - - return true; + int i = 0; + int messLength = buffer.size(); + + if (buffer.empty()) + { + return false; + } + + if (messLength > 1023) + { + BOOST_LOG_TRIVIAL(error) << "Message length exceeds the limit."; + return false; + } + + // unsigned char nbuf[messLength+6]; + vector newbuffer(messLength + 6); + unsigned char* nbuf = newbuffer.data(); + unsigned char* buf = buffer.data(); + + i = setbituInc(nbuf, i, 8, RTCM_PREAMBLE); + i = setbituInc(nbuf, i, 6, 0); + i = setbituInc(nbuf, i, 10, messLength); + + memcpy(nbuf + 3, buf, sizeof(uint8_t) * messLength); + i = i + messLength * 8; + + const unsigned char* bCrcBuf = (const unsigned char*)nbuf; + unsigned int crcCalc = crc24q(bCrcBuf, sizeof(char) * (messLength + 3)); + + unsigned char* bCrcCalc = (unsigned char*)&crcCalc; + unsigned int b1 = 0; + unsigned int b2 = 0; + unsigned int b3 = 0; + setbituInc((unsigned char*)&b1, 0, 8, *(bCrcCalc + 0)); + setbituInc((unsigned char*)&b2, 0, 8, *(bCrcCalc + 1)); + setbituInc((unsigned char*)&b3, 0, 8, *(bCrcCalc + 2)); + i = setbituInc(nbuf, i, 8, b3); + i = setbituInc(nbuf, i, 8, b2); + i = setbituInc(nbuf, i, 8, b1); + + data.insert(data.end(), &nbuf[0], &nbuf[messLength + 6]); + + return true; } - vector RtcmEncoder::encodeTimeStampRTCM() { - // Custom message code, for crcsi maximum length 4096 bits or 512 bytes. - unsigned int messCode = +RtcmMessageType::CUSTOM; - unsigned int messType = +E_RTCMSubmessage::TIMESTAMP; + // Custom message code, for crcsi maximum length 4096 bits or 512 bytes. + unsigned int messCode = rtcmTypeToMessageNumber(RtcmMessageType::CUSTOM); + unsigned int messType = static_cast(E_RTCMSubmessage::TIMESTAMP); - GTime now = timeGet(); + GTime now = timeGet(); - int i = 0; - int byteLen = 11; - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - unsigned int reserved = 0; - i = setbituInc(buf, i, 12, messCode); - i = setbituInc(buf, i, 8, messType); - i = setbituInc(buf, i, 4, reserved); + int i = 0; + int byteLen = 11; + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + unsigned int reserved = 0; + i = setbituInc(buf, i, 12, messCode); + i = setbituInc(buf, i, 8, messType); + i = setbituInc(buf, i, 4, reserved); - long int milliseconds = now.bigTime * 1000; + long int milliseconds = now.bigTime * 1000; - unsigned int chunk; - chunk = milliseconds; i = setbituInc(buf, i, 32, chunk); milliseconds >>= 32; - chunk = milliseconds; i = setbituInc(buf, i, 32, chunk); + unsigned int chunk; + chunk = milliseconds; + i = setbituInc(buf, i, 32, chunk); + milliseconds >>= 32; + chunk = milliseconds; + i = setbituInc(buf, i, 32, chunk); - traceTimestamp(now); + traceTimestamp(now); - return buffer; + return buffer; } - /** encode SSR header information -*/ + */ int RtcmEncoder::encodeSsrHeader( - unsigned char* buf, ///< byte data - E_Sys sys, ///< system to encode - RtcmMessageType messCode, ///< RTCM message code to encode ephemeris of - SSRMeta& ssrMeta, ///< SSR metadata - int iod, ///< IOD SSR - int dispBiasConistInd, ///< Dispersive Bias Consistency Indicator (for phase bias only) - int MWConistInd) ///< MW Consistency Indicator (for phase bias only) + unsigned char* buf, ///< byte data + E_Sys sys, ///< system to encode + RtcmMessageType messCode, ///< RTCM message code to encode ephemeris of + SSRMeta& ssrMeta, ///< SSR metadata + int iod, ///< IOD SSR + int dispBiasConistInd, ///< Dispersive Bias Consistency Indicator (for phase bias only) + int MWConistInd ///< MW Consistency Indicator (for phase bias only) +) { - string messCodeStr = messCode._to_string(); - string messTypeStr = messCodeStr.substr(8); - - int ne = 0; - int ns = 0; - switch (sys) - { - case E_Sys::GPS: ne = 20; ns = 6; break; - case E_Sys::GLO: ne = 17; ns = 6; break; - case E_Sys::GAL: ne = 20; ns = 6; break; - case E_Sys::QZS: ne = 20; ns = 4; break; - case E_Sys::BDS: ne = 20; ns = 6; break; - case E_Sys::SBS: ne = 20; ns = 6; break; - default: - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system: " << sys._to_string() << " in " << __FUNCTION__; - return 0; - } - - int i = 0; - i = setbituInc(buf, i, 12, messCode); - i = setbituInc(buf, i, ne, ssrMeta.epochTime1s); - i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); - i = setbituInc(buf, i, 1, ssrMeta.multipleMessage); - - if ( messTypeStr == "ORB_CORR" - ||messTypeStr == "COMB_CORR") - { - i = setbituInc(buf, i, 1, ssrMeta.referenceDatum); - } - - i = setbituInc(buf, i, 4, iod); - i = setbituInc(buf, i, 16, ssrMeta.provider); - i = setbituInc(buf, i, 4, ssrMeta.solution); - - if ( messTypeStr == "PHASE_BIAS") - { - i = setbituInc(buf, i, 1, dispBiasConistInd); - i = setbituInc(buf, i, 1, MWConistInd); - } - - i = setbituInc(buf, i, ns, ssrMeta.numSats); - - return i; + string messCodeStr = enum_to_string(messCode); + string messTypeStr = messCodeStr.substr(8); + + int ne = 0; + int ns = 0; + switch (sys) + { + case E_Sys::GPS: + ne = 20; + ns = 6; + break; + case E_Sys::GLO: + ne = 17; + ns = 6; + break; + case E_Sys::GAL: + ne = 20; + ns = 6; + break; + case E_Sys::QZS: + ne = 20; + ns = 4; + break; + case E_Sys::BDS: + ne = 20; + ns = 6; + break; + case E_Sys::SBS: + ne = 20; + ns = 6; + break; + default: + BOOST_LOG_TRIVIAL(error) + << "Unrecognised system: " << enum_to_string(sys) << " in " << __FUNCTION__; + return 0; + } + + int i = 0; + i = setbituInc(buf, i, 12, rtcmTypeToMessageNumber(messCode)); + i = setbituInc(buf, i, ne, ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, ssrMeta.multipleMessage); + + if (messTypeStr == "ORB_CORR" || messTypeStr == "COMB_CORR") + { + i = setbituInc(buf, i, 1, ssrMeta.referenceDatum); + } + + i = setbituInc(buf, i, 4, iod); + i = setbituInc(buf, i, 16, ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrMeta.solution); + + if (messTypeStr == "PHASE_BIAS") + { + i = setbituInc(buf, i, 1, dispBiasConistInd); + i = setbituInc(buf, i, 1, MWConistInd); + } + + i = setbituInc(buf, i, ns, ssrMeta.numSats); + + return i; } /** encode orbit/clock messages -*/ + */ vector RtcmEncoder::encodeSsrOrbClk( - SsrOutMap& ssrOutMap, ///< orbits/clocks to encode - RtcmMessageType messCode) ///< RTCM message code to encode ephemeris of + SsrOutMap& ssrOutMap, ///< orbits/clocks to encode + RtcmMessageType messCode ///< RTCM message code to encode ephemeris of +) { - string messCodeStr = messCode._to_string(); - string messTypeStr = messCodeStr.substr(8); - - int numSats = ssrOutMap.size(); - if (numSats == 0) - { - return vector(); - } - - auto& [Sat, ssrOut] = *ssrOutMap.begin(); - auto& ssrEph = ssrOut.ssrEph; - auto& ssrMeta = ssrEph.ssrMeta; - ssrMeta.numSats = numSats; - - int np = 0; - int ni = 0; - int nj = 0; - switch (Sat.sys) - { - case E_Sys::GPS: np = 6; ni = 8; nj = 0; break; - case E_Sys::GLO: np = 5; ni = 8; nj = 0; break; - case E_Sys::GAL: np = 6; ni = 10; nj = 0; break; - case E_Sys::QZS: np = 4; ni = 8; nj = 0; break; - case E_Sys::BDS: np = 6; ni = 8; nj = 10; break; - case E_Sys::SBS: np = 6; ni = 9; nj = 0; break; - default: - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system: " << Sat.sysName() << " in " << __FUNCTION__; - return vector(); - } - - int bitLen = 0; - switch (messCode) - { - case RtcmMessageType::GPS_SSR_ORB_CORR: bitLen = 68 + numSats * 135; break; - case RtcmMessageType::GPS_SSR_CLK_CORR: bitLen = 67 + numSats * 76; break; - case RtcmMessageType::GPS_SSR_COMB_CORR: bitLen = 68 + numSats * 205; break; - case RtcmMessageType::GPS_SSR_HR_CLK_CORR: bitLen = 67 + numSats * 28; break; - case RtcmMessageType::GLO_SSR_ORB_CORR: bitLen = 65 + numSats * 134; break; - case RtcmMessageType::GLO_SSR_CLK_CORR: bitLen = 64 + numSats * 75; break; - case RtcmMessageType::GLO_SSR_COMB_CORR: bitLen = 65 + numSats * 204; break; - case RtcmMessageType::GLO_SSR_HR_CLK_CORR: bitLen = 64 + numSats * 27; break; - case RtcmMessageType::GAL_SSR_ORB_CORR: bitLen = 68 + numSats * 137; break; - case RtcmMessageType::GAL_SSR_CLK_CORR: bitLen = 67 + numSats * 76; break; - case RtcmMessageType::GAL_SSR_COMB_CORR: bitLen = 68 + numSats * 207; break; - case RtcmMessageType::GAL_SSR_HR_CLK_CORR: bitLen = 67 + numSats * 28; break; - case RtcmMessageType::QZS_SSR_ORB_CORR: bitLen = 66 + numSats * 133; break; - case RtcmMessageType::QZS_SSR_CLK_CORR: bitLen = 65 + numSats * 74; break; - case RtcmMessageType::QZS_SSR_COMB_CORR: bitLen = 66 + numSats * 203; break; - case RtcmMessageType::QZS_SSR_HR_CLK_CORR: bitLen = 65 + numSats * 26; break; - case RtcmMessageType::BDS_SSR_ORB_CORR: bitLen = 68 + numSats * 145; break; - case RtcmMessageType::BDS_SSR_CLK_CORR: bitLen = 67 + numSats * 76; break; - case RtcmMessageType::BDS_SSR_COMB_CORR: bitLen = 68 + numSats * 215; break; - case RtcmMessageType::BDS_SSR_HR_CLK_CORR: bitLen = 67 + numSats * 28; break; - case RtcmMessageType::SBS_SSR_ORB_CORR: bitLen = 68 + numSats * 160; break; - case RtcmMessageType::SBS_SSR_CLK_CORR: bitLen = 67 + numSats * 76; break; - case RtcmMessageType::SBS_SSR_COMB_CORR: bitLen = 68 + numSats * 230; break; - case RtcmMessageType::SBS_SSR_HR_CLK_CORR: bitLen = 67 + numSats * 28; break; - default: return vector(); - } - - int byteLen = ceil(bitLen / 8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - int i = 0; - // Write the header information. - i = encodeSsrHeader(buf, Sat.sys, messCode, ssrMeta, ssrEph.iod); - - for (auto& [Sat, ssrOut] : ssrOutMap) - { - auto& ssrEph = ssrOut.ssrEph; - auto& ssrClk = ssrOut.ssrClk; - - SSRHRClk ssrHRClk; - ssrHRClk.ssrMeta = ssrClk.ssrMeta; - ssrHRClk.t0 = ssrClk.t0; - ssrHRClk.udi = ssrClk.udi; - ssrHRClk.iod = ssrClk.iod; - - //convert doubles to scaled integers - int deph [3]; - int ddeph [3]; - int dclk [3]; - deph[0] = (int)round(ssrEph.deph[0] / 0.1e-3); - deph[1] = (int)round(ssrEph.deph[1] / 0.4e-3); - deph[2] = (int)round(ssrEph.deph[2] / 0.4e-3); - ddeph[0] = (int)round(ssrEph.ddeph[0] / 0.001e-3); - ddeph[1] = (int)round(ssrEph.ddeph[1] / 0.004e-3); - ddeph[2] = (int)round(ssrEph.ddeph[2] / 0.004e-3); - dclk[0] = (int)round(ssrClk.dclk[0] / 0.1e-3); - dclk[1] = (int)round(ssrClk.dclk[1] / 0.001e-3); - dclk[2] = (int)round(ssrClk.dclk[2] / 0.00002e-3); - - i = setbituInc(buf, i, np, Sat.prn); - - if ( messTypeStr == "ORB_CORR" - ||messTypeStr == "COMB_CORR") - { - i = setbituInc(buf, i, nj, ssrEph.iodcrc); - i = setbituInc(buf, i, ni, ssrEph.iode); - - i = setbitsInc(buf, i, 22, deph[0]); - i = setbitsInc(buf, i, 20, deph[1]); - i = setbitsInc(buf, i, 20, deph[2]); - i = setbitsInc(buf, i, 21, ddeph[0]); - i = setbitsInc(buf, i, 19, ddeph[1]); - i = setbitsInc(buf, i, 19, ddeph[2]); - - lastRegSsrEphMap[Sat] = ssrEph; - - traceSsrEph(messCode, Sat, ssrEph); - } - - if ( messTypeStr == "CLK_CORR" - ||messTypeStr == "COMB_CORR") - { - try - { - auto& lastSsrEph = lastRegSsrEphMap.at(Sat); - - if (ssrEph.iode != lastSsrEph.iode) - { - return vector(); - } - } - catch (...) - { - return vector(); - } - - i = setbitsInc(buf, i, 22, dclk[0]); - i = setbitsInc(buf, i, 21, dclk[1]); - i = setbitsInc(buf, i, 27, dclk[2]); - - lastRegSsrClkMap[Sat] = ssrClk; - - traceSsrClk(messCode, Sat, ssrClk); - } - - if ( messTypeStr == "HR_CLK_CORR") - { - try - { - auto& lastSsrEph = lastRegSsrEphMap.at(Sat); - - if (ssrEph.iode != lastSsrEph.iode) - { - return vector(); - } - } - catch (...) - { - return vector(); - } - - try - { - auto& lastRegSsrClk = lastRegSsrClkMap.at(Sat); - - GTime currentTime = ssrHRClk.t0; - double tClk = (currentTime - lastRegSsrClk.t0).to_double(); - - double dclkReg = lastRegSsrClk.dclk[0] - + lastRegSsrClk.dclk[1] * tClk - + lastRegSsrClk.dclk[2] * tClk * tClk; - - ssrHRClk.hrclk = ssrClk.dclk[0] - dclkReg; - } - catch (...) - { - return vector(); - } - - i = setbitsInc(buf, i, 22, ssrHRClk.hrclk / 0.1e-3); - - traceSsrHRClk(messCode, Sat, ssrHRClk); - } - } - - int bitl = byteLen * 8 - i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Orbit/Clock.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - return buffer; + string messCodeStr = enum_to_string(messCode); + string messTypeStr = messCodeStr.substr(8); + + int numSats = ssrOutMap.size(); + if (numSats == 0) + { + return vector(); + } + + auto& [Sat, ssrOut] = *ssrOutMap.begin(); + auto& ssrEph = ssrOut.ssrEph; + auto& ssrMeta = ssrEph.ssrMeta; + ssrMeta.numSats = numSats; + + int np = 0; + int ni = 0; + int nj = 0; + switch (Sat.sys) + { + case E_Sys::GPS: + np = 6; + ni = 8; + nj = 0; + break; + case E_Sys::GLO: + np = 5; + ni = 8; + nj = 0; + break; + case E_Sys::GAL: + np = 6; + ni = 10; + nj = 0; + break; + case E_Sys::QZS: + np = 4; + ni = 8; + nj = 0; + break; + case E_Sys::BDS: + np = 6; + ni = 8; + nj = 10; + break; + case E_Sys::SBS: + np = 6; + ni = 9; + nj = 0; + break; + default: + BOOST_LOG_TRIVIAL(error) + << "Unrecognised system: " << Sat.sysName() << " in " << __FUNCTION__; + return vector(); + } + + int bitLen = 0; + switch (messCode) + { + case RtcmMessageType::GPS_SSR_ORB_CORR: + bitLen = 68 + numSats * 135; + break; + case RtcmMessageType::GPS_SSR_CLK_CORR: + bitLen = 67 + numSats * 76; + break; + case RtcmMessageType::GPS_SSR_COMB_CORR: + bitLen = 68 + numSats * 205; + break; + case RtcmMessageType::GPS_SSR_HR_CLK_CORR: + bitLen = 67 + numSats * 28; + break; + case RtcmMessageType::GLO_SSR_ORB_CORR: + bitLen = 65 + numSats * 134; + break; + case RtcmMessageType::GLO_SSR_CLK_CORR: + bitLen = 64 + numSats * 75; + break; + case RtcmMessageType::GLO_SSR_COMB_CORR: + bitLen = 65 + numSats * 204; + break; + case RtcmMessageType::GLO_SSR_HR_CLK_CORR: + bitLen = 64 + numSats * 27; + break; + case RtcmMessageType::GAL_SSR_ORB_CORR: + bitLen = 68 + numSats * 137; + break; + case RtcmMessageType::GAL_SSR_CLK_CORR: + bitLen = 67 + numSats * 76; + break; + case RtcmMessageType::GAL_SSR_COMB_CORR: + bitLen = 68 + numSats * 207; + break; + case RtcmMessageType::GAL_SSR_HR_CLK_CORR: + bitLen = 67 + numSats * 28; + break; + case RtcmMessageType::QZS_SSR_ORB_CORR: + bitLen = 66 + numSats * 133; + break; + case RtcmMessageType::QZS_SSR_CLK_CORR: + bitLen = 65 + numSats * 74; + break; + case RtcmMessageType::QZS_SSR_COMB_CORR: + bitLen = 66 + numSats * 203; + break; + case RtcmMessageType::QZS_SSR_HR_CLK_CORR: + bitLen = 65 + numSats * 26; + break; + case RtcmMessageType::BDS_SSR_ORB_CORR: + bitLen = 68 + numSats * 145; + break; + case RtcmMessageType::BDS_SSR_CLK_CORR: + bitLen = 67 + numSats * 76; + break; + case RtcmMessageType::BDS_SSR_COMB_CORR: + bitLen = 68 + numSats * 215; + break; + case RtcmMessageType::BDS_SSR_HR_CLK_CORR: + bitLen = 67 + numSats * 28; + break; + case RtcmMessageType::SBS_SSR_ORB_CORR: + bitLen = 68 + numSats * 160; + break; + case RtcmMessageType::SBS_SSR_CLK_CORR: + bitLen = 67 + numSats * 76; + break; + case RtcmMessageType::SBS_SSR_COMB_CORR: + bitLen = 68 + numSats * 230; + break; + case RtcmMessageType::SBS_SSR_HR_CLK_CORR: + bitLen = 67 + numSats * 28; + break; + default: + return vector(); + } + + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + int i = 0; + // Write the header information. + i = encodeSsrHeader(buf, Sat.sys, messCode, ssrMeta, ssrEph.iod); + + for (auto& [Sat, ssrOut] : ssrOutMap) + { + auto& ssrEph = ssrOut.ssrEph; + auto& ssrClk = ssrOut.ssrClk; + + SSRHRClk ssrHRClk; + ssrHRClk.ssrMeta = ssrClk.ssrMeta; + ssrHRClk.t0 = ssrClk.t0; + ssrHRClk.udi = ssrClk.udi; + ssrHRClk.iod = ssrClk.iod; + + // convert doubles to scaled integers + int deph[3]; + int ddeph[3]; + int dclk[3]; + deph[0] = (int)round(ssrEph.deph[0] / 0.1e-3); + deph[1] = (int)round(ssrEph.deph[1] / 0.4e-3); + deph[2] = (int)round(ssrEph.deph[2] / 0.4e-3); + ddeph[0] = (int)round(ssrEph.ddeph[0] / 0.001e-3); + ddeph[1] = (int)round(ssrEph.ddeph[1] / 0.004e-3); + ddeph[2] = (int)round(ssrEph.ddeph[2] / 0.004e-3); + dclk[0] = (int)round(ssrClk.dclk[0] / 0.1e-3); + dclk[1] = (int)round(ssrClk.dclk[1] / 0.001e-3); + dclk[2] = (int)round(ssrClk.dclk[2] / 0.00002e-3); + + i = setbituInc(buf, i, np, Sat.prn); + + if (messTypeStr == "ORB_CORR" || messTypeStr == "COMB_CORR") + { + i = setbituInc(buf, i, nj, ssrEph.iodcrc); + i = setbituInc(buf, i, ni, ssrEph.iode); + + i = setbitsInc(buf, i, 22, deph[0]); + i = setbitsInc(buf, i, 20, deph[1]); + i = setbitsInc(buf, i, 20, deph[2]); + i = setbitsInc(buf, i, 21, ddeph[0]); + i = setbitsInc(buf, i, 19, ddeph[1]); + i = setbitsInc(buf, i, 19, ddeph[2]); + + lastRegSsrEphMap[Sat] = ssrEph; + + traceSsrEph(messCode, Sat, ssrEph); + } + + if (messTypeStr == "CLK_CORR" || messTypeStr == "COMB_CORR") + { + try + { + auto& lastSsrEph = lastRegSsrEphMap.at(Sat); + + if (ssrEph.iode != lastSsrEph.iode) + { + return vector(); + } + } + catch (...) + { + return vector(); + } + + i = setbitsInc(buf, i, 22, dclk[0]); + i = setbitsInc(buf, i, 21, dclk[1]); + i = setbitsInc(buf, i, 27, dclk[2]); + + lastRegSsrClkMap[Sat] = ssrClk; + + traceSsrClk(messCode, Sat, ssrClk); + } + + if (messTypeStr == "HR_CLK_CORR") + { + try + { + auto& lastSsrEph = lastRegSsrEphMap.at(Sat); + + if (ssrEph.iode != lastSsrEph.iode) + { + return vector(); + } + } + catch (...) + { + return vector(); + } + + try + { + auto& lastRegSsrClk = lastRegSsrClkMap.at(Sat); + + GTime currentTime = ssrHRClk.t0; + double tClk = (currentTime - lastRegSsrClk.t0).to_double(); + + double dclkReg = lastRegSsrClk.dclk[0] + lastRegSsrClk.dclk[1] * tClk + + lastRegSsrClk.dclk[2] * tClk * tClk; + + ssrHRClk.hrclk = ssrClk.dclk[0] - dclkReg; + } + catch (...) + { + return vector(); + } + + i = setbitsInc(buf, i, 22, ssrHRClk.hrclk / 0.1e-3); + + traceSsrHRClk(messCode, Sat, ssrHRClk); + } + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Orbit/Clock.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + return buffer; } /** encode phase bias messages -*/ + */ vector RtcmEncoder::encodeSsrPhase( - SsrPBMap& ssrPBMap, ///< phase biases to encode - RtcmMessageType messCode) ///< RTCM message code to encode ephemeris of + SsrPBMap& ssrPBMap, ///< phase biases to encode + RtcmMessageType messCode ///< RTCM message code to encode ephemeris of +) { - int numSats = ssrPBMap.size(); - if (numSats == 0) - { - return vector(); - } - - int totalNumBias = 0; - for (auto& [Sat, ssrPhasBias] : ssrPBMap) - { - totalNumBias += ssrPhasBias.obsCodeBiasMap.size(); - } - if (totalNumBias == 0) - { - return vector(); - } - - auto& [Sat, ssrPhasBias] = *ssrPBMap.begin(); - auto& ssrMeta = ssrPhasBias.ssrMeta; - ssrMeta.numSats = numSats; - - int np = 0; - switch (Sat.sys) - { - case E_Sys::GPS: np = 6; break; - case E_Sys::GLO: np = 5; break; - case E_Sys::GAL: np = 6; break; - case E_Sys::QZS: np = 4; break; - case E_Sys::BDS: np = 6; break; - case E_Sys::SBS: np = 6; break; - default: - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system: " << Sat.sysName() << " in " << __FUNCTION__; - return vector(); - } - - int bitLen = 0; - switch (messCode) - { - case RtcmMessageType::GPS_SSR_PHASE_BIAS: bitLen = 69 + numSats * 28 + totalNumBias * 32; break; - case RtcmMessageType::GLO_SSR_PHASE_BIAS: bitLen = 66 + numSats * 27 + totalNumBias * 32; break; - case RtcmMessageType::GAL_SSR_PHASE_BIAS: bitLen = 69 + numSats * 28 + totalNumBias * 32; break; - case RtcmMessageType::QZS_SSR_PHASE_BIAS: bitLen = 67 + numSats * 26 + totalNumBias * 32; break; - case RtcmMessageType::BDS_SSR_PHASE_BIAS: bitLen = 69 + numSats * 28 + totalNumBias * 32; break; - case RtcmMessageType::SBS_SSR_PHASE_BIAS: bitLen = 69 + numSats * 28 + totalNumBias * 32; break; - default: return vector(); - } - - int byteLen = ceil(bitLen / 8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - int i = 0; - // Write the header information. - int dispBiasConistInd = ssrPhasBias.ssrPhase.dispBiasConistInd; - int MWConistInd = ssrPhasBias.ssrPhase.MWConistInd; - i = encodeSsrHeader(buf, Sat.sys, messCode, ssrMeta, ssrPhasBias.iod, dispBiasConistInd, MWConistInd); - - for (auto& [Sat, ssrPhasBias] : ssrPBMap) - { - ssrPhasBias.udi = updateInterval[ssrMeta.updateIntIndex]; // for rtcmTrace (debugging) - - SSRPhase ssrPhase = ssrPhasBias.ssrPhase; - - unsigned int nbias = ssrPhasBias.obsCodeBiasMap.size(); - int yawAngle = (int)round(ssrPhase.yawAngle * 256 / SC2RAD); - int yawRate = (int)round(ssrPhase.yawRate * 8192 / SC2RAD); - - i = setbituInc(buf, i, np, Sat.prn); - i = setbituInc(buf, i, 5, nbias); - i = setbituInc(buf, i, 9, yawAngle); - i = setbitsInc(buf, i, 8, yawRate); - - for (auto& [obsCode, entry] : ssrPhasBias.obsCodeBiasMap) - { - SSRPhaseCh ssrPhaseCh = ssrPhasBias.ssrPhaseChs[obsCode]; - - //BOOST_LOG_TRIVIAL(debug) << "Phase, obsCode : " << obsCode << "\n"; - //BOOST_LOG_TRIVIAL(debug) << "E_sys : " << sys << "\n"; - //BOOST_LOG_TRIVIAL(debug) << "mCodes_gps.size() : " << mCodes_gps.size() << "\n"; - //print_map( mCodes_gps.left, " E_ObsCode <--> RTCM ", BOOST_LOG_TRIVIAL(debug) ); - - int rtcmCode = 0; - if (Sat.sys == +E_Sys::GPS) { rtcmCode = mCodes_gps.left.at(obsCode); } //todo aaron, crash heaven, needs else, try - else if (Sat.sys == +E_Sys::GLO) { rtcmCode = mCodes_glo.left.at(obsCode); } - else if (Sat.sys == +E_Sys::GAL) { rtcmCode = mCodes_gal.left.at(obsCode); } - else if (Sat.sys == +E_Sys::QZS) { rtcmCode = mCodes_qzs.left.at(obsCode); } - else if (Sat.sys == +E_Sys::BDS) { rtcmCode = mCodes_bds.left.at(obsCode); } - else if (Sat.sys == +E_Sys::SBS) { rtcmCode = mCodes_sbs.left.at(obsCode); } - - //BOOST_LOG_TRIVIAL(debug) << "rtcmCode : " << rtcmCode << "\n"; - - int bias = (int)round(entry.bias / 0.0001); - - i = setbituInc(buf, i, 5, rtcmCode); - i = setbituInc(buf, i, 1, ssrPhaseCh.signalIntInd); - i = setbituInc(buf, i, 2, ssrPhaseCh.signalWLIntInd); - i = setbituInc(buf, i, 4, ssrPhaseCh.signalDisconCnt); - i = setbitsInc(buf, i, 20, bias); - - traceSsrPhasBias(messCode, Sat, obsCode, ssrPhasBias); - } - } - - int bitl = byteLen * 8 - i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Phase.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - - i = setbituInc(buf, i, bitl, 0); - - return buffer; + int numSats = ssrPBMap.size(); + if (numSats == 0) + { + return vector(); + } + + int totalNumBias = 0; + for (auto& [Sat, ssrPhasBias] : ssrPBMap) + { + totalNumBias += ssrPhasBias.obsCodeBiasMap.size(); + } + if (totalNumBias == 0) + { + return vector(); + } + + auto& [Sat, ssrPhasBias] = *ssrPBMap.begin(); + auto& ssrMeta = ssrPhasBias.ssrMeta; + ssrMeta.numSats = numSats; + + int np = 0; + switch (Sat.sys) + { + case E_Sys::GPS: + np = 6; + break; + case E_Sys::GLO: + np = 5; + break; + case E_Sys::GAL: + np = 6; + break; + case E_Sys::QZS: + np = 4; + break; + case E_Sys::BDS: + np = 6; + break; + case E_Sys::SBS: + np = 6; + break; + default: + BOOST_LOG_TRIVIAL(error) + << "Unrecognised system: " << Sat.sysName() << " in " << __FUNCTION__; + return vector(); + } + + int bitLen = 0; + switch (messCode) + { + case RtcmMessageType::GPS_SSR_PHASE_BIAS: + bitLen = 69 + numSats * 28 + totalNumBias * 32; + break; + case RtcmMessageType::GLO_SSR_PHASE_BIAS: + bitLen = 66 + numSats * 27 + totalNumBias * 32; + break; + case RtcmMessageType::GAL_SSR_PHASE_BIAS: + bitLen = 69 + numSats * 28 + totalNumBias * 32; + break; + case RtcmMessageType::QZS_SSR_PHASE_BIAS: + bitLen = 67 + numSats * 26 + totalNumBias * 32; + break; + case RtcmMessageType::BDS_SSR_PHASE_BIAS: + bitLen = 69 + numSats * 28 + totalNumBias * 32; + break; + case RtcmMessageType::SBS_SSR_PHASE_BIAS: + bitLen = 69 + numSats * 28 + totalNumBias * 32; + break; + default: + return vector(); + } + + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + int i = 0; + // Write the header information. + int dispBiasConistInd = ssrPhasBias.ssrPhase.dispBiasConistInd; + int MWConistInd = ssrPhasBias.ssrPhase.MWConistInd; + i = encodeSsrHeader( + buf, + Sat.sys, + messCode, + ssrMeta, + ssrPhasBias.iod, + dispBiasConistInd, + MWConistInd + ); + + for (auto& [Sat, ssrPhasBias] : ssrPBMap) + { + ssrPhasBias.udi = updateInterval[ssrMeta.updateIntIndex]; // for rtcmTrace (debugging) + + SSRPhase ssrPhase = ssrPhasBias.ssrPhase; + + unsigned int nbias = ssrPhasBias.obsCodeBiasMap.size(); + int yawAngle = (int)round(ssrPhase.yawAngle * 256 / SC2RAD); + int yawRate = (int)round(ssrPhase.yawRate * 8192 / SC2RAD); + + i = setbituInc(buf, i, np, Sat.prn); + i = setbituInc(buf, i, 5, nbias); + i = setbituInc(buf, i, 9, yawAngle); + i = setbitsInc(buf, i, 8, yawRate); + + for (auto& [obsCode, entry] : ssrPhasBias.obsCodeBiasMap) + { + SSRPhaseCh ssrPhaseCh = ssrPhasBias.ssrPhaseChs[obsCode]; + + // BOOST_LOG_TRIVIAL(debug) << "Phase, obsCode : " << obsCode << "\n"; + // BOOST_LOG_TRIVIAL(debug) << "E_sys : " << sys << "\n"; + // BOOST_LOG_TRIVIAL(debug) << "mCodes_gps.size() : " << mCodes_gps.size() << "\n"; + // print_map( mCodes_gps.left, " E_ObsCode <--> RTCM ", BOOST_LOG_TRIVIAL(debug) ); + + int rtcmCode = 0; + if (Sat.sys == E_Sys::GPS) + { + rtcmCode = mCodes_gps.left.at(obsCode); + } // todo aaron, crash heaven, needs else, try + else if (Sat.sys == E_Sys::GLO) + { + rtcmCode = mCodes_glo.left.at(obsCode); + } + else if (Sat.sys == E_Sys::GAL) + { + rtcmCode = mCodes_gal.left.at(obsCode); + } + else if (Sat.sys == E_Sys::QZS) + { + rtcmCode = mCodes_qzs.left.at(obsCode); + } + else if (Sat.sys == E_Sys::BDS) + { + rtcmCode = mCodes_bds.left.at(obsCode); + } + else if (Sat.sys == E_Sys::SBS) + { + rtcmCode = mCodes_sbs.left.at(obsCode); + } + + // BOOST_LOG_TRIVIAL(debug) << "rtcmCode : " << rtcmCode << "\n"; + + int bias = (int)round(entry.bias / 0.0001); + + i = setbituInc(buf, i, 5, rtcmCode); + i = setbituInc(buf, i, 1, ssrPhaseCh.signalIntInd); + i = setbituInc(buf, i, 2, ssrPhaseCh.signalWLIntInd); + i = setbituInc(buf, i, 4, ssrPhaseCh.signalDisconCnt); + i = setbitsInc(buf, i, 20, bias); + + traceSsrPhasBias(messCode, Sat, obsCode, ssrPhasBias); + } + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Phase.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + + i = setbituInc(buf, i, bitl, 0); + + return buffer; } /** encode code bias messages -*/ + */ vector RtcmEncoder::encodeSsrCode( - SsrCBMap& ssrCBMap, ///< code biases to encode - RtcmMessageType messCode) ///< RTCM message code to encode ephemeris of + SsrCBMap& ssrCBMap, ///< code biases to encode + RtcmMessageType messCode ///< RTCM message code to encode ephemeris of +) { - int numSats = ssrCBMap.size(); - if (numSats == 0) - { - return vector(); - } - - int totalNumBias = 0; - for (auto& [Sat, ssrCodeBias] : ssrCBMap) - { - totalNumBias += ssrCodeBias.obsCodeBiasMap.size(); - } - if (totalNumBias == 0) - { - return vector(); - } - - auto& [Sat, ssrCodeBias] = *ssrCBMap.begin(); - auto& ssrMeta = ssrCodeBias.ssrMeta; - ssrMeta.numSats = numSats; - - int np = 0; - switch (Sat.sys) - { - case E_Sys::GPS: np = 6; break; - case E_Sys::GLO: np = 5; break; - case E_Sys::GAL: np = 6; break; - case E_Sys::QZS: np = 4; break; - case E_Sys::BDS: np = 6; break; - case E_Sys::SBS: np = 6; break; - default: - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system: " << Sat.sysName() << " in " << __FUNCTION__; - return vector(); - } - - int bitLen = 0; - switch (messCode) - { - case RtcmMessageType::GPS_SSR_CODE_BIAS: bitLen = 67 + numSats * 11 + totalNumBias * 19; break; - case RtcmMessageType::GLO_SSR_CODE_BIAS: bitLen = 64 + numSats * 10 + totalNumBias * 19; break; - case RtcmMessageType::GAL_SSR_CODE_BIAS: bitLen = 67 + numSats * 11 + totalNumBias * 19; break; - case RtcmMessageType::QZS_SSR_CODE_BIAS: bitLen = 65 + numSats * 9 + totalNumBias * 19; break; - case RtcmMessageType::BDS_SSR_CODE_BIAS: bitLen = 67 + numSats * 11 + totalNumBias * 19; break; - case RtcmMessageType::SBS_SSR_CODE_BIAS: bitLen = 67 + numSats * 11 + totalNumBias * 19; break; - default: return vector(); - } - - int byteLen = ceil(bitLen / 8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - int i = 0; - // Write the header information. - i = encodeSsrHeader(buf, Sat.sys, messCode, ssrMeta, ssrCodeBias.iod); - - for (auto& [Sat, ssrCodeBias] : ssrCBMap) - { - ssrCodeBias.udi = updateInterval[ssrMeta.updateIntIndex]; // for rtcmTrace (debugging) - - unsigned int nbias = ssrCodeBias.obsCodeBiasMap.size(); - - i = setbituInc(buf, i, np, Sat.prn); - i = setbituInc(buf, i, 5, nbias); - - for (auto& [obsCode, entry] : ssrCodeBias.obsCodeBiasMap) - { - int rtcmCode = 0; - if (Sat.sys == +E_Sys::GPS) { rtcmCode = mCodes_gps.left.at(obsCode); } - else if (Sat.sys == +E_Sys::GLO) { rtcmCode = mCodes_glo.left.at(obsCode); } - else if (Sat.sys == +E_Sys::GAL) { rtcmCode = mCodes_gal.left.at(obsCode); } - else if (Sat.sys == +E_Sys::QZS) { rtcmCode = mCodes_qzs.left.at(obsCode); } - else if (Sat.sys == +E_Sys::BDS) { rtcmCode = mCodes_bds.left.at(obsCode); } - else if (Sat.sys == +E_Sys::SBS) { rtcmCode = mCodes_sbs.left.at(obsCode); } - - int bias = (int)round(entry.bias / 0.01); - - i = setbituInc(buf, i, 5, rtcmCode); - i = setbitsInc(buf, i, 14, bias); - - traceSsrCodeBias(messCode, Sat, obsCode, ssrCodeBias); - } - } - - int bitl = byteLen * 8 - i; - if (bitl > 7) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Code.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - return buffer; + int numSats = ssrCBMap.size(); + if (numSats == 0) + { + return vector(); + } + + int totalNumBias = 0; + for (auto& [Sat, ssrCodeBias] : ssrCBMap) + { + totalNumBias += ssrCodeBias.obsCodeBiasMap.size(); + } + if (totalNumBias == 0) + { + return vector(); + } + + auto& [Sat, ssrCodeBias] = *ssrCBMap.begin(); + auto& ssrMeta = ssrCodeBias.ssrMeta; + ssrMeta.numSats = numSats; + + int np = 0; + switch (Sat.sys) + { + case E_Sys::GPS: + np = 6; + break; + case E_Sys::GLO: + np = 5; + break; + case E_Sys::GAL: + np = 6; + break; + case E_Sys::QZS: + np = 4; + break; + case E_Sys::BDS: + np = 6; + break; + case E_Sys::SBS: + np = 6; + break; + default: + BOOST_LOG_TRIVIAL(error) + << "Unrecognised system: " << Sat.sysName() << " in " << __FUNCTION__; + return vector(); + } + + int bitLen = 0; + switch (messCode) + { + case RtcmMessageType::GPS_SSR_CODE_BIAS: + bitLen = 67 + numSats * 11 + totalNumBias * 19; + break; + case RtcmMessageType::GLO_SSR_CODE_BIAS: + bitLen = 64 + numSats * 10 + totalNumBias * 19; + break; + case RtcmMessageType::GAL_SSR_CODE_BIAS: + bitLen = 67 + numSats * 11 + totalNumBias * 19; + break; + case RtcmMessageType::QZS_SSR_CODE_BIAS: + bitLen = 65 + numSats * 9 + totalNumBias * 19; + break; + case RtcmMessageType::BDS_SSR_CODE_BIAS: + bitLen = 67 + numSats * 11 + totalNumBias * 19; + break; + case RtcmMessageType::SBS_SSR_CODE_BIAS: + bitLen = 67 + numSats * 11 + totalNumBias * 19; + break; + default: + return vector(); + } + + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + int i = 0; + // Write the header information. + i = encodeSsrHeader(buf, Sat.sys, messCode, ssrMeta, ssrCodeBias.iod); + + for (auto& [Sat, ssrCodeBias] : ssrCBMap) + { + ssrCodeBias.udi = updateInterval[ssrMeta.updateIntIndex]; // for rtcmTrace (debugging) + + unsigned int nbias = ssrCodeBias.obsCodeBiasMap.size(); + + i = setbituInc(buf, i, np, Sat.prn); + i = setbituInc(buf, i, 5, nbias); + + for (auto& [obsCode, entry] : ssrCodeBias.obsCodeBiasMap) + { + int rtcmCode = 0; + if (Sat.sys == E_Sys::GPS) + { + rtcmCode = mCodes_gps.left.at(obsCode); + } + else if (Sat.sys == E_Sys::GLO) + { + rtcmCode = mCodes_glo.left.at(obsCode); + } + else if (Sat.sys == E_Sys::GAL) + { + rtcmCode = mCodes_gal.left.at(obsCode); + } + else if (Sat.sys == E_Sys::QZS) + { + rtcmCode = mCodes_qzs.left.at(obsCode); + } + else if (Sat.sys == E_Sys::BDS) + { + rtcmCode = mCodes_bds.left.at(obsCode); + } + else if (Sat.sys == E_Sys::SBS) + { + rtcmCode = mCodes_sbs.left.at(obsCode); + } + + int bias = (int)round(entry.bias / 0.01); + + i = setbituInc(buf, i, 5, rtcmCode); + i = setbitsInc(buf, i, 14, bias); + + traceSsrCodeBias(messCode, Sat, obsCode, ssrCodeBias); + } + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Code.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + return buffer; } /** encode URA messages -*/ + */ vector RtcmEncoder::encodeSsrUra( - SsrOutMap& ssrOutMap, ///< URAs to encode - RtcmMessageType messCode) ///< RTCM message code to encode ephemeris of + SsrOutMap& ssrOutMap, ///< URAs to encode + RtcmMessageType messCode ///< RTCM message code to encode ephemeris of +) { - int numSats = ssrOutMap.size(); - if (numSats == 0) - { - return vector(); - } - - auto& [Sat, ssrOut] = *ssrOutMap.begin(); - auto& ssrUra = ssrOut.ssrUra; - auto& ssrMeta = ssrUra.ssrMeta; - ssrMeta.numSats = numSats; - - int np = 0; - switch (Sat.sys) - { - case E_Sys::GPS: np = 6; break; - case E_Sys::GLO: np = 5; break; - case E_Sys::GAL: np = 6; break; - case E_Sys::QZS: np = 4; break; - case E_Sys::BDS: np = 6; break; - case E_Sys::SBS: np = 6; break; - default: - BOOST_LOG_TRIVIAL(error) << "Error: unrecognised system: " << Sat.sysName() << " in " << __FUNCTION__; - return vector(); - } - - int bitLen = 0; - switch (messCode) - { - case RtcmMessageType::GPS_SSR_URA: bitLen = 67 + numSats * 12; break; - case RtcmMessageType::GLO_SSR_URA: bitLen = 64 + numSats * 11; break; - case RtcmMessageType::GAL_SSR_URA: bitLen = 67 + numSats * 12; break; - case RtcmMessageType::QZS_SSR_URA: bitLen = 65 + numSats * 10; break; - case RtcmMessageType::BDS_SSR_URA: bitLen = 67 + numSats * 12; break; - case RtcmMessageType::SBS_SSR_URA: bitLen = 67 + numSats * 12; break; - default: return vector(); - } - - int byteLen = ceil(bitLen / 8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - int i = 0; - // Write the header information. - i = encodeSsrHeader(buf, Sat.sys, messCode, ssrMeta, ssrUra.iod); - - for (auto& [Sat, ssrOut] : ssrOutMap) - { - auto& ssrUra = ssrOut.ssrUra; - - ssrUra.udi = updateInterval[ssrMeta.updateIntIndex]; // for rtcmTrace (debugging) - - int uraClassValue = uraToClassValue(ssrUra.ura); - - i = setbituInc(buf, i, np, Sat.prn); - i = setbituInc(buf, i, 6, uraClassValue); - - traceSsrUra(messCode, Sat, ssrUra); - } - - int bitl = byteLen * 8 - i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding SSR URA.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - return buffer; + int numSats = ssrOutMap.size(); + if (numSats == 0) + { + return vector(); + } + + auto& [Sat, ssrOut] = *ssrOutMap.begin(); + auto& ssrUra = ssrOut.ssrUra; + auto& ssrMeta = ssrUra.ssrMeta; + ssrMeta.numSats = numSats; + + int np = 0; + switch (Sat.sys) + { + case E_Sys::GPS: + np = 6; + break; + case E_Sys::GLO: + np = 5; + break; + case E_Sys::GAL: + np = 6; + break; + case E_Sys::QZS: + np = 4; + break; + case E_Sys::BDS: + np = 6; + break; + case E_Sys::SBS: + np = 6; + break; + default: + BOOST_LOG_TRIVIAL(error) + << "Unrecognised system: " << Sat.sysName() << " in " << __FUNCTION__; + return vector(); + } + + int bitLen = 0; + switch (messCode) + { + case RtcmMessageType::GPS_SSR_URA: + bitLen = 67 + numSats * 12; + break; + case RtcmMessageType::GLO_SSR_URA: + bitLen = 64 + numSats * 11; + break; + case RtcmMessageType::GAL_SSR_URA: + bitLen = 67 + numSats * 12; + break; + case RtcmMessageType::QZS_SSR_URA: + bitLen = 65 + numSats * 10; + break; + case RtcmMessageType::BDS_SSR_URA: + bitLen = 67 + numSats * 12; + break; + case RtcmMessageType::SBS_SSR_URA: + bitLen = 67 + numSats * 12; + break; + default: + return vector(); + } + + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + int i = 0; + // Write the header information. + i = encodeSsrHeader(buf, Sat.sys, messCode, ssrMeta, ssrUra.iod); + + for (auto& [Sat, ssrOut] : ssrOutMap) + { + auto& ssrUra = ssrOut.ssrUra; + + ssrUra.udi = updateInterval[ssrMeta.updateIntIndex]; // for rtcmTrace (debugging) + + int uraClassValue = uraToClassValue(ssrUra.ura); + + i = setbituInc(buf, i, np, Sat.prn); + i = setbituInc(buf, i, 6, uraClassValue); + + traceSsrUra(messCode, Sat, ssrUra); + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding SSR URA.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + return buffer; } /** encode GPS/GAL/BDS/QZS ephemeris messages -*/ + */ vector RtcmEncoder::encodeEphemeris( - Eph& eph, ///< ephemeris to encode - RtcmMessageType messCode) ///< RTCM message code to encode ephemeris of + Eph& eph, ///< ephemeris to encode + RtcmMessageType messCode ///< RTCM message code to encode ephemeris of +) { - int bitLen = 0; - switch (messCode) - { - case RtcmMessageType:: GPS_EPHEMERIS: { bitLen = 488; break; } - case RtcmMessageType:: BDS_EPHEMERIS: { bitLen = 511; break; } - case RtcmMessageType:: QZS_EPHEMERIS: { bitLen = 485; break; } - case RtcmMessageType:: GAL_FNAV_EPHEMERIS: { bitLen = 496; break; } - case RtcmMessageType:: GAL_INAV_EPHEMERIS: { bitLen = 504; break; } - default: return vector(); - } - - int byteLen = ceil(bitLen / 8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - int i = 0; - i = setbituInc(buf, i, 12, messCode); - - auto Sat = eph.Sat; - auto type = eph.type; - if (Sat.sys == +E_Sys::GPS) - { - int idot = (int) round(eph.idot /P2_43/SC2RAD); - int tocs = (int) round(eph.tocs /16.0 ); - int f2 = (int) round(eph.f2 /P2_55); - int f1 = (int) round(eph.f1 /P2_43); - int f0 = (int) round(eph.f0 /P2_31); - int crs = (int) round(eph.crs /P2_5 ); - int deln = (int) round(eph.deln /P2_43/SC2RAD); - int M0 = (int) round(eph.M0 /P2_31/SC2RAD); - int cuc = (int) round(eph.cuc /P2_29); - unsigned int e = (unsigned int)round(eph.e /P2_33); - int cus = (int) round(eph.cus /P2_29); - unsigned int sqrtA = (unsigned int)round(eph.sqrtA /P2_19); - int toes = (int) round(eph.toes /16.0 ); - int cic = (int) round(eph.cic /P2_29); - int OMG0 = (int) round(eph.OMG0 /P2_31/SC2RAD); - int cis = (int) round(eph.cis /P2_29); - int i0 = (int) round(eph.i0 /P2_31/SC2RAD); - int crc = (int) round(eph.crc /P2_5 ); - int omg = (int) round(eph.omg /P2_31/SC2RAD); - int OMGd = (int) round(eph.OMGd /P2_43/SC2RAD); - int tgd = (int) round(eph.tgd[0]/P2_31); - - i = setbituInc(buf, i, 6, Sat.prn); - i = setbituInc(buf, i, 10, eph.weekRollOver); - i = setbituInc(buf, i, 4, eph.sva); - i = setbituInc(buf, i, 2, eph.code); - i = setbitsInc(buf, i, 14, idot); - i = setbituInc(buf, i, 8, eph.iode); - i = setbituInc(buf, i, 16, tocs); - i = setbitsInc(buf, i, 8, f2); - i = setbitsInc(buf, i, 16, f1); - i = setbitsInc(buf, i, 22, f0); - i = setbituInc(buf, i, 10, eph.iodc); - i = setbitsInc(buf, i, 16, crs); - i = setbitsInc(buf, i, 16, deln); - i = setbitsInc(buf, i, 32, M0); - i = setbitsInc(buf, i, 16, cuc); - i = setbituInc(buf, i, 32, e); - i = setbitsInc(buf, i, 16, cus); - i = setbituInc(buf, i, 32, sqrtA); - i = setbituInc(buf, i, 16, toes); - i = setbitsInc(buf, i, 16, cic); - i = setbitsInc(buf, i, 32, OMG0); - i = setbitsInc(buf, i, 16, cis); - i = setbitsInc(buf, i, 32, i0); - i = setbitsInc(buf, i, 16, crc); - i = setbitsInc(buf, i, 32, omg); - i = setbitsInc(buf, i, 24, OMGd); - i = setbitsInc(buf, i, 8, tgd); - i = setbituInc(buf, i, 6, eph.svh); - i = setbituInc(buf, i, 1, eph.flag); - i = setbituInc(buf, i, 1, eph.fitFlag); - } - else if (Sat.sys == +E_Sys::BDS) - { - int idot = (int) round(eph.idot /P2_43/SC2RAD); - int tocs = (int) round(eph.tocs /8.0 ); - int f2 = (int) round(eph.f2 /P2_66); - int f1 = (int) round(eph.f1 /P2_50); - int f0 = (int) round(eph.f0 /P2_33); - int crs = (int) round(eph.crs /P2_6 ); - int deln = (int) round(eph.deln /P2_43/SC2RAD); - int M0 = (int) round(eph.M0 /P2_31/SC2RAD); - int cuc = (int) round(eph.cuc /P2_31); - unsigned int e = (unsigned int)round(eph.e /P2_33); - int cus = (int) round(eph.cus /P2_31); - unsigned int sqrtA = (unsigned int)round(eph.sqrtA /P2_19); - int toes = (int) round(eph.toes /8.0 ); - int cic = (int) round(eph.cic /P2_31); - int OMG0 = (int) round(eph.OMG0 /P2_31/SC2RAD); - int cis = (int) round(eph.cis /P2_31); - int i0 = (int) round(eph.i0 /P2_31/SC2RAD); - int crc = (int) round(eph.crc /P2_6 ); - int omg = (int) round(eph.omg /P2_31/SC2RAD); - int OMGd = (int) round(eph.OMGd /P2_43/SC2RAD); - int tgd1 = (int) round(eph.tgd[0]/1E-10); - int tgd2 = (int) round(eph.tgd[1]/1E-10); - - i = setbituInc(buf, i, 6, Sat.prn); - i = setbituInc(buf, i, 13, eph.weekRollOver); - i = setbituInc(buf, i, 4, eph.sva); - i = setbitsInc(buf, i, 14, idot); - i = setbituInc(buf, i, 5, eph.aode); - i = setbituInc(buf, i, 17, tocs); - i = setbitsInc(buf, i, 11, f2); - i = setbitsInc(buf, i, 22, f1); - i = setbitsInc(buf, i, 24, f0); - i = setbituInc(buf, i, 5, eph.aodc); - i = setbitsInc(buf, i, 18, crs); - i = setbitsInc(buf, i, 16, deln); - i = setbitsInc(buf, i, 32, M0); - i = setbitsInc(buf, i, 18, cuc); - i = setbituInc(buf, i, 32, e); - i = setbitsInc(buf, i, 18, cus); - i = setbituInc(buf, i, 32, sqrtA); - i = setbituInc(buf, i, 17, toes); - i = setbitsInc(buf, i, 18, cic); - i = setbitsInc(buf, i, 32, OMG0); - i = setbitsInc(buf, i, 18, cis); - i = setbitsInc(buf, i, 32, i0); - i = setbitsInc(buf, i, 18, crc); - i = setbitsInc(buf, i, 32, omg); - i = setbitsInc(buf, i, 24, OMGd); - i = setbitsInc(buf, i, 10, tgd1); - i = setbitsInc(buf, i, 10, tgd2); - i = setbituInc(buf, i, 1, eph.svh); - } - else if (Sat.sys == +E_Sys::QZS) - { - int tocs = (int) round(eph.tocs /16.0 ); - int f2 = (int) round(eph.f2 /P2_55); - int f1 = (int) round(eph.f1 /P2_43); - int f0 = (int) round(eph.f0 /P2_31); - int crs = (int) round(eph.crs /P2_5 ); - int deln = (int) round(eph.deln /P2_43/SC2RAD); - int M0 = (int) round(eph.M0 /P2_31/SC2RAD); - int cuc = (int) round(eph.cuc /P2_29); - unsigned int e = (unsigned int)round(eph.e /P2_33); - int cus = (int) round(eph.cus /P2_29); - unsigned int sqrtA = (unsigned int)round(eph.sqrtA /P2_19); - int toes = (int) round(eph.toes /16.0 ); - int cic = (int) round(eph.cic /P2_29); - int OMG0 = (int) round(eph.OMG0 /P2_31/SC2RAD); - int cis = (int) round(eph.cis /P2_29); - int i0 = (int) round(eph.i0 /P2_31/SC2RAD); - int crc = (int) round(eph.crc /P2_5 ); - int omg = (int) round(eph.omg /P2_31/SC2RAD); - int OMGd = (int) round(eph.OMGd /P2_43/SC2RAD); - int idot = (int) round(eph.idot /P2_43/SC2RAD); - int tgd = (int) round(eph.tgd[0]/P2_31); - - i = setbituInc(buf, i, 4, Sat.prn); - i = setbituInc(buf, i, 16, tocs); - i = setbitsInc(buf, i, 8, f2); - i = setbitsInc(buf, i, 16, f1); - i = setbitsInc(buf, i, 22, f0); - i = setbituInc(buf, i, 8, eph.iode); - i = setbitsInc(buf, i, 16, crs); - i = setbitsInc(buf, i, 16, deln); - i = setbitsInc(buf, i, 32, M0); - i = setbitsInc(buf, i, 16, cuc); - i = setbituInc(buf, i, 32, e); - i = setbitsInc(buf, i, 16, cus); - i = setbituInc(buf, i, 32, sqrtA); - i = setbituInc(buf, i, 16, toes); - i = setbitsInc(buf, i, 16, cic); - i = setbitsInc(buf, i, 32, OMG0); - i = setbitsInc(buf, i, 16, cis); - i = setbitsInc(buf, i, 32, i0); - i = setbitsInc(buf, i, 16, crc); - i = setbitsInc(buf, i, 32, omg); - i = setbitsInc(buf, i, 24, OMGd); - i = setbitsInc(buf, i, 14, idot); - i = setbituInc(buf, i, 2, eph.code); - i = setbituInc(buf, i, 10, eph.weekRollOver); - i = setbituInc(buf, i, 4, eph.sva); - i = setbituInc(buf, i, 6, eph.svh); - i = setbitsInc(buf, i, 8, tgd); - i = setbituInc(buf, i, 10, eph.iodc); - i = setbituInc(buf, i, 1, eph.fitFlag); - } - else if (Sat.sys == +E_Sys::GAL) - { - int idot = (int) round(eph.idot /P2_43/SC2RAD); - int tocs = (int) round(eph.tocs /60.0 ); - int f2 = (int) round(eph.f2 /P2_59); - int f1 = (int) round(eph.f1 /P2_46); - int f0 = (int) round(eph.f0 /P2_34); - int crs = (int) round(eph.crs /P2_5 ); - int deln = (int) round(eph.deln /P2_43/SC2RAD); - int M0 = (int) round(eph.M0 /P2_31/SC2RAD); - int cuc = (int) round(eph.cuc /P2_29); - unsigned int e = (unsigned int)round(eph.e /P2_33); - int cus = (int) round(eph.cus /P2_29); - unsigned int sqrtA = (unsigned int)round(eph.sqrtA /P2_19); - int toes = (int) round(eph.toes /60.0 ); - int cic = (int) round(eph.cic /P2_29); - int OMG0 = (int) round(eph.OMG0 /P2_31/SC2RAD); - int cis = (int) round(eph.cis /P2_29); - int i0 = (int) round(eph.i0 /P2_31/SC2RAD); - int crc = (int) round(eph.crc /P2_5 ); - int omg = (int) round(eph.omg /P2_31/SC2RAD); - int OMGd = (int) round(eph.OMGd /P2_43/SC2RAD); - int bgd1 = (int) round(eph.tgd[0]/P2_32); - int bgd2 = (int) round(eph.tgd[1]/P2_32); - - i = setbituInc(buf, i, 6, Sat.prn); - i = setbituInc(buf, i, 12, eph.weekRollOver); - i = setbituInc(buf, i, 10, eph.iode); - i = setbituInc(buf, i, 8, eph.sva); - i = setbitsInc(buf, i, 14, idot); - i = setbituInc(buf, i, 14, tocs); - i = setbitsInc(buf, i, 6, f2); - i = setbitsInc(buf, i, 21, f1); - i = setbitsInc(buf, i, 31, f0); - i = setbitsInc(buf, i, 16, crs); - i = setbitsInc(buf, i, 16, deln); - i = setbitsInc(buf, i, 32, M0); - i = setbitsInc(buf, i, 16, cuc); - i = setbituInc(buf, i, 32, e); - i = setbitsInc(buf, i, 16, cus); - i = setbituInc(buf, i, 32, sqrtA); - i = setbituInc(buf, i, 14, toes); - i = setbitsInc(buf, i, 16, cic); - i = setbitsInc(buf, i, 32, OMG0); - i = setbitsInc(buf, i, 16, cis); - i = setbitsInc(buf, i, 32, i0); - i = setbitsInc(buf, i, 16, crc); - i = setbitsInc(buf, i, 32, omg); - i = setbitsInc(buf, i, 24, OMGd); - i = setbitsInc(buf, i, 10, bgd1); - - if (type == +E_NavMsgType::FNAV) - { - i = setbituInc(buf, i, 2, eph.e5a_hs); - i = setbituInc(buf, i, 1, eph.e5a_dvs); - i = setbituInc(buf, i, 7, 0); /* reserved */ - } - else if (type == +E_NavMsgType::INAV) - { - i = setbitsInc(buf, i, 10, bgd2); - i = setbituInc(buf, i, 2, eph.e5b_hs); - i = setbituInc(buf, i, 1, eph.e5b_dvs); - i = setbituInc(buf, i, 2, eph.e1_hs); - i = setbituInc(buf, i, 1, eph.e1_dvs); - } - } - - int bitl = byteLen * 8 - i; - if (bitl > 7) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding ephmeris.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - if (acsConfig.output_encoded_rtcm_json) - traceBrdcEph(messCode, eph); - - return buffer; + int bitLen = 0; + switch (messCode) + { + case RtcmMessageType::GPS_EPHEMERIS: + { + bitLen = 488; + break; + } + case RtcmMessageType::BDS_EPHEMERIS: + { + bitLen = 511; + break; + } + case RtcmMessageType::QZS_EPHEMERIS: + { + bitLen = 485; + break; + } + case RtcmMessageType::GAL_FNAV_EPHEMERIS: + { + bitLen = 496; + break; + } + case RtcmMessageType::GAL_INAV_EPHEMERIS: + { + bitLen = 504; + break; + } + default: + return vector(); + } + + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + int i = 0; + i = setbituInc(buf, i, 12, rtcmTypeToMessageNumber(messCode)); + + auto Sat = eph.Sat; + auto type = eph.type; + if (Sat.sys == E_Sys::GPS) + { + int idot = (int)round(eph.idot / P2_43 / SC2RAD); + int tocs = (int)round(eph.tocs / 16.0); + int f2 = (int)round(eph.f2 / P2_55); + int f1 = (int)round(eph.f1 / P2_43); + int f0 = (int)round(eph.f0 / P2_31); + int crs = (int)round(eph.crs / P2_5); + int deln = (int)round(eph.deln / P2_43 / SC2RAD); + int M0 = (int)round(eph.M0 / P2_31 / SC2RAD); + int cuc = (int)round(eph.cuc / P2_29); + unsigned int e = (unsigned int)round(eph.e / P2_33); + int cus = (int)round(eph.cus / P2_29); + unsigned int sqrtA = (unsigned int)round(eph.sqrtA / P2_19); + int toes = (int)round(eph.toes / 16.0); + int cic = (int)round(eph.cic / P2_29); + int OMG0 = (int)round(eph.OMG0 / P2_31 / SC2RAD); + int cis = (int)round(eph.cis / P2_29); + int i0 = (int)round(eph.i0 / P2_31 / SC2RAD); + int crc = (int)round(eph.crc / P2_5); + int omg = (int)round(eph.omg / P2_31 / SC2RAD); + int OMGd = (int)round(eph.OMGd / P2_43 / SC2RAD); + int tgd = (int)round(eph.tgd[0] / P2_31); + + i = setbituInc(buf, i, 6, Sat.prn); + i = setbituInc(buf, i, 10, eph.weekRollOver); + i = setbituInc(buf, i, 4, eph.sva); + i = setbituInc(buf, i, 2, eph.code); + i = setbitsInc(buf, i, 14, idot); + i = setbituInc(buf, i, 8, eph.iode); + i = setbituInc(buf, i, 16, tocs); + i = setbitsInc(buf, i, 8, f2); + i = setbitsInc(buf, i, 16, f1); + i = setbitsInc(buf, i, 22, f0); + i = setbituInc(buf, i, 10, eph.iodc); + i = setbitsInc(buf, i, 16, crs); + i = setbitsInc(buf, i, 16, deln); + i = setbitsInc(buf, i, 32, M0); + i = setbitsInc(buf, i, 16, cuc); + i = setbituInc(buf, i, 32, e); + i = setbitsInc(buf, i, 16, cus); + i = setbituInc(buf, i, 32, sqrtA); + i = setbituInc(buf, i, 16, toes); + i = setbitsInc(buf, i, 16, cic); + i = setbitsInc(buf, i, 32, OMG0); + i = setbitsInc(buf, i, 16, cis); + i = setbitsInc(buf, i, 32, i0); + i = setbitsInc(buf, i, 16, crc); + i = setbitsInc(buf, i, 32, omg); + i = setbitsInc(buf, i, 24, OMGd); + i = setbitsInc(buf, i, 8, tgd); + i = setbituInc(buf, i, 6, eph.svh); + i = setbituInc(buf, i, 1, eph.flag); + i = setbituInc(buf, i, 1, eph.fitFlag); + } + else if (Sat.sys == E_Sys::BDS) + { + int idot = (int)round(eph.idot / P2_43 / SC2RAD); + int tocs = (int)round(eph.tocs / 8.0); + int f2 = (int)round(eph.f2 / P2_66); + int f1 = (int)round(eph.f1 / P2_50); + int f0 = (int)round(eph.f0 / P2_33); + int crs = (int)round(eph.crs / P2_6); + int deln = (int)round(eph.deln / P2_43 / SC2RAD); + int M0 = (int)round(eph.M0 / P2_31 / SC2RAD); + int cuc = (int)round(eph.cuc / P2_31); + unsigned int e = (unsigned int)round(eph.e / P2_33); + int cus = (int)round(eph.cus / P2_31); + unsigned int sqrtA = (unsigned int)round(eph.sqrtA / P2_19); + int toes = (int)round(eph.toes / 8.0); + int cic = (int)round(eph.cic / P2_31); + int OMG0 = (int)round(eph.OMG0 / P2_31 / SC2RAD); + int cis = (int)round(eph.cis / P2_31); + int i0 = (int)round(eph.i0 / P2_31 / SC2RAD); + int crc = (int)round(eph.crc / P2_6); + int omg = (int)round(eph.omg / P2_31 / SC2RAD); + int OMGd = (int)round(eph.OMGd / P2_43 / SC2RAD); + int tgd1 = (int)round(eph.tgd[0] / 1E-10); + int tgd2 = (int)round(eph.tgd[1] / 1E-10); + + i = setbituInc(buf, i, 6, Sat.prn); + i = setbituInc(buf, i, 13, eph.weekRollOver); + i = setbituInc(buf, i, 4, eph.sva); + i = setbitsInc(buf, i, 14, idot); + i = setbituInc(buf, i, 5, eph.aode); + i = setbituInc(buf, i, 17, tocs); + i = setbitsInc(buf, i, 11, f2); + i = setbitsInc(buf, i, 22, f1); + i = setbitsInc(buf, i, 24, f0); + i = setbituInc(buf, i, 5, eph.aodc); + i = setbitsInc(buf, i, 18, crs); + i = setbitsInc(buf, i, 16, deln); + i = setbitsInc(buf, i, 32, M0); + i = setbitsInc(buf, i, 18, cuc); + i = setbituInc(buf, i, 32, e); + i = setbitsInc(buf, i, 18, cus); + i = setbituInc(buf, i, 32, sqrtA); + i = setbituInc(buf, i, 17, toes); + i = setbitsInc(buf, i, 18, cic); + i = setbitsInc(buf, i, 32, OMG0); + i = setbitsInc(buf, i, 18, cis); + i = setbitsInc(buf, i, 32, i0); + i = setbitsInc(buf, i, 18, crc); + i = setbitsInc(buf, i, 32, omg); + i = setbitsInc(buf, i, 24, OMGd); + i = setbitsInc(buf, i, 10, tgd1); + i = setbitsInc(buf, i, 10, tgd2); + i = setbituInc(buf, i, 1, eph.svh); + } + else if (Sat.sys == E_Sys::QZS) + { + int tocs = (int)round(eph.tocs / 16.0); + int f2 = (int)round(eph.f2 / P2_55); + int f1 = (int)round(eph.f1 / P2_43); + int f0 = (int)round(eph.f0 / P2_31); + int crs = (int)round(eph.crs / P2_5); + int deln = (int)round(eph.deln / P2_43 / SC2RAD); + int M0 = (int)round(eph.M0 / P2_31 / SC2RAD); + int cuc = (int)round(eph.cuc / P2_29); + unsigned int e = (unsigned int)round(eph.e / P2_33); + int cus = (int)round(eph.cus / P2_29); + unsigned int sqrtA = (unsigned int)round(eph.sqrtA / P2_19); + int toes = (int)round(eph.toes / 16.0); + int cic = (int)round(eph.cic / P2_29); + int OMG0 = (int)round(eph.OMG0 / P2_31 / SC2RAD); + int cis = (int)round(eph.cis / P2_29); + int i0 = (int)round(eph.i0 / P2_31 / SC2RAD); + int crc = (int)round(eph.crc / P2_5); + int omg = (int)round(eph.omg / P2_31 / SC2RAD); + int OMGd = (int)round(eph.OMGd / P2_43 / SC2RAD); + int idot = (int)round(eph.idot / P2_43 / SC2RAD); + int tgd = (int)round(eph.tgd[0] / P2_31); + + i = setbituInc(buf, i, 4, Sat.prn); + i = setbituInc(buf, i, 16, tocs); + i = setbitsInc(buf, i, 8, f2); + i = setbitsInc(buf, i, 16, f1); + i = setbitsInc(buf, i, 22, f0); + i = setbituInc(buf, i, 8, eph.iode); + i = setbitsInc(buf, i, 16, crs); + i = setbitsInc(buf, i, 16, deln); + i = setbitsInc(buf, i, 32, M0); + i = setbitsInc(buf, i, 16, cuc); + i = setbituInc(buf, i, 32, e); + i = setbitsInc(buf, i, 16, cus); + i = setbituInc(buf, i, 32, sqrtA); + i = setbituInc(buf, i, 16, toes); + i = setbitsInc(buf, i, 16, cic); + i = setbitsInc(buf, i, 32, OMG0); + i = setbitsInc(buf, i, 16, cis); + i = setbitsInc(buf, i, 32, i0); + i = setbitsInc(buf, i, 16, crc); + i = setbitsInc(buf, i, 32, omg); + i = setbitsInc(buf, i, 24, OMGd); + i = setbitsInc(buf, i, 14, idot); + i = setbituInc(buf, i, 2, eph.code); + i = setbituInc(buf, i, 10, eph.weekRollOver); + i = setbituInc(buf, i, 4, eph.sva); + i = setbituInc(buf, i, 6, eph.svh); + i = setbitsInc(buf, i, 8, tgd); + i = setbituInc(buf, i, 10, eph.iodc); + i = setbituInc(buf, i, 1, eph.fitFlag); + } + else if (Sat.sys == E_Sys::GAL) + { + int idot = (int)round(eph.idot / P2_43 / SC2RAD); + int tocs = (int)round(eph.tocs / 60.0); + int f2 = (int)round(eph.f2 / P2_59); + int f1 = (int)round(eph.f1 / P2_46); + int f0 = (int)round(eph.f0 / P2_34); + int crs = (int)round(eph.crs / P2_5); + int deln = (int)round(eph.deln / P2_43 / SC2RAD); + int M0 = (int)round(eph.M0 / P2_31 / SC2RAD); + int cuc = (int)round(eph.cuc / P2_29); + unsigned int e = (unsigned int)round(eph.e / P2_33); + int cus = (int)round(eph.cus / P2_29); + unsigned int sqrtA = (unsigned int)round(eph.sqrtA / P2_19); + int toes = (int)round(eph.toes / 60.0); + int cic = (int)round(eph.cic / P2_29); + int OMG0 = (int)round(eph.OMG0 / P2_31 / SC2RAD); + int cis = (int)round(eph.cis / P2_29); + int i0 = (int)round(eph.i0 / P2_31 / SC2RAD); + int crc = (int)round(eph.crc / P2_5); + int omg = (int)round(eph.omg / P2_31 / SC2RAD); + int OMGd = (int)round(eph.OMGd / P2_43 / SC2RAD); + int bgd1 = (int)round(eph.tgd[0] / P2_32); + int bgd2 = (int)round(eph.tgd[1] / P2_32); + + i = setbituInc(buf, i, 6, Sat.prn); + i = setbituInc(buf, i, 12, eph.weekRollOver); + i = setbituInc(buf, i, 10, eph.iode); + i = setbituInc(buf, i, 8, eph.sva); + i = setbitsInc(buf, i, 14, idot); + i = setbituInc(buf, i, 14, tocs); + i = setbitsInc(buf, i, 6, f2); + i = setbitsInc(buf, i, 21, f1); + i = setbitsInc(buf, i, 31, f0); + i = setbitsInc(buf, i, 16, crs); + i = setbitsInc(buf, i, 16, deln); + i = setbitsInc(buf, i, 32, M0); + i = setbitsInc(buf, i, 16, cuc); + i = setbituInc(buf, i, 32, e); + i = setbitsInc(buf, i, 16, cus); + i = setbituInc(buf, i, 32, sqrtA); + i = setbituInc(buf, i, 14, toes); + i = setbitsInc(buf, i, 16, cic); + i = setbitsInc(buf, i, 32, OMG0); + i = setbitsInc(buf, i, 16, cis); + i = setbitsInc(buf, i, 32, i0); + i = setbitsInc(buf, i, 16, crc); + i = setbitsInc(buf, i, 32, omg); + i = setbitsInc(buf, i, 24, OMGd); + i = setbitsInc(buf, i, 10, bgd1); + + if (type == E_NavMsgType::FNAV) + { + i = setbituInc(buf, i, 2, eph.e5a_hs); + i = setbituInc(buf, i, 1, eph.e5a_dvs); + i = setbituInc(buf, i, 7, 0); /* reserved */ + } + else if (type == E_NavMsgType::INAV) + { + i = setbitsInc(buf, i, 10, bgd2); + i = setbituInc(buf, i, 2, eph.e5b_hs); + i = setbituInc(buf, i, 1, eph.e5b_dvs); + i = setbituInc(buf, i, 2, eph.e1_hs); + i = setbituInc(buf, i, 1, eph.e1_dvs); + } + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding ephmeris.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + if (acsConfig.output_encoded_rtcm_json) + traceBrdcEph(messCode, eph); + + return buffer; } /** encode GLO ephemeris messages -*/ + */ vector RtcmEncoder::encodeEphemeris( - Geph& geph, ///< ephemeris to encode - RtcmMessageType messCode) ///< RTCM message code to encode ephemeris of + Geph& geph, ///< ephemeris to encode + RtcmMessageType messCode ///< RTCM message code to encode ephemeris of +) { - int bitLen = 360; - - int byteLen = ceil(bitLen / 8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - int i = 0; - i = setbituInc(buf, i, 12, messCode); - - auto Sat = geph.Sat; - { - int vel[3], pos[3], acc[3]; - for (int j=0; j<3; j++) - { - vel[j] = (int)round(geph.vel[j]/P2_20/1E3); - pos[j] = (int)round(geph.pos[j]/P2_11/1E3); - acc[j] = (int)round(geph.acc[j]/P2_30/1E3); - } - - int gammaN = (int)round(geph.gammaN / P2_40); - int taun = (int)round(geph.taun / P2_30); - int dtaun = (int)round(geph.dtaun / P2_30); - - i = setbituInc(buf, i, 6, Sat.prn); - i = setbituInc(buf, i, 5, geph.frq+7); - i = setbituInc(buf, i, 4, 0); // almanac health, P1 - i = setbituInc(buf, i, 5, geph.tk_hour); - i = setbituInc(buf, i, 6, geph.tk_min); - i = setbituInc(buf, i, 1, geph.tk_sec); - i = setbituInc(buf, i, 1, geph.svh); - i = setbituInc(buf, i, 1, 0); // P2 - i = setbituInc(buf, i, 7, geph.tb); - i = setbitgInc(buf, i, 24, vel[0]); - i = setbitgInc(buf, i, 27, pos[0]); - i = setbitgInc(buf, i, 5, acc[0]); - i = setbitgInc(buf, i, 24, vel[1]); - i = setbitgInc(buf, i, 27, pos[1]); - i = setbitgInc(buf, i, 5, acc[1]); - i = setbitgInc(buf, i, 24, vel[2]); - i = setbitgInc(buf, i, 27, pos[2]); - i = setbitgInc(buf, i, 5, acc[2]); - i = setbituInc(buf, i, 1, 0); // P3 - i = setbitgInc(buf, i, 11, gammaN); - i = setbituInc(buf, i, 3, 0); // P, ln - i = setbitgInc(buf, i, 22, taun); - i = setbitgInc(buf, i, 5, dtaun); - i = setbituInc(buf, i, 5, geph.age); - i = setbituInc(buf, i, 5, 0); // P4, FT - i = setbituInc(buf, i, 11, geph.NT); // GLONASS-M only - i = setbituInc(buf, i, 2, geph.glonassM); // M (if GLONASS-M data feilds valid) - i = setbituInc(buf, i, 1, geph.moreData); // availability of additional data - i = setbituInc(buf, i, 11, 0); // NA - i = setbitgInc(buf, i, 32, 0); // tauc - i = setbituInc(buf, i, 5, geph.N4); // additional data and GLONASS-M only - i = setbitgInc(buf, i, 22, 0); // tauGPS - i = setbituInc(buf, i, 1, 0); // ln - i = setbituInc(buf, i, 7, 0); // reserved - } - - int bitl = byteLen * 8 - i; - if (bitl > 7) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding ephmeris.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - if (acsConfig.output_encoded_rtcm_json) - traceBrdcEph(messCode, geph); - - return buffer; + int bitLen = 360; + + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + int i = 0; + i = setbituInc(buf, i, 12, rtcmTypeToMessageNumber(messCode)); + + auto Sat = geph.Sat; + { + int vel[3], pos[3], acc[3]; + for (int j = 0; j < 3; j++) + { + vel[j] = (int)round(geph.vel[j] / P2_20 / 1E3); + pos[j] = (int)round(geph.pos[j] / P2_11 / 1E3); + acc[j] = (int)round(geph.acc[j] / P2_30 / 1E3); + } + + int gammaN = (int)round(geph.gammaN / P2_40); + int taun = (int)round(geph.taun / P2_30); + int dtaun = (int)round(geph.dtaun / P2_30); + + i = setbituInc(buf, i, 6, Sat.prn); + i = setbituInc(buf, i, 5, geph.frq + 7); + i = setbituInc(buf, i, 4, 0); // almanac health, P1 + i = setbituInc(buf, i, 5, geph.tk_hour); + i = setbituInc(buf, i, 6, geph.tk_min); + i = setbituInc(buf, i, 1, geph.tk_sec); + i = setbituInc(buf, i, 1, geph.svh); + i = setbituInc(buf, i, 1, 0); // P2 + i = setbituInc(buf, i, 7, geph.tb); + i = setbitgInc(buf, i, 24, vel[0]); + i = setbitgInc(buf, i, 27, pos[0]); + i = setbitgInc(buf, i, 5, acc[0]); + i = setbitgInc(buf, i, 24, vel[1]); + i = setbitgInc(buf, i, 27, pos[1]); + i = setbitgInc(buf, i, 5, acc[1]); + i = setbitgInc(buf, i, 24, vel[2]); + i = setbitgInc(buf, i, 27, pos[2]); + i = setbitgInc(buf, i, 5, acc[2]); + i = setbituInc(buf, i, 1, 0); // P3 + i = setbitgInc(buf, i, 11, gammaN); + i = setbituInc(buf, i, 3, 0); // P, ln + i = setbitgInc(buf, i, 22, taun); + i = setbitgInc(buf, i, 5, dtaun); + i = setbituInc(buf, i, 5, geph.age); + i = setbituInc(buf, i, 5, 0); // P4, FT + i = setbituInc(buf, i, 11, geph.NT); // GLONASS-M only + i = setbituInc(buf, i, 2, geph.glonassM); // M (if GLONASS-M data feilds valid) + i = setbituInc(buf, i, 1, geph.moreData); // availability of additional data + i = setbituInc(buf, i, 11, 0); // NA + i = setbitgInc(buf, i, 32, 0); // tauc + i = setbituInc(buf, i, 5, geph.N4); // additional data and GLONASS-M only + i = setbitgInc(buf, i, 22, 0); // tauGPS + i = setbituInc(buf, i, 1, 0); // ln + i = setbituInc(buf, i, 7, 0); // reserved + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding ephmeris.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + if (acsConfig.output_encoded_rtcm_json) + traceBrdcEph(messCode, geph); + + return buffer; } - /** set unsigned bits to byte data -*/ + */ void setbitu( - unsigned char* buff, ///< byte data - int pos, ///< bit position from start of data (bits) - int len, ///< bit length (bits) (len<=32) - unsigned int value) ///< value to set + unsigned char* buff, ///< byte data + int pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + unsigned int value ///< value to set +) { - unsigned int mask=1u<<(len-1); - - if ( len <= 0 - ||len > 32) - { - return; - } - - unsigned long int invalid = (1ul<= invalid) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: " << __FUNCTION__ << " has data outside range\n"; - } - - for (int i = pos; i < pos+len; i++, mask >>= 1) - { - if (value&mask) buff[i/8] |= (1u<<(7-i%8)); - else buff[i/8] &= ~(1u<<(7-i%8)); - } + unsigned int mask = 1u << (len - 1); + + if (len <= 0 || len > 32) + { + return; + } + + unsigned long int invalid = (1ul << len); + + if (value >= invalid) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " has data outside range\n"; + } + + for (int i = pos; i < pos + len; i++, mask >>= 1) + { + if (value & mask) + buff[i / 8] |= (1u << (7 - i % 8)); + else + buff[i / 8] &= ~(1u << (7 - i % 8)); + } } /** set signed bits to byte data -*/ + */ void setbits( - unsigned char* buff, ///< byte data - int pos, ///< bit position from start of data (bits) - int len, ///< bit length (bits) (len<=32) - int value) ///< value to set + unsigned char* buff, ///< byte data + int pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + int value ///< value to set +) { - unsigned int mask = 1u<<(len-1); - - if ( len <= 0 - ||len > 32) - { - return; - } - - long int invalid = (1ul<<(len-1)); - - if ( +value >= invalid - ||-value >= invalid) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: " << __FUNCTION__ << " has data outside range, setting invalid\n"; - value = -invalid; - } - - for (int i = pos; i < pos+len; i++, mask >>= 1) - { - if (value&mask) buff[i/8] |= (1u<<(7-i%8)); - else buff[i/8] &= ~(1u<<(7-i%8)); - } + unsigned int mask = 1u << (len - 1); + + if (len <= 0 || len > 32) + { + return; + } + + long int invalid = (1ul << (len - 1)); + + if (+value >= invalid || -value >= invalid) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " has data outside range, setting invalid\n"; + value = -invalid; + } + + for (int i = pos; i < pos + len; i++, mask >>= 1) + { + if (value & mask) + buff[i / 8] |= (1u << (7 - i % 8)); + else + buff[i / 8] &= ~(1u << (7 - i % 8)); + } } /** increasingly set unsigned bits to byte data -*/ + */ int setbituInc( - unsigned char* buff, ///< byte data - int pos, ///< bit position from start of data (bits) - int len, ///< bit length (bits) (len<=32) - unsigned int value) ///< value to set + unsigned char* buff, ///< byte data + int pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + unsigned int value ///< value to set +) { - setbitu(buff, pos, len, value); - return pos + len; + setbitu(buff, pos, len, value); + return pos + len; } /** increasingly set signed bits to byte data -*/ + */ int setbitsInc( - unsigned char* buff, ///< byte data - int pos, ///< bit position from start of data (bits) - int len, ///< bit length (bits) (len<=32) - int value) ///< value to set + unsigned char* buff, ///< byte data + int pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + int value ///< value to set +) { - setbits(buff, pos, len, value); - return pos + len; + setbits(buff, pos, len, value); + return pos + len; } /** set sign-magnitude bits applied in GLO nav messages -*/ + */ void setbitg( - unsigned char* buff, ///< byte data - int pos, ///< bit position from start of data (bits) - int len, ///< bit length (bits) (len<=32) - int value) ///< value to set + unsigned char* buff, ///< byte data + int pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + int value ///< value to set +) { - setbitu(buff, pos, 1, value<0?1:0); - setbitu(buff, pos+1, len-1, value<0?-value:value); + setbitu(buff, pos, 1, value < 0 ? 1 : 0); + setbitu(buff, pos + 1, len - 1, value < 0 ? -value : value); } /** increasingly set sign-magnitude bits applied in GLO nav messages -*/ + */ int setbitgInc( - unsigned char* buff, ///< byte data - int pos, ///< bit position from start of data (bits) - int len, ///< bit length (bits) (len<=32) - int value) ///< value to set + unsigned char* buff, ///< byte data + int pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + int value ///< value to set +) { - setbitg(buff, pos, len, value); - return pos + len; + setbitg(buff, pos, len, value); + return pos + len; } diff --git a/src/cpp/common/rtcmEncoder.hpp b/src/cpp/common/rtcmEncoder.hpp index 0411e6d3c..0ae228ce5 100644 --- a/src/cpp/common/rtcmEncoder.hpp +++ b/src/cpp/common/rtcmEncoder.hpp @@ -1,97 +1,77 @@ - #pragma once -#include "rtcmTrace.hpp" -#include "satSys.hpp" -#include "enums.h" -#include "ssr.hpp" - +#include "common/enums.h" +#include "common/rtcmTrace.hpp" +#include "common/satSys.hpp" +#include "common/ssr.hpp" using std::pair; -typedef map SsrEphMap; -typedef map SsrClkMap; -typedef map SsrUraMap; -typedef map SsrHRClkMap; -typedef map SsrCBMap; -typedef map SsrPBMap; - -typedef map SsrOutMap; +typedef map SsrEphMap; +typedef map SsrClkMap; +typedef map SsrUraMap; +typedef map SsrHRClkMap; +typedef map SsrCBMap; +typedef map SsrPBMap; +typedef map SsrOutMap; void calculateSsrComb( - GTime referenceTime, - int udi, - SSRMeta& ssrMeta, - int masterIod, - SsrOutMap& ssrOutMap); + GTime referenceTime, + int udi, + SSRMeta& ssrMeta, + int masterIod, + SsrOutMap& ssrOutMap +); struct RtcmEncoder : RtcmTrace { - RtcmEncoder( - string rtcmMountpoint = "", - string rtcmTraceFilename = "") - : RtcmTrace {rtcmMountpoint, rtcmTraceFilename} - { - - } - - constexpr static int updateInterval[16] = - { - 1, 2, 5, 10, 15, 30, 60, 120, 240, 300, 600, 900, 1800, 3600, 7200, 10800 - }; - - vector data; - - int masterIod = 1; - - SsrEphMap lastRegSsrEphMap; ///< last SSR orbit corrections uploaded, used to check IODE's - SsrClkMap lastRegSsrClkMap; ///< last regular SSR clock corrections uploaded, used to calculate high rate SSR clock corrections - - static int getUdiIndex( - int udi); - - void encodeWriteMessages( - std::ostream& outputStream); - - bool encodeWriteMessageToBuffer( - vector& buffer); - - vector encodeTimeStampRTCM(); - - int encodeSsrHeader( - unsigned char* buf, - E_Sys sys, - RtcmMessageType messCode, - SSRMeta& ssrMeta, - int iod, - int dispBiasConistInd = -1, - int MWConistInd = -1); - - vector encodeSsrOrbClk( - SsrOutMap& ssrOutMap, - RtcmMessageType messCode); - - vector encodeSsrUra( - SsrOutMap& ssrOutMap, - RtcmMessageType messCode); - - vector encodeSsrCode( - SsrCBMap& ssrCBMap, - RtcmMessageType messCode); - - vector encodeSsrPhase( - SsrPBMap& ssrPBMap, - RtcmMessageType messCode); - - vector encodeEphemeris( - Eph& eph, - RtcmMessageType messCode); - - vector encodeEphemeris( - Geph& geph, - RtcmMessageType messCode); + RtcmEncoder(string rtcmMountpoint = "", string rtcmTraceFilename = "") + : RtcmTrace{rtcmMountpoint, rtcmTraceFilename} + { + } + + constexpr static int updateInterval[16] = + {1, 2, 5, 10, 15, 30, 60, 120, 240, 300, 600, 900, 1800, 3600, 7200, 10800}; + + vector data; + + int masterIod = 1; + + SsrEphMap lastRegSsrEphMap; ///< last SSR orbit corrections uploaded, used to check IODE's + SsrClkMap lastRegSsrClkMap; ///< last regular SSR clock corrections uploaded, used to calculate + ///< high rate SSR clock corrections + + static int getUdiIndex(int udi); + + void encodeWriteMessages(std::ostream& outputStream); + + bool encodeWriteMessageToBuffer(vector& buffer); + + vector encodeTimeStampRTCM(); + + int encodeSsrHeader( + unsigned char* buf, + E_Sys sys, + RtcmMessageType messCode, + SSRMeta& ssrMeta, + int iod, + int dispBiasConistInd = -1, + int MWConistInd = -1 + ); + + vector encodeSsrOrbClk(SsrOutMap& ssrOutMap, RtcmMessageType messCode); + + vector encodeSsrUra(SsrOutMap& ssrOutMap, RtcmMessageType messCode); + + vector encodeSsrCode(SsrCBMap& ssrCBMap, RtcmMessageType messCode); + + vector encodeSsrPhase(SsrPBMap& ssrPBMap, RtcmMessageType messCode); + + vector encodeEphemeris(Eph& eph, RtcmMessageType messCode); + + vector encodeEphemeris(Geph& geph, RtcmMessageType messCode); }; -int setbitsInc(unsigned char *buff, int pos, int len, const int value); -int setbituInc(unsigned char *buff, int pos, int len, const unsigned int value); -int setbitgInc(unsigned char *buff, int pos, int len, const int value); +int setbitsInc(unsigned char* buff, int pos, int len, const int value); +int setbituInc(unsigned char* buff, int pos, int len, const unsigned int value); +int setbitgInc(unsigned char* buff, int pos, int len, const int value); diff --git a/src/cpp/common/rtcmTrace.cpp b/src/cpp/common/rtcmTrace.cpp index 25801fbca..efcd0f2be 100644 --- a/src/cpp/common/rtcmTrace.cpp +++ b/src/cpp/common/rtcmTrace.cpp @@ -1,726 +1,702 @@ - // #pragma GCC optimize ("O0") -#include - -#include "rtcmTrace.hpp" -#include "ephemeris.hpp" -#include "common.hpp" -#include "ssr.hpp" - -using bsoncxx::builder::basic::kvp; - -map rtcmMessageSystemMap = -{ - {RtcmMessageType::GPS_EPHEMERIS , E_Sys::GPS}, - {RtcmMessageType::GLO_EPHEMERIS , E_Sys::GLO}, - {RtcmMessageType::BDS_EPHEMERIS , E_Sys::BDS}, - {RtcmMessageType::QZS_EPHEMERIS , E_Sys::QZS}, - {RtcmMessageType::GAL_FNAV_EPHEMERIS , E_Sys::GAL}, - {RtcmMessageType::GAL_INAV_EPHEMERIS , E_Sys::GAL}, - {RtcmMessageType::GPS_SSR_ORB_CORR , E_Sys::GPS}, - {RtcmMessageType::GPS_SSR_CLK_CORR , E_Sys::GPS}, - {RtcmMessageType::GPS_SSR_CODE_BIAS , E_Sys::GPS}, - {RtcmMessageType::GPS_SSR_COMB_CORR , E_Sys::GPS}, - {RtcmMessageType::GPS_SSR_URA , E_Sys::GPS}, - {RtcmMessageType::GPS_SSR_HR_CLK_CORR , E_Sys::GPS}, - {RtcmMessageType::GLO_SSR_ORB_CORR , E_Sys::GLO}, - {RtcmMessageType::GLO_SSR_CLK_CORR , E_Sys::GLO}, - {RtcmMessageType::GLO_SSR_CODE_BIAS , E_Sys::GLO}, - {RtcmMessageType::GLO_SSR_COMB_CORR , E_Sys::GLO}, - {RtcmMessageType::GLO_SSR_URA , E_Sys::GLO}, - {RtcmMessageType::GLO_SSR_HR_CLK_CORR , E_Sys::GLO}, - {RtcmMessageType::MSM4_GPS , E_Sys::GPS}, - {RtcmMessageType::MSM5_GPS , E_Sys::GPS}, - {RtcmMessageType::MSM6_GPS , E_Sys::GPS}, - {RtcmMessageType::MSM7_GPS , E_Sys::GPS}, - {RtcmMessageType::MSM4_GLONASS , E_Sys::GLO}, - {RtcmMessageType::MSM5_GLONASS , E_Sys::GLO}, - {RtcmMessageType::MSM6_GLONASS , E_Sys::GLO}, - {RtcmMessageType::MSM7_GLONASS , E_Sys::GLO}, - {RtcmMessageType::MSM4_GALILEO , E_Sys::GAL}, - {RtcmMessageType::MSM5_GALILEO , E_Sys::GAL}, - {RtcmMessageType::MSM6_GALILEO , E_Sys::GAL}, - {RtcmMessageType::MSM7_GALILEO , E_Sys::GAL}, - {RtcmMessageType::MSM4_QZSS , E_Sys::QZS}, - {RtcmMessageType::MSM5_QZSS , E_Sys::QZS}, - {RtcmMessageType::MSM6_QZSS , E_Sys::QZS}, - {RtcmMessageType::MSM7_QZSS , E_Sys::QZS}, - {RtcmMessageType::MSM4_BEIDOU , E_Sys::BDS}, - {RtcmMessageType::MSM5_BEIDOU , E_Sys::BDS}, - {RtcmMessageType::MSM6_BEIDOU , E_Sys::BDS}, - {RtcmMessageType::MSM7_BEIDOU , E_Sys::BDS}, - {RtcmMessageType::GAL_SSR_ORB_CORR , E_Sys::GAL}, - {RtcmMessageType::GAL_SSR_CLK_CORR , E_Sys::GAL}, - {RtcmMessageType::GAL_SSR_CODE_BIAS , E_Sys::GAL}, - {RtcmMessageType::GAL_SSR_COMB_CORR , E_Sys::GAL}, - {RtcmMessageType::GAL_SSR_URA , E_Sys::GAL}, - {RtcmMessageType::GAL_SSR_HR_CLK_CORR , E_Sys::GAL}, - {RtcmMessageType::QZS_SSR_ORB_CORR , E_Sys::QZS}, - {RtcmMessageType::QZS_SSR_CLK_CORR , E_Sys::QZS}, - {RtcmMessageType::QZS_SSR_CODE_BIAS , E_Sys::QZS}, - {RtcmMessageType::QZS_SSR_COMB_CORR , E_Sys::QZS}, - {RtcmMessageType::QZS_SSR_URA , E_Sys::QZS}, - {RtcmMessageType::QZS_SSR_HR_CLK_CORR , E_Sys::QZS}, - {RtcmMessageType::SBS_SSR_ORB_CORR , E_Sys::SBS}, - {RtcmMessageType::SBS_SSR_CLK_CORR , E_Sys::SBS}, - {RtcmMessageType::SBS_SSR_CODE_BIAS , E_Sys::SBS}, - {RtcmMessageType::SBS_SSR_COMB_CORR , E_Sys::SBS}, - {RtcmMessageType::SBS_SSR_URA , E_Sys::SBS}, - {RtcmMessageType::SBS_SSR_HR_CLK_CORR , E_Sys::SBS}, - {RtcmMessageType::BDS_SSR_ORB_CORR , E_Sys::BDS}, - {RtcmMessageType::BDS_SSR_CLK_CORR , E_Sys::BDS}, - {RtcmMessageType::BDS_SSR_CODE_BIAS , E_Sys::BDS}, - {RtcmMessageType::BDS_SSR_COMB_CORR , E_Sys::BDS}, - {RtcmMessageType::BDS_SSR_URA , E_Sys::BDS}, - {RtcmMessageType::BDS_SSR_HR_CLK_CORR , E_Sys::BDS}, - {RtcmMessageType::GPS_SSR_PHASE_BIAS , E_Sys::GPS}, - {RtcmMessageType::GLO_SSR_PHASE_BIAS , E_Sys::GLO}, - {RtcmMessageType::GAL_SSR_PHASE_BIAS , E_Sys::GAL}, - {RtcmMessageType::QZS_SSR_PHASE_BIAS , E_Sys::QZS}, - {RtcmMessageType::SBS_SSR_PHASE_BIAS , E_Sys::SBS}, - {RtcmMessageType::BDS_SSR_PHASE_BIAS , E_Sys::BDS}, - {RtcmMessageType::COMPACT_SSR , E_Sys::SUPPORTED}, - {RtcmMessageType::IGS_SSR , E_Sys::SUPPORTED} +#include "common/rtcmTrace.hpp" +#include +#include "common/common.hpp" +#include "common/ephemeris.hpp" +#include "common/ssr.hpp" + +map rtcmMessageSystemMap = { + {RtcmMessageType::GPS_EPHEMERIS, E_Sys::GPS}, + {RtcmMessageType::GLO_EPHEMERIS, E_Sys::GLO}, + {RtcmMessageType::BDS_EPHEMERIS, E_Sys::BDS}, + {RtcmMessageType::QZS_EPHEMERIS, E_Sys::QZS}, + {RtcmMessageType::GAL_FNAV_EPHEMERIS, E_Sys::GAL}, + {RtcmMessageType::GAL_INAV_EPHEMERIS, E_Sys::GAL}, + {RtcmMessageType::GPS_SSR_ORB_CORR, E_Sys::GPS}, + {RtcmMessageType::GPS_SSR_CLK_CORR, E_Sys::GPS}, + {RtcmMessageType::GPS_SSR_CODE_BIAS, E_Sys::GPS}, + {RtcmMessageType::GPS_SSR_COMB_CORR, E_Sys::GPS}, + {RtcmMessageType::GPS_SSR_URA, E_Sys::GPS}, + {RtcmMessageType::GPS_SSR_HR_CLK_CORR, E_Sys::GPS}, + {RtcmMessageType::GLO_SSR_ORB_CORR, E_Sys::GLO}, + {RtcmMessageType::GLO_SSR_CLK_CORR, E_Sys::GLO}, + {RtcmMessageType::GLO_SSR_CODE_BIAS, E_Sys::GLO}, + {RtcmMessageType::GLO_SSR_COMB_CORR, E_Sys::GLO}, + {RtcmMessageType::GLO_SSR_URA, E_Sys::GLO}, + {RtcmMessageType::GLO_SSR_HR_CLK_CORR, E_Sys::GLO}, + {RtcmMessageType::MSM4_GPS, E_Sys::GPS}, + {RtcmMessageType::MSM5_GPS, E_Sys::GPS}, + {RtcmMessageType::MSM6_GPS, E_Sys::GPS}, + {RtcmMessageType::MSM7_GPS, E_Sys::GPS}, + {RtcmMessageType::MSM4_GLONASS, E_Sys::GLO}, + {RtcmMessageType::MSM5_GLONASS, E_Sys::GLO}, + {RtcmMessageType::MSM6_GLONASS, E_Sys::GLO}, + {RtcmMessageType::MSM7_GLONASS, E_Sys::GLO}, + {RtcmMessageType::MSM4_GALILEO, E_Sys::GAL}, + {RtcmMessageType::MSM5_GALILEO, E_Sys::GAL}, + {RtcmMessageType::MSM6_GALILEO, E_Sys::GAL}, + {RtcmMessageType::MSM7_GALILEO, E_Sys::GAL}, + {RtcmMessageType::MSM4_QZSS, E_Sys::QZS}, + {RtcmMessageType::MSM5_QZSS, E_Sys::QZS}, + {RtcmMessageType::MSM6_QZSS, E_Sys::QZS}, + {RtcmMessageType::MSM7_QZSS, E_Sys::QZS}, + {RtcmMessageType::MSM4_BEIDOU, E_Sys::BDS}, + {RtcmMessageType::MSM5_BEIDOU, E_Sys::BDS}, + {RtcmMessageType::MSM6_BEIDOU, E_Sys::BDS}, + {RtcmMessageType::MSM7_BEIDOU, E_Sys::BDS}, + {RtcmMessageType::GAL_SSR_ORB_CORR, E_Sys::GAL}, + {RtcmMessageType::GAL_SSR_CLK_CORR, E_Sys::GAL}, + {RtcmMessageType::GAL_SSR_CODE_BIAS, E_Sys::GAL}, + {RtcmMessageType::GAL_SSR_COMB_CORR, E_Sys::GAL}, + {RtcmMessageType::GAL_SSR_URA, E_Sys::GAL}, + {RtcmMessageType::GAL_SSR_HR_CLK_CORR, E_Sys::GAL}, + {RtcmMessageType::QZS_SSR_ORB_CORR, E_Sys::QZS}, + {RtcmMessageType::QZS_SSR_CLK_CORR, E_Sys::QZS}, + {RtcmMessageType::QZS_SSR_CODE_BIAS, E_Sys::QZS}, + {RtcmMessageType::QZS_SSR_COMB_CORR, E_Sys::QZS}, + {RtcmMessageType::QZS_SSR_URA, E_Sys::QZS}, + {RtcmMessageType::QZS_SSR_HR_CLK_CORR, E_Sys::QZS}, + {RtcmMessageType::SBS_SSR_ORB_CORR, E_Sys::SBS}, + {RtcmMessageType::SBS_SSR_CLK_CORR, E_Sys::SBS}, + {RtcmMessageType::SBS_SSR_CODE_BIAS, E_Sys::SBS}, + {RtcmMessageType::SBS_SSR_COMB_CORR, E_Sys::SBS}, + {RtcmMessageType::SBS_SSR_URA, E_Sys::SBS}, + {RtcmMessageType::SBS_SSR_HR_CLK_CORR, E_Sys::SBS}, + {RtcmMessageType::BDS_SSR_ORB_CORR, E_Sys::BDS}, + {RtcmMessageType::BDS_SSR_CLK_CORR, E_Sys::BDS}, + {RtcmMessageType::BDS_SSR_CODE_BIAS, E_Sys::BDS}, + {RtcmMessageType::BDS_SSR_COMB_CORR, E_Sys::BDS}, + {RtcmMessageType::BDS_SSR_URA, E_Sys::BDS}, + {RtcmMessageType::BDS_SSR_HR_CLK_CORR, E_Sys::BDS}, + {RtcmMessageType::GPS_SSR_PHASE_BIAS, E_Sys::GPS}, + {RtcmMessageType::GLO_SSR_PHASE_BIAS, E_Sys::GLO}, + {RtcmMessageType::GAL_SSR_PHASE_BIAS, E_Sys::GAL}, + {RtcmMessageType::QZS_SSR_PHASE_BIAS, E_Sys::QZS}, + {RtcmMessageType::SBS_SSR_PHASE_BIAS, E_Sys::SBS}, + {RtcmMessageType::BDS_SSR_PHASE_BIAS, E_Sys::BDS}, + {RtcmMessageType::COMPACT_SSR, E_Sys::SUPPORTED}, + {RtcmMessageType::IGS_SSR, E_Sys::SUPPORTED} }; - -void RtcmTrace::traceSsrEph( - RtcmMessageType messCode, - SatSys Sat, - SSREph& ssrEph) +void RtcmTrace::traceSsrEph(RtcmMessageType messCode, SatSys Sat, SSREph& ssrEph) { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - GTime nearTime = timeGet(); - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "ssrEph")); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("EpochTimeGPST", ssrEph.ssrMeta.receivedTime.to_string() )); - doc.append(kvp("ReferenceTimeGPST", ssrEph.t0.to_string() )); - doc.append(kvp("EpochTime1s", ssrEph.ssrMeta.epochTime1s )); - doc.append(kvp("SSRUpdateIntervalSec", ssrEph.udi )); - doc.append(kvp("SSRUpdateIntervalIndex", ssrEph.ssrMeta.updateIntIndex )); - doc.append(kvp("MultipleMessageIndicator", ssrEph.ssrMeta.multipleMessage )); - doc.append(kvp("SatReferenceDatum", (int)ssrEph.ssrMeta.referenceDatum )); // 0 = ITRF, 1 = Regional // bsoncxx doesn't like uints - doc.append(kvp("IODSSR", ssrEph.iod )); - doc.append(kvp("SSRProviderID", (int)ssrEph.ssrMeta.provider )); - doc.append(kvp("SSRSolutionID", (int)ssrEph.ssrMeta.solution )); - doc.append(kvp("Sat", Sat.id() )); - doc.append(kvp("IODE", ssrEph.iode )); - doc.append(kvp("IODCRC", ssrEph.iodcrc )); - doc.append(kvp("DeltaRadial", ssrEph.deph[0] )); - doc.append(kvp("DeltaAlongTrack", ssrEph.deph[1] )); - doc.append(kvp("DeltaCrossTrack", ssrEph.deph[2] )); - doc.append(kvp("DotDeltaRadial", ssrEph.ddeph[0] )); - doc.append(kvp("DotDeltaAlongTrack", ssrEph.ddeph[1] )); - doc.append(kvp("DotDeltaCrossTrack", ssrEph.ddeph[2] )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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; + doc["type"] = "ssrEph"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["EpochTimeGPST"] = ssrEph.ssrMeta.receivedTime.to_string(); + doc["ReferenceTimeGPST"] = ssrEph.t0.to_string(); + doc["EpochTime1s"] = ssrEph.ssrMeta.epochTime1s; + doc["SSRUpdateIntervalSec"] = ssrEph.udi; + doc["SSRUpdateIntervalIndex"] = ssrEph.ssrMeta.updateIntIndex; + doc["MultipleMessageIndicator"] = ssrEph.ssrMeta.multipleMessage; + doc["SatReferenceDatum"] = + static_cast(ssrEph.ssrMeta.referenceDatum); // 0 = ITRF, 1 = Regional + doc["IODSSR"] = ssrEph.iod; + doc["SSRProviderID"] = static_cast(ssrEph.ssrMeta.provider); + doc["SSRSolutionID"] = static_cast(ssrEph.ssrMeta.solution); + doc["Sat"] = Sat.id(); + doc["IODE"] = ssrEph.iode; + doc["IODCRC"] = ssrEph.iodcrc; + doc["DeltaRadial"] = ssrEph.deph[0]; + doc["DeltaAlongTrack"] = ssrEph.deph[1]; + doc["DeltaCrossTrack"] = ssrEph.deph[2]; + doc["DotDeltaRadial"] = ssrEph.ddeph[0]; + doc["DotDeltaAlongTrack"] = ssrEph.ddeph[1]; + doc["DotDeltaCrossTrack"] = ssrEph.ddeph[2]; + + fout << boost::json::serialize(doc) << "\n"; } -void RtcmTrace::traceSsrClk( - RtcmMessageType messCode, - SatSys Sat, - SSRClk& ssrClk) +void RtcmTrace::traceSsrClk(RtcmMessageType messCode, SatSys Sat, SSRClk& ssrClk) { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - GTime nearTime = timeGet(); - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "ssrClk")); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("EpochTimeGPST", ssrClk.ssrMeta.receivedTime.to_string() )); - doc.append(kvp("ReferenceTimeGPST", ssrClk.t0.to_string() )); - doc.append(kvp("EpochTime1s", ssrClk.ssrMeta.epochTime1s )); - doc.append(kvp("SSRUpdateIntervalSec", ssrClk.udi )); - doc.append(kvp("SSRUpdateIntervalIndex", ssrClk.ssrMeta.updateIntIndex )); - doc.append(kvp("MultipleMessageIndicator", ssrClk.ssrMeta.multipleMessage )); - doc.append(kvp("SatReferenceDatum", (int)ssrClk.ssrMeta.referenceDatum )); // 0 = ITRF, 1 = Regional // could be combined corrections - doc.append(kvp("IODSSR", ssrClk.iod )); - doc.append(kvp("SSRProviderID", (int)ssrClk.ssrMeta.provider )); - doc.append(kvp("SSRSolutionID", (int)ssrClk.ssrMeta.solution )); - doc.append(kvp("Sat", Sat.id() )); - doc.append(kvp("DeltaClockC0", ssrClk.dclk[0] )); - doc.append(kvp("DeltaClockC1", ssrClk.dclk[1] )); - doc.append(kvp("DeltaClockC2", ssrClk.dclk[2] )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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; + doc["type"] = "ssrClk"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["EpochTimeGPST"] = ssrClk.ssrMeta.receivedTime.to_string(); + doc["ReferenceTimeGPST"] = ssrClk.t0.to_string(); + doc["EpochTime1s"] = ssrClk.ssrMeta.epochTime1s; + doc["SSRUpdateIntervalSec"] = ssrClk.udi; + doc["SSRUpdateIntervalIndex"] = ssrClk.ssrMeta.updateIntIndex; + doc["MultipleMessageIndicator"] = ssrClk.ssrMeta.multipleMessage; + 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"; } -void RtcmTrace::traceSsrUra( - RtcmMessageType messCode, - SatSys Sat, - SSRUra& ssrUra) +void RtcmTrace::traceSsrUra(RtcmMessageType messCode, SatSys Sat, SSRUra& ssrUra) { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - GTime nearTime = timeGet(); - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "ssrURA")); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("EpochTimeGPST", ssrUra.ssrMeta.receivedTime.to_string() )); - doc.append(kvp("ReferenceTimeGPST", ssrUra.t0.to_string() )); - doc.append(kvp("EpochTime1s", ssrUra.ssrMeta.epochTime1s )); - doc.append(kvp("SSRUpdateIntervalSec", ssrUra.udi )); - doc.append(kvp("SSRUpdateIntervalIndex", ssrUra.ssrMeta.updateIntIndex )); - doc.append(kvp("MultipleMessageIndicator", ssrUra.ssrMeta.multipleMessage )); - doc.append(kvp("IODSSR", ssrUra.iod )); - doc.append(kvp("SSRProviderID", (int)ssrUra.ssrMeta.provider )); - doc.append(kvp("SSRSolutionID", (int)ssrUra.ssrMeta.solution )); - doc.append(kvp("Sat", Sat.id() )); - doc.append(kvp("SSRURA", ssrUra.ura )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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; + doc["type"] = "ssrURA"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["EpochTimeGPST"] = ssrUra.ssrMeta.receivedTime.to_string(); + doc["ReferenceTimeGPST"] = ssrUra.t0.to_string(); + doc["EpochTime1s"] = ssrUra.ssrMeta.epochTime1s; + doc["SSRUpdateIntervalSec"] = ssrUra.udi; + doc["SSRUpdateIntervalIndex"] = ssrUra.ssrMeta.updateIntIndex; + doc["MultipleMessageIndicator"] = ssrUra.ssrMeta.multipleMessage; + doc["IODSSR"] = ssrUra.iod; + doc["SSRProviderID"] = (int)ssrUra.ssrMeta.provider; + doc["SSRSolutionID"] = (int)ssrUra.ssrMeta.solution; + doc["Sat"] = Sat.id(); + doc["SSRURA"] = ssrUra.ura; + + fout << boost::json::serialize(doc) << "\n"; } -void RtcmTrace::traceSsrHRClk( - RtcmMessageType messCode, - SatSys Sat, - SSRHRClk& SsrHRClk) +void RtcmTrace::traceSsrHRClk(RtcmMessageType messCode, SatSys Sat, SSRHRClk& SsrHRClk) { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - GTime nearTime = timeGet(); - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "ssrHRClk")); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("EpochTimeGPST", SsrHRClk.ssrMeta.receivedTime.to_string() )); - doc.append(kvp("ReferenceTimeGPST", SsrHRClk.t0.to_string() )); - doc.append(kvp("EpochTime1s", SsrHRClk.ssrMeta.epochTime1s )); - doc.append(kvp("SSRUpdateIntervalSec", SsrHRClk.udi )); - doc.append(kvp("SSRUpdateIntervalIndex", SsrHRClk.ssrMeta.updateIntIndex )); - doc.append(kvp("MultipleMessageIndicator", SsrHRClk.ssrMeta.multipleMessage )); - doc.append(kvp("IODSSR", SsrHRClk.iod )); - doc.append(kvp("SSRProviderID", (int)SsrHRClk.ssrMeta.provider )); - doc.append(kvp("SSRSolutionID", (int)SsrHRClk.ssrMeta.solution )); - doc.append(kvp("Sat", Sat.id() )); - doc.append(kvp("HighRateClockCorr", SsrHRClk.hrclk )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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; + doc["type"] = "ssrHRClk"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["EpochTimeGPST"] = SsrHRClk.ssrMeta.receivedTime.to_string(); + doc["ReferenceTimeGPST"] = SsrHRClk.t0.to_string(); + doc["EpochTime1s"] = SsrHRClk.ssrMeta.epochTime1s; + doc["SSRUpdateIntervalSec"] = SsrHRClk.udi; + doc["SSRUpdateIntervalIndex"] = SsrHRClk.ssrMeta.updateIntIndex; + doc["MultipleMessageIndicator"] = SsrHRClk.ssrMeta.multipleMessage; + doc["IODSSR"] = SsrHRClk.iod; + doc["SSRProviderID"] = (int)SsrHRClk.ssrMeta.provider; + doc["SSRSolutionID"] = (int)SsrHRClk.ssrMeta.solution; + doc["Sat"] = Sat.id(); + doc["HighRateClockCorr"] = SsrHRClk.hrclk; + + fout << boost::json::serialize(doc) << "\n"; } void RtcmTrace::traceSsrCodeBias( - RtcmMessageType messCode, - SatSys Sat, - E_ObsCode code, - SSRCodeBias& ssrBias) + RtcmMessageType messCode, + SatSys Sat, + E_ObsCode code, + SSRCodeBias& ssrBias +) { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - GTime nearTime = timeGet(); - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "ssrCodeBias")); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("EpochTimeGPST", ssrBias.ssrMeta.receivedTime.to_string() )); - doc.append(kvp("ReferenceTimeGPST", ssrBias.t0.to_string() )); - doc.append(kvp("EpochTime1s", ssrBias.ssrMeta.epochTime1s )); - doc.append(kvp("SSRUpdateIntervalSec", ssrBias.udi )); - doc.append(kvp("SSRUpdateIntervalIndex", ssrBias.ssrMeta.updateIntIndex )); - doc.append(kvp("MultipleMessageIndicator", ssrBias.ssrMeta.multipleMessage )); - doc.append(kvp("IODSSR", ssrBias.iod )); - doc.append(kvp("SSRProviderID", (int)ssrBias.ssrMeta.provider )); - doc.append(kvp("SSRSolutionID", (int)ssrBias.ssrMeta.solution )); - doc.append(kvp("Sat", Sat.id() )); - doc.append(kvp("Code", code._to_string() )); - doc.append(kvp("Bias", ssrBias.obsCodeBiasMap[code].bias )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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; + doc["type"] = "ssrCodeBias"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["EpochTimeGPST"] = ssrBias.ssrMeta.receivedTime.to_string(); + doc["ReferenceTimeGPST"] = ssrBias.t0.to_string(); + doc["EpochTime1s"] = ssrBias.ssrMeta.epochTime1s; + doc["SSRUpdateIntervalSec"] = ssrBias.udi; + doc["SSRUpdateIntervalIndex"] = ssrBias.ssrMeta.updateIntIndex; + doc["MultipleMessageIndicator"] = ssrBias.ssrMeta.multipleMessage; + doc["IODSSR"] = ssrBias.iod; + doc["SSRProviderID"] = (int)ssrBias.ssrMeta.provider; + doc["SSRSolutionID"] = (int)ssrBias.ssrMeta.solution; + doc["Sat"] = Sat.id(); + doc["Code"] = enum_to_string(code); + doc["Bias"] = ssrBias.obsCodeBiasMap[code].bias; + + fout << boost::json::serialize(doc) << "\n"; } void RtcmTrace::traceSsrPhasBias( - RtcmMessageType messCode, - SatSys Sat, - E_ObsCode code, - SSRPhasBias& ssrBias) + RtcmMessageType messCode, + SatSys Sat, + E_ObsCode code, + SSRPhasBias& ssrBias +) { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - GTime nearTime = timeGet(); - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "ssrPhasBias")); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("EpochTimeGPST", ssrBias.ssrMeta.receivedTime.to_string() )); - doc.append(kvp("ReferenceTimeGPST", ssrBias.t0.to_string() )); - doc.append(kvp("EpochTime1s", ssrBias.ssrMeta.epochTime1s )); - doc.append(kvp("SSRUpdateIntervalSec", ssrBias.udi )); - doc.append(kvp("SSRUpdateIntervalIndex", ssrBias.ssrMeta.updateIntIndex )); - doc.append(kvp("MultipleMessageIndicator", ssrBias.ssrMeta.multipleMessage )); - doc.append(kvp("IODSSR", ssrBias.iod )); - doc.append(kvp("SSRProviderID", (int)ssrBias.ssrMeta.provider )); - doc.append(kvp("SSRSolutionID", (int)ssrBias.ssrMeta.solution )); - doc.append(kvp("DisperBiasConsisIndicator", ssrBias.ssrPhase.dispBiasConistInd )); - doc.append(kvp("MWConsistencyIndicator", ssrBias.ssrPhase.MWConistInd )); - doc.append(kvp("Sat", Sat.id() )); - doc.append(kvp("YawAngle", ssrBias.ssrPhase.yawAngle )); - doc.append(kvp("YawRate", ssrBias.ssrPhase.yawRate )); - doc.append(kvp("Code", code._to_string() )); - doc.append(kvp("SignalIntegerIndicator", (int)ssrBias.ssrPhaseChs[code].signalIntInd )); - doc.append(kvp("SignalsWLIntegerIndicator", (int)ssrBias.ssrPhaseChs[code].signalWLIntInd )); - doc.append(kvp("SignalDiscontinuityCount", (int)ssrBias.ssrPhaseChs[code].signalDisconCnt )); - doc.append(kvp("Bias", ssrBias.obsCodeBiasMap[code].bias )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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; + doc["type"] = "ssrPhasBias"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["EpochTimeGPST"] = ssrBias.ssrMeta.receivedTime.to_string(); + doc["ReferenceTimeGPST"] = ssrBias.t0.to_string(); + doc["EpochTime1s"] = ssrBias.ssrMeta.epochTime1s; + doc["SSRUpdateIntervalSec"] = ssrBias.udi; + doc["SSRUpdateIntervalIndex"] = ssrBias.ssrMeta.updateIntIndex; + doc["MultipleMessageIndicator"] = ssrBias.ssrMeta.multipleMessage; + doc["IODSSR"] = ssrBias.iod; + doc["SSRProviderID"] = (int)ssrBias.ssrMeta.provider; + doc["SSRSolutionID"] = (int)ssrBias.ssrMeta.solution; + doc["DisperBiasConsisIndicator"] = ssrBias.ssrPhase.dispBiasConistInd; + doc["MWConsistencyIndicator"] = ssrBias.ssrPhase.MWConistInd; + doc["Sat"] = Sat.id(); + doc["YawAngle"] = ssrBias.ssrPhase.yawAngle; + doc["YawRate"] = ssrBias.ssrPhase.yawRate; + doc["Code"] = enum_to_string(code); + doc["SignalIntegerIndicator"] = (int)ssrBias.ssrPhaseChs[code].signalIntInd; + doc["SignalsWLIntegerIndicator"] = (int)ssrBias.ssrPhaseChs[code].signalWLIntInd; + doc["SignalDiscontinuityCount"] = (int)ssrBias.ssrPhaseChs[code].signalDisconCnt; + doc["Bias"] = ssrBias.obsCodeBiasMap[code].bias; + + fout << boost::json::serialize(doc) << "\n"; } -void RtcmTrace::traceTimestamp( - GTime time) +void RtcmTrace::traceTimestamp(GTime time) { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "timestamp" )); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("time", (string)time )); - doc.append(kvp("ticks", (double)time.bigTime )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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"; } /** Write decoded/encoded GPS/GAL/BDS/QZS ephemeris messages to a json file -*/ -void RtcmTrace::traceBrdcEph( //todo aaron, template this for gps/glo? - RtcmMessageType messCode, - Eph& eph) + */ +void RtcmTrace::traceBrdcEph( // todo aaron, template this for gps/glo? + RtcmMessageType messCode, + Eph& eph +) { - if (rtcmTraceFilename.empty()) - { - return; - } + if (rtcmTraceFilename.empty()) + { + return; + } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } + std::ofstream fout(rtcmTraceFilename, std::ios::app); + if (!fout) + { + std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; + return; + } - bsoncxx::builder::basic::document doc = {}; + boost::json::object doc; - GTime nearTime = timeGet(); + GTime nearTime = timeGet(); - // Note the Satellite id is not set in rinex correctly as we a mixing GNSS systems. - doc.append(kvp("type", "brdcEph" )); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("Type", eph.type._to_string() )); + // Note the Satellite id is not set in rinex correctly as we a mixing GNSS systems. + doc["type"] = "brdcEph"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["Type"] = enum_to_string(eph.type); - doc.append(kvp("ToeGPST", eph.toe.to_string() )); - doc.append(kvp("TocGPST", eph.toc.to_string() )); + doc["ToeGPST"] = eph.toe.to_string(); + doc["TocGPST"] = eph.toc.to_string(); - traceBrdcEphBody(doc, eph); + traceBrdcEphBody(doc, eph); - fout << bsoncxx::to_json(doc) << "\n"; + fout << boost::json::serialize(doc) << "\n"; } /** Write decoded/encoded GAL ephemeris messages to a json file -*/ -void RtcmTrace::traceBrdcEph( - RtcmMessageType messCode, - Geph& geph) + */ +void RtcmTrace::traceBrdcEph(RtcmMessageType messCode, Geph& geph) { - if (rtcmTraceFilename.empty()) - { - return; - } + if (rtcmTraceFilename.empty()) + { + return; + } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } + std::ofstream fout(rtcmTraceFilename, std::ios::app); + if (!fout) + { + std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; + return; + } - bsoncxx::builder::basic::document doc = {}; + boost::json::object doc; - GTime nearTime = timeGet(); + GTime nearTime = timeGet(); - // Note the Satellite id is not set in rinex correctly as we a mixing GNSS systems. - doc.append(kvp("type", "brdcEph" )); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("Sat", geph.Sat.id() )); - doc.append(kvp("Type", geph.type._to_string() )); + // Note the Satellite id is not set in rinex correctly as we a mixing GNSS systems. + doc["type"] = "brdcEph"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["Sat"] = geph.Sat.id(); + doc["Type"] = enum_to_string(geph.type); - doc.append(kvp("ToeGPST", geph.toe.to_string() )); - doc.append(kvp("TofGPST", geph.tof.to_string() )); + doc["ToeGPST"] = geph.toe.to_string(); + doc["TofGPST"] = geph.tof.to_string(); - traceBrdcEphBody(doc, geph); + traceBrdcEphBody(doc, geph); - fout << bsoncxx::to_json(doc) << "\n"; + fout << boost::json::serialize(doc) << "\n"; } -void traceBrdcEphBody( - bsoncxx::builder::basic::document& doc, - Eph& eph) +void traceBrdcEphBody(boost::json::object& doc, Eph& eph) { - doc.append(kvp("Sat", eph.Sat.id() )); - doc.append(kvp("weekRollOver", eph.weekRollOver )); - doc.append(kvp("week", eph.week )); - doc.append(kvp("toes", eph.toes )); - doc.append(kvp("tocs", eph.tocs )); - doc.append(kvp("howTow", eph.howTow )); - doc.append(kvp("toe", eph.toe.to_string() )); - doc.append(kvp("toc", eph.toc.to_string() )); - doc.append(kvp("ttm", eph.ttm.to_string() )); - - doc.append(kvp("aode", eph.aode )); - doc.append(kvp("aodc", eph.aodc )); - doc.append(kvp("iode", eph.iode )); - doc.append(kvp("iodc", eph.iodc )); - - doc.append(kvp("f0", eph.f0 )); - doc.append(kvp("f1", eph.f1 )); - doc.append(kvp("f2", eph.f2 )); - - doc.append(kvp("sqrtA", eph.sqrtA )); - doc.append(kvp("A", eph.A )); - doc.append(kvp("e", eph.e )); - doc.append(kvp("i0", eph.i0 )); - doc.append(kvp("idot", eph.idot )); - doc.append(kvp("omg", eph.omg )); - doc.append(kvp("OMG0", eph.OMG0 )); - doc.append(kvp("OMGd", eph.OMGd )); - doc.append(kvp("M0", eph.M0 )); - doc.append(kvp("deln", eph.deln )); - doc.append(kvp("crc", eph.crc )); - doc.append(kvp("crs", eph.crs )); - doc.append(kvp("cic", eph.cic )); - doc.append(kvp("cis", eph.cis )); - doc.append(kvp("cuc", eph.cuc )); - doc.append(kvp("cus", eph.cus )); - - doc.append(kvp("tgd0", eph.tgd[0] )); - doc.append(kvp("tgd1", eph.tgd[1] )); // GPS/QZS no tgd[1] - doc.append(kvp("sva", eph.sva )); - - if ( eph.Sat.sys == +E_Sys::GPS - ||eph.Sat.sys == +E_Sys::QZS) - { - doc.append(kvp("ura", eph.ura[0] )); - doc.append(kvp("svh", eph.svh )); - doc.append(kvp("code", eph.code )); - doc.append(kvp("flag", eph.flag )); // QZS no flag - doc.append(kvp("fitFlag", eph.fitFlag )); - doc.append(kvp("fit", eph.fit )); - } - else if (eph.Sat.sys == +E_Sys::GAL) - { - doc.append(kvp("SISA", eph.ura[0] )); - doc.append(kvp("SVHealth", eph.svh )); - doc.append(kvp("E5aHealth", eph.e5a_hs )); - doc.append(kvp("E5aDataValidity", eph.e5a_dvs )); - doc.append(kvp("E5bHealth", eph.e5b_hs )); - doc.append(kvp("E5bDataValidity", eph.e5b_dvs )); - doc.append(kvp("E1Health", eph.e1_hs )); - doc.append(kvp("E1DataValidity", eph.e1_dvs )); - doc.append(kvp("DataSource", eph.code )); - } - else if (eph.Sat.sys == +E_Sys::BDS) - { - doc.append(kvp("URA", eph.ura[0] )); - doc.append(kvp("SVHealth", eph.svh )); - } + doc["Sat"] = eph.Sat.id(); + doc["weekRollOver"] = eph.weekRollOver; + doc["week"] = eph.week; + doc["toes"] = eph.toes; + doc["tocs"] = eph.tocs; + doc["howTow"] = eph.howTow; + doc["toe"] = eph.toe.to_string(); + doc["toc"] = eph.toc.to_string(); + doc["ttm"] = eph.ttm.to_string(); + + doc["aode"] = eph.aode; + doc["aodc"] = eph.aodc; + doc["iode"] = eph.iode; + doc["iodc"] = eph.iodc; + + doc["f0"] = eph.f0; + doc["f1"] = eph.f1; + doc["f2"] = eph.f2; + + doc["sqrtA"] = eph.sqrtA; + doc["A"] = eph.A; + doc["e"] = eph.e; + doc["i0"] = eph.i0; + doc["idot"] = eph.idot; + doc["omg"] = eph.omg; + doc["OMG0"] = eph.OMG0; + doc["OMGd"] = eph.OMGd; + doc["M0"] = eph.M0; + doc["deln"] = eph.deln; + doc["crc"] = eph.crc; + doc["crs"] = eph.crs; + doc["cic"] = eph.cic; + doc["cis"] = eph.cis; + doc["cuc"] = eph.cuc; + doc["cus"] = eph.cus; + + doc["tgd0"] = eph.tgd[0]; + doc["tgd1"] = eph.tgd[1]; // GPS/QZS no tgd[1] + doc["sva"] = eph.sva; + + if (eph.Sat.sys == E_Sys::GPS || eph.Sat.sys == E_Sys::QZS) + { + doc["ura"] = eph.ura[0]; + doc["svh"] = eph.svh; + doc["code"] = eph.code; + doc["flag"] = eph.flag; // QZS no flag + doc["fitFlag"] = eph.fitFlag; + doc["fit"] = eph.fit; + } + else if (eph.Sat.sys == E_Sys::GAL) + { + doc["SISA"] = eph.ura[0]; + doc["SVHealth"] = eph.svh; + doc["E5aHealth"] = eph.e5a_hs; + doc["E5aDataValidity"] = eph.e5a_dvs; + doc["E5bHealth"] = eph.e5b_hs; + doc["E5bDataValidity"] = eph.e5b_dvs; + doc["E1Health"] = eph.e1_hs; + doc["E1DataValidity"] = eph.e1_dvs; + doc["DataSource"] = eph.code; + } + else if (eph.Sat.sys == E_Sys::BDS) + { + doc["URA"] = eph.ura[0]; + doc["SVHealth"] = eph.svh; + } } -void traceBrdcEphBody( - bsoncxx::builder::basic::document& doc, - Geph& geph) +void traceBrdcEphBody(boost::json::object& doc, Geph& geph) { - doc.append(kvp("ToeSecOfDay", geph.tb )); - doc.append(kvp("TofHour", geph.tk_hour )); - doc.append(kvp("TofMin", geph.tk_min )); - doc.append(kvp("TofSec", geph.tk_sec )); - - doc.append(kvp("IODE", geph.iode )); - - doc.append(kvp("TauN", geph.taun )); - doc.append(kvp("GammaN", geph.gammaN )); - doc.append(kvp("DeltaTauN", geph.dtaun )); - - doc.append(kvp("PosX", geph.pos[0] )); - doc.append(kvp("PosY", geph.pos[1] )); - doc.append(kvp("PosZ", geph.pos[2] )); - doc.append(kvp("VelX", geph.vel[0] )); - doc.append(kvp("VelY", geph.vel[1] )); - doc.append(kvp("VelZ", geph.vel[2] )); - doc.append(kvp("AccX", geph.acc[0] )); - doc.append(kvp("AccY", geph.acc[1] )); - doc.append(kvp("AccZ", geph.acc[2] )); - - doc.append(kvp("FrquencyNumber", geph.frq )); - doc.append(kvp("SVHealth", geph.svh )); - doc.append(kvp("Age", geph.age )); - - doc.append(kvp("GLONASSM", geph.glonassM )); - doc.append(kvp("NumberOfDayIn4Year", geph.NT )); - doc.append(kvp("AdditionalData", geph.moreData )); - doc.append(kvp("4YearIntervalNumber", geph.N4 )); + doc["ToeSecOfDay"] = geph.tb; + doc["TofHour"] = geph.tk_hour; + doc["TofMin"] = geph.tk_min; + doc["TofSec"] = geph.tk_sec; + + doc["IODE"] = geph.iode; + + doc["TauN"] = geph.taun; + doc["GammaN"] = geph.gammaN; + doc["DeltaTauN"] = geph.dtaun; + + doc["PosX"] = geph.pos[0]; + doc["PosY"] = geph.pos[1]; + doc["PosZ"] = geph.pos[2]; + doc["VelX"] = geph.vel[0]; + doc["VelY"] = geph.vel[1]; + doc["VelZ"] = geph.vel[2]; + doc["AccX"] = geph.acc[0]; + doc["AccY"] = geph.acc[1]; + doc["AccZ"] = geph.acc[2]; + + doc["FrquencyNumber"] = geph.frq; + doc["SVHealth"] = geph.svh; + doc["Age"] = geph.age; + + doc["GLONASSM"] = geph.glonassM; + doc["NumberOfDayIn4Year"] = geph.NT; + doc["AdditionalData"] = geph.moreData; + doc["4YearIntervalNumber"] = geph.N4; } /** Writes nav.satNavMap[].ssrOut to a human-readable file -*/ -void writeSsrOutToFile( - int epochNum, - map& ssrOutMap) + */ +void writeSsrOutToFile(int epochNum, map& ssrOutMap) { - string filename = "ssrOut.dbg"; - std::ofstream out(filename, std::ios::app); - - if (!out) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Could not open trace file for SSR messages at " << filename; - return; - } - out.precision(17); - - // Header - out << "epochNum" << "\t"; - out << "satId" << "\t"; - -// out << "SSREph.canExport" << "\t"; - out << "SSREph.t0" << "\t"; - out << "SSREph.udi" << "\t"; - out << "SSREph.iod" << "\t"; - out << "SSREph.iode" << "\t"; - out << "SSREph.deph[0]" << "\t"; - out << "SSREph.deph[1]" << "\t"; - out << "SSREph.deph[2]" << "\t"; - out << "SSREph.ddeph[0]" << "\t"; - out << "SSREph.ddeph[1]" << "\t"; - out << "SSREph.ddeph[2]" << "\t"; - -// out << "SSRClk.canExport" << "\t"; - out << "SSRClk.t0" << "\t"; - out << "SSRClk.udi" << "\t"; - out << "SSRClk.iod" << "\t"; - out << "SSRClk.dclk[0]" << "\t"; - out << "SSRClk.dclk[1]" << "\t"; - out << "SSRClk.dclk[2]" << "\t"; - - out << "SSRBias.t0_code" << "\t"; - out << "SSRBias.t0_phas" << "\t"; - out << "SSRBias.udi_code" << "\t"; - out << "SSRBias.udi_phas" << "\t"; - out << "SSRBias.iod_code" << "\t"; - out << "SSRBias.iod_phas" << "\t"; - for (int i=0; i<2; ++i) out << "ssrBias.cbias_"<< i << "\t"; - for (int i=0; i<2; ++i) out << "ssrBias.cvari_"<< i << "\t"; - for (int i=0; i<2; ++i) out << "ssrBias.pbias_"<< i << "\t"; - for (int i=0; i<2; ++i) out << "ssrBias.pvari_"<< i << "\t"; - for (int i=0; i<2; ++i) - { - out << "ssrBias.ssrPhaseCh.signalIntInd_" << i << "\t"; - out << "ssrBias.ssrPhaseCh.signalWLIntInd_" << i << "\t"; - out << "ssrBias.ssrPhaseCh.signalDisconCnt_" << i << "\t"; - } - out << "\n"; - - // Body - for (auto& [Sat, ssrOut] : ssrOutMap) - { - out << epochNum << "\t"; - out << Sat.id() << "\t"; -// out << ssrOut.ssrEph.canExport<< "\t"; - out << ssrOut.ssrEph.t0 << "\t"; - out << ssrOut.ssrEph.udi << "\t"; - out << ssrOut.ssrEph.iod << "\t"; - out << ssrOut.ssrEph.iode << "\t"; - out << ssrOut.ssrEph.deph[0] << "\t"; - out << ssrOut.ssrEph.deph[1] << "\t"; - out << ssrOut.ssrEph.deph[2] << "\t"; - out << ssrOut.ssrEph.ddeph[0] << "\t"; - out << ssrOut.ssrEph.ddeph[1] << "\t"; - out << ssrOut.ssrEph.ddeph[2] << "\t"; - -// out << ssrOut.ssrClk.canExport<< "\t"; - out << ssrOut.ssrClk.t0 << "\t"; - out << ssrOut.ssrClk.udi << "\t"; - out << ssrOut.ssrClk.iod << "\t"; - out << ssrOut.ssrClk.dclk[0] << "\t"; - out << ssrOut.ssrClk.dclk[1] << "\t"; - out << ssrOut.ssrClk.dclk[2] << "\t"; - - out << ssrOut.ssrCodeBias.t0 << "\t"; - out << ssrOut.ssrPhasBias.t0 << "\t"; - out << ssrOut.ssrCodeBias.udi << "\t"; - out << ssrOut.ssrPhasBias.udi << "\t"; - out << ssrOut.ssrCodeBias.iod << "\t"; - out << ssrOut.ssrPhasBias.iod << "\t"; - for (auto& [key, val] : ssrOut.ssrCodeBias.obsCodeBiasMap) out << val.bias << "\t" << val.var << "\t"; - for (auto& [key, val] : ssrOut.ssrPhasBias.obsCodeBiasMap) out << val.bias << "\t" << val.var << "\t"; - - for (auto& [key, ssrPhaseCh] : ssrOut.ssrPhasBias.ssrPhaseChs) - { - out << ssrPhaseCh.signalIntInd << "\t"; - out << ssrPhaseCh.signalWLIntInd << "\t"; - out << ssrPhaseCh.signalDisconCnt << "\t"; - } - out << "\n"; - } - out << "\n"; + string filename = "ssrOut.dbg"; + std::ofstream out(filename, std::ios::app); + + if (!out) + { + BOOST_LOG_TRIVIAL(error) << "Could not open trace file for SSR messages at " << filename; + return; + } + out.precision(17); + + // Header + out << "epochNum" << "\t"; + out << "satId" << "\t"; + + // out << "SSREph.canExport" << "\t"; + out << "SSREph.t0" << "\t"; + out << "SSREph.udi" << "\t"; + out << "SSREph.iod" << "\t"; + out << "SSREph.iode" << "\t"; + out << "SSREph.deph[0]" << "\t"; + out << "SSREph.deph[1]" << "\t"; + out << "SSREph.deph[2]" << "\t"; + out << "SSREph.ddeph[0]" << "\t"; + out << "SSREph.ddeph[1]" << "\t"; + out << "SSREph.ddeph[2]" << "\t"; + + // out << "SSRClk.canExport" << "\t"; + out << "SSRClk.t0" << "\t"; + out << "SSRClk.udi" << "\t"; + out << "SSRClk.iod" << "\t"; + out << "SSRClk.dclk[0]" << "\t"; + out << "SSRClk.dclk[1]" << "\t"; + out << "SSRClk.dclk[2]" << "\t"; + + out << "SSRBias.t0_code" << "\t"; + out << "SSRBias.t0_phas" << "\t"; + out << "SSRBias.udi_code" << "\t"; + out << "SSRBias.udi_phas" << "\t"; + out << "SSRBias.iod_code" << "\t"; + out << "SSRBias.iod_phas" << "\t"; + for (int i = 0; i < 2; ++i) + out << "ssrBias.cbias_" << i << "\t"; + for (int i = 0; i < 2; ++i) + out << "ssrBias.cvari_" << i << "\t"; + for (int i = 0; i < 2; ++i) + out << "ssrBias.pbias_" << i << "\t"; + for (int i = 0; i < 2; ++i) + out << "ssrBias.pvari_" << i << "\t"; + for (int i = 0; i < 2; ++i) + { + out << "ssrBias.ssrPhaseCh.signalIntInd_" << i << "\t"; + out << "ssrBias.ssrPhaseCh.signalWLIntInd_" << i << "\t"; + out << "ssrBias.ssrPhaseCh.signalDisconCnt_" << i << "\t"; + } + out << "\n"; + + // Body + for (auto& [Sat, ssrOut] : ssrOutMap) + { + out << epochNum << "\t"; + out << Sat.id() << "\t"; + // out << ssrOut.ssrEph.canExport<< "\t"; + out << ssrOut.ssrEph.t0 << "\t"; + out << ssrOut.ssrEph.udi << "\t"; + out << ssrOut.ssrEph.iod << "\t"; + out << ssrOut.ssrEph.iode << "\t"; + out << ssrOut.ssrEph.deph[0] << "\t"; + out << ssrOut.ssrEph.deph[1] << "\t"; + out << ssrOut.ssrEph.deph[2] << "\t"; + out << ssrOut.ssrEph.ddeph[0] << "\t"; + out << ssrOut.ssrEph.ddeph[1] << "\t"; + out << ssrOut.ssrEph.ddeph[2] << "\t"; + + // out << ssrOut.ssrClk.canExport<< "\t"; + out << ssrOut.ssrClk.t0 << "\t"; + out << ssrOut.ssrClk.udi << "\t"; + out << ssrOut.ssrClk.iod << "\t"; + out << ssrOut.ssrClk.dclk[0] << "\t"; + out << ssrOut.ssrClk.dclk[1] << "\t"; + out << ssrOut.ssrClk.dclk[2] << "\t"; + + out << ssrOut.ssrCodeBias.t0 << "\t"; + out << ssrOut.ssrPhasBias.t0 << "\t"; + out << ssrOut.ssrCodeBias.udi << "\t"; + out << ssrOut.ssrPhasBias.udi << "\t"; + out << ssrOut.ssrCodeBias.iod << "\t"; + out << ssrOut.ssrPhasBias.iod << "\t"; + for (auto& [key, val] : ssrOut.ssrCodeBias.obsCodeBiasMap) + out << val.bias << "\t" << val.var << "\t"; + for (auto& [key, val] : ssrOut.ssrPhasBias.obsCodeBiasMap) + out << val.bias << "\t" << val.var << "\t"; + + for (auto& [key, ssrPhaseCh] : ssrOut.ssrPhasBias.ssrPhaseChs) + { + out << ssrPhaseCh.signalIntInd << "\t"; + out << ssrPhaseCh.signalWLIntInd << "\t"; + out << ssrPhaseCh.signalDisconCnt << "\t"; + } + out << "\n"; + } + out << "\n"; } - /** Write msm message to a json file -*/ -void RtcmTrace::traceMSM( - RtcmMessageType messCode, - GTime time, - SatSys Sat, - Sig& sig) + */ +void RtcmTrace::traceMSM(RtcmMessageType messCode, GTime time, SatSys Sat, Sig& sig) { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - GTime nearTime = timeGet(); - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "MSM" )); - doc.append(kvp("Mountpoint", rtcmMountpoint )); - doc.append(kvp("MessageNumber", messCode._to_integral() )); - doc.append(kvp("MessageType", messCode._to_string() )); - doc.append(kvp("ReceivedSentTimeGPST", nearTime.to_string() )); - doc.append(kvp("EpochTimeGPST", time.to_string() )); - doc.append(kvp("Sat", Sat.id() )); - doc.append(kvp("Code", sig.code._to_string() )); - doc.append(kvp("Pseudorange", sig.P )); - doc.append(kvp("CarrierPhase", sig.L )); - doc.append(kvp("Doppler", sig.D )); - doc.append(kvp("SNR", sig.snr )); - doc.append(kvp("LLI", sig.LLI )); - doc.append(kvp("IsInvalid", sig.invalid )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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; + doc["type"] = "MSM"; + doc["Mountpoint"] = rtcmMountpoint; + doc["MessageNumber"] = static_cast(messCode); + doc["MessageType"] = enum_to_string(messCode); + doc["ReceivedSentTimeGPST"] = nearTime.to_string(); + doc["EpochTimeGPST"] = time.to_string(); + doc["Sat"] = Sat.id(); + doc["Code"] = enum_to_string(sig.code); + doc["Pseudorange"] = sig.P; + doc["CarrierPhase"] = sig.L; + doc["Doppler"] = sig.D; + doc["SNR"] = sig.snr; + doc["LLI"] = sig.LLI; + doc["IsInvalid"] = sig.invalid; + + fout << boost::json::serialize(doc) << "\n"; } - /** Write unknown message to a json file -*/ + */ void RtcmTrace::traceUnknown() { - if (rtcmTraceFilename.empty()) - { - return; - } - - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("type", "?" )); - - fout << bsoncxx::to_json(doc) << "\n"; + if (rtcmTraceFilename.empty()) + { + 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"; } diff --git a/src/cpp/common/rtcmTrace.hpp b/src/cpp/common/rtcmTrace.hpp index cc3e8b420..1877d9fd4 100644 --- a/src/cpp/common/rtcmTrace.hpp +++ b/src/cpp/common/rtcmTrace.hpp @@ -1,19 +1,16 @@ - #pragma once -#include -#include +#include +#include #include +#include +#include #include +#include "common/gTime.hpp" +#include "common/satSys.hpp" using std::string; -#include -#include - -#include "satSys.hpp" -#include "gTime.hpp" - struct Sig; struct Eph; struct Geph; @@ -26,109 +23,70 @@ struct SSRPhasBias; struct RtcmTrace { - string rtcmTraceFilename = ""; - string rtcmMountpoint; - bool qzssL6 = false; - - RtcmTrace( - string mountpoint = "", - string filename = "") - : rtcmTraceFilename {filename}, - rtcmMountpoint {mountpoint} - { - } - - void networkLog( - string message) - { - std::ofstream outStream(rtcmTraceFilename, std::iostream::app); - if (!outStream) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - outStream << timeGet(); - outStream << " " << __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"; - } - - void traceSsrEph( - RtcmMessageType messCode, - SatSys Sat, - SSREph& ssrEph); - - void traceSsrClk( - RtcmMessageType messCode, - SatSys Sat, - SSRClk& ssrClk); - - void traceSsrUra( - RtcmMessageType messCode, - SatSys Sat, - SSRUra& ssrUra); - - void traceSsrHRClk( - RtcmMessageType messCode, - SatSys Sat, - SSRHRClk& ssrHRClk); - - void traceSsrCodeBias( - RtcmMessageType messCode, - SatSys Sat, - E_ObsCode code, - SSRCodeBias& ssrBias); - - void traceSsrPhasBias( - RtcmMessageType messCode, - SatSys Sat, - E_ObsCode code, - SSRPhasBias& ssrBias); - - void traceTimestamp( - GTime time); - - void traceBrdcEph( - RtcmMessageType messCode, - Eph& eph); - - void traceBrdcEph( - RtcmMessageType messCode, - Geph& geph); - - void traceMSM( - RtcmMessageType messCode, - GTime time, - SatSys Sat, - Sig& sig); - - void traceUnknown(); + string rtcmTraceFilename = ""; + string rtcmMountpoint; + bool qzssL6 = false; + + RtcmTrace(string mountpoint = "", string filename = "") + : rtcmTraceFilename{filename}, rtcmMountpoint{mountpoint} + { + } + + void networkLog(string message) + { + std::ofstream outStream(rtcmTraceFilename, std::iostream::app); + if (!outStream) + { + std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; + return; + } + + outStream << timeGet(); + outStream << " " << __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"; + } + + void traceSsrEph(RtcmMessageType messCode, SatSys Sat, SSREph& ssrEph); + + void traceSsrClk(RtcmMessageType messCode, SatSys Sat, SSRClk& ssrClk); + + void traceSsrUra(RtcmMessageType messCode, SatSys Sat, SSRUra& ssrUra); + + void traceSsrHRClk(RtcmMessageType messCode, SatSys Sat, SSRHRClk& ssrHRClk); + + void + traceSsrCodeBias(RtcmMessageType messCode, SatSys Sat, E_ObsCode code, SSRCodeBias& ssrBias); + + void + traceSsrPhasBias(RtcmMessageType messCode, SatSys Sat, E_ObsCode code, SSRPhasBias& ssrBias); + + void traceTimestamp(GTime time); + + void traceBrdcEph(RtcmMessageType messCode, Eph& eph); + + void traceBrdcEph(RtcmMessageType messCode, Geph& geph); + + void traceMSM(RtcmMessageType messCode, GTime time, SatSys Sat, Sig& sig); + + void traceUnknown(); }; -void traceBrdcEphBody( - bsoncxx::builder::basic::document& doc, - Eph& eph); +void traceBrdcEphBody(boost::json::object& obj, Eph& eph); -void traceBrdcEphBody( - bsoncxx::builder::basic::document& doc, - Geph& geph); +void traceBrdcEphBody(boost::json::object& obj, Geph& geph); extern map rtcmMessageSystemMap; diff --git a/src/cpp/common/rtsSmoothing.cpp b/src/cpp/common/rtsSmoothing.cpp index ecd6797a0..37164c7ec 100644 --- a/src/cpp/common/rtsSmoothing.cpp +++ b/src/cpp/common/rtsSmoothing.cpp @@ -1,726 +1,1073 @@ - - +#include "common/rtsSmoothing.hpp" +#include +#include #include #include -#include +#include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/algebraTrace.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/lapackWrapper.hpp" +#include "common/metaData.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "pea/inputsOutputs.hpp" -using std::this_thread::sleep_for; using std::make_shared; -using std::shared_ptr; using std::map; +using std::shared_ptr; +using std::this_thread::sleep_for; -#include +// #pragma GCC optimize ("O0") -#include "interactiveTerminal.hpp" -#include "minimumConstraints.hpp" -#include "architectureDocs.hpp" -#include "rinexNavWrite.hpp" -#include "eigenIncluder.hpp" -#include "rinexObsWrite.hpp" -#include "rinexClkWrite.hpp" -#include "algebraTrace.hpp" -#include "rtsSmoothing.hpp" -#include "orbexWrite.hpp" -#include "mongoWrite.hpp" -#include "GNSSambres.hpp" -#include "orbitProp.hpp" -#include "acsConfig.hpp" -#include "testUtils.hpp" -#include "constants.hpp" -#include "ionoModel.hpp" -#include "sp3Write.hpp" -#include "metaData.hpp" -#include "algebra.hpp" -#include "sinex.hpp" -#include "cost.hpp" -#include "ppp.hpp" -#include "gpx.hpp" -#include "pos.hpp" +//================================================================================ +// RtsConfiguration Implementation +//================================================================================ -// #pragma GCC optimize ("O0") +/** Create configuration from global acsConfig */ +RtsConfiguration RtsConfiguration::fromAcsConfig() +{ + RtsConfiguration config; + config.queue_rts_outputs = acsConfig.pppOpts.queue_rts_outputs; + config.rts_only = acsConfig.rts_only; + config.output_residuals = acsConfig.output_residuals; + config.retain_rts_files = acsConfig.retain_rts_files; + config.output_measurements = acsConfig.mongoOpts.output_measurements != E_Mongo::NONE; + config.queue_mongo_outputs = acsConfig.mongoOpts.queue_outputs; + config.sleep_milliseconds = acsConfig.sleep_milliseconds; + config.regularisation = acsConfig.pppOpts.rts_regularisation; + return config; +} + +//================================================================================ +// RtsFileReader Implementation +//================================================================================ + +/** Constructor */ +RtsFileReader::RtsFileReader(const string& filename) : inputFile(filename) +{ + checkValidFile(inputFile, "RTS file (rts_forward)"); + resetEpochFlags(); +} + +/** Read the next object from file and update FilterData */ +E_SerialObject RtsFileReader::readNextObject(FilterData& filterData) +{ + E_SerialObject type = getFilterTypeFromFile(currentPosition, inputFile); + + if (type == E_SerialObject::NONE) + { + return type; + } + + bool success = false; + + switch (type) + { + case E_SerialObject::METADATA: + success = processMetadata(filterData); + break; + + case E_SerialObject::MEASUREMENT: + success = processMeasurement(filterData); + break; + + case E_SerialObject::TRANSITION_MATRIX: + success = processTransitionMatrix(filterData); + break; + + case E_SerialObject::FILTER_MINUS: + success = processFilterMinus(filterData); + break; + + case E_SerialObject::FILTER_PLUS: + success = processFilterPlus(filterData); + break; + + default: + BOOST_LOG_TRIVIAL(error) << "Unknown rts type" << "\n"; + return E_SerialObject::NONE; + } + + if (!success) + { + return E_SerialObject::NONE; + } + + return type; +} + +/** Check if we have all required data for RTS processing */ +bool RtsFileReader::isReadyForProcessing() const +{ + // Processing is ready when we have FILTER_PLUS (which is the trigger) + // and the FilterData has been properly initialized + return hasFilterPlus; +} + +/** Reset data presence flags for new epoch */ +void RtsFileReader::resetEpochFlags() +{ + hasMetadata = false; + hasMeasurements = false; + hasTransitionMatrix = false; + hasFilterMinus = false; + hasFilterPlus = false; +} + +/** Process metadata object */ +bool RtsFileReader::processMetadata(FilterData& filterData) +{ + // Read metadata from file into the epoch-specific map. + bool success = getFilterObjectFromFile( + E_SerialObject::METADATA, + filterData.metaDataMap, + currentPosition, + inputFile + ); + + if (!success) + { + BOOST_LOG_TRIVIAL(debug) << "Failed to read metadata" << "\n"; + return false; + } + + // Mark presence + hasMetadata = true; + + // Keep the smoothedKF's metadata in sync so later code that checks + // smoothedKF.metaDataMap (e.g. for TRACE filename) works correctly. + // Previously this map was never updated after the first epoch, causing + // empty lookups and early returns in output routines. + filterData.smoothedKF.metaDataMap = filterData.metaDataMap; + + // Only now evaluate skip flag using freshly loaded metadata. + auto itSkip = filterData.smoothedKF.metaDataMap.find("SKIP_PREV_RTS"); + if (itSkip != filterData.smoothedKF.metaDataMap.end()) + { + filterData.skipNextRts = (itSkip->second == "TRUE"); + } + else + { + filterData.skipNextRts = false; + } + + return true; +} + +/** Process measurement object */ +bool RtsFileReader::processMeasurement(FilterData& filterData) +{ + bool success = getFilterObjectFromFile( + E_SerialObject::MEASUREMENT, + filterData.measurements, + currentPosition, + inputFile + ); + if (success) + { + hasMeasurements = true; + } + + return success; +} + +/** Process transition matrix object */ +bool RtsFileReader::processTransitionMatrix(FilterData& filterData) +{ + bool success = getFilterObjectFromFile( + E_SerialObject::TRANSITION_MATRIX, + filterData.transitionMatrixObject, + currentPosition, + inputFile + ); + if (success) + { + MatrixXd transition = filterData.transitionMatrixObject.asMatrix(); + filterData.updateTransitionMatrix(transition); + hasTransitionMatrix = true; + } + + return success; +} + +/** Process filter minus object */ +bool RtsFileReader::processFilterMinus(FilterData& filterData) +{ + bool success = getFilterObjectFromFile( + E_SerialObject::FILTER_MINUS, + filterData.kalmanMinus, + currentPosition, + inputFile + ); + if (success) + { + if (filterData.smoothedXready == false) + { + filterData.smoothedXready = true; + } + + // assume a trivial transition matrix unless overridden + filterData.resetTransitionMatrix(filterData.kalmanMinus.x.rows()); + hasFilterMinus = true; + } + + return success; +} + +/** Process filter plus object */ +bool RtsFileReader::processFilterPlus(FilterData& filterData) +{ + bool success = getFilterObjectFromFile( + E_SerialObject::FILTER_PLUS, + filterData.kalmanPlus, + currentPosition, + inputFile + ); + if (success) + { + hasFilterPlus = true; + } + + return success; +} + +//================================================================================ +// FilterData Implementation +//================================================================================ + +/** Reset transition matrix to identity */ +void FilterData::resetTransitionMatrix(int size) +{ + transitionMatrix = MatrixXd::Identity(size, size); +} + +/** Update transition matrix by multiplying with new transition */ +void FilterData::updateTransitionMatrix(const MatrixXd& newTransition) +{ + if (transitionMatrix.rows() == 0) + transitionMatrix = newTransition; + else + transitionMatrix = (transitionMatrix * newTransition).eval(); +} + +/** Initialize smoothed filter from kalman plus state */ +void FilterData::initializeSmoothedFilter(const KFState& kfState) +{ + smoothedKF = kfState; + smoothedPready = true; +} + +//================================================================================ +// RtsProcessor Implementation +//================================================================================ + +/** Constructor with dependency injection */ +RtsProcessor::RtsProcessor( + const string& outputFilename, + bool write, + const RtsConfiguration& configuration +) + : outputFile(outputFilename), writeOutput(write), config(configuration) +{ +} + +/** Initialize first epoch smoothed filter */ +bool RtsProcessor::initializeFirstEpoch(FilterData& filterData, KFState& kfState) +{ + filterData.kalmanPlus.metaDataMap = kfState.metaDataMap; + filterData.initializeSmoothedFilter(filterData.kalmanPlus); + + if (writeOutput) + { + spitFilterToFile( + filterData.smoothedKF, + E_SerialObject::FILTER_SMOOTHED, + outputFile, + config.queue_rts_outputs + ); + spitFilterToFile( + filterData.measurements, + E_SerialObject::MEASUREMENT, + outputFile, + config.queue_rts_outputs + ); + } + + return true; +} + +/** Perform RTS computation and output for current epoch */ +bool RtsProcessor::performRtsComputationAndOutput( + FilterData& filterData, + KFState& kfState, + GTime& epochStartTime +) +{ + // Perform the mathematical RTS computation + bool rtsSuccess = filterData.performRtsComputation(kfState, config); + if (!rtsSuccess) + { + return false; + } + + // Handle output operations + handleOutput(filterData); + + // Handle timing and logging + GTime epochStopTime = timeGet(); + handleTimingAndLogging(filterData, epochStartTime, epochStopTime); + + return true; +} + +/** Process complete epoch data (computation + output) */ +bool RtsProcessor::processEpoch( + FilterData& filterData, + KFState& kfState, + GTime& epochStartTime, + double& lag +) +{ + if (filterData.smoothedPready == false) + { + return initializeFirstEpoch(filterData, kfState); + } + + // Update lag calculation + lag = (kfState.time - filterData.kalmanPlus.time).to_double(); + + return performRtsComputationAndOutput(filterData, kfState, epochStartTime); +} + +/** Write metadata to output file */ +void RtsProcessor::writeMetadata(FilterData& filterData) +{ + if (writeOutput) + { + spitFilterToFile( + filterData.metaDataMap, + E_SerialObject::METADATA, + outputFile, + config.queue_rts_outputs + ); + } +} + +/** Handle output operations */ +void RtsProcessor::handleOutput(FilterData& filterData) +{ + if (writeOutput) + { + spitFilterToFile( + filterData.smoothedKF, + E_SerialObject::FILTER_SMOOTHED, + outputFile, + config.queue_rts_outputs + ); + spitFilterToFile( + filterData.measurements, + E_SerialObject::MEASUREMENT, + outputFile, + config.queue_rts_outputs + ); + + if (filterData.skipNextRts) + { + filterData.skipNextRts = false; + filterData.smoothedKF.metaDataMap["SKIP_RTS_OUTPUT"] = "TRUE"; + spitFilterToFile( + filterData.smoothedKF.metaDataMap, + E_SerialObject::METADATA, + outputFile, + config.queue_rts_outputs + ); + } + } + else // Eugene: not used? + { + // Handle non-write case if needed + if (filterData.smoothedKF.metaDataMap.find(TRACE_FILENAME_STR + SMOOTHED_SUFFIX) != + filterData.smoothedKF.metaDataMap.end()) + { + GTime dummyTime; + Network dummyNet; + std::ofstream trace( + filterData.smoothedKF.metaDataMap.at(TRACE_FILENAME_STR + SMOOTHED_SUFFIX), + std::ofstream::out | std::ofstream::app + ); + } + } +} + +/** Handle timing and logging */ +void RtsProcessor::handleTimingAndLogging( + const FilterData& filterData, + GTime& epochStartTime, + GTime epochStopTime +) +{ + RtsTimingLogger::logEpochTiming(filterData, epochStartTime, epochStopTime); +} + +//================================================================================ +// RtsTimingLogger Implementation +//================================================================================ + +/** Log epoch processing timing and update progress */ +void RtsTimingLogger::logEpochTiming( + const FilterData& filterData, + GTime& epochStartTime, + GTime epochStopTime +) +{ + auto boostTime = formatTimeForLogging(filterData.kalmanPlus.time); + + BOOST_LOG_TRIVIAL(info) << "Processed epoch" << " - " << boostTime << " (took " + << (epochStopTime - epochStartTime) << ")"; + + updateTerminalProgress(filterData, epochStartTime, epochStopTime); + + 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, + GTime epochStartTime, + GTime epochStopTime +) +{ + // 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 +//================================================================================ +/** + * Solve a system of linear equations Ax = b using LAPACKE. + * Due to the characteristics of the matrix being used in the KF, we expect the matrix to be + * symmetric and positive definite Fallback order: posv -> sysv -> gesv -> fatal (positive definite, + * symmetric, general) + * TODO add chunking to solve large systems + * TODO move to algebra.cpp/hpp [might be needed for the KF as well] + */ +void solveSystem( + const int n, ///< Size of the system + const int neqs, ///< Number of right-hand sides + double* A, ///< Matrix A + double* b +) ///< Right-hand side b +{ + int info; + // Backup original matrices + std::vector A_backup(A, A + n * n); + std::vector b_backup(b, b + n * neqs); + + // LAPACKE uses column-major order by default (LAPACK_COL_MAJOR) + info = LapackWrapper::dposv(LapackWrapper::COL_MAJOR, 'U', n, neqs, A, n, b, n); + if (info == 0) + { + return; + } + BOOST_LOG_TRIVIAL(warning) << "Solver posv failed, moving to sysv. " << info << " " + << A[(info - 1) * n + info - 1]; + + std::vector ipiv(n); + + // Reinitialize matrices + std::memcpy(A, A_backup.data(), n * n * sizeof(double)); + std::memcpy(b, b_backup.data(), n * neqs * sizeof(double)); + + info = LapackWrapper::dsysv(LapackWrapper::COL_MAJOR, 'U', n, neqs, A, n, ipiv.data(), b, n); + if (info == 0) + { + return; + } + BOOST_LOG_TRIVIAL(warning) << "Solver sysv failed, moving to gesv."; + + // Reinitialize matrices + std::memcpy(A, A_backup.data(), n * n * sizeof(double)); + std::memcpy(b, b_backup.data(), n * neqs * sizeof(double)); + + info = LapackWrapper::dgetrf(LapackWrapper::COL_MAJOR, n, n, A, n, ipiv.data()); + if (info == 0) + { + info = + LapackWrapper::dgetrs(LapackWrapper::COL_MAJOR, 'N', n, neqs, A, n, ipiv.data(), b, n); + if (info == 0) + { + return; + } + } + // If all solvers fail + BOOST_LOG_TRIVIAL(fatal) << "All LAPACK solvers failed. Something is really wrong. " << info; +} + +/** Perform RTS smoothing computation for current epoch */ +bool FilterData::performRtsComputation(KFState& kfState, const RtsConfiguration& config) +{ + if (smoothedXready == false) + { + return false; + } + + if (config.rts_only && kfState.time == GTime::noTime()) + { + kfState.time = kalmanPlus.time; + } + double lag = (kfState.time - kalmanPlus.time).to_double(); + + BOOST_LOG_TRIVIAL(info) << "RTS lag: " << lag; + + smoothedKF.time = kalmanPlus.time; + + smoothedKF.P = (smoothedKF.P + smoothedKF.P.transpose()).eval() / 2; + + // get process noise and dynamics + auto& F = transitionMatrix; + + if (F.rows() == 0 && F.cols() == 0) + { + // assume identity state transition if none was performed/required + F = MatrixXd::Identity(kalmanPlus.P.rows(), kalmanPlus.P.rows()); + } + + MatrixXd FP = F * kalmanPlus.P; + + VectorXd deltaX = VectorXd::Zero(kalmanPlus.x.rows()); + MatrixXd deltaP = MatrixXd::Zero(kalmanPlus.P.rows(), kalmanPlus.P.cols()); + + map filterChunks; + for (auto& [id, fcP] : kalmanPlus.filterChunkMap) + filterChunks[id] = true; + for (auto& [id, fcM] : kalmanMinus.filterChunkMap) + filterChunks[id] = true; + + for (auto& [id, dummy] : filterChunks) + { + auto& fcP = kalmanPlus.filterChunkMap[id]; + auto& fcM = kalmanMinus.filterChunkMap[id]; + + if (fcP.begX == 0) + { + fcP.begX = 1; + fcP.numX -= 1; + } + if (fcM.begX == 0) + { + fcM.begX = 1; + fcM.numX -= 1; + } + + if (fcP.numX == 0 || fcM.numX == 0) + { + BOOST_LOG_TRIVIAL(debug) << "Ignoring chunk " << id; + continue; + } + + MatrixXd Q = kalmanMinus.P.block(fcM.begX, fcM.begX, fcM.numX, fcM.numX); + MatrixXd FP_ = FP.block(fcM.begX, fcP.begX, fcM.numX, fcP.numX); + int n = fcM.numX; + int neqs = fcP.numX; + Q += MatrixXd::Identity(fcM.numX, fcM.numX) * config.regularisation; + + solveSystem(fcM.numX, fcP.numX, Q.data(), FP_.data()); + + auto deltaX_ = deltaX.segment(fcP.begX, fcP.numX); + auto smoothedX = smoothedKF.x.segment(fcM.begX, fcM.numX); + auto xMinus = kalmanMinus.x.segment(fcM.begX, fcM.numX); + auto deltaP_ = deltaP.block(fcP.begX, fcP.begX, fcP.numX, fcP.numX); + auto smoothedP = smoothedKF.P.block(fcM.begX, fcM.begX, fcM.numX, fcM.numX); + auto minuxP = kalmanMinus.P.block(fcM.begX, fcM.begX, fcM.numX, fcM.numX); + + VectorXd xChanged = smoothedX - xMinus; + + // Use CBLAS for matrix-vector multiplication: deltaX_ = FP_^T * xChanged + LapackWrapper::dgemv( + LapackWrapper::COL_MAJOR, + LapackWrapper::CblasTrans, + n, + neqs, + 1.0, + FP_.data(), + n, + xChanged.data(), + 1, + 0.0, + deltaX_.data(), + 1 + ); + + MatrixXd dP = smoothedP - minuxP; + MatrixXd temp = MatrixXd::Zero(neqs, n); + + // Use CBLAS for matrix-matrix multiplication: temp = FP_^T * dP + LapackWrapper::dgemm( + LapackWrapper::COL_MAJOR, + LapackWrapper::CblasTrans, + LapackWrapper::CblasNoTrans, + neqs, + n, + n, + 1.0, + FP_.data(), + n, + dP.data(), + n, + 0.0, + temp.data(), + neqs + ); + + // Use CBLAS for matrix-matrix multiplication: deltaP_ = temp * FP_ + // NOTE: deltaP_ is a block reference, so leading dimension is deltaP.rows() + LapackWrapper::dgemm( + LapackWrapper::COL_MAJOR, + LapackWrapper::CblasNoTrans, + LapackWrapper::CblasNoTrans, + neqs, + neqs, + n, + 1.0, + temp.data(), + neqs, + FP_.data(), + n, + 0.0, + deltaP_.data(), + deltaP.rows() // Parent matrix row count, not block size + ); + } + + smoothedKF.dx = deltaX; + smoothedKF.x = deltaX + kalmanPlus.x; + smoothedKF.P = deltaP + kalmanPlus.P; + + if (measurements.H.rows()) + if (measurements.H.cols() == deltaX.rows()) + { + measurements.VV -= measurements.H * deltaX; + } + else + { + BOOST_LOG_TRIVIAL(error) << "RTScrewy" << "\n"; + } + + smoothedKF.kfIndexMap = kalmanPlus.kfIndexMap; + + return true; +} + +//================================================================================ +// RtsOutputFileReader Implementation +//================================================================================ + +/** Constructor */ +RtsOutputFileReader::RtsOutputFileReader(const string& filename) : reversedStatesFilename(filename) +{ +} + +/** Read the next object from reversed file */ +E_SerialObject RtsOutputFileReader::readNextObject(FilterData& filterData) +{ + E_SerialObject type = getFilterTypeFromFile(currentPosition, reversedStatesFilename); + + BOOST_LOG_TRIVIAL(debug) << "Outputting " << enum_to_string(type) << " from file position " + << currentPosition << "\n"; + + if (type == E_SerialObject::NONE) + { + return type; + } + + bool success = false; + + switch (type) + { + case E_SerialObject::METADATA: + success = processMetadataForOutput(filterData); + break; + + case E_SerialObject::MEASUREMENT: + success = processMeasurementForOutput(filterData); + break; + + case E_SerialObject::FILTER_SMOOTHED: + success = processSmoothedFilterForOutput(filterData); + break; + + default: + BOOST_LOG_TRIVIAL(error) << "UNEXPECTED RTS OUTPUT TYPE"; + return E_SerialObject::NONE; + } + + if (!success) + { + return E_SerialObject::NONE; + } + + return type; +} + +/** Process metadata object for output */ +bool RtsOutputFileReader::processMetadataForOutput(FilterData& filterData) +{ + bool success = getFilterObjectFromFile( + E_SerialObject::METADATA, + filterData.metaDataMap, + currentPosition, + reversedStatesFilename + ); + if (!success) + { + BOOST_LOG_TRIVIAL(error) << "BAD RTS OUTPUT read"; + } + return success; +} + +/** Process measurement object for output */ +bool RtsOutputFileReader::processMeasurementForOutput(FilterData& filterData) +{ + bool success = getFilterObjectFromFile( + E_SerialObject::MEASUREMENT, + filterData.measurements, + currentPosition, + reversedStatesFilename + ); + if (!success) + { + BOOST_LOG_TRIVIAL(error) << "BAD RTS OUTPUT read"; + } + return success; +} + +/** Process smoothed filter object for output */ +bool RtsOutputFileReader::processSmoothedFilterForOutput(FilterData& filterData) +{ + bool success = getFilterObjectFromFile( + E_SerialObject::FILTER_SMOOTHED, + filterData.smoothedKF, + currentPosition, + reversedStatesFilename + ); + if (!success) + { + BOOST_LOG_TRIVIAL(error) << "BAD RTS OUTPUT READ"; + } + else + { + filterData.smoothedKF.metaDataMap = filterData.metaDataMap; + } + return success; +} + +//================================================================================ +// RtsOutputProcessor Implementation +//================================================================================ + +/** Constructor with dependency injection */ +RtsOutputProcessor::RtsOutputProcessor(const RtsConfiguration& configuration) + : config(configuration) +{ +} + +/** Process measurement output */ +void RtsOutputProcessor::processMeasurementOutput(FilterData& filterData) +{ + outputResidualsToFile(filterData); + outputMeasurementsToMongo(filterData); +} + +/** Process smoothed filter output */ +void RtsOutputProcessor::processSmoothedFilterOutput( + FilterData& filterData, + ReceiverMap& receiverMap +) +{ + performEpochPostProcessing(filterData, receiverMap); + firstEpoch = false; +} + +/** Output residuals to file */ +void RtsOutputProcessor::outputResidualsToFile(FilterData& filterData) +{ + string filename = filterData.metaDataMap[TRACE_FILENAME_STR + SMOOTHED_SUFFIX]; + std::ofstream trace(filename, std::ofstream::out | std::ofstream::app); + + if (trace && config.output_residuals) + { + outputResiduals(trace, filterData.measurements, "/RTS"); + } +} + +/** Output measurements to MongoDB */ +void RtsOutputProcessor::outputMeasurementsToMongo(FilterData& filterData) +{ + if (config.output_measurements) + { + mongoMeasResiduals( + filterData.measurements.time, + filterData.measurements, + config.queue_mongo_outputs, + "/PPP_RTS" + ); + } +} + +/** Perform epoch post-processing and outputs */ +void RtsOutputProcessor::performEpochPostProcessing( + FilterData& filterData, + ReceiverMap& receiverMap +) +{ + string filename = filterData.smoothedKF.metaDataMap[TRACE_FILENAME_STR + SMOOTHED_SUFFIX]; + std::ofstream trace(filename, std::ofstream::out | std::ofstream::app); + + GTime dummyTime; + Network dummyNet; + perEpochPostProcessingAndOutputs( + trace, + dummyTime, + dummyNet, + receiverMap, + filterData.smoothedKF, + false, + true, + firstEpoch + ); +} /** Rauch-Tung-Striebel Smoothing. * Combine estimations using filtered data from before and after each epoch. - * Complete filter vectors and matrices are stored in a binary file that is able to be read in reverse. + * Complete filter vectors and matrices are stored in a binary file that is able to be read in + * reverse. */ Architecture RTS_Smoothing__() { - DOCS_REFERENCE(Binary_Archive__); -} - -void postRTSActions( - bool final, ///< This is a final answer, not intermediate - output to files - KFState& kfStateIn, ///< State to get filter traces from - ReceiverMap& receiverMap) ///< map of receivers -{ - //need to copy to not destroy the smoothed filter in those cases this is called from there - KFState kfState = kfStateIn; - - tryPrepareFilterPointers(kfState, receiverMap); - - if ( final - ||acsConfig.pppOpts.output_intermediate_rts) - { - mongoStates(kfState, - { - .suffix = "/RTS", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - if (final == false) - { - return; - } - - if (kfState.metaDataMap["SKIP_RTS_OUTPUT"] == "TRUE") - { - return; - } - - std::ofstream pppTrace(kfState.metaDataMap[TRACE_FILENAME_STR + SMOOTHED_SUFFIX], std::ofstream::out | std::ofstream::app); - kfState.outputStates(pppTrace, "/RTS"); - - if (acsConfig.pivot_receiver != "NO_PIVOT") - { - KFState pivotedState = propagateUncertainty(pppTrace, kfState); - - pivotedState.outputStates(pppTrace, "/RTS_PIVOT"); - - mongoStates(pivotedState, - { - .suffix = "/RTS_PIVOT", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - //if AR and its not the special case of forward per epoch fixed and held which already has AR in the smoothed version - if ( acsConfig.ambrOpts.mode != +E_ARmode::OFF - && acsConfig.ambrOpts.once_per_epoch - && acsConfig.ambrOpts.fix_and_hold == false) - { - fixAndHoldAmbiguities(pppTrace, kfState); - - kfState.outputStates(pppTrace, "/RTS_AR"); - - mongoStates(kfState, - { - .suffix = "/RTS_AR", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - if ( acsConfig.process_minimum_constraints - && acsConfig.minconOpts.once_per_epoch) - { - BOOST_LOG_TRIVIAL(info) << " ------- PERFORMING MIN-CONSTRAINTS --------" << "\n"; - - for (auto& [id, rec] : receiverMap) - { - rec.minconApriori = rec.aprioriPos; - } - - MinconStatistics minconStatistics; - - mincon(pppTrace, kfState, &minconStatistics); - - kfState.outputStates(pppTrace, "/RTS_CONSTRAINED"); - - mongoStates(kfState, - { - .suffix = "/RTS_CONSTRAINED", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - - outputMinconStatistics(pppTrace, minconStatistics); - } - - nav.erp.filterValues = getErpFromFilter(kfState); //todo aaron, this doesnt react well with remote filter values - - if (acsConfig.output_bias_sinex) - { - //todo aaron, this requires another ionospher kfState -// writeBiasSinex(nullStream, kfState.time, kfState, kfState.metaDataMap[BSX_FILENAME_STR + SMOOTHED_SUFFIX], receiverMap); - } - - auto time = kfState.time; - - static GTime clkOutputTime = time.floorTime(acsConfig.clocks_output_interval); - static GTime obxOutputTime = time.floorTime(acsConfig.orbex_output_interval); - static GTime sp3OutputTime = time.floorTime(acsConfig.sp3_output_interval); - - { - if (acsConfig.output_orbit_ics) { outputOrbitConfig ( kfState, acsConfig.pppOpts.rts_smoothed_suffix); } - if (acsConfig.output_clocks) while (clkOutputTime <= time) { outputClocks ( kfState.metaDataMap[CLK_FILENAME_STR + SMOOTHED_SUFFIX], clkOutputTime, acsConfig.clocks_receiver_sources, acsConfig.clocks_satellite_sources, kfState, &receiverMap); clkOutputTime += std::max(acsConfig.epoch_interval, acsConfig.clocks_output_interval); } - if (acsConfig.output_orbex) while (obxOutputTime <= time) { outputOrbex ( kfState.metaDataMap[ORBEX_FILENAME_STR + SMOOTHED_SUFFIX], obxOutputTime, acsConfig.orbex_orbit_sources, acsConfig.orbex_clock_sources, acsConfig.orbex_attitude_sources, &kfState); obxOutputTime += std::max(acsConfig.epoch_interval, acsConfig.orbex_output_interval); } - if (acsConfig.output_sp3) while (sp3OutputTime <= time) { outputSp3 ( kfState.metaDataMap[SP3_FILENAME_STR + SMOOTHED_SUFFIX], sp3OutputTime, acsConfig.sp3_orbit_sources, acsConfig.sp3_clock_sources, &kfState); sp3OutputTime += std::max(acsConfig.epoch_interval, acsConfig.sp3_output_interval); } - if (acsConfig.output_trop_sinex) { outputTropSinex ( kfState.metaDataMap[TROP_FILENAME_STR + SMOOTHED_SUFFIX], time, kfState, "MIX", true); } - if (acsConfig.output_ionex) { ionexFileWrite (nullStream, kfState.metaDataMap[IONEX_FILENAME_STR + SMOOTHED_SUFFIX], time, kfState); } - if (acsConfig.output_erp) { writeErpFromNetwork ( kfState.metaDataMap[ERP_FILENAME_STR + SMOOTHED_SUFFIX], kfState); } - if (acsConfig.output_ionstec) { writeIonStec ( kfState.metaDataMap[IONSTEC_FILENAME_STR + SMOOTHED_SUFFIX], kfState); } - } - - for (auto& [id, rec] : receiverMap) - { - if (acsConfig.output_cost) { outputCost ( kfState.metaDataMap[COST_FILENAME_STR + id + SMOOTHED_SUFFIX], kfState, rec); } - if (acsConfig.output_gpx) { writeGPX ( kfState.metaDataMap[GPX_FILENAME_STR + id + SMOOTHED_SUFFIX], kfState, rec); } - if (acsConfig.output_pos) { writePOS ( kfState.metaDataMap[POS_FILENAME_STR + id + SMOOTHED_SUFFIX], kfState, rec); } - } + DOCS_REFERENCE(Binary_Archive__); } /** Output filter states in chronological order from a reversed binary trace file -*/ -void RTS_Output( - KFState& kfState, ///< State to get filter traces from - ReceiverMap& receiverMap) ///< map of receivers -{ - InteractiveTerminal::setMode(E_InteractiveMode::Outputs); - - string reversedStatesFilename = kfState.rts_basename + BACKWARD_SUFFIX; - - long int startPos = -1; - - BOOST_LOG_TRIVIAL(info) - << "Outputting RTS products..." << "\n"; - - map metaDataMap = kfState.metaDataMap; - - while (1) - { - E_SerialObject type = getFilterTypeFromFile(startPos, reversedStatesFilename); - - BOOST_LOG_TRIVIAL(debug) - << "Outputting " << type._to_string() << " from file position " << startPos << "\n"; - - if (type == +E_SerialObject::NONE) - { - break; - } - - switch (type) - { - default: - { - BOOST_LOG_TRIVIAL(error) << "UNEXPECTED RTS OUTPUT TYPE"; - return; - } - - case E_SerialObject::METADATA: - { - bool pass = getFilterObjectFromFile(type, metaDataMap, startPos, reversedStatesFilename); - if (pass == false) - { - BOOST_LOG_TRIVIAL(error) << "BAD RTS OUTPUT read"; - return; - } - - break; - } - - case E_SerialObject::MEASUREMENT: - { - KFMeas archiveMeas; - bool pass = getFilterObjectFromFile(type, archiveMeas, startPos, reversedStatesFilename); - if (pass == false) - { - BOOST_LOG_TRIVIAL(error) << "BAD RTS OUTPUT read"; - return; - } - - string filename = metaDataMap[TRACE_FILENAME_STR + SMOOTHED_SUFFIX]; - - std::ofstream ofs(filename, std::ofstream::out | std::ofstream::app); - if ( ofs - &&acsConfig.output_residuals) - { - outputResiduals(ofs, archiveMeas, -1, "/RTS", 0, archiveMeas.obsKeys.size()); - } - - if (acsConfig.mongoOpts.output_measurements) - { - mongoMeasResiduals(archiveMeas.time, archiveMeas, acsConfig.mongoOpts.queue_outputs, "/RTS"); - } - - break; - } - - case E_SerialObject::FILTER_SMOOTHED: - { - KFState archiveKF; - bool pass = getFilterObjectFromFile(type, archiveKF, startPos, reversedStatesFilename); - - if (pass == false) - { - BOOST_LOG_TRIVIAL(error) << "BAD RTS OUTPUT READ"; - return; - } - - archiveKF.metaDataMap = metaDataMap; - - postRTSActions(true, archiveKF, receiverMap); - - break; - } - } - - if (startPos == 0) - { - return; - } - - if (startPos < 0) - { - BOOST_LOG_TRIVIAL(error) - << "Oopsie " << "\n"; - - return; - } - } + */ +void rtsOutput( + KFState& kfState, ///< State to get filter traces from + ReceiverMap& receiverMap, ///< map of receivers + const RtsConfiguration* config ///< Configuration for dependency injection +) +{ + string reversedStatesFilename = kfState.rts_basename + BACKWARD_SUFFIX; + + BOOST_LOG_TRIVIAL(info) << "Outputting RTS products..." << "\n"; + + // Use provided config or create default from acsConfig + RtsConfiguration defaultConfig = RtsConfiguration::fromAcsConfig(); + const RtsConfiguration& rtsConfig = config ? *config : defaultConfig; + + // Initialize the output reader and processor using SOLID principles + RtsOutputFileReader outputReader(reversedStatesFilename); + RtsOutputProcessor outputProcessor(rtsConfig); + FilterData filterData; + filterData.metaDataMap = kfState.metaDataMap; + + int objectCount = 0; + while (true) + { + E_SerialObject type = outputReader.readNextObject(filterData); + if (type == E_SerialObject::NONE) + { + break; + } + + objectCount++; + + // Process different object types using the processor + switch (type) + { + case E_SerialObject::METADATA: + // Metadata is read but no additional processing needed + break; + + case E_SerialObject::MEASUREMENT: + outputProcessor.processMeasurementOutput(filterData); + break; + + case E_SerialObject::FILTER_SMOOTHED: + outputProcessor.processSmoothedFilterOutput(filterData, receiverMap); + break; + + default: + // Error handling is already done in the reader + break; + } + + if (outputReader.isAtBeginning()) + { + return; + } + + if (outputReader.isInvalidPosition()) + { + BOOST_LOG_TRIVIAL(error) << "Invalid file position reached during RTS output" << "\n"; + return; + } + } } /** Iterate over stored filter states in reverse and perform filtering. - * Saves filtered states to a secondary binary file, which is in reverse-chronological order due to the save sequence. - * Most serial objects that are processed are merely stored or accumulated as prerequisites for the FILTER_PLUS object, - * which contains the state of the filter immediately after the update step. - * At that stage, the previously smoothed (next chronologically) filter state is combined with the next filter minus state - * (immediately before the next chronological update step), any state transitions, and the filter plus state, using the standard rts algorithm. - * The filtered state and a measurements object which has updated residuals are then stored in a binary file. - * If intermediate outputs are enabled (rare) it performs some outputs using each filter state, but typically outputs all states chronologically - * after the reverse running rts procedure has reached the first epoch and all data is available for output in the correct sequence. + * Saves filtered states to a secondary binary file, which is in reverse-chronological order due to + * the save sequence. Most serial objects that are processed are merely stored or accumulated as + * prerequisites for the FILTER_PLUS object, which contains the state of the filter immediately + * after the update step. At that stage, the previously smoothed (next chronologically) filter state + * is combined with the next filter minus state (immediately before the next chronological update + * step), any state transitions, and the filter plus state, using the standard rts algorithm. The + * filtered state and a measurements object which has updated residuals are then stored in a binary + * file. If intermediate outputs are enabled (rare) it performs some outputs using each filter + * state, but typically outputs all states chronologically after the reverse running rts procedure + * has reached the first epoch and all data is available for output in the correct sequence. */ void rtsSmoothing( - KFState& kfState, - ReceiverMap& receiverMap, - bool write) -{ - DOCS_REFERENCE(RTS_Smoothing__); - - if (kfState.rts_lag == 0) - { - return; - } - - BOOST_LOG_TRIVIAL(info) - << "\n" - << "---------------PROCESSING WITH RTS--------------------- " << "\n"; - - for (auto& [id, rec] : receiverMap) - rec.obsList.clear(); - - for (auto& [dummy, satNav] : nav.satNavMap) - satNav.attStatus = {}; - - MatrixXd transitionMatrix; - - KFState kalmanMinus; - KFState smoothedKF; - KFMeas measurements; - - bool smoothedXready = false; - bool smoothedPready = false; - bool skipNextRts = false; - - string inputFile = kfState.rts_basename + FORWARD_SUFFIX; - string outputFile = kfState.rts_basename + BACKWARD_SUFFIX; - - if (write) - { - std::ofstream ofs(outputFile, std::ofstream::out | std::ofstream::trunc); - } - - long int startPos = -1; - double lag = 0; - - GTime epochStartTime = timeGet(); - - while (lag != kfState.rts_lag) - { - E_SerialObject type = getFilterTypeFromFile(startPos, inputFile); - - BOOST_LOG_TRIVIAL(debug) << "Found " << type._to_string() << "\n"; - - if (type == +E_SerialObject::NONE) - { - break; - } - - switch (type) - { - default: - { - BOOST_LOG_TRIVIAL(error) << "Error: Unknown rts type" << "\n"; - break; - } - case E_SerialObject::METADATA: - { - skipNextRts = (smoothedKF.metaDataMap["SKIP_PREV_RTS"] == "TRUE"); - - bool pass = getFilterObjectFromFile(type, smoothedKF.metaDataMap, startPos, inputFile); - if (pass == false) - { - BOOST_LOG_TRIVIAL(debug) << "CREASS" << "\n"; - return; - } - - if (write) - { - spitFilterToFile(smoothedKF.metaDataMap, E_SerialObject::METADATA, outputFile, acsConfig.pppOpts.queue_rts_outputs); - } - - break; - } - case E_SerialObject::MEASUREMENT: - { - bool pass = getFilterObjectFromFile(type, measurements, startPos, inputFile); - if (pass == false) - { - return; - } - - break; - } - case E_SerialObject::TRANSITION_MATRIX: - { - TransitionMatrixObject transistionMatrixObject; - bool pass = getFilterObjectFromFile(type, transistionMatrixObject, startPos, inputFile); - if (pass == false) - { - return; - } - - MatrixXd transition = transistionMatrixObject.asMatrix(); - - if (transitionMatrix.rows() == 0) transitionMatrix = transition; - else transitionMatrix = (transitionMatrix * transition).eval(); - - break; - } - case E_SerialObject::FILTER_MINUS: - { - bool pass = getFilterObjectFromFile(type, kalmanMinus, startPos, inputFile); - if (pass == false) - { - return; - } - - if (smoothedXready == false) - { - smoothedXready = true; - } - - transitionMatrix = MatrixXd::Identity(0, 0); - - break; - } - case E_SerialObject::FILTER_PLUS: - { - KFState kalmanPlus; - bool pass = getFilterObjectFromFile(type, kalmanPlus, startPos, inputFile); - if (pass == false) - { - return; - } - - lag = (kfState.time - kalmanPlus.time).to_double(); - - if (write) - { - BOOST_LOG_TRIVIAL(info) - << "RTS lag: " << lag; - } - - if (smoothedPready == false) - { - kalmanPlus.metaDataMap = kfState.metaDataMap; - - smoothedPready = true; - smoothedKF = kalmanPlus; - - if (write) - { - spitFilterToFile(smoothedKF, E_SerialObject::FILTER_SMOOTHED, outputFile, acsConfig.pppOpts.queue_rts_outputs); - spitFilterToFile(measurements, E_SerialObject::MEASUREMENT, outputFile, acsConfig.pppOpts.queue_rts_outputs); - } - - break; - } - - if (smoothedXready == false) - { - break; - } - - InteractiveTerminal::setMode(E_InteractiveMode::Filtering); - - smoothedKF.time = kalmanPlus.time; - - smoothedKF.P = (smoothedKF.P + smoothedKF.P.transpose()).eval() / 2; - - //get process noise and dynamics - auto& F = transitionMatrix; - - if ( F.rows() == 0 - &&F.cols() == 0) - { - //assume identity state transition if none was performed/required - F = MatrixXd::Identity(kalmanPlus.P.rows(), kalmanPlus.P.rows()); - } - - MatrixXd FP = F * kalmanPlus.P; - - E_Inverter inverter = acsConfig.pppOpts.rts_inverter; - - auto failInversion = [&]() - { - auto oldInverter = inverter; - inverter = E_Inverter::_from_integral(((int)inverter)+1); - - BOOST_LOG_TRIVIAL(warning) - << "Warning: Inverter type " << oldInverter._to_string() << " failed, trying " << inverter._to_string(); - }; - VectorXd deltaX = VectorXd::Zero(kalmanPlus.x.rows()); - MatrixXd deltaP = MatrixXd::Zero(kalmanPlus.P.rows(), kalmanPlus.P.cols()); - - map filterChunks; - for (auto& [id, fcP] : kalmanPlus. filterChunkMap) filterChunks[id] = true; - for (auto& [id, fcM] : kalmanMinus.filterChunkMap) filterChunks[id] = true; - - for (auto& [id, dummy] : filterChunks) - { - auto& fcP = kalmanPlus. filterChunkMap[id]; - auto& fcM = kalmanMinus.filterChunkMap[id]; - - if ( fcP.numX == 0 - ||fcM.numX == 0) - { - BOOST_LOG_TRIVIAL(debug) << "Ignoring chunk " << id; - continue; - } - - BOOST_LOG_TRIVIAL(debug) << "Filtering chunk " << id; - auto Q = kalmanMinus.P .block(fcM.begX, fcM.begX, fcM.numX, fcM.numX).triangularView().transpose(); - auto FP_ = FP .block(fcM.begX, fcP.begX, fcM.numX, fcP.numX); - - MatrixXd Ck; - - int pass = false; - - auto solve = [&](SOLVER solver) -> bool - { - solver.compute(Q); - if (solver.info()) - { - pass = false; - return pass; - } - - Ck = solver.solve(FP_).transpose(); - - if (solver.info()) - { - pass = false; - return pass; - } - - pass = true; - return pass; - }; - - while (inverter != +E_Inverter::FIRST_UNSUPPORTED - &&(pass == false)) - switch (inverter) - { - default: - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Inverter type " << acsConfig.pppOpts.rts_inverter._to_string() << " not supported, reverting to LDLT"; - - acsConfig.pppOpts.rts_inverter = E_Inverter::LDLT; - inverter = E_Inverter::LDLT; - - continue; - } - case E_Inverter::FULLPIVLU: - case E_Inverter::INV: - { - Eigen::FullPivLU solver(kalmanMinus.P.block(fcM.begX, fcM.begX, fcM.numX, fcM.numX)); - - if (solver.isInvertible() == false) - { - failInversion(); - - break; - } - - MatrixXd Pinv = solver.inverse(); - - Pinv = (Pinv + Pinv.transpose()).eval() / 2; - - Ck = (FP_.transpose() * Pinv); - - pass = true; - - break; - } - case E_Inverter::LLT: { solve(Eigen::LLT ()); if (pass == false) failInversion(); break; } - case E_Inverter::LDLT: { solve(Eigen::LDLT ()); if (pass == false) failInversion(); break; } - case E_Inverter::COLPIVHQR: { solve(Eigen::ColPivHouseholderQR ()); if (pass == false) failInversion(); break; } - case E_Inverter::BDCSVD: { solve(Eigen::BDCSVD ()); if (pass == false) failInversion(); break; } - case E_Inverter::JACOBISVD: { solve(Eigen::JacobiSVD ()); if (pass == false) failInversion(); break; } - // case E_Inverter::FULLPIVHQR:{ solve(Eigen::FullPivHouseholderQR ()); if (pass == false) failInversion(); break; } - } - - if (pass == false) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: RTS failed to find solution to invert system of equations, smoothed values may be bad"; - - BOOST_LOG_TRIVIAL(debug) << "P-det: " << kalmanMinus.P.determinant(); - - kalmanMinus.outputConditionNumber(std::cout); - - BOOST_LOG_TRIVIAL(debug) << "P:\n" << kalmanMinus.P.format(heavyFmt); - kalmanMinus.outputCorrelations(std::cout); - std::cout << "\n"; - - //break out of the loop - lag = kfState.rts_lag; - break; - } - - auto deltaX_ = deltaX .segment (fcP.begX, fcP.numX); - auto smoothedX = smoothedKF. x.segment (fcM.begX, fcM.numX); - auto xMinus = kalmanMinus. x.segment (fcM.begX, fcM.numX); - auto deltaP_ = deltaP .block (fcP.begX, fcP.begX, fcP.numX, fcP.numX); - auto smoothedP = smoothedKF. P.block (fcM.begX, fcM.begX, fcM.numX, fcM.numX); - auto minuxP = kalmanMinus. P.block (fcM.begX, fcM.begX, fcM.numX, fcM.numX); - - deltaX_ = Ck * (smoothedX - xMinus); - deltaP_ = Ck * (smoothedP - minuxP) * Ck.transpose(); - } - - smoothedKF.dx = deltaX; - smoothedKF.x = deltaX + kalmanPlus.x; - smoothedKF.P = deltaP + kalmanPlus.P; - - if (measurements.H.rows()) - if (measurements.H.cols() == deltaX.rows()) - { - measurements.VV -= measurements.H * deltaX; - } - else - { - BOOST_LOG_TRIVIAL(error) << "RTScrewy" << "\n"; - } - - smoothedKF.kfIndexMap = kalmanPlus.kfIndexMap; - - if (write) - { - InteractiveTerminal::setMode(E_InteractiveMode::Outputs); - - spitFilterToFile(smoothedKF, E_SerialObject::FILTER_SMOOTHED, outputFile, acsConfig.pppOpts.queue_rts_outputs); - spitFilterToFile(measurements, E_SerialObject::MEASUREMENT, outputFile, acsConfig.pppOpts.queue_rts_outputs); - - if (skipNextRts) - { - skipNextRts = false; - smoothedKF.metaDataMap["SKIP_RTS_OUTPUT"] = "TRUE"; - spitFilterToFile(smoothedKF.metaDataMap, E_SerialObject::METADATA, outputFile, acsConfig.pppOpts.queue_rts_outputs); - } - } - else - { - bool final = false; - if (lag >= kfState.rts_lag) - { - final = true; - } - - postRTSActions(final, smoothedKF, receiverMap); - - if (acsConfig.pppOpts.output_intermediate_rts) - { - mongoMeasResiduals(smoothedKF.time, measurements, acsConfig.mongoOpts.queue_outputs, "/RTS"); - } - } - - GTime epochStopTime = timeGet(); - if (write) - { - int fractionalMilliseconds = (kalmanPlus.time.bigTime - (long int) kalmanPlus.time.bigTime) * 1000; - auto boostTime = boost::posix_time::from_time_t((time_t)((PTime)kalmanPlus.time).bigTime) + boost::posix_time::millisec(fractionalMilliseconds); - - BOOST_LOG_TRIVIAL(info) - << "Processed epoch" - << " - " << boostTime - << " (took " << (epochStopTime-epochStartTime) << ")"; - - InteractiveTerminal::clearModes( - (string)" Processing epoch " + kalmanPlus.time.to_string(), - (string)" Last Epoch took " + std::to_string((epochStopTime-epochStartTime).to_double()) + "s"); - InteractiveTerminal::setMode(E_InteractiveMode::Syncing); - } - epochStartTime = timeGet(); - - break; - } - } - - if (startPos == 0) - { - break; - } - } - - if (write) - { - while (spitQueueRunning) - { - sleep_for(std::chrono::milliseconds(acsConfig.sleep_milliseconds)); - } - - RTS_Output(kfState, receiverMap); - } - - if (lag == kfState.rts_lag) - { - //delete the beginning of the history file - string tempFile = kfState.rts_basename + FORWARD_SUFFIX + "_temp"; - { - std::ofstream tempStream(tempFile, std::ifstream::binary | std::ofstream::out | std::ofstream::trunc); - std::fstream inputStream(inputFile, std::ifstream::binary | std::ifstream::in); - - inputStream.seekg(0, inputStream.end); - long int lengthPos = inputStream.tellg(); - - vector fileContents(lengthPos - startPos); - - inputStream.seekg(startPos, inputStream.beg); - - inputStream.read(&fileContents[0], lengthPos - startPos); - tempStream.write(&fileContents[0], lengthPos - startPos); - } - - std::remove(inputFile.c_str()); - std::rename(tempFile.c_str(), inputFile.c_str()); - } - - if ( kfState.rts_lag <= 0 - && acsConfig.retain_rts_files == false) - { - BOOST_LOG_TRIVIAL(info) - << "Removing RTS file: " << inputFile; - - std::remove(inputFile.c_str()); - - BOOST_LOG_TRIVIAL(info) - << "Removing RTS file: " << outputFile; - - std::remove(outputFile.c_str()); - } + KFState& kfState, + ReceiverMap& receiverMap, + bool write, + const RtsConfiguration* config +) +{ + DOCS_REFERENCE(RTS_Smoothing__); + + if (kfState.rts_lag == 0) + { + return; + } + + BOOST_LOG_TRIVIAL(info) << "\n" + << "---------------PROCESSING WITH RTS--------------------- " << "\n"; + + for (auto& [id, rec] : receiverMap) + rec.obsList.clear(); + + for (auto& [dummy, satNav] : nav.satNavMap) + satNav.attStatus = {}; + + // Use provided config or create default from acsConfig + RtsConfiguration defaultConfig = RtsConfiguration::fromAcsConfig(); + const RtsConfiguration& rtsConfig = config ? *config : defaultConfig; + + // Initialize the reader and processor using SOLID principles + string inputFile = kfState.rts_basename + FORWARD_SUFFIX; + string outputFile = kfState.rts_basename + BACKWARD_SUFFIX; + + RtsFileReader reader(inputFile); + RtsProcessor processor(outputFile, write, rtsConfig); + FilterData filterData; + + if (write) // Eugene: not used? + { + std::ofstream ofs(outputFile, std::ofstream::out | std::ofstream::trunc); + } + + double lag = 0; + GTime epochStartTime = timeGet(); + + while (lag != kfState.rts_lag) + { + E_SerialObject type = reader.readNextObject(filterData); + + if (type == E_SerialObject::NONE) + { + break; + } + + // Handle metadata writing + if (type == E_SerialObject::METADATA) + { + processor.writeMetadata(filterData); + } + + // Check if we're ready for RTS processing (FILTER_PLUS received) + if (reader.isReadyForProcessing()) + { + bool success = processor.processEpoch(filterData, kfState, epochStartTime, lag); + if (!success) + { + // RTS computation failed, break out of the loop + lag = kfState.rts_lag; + break; + } + + // Reset flags for next epoch + reader.resetEpochFlags(); + } + + if (reader.isAtBeginning()) + { + break; + } + } + + if (write) + { + while (spitQueueRunning) + { + sleep_for(std::chrono::milliseconds(rtsConfig.sleep_milliseconds)); + } + + rtsOutput(kfState, receiverMap, &rtsConfig); + } + + if (lag == kfState.rts_lag) + { + // delete the beginning of the history file + string tempFile = kfState.rts_basename + FORWARD_SUFFIX + "_temp"; + { + std::ofstream tempStream( + tempFile, + std::ifstream::binary | std::ofstream::out | std::ofstream::trunc + ); + 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(); + + vector fileContents(lengthPos - currentPos); + + inputStream.seekg(currentPos, inputStream.beg); + + inputStream.read(&fileContents[0], lengthPos - currentPos); + tempStream.write(&fileContents[0], lengthPos - currentPos); + } + + std::remove(inputFile.c_str()); + std::rename(tempFile.c_str(), inputFile.c_str()); + } + + if (kfState.rts_lag <= 0 && rtsConfig.retain_rts_files == false) + { + BOOST_LOG_TRIVIAL(info) << "Removing RTS file: " << inputFile; + + std::remove(inputFile.c_str()); + + BOOST_LOG_TRIVIAL(info) << "Removing RTS file: " << outputFile; + + std::remove(outputFile.c_str()); + } } diff --git a/src/cpp/common/rtsSmoothing.hpp b/src/cpp/common/rtsSmoothing.hpp index 500e8f0e8..2fa9c322d 100644 --- a/src/cpp/common/rtsSmoothing.hpp +++ b/src/cpp/common/rtsSmoothing.hpp @@ -1,19 +1,251 @@ - #pragma once #include #include +#include "common/algebra.hpp" +#include "common/algebraTrace.hpp" +#include "common/eigenIncluder.hpp" using std::map; using std::string; -#include "algebra.hpp" - struct ReceiverMap; -void rtsSmoothing( - KFState& kfState, - ReceiverMap& receiverMap, - bool write = false); +/** Configuration interface for RTS processing to support dependency injection */ +struct RtsConfiguration +{ + // PPP options + bool queue_rts_outputs = false; + E_Inverter rts_inverter = E_Inverter::LDLT; + + // RTS specific options + bool rts_only = false; + + // Output options + bool output_residuals = false; + bool retain_rts_files = true; + + // MongoDB options + bool output_measurements = false; + bool queue_mongo_outputs = false; + + // Sleep timing + int sleep_milliseconds = 10; + double regularisation = 1e-10; ///< Regularisation term for RTS smoothing + /** Create configuration from global acsConfig */ + static RtsConfiguration fromAcsConfig(); +}; + +/** Structure to encapsulate all filter data objects read from binary files */ +struct FilterData +{ + KFState kalmanMinus; + KFState kalmanPlus; + KFState smoothedKF; + KFMeas measurements; + MatrixXd transitionMatrix; + TransitionMatrixObject transitionMatrixObject; + map metaDataMap; + + // State tracking flags + bool smoothedXready = false; + bool smoothedPready = false; + bool skipNextRts = false; + + /** Reset transition matrix to identity */ + void resetTransitionMatrix(int size); + + /** Update transition matrix by multiplying with new transition */ + void updateTransitionMatrix(const MatrixXd& newTransition); + + /** Initialize smoothed filter from kalman plus state */ + void initializeSmoothedFilter(const KFState& kfState); + + /** Perform RTS smoothing computation for current epoch */ + bool performRtsComputation(KFState& kfState, const RtsConfiguration& config); +}; + +/** Reader class for RTS binary file data following SOLID principles */ +class RtsFileReader +{ + private: + string inputFile; + long int currentPosition = -1; + + // Data presence flags for current epoch + bool hasMetadata = false; + bool hasMeasurements = false; + bool hasTransitionMatrix = false; + bool hasFilterMinus = false; + bool hasFilterPlus = false; + + public: + /** Constructor */ + explicit RtsFileReader(const string& filename); + + /** Read the next object from file and update FilterData */ + E_SerialObject readNextObject(FilterData& filterData); + + /** Check if we have all required data for RTS processing */ + bool isReadyForProcessing() const; + + /** Reset data presence flags for new epoch */ + void resetEpochFlags(); + + /** Get current file position */ + long int getCurrentPosition() const { return currentPosition; } + + /** Check if we've reached the beginning of file */ + bool isAtBeginning() const { return currentPosition == 0; } + + private: + /** Process metadata object */ + bool processMetadata(FilterData& filterData); + + /** Process measurement object */ + bool processMeasurement(FilterData& filterData); + + /** Process transition matrix object */ + bool processTransitionMatrix(FilterData& filterData); + + /** Process filter minus object */ + bool processFilterMinus(FilterData& filterData); + + /** Process filter plus object */ + bool processFilterPlus(FilterData& filterData); +}; + +/** Processor class for RTS computation and output following SOLID principles */ +class RtsProcessor +{ + private: + string outputFile; + bool writeOutput; + const RtsConfiguration& config; + + public: + /** Constructor with dependency injection */ + RtsProcessor(const string& outputFilename, bool write, const RtsConfiguration& configuration); + /** Initialize first epoch smoothed filter */ + bool initializeFirstEpoch(FilterData& filterData, KFState& kfState); + + /** Process complete epoch data (computation + output) */ + bool processEpoch(FilterData& filterData, KFState& kfState, GTime& epochStartTime, double& lag); + + /** Write metadata to output file */ + void writeMetadata(FilterData& filterData); + + private: + /** Perform RTS computation and output for current epoch */ + bool performRtsComputationAndOutput( + FilterData& filterData, + KFState& kfState, + GTime& epochStartTime + /* lag removed: computed where needed */ + ); + + /** Handle output operations */ + void handleOutput(FilterData& filterData); + + /** Handle timing and logging */ + void handleTimingAndLogging( + const FilterData& filterData, + GTime& epochStartTime, + GTime epochStopTime + ); +}; + +/** Timing and logging handler for RTS processing */ +class RtsTimingLogger +{ + public: + /** Log epoch processing timing and update progress */ + static void + logEpochTiming(const FilterData& filterData, GTime& epochStartTime, GTime epochStopTime); + + /** Format time for logging output */ + static boost::posix_time::ptime formatTimeForLogging(const GTime& time); + + /** Update interactive terminal with progress information */ + static void + updateTerminalProgress(const FilterData& filterData, GTime epochStartTime, GTime epochStopTime); + + /** Calculate fractional milliseconds from time */ + static int calculateFractionalMilliseconds(const GTime& time); +}; + +/** Output reader class for reading smoothed results from backward file */ +class RtsOutputFileReader +{ + private: + string reversedStatesFilename; + long int currentPosition = -1; + + public: + /** Constructor */ + explicit RtsOutputFileReader(const string& filename); + + /** Read the next object from reversed file */ + E_SerialObject readNextObject(FilterData& filterData); + + /** Check if we've reached the beginning of file */ + bool isAtBeginning() const { return currentPosition == 0; } + + /** Check if position is invalid */ + bool isInvalidPosition() const { return currentPosition < 0; } + + private: + /** Process metadata object for output */ + bool processMetadataForOutput(FilterData& filterData); + + /** Process measurement object for output */ + bool processMeasurementForOutput(FilterData& filterData); + + /** Process smoothed filter object for output */ + bool processSmoothedFilterForOutput(FilterData& filterData); +}; + +/** Output processor class for handling RTS output operations */ +class RtsOutputProcessor +{ + private: + bool firstEpoch = true; + const RtsConfiguration& config; + + public: + /** Constructor with dependency injection */ + explicit RtsOutputProcessor(const RtsConfiguration& configuration); + + /** Process measurement output */ + void processMeasurementOutput(FilterData& filterData); + + /** Process smoothed filter output */ + void processSmoothedFilterOutput(FilterData& filterData, ReceiverMap& receiverMap); + + /** Reset first epoch flag */ + void resetFirstEpoch() { firstEpoch = true; } + + private: + /** Output residuals to file */ + void outputResidualsToFile(FilterData& filterData); + + /** Output measurements to MongoDB */ + void outputMeasurementsToMongo(FilterData& filterData); + + /** Perform epoch post-processing and outputs */ + void performEpochPostProcessing(FilterData& filterData, ReceiverMap& receiverMap); +}; + +void rtsSmoothing( + KFState& kfState, + ReceiverMap& receiverMap, + bool write = false, + const RtsConfiguration* config = nullptr +); +void rtsOutput( + KFState& kfState, + ReceiverMap& receiverMap, + const RtsConfiguration* config = nullptr +); diff --git a/src/cpp/common/satStat.hpp b/src/cpp/common/satStat.hpp index e8bed2431..207e9dd29 100644 --- a/src/cpp/common/satStat.hpp +++ b/src/cpp/common/satStat.hpp @@ -1,99 +1,96 @@ - #pragma once #include +#include "common/acsQC.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/linearCombo.hpp" using std::map; -#include "eigenIncluder.hpp" - -#include "linearCombo.hpp" -#include "common.hpp" -#include "acsQC.hpp" -#include "enums.h" - /** Object containing persistent status parameters of individual signals -*/ + */ struct SigStat { - union SlipStat - { - unsigned int any = 0; ///< Non zero value indicates a slip has been detected - struct - { - unsigned LLI : 1; ///< Slip detected by loss of lock indicator - unsigned GF : 1; ///< Slip detected by geometry free combination - unsigned MW : 1; ///< Slip detected by Melbourne Wubenna combination - unsigned SCDIA : 1; ///< Slip detected DIA - }; - }; - - SlipStat savedSlip; - SlipStat slip; - - unsigned int phaseRejectCount = 0; - GTime lastPhaseTime; + union SlipStat + { + unsigned int any = 0; ///< Non zero value indicates a slip has been detected + struct + { + unsigned LLI : 1; ///< Slip detected by loss of lock indicator + unsigned GF : 1; ///< Slip detected by geometry free combination + unsigned MW : 1; ///< Slip detected by Melbourne Wubenna combination + unsigned SCDIA : 1; ///< Slip detected DIA + unsigned retrack : 1; ///< Slip detected by retrack + unsigned singleFreq : 1; ///< single frequency data + }; + }; + + SlipStat savedSlip; + SlipStat slip; + + unsigned int phaseRejectCount = 0; }; struct IonoStat { - double ambvar = 0; - double gf_amb = 0; - GTime lastObsTime = {}; - double extiono = 0; - double extionovar = 0; + double ambvar = 0; + double gf_amb = 0; + GTime lastObsTime = {}; + double extiono = 0; + double extionovar = 0; }; /** Cycle slip repair filter -*/ + */ struct flt_t { - double a[3]; ///< cycle slip state vector - double Qa[3][3]; ///< cycle slip state variance-covariance matrix - int slip; ///< cycle slip indicator for multi-epoch - int amb[3]; ///< repaired cycle slip - int ne; ///< number of epochs involved - lc_t lc_pre; ///< lc information used for cycle slip repair + double a[3]; ///< cycle slip state vector + double Qa[3][3]; ///< cycle slip state variance-covariance matrix + int slip; ///< cycle slip indicator for multi-epoch + int amb[3]; ///< repaired cycle slip + int ne; ///< number of epochs involved + lc_t lc_pre; ///< lc information used for cycle slip repair }; struct QC { - Average mwSlip = {}; ///< - Average emwSlip = {}; ///< - int amb[3] = {}; ///< repaired integer cycle slip - double mw = 0; ///< MW-LC (m) - double gf = 0; - flt_t flt = {}; ///< cycle slip repair filter - lc_t lc_pre = {}; ///< lc information - lc_t lc_new = {}; ///< lc information + Average mwSlip = {}; ///< + Average emwSlip = {}; ///< + int amb[3] = {}; ///< repaired integer cycle slip + double mw = 0; ///< MW-LC (m) + double gf = 0; + flt_t flt = {}; ///< cycle slip repair filter + lc_t lc_pre = {}; ///< lc information + lc_t lc_new = {}; ///< lc information }; struct AzEl { - double az = 0; ///< azimuth angle (rad) - double el = 0; ///< elevation angle (rad) + double az = 0; ///< azimuth angle (rad) + double el = 0; ///< elevation angle (rad) }; /** Object containing persistent status parameters of individual satellites -*/ + */ struct SatStat : IonoStat, QC, AzEl { - double phw = 0; ///< Phase windup (cycle) - double mapWet = 0; ///< troposphere wet mapping function - double mapWetGrads[2] = {}; ///< troposphere wet mapping function - VectorEcef e; ///< Line-of-sight unit vector - - GTime lastIonTime; + double phw = 0; ///< Phase windup (cycle) + double mapWet = 0; ///< troposphere wet mapping function + double mapWetGrads[2] = {}; ///< troposphere wet mapping function + VectorEcef e; ///< Line-of-sight unit vector - double dIono = 0; ///< TD ionosphere residual - double sigmaIono = 0; ///< TD ionosphere residual noise - double prevSTEC = 0; + GTime lastIonTime; - double nadir = 0; - bool slip = false; + double dIono = 0; ///< TD ionosphere residual + double sigmaIono = 0; ///< TD ionosphere residual noise + double prevSTEC = 0; - map sigStatMap; ///< Map for individual signal status for this SatStat object + double nadir = 0; + bool slip = false; + map sigStatMap; ///< Map for individual signal status for this SatStat object }; string ft2string(E_FType ft); diff --git a/src/cpp/common/satSys.cpp b/src/cpp/common/satSys.cpp index 702431829..db50b76c1 100644 --- a/src/cpp/common/satSys.cpp +++ b/src/cpp/common/satSys.cpp @@ -1,37 +1,64 @@ - // #pragma GCC optimize ("O0") -#include "satSys.hpp" - +#include "common/satSys.hpp" -map SatSys::satDataMap = -{ +map SatSys::satDataMap = { }; /** Get all satellites of target system -*/ -vector getSysSats( - E_Sys targetSys) ///< Target system + */ +vector getSysSats(E_Sys targetSys) ///< Target system { - vector sats; - /* output satellite PRN*/ - if (targetSys == +E_Sys::GPS) for (int prn = 1; prn <= NSATGPS; prn++) { sats.push_back(SatSys(E_Sys::GPS, prn)); } - if (targetSys == +E_Sys::GLO) for (int prn = 1; prn <= NSATGLO; prn++) { sats.push_back(SatSys(E_Sys::GLO, prn)); } - if (targetSys == +E_Sys::GAL) for (int prn = 1; prn <= NSATGAL; prn++) { sats.push_back(SatSys(E_Sys::GAL, prn)); } - if (targetSys == +E_Sys::BDS) for (int prn = 1; prn <= NSATBDS; prn++) { sats.push_back(SatSys(E_Sys::BDS, prn)); } - if (targetSys == +E_Sys::QZS) for (int prn = 1; prn <= NSATQZS; prn++) { sats.push_back(SatSys(E_Sys::QZS, prn)); } - if (targetSys == +E_Sys::SBS) for (int prn = 1; prn <= NSATSBS; prn++) { sats.push_back(SatSys(E_Sys::SBS, prn)); } - if (targetSys == +E_Sys::LEO) for (int prn = 1; prn <= NSATLEO; prn++) { sats.push_back(SatSys(E_Sys::LEO, prn)); } + vector sats; + /* output satellite PRN*/ + if (targetSys == E_Sys::GPS) + for (int prn = 1; prn <= NSATGPS; prn++) + { + sats.push_back(SatSys(E_Sys::GPS, prn)); + } + if (targetSys == E_Sys::GLO) + for (int prn = 1; prn <= NSATGLO; prn++) + { + sats.push_back(SatSys(E_Sys::GLO, prn)); + } + if (targetSys == E_Sys::GAL) + for (int prn = 1; prn <= NSATGAL; prn++) + { + sats.push_back(SatSys(E_Sys::GAL, prn)); + } + if (targetSys == E_Sys::BDS) + for (int prn = 1; prn <= NSATBDS; prn++) + { + sats.push_back(SatSys(E_Sys::BDS, prn)); + } + if (targetSys == E_Sys::QZS) + for (int prn = 1; prn <= NSATQZS; prn++) + { + sats.push_back(SatSys(E_Sys::QZS, prn)); + } + if (targetSys == E_Sys::SBS) + for (int prn = 1; prn <= NSATSBS; prn++) + { + sats.push_back(SatSys(E_Sys::SBS, prn)); + } + if (targetSys == E_Sys::LEO) + for (int prn = 1; prn <= NSATLEO; prn++) + { + sats.push_back(SatSys(E_Sys::LEO, prn)); + } - return sats; + return sats; } void SatSys::getId(char* str) const -{ - char sys_c = sysChar(); - - if (sys == +E_Sys::NONE) sprintf(str, ""); - else if (prn == 0) sprintf(str, "%c--", sys_c); - else sprintf(str, "%c%02d", sys_c, prn); +{ + char sys_c = sysChar(); + + if (sys == E_Sys::NONE) + sprintf(str, ""); + else if (prn == 0) + sprintf(str, "%c--", sys_c); + else + sprintf(str, "%c%02d", sys_c, prn); } diff --git a/src/cpp/common/satSys.hpp b/src/cpp/common/satSys.hpp index ce1bfffab..db45003e8 100644 --- a/src/cpp/common/satSys.hpp +++ b/src/cpp/common/satSys.hpp @@ -1,218 +1,228 @@ - #pragma once +#include #include #include -#include +#include "common/enums.h" +using std::map; using std::string; using std::vector; -using std::map; +constexpr int RAW_SBAS_PRN_OFFSET = 119; +constexpr int RAW_QZSS_PRN_OFFSET = 192; +constexpr int QZS_SAIF_PRN_OFFSET = 182; -#include "enums.h" - -#define RAW_SBAS_PRN_OFFSET 119 -#define RAW_QZSS_PRN_OFFSET 192 -#define QZS_SAIF_PRN_OFFSET 182 - -#define NSYSGPS 1 -#define NSATGPS 32 ///< potential number of GPS satellites, PRN goes from 1 to this number -#define NSATGLO 27 ///< potential number of GLONASS satellites, PRN goes from 1 to this number -#define NSATGAL 36 ///< potential number of Galileo satellites, PRN goes from 1 to this number -#define NSATQZS 7 ///< potential number of QZSS satellites, PRN goes from 1 to this number -#define NSATLEO 78 ///< potential number of LEO satellites, PRN goes from 1 to this number -#define NSATBDS 62 ///< potential number of Beidou satellites, PRN goes from 1 to this number -#define NSATSBS 39 ///< potential number of SBAS satellites, PRN goes from 1 to this number +constexpr int NSYSGPS = 1; +constexpr int NSATGPS = 32; ///< potential max number of GPS satellites +constexpr int NSATGLO = 27; ///< potential max number of GLONASS satellites +constexpr int NSATGAL = 36; ///< potential max number of Galileo satellites +constexpr int NSATQZS = 7; ///< potential max number of QZSS satellites +constexpr int NSATLEO = 78; ///< potential max number of LEO satellites +constexpr int NSATBDS = 62; ///< potential max number of Beidou satellites +constexpr int NSATSBS = 39; ///< potential max number of SBAS satellites /** Object holding satellite id, and providing related functions -*/ + */ struct SatSys { - E_Sys sys = E_Sys::NONE; ///< Satellite system - short int prn = 0; ///< PRN for this satellite - - /** Constructor using satellite system and prn - */ - SatSys(E_Sys _sys = E_Sys::NONE, int _prn = 0) - : sys(_sys) - , prn(_prn) - { - - } - - struct SatData - { - string block; - string svn; - }; - - /** Returns the character used as a prefix for this system. - */ - char sysChar() const - { - switch (sys) - { - case E_Sys::GPS: return 'G'; - case E_Sys::GLO: return 'R'; - case E_Sys::GAL: return 'E'; - case E_Sys::QZS: return 'J'; - case E_Sys::BDS: return 'C'; - case E_Sys::LEO: return 'L'; - case E_Sys::IRN: return 'I'; - case E_Sys::SBS: return 'S'; - default: return '-'; - } - } - - void getId(char* str) const; - - /** Returns a unique id for this satellite (for use in hashes) - */ - operator int() const - { - int intval = (sys << 16) - + (prn << 8); - return intval; - } - - static map satDataMap; - - void setBlockType( - string blockType) - { - satDataMap[*this].block = blockType; - } - - void setSvn( - string svn) - { - satDataMap[*this].svn = svn; - } - - string blockType() const - { - return satDataMap[*this].block; - } - - string svn() const - { - return satDataMap[*this].svn; - } - - /** Constructs a SatSys object from it's hash uid - */ - void fromHash(int intval) - { - sys = E_Sys::_from_integral((intval >> 16) & 0xFF); - prn = (intval >> 8) & 0xFF; - } - - /** Returns a std::string of this satellite's id - */ - string id() const - { - char cstring[5]; - - getId(cstring); - - string str = cstring; - - if (str != "-00") - return str; - else - return ""; - } - - /** Returns a string of this satellite's id - */ - operator string() const - { - return id(); - } - - static E_Sys sysFromChar( - char sysChar) - { - switch (sysChar) - { - case 'G': return E_Sys::GPS; - case 'R': return E_Sys::GLO; - case 'E': return E_Sys::GAL; - case 'J': return E_Sys::QZS; - case 'C': return E_Sys::BDS; - case 'L': return E_Sys::LEO; - case 'I': return E_Sys::IRN; - case 'S': return E_Sys::SBS; - default: return E_Sys::NONE; - } - } - - /** Constructs a SatSys object from a c_string id - */ - SatSys(const char* id) - { - char code; - int prn_; - - if (sscanf(id, "%d", &prn_) == 1) - { - prn = prn_; - if (1 <= prn && prn <= NSATGPS) { sys = E_Sys::GPS; return;} - - prn = prn_ - RAW_SBAS_PRN_OFFSET; - if (1 <= prn && prn <= NSATSBS) { sys = E_Sys::SBS; return;} - - prn = prn_ - RAW_QZSS_PRN_OFFSET; - if (1 <= prn && prn <= NSATQZS) { sys = E_Sys::QZS; return;} - - prn = prn_; sys = E_Sys::NONE; return; - } - - int found = sscanf(id, "%c%d", &code, &prn_); - if (found > 0) - { - sys = sysFromChar(code); - } - - if (found > 1) - prn = prn_; - } - - /* Returns a string of this satellite's system id - */ - string sysName() const - { - return sys._to_string(); - } - - - template - void serialize(ARCHIVE& ar, const unsigned int& version) - { - int sysInt = sys; - ar & sysInt; - ar & prn; - try - { - sys = E_Sys::_from_integral(sysInt); - } - catch (...) {} - } + E_Sys sys = E_Sys::NONE; ///< Satellite system + short int prn = 0; ///< PRN for this satellite + + /** Constructor using satellite system and prn + */ + SatSys(E_Sys _sys = E_Sys::NONE, int _prn = 0) : sys(_sys), prn(_prn) {} + + /** Uninitialised sat for comparisons + */ + static SatSys noSat() + { + SatSys nothing; + return nothing; + } + + struct SatData + { + string block; + string svn; + }; + + /** Returns the character used as a prefix for this system. + */ + char sysChar() const + { + switch (sys) + { + case E_Sys::GPS: + return 'G'; + case E_Sys::GLO: + return 'R'; + case E_Sys::GAL: + return 'E'; + case E_Sys::QZS: + return 'J'; + case E_Sys::BDS: + return 'C'; + case E_Sys::LEO: + return 'L'; + case E_Sys::IRN: + return 'I'; + case E_Sys::SBS: + return 'S'; + default: + return '-'; + } + } + + void getId(char* str) const; + + /** Returns a unique id for this satellite (for use in hashes) + */ + operator int() const + { + int intval = (static_cast(sys) << 16) + (prn << 8); + return intval; + } + + static map satDataMap; + + void setBlockType(string blockType) { satDataMap[*this].block = blockType; } + + void setSvn(string svn) { satDataMap[*this].svn = svn; } + + string blockType() const { return satDataMap[*this].block; } + + string svn() const { return satDataMap[*this].svn; } + + /** Constructs a SatSys object from it's hash uid + */ + void fromHash(int intval) + { + sys = static_cast((intval >> 16) & 0xFF); + prn = (intval >> 8) & 0xFF; + } + + /** Returns a std::string of this satellite's id + */ + string id() const + { + char cstring[5]; + + getId(cstring); + + string str = cstring; + + if (str != "-00") + return str; + else + return ""; + } + + /** Returns a string of this satellite's id + */ + operator string() const { return id(); } + + static E_Sys sysFromChar(char sysChar) + { + switch (sysChar) + { + case 'G': + return E_Sys::GPS; + case 'R': + return E_Sys::GLO; + case 'E': + return E_Sys::GAL; + case 'J': + return E_Sys::QZS; + case 'C': + return E_Sys::BDS; + case 'L': + return E_Sys::LEO; + case 'I': + return E_Sys::IRN; + case 'S': + return E_Sys::SBS; + default: + return E_Sys::NONE; + } + } + + /** Constructs a SatSys object from a c_string id + */ + SatSys(const char* id) + { + char code; + int prn_; + + if (sscanf(id, "%d", &prn_) == 1) + { + prn = prn_; + if (1 <= prn && prn <= NSATGPS) + { + sys = E_Sys::GPS; + return; + } + + prn = prn_ - RAW_SBAS_PRN_OFFSET; + if (1 <= prn && prn <= NSATSBS) + { + sys = E_Sys::SBS; + return; + } + + prn = prn_ - RAW_QZSS_PRN_OFFSET; + if (1 <= prn && prn <= NSATQZS) + { + sys = E_Sys::QZS; + return; + } + + prn = prn_; + sys = E_Sys::NONE; + return; + } + + int found = sscanf(id, "%c%d", &code, &prn_); + if (found > 0) + { + sys = sysFromChar(code); + } + + if (found > 1) + prn = prn_; + } + + /* Returns a string of this satellite's system id + */ + string sysName() const { return string(magic_enum::enum_name(sys)); } + + template + void serialize(ARCHIVE& ar, const unsigned int& version) + { + int sysInt = static_cast(sys); + ar & sysInt; + ar & prn; + try + { + sys = static_cast(sysInt); + } + catch (...) + { + } + } }; namespace std { - template<> struct hash - { - size_t operator()(SatSys const& Sat) const - { - size_t hashval = hash {}(Sat); - return hashval; - } - }; -} - -vector getSysSats( - E_Sys targetSys); +template <> +struct hash +{ + size_t operator()(SatSys const& Sat) const + { + size_t hashval = hash{}(Sat); + return hashval; + } +}; +} // namespace std +vector getSysSats(E_Sys targetSys); diff --git a/src/cpp/common/sinex.cpp b/src/cpp/common/sinex.cpp index 43a20772e..855245cbc 100644 --- a/src/cpp/common/sinex.cpp +++ b/src/cpp/common/sinex.cpp @@ -1,41 +1,32 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -/** - */ -FileType SNX__() -{ - -} - -#include -#include -#include +#include "common/sinex.hpp" +#include #include +#include #include +#include +#include +#ifndef _WIN32 #include - -#include - -#include "eigenIncluder.hpp" -#include "navigation.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "gTime.hpp" -#include "sinex.hpp" -#include "trace.hpp" +#endif +#include "architectureDocs.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" using std::getline; using std::ifstream; using std::ofstream; +/** + */ +FileType SNX__() {} - - -Sinex theSinex(false); // the one and only sinex object. - +Sinex theSinex(false); // the one and only sinex object. // 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 @@ -44,1894 +35,1879 @@ Sinex theSinex(false); // the one and only sinex object. // This only applies to site data, for satellites it is using 4 digit years void nearestYear(double& year) { - if (year > 50) year += 1900; - else year += 2000; + if (year > 50) + year += 1900; + else + year += 2000; } /** Trims leading & trailing whitespace */ -string trim( - const string& ref) ///< string to trim +string trim(const string& ref) ///< string to trim { - int start = 0, stop = ref.length() - 1, len; + int start = 0, stop = ref.length() - 1, len; - while (start != stop && isspace(ref[start])) start++; - while (stop != start && isspace(ref[stop])) stop--; + while (start != stop && isspace(ref[start])) + start++; + while (stop != start && isspace(ref[stop])) + stop--; - len = stop - start + 1; + len = stop - start + 1; - return ref.substr(start, len); + return ref.substr(start, len); } /** Cuts string after first space, deletes trailing carriage return */ -void trimCut( - string& line) ///< string to trim -{ - line = line.substr(0, line.find(' ')); - if (line.back() == '\r') - line.pop_back(); -} - -bool compare( - string& one, - string& two) -{ - if (one.compare(two) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexInputFile& one, - SinexInputFile& two) -{ - 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( - SinexSolStatistic& one, - SinexSolStatistic& two) -{ - if (one.name.compare(two.name) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexSatPc& one, - SinexSatPc& two) -{ - if ( one.svn.compare(two.svn) == 0 - &&one.freq == two.freq - &&one.freq2 == two.freq2) - { - return true; - } - return false; -} - -bool compare( - SinexSatEcc& one, - SinexSatEcc& two) -{ - if ( one.svn.compare(two.svn) == 0 - &&one.type == two.type) - { - return true; - } - return false; -} - -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( - 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( - SinexSatId& one, - SinexSatId& 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]) - { - return true; - } - return false; -} - -bool compare( - SinexPreCode& one, - SinexPreCode& two) -{ - if (one.precesscode.compare(two.precesscode) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexSourceId& one, - SinexSourceId& two) -{ - if (one.source.compare(two.source) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexNutCode& one, - SinexNutCode& two) -{ - if (one.nutcode.compare(two.nutcode) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexSatPrn& one, - SinexSatPrn& 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]) - { - return true; - } - return false; -} - -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( - SinexSatCom& one, - SinexSatCom& 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( - SinexAck& one, - SinexAck& two) -{ - if ( one.agency .compare(two.agency) == 0 - &&one.description .compare(two.description) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexInputHistory& one, - SinexInputHistory& 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) - { - return true; - } - return false; -} - -bool compare( - SinexSiteId& one, - SinexSiteId& two) -{ - if (one.sitecode.compare(two.sitecode) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexSiteData& one, - SinexSiteData& two) -{ - if ( one.site. compare(two.site) == 0 - &&one.sitecode. compare(two.sitecode) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexReceiver& one, - SinexReceiver& 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 compare( - SinexAntenna& one, - SinexAntenna& 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 compare( - SinexGpsPhaseCenter& one, - SinexGpsPhaseCenter& two) -{ - if ( one.antname .compare(two.antname) == 0 - &&one.serialno .compare(two.serialno) == 0) - { - return true; - } - return false; -} - -bool compare( - SinexGalPhaseCenter& one, - SinexGalPhaseCenter& two) -{ - if ( one.antname .compare(two.antname) == 0 - &&one.serialno .compare(two.serialno) == 0) - { - return true; - } - return false; -} - -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] - &&one.end[1] == two.end[1] - &&one.end[2] == two.end[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 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) +void trimCut(string& line) ///< string to trim { - list copy; - - for (auto it = source.begin(); it != source.end(); ) - { - bool found = false; - - for (auto it2 = copy.begin(); it2 != copy.end(); it2++) - { - if (compare(*it, *it2)) - { - found = true; - break; - } - } - - if (found) - { - it = source.erase(it); - } - else - { - copy.push_back(*it); - it++; - } - } -} - -template -void dedupeB(list& source) + line = line.substr(0, line.find(' ')); + if (line.back() == '\r') + line.pop_back(); +} + +bool compare(string& one, string& two) { - TYPE previous; - bool first = true; - - for (auto it = source.begin(); it != source.end(); ) - { - bool found = false; - - if (!first) - { - if (compare(*it, previous)) - { - found = true; - } - } - - if (found) - { - it = source.erase(it); - } - else - { - previous = *it; - it++; - first = false; - } - } -} - -// each of the lists is parsed for duplicates. When a dup is found it is erased. At the end of each loop the _copy list should contain the same stuff -// as the original -void dedupeSinex() + if (one.compare(two) == 0) + { + return true; + } + return false; +} + +bool compare(SinexInputFile& one, SinexInputFile& two) { - // do the lists which are not sorted first - - // general stuff - dedupe(theSinex.refstrings); - dedupe(theSinex.inputHistory); - dedupe(theSinex.inputFiles); - dedupe(theSinex.acknowledgements); - dedupe(theSinex.listnutcodes); - dedupe(theSinex.listprecessions); - dedupe(theSinex.listsourceids); - dedupe(theSinex.listsatids); - 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 - // site stuff - // all data is sorted before coming in here, so it suffices to just check against the previous value - dedupeB(theSinex.listsitedata); - dedupeB(theSinex.listgpspcs); - dedupeB(theSinex.listgalpcs); - dedupeB(theSinex.listnormaleqns); - - for (matrix_type t = ESTIMATE; t < MAX_MATRIX_TYPE; t = static_cast (static_cast(t) + 1)) - for (matrix_value v = CORRELATION; v < MAX_MATRIX_VALUE; v = static_cast (static_cast(v) + 1)) - { - if (theSinex.matrixmap[t][v].empty()) - continue; - - dedupeB(theSinex.matrixmap[t][v]); - } - - return; + 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; } -// TODO; What if we are reading a second file. What wins? -bool readSnxHeader(std::ifstream& in) +bool compare(SinexSolStatistic& one, SinexSolStatistic& two) { - string line; - - std::getline(in, line); - - if (in.eof()) - { - BOOST_LOG_TRIVIAL(error) << "Error: empty file"; - return false; - } - - // verify line contents - if ( line[0] != '%' - || line[1] != '=' - || line[2] != 'S' - || line[3] != 'N' - || line[4] != 'X') - { - // error. not a sinex file - BOOST_LOG_TRIVIAL(error) << "Error: Not a sinex file"; - return false; - } - - // remaining characters indiciate properties of the file - if (line.length() > 5) - { - const char* buff = line.c_str(); - char create_agc[4]; - char data_agc[4]; - char solcontents[7]; - - int 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", - &theSinex.ver, - create_agc, - &theSinex.filedate[0], - &theSinex.filedate[1], - &theSinex.filedate[2], - data_agc, - &theSinex.solutionstartdate[0], - &theSinex.solutionstartdate[1], - &theSinex.solutionstartdate[2], - &theSinex.solutionenddate[0], - &theSinex.solutionenddate[1], - &theSinex.solutionenddate[2], - &theSinex.obsCode, - &theSinex.numparam, - &theSinex.constCode, - &solcontents[0], - &solcontents[1], - &solcontents[2], - &solcontents[3], - &solcontents[4], - &solcontents[5]); - - if (readcount < 15) - { - // error, not enough parameters - BOOST_LOG_TRIVIAL(error) << "Error: Not enough parameters on header line (expected min 15), got " << readcount; - return false; - } - - while (readcount < 21) - { - solcontents[readcount - 15] = ' '; - readcount++; - } - - solcontents[6] = '\0'; - - theSinex.createagc = create_agc; - theSinex.dataagc = data_agc; - theSinex.solcont = solcontents; - - nearestYear(theSinex.filedate[0]); - nearestYear(theSinex.solutionstartdate[0]); - nearestYear(theSinex.solutionenddate[0]); - } - - return true; + if (one.name.compare(two.name) == 0) + { + return true; + } + return 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) +bool compare(SinexSatPc& one, SinexSatPc& two) { - SinexInputHistory siht; + if (one.svn.compare(two.svn) == 0 && one.freq == two.freq && one.freq2 == two.freq2) + { + return true; + } + return false; +} - 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; +bool compare(SinexSatEcc& one, SinexSatEcc& two) +{ + if (one.svn.compare(two.svn) == 0 && one.type == two.type) + { + return true; + } + return false; +} - if (theSinex.inputHistory.empty()) - theSinex.inputHistory.push_back(siht); +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; +} - theSinex.ver = sinexVer; +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; +} - if (data_agc.size() > 0) theSinex.dataagc = data_agc; - else theSinex.dataagc = theSinex.createagc; +bool compare(SinexSatId& one, SinexSatId& 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]) + { + return true; + } + return false; +} - theSinex.createagc = create_agc; - theSinex.solcont = contents; - theSinex.filedate = timeGet(); - theSinex.solutionstartdate = soln_start; - theSinex.solutionenddate = soln_end; +bool compare(SinexPreCode& one, SinexPreCode& two) +{ + if (one.precesscode.compare(two.precesscode) == 0) + { + return true; + } + return false; +} - if (obsCode != ' ') - theSinex.obsCode = obsCode; +bool compare(SinexSourceId& one, SinexSourceId& two) +{ + if (one.source.compare(two.source) == 0) + { + return true; + } + return false; +} - if (constCode != ' ') - theSinex.constCode = constCode; +bool compare(SinexNutCode& one, SinexNutCode& two) +{ + if (one.nutcode.compare(two.nutcode) == 0) + { + return true; + } + return false; +} - theSinex.numparam = numParam; +bool compare(SinexSatPrn& one, SinexSatPrn& 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]) + { + return true; + } + return false; } -void writeSnxHeader(std::ofstream& out) +bool compare(SinexSatPower& one, SinexSatPower& two) { - char line[81]; - char c; - int i; + 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 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); +bool compare(SinexSatCom& one, SinexSatCom& 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; +} - i = 0; - c = theSinex.solcont[0]; +bool compare(SinexAck& one, SinexAck& two) +{ + if (one.agency.compare(two.agency) == 0 && one.description.compare(two.description) == 0) + { + return true; + } + return false; +} - while (c != ' ') - { - snprintf(line + offset, sizeof(line) - offset, " %c", c); +bool compare(SinexInputHistory& one, SinexInputHistory& 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) + { + return true; + } + return false; +} - i++; +bool compare(SinexSiteId& one, SinexSiteId& two) +{ + if (one.sitecode.compare(two.sitecode) == 0) + { + return true; + } + return false; +} - if (i <= theSinex.solcont.length()) c = theSinex.solcont[i]; - else c = ' '; - } +bool compare(SinexSiteData& one, SinexSiteData& two) +{ + if (one.site.compare(two.site) == 0 && one.sitecode.compare(two.sitecode) == 0) + { + return true; + } + return false; +} - out << line << "\n"; +bool compare(SinexReceiver& one, SinexReceiver& 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 compare(SinexAntenna& one, SinexAntenna& 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 compare(SinexGpsPhaseCenter& one, SinexGpsPhaseCenter& two) +{ + if (one.antname.compare(two.antname) == 0 && one.serialno.compare(two.serialno) == 0) + { + return true; + } + return false; +} + +bool compare(SinexGalPhaseCenter& one, SinexGalPhaseCenter& two) +{ + if (one.antname.compare(two.antname) == 0 && one.serialno.compare(two.serialno) == 0) + { + return true; + } + return false; +} + +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] && + one.end[1] == two.end[1] && one.end[2] == two.end[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 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++) + { + if (compare(*it, *it2)) + { + found = true; + break; + } + } + + if (found) + { + it = source.erase(it); + } + else + { + copy.push_back(*it); + it++; + } + } +} + +template +void dedupeB(list& source) +{ + TYPE previous; + bool first = true; + + for (auto it = source.begin(); it != source.end();) + { + bool found = false; + + if (!first) + { + if (compare(*it, previous)) + { + found = true; + } + } + + if (found) + { + it = source.erase(it); + } + else + { + previous = *it; + it++; + first = false; + } + } +} + +// each of the lists is parsed for duplicates. When a dup is found it is erased. At the end of each +// loop the _copy list should contain the same stuff as the original +void dedupeSinex() +{ + // do the lists which are not sorted first + + // general stuff + dedupe(theSinex.refstrings); + dedupe(theSinex.inputHistory); + dedupe(theSinex.inputFiles); + dedupe(theSinex.acknowledgements); + dedupe(theSinex.listnutcodes); + dedupe(theSinex.listprecessions); + dedupe(theSinex.listsourceids); + dedupe(theSinex.listsatids); + 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 + // site stuff + // all data is sorted before coming in here, so it suffices to just check against the previous + // value + dedupeB(theSinex.listsitedata); + dedupeB(theSinex.listgpspcs); + dedupeB(theSinex.listgalpcs); + dedupeB(theSinex.listnormaleqns); + + for (matrix_type t = ESTIMATE; t < MAX_MATRIX_TYPE; + t = static_cast(static_cast(t) + 1)) + for (matrix_value v = CORRELATION; v < MAX_MATRIX_VALUE; + v = static_cast(static_cast(v) + 1)) + { + if (theSinex.matrixmap[t][v].empty()) + continue; + + dedupeB(theSinex.matrixmap[t][v]); + } + + return; +} + +// TODO; What if we are reading a second file. What wins? +bool readSnxHeader(std::ifstream& in) +{ + string line; + + std::getline(in, line); + + if (in.eof()) + { + BOOST_LOG_TRIVIAL(error) << "Empty file"; + return false; + } + + // verify line contents + if (line[0] != '%' || line[1] != '=' || line[2] != 'S' || line[3] != 'N' || line[4] != 'X') + { + // error. not a sinex file + BOOST_LOG_TRIVIAL(error) << "Not a sinex file"; + return false; + } + + // remaining characters indiciate properties of the file + if (line.length() > 5) + { + const char* buff = line.c_str(); + char create_agc[4]; + char data_agc[4]; + char solcontents[7]; + + int 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", + &theSinex.ver, + create_agc, + &theSinex.filedate[0], + &theSinex.filedate[1], + &theSinex.filedate[2], + data_agc, + &theSinex.solutionstartdate[0], + &theSinex.solutionstartdate[1], + &theSinex.solutionstartdate[2], + &theSinex.solutionenddate[0], + &theSinex.solutionenddate[1], + &theSinex.solutionenddate[2], + &theSinex.obsCode, + &theSinex.numparam, + &theSinex.constCode, + &solcontents[0], + &solcontents[1], + &solcontents[2], + &solcontents[3], + &solcontents[4], + &solcontents[5] + ); + + if (readcount < 15) + { + // error, not enough parameters + BOOST_LOG_TRIVIAL(error) + << "Not enough parameters on header line (expected min 15), got " << readcount; + return false; + } + + while (readcount < 21) + { + solcontents[readcount - 15] = ' '; + readcount++; + } + + solcontents[6] = '\0'; + + theSinex.createagc = create_agc; + theSinex.dataagc = data_agc; + theSinex.solcont = solcontents; + + nearestYear(theSinex.filedate[0]); + nearestYear(theSinex.solutionstartdate[0]); + nearestYear(theSinex.solutionenddate[0]); + } + + 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 +) +{ + 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 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 %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 + ); + + 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 parseReference(string& line) { - theSinex.refstrings.push_back(line); + theSinex.refstrings.push_back(line); } -void writeAsComments( - Trace& out, - list& comments) +void writeAsComments(Trace& out, list& comments) { - for (auto& comment : comments) - { - string line = comment; + for (auto& comment : comments) + { + string line = comment; - // just make sure it starts with * as required by format - line[0] = '*'; + // just make sure it starts with * as required by format + line[0] = '*'; - out << line << "\n"; - } + out << line << "\n"; + } } void commentsOverride() { - // 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 + // 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 } void writeSnxReference(ofstream& out) { - Block block(out, "FILE/REFERENCE"); + Block block(out, "FILE/REFERENCE"); - for (auto& refString : theSinex.refstrings) - { - out << refString << "\n"; - } + for (auto& refString : theSinex.refstrings) + { + out << refString << "\n"; + } } void writeSnxComments(ofstream& out) { - Block block(out, "FILE/COMMENT"); + Block block(out, "FILE/COMMENT"); - for (auto& commentstring : theSinex.blockComments[block.blockName]) - { - out << commentstring << "\n"; - } + for (auto& commentstring : theSinex.blockComments[block.blockName]) + { + out << commentstring << "\n"; + } } void parseInputHistory(string& line) { - SinexInputHistory siht; - // remaining characters indiciate properties of the history - - if (line.length() > 5) - { - const char* buff = line.c_str(); - char create_agc[4]; - char data_agc[4]; - char solcontents[7]; - int readcount; - - siht.code = line[1]; - - 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 (readcount >= 15) - { - while (readcount < 21) - { - solcontents[readcount - 15] = ' '; - readcount++; - } - - solcontents[6] = '\0'; - - siht.create_agency = create_agc; - siht.data_agency = data_agc; - siht.contents = solcontents; - - nearestYear(siht.create_time[0]); - nearestYear(siht.start[0]); - nearestYear(siht.stop[0]); - - theSinex.inputHistory.push_back(siht); - } - } + SinexInputHistory siht; + // remaining characters indiciate properties of the history + + if (line.length() > 5) + { + const char* buff = line.c_str(); + char create_agc[4]; + char data_agc[4]; + char solcontents[7]; + int readcount; + + siht.code = line[1]; + + 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 (readcount >= 15) + { + while (readcount < 21) + { + solcontents[readcount - 15] = ' '; + readcount++; + } + + solcontents[6] = '\0'; + + siht.create_agency = create_agc; + siht.data_agency = data_agc; + siht.contents = solcontents; + + nearestYear(siht.create_time[0]); + nearestYear(siht.start[0]); + nearestYear(siht.stop[0]); + + theSinex.inputHistory.push_back(siht); + } + } } void writeSnxInputHistory(ofstream& out) { - Block block(out, "INPUT/HISTORY"); - - 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 %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); - - 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"; - } + Block block(out, "INPUT/HISTORY"); + + 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 %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 + ); + + 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 parseInputFiles(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); + SinexInputFile sif; + char agency[4]; + const char* buff = line.c_str(); + sif.file = line.substr(18, 29); + sif.description = line.substr(48, 32); - int readcount = sscanf(buff + 1, "%3s %2lf:%3lf:%5lf", - agency, - &sif.yds[0], - &sif.yds[1], - &sif.yds[2]); + int readcount = + sscanf(buff + 1, "%3s %2lf:%3lf:%5lf", agency, &sif.yds[0], &sif.yds[1], &sif.yds[2]); - if (readcount == 4) - { - sif.agency = agency; + if (readcount == 4) + { + sif.agency = agency; - nearestYear(sif.yds[0]); + nearestYear(sif.yds[0]); - theSinex.inputFiles.push_back(sif); - } + theSinex.inputFiles.push_back(sif); + } } void writeSnxInputFiles(ofstream& out) { - Block block(out, "INPUT/FILES"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& inputFile : theSinex.inputFiles) - { - SinexInputFile& sif = inputFile; - - 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]); - - // 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"; - } + Block block(out, "INPUT/FILES"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& inputFile : theSinex.inputFiles) + { + SinexInputFile& sif = inputFile; + + 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] + ); + + // 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"; + } } void parseAcknowledgements(string& line) { - SinexAck sat; + SinexAck sat; - sat.description = line.substr(5); - sat.agency = line.substr(1, 3); + sat.description = line.substr(5); + sat.agency = line.substr(1, 3); - theSinex.acknowledgements.push_back(sat); + theSinex.acknowledgements.push_back(sat); } void writeSnxAcknowledgements(ofstream& out) { - Block block(out, "INPUT/ACKNOWLEDGEMENTS"); + Block block(out, "INPUT/ACKNOWLEDGEMENTS"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& acknowledgement : theSinex.acknowledgements) - { - SinexAck& ack = acknowledgement; + for (auto& acknowledgement : theSinex.acknowledgements) + { + SinexAck& ack = acknowledgement; - char line[81]; - snprintf(line, sizeof(line), " %3s %s", ack.agency.c_str(), ack.description.c_str()); + char line[81]; + snprintf(line, sizeof(line), " %3s %s", ack.agency.c_str(), ack.description.c_str()); - out << line << "\n"; - } + out << line << "\n"; + } } void parseSiteIds(string& line) { - 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); - - if (readcount == 7) - { - theSinex.mapsiteids[sst.sitecode] = sst; - } + 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 + ); + + if (readcount == 7) + { + theSinex.mapsiteids[sst.sitecode] = sst; + } } void writeSnxSiteids(ofstream& out) { - Block block(out, "SITE/ID"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [id, ssi] : theSinex.mapsiteids) - { - 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); - } + Block block(out, "SITE/ID"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [id, ssi] : theSinex.mapsiteids) + { + 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 + ); + } } // compare by the 2 station ids only. -bool compareSiteData( - const SinexSiteData& left, - const SinexSiteData& right) +bool compareSiteData(const SinexSiteData& left, const SinexSiteData& right) { - int comp = left.site.compare(right.site); + int comp = left.site.compare(right.site); - if (comp == 0) - comp = left.sitecode.compare(right.sitecode); + if (comp == 0) + comp = left.sitecode.compare(right.sitecode); - return (comp < 0); + return (comp < 0); } void parseSiteData(string& line) { - const char* buff = line.c_str(); - - SinexSiteData 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.obscode = line[24]; - UYds start; - UYds end; - UYds create; - char agency[4]; - - int readcount; - - 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]); - - if (readcount == 10) - { - sst.agency = agency; - sst.start = start; - sst.stop = end; - sst.create = create; - - // 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.stop[0] != 0 - || sst.stop[1] != 0 - || sst.stop[2] != 0) - { - nearestYear(sst.stop[0]); - } - - nearestYear(sst.create[0]); - - theSinex.listsitedata.push_back(sst); - } + const char* buff = line.c_str(); + + SinexSiteData 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.obscode = line[24]; + UYds start; + UYds end; + UYds create; + char agency[4]; + + int readcount; + + 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] + ); + + if (readcount == 10) + { + sst.agency = agency; + sst.start = start; + sst.stop = end; + sst.create = create; + + // 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.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 writeSnxSitedata(ofstream& out, list* pstns) { - Block block(out, "SITE/DATA"); - - 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 %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]); - - if (pstns == nullptr) - doit = true; - else - { - for (auto& stn : *pstns) - { - if (ssd.site.compare(stn.id_ptr->sitecode) == 0) - { - doit = true; - break; - } - } - } - - if (doit) - out << line << "\n"; - } + Block block(out, "SITE/DATA"); + + 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 %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] + ); + + if (pstns == nullptr) + doit = true; + else + { + for (auto& stn : *pstns) + { + if (ssd.site.compare(stn.id_ptr->sitecode) == 0) + { + doit = true; + break; + } + } + } + + if (doit) + out << line << "\n"; + } } void parseReceivers(string& line) { - const char* buff = line.c_str(); - - SinexReceiver srt; - - 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; - - 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]); - - if (readcount == 6) - { - // see comment at top of file - if ( srt.start[0] != 0 - || srt.start[1] != 0 - || srt.start[2] != 0) - { - nearestYear(srt.start[0]); - } - - if ( srt.end[0] != 0 - || srt.end[1] != 0 - || srt.end[2] != 0) - { - nearestYear(srt.end[0]); - } - - theSinex.mapreceivers[srt.sitecode][srt.start] = srt; - } + const char* buff = line.c_str(); + + SinexReceiver srt; + + 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; + + 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] + ); + + if (readcount == 6) + { + // see comment at top of file + if (srt.start[0] != 0 || srt.start[1] != 0 || srt.start[2] != 0) + { + nearestYear(srt.start[0]); + } + + if (srt.end[0] != 0 || srt.end[1] != 0 || srt.end[2] != 0) + { + nearestYear(srt.end[0]); + } + + theSinex.mapreceivers[srt.sitecode][srt.start] = srt; + } } void writeSnxReceivers(ofstream& out) { - Block block(out, "SITE/RECEIVER"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [site, timemap] : theSinex.mapreceivers) - for (auto it = timemap.rbegin(); it != timemap.rend(); it++) - { - auto& [time, receiver] = *it; - - if (receiver.used == false) - { - continue; - } - - 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()); - } + Block block(out, "SITE/RECEIVER"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [site, timemap] : theSinex.mapreceivers) + for (auto it = timemap.rbegin(); it != timemap.rend(); it++) + { + auto& [time, receiver] = *it; + + if (receiver.used == false) + { + continue; + } + + 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() + ); + } } void parseAntennas(string& line) { - const char* buff = line.c_str(); - - SinexAntenna ant; - - 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)); - - 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) - { - // 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); - } + const char* buff = line.c_str(); + + SinexAntenna ant; + + 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)); + + 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) + { + // 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); + } } void writeSnxAntennas(ofstream& out) { - Block block(out, "SITE/ANTENNA"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [site, antmap] : theSinex.mapantennas) - for (auto it = antmap.rbegin(); it != antmap.rend(); it++) - { - auto& [time, ant] = *it; - - if (ant.used == false) - { - continue; - } - - 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()); - } + Block block(out, "SITE/ANTENNA"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [site, antmap] : theSinex.mapantennas) + for (auto it = antmap.rbegin(); it != antmap.rend(); it++) + { + auto& [time, ant] = *it; + + if (ant.used == false) + { + continue; + } + + 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() + ); + } } // compare by antenna type and serial number. -bool compareGpsPc( - SinexGpsPhaseCenter& left, - SinexGpsPhaseCenter& right) +bool compareGpsPc(SinexGpsPhaseCenter& left, SinexGpsPhaseCenter& right) { - int comp = left.antname.compare(right.antname); + int comp = left.antname.compare(right.antname); - if (comp == 0) - comp = left.serialno.compare(right.serialno); + if (comp == 0) + comp = left.serialno.compare(right.serialno); - return (comp < 0); + return (comp < 0); } void parseGpsPhaseCenters(string& line) { - const char* buff = line.c_str(); - SinexGpsPhaseCenter sgpct; + const char* buff = line.c_str(); + SinexGpsPhaseCenter sgpct; - sgpct.antname = line.substr(1, 20); - sgpct.serialno = line.substr(22, 5); - sgpct.calib = line.substr(70, 10); + sgpct.antname = line.substr(1, 20); + sgpct.serialno = line.substr(22, 5); + sgpct.calib = line.substr(70, 10); - 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]); + 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 (readcount == 6) - { - theSinex.listgpspcs.push_back(sgpct); - } + if (readcount == 6) + { + theSinex.listgpspcs.push_back(sgpct); + } } void truncateSomething(char* buf) { - if ( strlen(buf) == 7 - && buf[1] == '0' - && buf[0] == '-') - { - for (int j = 2; j < 8; j++) - { - buf[j - 1] = buf[j]; - } - } + if (strlen(buf) == 7 && buf[1] == '0' && buf[0] == '-') + { + for (int j = 2; j < 8; j++) + { + buf[j - 1] = buf[j]; + } + } } - void writeSnxGpsPcs(ofstream& out, list* pstns) { - Block block(out, "SITE/GPS_PHASE_CENTER"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& gps_pc : theSinex.listgpspcs) - { - SinexGpsPhaseCenter& sgt = gps_pc; - char buf[8]; - bool doit = false; - - 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.L1[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.L2[i]); - truncateSomething(buf); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); - } - - offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); - - 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"; - } - } + Block block(out, "SITE/GPS_PHASE_CENTER"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& gps_pc : theSinex.listgpspcs) + { + SinexGpsPhaseCenter& sgt = gps_pc; + char buf[8]; + bool doit = false; + + 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.L1[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.L2[i]); + truncateSomething(buf); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); + } + + offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); + + 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"; + } + } } // compare by antenna type and serial number. return true0 if left < right -bool compareGalPc( - SinexGalPhaseCenter& left, - SinexGalPhaseCenter& right) +bool compareGalPc(SinexGalPhaseCenter& left, SinexGalPhaseCenter& right) { - int comp = left.antname.compare(right.antname); + int comp = left.antname.compare(right.antname); - if (comp == 0) - comp = left.serialno.compare(right.serialno); + if (comp == 0) + comp = left.serialno.compare(right.serialno); - return (comp < 0); + 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; - - lineNum++; - if (lineNum != 3) - { - //wait for 3 lines. - return; - } - - 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); - - 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]); - - // 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); - } + static int lineNum = 0; + static string lines[3]; + lines[lineNum] = s_x; + + lineNum++; + if (lineNum != 3) + { + // wait for 3 lines. + return; + } + + 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); + + 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] + ); + + // 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 writeSnxGalPcs(ofstream& out, list* pstns) { - 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; - - if (pstns == nullptr) - doit = true; - else - { - for (auto& stn : *pstns) - { - if (sgt.antname == stn.ant_ptr->type) - { - doit = true; - break; - } - } - } - - if (!doit) - continue; - - { - 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.L1[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.L5[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"; - } - - { - 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"; - } - - { - 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"; - } - } + 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; + + if (pstns == nullptr) + doit = true; + else + { + for (auto& stn : *pstns) + { + if (sgt.antname == stn.ant_ptr->type) + { + doit = true; + break; + } + } + } + + if (!doit) + continue; + + { + 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.L1[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.L5[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"; + } + + { + 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"; + } + + { + 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"; + } + } } void parseSiteEccentricity(string& line) { - const char* buff = line.c_str(); - SinexSiteEcc sset; - - 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 + 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 == 10) - { - // 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; - } + const char* buff = line.c_str(); + SinexSiteEcc sset; + + 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 + 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 == 10) + { + // 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 writeSnxSiteEccs(ofstream& out) { - Block block(out, "SITE/ECCENTRICITY"); + Block block(out, "SITE/ECCENTRICITY"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& [id, setMap] : theSinex.mapeccentricities) - for (auto it = setMap.rbegin(); it != setMap.rend(); it++) - { - auto& [time, set] = *it; + for (auto& [id, setMap] : theSinex.mapeccentricities) + for (auto it = setMap.rbegin(); it != setMap.rend(); it++) + { + auto& [time, set] = *it; - if (set.used == false) - { - continue; - } + if (set.used == false) + { + continue; + } - 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()); - } + 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() + ); + } } -bool compareSiteEpochs( - SinexSolEpoch& left, - SinexSolEpoch& right) +bool compareSiteEpochs(SinexSolEpoch& left, SinexSolEpoch& right) { - int comp = left.sitecode.compare(right.sitecode); - int i = 0; + int comp = left.sitecode.compare(right.sitecode); + int i = 0; - while (comp == 0 && i < 3) - { - comp = left.start[i] - right.start[i]; - i++; - } + while (comp == 0 && i < 3) + { + comp = left.start[i] - right.start[i]; + i++; + } - return (comp < 0); + return (comp < 0); } void parseEpochs(string& line) { - 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]; - - 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 (readcount == 9) - { - // 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) - { - nearestYear(sst.end[0]); - } - - if ( sst.mean[0] != 0 - || sst.mean[1] != 0 - || sst.mean[2] != 0) - { - nearestYear(sst.mean[0]); - } - } -} - -void writeSnxEpochs( - Trace& out) -{ - string blockName; - if (theSinex.epochshavebias) blockName = "BIAS/EPOCHS"; - else blockName = "SOLUTION/EPOCHS"; - - Block block(out, blockName); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - 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]); - } -} - -void parseStatistics(string& line) //todo aaron, is this type stuff really necessary -{ - const char* buff = line.c_str(); - - string stat = line.substr(1, 30); - double dval; - int ival; - short etype; - - if (line.substr(33).find(".") != string::npos) - { - dval = (double)atof(buff + 33); - etype = 1; - } - else - { - ival = atoi(buff + 33); - etype = 0; - } - - SinexSolStatistic sst; - sst.name = trim(stat); - sst.etype = etype; - - if (etype == 0) - sst.value.ival = ival; - - if (etype == 1) - sst.value.dval = dval; - - theSinex.liststatistics.push_back(sst); + 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]; + + 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 (readcount == 9) + { + // 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) + { + nearestYear(sst.end[0]); + } + + if (sst.mean[0] != 0 || sst.mean[1] != 0 || sst.mean[2] != 0) + { + nearestYear(sst.mean[0]); + } + } +} + +void writeSnxEpochs(Trace& out) +{ + string blockName; + if (theSinex.epochshavebias) + blockName = "BIAS/EPOCHS"; + else + blockName = "SOLUTION/EPOCHS"; + + Block block(out, blockName); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + 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] + ); + } +} + +void parseStatistics(string& line) // todo aaron, is this type stuff really necessary +{ + const char* buff = line.c_str(); + + string stat = line.substr(1, 30); + double dval; + int ival; + short etype; + + if (line.substr(33).find(".") != string::npos) + { + dval = (double)atof(buff + 33); + etype = 1; + } + else + { + ival = atoi(buff + 33); + etype = 0; + } + + SinexSolStatistic sst; + sst.name = trim(stat); + sst.etype = etype; + + if (etype == 0) + sst.value.ival = ival; + + if (etype == 1) + sst.value.dval = dval; + + theSinex.liststatistics.push_back(sst); } void writeSnxStatistics(ofstream& out) { - Block block(out, "SOLUTION/STATISTICS"); + Block block(out, "SOLUTION/STATISTICS"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& statistic : theSinex.liststatistics) - { - char line[81]; + for (auto& statistic : theSinex.liststatistics) + { + char line[81]; - if (statistic.etype == 0) // int - snprintf(line, sizeof(line), " %-30s %22d", statistic.name.c_str(), statistic.value.ival); - - if (statistic.etype == 1) // double - snprintf(line, sizeof(line), " %-30s %22.15lf", statistic.name.c_str(), statistic.value.dval); - - out << line << "\n"; - } -} - - -void parseSolutionEstimates( - string& line) -{ - const char* buff = line.c_str(); - - SinexSolEstimate sst; - - 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); - - sst.index = (int)str2num(buff, 1, 5); - - int readcount = sscanf(buff + 27, "%2lf:%3lf:%5lf", - &sst.refepoch[0], - &sst.refepoch[1], - &sst.refepoch[2]); - - sst.unit = line.substr(40, 4); - - sst.constraint = line[45]; - - 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) - { - nearestYear(sst.refepoch[0]); - } - - auto it = theSinex.estimatesMap.find(sst.sitecode); - if (it != theSinex.estimatesMap.end()) - { - auto& firstEntry = it->second.begin()->second.begin()->second; - - if (firstEntry.file != sst.file) - { - 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(); - } - } - 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) - { - 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 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))); - } + if (statistic.etype == 0) // int + snprintf( + line, + sizeof(line), + " %-30s %22d", + statistic.name.c_str(), + statistic.value.ival + ); + + if (statistic.etype == 1) // double + snprintf( + line, + sizeof(line), + " %-30s %22.15lf", + statistic.name.c_str(), + statistic.value.dval + ); + + out << line << "\n"; + } +} + +void parseSolutionEstimates(string& line) +{ + const char* buff = line.c_str(); + + SinexSolEstimate sst; + + 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); + + sst.index = (int)str2num(buff, 1, 5); + + int readcount = + sscanf(buff + 27, "%2lf:%3lf:%5lf", &sst.refepoch[0], &sst.refepoch[1], &sst.refepoch[2]); + + sst.unit = line.substr(40, 4); + + sst.constraint = line[45]; + + 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) + { + nearestYear(sst.refepoch[0]); + } + + auto it = theSinex.estimatesMap.find(sst.sitecode); + if (it != theSinex.estimatesMap.end()) + { + auto& firstEntry = it->second.begin()->second.begin()->second; + + if (firstEntry.file != sst.file) + { + 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(); + } + } + 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) + { + 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 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( @@ -1963,19 +1939,11 @@ void writeSnxEstimatesFromFilter( // // 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); +// 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"; // } @@ -1983,1953 +1951,2172 @@ void writeSnxEstimatesFromFilter( // out << "-SOLUTION/ESTIMATE" << "\n"; // } - void parseApriori(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSolApriori sst = {}; + 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); + 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]; + char unit[5]; - unit[4] = '\0'; + 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); + 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; + 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) - { - nearestYear(sst.epoch[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.apriorimap[sst.idx] = sst; - } + theSinex.apriorimap[sst.idx] = sst; + } } void writeSnxApriori(ofstream& out, list* pstns = nullptr) { - Block block(out, "SOLUTION/APRIORI"); - - 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) - { - if (sst.sitecode.compare(stn.id_ptr->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.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.aprioriVar(i)); - - index++; - } - } + Block block(out, "SOLUTION/APRIORI"); + + 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) + { + if (sst.sitecode.compare(stn.id_ptr->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.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++; + } + } } void parseNormals(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSolNeq sst; + SinexSolNeq sst; - 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]; + 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]; - unit[4] = '\0'; + unit[4] = '\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); + 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 + ); - if (readcount == 6) - { - sst.unit = unit; + if (readcount == 6) + { + sst.unit = unit; - // see comment at top of file - if (sst.epoch[0] != 0 || sst.epoch[1] != 0 || sst.epoch[2] != 0) - { - nearestYear(sst.epoch[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); - } + theSinex.listnormaleqns.push_back(sst); + } } void writeSnxNormal(ofstream& out, list* pstns = nullptr) { - Block block(out, "SOLUTION/NORMAL_EQUATION_VECTOR"); + Block block(out, "SOLUTION/NORMAL_EQUATION_VECTOR"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& sst : theSinex.listnormaleqns) - { - bool doit = (pstns == nullptr); + 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; - } - } + if (pstns) + for (auto& stn : *pstns) + { + if (sst.site.compare(stn.id_ptr->sitecode) != 0) + { + doit = true; + break; + } + } - if (!doit) - continue; + if (!doit) + continue; - char line[81]; + 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); + 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"; - } + out << line << "\n"; + } } -matrix_type mat_type; -matrix_value mat_value; +matrix_type mat_type; +matrix_value mat_value; -void parseMatrix(string& line)//, matrix_type type, matrix_value value) +void parseMatrix(string& line) //, matrix_type type, matrix_value value) { - 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; - - int readcount = sscanf(buff, " %5d %5d %21lf %21lf %21lf", - &smt.row, - &smt.col, - &smt.value[0], - &smt.value[1], - &smt.value[2]); + const char* buff = line.c_str(); - if (readcount > 2) - { - if (smt.row < smt.col) - { - //xor swap - smt.row ^= smt.col; - smt.col ^= smt.row; - smt.row ^= smt.col; - } + // //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; - int covars = readcount - 2; + int readcount = sscanf( + buff, + " %5d %5d %21lf %21lf %21lf", + &smt.row, + &smt.col, + &smt.value[0], + &smt.value[1], + &smt.value[2] + ); - for (int i = readcount - 2; i < 3; i++) - smt.value[i] = -1; + if (readcount > 2) + { + if (smt.row < smt.col) + { + // xor swap + smt.row ^= smt.col; + smt.col ^= smt.row; + smt.row ^= smt.col; + } - smt.numvals = readcount - 2; + int covars = readcount - 2; - if (smt.row > maxrow) maxrow = smt.row; - if (smt.col > maxcol) maxcol = smt.col; + for (int i = readcount - 2; i < 3; i++) + smt.value[i] = -1; - theSinex.matrixmap[mat_type][mat_value].push_back(smt); - } -} + smt.numvals = readcount - 2; -void parseSinexEstimates( - string& line) -{ + if (smt.row > maxrow) + maxrow = smt.row; + if (smt.col > maxcol) + maxcol = smt.col; + theSinex.matrixmap[mat_type][mat_value].push_back(smt); + } } -void parseSinexEstimateMatrix( - string& line) -{ +void parseSinexEstimates(string& line) {} -} +void parseSinexEstimateMatrix(string& line) {} -void writeSnxMatricesFromFilter( - ofstream& out, - KFState& kfState) +void writeSnxMatricesFromFilter(ofstream& out, KFState& kfState) { - const char* type_strings [MAX_MATRIX_TYPE]; - const char* value_strings [MAX_MATRIX_VALUE]; + 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"; + 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"; // 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); - - 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"); - } - } + 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); + + 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 parseDataHandling(string& line) { - 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 - - 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); - } - - 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]); - } - - GTime time = sdt.epochstart; - - theSinex.mapdatahandling[sdt.sitecode][sdt.ptcode][sdt.m.front()][time] = sdt; - } + 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 + + 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); + } + + 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]); + } + + GTime time = sdt.epochstart; + + theSinex.mapdatahandling[sdt.sitecode][sdt.ptcode][sdt.m.front()][time] = sdt; + } } void parsePrecode(string& line) { - SinexPreCode snt; + SinexPreCode snt; - snt.precesscode = line.substr(1, 8); - snt.comment = line.substr(10); + snt.precesscode = line.substr(1, 8); + snt.comment = line.substr(10); - theSinex.listprecessions.push_back(snt); + theSinex.listprecessions.push_back(snt); } void writeSnxPreCodes(ofstream& out) { - Block block(out, "PRECESSION/DATA"); + Block block(out, "PRECESSION/DATA"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& spt : theSinex.listprecessions) - { - char line[81]; + for (auto& spt : theSinex.listprecessions) + { + char line[81]; - snprintf(line, sizeof(line), " %8s %s", spt.precesscode.c_str(), spt.comment.c_str()); + snprintf(line, sizeof(line), " %8s %s", spt.precesscode.c_str(), spt.comment.c_str()); - out << line << "\n"; - } + out << line << "\n"; + } } void parseNutcode(string& line) { - SinexNutCode snt; + SinexNutCode snt; - snt.nutcode = line.substr(1, 8); - snt.comment = line.substr(10); + snt.nutcode = line.substr(1, 8); + snt.comment = line.substr(10); - theSinex.listnutcodes.push_back(snt); + theSinex.listnutcodes.push_back(snt); } void writeSnxNutCodes(ofstream& out) { - Block block(out, "NUTATION/DATA"); + Block block(out, "NUTATION/DATA"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& nutcode : theSinex.listnutcodes) - { - SinexNutCode& snt = nutcode; + for (auto& nutcode : theSinex.listnutcodes) + { + SinexNutCode& snt = nutcode; - char line[81]; + char line[81]; - snprintf(line, sizeof(line), " %8s %s", snt.nutcode.c_str(), snt.comment.c_str()); + snprintf(line, sizeof(line), " %8s %s", snt.nutcode.c_str(), snt.comment.c_str()); - out << line << "\n"; - } + out << line << "\n"; + } } void parseSourceIds(string& line) { - SinexSourceId ssi; + SinexSourceId ssi; - ssi.source = line.substr(1, 4); - ssi.iers = line.substr(6, 8); - ssi.icrf = line.substr(15, 16); - ssi.comments = line.substr(32); + 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); + theSinex.listsourceids.push_back(ssi); } void writeSnxSourceIds(ofstream& out) { - Block block(out, "SOURCE/ID"); + Block block(out, "SOURCE/ID"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& source_id : theSinex.listsourceids) - { - SinexSourceId& ssi = source_id; + for (auto& source_id : theSinex.listsourceids) + { + SinexSourceId& ssi = source_id; - char line[101]; + 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()); + 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"; - } + out << line << "\n"; + } } -bool compareSatIds( - SinexSatId& left, - SinexSatId& right) +bool compareSatIds(SinexSatId& left, SinexSatId& right) { - int comp = left.svn.compare(right.svn); + int comp = left.svn.compare(right.svn); - return (comp < 0); + return (comp < 0); } void parseSatelliteIds(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatId sst; + 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); + 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]); + 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] + ); - if (readcount == 6) - { - // TODO: make the following adjustements - // TSL if 0 is Sinex file start date - // TUD if 0 is Sinex file end date + if (readcount == 6) + { + // TODO: make the following adjustements + // TSL if 0 is Sinex file start date + // TUD if 0 is Sinex file end date - theSinex.listsatids.push_back(sst); - } + theSinex.listsatids.push_back(sst); + } } void writeSnxSatIds(ofstream& out) { - Block block(out, "SATELLITE/ID"); + Block block(out, "SATELLITE/ID"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& ssi : theSinex.listsatids) - { - char line[101]; + for (auto& ssi : theSinex.listsatids) + { + char line[101]; - snprintf(line, sizeof(line), " %4s %2s %9s %c %2.2d:%3.3d:%5.5d %2.2d:%3.3d:%5.5d %20s", - ssi.svn.c_str(), - ssi.prn.c_str() + 1, - ssi.cospar.c_str(), - ssi.obsCode, - (int)ssi.timeSinceLaunch[0], - (int)ssi.timeSinceLaunch[1], - (int)ssi.timeSinceLaunch[2], - (int)ssi.timeUntilDecom[0], - (int)ssi.timeUntilDecom[1], - (int)ssi.timeUntilDecom[2], - ssi.antRcvType.c_str()); + snprintf( + line, + sizeof(line), + " %4s %2s %9s %c %2.2d:%3.3d:%5.5d %2.2d:%3.3d:%5.5d %20s", + ssi.svn.c_str(), + ssi.prn.c_str() + 1, + ssi.cospar.c_str(), + ssi.obsCode, + (int)ssi.timeSinceLaunch[0], + (int)ssi.timeSinceLaunch[1], + (int)ssi.timeSinceLaunch[2], + (int)ssi.timeUntilDecom[0], + (int)ssi.timeUntilDecom[1], + (int)ssi.timeUntilDecom[2], + ssi.antRcvType.c_str() + ); - out << line << "\n"; - } + out << line << "\n"; + } } void parseSatelliteIdentifiers(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatIdentity sst; + SinexSatIdentity sst; - 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.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); - theSinex.satIdentityMap[sst.svn] = sst; + theSinex.satIdentityMap[sst.svn] = sst; - nav.blocktypeMap[sst.svn] = sst.blocktype; + nav.blocktypeMap[sst.svn] = sst.blocktype; } void writeSnxSatIdents(ofstream& out) { - Block block(out, "SATELLITE/IDENTIFIER"); + Block block(out, "SATELLITE/IDENTIFIER"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& [svn, ssi] : theSinex.satIdentityMap) - { - char line[101]; + for (auto& [svn, ssi] : theSinex.satIdentityMap) + { + char line[101]; - snprintf(line, sizeof(line), " %4s %9s %6d %-15s %s", - ssi.svn.c_str(), - ssi.cospar.c_str(), - ssi.category, - ssi.blocktype.c_str(), - ssi.comment.c_str()); + snprintf( + line, + sizeof(line), + " %4s %9s %6d %-15s %s", + ssi.svn.c_str(), + ssi.cospar.c_str(), + ssi.category, + ssi.blocktype.c_str(), + ssi.comment.c_str() + ); - out << line << "\n"; - } + out << line << "\n"; + } } -bool compareSatPrns( - SinexSatPrn& left, - SinexSatPrn& right) +bool compareSatPrns(SinexSatPrn& left, SinexSatPrn& right) { - int comp = left.prn.compare(right.prn); + int comp = left.prn.compare(right.prn); - return (comp < 0); + return (comp < 0); } void parseSatPrns(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatPrn spt; + SinexSatPrn spt; - spt.svn = line.substr(1, 4); - spt.prn = line.substr(36, 3); - spt.comment = line.substr(40); + 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]); + 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); + 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; - } + 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"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - char line[101]; + char line[101]; - for (auto& spt : theSinex.listsatprns) - { - snprintf(line, sizeof(line), " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %3s %s", - spt.svn.c_str(), - (int)spt.start[0], - (int)spt.start[1], - (int)spt.start[2], - (int)spt.stop[0], - (int)spt.stop[1], - (int)spt.stop[2], - spt.prn.c_str(), - spt.comment.c_str()); + for (auto& spt : theSinex.listsatprns) + { + snprintf( + line, + sizeof(line), + " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %3s %s", + spt.svn.c_str(), + (int)spt.start[0], + (int)spt.start[1], + (int)spt.start[2], + (int)spt.stop[0], + (int)spt.stop[1], + (int)spt.stop[2], + spt.prn.c_str(), + spt.comment.c_str() + ); - out << line << "\n"; - } + out << line << "\n"; + } } -bool compareFreqChannels( - SinexSatFreqChn& left, - SinexSatFreqChn& right) +bool compareFreqChannels(SinexSatFreqChn& left, SinexSatFreqChn& right) { - // start by comparing SVN... - int comp = left.svn.compare(right.svn); + // 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]; + // 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); + return (comp < 0); } void parseSatFreqChannels(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatFreqChn sfc; + SinexSatFreqChn sfc; - sfc.svn = line.substr(1, 4); - sfc.comment = line.substr(40); + 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); + 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); - } + 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"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& sfc : theSinex.listsatfreqchns) - { - char line[101]; + for (auto& sfc : theSinex.listsatfreqchns) + { + char line[101]; - snprintf(line, sizeof(line), " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %3d %s", - sfc.svn.c_str(), - (int)sfc.start[0], - (int)sfc.start[1], - (int)sfc.start[2], - (int)sfc.stop[0], - (int)sfc.stop[1], - (int)sfc.stop[2], - sfc.channel, - sfc.comment.c_str()); + snprintf( + line, + sizeof(line), + " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %3d %s", + sfc.svn.c_str(), + (int)sfc.start[0], + (int)sfc.start[1], + (int)sfc.start[2], + (int)sfc.stop[0], + (int)sfc.stop[1], + (int)sfc.stop[2], + sfc.channel, + sfc.comment.c_str() + ); - out << line << "\n"; - } + out << line << "\n"; + } } void parseSatelliteMass(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatMass ssm; + SinexSatMass ssm; - ssm.svn = line.substr(1, 4); - ssm.comment = line.substr(46); + 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); + 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; - } + 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"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& [svn, ssmMap] : theSinex.mapsatmasses) - for (auto& [time, ssm] : ssmMap) - { - char line[101]; + for (auto& [svn, ssmMap] : theSinex.mapsatmasses) + for (auto& [time, ssm] : ssmMap) + { + char line[101]; - snprintf(line, sizeof(line), " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %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()); + snprintf( + line, + sizeof(line), + " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %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"; - } + out << line << "\n"; + } } -bool compareSatCom( - SinexSatCom& left, - SinexSatCom& right) +bool compareSatCom(SinexSatCom& left, SinexSatCom& right) { - // start by comparing SVN... - int comp = left.svn.compare(right.svn); + // 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]; + // 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); + return (comp < 0); } void parseSatelliteComs(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatCom sct; + SinexSatCom sct; - sct.svn = line.substr(1, 4); - sct.comment = line.substr(66); + 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 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] + ); - if (readcount == 9) - { - // No need to adjust years since for satellites the year is 4 digits ... - theSinex.listsatcoms.push_back(sct); - } + if (readcount == 9) + { + // No need to adjust years since for satellites the year is 4 digits ... + theSinex.listsatcoms.push_back(sct); + } } void writeSnxSatCom(ofstream& out) { - Block block(out, "SATELLITE/COM"); + Block block(out, "SATELLITE/COM"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& sct : theSinex.listsatcoms) - { - char line[101]; + for (auto& sct : theSinex.listsatcoms) + { + char line[101]; - snprintf(line, sizeof(line), " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %9.4lf %9.4lf %9.4lf %s", - sct.svn.c_str(), - (int)sct.start[0], - (int)sct.start[1], - (int)sct.start[2], - (int)sct.stop[0], - (int)sct.stop[1], - (int)sct.stop[2], - sct.com[0], - sct.com[1], - sct.com[2], - sct.comment.c_str()); + snprintf( + line, + sizeof(line), + " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %9.4lf %9.4lf %9.4lf %s", + sct.svn.c_str(), + (int)sct.start[0], + (int)sct.start[1], + (int)sct.start[2], + (int)sct.stop[0], + (int)sct.stop[1], + (int)sct.stop[2], + sct.com[0], + sct.com[1], + sct.com[2], + sct.comment.c_str() + ); - out << line << "\n"; - } + out << line << "\n"; + } } -bool compareSatEcc( - SinexSatEcc& left, - SinexSatEcc& right) +bool compareSatEcc(SinexSatEcc& left, SinexSatEcc& right) { - // start by comparing SVN... - int comp = left.svn.compare(right.svn); + // 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); + // then by type (P or L) + if (comp == 0) + comp = static_cast(left.type) - static_cast(right.type); - return (comp < 0); + return (comp < 0); } void parseSatelliteEccentricities(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatEcc set; + SinexSatEcc set; - set.svn = line.substr(1, 4); - set.equip = line.substr(6, 20); - set.type = line[27]; - set.comment = line.substr(59); + 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]); + 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); - } + if (readcount == 3) + { + theSinex.listsateccs.push_back(set); + } } void writeSnxSatEcc(ofstream& out) { - Block block(out, "SATELLITE/ECCENTRICITY"); + Block block(out, "SATELLITE/ECCENTRICITY"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& set : theSinex.listsateccs) - { - char line[101]; + for (auto& set : theSinex.listsateccs) + { + char line[101]; - snprintf(line, sizeof(line), " %4s %-20s %c %9.4lf %9.4lf %9.4lf %s", - set.svn.c_str(), - set.equip.c_str(), - set.type, - set.ecc[0], - set.ecc[1], - set.ecc[2], - set.comment.c_str()); + snprintf( + line, + sizeof(line), + " %4s %-20s %c %9.4lf %9.4lf %9.4lf %s", + set.svn.c_str(), + set.equip.c_str(), + set.type, + set.ecc[0], + set.ecc[1], + set.ecc[2], + set.comment.c_str() + ); - out << line << "\n"; - } + out << line << "\n"; + } } void parseSatellitePowers(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatPower spt; + SinexSatPower spt; - spt.svn = line.substr(1, 4); - spt.comment = line.substr(41); + 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); + 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; - } + 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"); - writeAsComments(out, theSinex.blockComments[block.blockName]); + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& [svn, sptmap] : theSinex.mapsatpowers) - for (auto& [time, spt] : sptmap) - { - char line[101]; + for (auto& [svn, sptmap] : theSinex.mapsatpowers) + for (auto& [time, spt] : sptmap) + { + char line[101]; - snprintf(line, sizeof(line), " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %4d %s", - spt.svn.c_str(), - (int)spt.start[0], - (int)spt.start[1], - (int)spt.start[2], - (int)spt.stop[0], - (int)spt.stop[1], - (int)spt.stop[2], - spt.power, - spt.comment.c_str()); + snprintf( + line, + sizeof(line), + " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %4d %s", + spt.svn.c_str(), + (int)spt.start[0], + (int)spt.start[1], + (int)spt.start[2], + (int)spt.stop[0], + (int)spt.stop[1], + (int)spt.stop[2], + spt.power, + spt.comment.c_str() + ); - out << line << "\n"; - } + out << line << "\n"; + } } -bool compareSatPc( - SinexSatPc& left, - SinexSatPc& right) +bool compareSatPc(SinexSatPc& left, SinexSatPc& right) { - // start by comparing SVN... - int comp = left.svn.compare(right.svn); + // 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); + // then by the first freq number + if (comp == 0) + comp = static_cast(left.freq) - static_cast(right.freq); - return (comp < 0); + return (comp < 0); } void parseSatellitePhaseCenters(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatPc spt; + SinexSatPc spt; - int readcount2; + 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]; + 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]); + 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 (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); - } + if (readcount == 3 && (spt.freq2 == ' ' || readcount2 == 3)) + { + theSinex.listsatpcs.push_back(spt); + } } void writeSnxSatPc(ofstream& out) { - Block block(out, "SATELLITE/PHASE_CENTER"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& spt : theSinex.listsatpcs) - { - 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); - - out << line << "\n"; - } + Block block(out, "SATELLITE/PHASE_CENTER"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& spt : theSinex.listsatpcs) + { + 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 + ); + + out << line << "\n"; + } } void parseSinexSatYawRates(string& line) { - const char* buff = line.c_str(); + const char* buff = line.c_str(); - SinexSatYawRate entry; + SinexSatYawRate entry; - entry.svn = line.substr(1, 4); - entry.comment = line.substr(51); + 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); + 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; + entry.maxYawRate *= D2R; - if (readCount == 8) - { - theSinex.satYawRateMap[entry.svn][entry.start] = entry; - } + 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) -{ -// 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; - } - - theSinex.currentFile = filepath; - - void (*parseFunction)(string&) = nullFunction; - - string closure; - - bool failure = false; - - int lineNumber = 0; - - while (filestream) - { - string line; - - getline(filestream, line); - - lineNumber++; - - // 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) - << "Error: Closure line not found before end."; - - failure = true; - break; - } - else if (line[0] == '*') - { - //comment - } - else if (line[0] == '-') - { - //end of block - parseFunction = nullFunction; - - if (line != closure) - { - BOOST_LOG_TRIVIAL(error) - << "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) - << "Error: Sinex line width error on line " << lineNumber << ": '" << line << "'"; - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Sinex parsing error on line " << lineNumber << ": '" << line << "'"; - } - } - else if (line[0] == '+') - { - string mvs; - - //prepare closing line for comparison - closure = line; - closure[0] = '-'; - - 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" ) { parseFunction = parseSinexSatAttMode; } - else - { - BOOST_LOG_TRIVIAL(error) - << "Error: unknown header line: " << line; - - failure = true; - } - -// 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) - << "Error: line starting '%' met not final line" << "\n" << line; - - failure = true; - } - - break; - } - - if (failure) - break; - } - - 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); - -// theSinex.matrix_map[type][value].sort(compare_matrix_entries); - dedupeSinex(); - - return failure == false; -} - - -void writeSinex( - string filepath, - KFState& kfState, - map& receiverMap) -{ - ofstream filestream(filepath); - - if (!filestream) - { - return; - } - - commentsOverride(); - - writeSnxHeader(filestream); - - if (!theSinex.refstrings. empty()) { writeSnxReference (filestream);} - if (!theSinex.blockComments["FILE/COMMENT"].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()) { writeSnxSiteEccs (filestream);} - if (!theSinex.solEpochMap. empty()) { writeSnxEpochs (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);} - - { -// 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); + 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) +{ + // 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; + } + + theSinex.currentFile = filepath; + + void (*parseFunction)(string&) = nullFunction; + + string closure; + + bool failure = false; + + int lineNumber = 0; + + while (filestream) + { + string line; + + getline(filestream, line); + + lineNumber++; + + // 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."; + + failure = true; + break; + } + else if (line[0] == '*') + { + // comment + } + else if (line[0] == '-') + { + // end of block + parseFunction = nullFunction; + + 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; + + // prepare closing line for comparison + closure = line; + closure[0] = '-'; + + 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") + { + parseFunction = parseSinexSatAttMode; + } + 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.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); + + // theSinex.matrix_map[type][value].sort(compare_matrix_entries); + dedupeSinex(); + + return failure == false; +} + +void writeSinex(string filepath, KFState& kfState, map& receiverMap) +{ + ofstream filestream(filepath); + + if (!filestream) + { + return; + } + + commentsOverride(); + + writeSnxHeader(filestream); + + if (!theSinex.refstrings.empty()) + { + writeSnxReference(filestream); + } + if (!theSinex.blockComments["FILE/COMMENT"].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()) + { + writeSnxSiteEccs(filestream); + } + if (!theSinex.solEpochMap.empty()) + { + writeSnxEpochs(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);} + + { + // 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 - struct utsname buf; - 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); - - 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); - } - - 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; + // 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); + theSinex.blockComments["FILE/COMMENT"].push_back(what); } void sinexAddFiles( - const string& who, - const GTime& time, - const vector& filenames, - const string& description) + const string& who, + const GTime& time, + const vector& filenames, + const string& description +) { - for (auto& filename : filenames) - { - SinexInputFile sif; + for (auto& filename : filenames) + { + SinexInputFile sif; - sif.yds = time; - sif.agency = who; - sif.file = filename; - sif.description = description; + sif.yds = time; + sif.agency = who; + sif.file = filename; + sif.description = description; - theSinex.inputFiles.push_back(sif); - } + theSinex.inputFiles.push_back(sif); + } } -void setRestrictiveEndTime( - UYds& current, - UYds& potential) +void setRestrictiveEndTime(UYds& current, UYds& potential) { - UYds zeros; + UYds zeros; - if (current == zeros) { current = potential; return; } //current is zero, just use the new version for the end time - if (potential == zeros) { return; } //potential time is zero, thats not restrictive, keep the current time - if (potential == current) { current = potential; return; } //potential end time is more restrictive + if (current == zeros) + { + current = potential; + return; + } // current is zero, just use the new version for the end time + if (potential == zeros) + { + return; + } // potential time is zero, thats not restrictive, keep the current time + if (potential == current) + { + current = potential; + return; + } // potential end time is more restrictive } -GetSnxResult getRecSnx( - string id, - GTime time, - SinexRecData& recSnx) +GetSnxResult getRecSnx(string id, GTime time, SinexRecData& recSnx) { - recSnx = SinexRecData(); - recSnx.start = time; - - GetSnxResult result; - - bool found = false; - - // search siteids for receiver (not time dependent) - auto siteIdIt = theSinex.mapsiteids.find(id); - if (siteIdIt != theSinex.mapsiteids.end()) - { - auto& [dummy, siteId] = *siteIdIt; - - recSnx.id_ptr = &siteId; - - siteId.used = true; - } - else - { - result.failureSiteId = true; - } - - auto receiverIt = theSinex.mapreceivers.find(id); - if (receiverIt != theSinex.mapreceivers.end()) - { - auto& [dummy, recTimeMap] = *receiverIt; - - auto timeRecIt = recTimeMap.lower_bound(time); - if (timeRecIt != recTimeMap.end()) - { - auto& [dummy, receiver] = *timeRecIt; - - receiver.used = true; - - recSnx.rec_ptr = &receiver; - - found = true; - - // get next next start time as end time for this aspect - if (timeRecIt != recTimeMap.begin()) - { - timeRecIt--; - auto& nextReceiver = timeRecIt->second; - - setRestrictiveEndTime(receiver.end, nextReceiver.start); - } - - setRestrictiveEndTime(recSnx.start, receiver.end); - } - } + recSnx = SinexRecData(); + recSnx.start = time; - if (!found) - result.failureReceiver = true; + GetSnxResult result; - found = false; + bool found = false; - auto antIt = theSinex.mapantennas.find(id); - if (antIt != theSinex.mapantennas.end()) - { - auto& [dummy, antTimeMap] = *antIt; + // search siteids for receiver (not time dependent) + auto siteIdIt = theSinex.mapsiteids.find(id); + if (siteIdIt != theSinex.mapsiteids.end()) + { + auto& [dummy, siteId] = *siteIdIt; - auto antIt2 = theSinex.mapantennas[id].lower_bound(time); - if (antIt2 != theSinex.mapantennas[id].end()) - { - auto& [dummy, antenna] = *antIt2; + recSnx.id_ptr = &siteId; - found = true; - antenna.used = true; + siteId.used = true; + } + else + { + result.failureSiteId = true; + } - recSnx.ant_ptr = &antenna; + auto receiverIt = theSinex.mapreceivers.find(id); + if (receiverIt != theSinex.mapreceivers.end()) + { + auto& [dummy, recTimeMap] = *receiverIt; - // get next next start time as end time for this aspect - if (antIt2 != theSinex.mapantennas[id].begin()) - { - antIt2--; - auto& [dummy, nextAntenna] = *antIt2; + auto timeRecIt = recTimeMap.lower_bound(time); + if (timeRecIt != recTimeMap.end()) + { + auto& [dummy, receiver] = *timeRecIt; - setRestrictiveEndTime(antenna.end, nextAntenna.start); - } + receiver.used = true; - setRestrictiveEndTime(recSnx.start, antenna.end); - } - } + recSnx.rec_ptr = &receiver; - if (!found) - result.failureAntenna = true; + found = true; - found = false; + // get next next start time as end time for this aspect + if (timeRecIt != recTimeMap.begin()) + { + timeRecIt--; + auto& nextReceiver = timeRecIt->second; - auto eccIt = theSinex.mapeccentricities.find(id); - if (eccIt != theSinex.mapeccentricities.end()) - { - auto& [dummy, eccMap] = *eccIt; + setRestrictiveEndTime(receiver.end, nextReceiver.start); + } - auto eccIt2 = eccMap.lower_bound(time); - if (eccIt2 != theSinex.mapeccentricities[id].end()) - { - auto& [dummy, ecc] = *eccIt2; + setRestrictiveEndTime(recSnx.start, receiver.end); + } + } - found = true; + if (!found) + result.failureReceiver = true; - ecc.used = true; + found = false; - recSnx.ecc_ptr = &ecc; + auto antIt = theSinex.mapantennas.find(id); + if (antIt != theSinex.mapantennas.end()) + { + auto& [dummy, antTimeMap] = *antIt; - // get next next start time as end time for this aspect - if (eccIt2 != theSinex.mapeccentricities[id].begin()) - { - eccIt2--; - auto& [dummy, nextEcc] = *eccIt2; + auto antIt2 = theSinex.mapantennas[id].lower_bound(time); + if (antIt2 != theSinex.mapantennas[id].end()) + { + auto& [dummy, antenna] = *antIt2; - setRestrictiveEndTime(ecc.end, nextEcc.start); - } + found = true; + antenna.used = true; - setRestrictiveEndTime(recSnx.stop, ecc.end); - } - } + recSnx.ant_ptr = &antenna; - if (!found) - result.failureEccentricity = true; - - found = false; -// -// for (auto& gps_pcs : theSinex.list_gps_pcs) -// { -// if ( (stn_snx.anttype.compare(gps_pcs.antname) == 0) -// &&(stn_snx.antsn.compare(gps_pcs.serialno) == 0)) -// { -// for (int i = 0; i < 3; i++) -// { -// stn_snx.gpsl1[i] = gps_pcs.L1[i]; -// stn_snx.gpsl2[i] = gps_pcs.L2[i]; -// } -// -// stn_snx.has_gps_pc = true; -// found = true; -// break; -// } -// } - - if (!found) - result.failurePhaseCentre = true; - - found = true; - - for (string type : {"STA? ", "VEL? "}) - for (int i = 0; i < 3; i++) - { - type[3] = 'X' + i; - - auto& estMap = theSinex.estimatesMap[id][type]; - - SinexSolEstimate* estimate_ptr = nullptr; - - auto est_it = estMap.lower_bound(time); - GTime refEpoch = {}; - if (est_it != estMap.end()) - { - estimate_ptr = &est_it->second; - refEpoch = est_it->first; - - // get next next start time as end time for this aspect - if (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()) - { - //actually theres no estimate for this thing - if (type.substr(0,3) == "STA") - found = false; - break; - } - - estimate_ptr = &est_Rit->second; - refEpoch = est_Rit->first; - } - - auto& estimate = *estimate_ptr; - - estimate.used = true; - - if (type.substr(0,3) == "STA") - { - recSnx.pos(i) = estimate.estimate; - recSnx.var(i) = SQR( estimate.stddev); - recSnx.refEpoch= refEpoch; - } - else if (type.substr(0,3) == "VEL") - { - recSnx.vel(i) = estimate.estimate; - } - } - - recSnx.pos += recSnx.vel * (time - recSnx.refEpoch).to_double() / 86400 / 365.25; //meters per year - - if (found == false) - { - result.failureEstimate = true; - } - - return result; -} - -GetSnxResult getSatSnx( - string prn, - GTime time, - SinexSatSnx& satSnx) -{ - bool found = false; - - GetSnxResult result; - - satSnx = SinexSatSnx(); - satSnx.start = time; - - // search satprns for prn and svn (not time dependent) - for (auto& satPrn : theSinex.listsatprns) - { - GTime startTime = satPrn.start; - GTime stopTime = satPrn.stop; - - if ( satPrn.prn == prn - && (time - startTime) .to_double() >= 0 - &&( (time - stopTime) .to_double() <= 0 - ||satPrn.stop[0] == 0)) - { - satSnx.prn = prn; - satSnx.svn = satPrn.svn; - //todo: start and stop time - found = true; - break; - } - } - - if (!found) - result.failurePRN = true; - - // sat identifiers - auto itr = theSinex.satIdentityMap.find(satSnx.svn); - if (itr != theSinex.satIdentityMap.end()) - { - auto& [dummy, satId] = *itr; - - satSnx.id_ptr = &satId; - } - else - { - result.failureSatId = true; - } - - - //todo: add other sections for satellite in theSinex - - // sat com - found = false; - for (auto& satCom : theSinex.listsatcoms) - { - GTime startTime = satCom.start; - GTime stopTime = satCom.stop; - - if ( satCom.svn == satSnx.svn - && (time - startTime) .to_double() >= 0 - &&( (time - stopTime) .to_double() <= 0 - ||satCom.stop[0] == 0)) - { - for (int i = 0; i < 3; i++) - satSnx.com[i] = satCom.com[i]; - //todo: start and stop time - found = true; - } - } - - if (!found) - result.failureCOM = true; - - found = false; - - // sat eccentricities - for (auto& satEcc : theSinex.listsateccs) - { - if (satEcc.svn == satSnx.svn) - { - E_EccType eccType; - switch (satEcc.type) - { - case 'P': { found = true; satSnx.ecc_ptrs[E_EccType::P_ANT] = &satEcc; break; } - case 'L': { found = true; satSnx.ecc_ptrs[E_EccType::L_LRA] = &satEcc; break; } - default: - { - BOOST_LOG_TRIVIAL(error) << "Unknown satellite eccentricity type"; - break; - } - } - } - } - - if (!found) - result.failureEccentricity = true; - - //todo: add other sections for satellite in theSinex - - return result; -} - -void getSlrRecBias( - string id, - string prn, - GTime time, - map& recBias) -{ - string ptcode; - if (prn[1] == 'L') ptcode = prn.substr(1, 2); // Eugene to confirm prn only applied for 'L' sats for ptcode - else ptcode = "--"; - - // Loop through "M" models codes - Ref: https://ilrs.gsfc.nasa.gov/docs/2024/ILRS_Data_Handling_File_2024.02.13.snx - for (auto code : {'R', 'T', 'X', 'E', 'H', 'P', 'U', 'N', 'Q', 'V'}) - { - auto it = theSinex.mapdatahandling[id][ptcode][code].lower_bound(time); - if (it == theSinex.mapdatahandling[id][ptcode][code].end()) - { - if (ptcode == "--") // have already searched for "--" (prn not applied for ptcode) - { - continue; - } - - it = theSinex.mapdatahandling[id]["--"][code].lower_bound(time); // not found for prn number, furthur search for "--" - if (it == theSinex.mapdatahandling[id]["--"][code].end()) - { - continue; - } - } - - double unitsFactor = -1; - auto& [dummy, dataHandling] = *it; - - GTime stopTime = dataHandling.epochend; - - if ( stopTime != GTime::noTime() - &&stopTime < time) - { - continue; - } - - switch (code) - { - case 'R': // Range bias to be applied, no estimation of bias - if (dataHandling.unit == "mm ") unitsFactor = 1e-3; - break; - case 'T': // Time bias in ms or µs & µs/d (T2L2) to be applied, NOT estimated - if (dataHandling.unit == "ms ") unitsFactor = 1e-3; - else if (dataHandling.unit == "us ") unitsFactor = 1e-6; - break; - case 'E': // Estimation of range bias, known a priori values are given - if (dataHandling.unit == "mm ") unitsFactor = 1e-3; - break; - case 'H': // humidity error (correction in %) - if (dataHandling.unit == "% ") unitsFactor = 1e-3; - break; - case 'P': // pressure bias (correction in mB) - if (dataHandling.unit == "mB ") unitsFactor = 1; - break; - case 'U': // Estimation of time bias in ms - if (dataHandling.unit == "ms ") unitsFactor = 1e-3; - else if (dataHandling.unit == "us ") unitsFactor = 1e-6; - break; - // Equivalent to 'E': - case 'C': // Target signature bias, correction different from standard - case 'S': // Stanford event counter bias - unitsFactor = 0; - break; - // Data to be excluded: - case 'X': // Exclude/delete data - case 'N': // unreliable station, should not be used in routine processing - case 'Q': // Receiver with data in quarantine, not to be used in official products - case 'V': // Receiver with not validated coordinates, not solving for biases - recBias[code] = true; // recBias['X'] == recBias['N'] == recBias['Q'] == recBias['V'] == true - continue; - } - - if (unitsFactor < 0) - { - BOOST_LOG_TRIVIAL(error) << "Error: unhandled units in " << __FUNCTION__ << ", model code " << code << " : " << dataHandling.unit; - continue; - } - - recBias[code] = dataHandling.estimate * unitsFactor; - - if ( code == 'T' - &&dataHandling.comments == "drift") - { - GTime end = dataHandling.epochend; - GTime start = dataHandling.epochstart; - - double interval = (end - start).to_double(); - GTime midInterval = start + interval / 2; - - double numDays = (time - midInterval).to_double() / S_IN_DAY; - - recBias[code] += dataHandling.estrate * numDays * 1e-6; // estrate units in us/day - } - - continue; - } + // get next next start time as end time for this aspect + if (antIt2 != theSinex.mapantennas[id].begin()) + { + antIt2--; + auto& [dummy, nextAntenna] = *antIt2; + + setRestrictiveEndTime(antenna.end, nextAntenna.start); + } + + setRestrictiveEndTime(recSnx.start, antenna.end); + } + } + + if (!found) + result.failureAntenna = true; + + found = false; + + auto eccIt = theSinex.mapeccentricities.find(id); + if (eccIt != theSinex.mapeccentricities.end()) + { + auto& [dummy, eccMap] = *eccIt; + + auto eccIt2 = eccMap.lower_bound(time); + if (eccIt2 != theSinex.mapeccentricities[id].end()) + { + auto& [dummy, ecc] = *eccIt2; + + found = true; + + ecc.used = true; + + 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; + + setRestrictiveEndTime(ecc.end, nextEcc.start); + } + + setRestrictiveEndTime(recSnx.stop, ecc.end); + } + } + + if (!found) + result.failureEccentricity = true; + + found = false; + // + // for (auto& gps_pcs : theSinex.list_gps_pcs) + // { + // if ( (stn_snx.anttype.compare(gps_pcs.antname) == 0) + // &&(stn_snx.antsn.compare(gps_pcs.serialno) == 0)) + // { + // for (int i = 0; i < 3; i++) + // { + // stn_snx.gpsl1[i] = gps_pcs.L1[i]; + // stn_snx.gpsl2[i] = gps_pcs.L2[i]; + // } + // + // stn_snx.has_gps_pc = true; + // found = true; + // break; + // } + // } + + if (!found) + result.failurePhaseCentre = true; + + found = true; + + for (string type : {"STA? ", "VEL? "}) + for (int i = 0; i < 3; i++) + { + type[3] = 'X' + i; + + auto& estMap = theSinex.estimatesMap[id][type]; + + SinexSolEstimate* estimate_ptr = nullptr; + + auto est_it = estMap.lower_bound(time); + GTime refEpoch = {}; + if (est_it != estMap.end()) + { + estimate_ptr = &est_it->second; + refEpoch = est_it->first; + + // get next next start time as end time for this aspect + if (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()) + { + // actually theres no estimate for this thing + if (type.substr(0, 3) == "STA") + found = false; + break; + } + + estimate_ptr = &est_Rit->second; + refEpoch = est_Rit->first; + } + + auto& estimate = *estimate_ptr; + + estimate.used = true; + + if (type.substr(0, 3) == "STA") + { + recSnx.pos(i) = estimate.estimate; + recSnx.var(i) = SQR(estimate.stddev); + recSnx.refEpoch = refEpoch; + } + else if (type.substr(0, 3) == "VEL") + { + recSnx.vel(i) = estimate.estimate; + } + } + + recSnx.pos += + recSnx.vel * (time - recSnx.refEpoch).to_double() / 86400 / 365.25; // meters per year + + if (found == false) + { + result.failureEstimate = true; + } + + return result; +} + +GetSnxResult getSatSnx(string prn, GTime time, SinexSatSnx& satSnx) +{ + bool found = false; + + GetSnxResult result; + + satSnx = SinexSatSnx(); + satSnx.start = time; + + // search satprns for prn and svn (not time dependent) + for (auto& satPrn : theSinex.listsatprns) + { + GTime startTime = satPrn.start; + GTime stopTime = satPrn.stop; + + if (satPrn.prn == prn && (time - startTime).to_double() >= 0 && + ((time - stopTime).to_double() <= 0 || satPrn.stop[0] == 0)) + { + satSnx.prn = prn; + satSnx.svn = satPrn.svn; + // todo: start and stop time + found = true; + break; + } + } + + if (!found) + result.failurePRN = true; + + // sat identifiers + auto itr = theSinex.satIdentityMap.find(satSnx.svn); + if (itr != theSinex.satIdentityMap.end()) + { + auto& [dummy, satId] = *itr; + + satSnx.id_ptr = &satId; + } + else + { + result.failureSatId = true; + } + + // todo: add other sections for satellite in theSinex + + // sat com + found = false; + for (auto& satCom : theSinex.listsatcoms) + { + GTime startTime = satCom.start; + GTime stopTime = satCom.stop; + + if (satCom.svn == satSnx.svn && (time - startTime).to_double() >= 0 && + ((time - stopTime).to_double() <= 0 || satCom.stop[0] == 0)) + { + for (int i = 0; i < 3; i++) + satSnx.com[i] = satCom.com[i]; + // todo: start and stop time + found = true; + } + } + + if (!found) + result.failureCOM = true; + + found = false; + + // sat eccentricities + for (auto& satEcc : theSinex.listsateccs) + { + if (satEcc.svn == satSnx.svn) + { + E_EccType eccType; + switch (satEcc.type) + { + case 'P': + { + found = true; + satSnx.ecc_ptrs[E_EccType::P_ANT] = &satEcc; + break; + } + case 'L': + { + found = true; + satSnx.ecc_ptrs[E_EccType::L_LRA] = &satEcc; + break; + } + default: + { + BOOST_LOG_TRIVIAL(error) << "Unknown satellite eccentricity type"; + break; + } + } + } + } + + if (!found) + result.failureEccentricity = true; + + // todo: add other sections for satellite in theSinex + + return result; +} + +void getSlrRecBias(string id, string prn, GTime time, map& recBias) +{ + string ptcode; + if (prn[1] == 'L') + ptcode = prn.substr(1, 2); // Eugene to confirm prn only applied for 'L' sats for ptcode + else + ptcode = "--"; + + // Loop through "M" models codes - Ref: + // https://ilrs.gsfc.nasa.gov/docs/2024/ILRS_Data_Handling_File_2024.02.13.snx + for (auto code : {'R', 'T', 'X', 'E', 'H', 'P', 'U', 'N', 'Q', 'V'}) + { + auto it = theSinex.mapdatahandling[id][ptcode][code].lower_bound(time); + if (it == theSinex.mapdatahandling[id][ptcode][code].end()) + { + if (ptcode == "--") // have already searched for "--" (prn not applied for ptcode) + { + continue; + } + + it = theSinex.mapdatahandling[id]["--"][code].lower_bound( + time + ); // not found for prn number, furthur search for "--" + if (it == theSinex.mapdatahandling[id]["--"][code].end()) + { + continue; + } + } + + double unitsFactor = -1; + auto& [dummy, dataHandling] = *it; + + GTime stopTime = dataHandling.epochend; + + if (stopTime != GTime::noTime() && stopTime < time) + { + continue; + } + + switch (code) + { + case 'R': // Range bias to be applied, no estimation of bias + if (dataHandling.unit == "mm ") + unitsFactor = 1e-3; + break; + case 'T': // Time bias in ms or µs & µs/d (T2L2) to be applied, NOT estimated + if (dataHandling.unit == "ms ") + unitsFactor = 1e-3; + else if (dataHandling.unit == "us ") + unitsFactor = 1e-6; + break; + case 'E': // Estimation of range bias, known a priori values are given + if (dataHandling.unit == "mm ") + unitsFactor = 1e-3; + break; + case 'H': // humidity error (correction in %) + if (dataHandling.unit == "% ") + unitsFactor = 1e-3; + break; + case 'P': // pressure bias (correction in mB) + if (dataHandling.unit == "mB ") + unitsFactor = 1; + break; + case 'U': // Estimation of time bias in ms + if (dataHandling.unit == "ms ") + unitsFactor = 1e-3; + else if (dataHandling.unit == "us ") + unitsFactor = 1e-6; + break; + // Equivalent to 'E': + case 'C': // Target signature bias, correction different from standard + case 'S': // Stanford event counter bias + unitsFactor = 0; + break; + // Data to be excluded: + case 'X': // Exclude/delete data + case 'N': // unreliable station, should not be used in routine processing + case 'Q': // Receiver with data in quarantine, not to be used in official products + case 'V': // Receiver with not validated coordinates, not solving for biases + recBias[code] = + true; // recBias['X'] == recBias['N'] == recBias['Q'] == recBias['V'] == true + continue; + } + + if (unitsFactor < 0) + { + BOOST_LOG_TRIVIAL(error) << "Unhandled units in " << __FUNCTION__ << ", model code " + << code << " : " << dataHandling.unit; + continue; + } + + recBias[code] = dataHandling.estimate * unitsFactor; + + if (code == 'T' && dataHandling.comments == "drift") + { + GTime end = dataHandling.epochend; + GTime start = dataHandling.epochstart; + + double interval = (end - start).to_double(); + GTime midInterval = start + interval / 2; + + double numDays = (time - midInterval).to_double() / S_IN_DAY; + + recBias[code] += dataHandling.estrate * numDays * 1e-6; // estrate units in us/day + } + + continue; + } } - /** Get yaw rate sinex entry for sat */ -bool getSnxSatMaxYawRate( - string svn, - GTime& time, - double& maxYawRate) +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 itr = theSinex.satYawRateMap[svn].lower_bound(time); + if (itr == theSinex.satYawRateMap[svn].end()) + return false; - auto& [dummy, entry] = *itr; - maxYawRate = entry.maxYawRate; + auto& [dummy, entry] = *itr; + maxYawRate = entry.maxYawRate; - return true; + 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; +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 cd411d448..c5dc55159 100644 --- a/src/cpp/common/sinex.hpp +++ b/src/cpp/common/sinex.hpp @@ -1,22 +1,19 @@ - #pragma once - #include -#include -#include #include #include +#include +#include +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/trace.hpp" -using std::vector; -using std::string; using std::list; using std::map; - -#include "eigenIncluder.hpp" -#include "trace.hpp" -#include "gTime.hpp" -#include "enums.h" +using std::string; +using std::vector; //=============================================================================== /* history structure (optional but recommended) @@ -27,17 +24,17 @@ using std::map; */ struct SinexInputHistory { - char code; // '+' for input, '-' for output - double fmt; // format (d4.2) - string create_agency; // 3 - UYds create_time; - string data_agency; // 3 - UYds start; - UYds stop; - char obs_tech; // - int num_estimates; - char constraint; - string contents; // S/O/E/T/C/A separated characters may have trailing blanks + char code; // '+' for input, '-' for output + double fmt; // format (d4.2) + string create_agency; // 3 + UYds create_time; + string data_agency; // 3 + UYds start; + UYds stop; + char obs_tech; // + int num_estimates; + char constraint; + string contents; // S/O/E/T/C/A separated characters may have trailing blanks }; /* files structure (optional) @@ -48,10 +45,10 @@ struct SinexInputHistory */ struct SinexInputFile { - string agency; // 3 - UYds yds; - string file; // 29 - name of file - string description; // 32 - description of file + string agency; // 3 + UYds yds; + string file; // 29 - name of file + string description; // 32 - description of file }; /* acknowledgements structure (optional) @@ -62,8 +59,8 @@ struct SinexInputFile */ struct SinexAck { - string agency; // 3 - string description; // 75 + string agency; // 3 + string description; // 75 }; /* nut-data structure (mandatory when VLBI) @@ -74,8 +71,8 @@ struct SinexAck */ struct SinexNutCode { - string nutcode; // 8 - one of IAU1980/IERS1996/IAU2000a/IAU2000b - string comment; // 70 - description of model + string nutcode; // 8 - one of IAU1980/IERS1996/IAU2000a/IAU2000b + string comment; // 70 - description of model }; /* precess-data structure (mandatory when VLBI) @@ -86,8 +83,8 @@ struct SinexNutCode */ struct SinexPreCode { - string precesscode; // 8 - one of IAU1976/IERS1996 - string comment; // 70 - description of model + string precesscode; // 8 - one of IAU1976/IERS1996 + string comment; // 70 - description of model }; /* source id structure (mandatory when VLBI) @@ -97,10 +94,10 @@ struct SinexPreCode */ struct SinexSourceId { - string source; // 4 - call sign - string iers; // 8 - 8 char designation - string icrf; // 16 - 16 char designation - string comments; // 58 + string source; // 4 - call sign + string iers; // 8 - 8 char designation + string icrf; // 16 - 16 char designation + string comments; // 58 }; // site/id block structurea (mandatory for GNSS) @@ -110,19 +107,19 @@ 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; // - bool used = false; + 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; // + bool used = false; }; // site/data block structure (optional) @@ -132,17 +129,17 @@ 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; // - UYds start; - UYds stop; - string agency; // 3 - code agency of creation - UYds create; + 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; // + UYds start; + UYds stop; + string agency; // 3 - code agency of creation + UYds create; }; /* receiver block structre (mandatory for GNSS) @@ -153,16 +150,16 @@ 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) - bool used = false; + 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) + bool used = false; }; /* antenna block structure (mandatory for GNSS) @@ -173,16 +170,16 @@ 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)*/ - bool used = false; + 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)*/ + bool used = false; }; /* gps phase centre block structure (mandatory for GPS) @@ -191,11 +188,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) @@ -207,14 +204,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 }; /* @@ -224,15 +221,15 @@ 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 */ - bool used = false; + 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 */ + bool used = false; }; /* @@ -243,13 +240,13 @@ struct SinexSiteEcc */ struct SinexSolEpoch { - string sitecode; //4 - string ptcode; //2 - physical monument used at the site - string solnnum; - char typecode; - UYds start; - UYds end; - UYds mean; + string sitecode; // 4 + string ptcode; // 2 - physical monument used at the site + string solnnum; + char typecode; + UYds start; + UYds end; + UYds mean; }; /* @@ -258,41 +255,40 @@ struct SinexSolEpoch */ struct SinexSolStatistic { - string name; - short etype; // 0 = int, 1 = double - union - { - int ival; - double dval; - } value; + 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 + 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; + 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; + bool used = false; }; /* @@ -302,16 +298,16 @@ struct SinexSolEstimate */ 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 + 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 }; /* @@ -321,15 +317,15 @@ struct SinexSolApriori */ 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 + 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 }; /* @@ -344,13 +340,12 @@ struct SinexSolNeq */ 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 + 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 @@ -363,40 +358,40 @@ struct SinexSolMatrix //============================================================================= 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 -} ; + 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 + ESTIMATE, + APRIORI, + NORMAL_EQN, + MAX_MATRIX_TYPE } matrix_type; typedef enum { - CORRELATION, - COVARIANCE, - INFORMATION, - MAX_MATRIX_VALUE + 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 + P_ANT, // P: antenna //todo: check the meaning of 'P' + L_LRA // L: laser retroreflector array } E_EccType; //============================================================================= @@ -408,13 +403,15 @@ typedef enum */ struct SinexSatId { - string svn; // 4 - string prn; // 2 - string cospar; // 9 - char obsCode; // - UYds timeSinceLaunch; // yy:doy:sod a value of 0 everywhere means launched before file epoch start - UYds timeUntilDecom; // yy:doy:sod a value of 0 everywhere mean still in commission after file epoch end - string antRcvType; // 20 - satellite antenna receiver type + string svn; // 4 + string prn; // 2 + string cospar; // 9 + char obsCode; // + UYds timeSinceLaunch; // yy:doy:sod a value of 0 everywhere means launched before file epoch + // start + UYds timeUntilDecom; // yy:doy:sod a value of 0 everywhere mean still in commission after file + // epoch end + string antRcvType; // 20 - satellite antenna receiver type }; /* @@ -424,11 +421,11 @@ struct SinexSatId */ struct SinexSatIdentity { - string svn; // 4 - string cospar; // 9 - int category; // 6 - string blocktype; // 15 - string comment; // 42 + string svn; // 4 + string cospar; // 9 + int category; // 6 + string blocktype; // 15 + string comment; // 42 }; /* @@ -438,11 +435,11 @@ struct SinexSatIdentity */ struct SinexSatPrn { - string svn; // 4 - UYds start; //yr:doy:sod - UYds stop; //yr:doy:sod - string prn; //3 - string comment; // 40 + string svn; // 4 + UYds start; // yr:doy:sod + UYds stop; // yr:doy:sod + string prn; // 3 + string comment; // 40 }; /* ONLY FOR GLONASS! @@ -452,11 +449,11 @@ struct SinexSatPrn */ struct SinexSatFreqChn { - string svn; // 4 - UYds start; - UYds stop; - int channel; - string comment; // 40? + string svn; // 4 + UYds start; + UYds stop; + int channel; + string comment; // 40? }; /* @@ -466,11 +463,11 @@ struct SinexSatFreqChn */ struct SinexSatMass { - string svn; // 4 - UYds start; - UYds stop; - double mass; // kg - string comment; // 40 + string svn; // 4 + UYds start; + UYds stop; + double mass; // kg + string comment; // 40 }; /* @@ -480,11 +477,11 @@ struct SinexSatMass */ struct SinexSatCom { - string svn; // 4 - UYds start; - UYds stop; - Vector3d com; //x/y/z (metres) - string comment; // 40 + string svn; // 4 + UYds start; + UYds stop; + Vector3d com; // x/y/z (metres) + string comment; // 40 }; /* @@ -494,11 +491,11 @@ struct SinexSatCom */ struct SinexSatEcc { - string svn; // 4 - string equip; // 20 - char type; // L or P - both can exist for the same satellite - VectorEnu ecc; - string comment; // 40 + string svn; // 4 + string equip; // 20 + char type; // L or P - both can exist for the same satellite + VectorEnu ecc; + string comment; // 40 }; /* @@ -508,11 +505,11 @@ struct SinexSatEcc */ struct SinexSatPower { - string svn; // 4p - UYds start; //yr:doy:sod - UYds stop; //yr:doy:sod - int power; // watts - string comment; // 40 + string svn; // 4p + UYds start; // yr:doy:sod + UYds stop; // yr:doy:sod + int power; // watts + string comment; // 40 }; /* @@ -522,29 +519,30 @@ struct SinexSatPower */ 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) + 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________________________________ - G001 1978:053:00000 1985:199:00000 U 0.1999 Launched 1978-02-22; NAVSTAR 1; mass 453800. in previous svnav.dat.allgnss + G001 1978:053:00000 1985:199:00000 U 0.1999 Launched 1978-02-22; NAVSTAR 1; mass 453800. in +previous svnav.dat.allgnss */ struct SinexSatYawRate { - string svn; ///< SVN - UYds start; ///< valid from (yr:doy:sod) - UYds stop; ///< valid until (yr:doy:sod) - char yawBias; ///< yaw bias - double maxYawRate; ///< maximum yaw rate for SV (rad/s) - string comment; ///< comment field + string svn; ///< SVN + UYds start; ///< valid from (yr:doy:sod) + UYds stop; ///< valid until (yr:doy:sod) + char yawBias; ///< yaw bias + double maxYawRate; ///< maximum yaw rate for SV (rad/s) + string comment; ///< comment field }; /* @@ -554,10 +552,10 @@ struct SinexSatYawRate */ struct SinexSatAttMode { - string svn; ///< SVN - GEpoch start; ///< valid from (yyyy-mm-dd hh-mm-ss) - GEpoch stop; ///< valid until (yyyy-mm-dd hh-mm-ss) - string attMode; ///< attitude mode + string svn; ///< SVN + GEpoch start; ///< valid from (yyyy-mm-dd hh-mm-ss) + GEpoch stop; ///< valid until (yyyy-mm-dd hh-mm-ss) + string attMode; ///< attitude mode }; /* @@ -571,12 +569,12 @@ struct SinexSatAttMode */ struct SinexTropDesc { - map strings; //1X,A22 - map ints; //1X,I22 - map doubles; //1X,F22 - map> vecStrings; //1X,n(1X,A6) - map> vecDoubles; //1X,F5.2,1X,F5.2,1X,F8.1 - bool isEmpty = true; + map strings; // 1X,A22 + map ints; // 1X,I22 + map doubles; // 1X,F22 + map> vecStrings; // 1X,n(1X,A6) + map> vecDoubles; // 1X,F5.2,1X,F5.2,1X,F8.1 + bool isEmpty = true; }; /* @@ -586,236 +584,213 @@ struct SinexTropDesc */ struct SinexTropSol { - string site; - UYds yds; - struct TropSolutionEntry //first Sinex_tropsol_t entry also used in TROP/DESCRIPTION block - { - string type; - double value; - double units; - int width; - }; - vector solutions; //map not used b/c may have multiple STDDEV entries + string site; + UYds yds; + struct TropSolutionEntry // first Sinex_tropsol_t entry also used in TROP/DESCRIPTION block + { + string type; + double value; + double units; + int width; + }; + vector solutions; // map not used b/c may have multiple STDDEV entries }; struct Sinex { - string currentFile; - - /* header block */ - 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; - 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; - - map> blockComments; - list refstrings; - list inputHistory; - list inputFiles; - list acknowledgements; - - /* site stuff */ - map mapsiteids; - list listsitedata; - map>> mapreceivers; - map>> mapantennas; - map>> mapeccentricities; - list listgpspcs; - list listgalpcs; - - /* solution stuff - tied to sites */ - bool epochshavebias; - map solEpochMap; -// list list_solepochs; - list liststatistics; - map>>> estimatesMap; - map apriorimap; - list listnormaleqns; - map> matrixmap[MAX_MATRIX_TYPE]; - 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; - - // 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; + string currentFile; + + /* header block */ + 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; + 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; + + map> blockComments; + list refstrings; + list inputHistory; + list inputFiles; + list acknowledgements; + + /* site stuff */ + map mapsiteids; + list listsitedata; + map>> mapreceivers; + map>> mapantennas; + map>> mapeccentricities; + list listgpspcs; + list listgalpcs; + + /* solution stuff - tied to sites */ + bool epochshavebias; + map solEpochMap; + // list list_solepochs; + list liststatistics; + map>>> estimatesMap; + map apriorimap; + list listnormaleqns; + map> matrixmap[MAX_MATRIX_TYPE]; + 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; + + // 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 */ + 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 SinexSatIdentity dummySinexSatIdentity; -extern SinexSatEcc dummySinexSatEcc; +extern SinexSatIdentity dummySinexSatIdentity; +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); */ + 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); */ - string antenna; - int numfreqs; /* number of phase center frequencies */ - char freq[5]; /* 1/2/5/6/7/8 (up to 5 freqs allowed) */ - Vector3d zxy[5]; /* phase offsets, order given by var name! */ - char pctype; - char pcmodel; + string antenna; + int numfreqs; /* number of phase center frequencies */ + char freq[5]; /* 1/2/5/6/7/8 (up to 5 freqs allowed) */ + Vector3d zxy[5]; /* phase offsets, order given by var name! */ + char pctype; + char pcmodel; - UYds start; - UYds stop; + UYds start; + UYds stop; }; +void nearestYear(double& year); -void nearestYear( - double& year); - -bool readSinex( - const string& filepath); +bool readSinex(const string& filepath); struct KFState; struct Receiver; -void writeSinex( - string filepath, - KFState& kfState, - map& receiverMap); +void writeSinex(string filepath, KFState& kfState, map& receiverMap); struct SinexRecData; union GetSnxResult { - const unsigned int failure = 0; - struct - { - unsigned failureSiteId : 1; - unsigned failureReceiver : 1; - unsigned failureAntenna : 1; - unsigned failureEccentricity : 1; - unsigned failurePhaseCentre : 1; - unsigned failureEstimate : 1; - unsigned failurePRN : 1; - unsigned failureSatId : 1; - unsigned failureCOM : 1; - }; -}; - - -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); - -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); + const unsigned int failure = 0; + struct + { + unsigned failureSiteId : 1; + unsigned failureReceiver : 1; + unsigned failureAntenna : 1; + unsigned failureEccentricity : 1; + unsigned failurePhaseCentre : 1; + unsigned failureEstimate : 1; + unsigned failurePRN : 1; + unsigned failureSatId : 1; + unsigned failureCOM : 1; + }; +}; + +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); + +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); - -void sinexPostProcessing( - GTime time, - map& receiverMap, - KFState& netKFState); - -void sinexPerEpochPerStation( - Trace& trace, - GTime time, - Receiver& rec); + 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 +); +void sinexPostProcessing(GTime time, map& receiverMap, KFState& netKFState); + +void sinexPerEpochPerStation(Trace& trace, GTime time, Receiver& rec); // Trop sinex void outputTropSinex( - string filename, - GTime time, - KFState& netKfState, - string markerName = "MIX", - bool isSmoothed = false); + 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); +void writeAsComments(Trace& out, list& comments); -bool getSnxSatMaxYawRate( - string prn, - GTime& time, - double& maxYawRate); +void writeSnxReference(std::ofstream& out); -bool getSnxSatBlockType( - string svn, - string& blockType); +bool getSnxSatMaxYawRate(string prn, GTime& time, double& maxYawRate); -bool getSnxSatAttMode( - string svn, - GTime& time, - string& attMode); +bool getSnxSatBlockType(string svn, string& blockType); -extern Sinex theSinex; // the one and only sinex object. +bool getSnxSatAttMode(string svn, GTime& time, string& attMode); -void getReceiversFromSinex( - map& receiverMap, - KFState& kfState); +extern Sinex theSinex; // the one and only sinex object. +void getReceiversFromSinex(map& receiverMap, KFState& kfState); diff --git a/src/cpp/common/sinexParser.cpp b/src/cpp/common/sinexParser.cpp index 57abc0708..106430034 100644 --- a/src/cpp/common/sinexParser.cpp +++ b/src/cpp/common/sinexParser.cpp @@ -1,162 +1,165 @@ - // #pragma GCC optimize ("O0") -#include "sinexParser.hpp" -#include "receiver.hpp" - +#include "common/sinexParser.hpp" #include +#include "common/receiver.hpp" using std::string; -void SinexParser::parseSinexEstimates( - string& s) +void SinexParser::parseSinexEstimates(string& s) { - KFKey kfKey; + KFKey kfKey; - string type = s.substr(7, 6); + string type = s.substr(7, 6); - if (type == "STAX ") { kfKey.num = 0; kfKey.type = KF::REC_POS; } - else if (type == "STAY ") { kfKey.num = 1; kfKey.type = KF::REC_POS; } - else if (type == "STAZ ") { kfKey.num = 2; kfKey.type = KF::REC_POS; } + if (type == "STAX ") + { + kfKey.num = 0; + kfKey.type = KF::REC_POS; + } + else if (type == "STAY ") + { + kfKey.num = 1; + kfKey.type = KF::REC_POS; + } + else if (type == "STAZ ") + { + kfKey.num = 2; + kfKey.type = KF::REC_POS; + } - if (kfKey.type != KF::REC_POS) - { - return; - } + if (kfKey.type != KF::REC_POS) + { + return; + } - kfKey.str = s.substr(14, 4); - kfKey.rec_ptr = &receiverMap[kfKey.str]; + kfKey.str = s.substr(14, 4); + kfKey.rec_ptr = &receiverMap[kfKey.str]; - int parameterNumber = atoi(s.substr(1, 5).c_str()); + int parameterNumber = atoi(s.substr(1, 5).c_str()); - UYds yds; - int readcount; - readcount = sscanf(s.c_str() + 27, "%2lf:%3lf:%5lf", - &yds[0], - &yds[1], - &yds[2]); + UYds yds; + int readcount; + readcount = sscanf(s.c_str() + 27, "%2lf:%3lf:%5lf", &yds[0], &yds[1], &yds[2]); - if (readcount != 3) - { - return; - } + if (readcount != 3) + { + return; + } - if ( yds[0] != 0 - ||yds[1] != 0 - ||yds[2] != 0) - { - nearestYear(yds[0]); - } + if (yds[0] != 0 || yds[1] != 0 || yds[2] != 0) + { + nearestYear(yds[0]); + } - double x; - double P0; + double x; + double P0; - readcount = sscanf(s.c_str() + 47, "%21lf %11lf", - &x, - &P0); + readcount = sscanf(s.c_str() + 47, "%21lf %11lf", &x, &P0); - if (readcount != 2) - { - return; - } + if (readcount != 2) + { + return; + } - GTime time = yds; + GTime time = yds; - parameterMap[parameterNumber] = {time, kfKey}; + parameterMap[parameterNumber] = {time, kfKey}; - auto& [value, covMap] = valueMap[time][kfKey]; + auto& [value, covMap] = valueMap[time][kfKey]; - value = x; - covMap[kfKey] = P0; + value = x; + covMap[kfKey] = P0; } -void SinexParser::parseSinexEstimateMatrix( - string& s) +void SinexParser::parseSinexEstimateMatrix(string& s) { - int row; - int col0; - double values[3]; - int readcount = sscanf(s.c_str(), " %5d %5d %21lf %21lf %21lf", - &row, - &col0, - &values[0], - &values[1], - &values[2]); - - if (readcount < 3) - { - return; - } - - int covars = readcount - 2; - - for (int i = 0; i < covars; i++) - { - auto& [timeA, kfKeyA] = parameterMap[row]; - auto& [timeB, kfKeyB] = parameterMap[col0 + i]; - - if (row != col0 + i) - { -// continue; - } - - if ( timeA.bigTime == 0 - || timeB.bigTime == 0) - { - continue; - } - - auto& [valueA, covMapA] = valueMap[timeA][kfKeyA]; covMapA[kfKeyB] = values[i]; - auto& [valueB, covMapB] = valueMap[timeB][kfKeyB]; covMapB[kfKeyA] = values[i]; - } + int row; + int col0; + double values[3]; + int readcount = sscanf( + s.c_str(), + " %5d %5d %21lf %21lf %21lf", + &row, + &col0, + &values[0], + &values[1], + &values[2] + ); + + if (readcount < 3) + { + return; + } + + int covars = readcount - 2; + + for (int i = 0; i < covars; i++) + { + auto& [timeA, kfKeyA] = parameterMap[row]; + auto& [timeB, kfKeyB] = parameterMap[col0 + i]; + + if (row != col0 + i) + { + // continue; + } + + if (timeA.bigTime == 0 || timeB.bigTime == 0) + { + continue; + } + + auto& [valueA, covMapA] = valueMap[timeA][kfKeyA]; + covMapA[kfKeyB] = values[i]; + auto& [valueB, covMapB] = valueMap[timeB][kfKeyB]; + covMapB[kfKeyA] = values[i]; + } } -void SinexParser::parseSinexDiscontinuities( - string& s) +void SinexParser::parseSinexDiscontinuities(string& s) { - // 0194 A 1 P 00:000:00000 03:160:00000 P - antenna change - DiscontinuityObject dObj; - - dObj.sitecode = s.substr(1, 4); - dObj.monuid = s[7]; - dObj.solution = atoi(s.substr(9, 4).c_str()); - dObj.isVelocity = s[42] == 'V'; - - UYds startYds; - UYds endYds; - - int readcount; - readcount = sscanf(s.c_str() + 16, "%2lf:%3lf:%5lf %2lf:%3lf:%5lf", - &startYds[0], - &startYds[1], - &startYds[2], - &endYds[0], - &endYds[1], - &endYds[2]); - - if (readcount != 6) - { - return; - } - - if ( startYds[0] != 0 - ||startYds[1] != 0 - ||startYds[2] != 0) - { - nearestYear(startYds[0]); - } - - if ( endYds[0] != 0 - ||endYds[1] != 0 - ||endYds[2] != 0) - { - nearestYear(endYds[0]); - } - - dObj.start = startYds; - dObj.end = endYds; - - GTime time = startYds; - - discontinuityMap[time][dObj.sitecode] = dObj; + // 0194 A 1 P 00:000:00000 03:160:00000 P - antenna change + DiscontinuityObject dObj; + + dObj.sitecode = s.substr(1, 4); + dObj.monuid = s[7]; + dObj.solution = atoi(s.substr(9, 4).c_str()); + dObj.isVelocity = s[42] == 'V'; + + UYds startYds; + UYds endYds; + + int readcount; + readcount = sscanf( + s.c_str() + 16, + "%2lf:%3lf:%5lf %2lf:%3lf:%5lf", + &startYds[0], + &startYds[1], + &startYds[2], + &endYds[0], + &endYds[1], + &endYds[2] + ); + + if (readcount != 6) + { + return; + } + + if (startYds[0] != 0 || startYds[1] != 0 || startYds[2] != 0) + { + nearestYear(startYds[0]); + } + + if (endYds[0] != 0 || endYds[1] != 0 || endYds[2] != 0) + { + nearestYear(endYds[0]); + } + + dObj.start = startYds; + dObj.end = endYds; + + GTime time = startYds; + + discontinuityMap[time][dObj.sitecode] = dObj; } diff --git a/src/cpp/common/sinexParser.hpp b/src/cpp/common/sinexParser.hpp index d894650d1..fb6385e25 100644 --- a/src/cpp/common/sinexParser.hpp +++ b/src/cpp/common/sinexParser.hpp @@ -1,19 +1,15 @@ - #pragma once - -#include +#include #include +#include #include #include - -#include - -#include "eigenIncluder.hpp" -#include "streamObs.hpp" -#include "algebra.hpp" -#include "gTime.hpp" -#include "trace.hpp" +#include "common/algebra.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/streamObs.hpp" +#include "common/trace.hpp" using std::getline; using std::ifstream; @@ -22,198 +18,196 @@ using std::tuple; struct DiscontinuityObject { - string sitecode; // site code - string monuid; // monument identification - int solution = 0; // valid solution number - // a P letter, no idea what it stands for ... - UYds start; - UYds end; - bool isVelocity = false; // position/velocity switch + string sitecode; // site code + string monuid; // monument identification + int solution = 0; // valid solution number + // a P letter, no idea what it stands for ... + UYds start; + UYds end; + bool isVelocity = false; // position/velocity switch }; struct SinexParser : Parser, ObsLister { - map> parameterMap; - map>>> valueMap; - map> discontinuityMap; - - // 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 - // cease to work safely around 2045! - // when we write years, write out modulo 100 - // This only applies to site data, for satellites it is using 4 digit years - void nearestYear( - double& year) - { - if (year > 50) year += 1900; - else year += 2000; - } - - void nullFunction( - string& s) - { - - } - - void parseSinexEstimates( - string& s); - - void parseSinexEstimateMatrix( - string& s); - - void parseSinexDiscontinuities( - string& s); - - void parse( - std::istream& inputStream) - { - string closure; - void (SinexParser::*parseFunction)(string&) = &SinexParser::nullFunction; - - if (!inputStream) - { - return; - } - - while (inputStream) - { - string line; - - getline(inputStream, line); - - // test below empty line (ie continue if something on the line) - if (!inputStream) - { - // error - did not find closure line. Report and clean up. - BOOST_LOG_TRIVIAL(error) - << "Error: Closure line not found before end."; - - break; - } - - if (line[0] == '*') - { - //comment - continue; - } - - if (line[0] == '-') - { - //end of block - parseFunction = &SinexParser::nullFunction; - - if (line != closure) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Incorrect section closure line encountered: " - << closure << " != " << line; - } - - closure = ""; - - continue; - } - - if (line[0] == ' ') - { - //this probably needs specialty parsing - use a prepared function pointer. - (this->*parseFunction)(line); - - continue; - } - - if (line[0] == '+') - { - string mvs; - - //prepare closing line for comparison - closure = line; - closure[0] = '-'; - string lineName = line.substr(0, line.find(' ')); - if (lineName == "+SOLUTION/ESTIMATE" ) { parseFunction = &SinexParser::parseSinexEstimates; } - else if (lineName == "+SOLUTION/DISCONTINUITY" ) { parseFunction = &SinexParser::parseSinexDiscontinuities; } - else if (lineName == "+SOLUTION/MATRIX_ESTIMATE" ) { parseFunction = &SinexParser::parseSinexEstimateMatrix; } - else - { - // BOOST_LOG_TRIVIAL(error) - // << "Error: error unknown header line: " << line; - } - - continue; - } - - if (line[0] == '%') - { - if (line.substr(0, 5) == "%=SNX") - { - continue; - } - - if (line != "%ENDSNX") - { - // error in file. report it. - BOOST_LOG_TRIVIAL(error) - << "Error: line starting '%' met not final line" << "\n" << line; - - return; - } - - continue; - } - } - - //aggregate all maps into an obsList type entity - for (auto& [time, someMap] : valueMap) - { - //make an Observation. - FObs obs; - obs.time = time; - - int index = 0; - - obs.obsState.kfIndexMap[KFState::oneKey] = index; - - index++; - - for (auto& [key, tuplet] : someMap) - { - obs.obsState.kfIndexMap[key] = index; - obs.obsState.stateTransitionMap[key][key][0] = 1; - - index++; - } - - obs.obsState.x = VectorXd::Zero(index); -// obs.obsState.Z = MatrixXd::Zero(index,index); - obs.obsState.P = MatrixXd::Zero(index,index); - - for (auto& [keyA, tuplet] : someMap) - { - auto indexA = obs.obsState.kfIndexMap[keyA]; - - auto& [value, covMap] = tuplet; - - obs.obsState.x(indexA) = value; - - for (auto& [keyB, cov] : covMap) - { - auto indexB = obs.obsState.kfIndexMap[keyB]; - - obs.obsState.P(indexA, indexB) = cov; - } - } - - ObsList obsList; - - obsList.push_back((shared_ptr)obs); - - std::cout << "Got obs for " << obs.time << "\n"; - obsListList.push_back(std::move(obsList)); - } - } - - string parserType() - { - return "SinexParser"; - } + map> parameterMap; + map>>> valueMap; + map> discontinuityMap; + + // 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 + // cease to work safely around 2045! + // when we write years, write out modulo 100 + // This only applies to site data, for satellites it is using 4 digit years + void nearestYear(double& year) + { + if (year > 50) + year += 1900; + else + year += 2000; + } + + void nullFunction(string& s) {} + + void parseSinexEstimates(string& s); + + void parseSinexEstimateMatrix(string& s); + + void parseSinexDiscontinuities(string& s); + + void parse(std::istream& inputStream) + { + string closure; + void (SinexParser::*parseFunction)(string&) = &SinexParser::nullFunction; + + if (!inputStream) + { + return; + } + + while (inputStream) + { + string line; + + getline(inputStream, line); + + // test below empty line (ie continue if something on the line) + if (!inputStream) + { + // error - did not find closure line. Report and clean up. + BOOST_LOG_TRIVIAL(error) << "Closure line not found before end."; + + break; + } + + if (line[0] == '*') + { + // comment + continue; + } + + if (line[0] == '-') + { + // end of block + parseFunction = &SinexParser::nullFunction; + + if (line != closure) + { + BOOST_LOG_TRIVIAL(error) + << "Incorrect section closure line encountered: " << closure + << " != " << line; + } + + closure = ""; + + continue; + } + + if (line[0] == ' ') + { + // this probably needs specialty parsing - use a prepared function pointer. + (this->*parseFunction)(line); + + continue; + } + + if (line[0] == '+') + { + string mvs; + + // prepare closing line for comparison + closure = line; + closure[0] = '-'; + string lineName = line.substr(0, line.find(' ')); + if (lineName == "+SOLUTION/ESTIMATE") + { + parseFunction = &SinexParser::parseSinexEstimates; + } + else if (lineName == "+SOLUTION/DISCONTINUITY") + { + parseFunction = &SinexParser::parseSinexDiscontinuities; + } + else if (lineName == "+SOLUTION/MATRIX_ESTIMATE") + { + parseFunction = &SinexParser::parseSinexEstimateMatrix; + } + else + { + // BOOST_LOG_TRIVIAL(error) + // << "Error unknown header line: " << line; + } + + continue; + } + + if (line[0] == '%') + { + if (line.substr(0, 5) == "%=SNX") + { + continue; + } + + if (line != "%ENDSNX") + { + // error in file. report it. + BOOST_LOG_TRIVIAL(error) << "Line starting '%' met not final line" << "\n" + << line; + + return; + } + + continue; + } + } + + // aggregate all maps into an obsList type entity + for (auto& [time, someMap] : valueMap) + { + // make an Observation. + FObs obs; + obs.time = time; + + int index = 0; + + obs.obsState.kfIndexMap[KFState::oneKey] = index; + + index++; + + for (auto& [key, tuplet] : someMap) + { + obs.obsState.kfIndexMap[key] = index; + obs.obsState.stateTransitionMap[key][key][0] = 1; + + index++; + } + + obs.obsState.x = VectorXd::Zero(index); + // obs.obsState.Z = MatrixXd::Zero(index,index); + obs.obsState.P = MatrixXd::Zero(index, index); + + for (auto& [keyA, tuplet] : someMap) + { + auto indexA = obs.obsState.kfIndexMap[keyA]; + + auto& [value, covMap] = tuplet; + + obs.obsState.x(indexA) = value; + + for (auto& [keyB, cov] : covMap) + { + auto indexB = obs.obsState.kfIndexMap[keyB]; + + obs.obsState.P(indexA, indexB) = cov; + } + } + + ObsList obsList; + + obsList.push_back((shared_ptr)obs); + + std::cout << "Got obs for " << obs.time << "\n"; + obsListList.push_back(std::move(obsList)); + } + } + + string parserType() { return "SinexParser"; } }; diff --git a/src/cpp/common/sp3.cpp b/src/cpp/common/sp3.cpp index b79bb920d..f18347aa7 100644 --- a/src/cpp/common/sp3.cpp +++ b/src/cpp/common/sp3.cpp @@ -1,353 +1,356 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -/** - */ -FileType SP3__() -{ - -} - +#include #include #include +#include "architectureDocs.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/trace.hpp" using std::string; -#include - -#include "eigenIncluder.hpp" - -#include "navigation.hpp" -#include "common.hpp" -#include "trace.hpp" -#include "gTime.hpp" -#include "enums.h" - +/** + */ +FileType SP3__() {} /** satellite code to satellite system -*/ + */ E_Sys code2sys(char code) { - if (code=='G'||code==' ') return E_Sys::GPS; - if (code=='R') return E_Sys::GLO; - if (code=='E') return E_Sys::GAL; /* extension to sp3-c */ - if (code=='J') return E_Sys::QZS; /* extension to sp3-c */ - if (code=='C') return E_Sys::BDS; /* extension to sp3-c */ - if (code=='L') return E_Sys::LEO; /* extension to sp3-c */ - return E_Sys::NONE; + if (code == 'G' || code == ' ') + return E_Sys::GPS; + if (code == 'R') + return E_Sys::GLO; + if (code == 'E') + return E_Sys::GAL; /* extension to sp3-c */ + if (code == 'J') + return E_Sys::QZS; /* extension to sp3-c */ + if (code == 'C') + return E_Sys::BDS; /* extension to sp3-c */ + if (code == 'L') + return E_Sys::LEO; /* extension to sp3-c */ + return E_Sys::NONE; } /** read an epoch of data from an sp3 precise ephemeris file */ bool readsp3( - std::istream& fileStream, ///< stream to read content from - vector& pephList, ///< vector of precise ephemerides for one epoch - int opt, ///< options options (1: only observed + 2: only predicted + 4: not combined) - E_TimeSys& tsys, ///< time system - double* bfact) ///< bfact values from header + std::istream& fileStream, ///< stream to read content from + vector& pephList, ///< vector of precise ephemerides for one epoch + int opt, ///< options options (1: only observed + 2: only predicted + 4: not combined) + E_TimeSys& tsys, ///< time system + double* bfact ///< bfact values from header +) { - GTime time = {}; - - //fprintf(stdout,"\n SP3READ: Expanded %s to %d files\n",file,n); - - //keep track of file number - static int index = 0; - index++; - - - int hashCount = 0; - int cCount = 0; - int fCount = 0; - - bool epochFound = false; - string line; - while (fileStream) - { - //return early when an epoch is complete - int peek = fileStream.peek(); - if ( peek == '*' - && epochFound) - { - return true; - } - - getline(fileStream, line); - - char* buff = &line[0]; - - if (buff[0] == '*') - { - //epoch line - epochFound = true; - - bool error = str2time(buff, 3, 28, time, tsys); - if (error) - { - printf("\nInvalid epoch line in sp3 file %s\n", line.c_str()); - return false; - } - - continue; - } - - if (buff[0] == 'P') - { - //position line - bool pred_p = false; - bool pred_c = false; - - E_Sys sys = code2sys(buff[1]); - int prn = (int)str2num(buff, 2, 2); - - SatSys Sat(sys, prn); - if (!Sat) - continue; - - Peph peph = {}; - peph.time = time; - peph.index = index; - peph.Sat = Sat; - bool valid = true; - - if (buff[0] == 'P') - { - pred_c = strlen(buff)>=76 && buff[75]=='P'; - pred_p = strlen(buff)>=80 && buff[79]=='P'; - } - - //positions/rates - for (int j = 0; j < 3; j++) - { - /* read option for predicted value */ - if (j < 3 && (opt&1) && pred_p) continue; - if (j < 3 && (opt&2) && !pred_p) continue; - - double val = str2num(buff, 4 + j * 14, 14); - double std = str2num(buff,61 + j * 3, 2); - - if (buff[0]=='P') - { - /* position */ - if ( val != 0) - { - peph.pos[j] = val * 1000; - } - else - { - valid = false; - } - - double base = bfact[0]; - if ( base > 0 - && std > 0) - { - peph.posStd[j] = pow(base, std) * 1E-3; - } - } - else if (valid) - { -// /* velocity */ -// if ( val !=0) -// { -// peph.vel[j] = val * 0.1; -// } -// -// double base = bfact[j < 3 ? 0 : 1]; -// if ( base > 0 -// && std > 0) -// { -// peph.velStd[j] = pow(base, std) * 1E-7; -// } - } - } - - //clocks / rates - for (int j = 3; j < 4; j++) - { - /* read option for predicted value */ - if (j == 3 && (opt&1) && pred_c) continue; - if (j == 3 && (opt&2) && !pred_c) continue; - - string checkValue; - checkValue.assign(buff + 4 + j * 14, 7); - double val = str2num(buff, 4 + j * 14, 14); - double std = str2num(buff,61 + j * 3, 3); - - if (buff[0] == 'P') - { - /* clock */ - if ( val != 0 - &&checkValue != " 999999") - { - peph.clk = val * 1E-6; - } - else - { - peph.clk = INVALID_CLOCK_VALUE; -// valid = false; //allow clocks to be invalid - } - - double base = bfact[1]; - if ( base > 0 - && std > 0) - { - peph.clkStd = pow(base, std) * 1E-12; - } - } - else if (valid) - { -// /* clock rate */ -// if ( val !=0 -// &&fabs(val) < NO_SP3_CLK) -// { -// peph.dCk = val * 1E-10; -// } -// -// double base = bfact[j < 3 ? 0 : 1]; -// if ( base > 0 -// && std > 0) -// { -// peph.dCkStd = pow(base, std) * 1E-16; -// } - } - } - - if (valid) - { - pephList.push_back(peph); - } - - continue; - } - /* - Quick and dirty read of the velocities - @todo change later. - */ - if (buff[0] == 'V') - { - for (int i=0; i < 3; i++) - { - double val = str2num(buff, 4 + i * 14, 14); - pephList.back().vel[i] = val * 0.1; - } - } - if (buff[0] == '#') - { - hashCount++; - - if (hashCount == 1) - { - //first line is time and type -// type = buff[2]; - int error = str2time(buff, 3, 28, time); // time system unknown at beginning but does not matter - if (error) - return false; - - continue; - } - } - - string twoChars = line.substr(0,2); - -// if (twoChars == "+ ") -// { -// plusCount++; -// //number and list of satellites included in the file - information only, sat ids are included in epoch lines.. -// if (lineNum == 2) -// { -// ns = (int)str2num(buff,4,2); -// } -// for (int j = 0; j < 17 && k < ns; j++) -// { -// E_Sys sys=code2sys(buff[9+3*j]); -// -// int prn = (int)str2num(buff,10+3*j,2); -// -// if (k < MAXSAT) -// sats[k++] = SatSys(sys, prn); -// } -// continue; -// } - -// if (twoChars == "++") -// { -// pplusCount++; -// continue; -// } - - if (twoChars == "%c") - { - cCount++; - - if (cCount == 1) - { - string timeSysStr = line.substr(9, 3); - - if (timeSysStr == "GPS") tsys = E_TimeSys::GPST; - else if (timeSysStr == "GLO") tsys = E_TimeSys::GLONASST; - else if (timeSysStr == "GAL") tsys = E_TimeSys::GST; - else if (timeSysStr == "QZS") tsys = E_TimeSys::QZSST; - else if (timeSysStr == "TAI") tsys = E_TimeSys::TAI; - else if (timeSysStr == "UTC") tsys = E_TimeSys::UTC; - else - { - BOOST_LOG_TRIVIAL(error) - << "Unknown sp3 time system: " << timeSysStr; - return false; - } - } - continue; - } - - if (twoChars == "%f") - { - fCount++; - - if (fCount == 1) - { - bfact[0] = str2num(buff, 3,10); - bfact[1] = str2num(buff,14,12); - } - continue; - } - - if (line.substr(0,3) == "EOF") - { - //all done - return true; - } - } - -// printf("\nDidnt find eof in sp3 file\n"); - return false; + GTime time = {}; + + // fprintf(stdout,"\n SP3READ: Expanded %s to %d files\n",file,n); + + // keep track of file number + static int index = 0; + index++; + + int hashCount = 0; + int cCount = 0; + int fCount = 0; + + bool epochFound = false; + string line; + while (fileStream) + { + // return early when an epoch is complete + int peek = fileStream.peek(); + if (peek == '*' && epochFound) + { + return true; + } + + getline(fileStream, line); + + char* buff = &line[0]; + + if (buff[0] == '*') + { + // epoch line + epochFound = true; + + bool error = str2time(buff, 3, 28, time, tsys); + if (error) + { + printf("\nInvalid epoch line in sp3 file %s\n", line.c_str()); + return false; + } + + continue; + } + + if (buff[0] == 'P') + { + // position line + bool pred_p = false; + bool pred_c = false; + + E_Sys sys = code2sys(buff[1]); + int prn = (int)str2num(buff, 2, 2); + + SatSys Sat(sys, prn); + if (!Sat) + continue; + + Peph peph = {}; + peph.time = time; + peph.index = index; + peph.Sat = Sat; + bool valid = true; + + if (buff[0] == 'P') + { + pred_c = strlen(buff) >= 76 && buff[75] == 'P'; + pred_p = strlen(buff) >= 80 && buff[79] == 'P'; + } + + // positions/rates + for (int j = 0; j < 3; j++) + { + /* read option for predicted value */ + if (j < 3 && (opt & 1) && pred_p) + continue; + if (j < 3 && (opt & 2) && !pred_p) + continue; + + double val = str2num(buff, 4 + j * 14, 14); + double std = str2num(buff, 61 + j * 3, 2); + + if (buff[0] == 'P') + { + /* position */ + if (val != 0) + { + peph.pos[j] = val * 1000; + } + else + { + valid = false; + } + + double base = bfact[0]; + if (base > 0 && std > 0) + { + peph.posStd[j] = pow(base, std) * 1E-3; + } + } + else if (valid) + { + // /* velocity */ + // if ( val !=0) + // { + // peph.vel[j] = val * 0.1; + // } + // + // double base = bfact[j < 3 ? 0 : 1]; + // if ( base > 0 + // && std > 0) + // { + // peph.velStd[j] = pow(base, std) * 1E-7; + // } + } + } + + // clocks / rates + for (int j = 3; j < 4; j++) + { + /* read option for predicted value */ + if (j == 3 && (opt & 1) && pred_c) + continue; + if (j == 3 && (opt & 2) && !pred_c) + continue; + + string checkValue; + checkValue.assign(buff + 4 + j * 14, 7); + double val = str2num(buff, 4 + j * 14, 14); + double std = str2num(buff, 61 + j * 3, 3); + + if (buff[0] == 'P') + { + /* clock */ + if (val != 0 && checkValue != " 999999") + { + peph.clk = val * 1E-6; + } + else + { + peph.clk = INVALID_CLOCK_VALUE; + // valid = false; //allow clocks to be invalid + } + + double base = bfact[1]; + if (base > 0 && std > 0) + { + peph.clkStd = pow(base, std) * 1E-12; + } + } + else if (valid) + { + // /* clock rate */ + // if ( val !=0 + // &&fabs(val) < NO_SP3_CLK) + // { + // peph.dCk = val * 1E-10; + // } + // + // double base = bfact[j < 3 ? 0 : 1]; + // if ( base > 0 + // && std > 0) + // { + // peph.dCkStd = pow(base, std) * 1E-16; + // } + } + } + + if (valid) + { + pephList.push_back(peph); + } + + continue; + } + /* + Quick and dirty read of the velocities + @todo change later. + */ + if (buff[0] == 'V') + { + for (int i = 0; i < 3; i++) + { + double val = str2num(buff, 4 + i * 14, 14); + pephList.back().vel[i] = val * 0.1; + } + } + if (buff[0] == '#') + { + hashCount++; + + if (hashCount == 1) + { + // first line is time and type + // type = buff[2]; + int error = str2time( + buff, + 3, + 28, + time + ); // time system unknown at beginning but does not matter + if (error) + return false; + + continue; + } + } + + string twoChars = line.substr(0, 2); + + // if (twoChars == "+ ") + // { + // plusCount++; + // //number and list of satellites included in the file - information only, sat ids + // are included in epoch lines.. if (lineNum == 2) + // { + // ns = (int)str2num(buff,4,2); + // } + // for (int j = 0; j < 17 && k < ns; j++) + // { + // E_Sys sys=code2sys(buff[9+3*j]); + // + // int prn = (int)str2num(buff,10+3*j,2); + // + // if (k < MAXSAT) + // sats[k++] = SatSys(sys, prn); + // } + // continue; + // } + + // if (twoChars == "++") + // { + // pplusCount++; + // continue; + // } + + if (twoChars == "%c") + { + cCount++; + + if (cCount == 1) + { + string timeSysStr = line.substr(9, 3); + + if (timeSysStr == "GPS") + tsys = E_TimeSys::GPST; + else if (timeSysStr == "GLO") + tsys = E_TimeSys::GLONASST; + else if (timeSysStr == "GAL") + tsys = E_TimeSys::GST; + else if (timeSysStr == "QZS") + tsys = E_TimeSys::QZSST; + else if (timeSysStr == "TAI") + tsys = E_TimeSys::TAI; + else if (timeSysStr == "UTC") + tsys = E_TimeSys::UTC; + else + { + BOOST_LOG_TRIVIAL(error) << "Unknown SP3 time system: " << timeSysStr; + return false; + } + } + continue; + } + + if (twoChars == "%f") + { + fCount++; + + if (fCount == 1) + { + bfact[0] = str2num(buff, 3, 10); + bfact[1] = str2num(buff, 14, 12); + } + continue; + } + + if (line.substr(0, 3) == "EOF") + { + // all done + return true; + } + } + + // printf("\nDidnt find eof in sp3 file\n"); + return false; } - -void readSp3ToNav( - string& file, - Navigation& nav, - int opt) +void readSp3ToNav(string& file, Navigation& nav, int opt) { - std::ifstream fileStream(file); - if (!fileStream) - { - printf("\nSp3 file open error %s\n", file.c_str()); - return; - } - - vector pephList; - - E_TimeSys tsys = E_TimeSys::NONE; - double bfact[2] = {}; - while (readsp3(fileStream, pephList, opt, tsys, bfact)) - { - //keep reading until it fails - for (auto& peph : pephList) - { - nav.pephMap[peph.Sat.id()][peph.time] = peph; - } - pephList.clear(); - } + std::ifstream fileStream(file); + if (!fileStream) + { + printf("\nSp3 file open error %s\n", file.c_str()); + return; + } + + vector pephList; + + E_TimeSys tsys = E_TimeSys::NONE; + double bfact[2] = {}; + while (readsp3(fileStream, pephList, opt, tsys, bfact)) + { + // keep reading until it fails + for (auto& peph : pephList) + { + nav.pephMap[peph.Sat.id()][peph.time] = peph; + } + pephList.clear(); + } } diff --git a/src/cpp/common/sp3Write.cpp b/src/cpp/common/sp3Write.cpp index 169674916..2e9c99ffe 100644 --- a/src/cpp/common/sp3Write.cpp +++ b/src/cpp/common/sp3Write.cpp @@ -1,407 +1,481 @@ - // #pragma GCC optimize ("O0") - +#include "common/sp3Write.hpp" #include - - -#include "rinexObsWrite.hpp" -#include "rinexClkWrite.hpp" -#include "coordinates.hpp" -#include "GNSSambres.hpp" -#include "navigation.hpp" -#include "ephemeris.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "mongoRead.hpp" -#include "sp3Write.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "enums.h" -#include "erp.hpp" +#include "ambres/GNSSambres.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/ephemeris.hpp" +#include "common/erp.hpp" +#include "common/mongoRead.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/rinexClkWrite.hpp" +#include "common/rinexObsWrite.hpp" +#include "orbprop/coordinates.hpp" /* macro defintions */ -#define VERSION 3.00 +constexpr double VERSION = 3.00; struct Sp3Entry { - SatSys Sat; - Vector3d satPos = Vector3d::Zero(); // Satellite position. - Vector3d satVel = Vector3d::Zero(); // Satellite velocity. - double satClk = INVALID_CLOCK_VALUE / 1e6; - double satClkVel = INVALID_CLOCK_VALUE / 1e10; - double sigma = 0; - bool predicted = false; + SatSys Sat; + Vector3d satPos = Vector3d::Zero(); // Satellite position. + Vector3d satVel = Vector3d::Zero(); // Satellite velocity. + double satClk = INVALID_CLOCK_VALUE / 1e6; + double satClkVel = INVALID_CLOCK_VALUE / 1e10; + double sigma = 0; + bool predicted = false; }; - struct Sp3FileData { - map sats; - long numEpoch = 0; - long numEpoch_pos = 0; + map sats; + long numEpoch = 0; + long numEpoch_pos = 0; }; map sp3FileDataMap; void writeSp3Header( - std::fstream& sp3Stream, - map& entryList, - GTime time, - map& outSys, - Sp3FileData& sp3FileData) + std::fstream& sp3Stream, + map& entryList, + GTime time, + map& outSys, + Sp3FileData& sp3FileData +) { - GEpoch ep = time; - - sp3FileData = {}; - sp3FileData.numEpoch = 1; - - // note "#dV" for velocity and position. - char velPos; - - if (acsConfig.output_sp3_velocities) velPos = 'V'; - else velPos = 'P'; - - tracepdeex(0, sp3Stream, "#d%c%4.0f %2.0f %2.0f %2.0f %2.0f %11.8f ", velPos, ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - sp3FileData.numEpoch_pos = sp3Stream.tellp(); - - //TODO Check, coordinate system and Orbit Type from example product file. - tracepdeex(0, sp3Stream, "%7d ORBIT IGS14 FIT %4s\n", sp3FileData.numEpoch, acsConfig.analysis_agency.c_str()); - - GWeek week = time; - GTow tow = time; - MjDateTT mjdate = time; - - tracepdeex(0, sp3Stream, "## %4d %15.8f %14.8f %5.0f %015.13f\n", - week, - tow, - acsConfig.sp3_output_interval, - mjdate.to_double(), - 0.0); - - for (auto sys : {E_Sys::GPS, E_Sys::GLO, E_Sys::GAL, E_Sys::BDS, E_Sys::LEO}) - { - if (outSys[sys]) - for (auto Sat : getSysSats(sys)) - { - auto satOpts = acsConfig.getSatOpts(Sat); - - if (satOpts.exclude) - continue; - - sp3FileData.sats[Sat] = true; - } - } - - int lineNumber = 1; - int lineEntries = 0; - tracepdeex(0, sp3Stream, "+ %3d ", sp3FileData.sats.size()); - for (auto& [Sat, enable] : sp3FileData.sats) - { - if (lineEntries == 17) - { - //start a new line - tracepdeex(0, sp3Stream, "\n+ "); - lineEntries = 0; - lineNumber++; - } - tracepdeex(0, sp3Stream, "%3s", Sat.id().c_str()); - - lineEntries++; - } - - while ( lineNumber < 17 - ||lineEntries < 17) - { - //keep adding entries and lines until we reach the minimum - - if (lineEntries == 17) - { - //start a new line - tracepdeex(0, sp3Stream, "\n+ "); - lineEntries = 0; - lineNumber++; - } - tracepdeex(0, sp3Stream, "%3s", "0"); - - lineEntries++; - } - tracepdeex(0, sp3Stream, "\n"); - - // ++ line entries one per satellite sigma = 2^val in millimeters. - lineNumber = 1; - lineEntries = 0; - tracepdeex(0, sp3Stream, "++ "); - for (auto& [Sat, enable] : sp3FileData.sats) - { - if (lineEntries == 17) - { - //start a new line - tracepdeex(0, sp3Stream, "\n++ "); - lineEntries = 0; - } - - auto it = entryList.find(Sat); - if (it == entryList.end()) - { - // Accuracy unknown. - tracepdeex(0, sp3Stream, " 0"); - } - else - { - auto& [key, entry] = *it; - - if (entry.sigma == 0) - { - // Accuracy unknown. - tracepdeex(0, sp3Stream, " 0"); - } - else - { - // Accuracy sigma = 2^(Accuracy) in millimeters. - //TODO Not sure if ceil or round is correct needs checking. - double val = std::ceil(std::log2(entry.sigma * 1000)); - tracepdeex(0, sp3Stream, "%3.0f", val); - } - } - - lineEntries++; - } - - while ( lineNumber < 17 - ||lineEntries < 17) - { - //keep adding entries and lines until we reach the minimum - - if (lineEntries == 17) - { - //start a new line - tracepdeex(0, sp3Stream, "\n++ "); - lineEntries = 0; - lineNumber++; - } - tracepdeex(0, sp3Stream, "%3s", "0"); - - lineEntries++; - } - tracepdeex(0, sp3Stream, "\n"); - - char syschar = 0; - for (auto& [Sat, enable] : sp3FileData.sats) - { - if (syschar == 0) { syschar = Sat.sysChar(); continue; } - if (syschar != Sat.sysChar()) { syschar = 'M'; break; } - } - if (syschar == 0) - { - BOOST_LOG_TRIVIAL(error) << "Error: Writing RINEX clock file no systems in process_sys."; - return; - } - - // Using GPS time. - tracepdeex(0, sp3Stream, "%%c %c cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n", syschar); - tracepdeex(0, sp3Stream, "%%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n"); - - // first variable is the base for the sigma of position, x, y, z, sigma = 1.25^val in millimeters. - // second variable is the base for the sigma of time sigma = 1.025^val in picoseconds. - tracepdeex(0, sp3Stream, "%%f 1.2500000 1.025000000 0.00000000000 0.000000000000000\n"); - tracepdeex(0, sp3Stream, "%%f 0.0000000 0.000000000 0.00000000000 0.000000000000000\n"); - - // float variable lines, unused. - tracepdeex(0, sp3Stream, "%%i 0 0 0 0 0 0 0 0 0\n"); - tracepdeex(0, sp3Stream, "%%i 0 0 0 0 0 0 0 0 0\n"); - - // There is a minimum of four comment lines. - tracepdeex(0, sp3Stream, "/* Created using Ginan at: %s.\n", timeGet().to_string().c_str()); - tracepdeex(0, sp3Stream, "/* WARNING: Not for operational use\n"); - tracepdeex(0, sp3Stream, "/*\n"); - tracepdeex(0, sp3Stream, "/*\n"); + GEpoch ep = time; + + sp3FileData = {}; + sp3FileData.numEpoch = 1; + + // note "#dV" for velocity and position. + char velPos; + + if (acsConfig.output_sp3_velocities) + velPos = 'V'; + else + velPos = 'P'; + + tracepdeex( + 0, + sp3Stream, + "#d%c%4.0f %2.0f %2.0f %2.0f %2.0f %11.8f ", + velPos, + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + sp3FileData.numEpoch_pos = sp3Stream.tellp(); + + // TODO Check, coordinate system and Orbit Type from example product file. + tracepdeex( + 0, + sp3Stream, + "%7d ORBIT %5s FIT %4s\n", + sp3FileData.numEpoch, + acsConfig.reference_system.c_str(), + acsConfig.analysis_agency.c_str() + ); + + GWeek week = time; + GTow tow = time; + MjDateTT mjdate = time; + + tracepdeex( + 0, + sp3Stream, + "## %4d %15.8f %14.8f %5.0f %015.13f\n", + week, + tow, + acsConfig.sp3_output_interval, + mjdate.to_double(), + 0.0 + ); + + for (auto sys : {E_Sys::GPS, E_Sys::GLO, E_Sys::GAL, E_Sys::BDS, E_Sys::LEO}) + { + if (outSys[sys]) + for (auto Sat : getSysSats(sys)) + { + auto satOpts = acsConfig.getSatOpts(Sat); + + if (satOpts.exclude) + continue; + + sp3FileData.sats[Sat] = true; + } + } + + int lineNumber = 1; + int lineEntries = 0; + tracepdeex(0, sp3Stream, "+ %3d ", sp3FileData.sats.size()); + for (auto& [Sat, enable] : sp3FileData.sats) + { + if (lineEntries == 17) + { + // start a new line + tracepdeex(0, sp3Stream, "\n+ "); + lineEntries = 0; + lineNumber++; + } + tracepdeex(0, sp3Stream, "%3s", Sat.id().c_str()); + + lineEntries++; + } + + while (lineNumber < 17 || lineEntries < 17) + { + // keep adding entries and lines until we reach the minimum + + if (lineEntries == 17) + { + // start a new line + tracepdeex(0, sp3Stream, "\n+ "); + lineEntries = 0; + lineNumber++; + } + tracepdeex(0, sp3Stream, "%3s", "0"); + + lineEntries++; + } + tracepdeex(0, sp3Stream, "\n"); + + // ++ line entries one per satellite sigma = 2^val in millimeters. + lineNumber = 1; + lineEntries = 0; + tracepdeex(0, sp3Stream, "++ "); + for (auto& [Sat, enable] : sp3FileData.sats) + { + if (lineEntries == 17) + { + // start a new line + tracepdeex(0, sp3Stream, "\n++ "); + lineEntries = 0; + lineNumber++; + } + + auto it = entryList.find(Sat); + if (it == entryList.end()) + { + // Accuracy unknown. + tracepdeex(0, sp3Stream, " 0"); + } + else + { + auto& [key, entry] = *it; + + if (entry.sigma == 0) + { + // Accuracy unknown. + tracepdeex(0, sp3Stream, " 0"); + } + else + { + // Accuracy sigma = 2^(Accuracy) in millimeters. + // TODO Not sure if ceil or round is correct needs checking. + double val = std::ceil(std::log2(entry.sigma * CLIGHT * 1000)); + tracepdeex(0, sp3Stream, "%3.0f", val); + } + } + + lineEntries++; + } + + while (lineNumber < 17 || lineEntries < 17) + { + // keep adding entries and lines until we reach the minimum + + if (lineEntries == 17) + { + // start a new line + tracepdeex(0, sp3Stream, "\n++ "); + lineEntries = 0; + lineNumber++; + } + tracepdeex(0, sp3Stream, "%3s", "0"); + + lineEntries++; + } + tracepdeex(0, sp3Stream, "\n"); + + char syschar = 0; + for (auto& [Sat, enable] : sp3FileData.sats) + { + if (syschar == 0) + { + syschar = Sat.sysChar(); + continue; + } + if (syschar != Sat.sysChar()) + { + syschar = 'M'; + break; + } + } + if (syschar == 0) + { + BOOST_LOG_TRIVIAL(error) << "Writing RINEX clock file no systems in process_sys."; + return; + } + + // Using GPS time. + tracepdeex( + 0, + sp3Stream, + "%%c %c cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n", + syschar + ); + tracepdeex(0, sp3Stream, "%%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n"); + + // first variable is the base for the sigma of position, x, y, z, sigma = 1.25^val in + // millimeters. second variable is the base for the sigma of time sigma = 1.025^val in + // picoseconds. + tracepdeex(0, sp3Stream, "%%f 1.2500000 1.025000000 0.00000000000 0.000000000000000\n"); + tracepdeex(0, sp3Stream, "%%f 0.0000000 0.000000000 0.00000000000 0.000000000000000\n"); + + // float variable lines, unused. + tracepdeex(0, sp3Stream, "%%i 0 0 0 0 0 0 0 0 0\n"); + tracepdeex(0, sp3Stream, "%%i 0 0 0 0 0 0 0 0 0\n"); + + // There is a minimum of four comment lines. + tracepdeex(0, sp3Stream, "/* Created using Ginan at: %s.\n", timeGet().to_string().c_str()); + tracepdeex(0, sp3Stream, "/* WARNING: Not for operational use\n"); + tracepdeex(0, sp3Stream, "/*\n"); + tracepdeex(0, sp3Stream, "/*\n"); } void updateSp3Body( - string filename, ///< Path to output file. - map& entryList, ///< List of data to print. - GTime time, ///< Epoch time. - map& outSys) ///< Systems to include in file. + string filename, ///< Path to output file. + map& entryList, ///< List of data to print. + GTime time, ///< Epoch time. + map& outSys ///< Systems to include in file. +) { - //first create if non existing - { - std::fstream maker(filename, std::ios::app); - } - std::fstream sp3Stream(filename); - - if (!sp3Stream) - { - BOOST_LOG_TRIVIAL(warning) << "Error opening " << filename << " for SP3 file."; - - return; - } - - sp3Stream.seekp(0, std::ios::end); - - long endFilePos = sp3Stream.tellp(); - - auto& sp3FileData = sp3FileDataMap[filename]; - - if (endFilePos == 0) - { - writeSp3Header(sp3Stream, entryList, time, outSys, sp3FileData); - } - else - { - sp3FileData.numEpoch++; - - sp3Stream.seekp(sp3FileData.numEpoch_pos); - - tracepdeex(0, sp3Stream, "%7d", sp3FileData.numEpoch); - - //go back to end of file (minus "EOF\n") - sp3Stream.seekp(-4, std::ios::end); - } - - GEpoch ep = time; - tracepdeex(0, sp3Stream, "* %4.0f %2.0f %2.0f %2.0f %2.0f %11.8f\n", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - // Note position is in kilometers and clock values microseconds. - // There need to be one entry per satellite in the header for correct file format. - for (auto& [Sat, enable] : sp3FileData.sats) - { - auto it = entryList.find(Sat); - if (it != entryList.end()) - { - auto& [key, entry] = *it; - - char predictedChar; - if (entry.predicted) predictedChar = 'P'; - else predictedChar = ' '; - - { - tracepdeex(0, sp3Stream, "P%s%14.6f%14.6f%14.6f%14.6f%15s%c%3s%c\n", - entry.Sat.id().c_str(), - entry.satPos.x() / 1000, - entry.satPos.y() / 1000, - entry.satPos.z() / 1000, - entry.satClk * 1e6, - "", - predictedChar, - "", - predictedChar); - } - - if (acsConfig.output_sp3_velocities) - { - tracepdeex(0, sp3Stream, "V%s%14.6f%14.6f%14.6f%14.6f%15s%c%3s%c\n", - entry.Sat.id().c_str(), - entry.satVel.x() * 10, - entry.satVel.y() * 10, - entry.satVel.z() * 10, - entry.satClkVel * 1e10, - "", - predictedChar, - "", - predictedChar); - } - } - else - { - { - tracepdeex(0, sp3Stream, "P%s%14.6f%14.6f%14.6f%14.6f\n", - Sat.id().c_str(), 0, 0, 0, NO_SP3_CLK); - } - - if (acsConfig.output_sp3_velocities) - { - tracepdeex(0, sp3Stream, "V%s%14.6f%14.6f%14.6f%14.6f\n", - Sat.id().c_str(), 0, 0, 0, NO_SP3_CLK); - } - } - } - - tracepdeex(0, sp3Stream, "EOF\n"); + // first create if non existing + { + std::fstream maker(filename, std::ios::app); + } + std::fstream sp3Stream(filename); + + if (!sp3Stream) + { + BOOST_LOG_TRIVIAL(warning) << "Error opening " << filename << " for SP3 file."; + + return; + } + + sp3Stream.seekp(0, std::ios::end); + + long endFilePos = sp3Stream.tellp(); + + auto& sp3FileData = sp3FileDataMap[filename]; + + if (endFilePos == 0) + { + writeSp3Header(sp3Stream, entryList, time, outSys, sp3FileData); + } + else // todo Eugene: update accuracy sigmas as well + { + sp3FileData.numEpoch++; + + sp3Stream.seekp(sp3FileData.numEpoch_pos); + + tracepdeex(0, sp3Stream, "%7d", sp3FileData.numEpoch); + + // go back to end of file (minus "EOF\n") + sp3Stream.seekp(-4, std::ios::end); + } + + GEpoch ep = time; + tracepdeex( + 0, + sp3Stream, + "* %4.0f %2.0f %2.0f %2.0f %2.0f %11.8f\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + // Note position is in kilometers and clock values microseconds. + // There need to be one entry per satellite in the header for correct file format. + for (auto& [Sat, enable] : sp3FileData.sats) + { + auto it = entryList.find(Sat); + if (it != entryList.end()) + { + auto& [key, entry] = *it; + + char predictedChar; + if (entry.predicted) + predictedChar = 'P'; + else + predictedChar = ' '; + + { + tracepdeex( + 0, + sp3Stream, + "P%s%14.6f%14.6f%14.6f%14.6f%15s%c%3s%c\n", + entry.Sat.id().c_str(), + entry.satPos.x() / 1000, + entry.satPos.y() / 1000, + entry.satPos.z() / 1000, + entry.satClk * 1e6, + "", + predictedChar, + "", + predictedChar + ); + } + + if (acsConfig.output_sp3_velocities) + { + tracepdeex( + 0, + sp3Stream, + "V%s%14.6f%14.6f%14.6f%14.6f%15s%c%3s%c\n", + entry.Sat.id().c_str(), + entry.satVel.x() * 10, + entry.satVel.y() * 10, + entry.satVel.z() * 10, + entry.satClkVel * 1e10, + "", + predictedChar, + "", + predictedChar + ); + } + } + else + { + { + tracepdeex( + 0, + sp3Stream, + "P%s%14.6f%14.6f%14.6f%14.6f\n", + Sat.id().c_str(), + 0, + 0, + 0, + NO_SP3_CLK + ); + } + + if (acsConfig.output_sp3_velocities) + { + tracepdeex( + 0, + sp3Stream, + "V%s%14.6f%14.6f%14.6f%14.6f\n", + Sat.id().c_str(), + 0, + 0, + 0, + NO_SP3_CLK + ); + } + } + } + + tracepdeex(0, sp3Stream, "EOF\n"); } void writeSysSetSp3( - string filename, - GTime time, - map& outSys, - vector sp3OrbitSrcs, - vector sp3ClockSrcs, - KFState* kfState_ptr, - bool predicted) + string filename, + GTime time, + map& outSys, + vector sp3OrbitSrcs, + vector sp3ClockSrcs, + KFState& kfState, + bool predicted +) { - map entryList; - - ERPValues erpv = getErp(nav.erp, time); - FrameSwapper frameSwapper(time, erpv); - - for (auto& [Sat, satNav] : nav.satNavMap) - { - if (outSys[Sat.sys] == false) - continue; - - // Create a dummy observation - GObs obs; - obs.Sat = Sat; - obs.satNav_ptr = &nav.satNavMap[Sat]; - - bool clkPass = satclk(nullStream, time, time, obs, sp3ClockSrcs, nav, kfState_ptr); - bool posPass = satpos(nullStream, time, time, obs, sp3OrbitSrcs, E_OffsetType::COM, nav, kfState_ptr); - - if (posPass == false) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Writing SP3 file, failed to get data for satellite " << Sat.id(); - continue; - } - - Sp3Entry entry; - - if (acsConfig.output_inertial_orbits) - { - obs.rSatEci0 = frameSwapper(obs.rSatCom, &obs.satVel, &obs.vSatEci0); - - entry.satPos = obs.rSatEci0; - entry.satVel = obs.vSatEci0; - } - else - { - entry.satPos = obs.rSatCom; - entry.satVel = obs.satVel; - } - - if (clkPass) - { - entry.satClk = obs.satClk; - entry.satClkVel = obs.satClkVel; - } - else - { - entry.satClk = INVALID_CLOCK_VALUE / 1e6; - entry.satClkVel = INVALID_CLOCK_VALUE / 1e10; - } - - entry.Sat = Sat; - entry.sigma = sqrt(obs.satClkVar); - entry.predicted = predicted; - - entryList[Sat] = entry; - } - - updateSp3Body(filename, entryList, time, outSys); + map entryList; + + ERPValues erpv = getErp(nav.erp, time); + FrameSwapper frameSwapper(time, erpv); + + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (outSys[Sat.sys] == false) + continue; + + // Create a dummy observation + GObs obs; + obs.Sat = Sat; + obs.satNav_ptr = &nav.satNavMap[Sat]; + + bool clkPass = satclk(nullStream, time, time, obs, sp3ClockSrcs, nav, &kfState); + bool posPass = + satpos(nullStream, time, time, obs, sp3OrbitSrcs, E_OffsetType::COM, nav, &kfState); + + if (posPass == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Writing SP3 file, failed to get data for satellite " << Sat.id(); + continue; + } + + Sp3Entry entry; + + if (acsConfig.output_inertial_orbits) + { + obs.rSatEci0 = frameSwapper(obs.rSatCom, &obs.satVel, &obs.vSatEci0); + + entry.satPos = obs.rSatEci0; + entry.satVel = obs.vSatEci0; + } + else + { + entry.satPos = obs.rSatCom; + entry.satVel = obs.satVel; + } + + if (clkPass) + { + entry.satClk = obs.satClk; + entry.satClkVel = obs.satClkVel; + } + else + { + entry.satClk = INVALID_CLOCK_VALUE / 1e6; + entry.satClkVel = INVALID_CLOCK_VALUE / 1e10; + } + + entry.Sat = Sat; + entry.sigma = sqrt(obs.satClkVar); + entry.predicted = predicted; + + entryList[Sat] = entry; + } + + updateSp3Body(filename, entryList, time, outSys); } void outputSp3( - string filename, - GTime time, - vector sp3OrbitSrcs, - vector sp3ClockSrcs, - KFState* kfState_ptr, - bool predicted) + string filename, + GTime time, + KFState& kfState, + vector sp3OrbitSrcs, + vector sp3ClockSrcs, + bool predicted +) { - auto sysFilenames = getSysOutputFilenames(filename, time); + auto sysFilenames = getSysOutputFilenames(filename, time); - for (auto [filename, sysMap] : sysFilenames) - { - writeSysSetSp3(filename, time, sysMap, sp3OrbitSrcs, sp3ClockSrcs, kfState_ptr, predicted); - } + for (auto [filename, sysMap] : sysFilenames) + { + writeSysSetSp3(filename, time, sysMap, sp3OrbitSrcs, sp3ClockSrcs, kfState, predicted); + } } diff --git a/src/cpp/common/sp3Write.hpp b/src/cpp/common/sp3Write.hpp index 87baad5a0..a85e7a6f4 100644 --- a/src/cpp/common/sp3Write.hpp +++ b/src/cpp/common/sp3Write.hpp @@ -1,21 +1,18 @@ - #pragma once #include +#include "common/rinexClkWrite.hpp" using std::string; -#include "rinexClkWrite.hpp" - struct GTime; -class E_Source; struct KFState; void outputSp3( - string filename, - GTime time, - vector sp3OrbitSrcs, - vector sp3ClockSrcs, - KFState* kfState_ptr = nullptr, - bool predicted = false); - + string filename, + GTime time, + KFState& kfState, + vector sp3OrbitSrcs, + vector sp3ClockSrcs, + bool predicted = false +); diff --git a/src/cpp/common/ssr.hpp b/src/cpp/common/ssr.hpp index e1eed88ec..312f7af7e 100644 --- a/src/cpp/common/ssr.hpp +++ b/src/cpp/common/ssr.hpp @@ -1,360 +1,292 @@ - #pragma once #include - -#include "eigenIncluder.hpp" -#include "gTime.hpp" -#include "trace.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/trace.hpp" // #include "gMap.hpp" -#define SSR_UNAVAILABLE -9999 +constexpr int SSR_UNAVAILABLE = -9999; struct SatPos; -const double uraSsr[] = -{ - 0, - 0.25, - 0.5, - 0.75, - 1, - 1.25, - 1.5, - 1.75, - 2, - 2.75, - 3.5, - 4.25, - 5, - 5.75, - 6.5, - 7.25, - 8, - 10.25, - 12.5, - 14.75, - 17, - 19.25, - 21.5, - 23.75, - 26, - 32.75, - 39.5, - 46.25, - 53, - 59.75, - 66.5, - 73.25, - 80, - 100.25, - 120.5, - 140.75, - 161, - 181.25, - 201.5, - 221.75, - 242, - 302.75, - 363.5, - 424.25, - 485, - 545.75, - 606.5, - 667.25, - 728, - 910.25, - 1092.5, - 1274.75, - 1457, - 1639.25, - 1821.5, - 2003.75, - 2186, - 2732.75, - 3279.5, - 3826.25, - 4373, - 4919.75, - 5466.5, - 6013.25 -}; +const double uraSsr[] = {0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, + 2, 2.75, 3.5, 4.25, 5, 5.75, 6.5, 7.25, + 8, 10.25, 12.5, 14.75, 17, 19.25, 21.5, 23.75, + 26, 32.75, 39.5, 46.25, 53, 59.75, 66.5, 73.25, + 80, 100.25, 120.5, 140.75, 161, 181.25, 201.5, 221.75, + 242, 302.75, 363.5, 424.25, 485, 545.75, 606.5, 667.25, + 728, 910.25, 1092.5, 1274.75, 1457, 1639.25, 1821.5, 2003.75, + 2186, 2732.75, 3279.5, 3826.25, 4373, 4919.75, 5466.5, 6013.25}; // SSR message metadata struct SSRMeta { - int epochTime1s = 0; - GTime receivedTime = {}; - int updateIntIndex = -1; - int multipleMessage = 0; - unsigned int referenceDatum = 0; - unsigned int provider = 0; - unsigned int solution = 0; - unsigned int numSats = 0; + int epochTime1s = 0; + GTime receivedTime = {}; + int updateIntIndex = -1; + int multipleMessage = 0; + unsigned int referenceDatum = 0; + unsigned int provider = 0; + unsigned int solution = 0; + unsigned int numSats = 0; }; struct SSREph { - SSRMeta ssrMeta = {}; - GTime t0 = {}; - double udi = 0; ///< update interval - int iod = -1; - int iode = -1; ///< issue of data - int iodcrc = -1; - Vector3d deph = Vector3d::Zero(); ///< delta orbit {radial,along,cross} (m) - Vector3d ddeph = Vector3d::Zero(); ///< dot delta orbit {radial,along,cross} (m/s) + SSRMeta ssrMeta = {}; + GTime t0 = {}; + double udi = 0; ///< update interval + int iod = -1; + int iode = -1; ///< issue of data + int iodcrc = -1; + Vector3d deph = Vector3d::Zero(); ///< delta orbit {radial,along,cross} (m) + Vector3d ddeph = Vector3d::Zero(); ///< dot delta orbit {radial,along,cross} (m/s) }; struct SSRClk { - SSRMeta ssrMeta = {}; - GTime t0 = {}; - double udi = 0; ///< update interval - int iod = -1; - double dclk[3] = {}; ///< delta clock {c0,c1,c2} (m,m/s,m/s^2) + SSRMeta ssrMeta = {}; + GTime t0 = {}; + double udi = 0; ///< update interval + int iod = -1; + double dclk[3] = {}; ///< delta clock {c0,c1,c2} (m,m/s,m/s^2) }; struct SSRUra { - SSRMeta ssrMeta = {}; - GTime t0 = {}; - double udi = 0; ///< update interval - int iod = -1; - int ura = 0; ///< URA indicator + SSRMeta ssrMeta = {}; + GTime t0 = {}; + double udi = 0; ///< update interval + int iod = -1; + int ura = 0; ///< URA indicator }; struct SSRHRClk { - SSRMeta ssrMeta = {}; - GTime t0 = {}; - double udi = 0; ///< update interval - int iod = -1; - double hrclk = 0; ///< high-rate clock corection (m) + SSRMeta ssrMeta = {}; + GTime t0 = {}; + double udi = 0; ///< update interval + int iod = -1; + double hrclk = 0; ///< high-rate clock corection (m) }; struct BiasVar { - double bias = 0; ///< biases (m) - double var = 0; ///< biases variance (m^2) + double bias = 0; ///< biases (m) + double var = 0; ///< biases variance (m^2) }; struct SSRBias { - SSRMeta ssrMeta = {}; - GTime t0 = {}; - double udi = 0; ///< update interval - int iod = -1; - unsigned int nbias = 0; - map obsCodeBiasMap; - map ionDCBOffset; + SSRMeta ssrMeta = {}; + GTime t0 = {}; + double udi = 0; ///< update interval + int iod = -1; + unsigned int nbias = 0; + map obsCodeBiasMap; + map ionDCBOffset; }; struct SSRCodeBias : SSRBias { - }; struct SSRPhase { - int dispBiasConistInd = -1; - int MWConistInd = -1; - double yawAngle = 0; - double yawRate = 0; + int dispBiasConistInd = -1; + int MWConistInd = -1; + double yawAngle = 0; + double yawRate = 0; }; struct SSRPhaseCh { - unsigned int signalIntInd = -1; - unsigned int signalWLIntInd = -1; - unsigned int signalDisconCnt = -1; + unsigned int signalIntInd = -1; + unsigned int signalWLIntInd = -1; + unsigned int signalDisconCnt = -1; }; struct SSRPhasBias : SSRBias { - SSRPhase ssrPhase; ///< Additional data for SSR phase messages - map ssrPhaseChs; ///< Additional data for SSR phase messages, for each channel + SSRPhase ssrPhase; ///< Additional data for SSR phase messages + map + ssrPhaseChs; ///< Additional data for SSR phase messages, for each channel }; struct SphComp { - int layer; - int order; - int degree; - E_TrigType trigType; - double value; - double variance; + int layer; + int order; + int degree; + E_TrigType trigType; + double value; + double variance; }; struct SSRVTEClayer { - double height = 0; - int maxOrder = 0; - int maxDegree = 0; - map sphHarmonic; + double height = 0; + int maxOrder = 0; + int maxDegree = 0; + map sphHarmonic; }; struct SSRAtmGlobal { - GTime time; - int numberLayers; - map layers; - double vtecQuality; - int iod = -1; + GTime time; + int numberLayers; + map layers; + double vtecQuality; + int iod = -1; }; struct SSRSTECData { - int iod = -1; - double sigma = 0.1; /* STEC maps accuracy in TECu */ - map poly; /* STEC polynomials in TECu (deg) */ - map grid; /* STEC gridmaps in TECu */ + int iod = -1; + double sigma = 0.1; /* STEC maps accuracy in TECu */ + map poly; /* STEC polynomials in TECu (deg) */ + map grid; /* STEC gridmaps in TECu */ }; struct SSRTropData { - double sigma = 0; - map polyDry; /* ZHD in meters (deg) */ - map gridDry; /* ZHD in meters */ - map gridWet; /* ZWD in meters */ + double sigma = 0; + map polyDry; /* ZHD in meters (deg) */ + map gridDry; /* ZHD in meters */ + map gridWet; /* ZWD in meters */ }; struct SSRAtmRegion { - int regionDefIOD = -1; - map gridLatDeg; - map gridLonDeg; - - double minLatDeg = 0; - double maxLatDeg = 0; - double intLatDeg = 0; - - double minLonDeg = 0; - double maxLonDeg = 0; - double intLonDeg = 0; - - int gridType = -1; - int tropPolySize = -1; - int ionoPolySize = -1; - bool ionoGrid = false; - bool tropGrid = false; - - map> tropData; - map>> stecData; - GTime stecUpdateTime; + int regionDefIOD = -1; + map gridLatDeg; + map gridLonDeg; + + double minLatDeg = 0; + double maxLatDeg = 0; + double intLatDeg = 0; + + double minLonDeg = 0; + double maxLonDeg = 0; + double intLonDeg = 0; + + int gridType = -1; + int tropPolySize = -1; + int ionoPolySize = -1; + bool ionoGrid = false; + bool tropGrid = false; + + map> tropData; + map>> stecData; + GTime stecUpdateTime; }; struct SSRAtm { - SSRMeta ssrMeta; - map> atmosGlobalMap; - map atmosRegionsMap; + SSRMeta ssrMeta; + map> atmosGlobalMap; + map atmosRegionsMap; }; struct EphValues { - GTime time; - unsigned int iode = -1; - Vector3d brdcPos = Vector3d::Zero(); - Vector3d brdcVel = Vector3d::Zero(); - Vector3d precPos = Vector3d::Zero(); - Vector3d precVel = Vector3d::Zero(); - - double ephVar = 0; + GTime time; + unsigned int iode = -1; + Vector3d brdcPos = Vector3d::Zero(); + Vector3d brdcVel = Vector3d::Zero(); + Vector3d precPos = Vector3d::Zero(); + Vector3d precVel = Vector3d::Zero(); + + double ephVar = 0; }; struct ClkValues { - GTime time; - unsigned int iode = -1; - double brdcClk = 0; - double precClk = 0; + GTime time; + unsigned int iode = -1; + double brdcClk = 0; + double precClk = 0; }; struct SSREphInput { - bool valid = false; - EphValues vals[2]; + bool valid = false; + EphValues vals[2]; }; struct SSRClkInput { - bool valid = false; - ClkValues vals[2]; + bool valid = false; + ClkValues vals[2]; }; /* SSR correction type */ struct SSRMaps { - map> ssrCodeBias_map; - map> ssrPhasBias_map; - map> ssrClk_map; - map> ssrEph_map; - map> ssrHRClk_map; - map> ssrUra_map; - - int refd_; ///< sat ref datum (0:ITRF,1:regional) - unsigned char update_; ///< update flag (0:no update,1:update) + map> ssrCodeBias_map; + map> ssrPhasBias_map; + map> ssrClk_map; + map> ssrEph_map; + map> ssrHRClk_map; + map> ssrUra_map; + + int refd_; ///< sat ref datum (0:ITRF,1:regional) + unsigned char update_; ///< update flag (0:no update,1:update) }; struct SSROut { - GTime epochTime; + GTime epochTime; - SSRPhasBias ssrPhasBias; - SSRCodeBias ssrCodeBias; + SSRPhasBias ssrPhasBias; + SSRCodeBias ssrCodeBias; - SSRClkInput clkInput; - SSREphInput ephInput; + SSRClkInput clkInput; + SSREphInput ephInput; - SSRClk ssrClk; - SSREph ssrEph; + SSRClk ssrClk; + SSREph ssrEph; - SSRHRClk ssrHRClk; - SSRUra ssrUra; + SSRHRClk ssrHRClk; + SSRUra ssrUra; - bool ephUpdated = false; - bool clkUpdated = false; - bool hrclkUpdated = false; - bool phaseUpdated = false; - bool codeUpdated = false; - bool uraUpdated = false; + bool ephUpdated = false; + bool clkUpdated = false; + bool hrclkUpdated = false; + bool phaseUpdated = false; + bool codeUpdated = false; + bool uraUpdated = false; }; struct KFState; -int uraToClassValue(double ura); +int uraToClassValue(double ura); double ephVarToUra(double ephVar); -void prepareSsrStates( - Trace& trace, - KFState& kfState, - KFState& ionState, - GTime time); - -void writeSsrOutToFile( - int epochNum, - std::set sats); +void prepareSsrStates(Trace& trace, KFState& kfState, KFState& ionState, GTime time); +void writeSsrOutToFile(int epochNum, std::set sats); bool ssrPosDelta( - GTime time, - GTime ephTime, - SatPos& satPos, - const SSRMaps& ssrMaps, - Vector3d& dPos, - int& iodPos, - int& iodEph, - GTime& validStart, - GTime& validStop); + GTime time, + GTime ephTime, + SatPos& satPos, + const SSRMaps& ssrMaps, + Vector3d& dPos, + int& iodPos, + int& iodEph, + GTime& validStart, + GTime& validStop +); bool ssrClkDelta( - GTime time, - GTime ephTime, - SatPos& satPos, - const SSRMaps& ssrMaps, - double& dclk, - int& iodClk, - GTime& validStart, - GTime& validStop); - - + GTime time, + GTime ephTime, + SatPos& satPos, + const SSRMaps& ssrMaps, + double& dclk, + int& iodClk, + GTime& validStart, + GTime& validStop +); diff --git a/src/cpp/common/streamCustom.cpp b/src/cpp/common/streamCustom.cpp index 66f1bdaa1..1d3e5da73 100644 --- a/src/cpp/common/streamCustom.cpp +++ b/src/cpp/common/streamCustom.cpp @@ -1,85 +1,88 @@ - +#include "common/streamCustom.hpp" #include - -#include "packetStatistics.hpp" -#include "streamCustom.hpp" - - -#define CLEAN_UP_AND_RETURN_ON_FAILURE \ - \ - if (inputStream.fail()) \ - { \ - inputStream.clear(); \ - inputStream.seekg(pos); \ - return; \ - } - - -void CustomParser::parse( - std::istream& inputStream) +#include "common/packetStatistics.hpp" +#define CLEAN_UP_AND_RETURN_ON_FAILURE \ + \ + if (inputStream.fail()) \ + { \ + inputStream.clear(); \ + inputStream.seekg(pos); \ + return; \ + } + +void CustomParser::parse(std::istream& inputStream) { -// std::cout << "Parsing ubx" << "\n"; - - while (inputStream) - { - int pos; - unsigned char c1 = 0; - unsigned char c2 = 0; - while (true) - { - pos = inputStream.tellg(); - - //move c2 back one and replace, (check one byte at a time, not in pairs) - c1 = c2; - inputStream.read((char*)&c2, 1); - - if (inputStream) - { - if ( c1 == CUSTOM_PREAMBLE) - { - break; - } - } - else - { - return; - } - nonFrameByteFound(c2); - } - CLEAN_UP_AND_RETURN_ON_FAILURE; - - //account for 2 byte preamble - pos--; - - preambleFound(); - - unsigned char ubxClass = 0; inputStream.read((char*)&ubxClass, 1); CLEAN_UP_AND_RETURN_ON_FAILURE; - unsigned char id = 0; inputStream.read((char*)&id, 1); CLEAN_UP_AND_RETURN_ON_FAILURE; - unsigned short int payload_length = 0; inputStream.read((char*)&payload_length, 2); CLEAN_UP_AND_RETURN_ON_FAILURE; - vector payload(payload_length); inputStream.read((char*)payload.data(), payload_length); CLEAN_UP_AND_RETURN_ON_FAILURE; // Read the frame data (include the header) - unsigned short int crcRead = 0; inputStream.read((char*)&crcRead, 2); CLEAN_UP_AND_RETURN_ON_FAILURE; - - //todo aaron calculate crcRead - if (0) - { - checksumFailure(); - - inputStream.seekg(pos + 1); - - continue; - } - - checksumSuccess(); - - recordFrame(ubxClass, id, payload, crcRead); - - decode(ubxClass, id, payload); - - if (obsListList.size() > 2) - { - return; - } - } - inputStream.clear(); + // std::cout << "Parsing ubx" << "\n"; + + while (inputStream) + { + int pos; + unsigned char c1 = 0; + unsigned char c2 = 0; + while (true) + { + pos = inputStream.tellg(); + + // move c2 back one and replace, (check one byte at a time, not in pairs) + c1 = c2; + inputStream.read((char*)&c2, 1); + + if (inputStream) + { + if (c1 == CUSTOM_PREAMBLE) + { + break; + } + } + else + { + return; + } + nonFrameByteFound(c2); + } + CLEAN_UP_AND_RETURN_ON_FAILURE; + + // account for 2 byte preamble + pos--; + + preambleFound(); + + unsigned char ubxClass = 0; + inputStream.read((char*)&ubxClass, 1); + CLEAN_UP_AND_RETURN_ON_FAILURE; + unsigned char id = 0; + inputStream.read((char*)&id, 1); + CLEAN_UP_AND_RETURN_ON_FAILURE; + unsigned short int payload_length = 0; + inputStream.read((char*)&payload_length, 2); + CLEAN_UP_AND_RETURN_ON_FAILURE; + vector payload(payload_length); + inputStream.read((char*)payload.data(), payload_length); + CLEAN_UP_AND_RETURN_ON_FAILURE; // Read the frame data (include the header) + unsigned short int crcRead = 0; + inputStream.read((char*)&crcRead, 2); + CLEAN_UP_AND_RETURN_ON_FAILURE; + + // todo aaron calculate crcRead + if (0) + { + checksumFailure(); + + inputStream.seekg(pos + 1); + + continue; + } + + checksumSuccess(); + + recordFrame(ubxClass, id, payload, crcRead); + + decode(ubxClass, id, payload); + + if (obsListList.size() > 2) + { + return; + } + } + inputStream.clear(); } - diff --git a/src/cpp/common/streamCustom.hpp b/src/cpp/common/streamCustom.hpp index 8d05c249a..24fbe4ddc 100644 --- a/src/cpp/common/streamCustom.hpp +++ b/src/cpp/common/streamCustom.hpp @@ -1,22 +1,15 @@ - #pragma once - -#include "packetStatistics.hpp" -#include "customDecoder.hpp" -#include "streamParser.hpp" -#include "streamObs.hpp" +#include "common/customDecoder.hpp" +#include "common/packetStatistics.hpp" +#include "common/streamObs.hpp" +#include "common/streamParser.hpp" #define CUSTOM_PREAMBLE 0xAC struct CustomParser : Parser, CustomDecoder, PacketStatistics { - void parse( - std::istream& inputStream); + void parse(std::istream& inputStream); - string parserType() - { - return "CustomParser"; - } + string parserType() { return "CustomParser"; } }; - diff --git a/src/cpp/common/streamFile.hpp b/src/cpp/common/streamFile.hpp index 1b45def25..791ecc3f3 100644 --- a/src/cpp/common/streamFile.hpp +++ b/src/cpp/common/streamFile.hpp @@ -1,99 +1,89 @@ - #pragma once - #include #include #include +#include "common/streamParser.hpp" using std::make_unique; -using std::unique_ptr; using std::string; - -#include "streamParser.hpp" - +using std::unique_ptr; struct FileState : std::ifstream { - long int& filePos; - - FileState( - string path, - long int& filePos, - std::ifstream::openmode mode = std::ifstream::in) - : filePos {filePos} - { - if (filePos < 0) - { -// BOOST_LOG_TRIVIAL(error) << "Error seeking to negative position in file at " << path << " to " << filePos; - close(); - return; - } - - open(path, mode); - - if (!*this) - { - BOOST_LOG_TRIVIAL(error) << "Error opening file at " << path - << "\n" << " - " << strerror(errno); - filePos = -1; - return; - } - - seekg(filePos); - - if (!*this) - { - BOOST_LOG_TRIVIAL(error) << "Error seeking in file at " << filePos << " in " << path - << "\n" << " - " << strerror(errno); - - filePos = -1; - return; - } - } - - ~FileState() - { - filePos = streamPos(*this); - } + long int& filePos; + + FileState(string path, long int& filePos, std::ifstream::openmode mode = std::ifstream::in) + : filePos{filePos} + { + if (filePos < 0) + { + // BOOST_LOG_TRIVIAL(error) << "Error seeking to negative position in file at " + // << path << " to " + // << filePos; + close(); + return; + } + + open(path, mode); + + if (!*this) + { + BOOST_LOG_TRIVIAL(error) << "Error opening file at " << path << "\n" + << " - " << strerror(errno); + filePos = -1; + return; + } + + seekg(filePos); + + if (!*this) + { + BOOST_LOG_TRIVIAL(error) + << "Error seeking in file at " << filePos << " in " << path << "\n" + << " - " << strerror(errno); + + filePos = -1; + return; + } + } + + ~FileState() { filePos = streamPos(*this); } }; struct FileStream : Stream { - string path; - long int filePos = 0; - - FileStream( - string path) - : path (path) - { - - } - - unique_ptr getIStream_ptr() override - { -// std::cout << "Getting FileStream" << "\n"; - - return make_unique(path, filePos); - } - - bool isDead() override - { - if (filePos < 0) - { - return true; - } - - auto iStream_ptr = this->getIStream_ptr(); - - if (*iStream_ptr) - { - return false; - } - else - { - return true; - } - } -}; + string path; + long int filePos = 0; + + FileStream(string path) : path(path) {} + unique_ptr getIStream_ptr() override + { + // std::cout << "Getting FileStream" << "\n"; + + return make_unique(path, filePos); + } + + bool isDead() override + { + if (filePos < 0) + { + return true; + } + + return false; + } + + bool isAvailable() override + { + std::ifstream input(path, std::ifstream::in); + + if (input) + { + return true; + } + + return false; + } +}; diff --git a/src/cpp/common/streamNtrip.cpp b/src/cpp/common/streamNtrip.cpp index 57829568d..19a9a618c 100644 --- a/src/cpp/common/streamNtrip.cpp +++ b/src/cpp/common/streamNtrip.cpp @@ -1,45 +1,41 @@ - // #pragma GCC optimize ("O0") +#include "common/streamNtrip.hpp" #include +#include "common/trace.hpp" using std::lock_guard; using std::mutex; - -#include "streamNtrip.hpp" -#include "trace.hpp" - void TcpSocket::getData() { - lock_guard guard(receivedDataBufferMtx); + lock_guard guard(receivedDataBufferMtx); - const int reserve = 8192; + const int reserve = 8192; - receivedData.reserve(reserve); + receivedData.reserve(reserve); - for (auto it = chunkList.begin(); it != chunkList.end(); ) - { - auto& chunk = *it; + for (auto it = chunkList.begin(); it != chunkList.end();) + { + auto& chunk = *it; - if (receivedData.size() + chunk.size() > reserve) - { - break; - } + if (receivedData.size() + chunk.size() > reserve) + { + break; + } -// std::cout << "\nCHUNK"; -// printHex(std::cout, *(vector*)&chunk); + // std::cout << "\nCHUNK"; + // printHex(std::cout, *(vector*)&chunk); - receivedData.insert(receivedData.end(), chunk.begin(), chunk.end()); + receivedData.insert(receivedData.end(), chunk.begin(), chunk.end()); - it = chunkList.erase(it); - } + it = chunkList.erase(it); + } } -void TcpSocket::dataChunkDownloaded( - vector& dataChunk) +void TcpSocket::dataChunkDownloaded(vector& dataChunk) { - lock_guard guard(receivedDataBufferMtx); + lock_guard guard(receivedDataBufferMtx); - chunkList.push_back(std::move(dataChunk)); + chunkList.push_back(std::move(dataChunk)); } diff --git a/src/cpp/common/streamNtrip.hpp b/src/cpp/common/streamNtrip.hpp index bf4beb823..8bd0fc271 100644 --- a/src/cpp/common/streamNtrip.hpp +++ b/src/cpp/common/streamNtrip.hpp @@ -1,46 +1,44 @@ - #pragma once -#include "streamSerial.hpp" -#include "tcpSocket.hpp" - - #include +#include "common/streamSerial.hpp" +#include "common/tcpSocket.hpp" +struct NtripResponder : TcpSocket +{ + NtripResponder(const string& url_str) : TcpSocket(url_str) {} + void requestResponseHandler(const boost::system::error_code& err) override; -struct NtripStream : TcpSocket -{ - NtripStream( - const string& url_str) - : TcpSocket(url_str) - { - std::stringstream requestStream; - requestStream << "GET " << url.path << " HTTP/1.1" << "\r\n"; - requestStream << "Host: " << url.host << "\r\n"; - requestStream << "Ntrip-Version: Ntrip/2.0" << "\r\n"; - requestStream << "User-Agent: NTRIP ACS/1.0" << "\r\n"; - if (!url.user.empty()) - { - requestStream << "Authorization: Basic " - << Base64::encode(string(url.user + ":" + url.pass)) << "\r\n"; - } - requestStream << "Connection: close" << "\r\n"; - requestStream << "\r\n"; - - requestString = requestStream.str(); - - connect(); - } - - void requestResponseHandler( - const boost::system::error_code& err) - override; - - void serverResponse( - unsigned int statusCode, - string httpVersion); - - ~NtripStream(){}; + virtual void serverResponse(unsigned int statusCode, string httpVersion) + { + std::cout << "Code Error: No server response defined" << std::endl; + }; }; +struct NtripStream : NtripResponder +{ + NtripStream(const string& url_str) : NtripResponder(url_str) + { + std::stringstream requestStream; + requestStream << "GET " << url.path << " HTTP/1.1" << "\r\n"; + requestStream << "Host: " << url.host << "\r\n"; + requestStream << "Ntrip-Version: Ntrip/2.0" << "\r\n"; + requestStream << "User-Agent: NTRIP ACS/1.0" << "\r\n"; + if (!url.user.empty()) + { + requestStream << "Authorization: Basic " + << Base64::encode(string(url.user + ":" + url.pass)) << "\r\n"; + } + requestStream << "Connection: close" << "\r\n"; + requestStream << "\r\n"; + + requestString = requestStream.str(); + + connect(); + } + + void serverResponse(unsigned int statusCode, string httpVersion) override; + + ~NtripStream() {}; +}; diff --git a/src/cpp/common/streamObs.hpp b/src/cpp/common/streamObs.hpp index aa4701e7b..92ee10908 100644 --- a/src/cpp/common/streamObs.hpp +++ b/src/cpp/common/streamObs.hpp @@ -1,178 +1,254 @@ - #pragma once -#include "streamParser.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "enums.h" - - +#include "common/acsConfig.hpp" +#include "common/enums.h" +#include "common/receiver.hpp" +#include "common/streamParser.hpp" struct ObsLister { - list obsListList; + list obsListList; }; struct ObsStream : StreamParser { - E_ObsWaitCode obsWaitCode = E_ObsWaitCode::OK; - - bool isPseudoRec; - - ObsStream( - unique_ptr stream_ptr, - unique_ptr parser_ptr, - bool isPseudoRec = false) - : StreamParser(std::move(stream_ptr), std::move(parser_ptr)), isPseudoRec{isPseudoRec} - { - - } - - ObsList getObs() - { - try - { - auto& obsLister = dynamic_cast(parser); - - if (obsLister.obsListList.size() < 2) - { - parse(); - } - - if (obsLister.obsListList.empty()) - { - return ObsList(); - } - - ObsList& obsList = obsLister.obsListList.front(); - - 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); - } - } - - return obsList; - } - catch(...){} - - return ObsList(); - } - - /** Return a list of observations from the stream, with a specified timestamp. - * This function may be overridden by objects that use this interface - */ - ObsList getObs( - GTime time, ///< Timestamp to get observations for - double delta = 0.5) ///< Acceptable tolerance around requested time - { - ObsList bigObsList; - bool foundGoodObs = false; - while (1) - { - ObsList obsList = getObs(); - - if (time == GTime::noTime()) { foundGoodObs = true; eatObs(); bigObsList += obsList; break; } - else if (obsList.empty()) { obsWaitCode = E_ObsWaitCode::NO_DATA_WAIT; bigObsList += obsList; break; } - else if (obsList.front()->time < time - delta) { obsWaitCode = E_ObsWaitCode::EARLY_DATA; eatObs(); bigObsList += obsList; break; } - else if (obsList.front()->time > time + delta) { obsWaitCode = E_ObsWaitCode::NO_DATA_EVER; break; } - else { foundGoodObs = true; eatObs(); bigObsList += obsList; } - } - - if (foundGoodObs) obsWaitCode = E_ObsWaitCode::OK; - else if (obsWaitCode == +E_ObsWaitCode::NO_DATA_EVER) return ObsList(); - return bigObsList; - } - - /** Remove some observations from memory - */ - void eatObs() - { - try - { - auto& obsLister = dynamic_cast(parser); - - if (obsLister.obsListList.size() > 0) - { - obsLister.obsListList.pop_front(); - } - } - catch(...){} - } - - bool hasObs() - { - try - { - auto& obsLister = dynamic_cast(parser); - - if (obsLister.obsListList.empty()) - { - return false; - } - - return true; - } - catch(...) - { - return false; - } - } + E_ObsAgeCode obsAgeCode = + E_ObsAgeCode::CURRENT_OBS; ///< Age code of observation retrieved from memory + + bool isPseudoRec; + + ObsStream( + unique_ptr stream_ptr, + unique_ptr parser_ptr, + bool isPseudoRec = false + ) + : StreamParser(std::move(stream_ptr), std::move(parser_ptr)), isPseudoRec{isPseudoRec} + { + } + + ObsList getObs() + { + try + { + auto& obsLister = dynamic_cast(parser); + + if (obsLister.obsListList.size() < 2) + { + parse(); + } + + if (obsLister.obsListList.empty()) + { + return ObsList(); + } + + ObsList& obsList = obsLister.obsListList.front(); + + 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); + } + } + + return obsList; + } + catch (...) + { + } + + return ObsList(); + } + + /** Retrieve observations with a specified timestamp from memory where observations are + * buffered, and update obsAgeCode according to the status of retrieved observations: + * NO_OBS: No observation at all in memory + * PAST_OBS: Closest observation time is earlier than current processing epoch without + * tolerance + * CURRENT_OBS: First processing epoch, or suitable observations found for current + * processing epoch + * FUTURE_OBS: Closest observation time is later than current processing epoch + * without tolerance + * NOTE: This function may be overridden by objects that use this interface + */ + ObsList getObs( + GTime time, ///< Timestamp to get observations for + double delta = 0.5 ///< Acceptable tolerance around requested time + ) + { + ObsList bigObsList; + bool foundGoodObs = false; + while (1) + { + ObsList obsList = getObs(); + + if (obsList.empty()) + { + obsAgeCode = E_ObsAgeCode::NO_OBS; + break; + } + else if (time == GTime::noTime()) + { + // Start epoch not given, use time of first obs as start time + foundGoodObs = true; + dropObs(); + bigObsList += obsList; + break; + } + else if (obsList.front()->time < time - delta) + { + // Save earlier data to preprocess in case preprocess_all_data is on + obsAgeCode = E_ObsAgeCode::PAST_OBS; + dropObs(); + if (foundGoodObs == false) + { + // Only push past obs when good obs not found yet, i.e. drop past obs coming + // late after current ones and continue to find good ones in case data is out of + // order + bigObsList += obsList; + break; + } + } + else if (obsList.front()->time > time + delta) + { + // Future obs, do nothing and leave the data to read later + obsAgeCode = E_ObsAgeCode::FUTURE_OBS; + break; + } + else + { + // Current obs (within epoch tolerance), continue with loop to get all current obs + foundGoodObs = true; + dropObs(); + bigObsList += obsList; + } + } + + if (foundGoodObs) + { + // Future obs may have been attempted (obsAgeCode is now FUTURE_OBS) or no more + // 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; + } + + /** Drop the front observation list from memory when it has been read sucessfully + */ + void dropObs() + { + try + { + auto& obsLister = dynamic_cast(parser); + + if (obsLister.obsListList.size() > 0) + { + obsLister.obsListList.pop_front(); + } + } + catch (...) + { + } + } + + bool hasObs() + { + try + { + auto& obsLister = dynamic_cast(parser); + + if (obsLister.obsListList.empty()) + { + return false; + } + + return true; + } + catch (...) + { + return false; + } + } }; diff --git a/src/cpp/common/streamParser.cpp b/src/cpp/common/streamParser.cpp index e5a06d5d7..aac4ddeb4 100644 --- a/src/cpp/common/streamParser.cpp +++ b/src/cpp/common/streamParser.cpp @@ -1,51 +1,44 @@ - // #pragma GCC optimize ("O0") +#include "common/streamParser.hpp" #include -#include "streamParser.hpp" - - -multimap streamParserMultimap; -map streamDOAMap; - +multimap streamParserMultimap; +map streamDOAMap; - - - -long int streamPos( - std::istream& stream) +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) << "Error: Negative file pos in file at " << filePos << "\n" << " - " << strerror(errno); - - return -1; - } - - return filePos; - } - else - { -// BOOST_LOG_TRIVIAL(error) << "InputStream is dead before destruction "; - - if (stream.eof()) - { -// BOOST_LOG_TRIVIAL(error) << "InputStream has end of file "; - } - return -1; - } + // 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; + } + else + { + // BOOST_LOG_TRIVIAL(error) << "InputStream is dead before destruction "; + + if (stream.eof()) + { + // BOOST_LOG_TRIVIAL(error) << "InputStream has end of file "; + } + return -1; + } } - diff --git a/src/cpp/common/streamParser.hpp b/src/cpp/common/streamParser.hpp index e301c7fdc..91aab7138 100644 --- a/src/cpp/common/streamParser.hpp +++ b/src/cpp/common/streamParser.hpp @@ -1,134 +1,101 @@ - #pragma once -#include -#include -#include -#include -#include -#include -#include - - -using std::multimap; -using std::string; -using std::tuple; -using std::pair; -using std::map; - - +#include +#include +#include #include +#include #include -#include #include #include #include -#include -#include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -namespace B_io = boost::iostreams; -namespace B_asio = boost::asio; +namespace B_io = boost::iostreams; +namespace B_asio = boost::asio; using boost::asio::ip::tcp; - - - - - -long int streamPos( - std::istream& stream); - - - - - - - - - - -#include -#include - -using std::unique_ptr; using std::make_unique; +using std::map; +using std::multimap; +using std::pair; +using std::string; +using std::tuple; +using std::unique_ptr; - +long int streamPos(std::istream& stream); struct Stream { - string sourceString; + string sourceString; + + virtual unique_ptr getIStream_ptr() = 0; - virtual unique_ptr getIStream_ptr() = 0; + /** Check to see if this stream has run out of data + */ + virtual bool isDead() { return false; } - /** Check to see if this stream has run out of data - */ - virtual bool isDead() - { - return false; - } + virtual bool isAvailable() { return true; } - virtual ~Stream() = default; + virtual ~Stream() = default; }; struct Parser { - virtual void parse(std::istream& inputStream) = 0; + virtual void parse(std::istream& inputStream) = 0; - virtual string parserType() = 0; + virtual string parserType() = 0; - virtual ~Parser() = default; + virtual ~Parser() = default; }; - struct SerialStream; struct StreamParser { -private: - unique_ptr stream_ptr; - unique_ptr parser_ptr; - - //for debugging access only - SerialStream* serialStream_ptr; - -public: - Stream& stream; - Parser& parser; - - StreamParser( - unique_ptr stream_ptr, - unique_ptr parser_ptr) - : stream_ptr (std::move(stream_ptr)), - parser_ptr (std::move(parser_ptr)), - stream (*this->stream_ptr), - parser (*this->parser_ptr) - { - serialStream_ptr = (SerialStream*) &stream; - } - - operator Stream&() - { - return stream; - } - - operator Parser&() - { - return parser; - } - - void parse() - { - auto iStream_ptr = stream.getIStream_ptr(); - parser.parse(*iStream_ptr); - } - - virtual ~StreamParser() = default; + private: + unique_ptr stream_ptr; + unique_ptr parser_ptr; + + // for debugging access only + SerialStream* serialStream_ptr; + + public: + Stream& stream; + Parser& parser; + + StreamParser(unique_ptr stream_ptr, unique_ptr parser_ptr) + : stream_ptr(std::move(stream_ptr)), + parser_ptr(std::move(parser_ptr)), + stream(*this->stream_ptr), + parser(*this->parser_ptr) + { + serialStream_ptr = (SerialStream*)&stream; + } + + operator Stream&() { return stream; } + + operator Parser&() { return parser; } + + void parse() + { + auto iStream_ptr = stream.getIStream_ptr(); + parser.parse(*iStream_ptr); + } + + virtual ~StreamParser() = default; }; -typedef std::shared_ptr StreamParserPtr; +typedef std::shared_ptr StreamParserPtr; -extern multimap streamParserMultimap; -extern map streamDOAMap; +extern multimap streamParserMultimap; +extern map streamDOAMap; diff --git a/src/cpp/common/streamRinex.hpp b/src/cpp/common/streamRinex.hpp index 5c6280583..dffb75f29 100644 --- a/src/cpp/common/streamRinex.hpp +++ b/src/cpp/common/streamRinex.hpp @@ -1,47 +1,48 @@ - #pragma once -#include "navigation.hpp" - -#include "streamObs.hpp" - -#include "rinex.hpp" - +#include "common/navigation.hpp" +#include "common/rinex.hpp" +#include "common/streamObs.hpp" struct RinexParser : Parser, ObsLister { - char ctype; - double version; - E_Sys nav_system; - E_TimeSys time_system; - map> sysCodeTypes; - ObsList tempObsList; - RinexStation rnxRec = {}; - - void parse( - std::istream& inputStream) - { - //read some of the input,(up to next epoch header?) - //save outputs to member variables. - //eg. header metadata - //eg. list of (ObsLists) with multiple sats, signals combined for each epoch. - - int stat = 0; - // account for rinex comment in the middle of the file - while ( stat <= 0 - && inputStream) - { - stat = readRnx(inputStream, ctype, tempObsList, nav, rnxRec, version, nav_system, time_system, sysCodeTypes); - } - - if (tempObsList.size() > 0) - { - obsListList.push_back(std::move(tempObsList)); - } - } - - string parserType() - { - return "RinexParser"; - } + char ctype; + double version; + E_Sys nav_system; + E_TimeSys time_system; + map> sysCodeTypes; + ObsList tempObsList; + RinexStation rnxRec = {}; + + void parse(std::istream& inputStream) + { + // read some of the input,(up to next epoch header?) + // save outputs to member variables. + // eg. header metadata + // eg. list of (ObsLists) with multiple sats, signals combined for each epoch. + + int stat = 0; + // account for rinex comment in the middle of the file + while (stat <= 0 && inputStream) + { + stat = readRnx( + inputStream, + ctype, + tempObsList, + nav, + rnxRec, + version, + nav_system, + time_system, + sysCodeTypes + ); + } + + if (tempObsList.size() > 0) + { + obsListList.push_back(std::move(tempObsList)); + } + } + + string parserType() { return "RinexParser"; } }; diff --git a/src/cpp/common/streamRtcm.hpp b/src/cpp/common/streamRtcm.hpp index 10f8fa58e..df291e8e8 100644 --- a/src/cpp/common/streamRtcm.hpp +++ b/src/cpp/common/streamRtcm.hpp @@ -1,263 +1,264 @@ - #pragma once #include - - -#include "streamParser.hpp" -#include "rtcmDecoder.hpp" -#include "streamObs.hpp" -#include "constants.hpp" -#include "otherSSR.hpp" - - -#define CLEAN_UP_AND_RETURN_ON_FAILURE \ - \ - if (inputStream.fail()) \ - { \ - inputStream.clear(); \ - inputStream.seekg(pos); \ - return; \ - } - - +#include "common/constants.hpp" +#include "common/rtcmDecoder.hpp" +#include "common/streamObs.hpp" +#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; \ + } struct RtcmParser : Parser, RtcmDecoder { - bool qzssL6 = false; - vector qzsL6buff; - int qzsL6BitsLeft = 0; - - void parse( - std::istream& inputStream) - { -// std::cout << "Parsing rtcm" << "\n"; - - if (qzssL6) //todo aaron move to own decoder type - { - int pos; - - int mess_state = 0; - const unsigned char L6header[4] = {0x1A,0xCF,0xFC,0x1D}; - - vector frame; - frame.resize(250,0x00); - unsigned char* data = frame.data(); - - - while (inputStream) - { - if (mess_state == 0) - pos = inputStream.tellg(); - - if (mess_state < 4) //todo aaron, change to fifo for preamble - { - unsigned char c; - inputStream.read((char*)&c, 1); CLEAN_UP_AND_RETURN_ON_FAILURE; - if (c == L6header[mess_state]) - data[mess_state++] = c; - else - mess_state = 0; - } - - if (mess_state < 4) - continue; - - inputStream.read((char*)data+4, 2); CLEAN_UP_AND_RETURN_ON_FAILURE; - inputStream.read((char*)data+6,244); CLEAN_UP_AND_RETURN_ON_FAILURE; - - // consider a RS decoder here - - - int sz = (qzsL6BitsLeft + 1695) / 8 + 1; - qzsL6buff.resize(sz); - - unsigned char* buf = qzsL6buff.data(); - - int j = 49; - - for (int i = 0; i < 113; i++) - { - unsigned int tmp = getbituInc(data, j, 15); - qzsL6BitsLeft = setbituInc(buf, qzsL6BitsLeft, 15, tmp); - } - - tracepdeex(6, std::cout,"\n New QZSS L6 frame\n"); - - int frameBits = 1; - while (frameBits > 0) - { - for (auto& byte : qzsL6buff) - tracepdeex(6, std::cout,"%02X", static_cast(byte)); - tracepdeex(6, std::cout,"\n"); - - int i = 0; - int messType = getbituInc(buf, i, 12); - - if (messType == 4073) //todo aaron enum - { - frameBits = decodecompactSSR(qzsL6buff, rtcmTime()); - } - else - { - frameBits = 0; - - if (messType > 0) - { - tracepdeex(1, std::cout," WARNING: Error decoding QZSS L6 messages"); - - while ( messType != 4073 - && (frameBits+13) < qzsL6BitsLeft) - { - frameBits++; - int j = frameBits; - messType = getbituInc(buf, j, 12); - if (messType == 4073) //todo aaron enum - { - int subtype = getbituInc(buf, j, 4); - if ( subtype != 6 - && subtype != 12) - { - messType = 0; - } - } - } - if (messType != 4073) - frameBits = 0; - } - } - - if (frameBits == -2) - { - inputStream.seekg(pos); - tracepdeex(4, std::cout," Future frame detected, waiting"); - return; - } - - if (frameBits == -1) - tracepdeex(4, std::cout," Incomplete frame detected, topping up "); - - if (frameBits == 0) - { - qzsL6buff.clear(); - qzsL6BitsLeft = 0; - } - - if (frameBits > 0) - { - int i = frameBits; - qzsL6BitsLeft -= frameBits; - - tracepdeex(4, std::cout," Valid frame detected (%d Bits)", frameBits); - - int nByte = frameBits / 8 + 1; - int nbit = nByte * 8 - frameBits; - int tmp = getbituInc(buf, i, nbit); - - j = setbituInc(buf, 0, nbit, tmp); - - nbit = qzsL6BitsLeft - nbit; - i = nByte; - while (nbit > 8) - { - j = setbituInc(buf, j, 8, qzsL6buff[i++]); - nbit -= 8; - } - j = setbituInc(buf, j, 8, qzsL6buff[i]); - - nByte = qzsL6BitsLeft / 8 + 1; - qzsL6buff.resize(nByte); - buf = qzsL6buff.data(); - - if (nByte<5) - frameBits=-1; - } - } - mess_state=0; - } - } - else - while (inputStream) - { - int byteCnt = 0; - int pos; - while (true) - { - // Skip to the start of the frame - marked by preamble character 0xD3 - pos = inputStream.tellg(); - - unsigned char c; - inputStream.read((char*)&c, 1); CLEAN_UP_AND_RETURN_ON_FAILURE; - - if (inputStream) - { - if (c == RTCM_PREAMBLE) - { - break; - } - } - else - { - printf("."); - return; - } - - nonFrameByteFound(c); - } - CLEAN_UP_AND_RETURN_ON_FAILURE; - - preambleFound(); - - // Read the frame length - 2 bytes big endian only want 10 bits - char buf[2]; inputStream.read((char*)buf, 2); CLEAN_UP_AND_RETURN_ON_FAILURE; - - // Message length is 10 bits starting at bit 6 - auto messageLength = getbitu((uint8_t*)buf, 6, 10); - auto dataFrameLength = messageLength + 3; - - // Read the frame data (include the header) - vector data(dataFrameLength); inputStream.read((char*)data.data() + 3, messageLength); CLEAN_UP_AND_RETURN_ON_FAILURE; - - // Read the frame CRC - unsigned int crcRead = 0; inputStream.read((char*)&crcRead, 3); CLEAN_UP_AND_RETURN_ON_FAILURE; - - data[0] = RTCM_PREAMBLE; - data[1] = buf[0]; - data[2] = buf[1]; - unsigned int crcCalc = crc24q(data.data(), data.size()); - - if ( (((char*)&crcCalc)[0] != ((char*)&crcRead)[2]) - ||(((char*)&crcCalc)[1] != ((char*)&crcRead)[1]) - ||(((char*)&crcCalc)[2] != ((char*)&crcRead)[0])) - { - checksumFailure(rtcmMountpoint); - -// printHex(std::cout, data); - - inputStream.seekg(pos + 1); - - continue; - } - - checksumSuccess(crcRead); - - recordFrame(data, crcRead); - - //remove the header to get to the meat of the message - auto& message = data; - message.erase(message.begin(), message.begin()+3); - - auto rtcmReturnType = decode(message); - - if (rtcmReturnType == E_ReturnType::GOT_OBS) { return; } - if (rtcmReturnType == E_ReturnType::WAIT) { inputStream.seekg(pos); return; } - - } - } - - string parserType() - { - return "RtcmParser"; - } + bool qzssL6 = false; + vector qzsL6buff; + int qzsL6BitsLeft = 0; + + void parse(std::istream& inputStream) + { + // std::cout << "Parsing rtcm" << "\n"; + + if (qzssL6) // todo aaron move to own decoder type + { + int pos; + + int mess_state = 0; + const unsigned char L6header[4] = {0x1A, 0xCF, 0xFC, 0x1D}; + + vector frame; + frame.resize(250, 0x00); + unsigned char* data = frame.data(); + + while (inputStream) + { + if (mess_state == 0) + pos = inputStream.tellg(); + + if (mess_state < 4) // todo aaron, change to fifo for preamble + { + unsigned char c; + inputStream.read((char*)&c, 1); + CLEAN_UP_AND_RETURN_ON_FAILURE; + if (c == L6header[mess_state]) + data[mess_state++] = c; + else + mess_state = 0; + } + + if (mess_state < 4) + continue; + + inputStream.read((char*)data + 4, 2); + CLEAN_UP_AND_RETURN_ON_FAILURE; + inputStream.read((char*)data + 6, 244); + CLEAN_UP_AND_RETURN_ON_FAILURE; + + // consider a RS decoder here + + int sz = (qzsL6BitsLeft + 1695) / 8 + 1; + qzsL6buff.resize(sz); + + unsigned char* buf = qzsL6buff.data(); + + int j = 49; + + for (int i = 0; i < 113; i++) + { + unsigned int tmp = getbituInc(data, j, 15); + qzsL6BitsLeft = setbituInc(buf, qzsL6BitsLeft, 15, tmp); + } + + tracepdeex(6, std::cout, "\n New QZSS L6 frame\n"); + + int frameBits = 1; + while (frameBits > 0) + { + for (auto& byte : qzsL6buff) + tracepdeex(6, std::cout, "%02X", static_cast(byte)); + tracepdeex(6, std::cout, "\n"); + + int i = 0; + int messType = getbituInc(buf, i, 12); + + if (messType == 4073) // todo aaron enum + { + frameBits = decodecompactSSR(qzsL6buff, rtcmTime()); + } + else + { + frameBits = 0; + + if (messType > 0) + { + tracepdeex(1, std::cout, " WARNING: Error decoding QZSS L6 messages"); + + while (messType != 4073 && (frameBits + 13) < qzsL6BitsLeft) + { + frameBits++; + int j = frameBits; + messType = getbituInc(buf, j, 12); + if (messType == 4073) // todo aaron enum + { + int subtype = getbituInc(buf, j, 4); + if (subtype != 6 && subtype != 12) + { + messType = 0; + } + } + } + if (messType != 4073) + frameBits = 0; + } + } + + if (frameBits == -2) + { + inputStream.seekg(pos); + tracepdeex(4, std::cout, " Future frame detected, waiting"); + return; + } + + if (frameBits == -1) + tracepdeex(4, std::cout, " Incomplete frame detected, topping up "); + + if (frameBits == 0) + { + qzsL6buff.clear(); + qzsL6BitsLeft = 0; + } + + if (frameBits > 0) + { + int i = frameBits; + qzsL6BitsLeft -= frameBits; + + tracepdeex(4, std::cout, " Valid frame detected (%d Bits)", frameBits); + + int nByte = frameBits / 8 + 1; + int nbit = nByte * 8 - frameBits; + int tmp = getbituInc(buf, i, nbit); + + j = setbituInc(buf, 0, nbit, tmp); + + nbit = qzsL6BitsLeft - nbit; + i = nByte; + while (nbit > 8) + { + j = setbituInc(buf, j, 8, qzsL6buff[i++]); + nbit -= 8; + } + j = setbituInc(buf, j, 8, qzsL6buff[i]); + + nByte = qzsL6BitsLeft / 8 + 1; + qzsL6buff.resize(nByte); + buf = qzsL6buff.data(); + + if (nByte < 5) + frameBits = -1; + } + } + mess_state = 0; + } + } + else + while (inputStream) + { + int byteCnt = 0; + int pos; + while (true) + { + // Skip to the start of the frame - marked by preamble character 0xD3 + pos = inputStream.tellg(); + + unsigned char c; + inputStream.read((char*)&c, 1); + CLEAN_UP_AND_RETURN_ON_FAILURE; + + if (inputStream) + { + if (c == RTCM_PREAMBLE) + { + break; + } + } + else + { + printf("."); + return; + } + + nonFrameByteFound(c); + } + CLEAN_UP_AND_RETURN_ON_FAILURE; + + preambleFound(); + + // Read the frame length - 2 bytes big endian only want 10 bits + char buf[2]; + inputStream.read((char*)buf, 2); + CLEAN_UP_AND_RETURN_ON_FAILURE; + + // Message length is 10 bits starting at bit 6 + auto messageLength = getbitu((uint8_t*)buf, 6, 10); + auto dataFrameLength = messageLength + 3; + + // Read the frame data (include the header) + vector data(dataFrameLength); + inputStream.read((char*)data.data() + 3, messageLength); + CLEAN_UP_AND_RETURN_ON_FAILURE; + + // Read the frame CRC + unsigned int crcRead = 0; + inputStream.read((char*)&crcRead, 3); + CLEAN_UP_AND_RETURN_ON_FAILURE; + + data[0] = RTCM_PREAMBLE; + data[1] = buf[0]; + data[2] = buf[1]; + unsigned int crcCalc = crc24q(data.data(), data.size()); + + if ((((char*)&crcCalc)[0] != ((char*)&crcRead)[2]) || + (((char*)&crcCalc)[1] != ((char*)&crcRead)[1]) || + (((char*)&crcCalc)[2] != ((char*)&crcRead)[0])) + { + checksumFailure(rtcmMountpoint); + + // printHex(std::cout, data); + + inputStream.seekg(pos + 1); + + continue; + } + + checksumSuccess(crcRead); + + recordFrame(data, crcRead); + + // remove the header to get to the meat of the message + auto& message = data; + message.erase(message.begin(), message.begin() + 3); + + auto rtcmReturnType = decode(message); + + if (rtcmReturnType == E_ReturnType::GOT_OBS) + { + return; + } + if (rtcmReturnType == E_ReturnType::WAIT) + { + inputStream.seekg(pos); + return; + } + } + } + + string parserType() { return "RtcmParser"; } }; - diff --git a/src/cpp/common/streamSerial.cpp b/src/cpp/common/streamSerial.cpp index 5827f7a11..a6f86dc9a 100644 --- a/src/cpp/common/streamSerial.cpp +++ b/src/cpp/common/streamSerial.cpp @@ -1,17 +1,32 @@ - - +#include "common/streamSerial.hpp" #include -#include -#include - -#include "streamSerial.hpp" +#include "common/platformCompat.hpp" void SerialStream::openStream() { - fileDescriptor = open(path.c_str(), O_RDWR | O_NONBLOCK); - - if (fileDescriptor < 0) - { - std::cout << "\n" << "Error opening " << path << " as SerialStream"; - } +#ifdef _WIN32 + fileDescriptor = CreateFileA( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + 0, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + + if (fileDescriptor == INVALID_HANDLE_VALUE) + { + std::cout << "\n" + << "Error opening " << path << " as SerialStream"; + } +#else + fileDescriptor = open(path.c_str(), O_RDWR | O_NONBLOCK); + + if (fileDescriptor < 0) + { + std::cout << "\n" + << "Error opening " << path << " as SerialStream"; + } +#endif } diff --git a/src/cpp/common/streamSerial.hpp b/src/cpp/common/streamSerial.hpp index f86ce9be0..23803238f 100644 --- a/src/cpp/common/streamSerial.hpp +++ b/src/cpp/common/streamSerial.hpp @@ -1,136 +1,134 @@ - #pragma once -#include -#include - +#include +#include #include #include +#include "common/platformCompat.hpp" +#include "common/streamParser.hpp" + +namespace B_io = boost::iostreams; using std::string; using std::vector; - -#include -#include - -namespace B_io = boost::iostreams; - - -#include "streamParser.hpp" - - - struct SerialStateMembers { - vector& inputVector; - B_io::basic_array_source input_source; - - SerialStateMembers( - vector& inputVector) - : inputVector (inputVector), - input_source (B_io::basic_array_source (inputVector.data(), inputVector.size())) - { - - } + vector& inputVector; + B_io::basic_array_source input_source; + + SerialStateMembers(vector& inputVector) + : inputVector(inputVector), + input_source(B_io::basic_array_source(inputVector.data(), inputVector.size())) + { + } }; struct SerialState : SerialStateMembers, B_io::stream> { - SerialState( - vector& inputSource) - : SerialStateMembers (inputSource), - B_io::stream> (input_source) - { -// std::cout << "Serial State created, has length " << inputVector.size() << "\n"; - } - - ~SerialState() - { - long int pos = streamPos(*this); - - if (pos == 0) - { - return; - } - else if (pos > 0) - { - inputVector.erase(inputVector.begin(), inputVector.begin() + pos); - } - else - { - inputVector.clear(); - } - -// std::cout << "Serial State destroyed, has length " << inputVector.size() << "\n"; - } + SerialState(vector& inputSource) + : SerialStateMembers(inputSource), + B_io::stream>(input_source) + { + // std::cout << "Serial State created, has length " << inputVector.size() << "\n"; + } + + ~SerialState() + { + long int pos = streamPos(*this); + + if (pos == 0) + { + return; + } + else if (pos > 0) + { + inputVector.erase(inputVector.begin(), inputVector.begin() + pos); + } + else + { + inputVector.clear(); + } + + // std::cout << "Serial State destroyed, has length " << inputVector.size() << "\n"; + } }; struct SerialStream : Stream { - string path; - - int fileDescriptor = -1; - - vector receivedData; - - SerialStream() - { - - } - - SerialStream( - string path) - : path (path) - { - openStream(); - } - - void openStream(); - - virtual void getData() - { - if (fileDescriptor < 0) - { - return; - } - - while (1) - { - const int reserve = 0x4000; - int oldSize = receivedData.size(); - - receivedData.resize(receivedData.size() + reserve); - - - int n = read(fileDescriptor, &receivedData[oldSize], reserve); - - receivedData.resize(oldSize + n); - - if (n == 0) - { - break; - } - } - } - - unique_ptr getIStream_ptr() override - { - getData(); - - return make_unique(receivedData); - } - - virtual ~SerialStream() - { - if (fileDescriptor < 0) - { - return; - } - - close(fileDescriptor); - }; + string path; + +#ifdef _WIN32 + HANDLE fileDescriptor = INVALID_HANDLE_VALUE; +#else + int fileDescriptor = -1; +#endif + + vector receivedData; + + SerialStream() {} + + SerialStream(string path) : path(path) { openStream(); } + + void openStream(); + + virtual void getData() + { +#ifdef _WIN32 + if (fileDescriptor == INVALID_HANDLE_VALUE) +#else + if (fileDescriptor < 0) +#endif + { + return; + } + + while (1) + { + const int reserve = 0x4000; + int oldSize = receivedData.size(); + + receivedData.resize(receivedData.size() + reserve); + +#ifdef _WIN32 + DWORD bytesRead = 0; + BOOL success = + ReadFile(fileDescriptor, &receivedData[oldSize], reserve, &bytesRead, NULL); + int n = success ? bytesRead : 0; +#else + int n = read(fileDescriptor, &receivedData[oldSize], reserve); +#endif + + receivedData.resize(oldSize + n); + + if (n == 0) + { + break; + } + } + } + + unique_ptr getIStream_ptr() override + { + getData(); + + return make_unique(receivedData); + } + + virtual ~SerialStream() + { +#ifdef _WIN32 + if (fileDescriptor == INVALID_HANDLE_VALUE) + { + return; + } + CloseHandle(fileDescriptor); +#else + if (fileDescriptor < 0) + { + return; + } + close(fileDescriptor); +#endif + }; }; - - - diff --git a/src/cpp/common/streamSlr.hpp b/src/cpp/common/streamSlr.hpp index c1d655aed..ffc5bb1a0 100644 --- a/src/cpp/common/streamSlr.hpp +++ b/src/cpp/common/streamSlr.hpp @@ -1,43 +1,33 @@ - #pragma once -#include "streamObs.hpp" -#include "slr.hpp" +#include "common/streamObs.hpp" +#include "slr/slr.hpp" /** Interface for slr streams -*/ + */ struct SlrParser : Parser, ObsLister { - SlrParser() - { - - } - - void parse( - std::istream& inputStream) - { - ObsList obsList; - //read some of the input - //save outputs to member variables. - //dont parse all, just some. - - //this structure to match real-time architecture. - int stat = 0; - while ( stat<=0 - &&inputStream) - { - stat = readSlrObs(inputStream, obsList); - } - - if (obsList.size() > 0) - { - obsListList.push_back(std::move(obsList)); - } - } - - string parserType() - { - return "SlrParser"; - } + SlrParser() {} + + void parse(std::istream& inputStream) + { + ObsList obsList; + // read some of the input + // save outputs to member variables. + // dont parse all, just some. + + // this structure to match real-time architecture. + int stat = 0; + while (stat <= 0 && inputStream) + { + stat = readSlrObs(inputStream, obsList); + } + + if (obsList.size() > 0) + { + obsListList.push_back(std::move(obsList)); + } + } + + string parserType() { return "SlrParser"; } }; - diff --git a/src/cpp/common/streamSp3.hpp b/src/cpp/common/streamSp3.hpp index 7894aaf7b..f616f0254 100644 --- a/src/cpp/common/streamSp3.hpp +++ b/src/cpp/common/streamSp3.hpp @@ -1,45 +1,38 @@ - #pragma once -#include "streamObs.hpp" -#include "ephemeris.hpp" +#include "common/ephemeris.hpp" +#include "common/streamObs.hpp" struct Sp3Parser : Parser, ObsLister { - ObsList tempObsList; - - - E_TimeSys tsys = E_TimeSys::GPST; - - double bfact[2] = {}; - - void parse( - std::istream& inputStream) - { - vector pephList; - - bool more = readsp3(inputStream, pephList, 0, tsys, bfact); - - for (auto& peph : pephList) - { - PObs pObs; - pObs.pos = peph.pos; - pObs.vel = peph.vel; - pObs.time = peph.time; - pObs.Sat = peph.Sat; - - tempObsList.push_back((shared_ptr)pObs); - } - - if (tempObsList.size() > 0) - { - obsListList.push_back(std::move(tempObsList)); - } - } - - string parserType() - { - return "Sp3Parser"; - } -}; + ObsList tempObsList; + + E_TimeSys tsys = E_TimeSys::GPST; + + double bfact[2] = {}; + + void parse(std::istream& inputStream) + { + vector pephList; + bool more = readsp3(inputStream, pephList, 0, tsys, bfact); + + for (auto& peph : pephList) + { + PObs pObs; + pObs.pos = peph.pos; + pObs.vel = peph.vel; + pObs.time = peph.time; + pObs.Sat = peph.Sat; + + tempObsList.push_back((shared_ptr)pObs); + } + + if (tempObsList.size() > 0) + { + obsListList.push_back(std::move(tempObsList)); + } + } + + string parserType() { return "Sp3Parser"; } +}; diff --git a/src/cpp/common/streamUbx.cpp b/src/cpp/common/streamUbx.cpp index 1d0a5ff2f..2042eca8e 100644 --- a/src/cpp/common/streamUbx.cpp +++ b/src/cpp/common/streamUbx.cpp @@ -1,86 +1,88 @@ - +#include "common/streamUbx.hpp" #include +#include "common/packetStatistics.hpp" +#define CLEAN_UP_AND_RETURN_ON_FAILURE \ + \ + if (inputStream.fail()) \ + { \ + inputStream.clear(); \ + inputStream.seekg(pos); \ + return; \ + } + +void UbxParser::parse(std::istream& inputStream) +{ + // std::cout << "Parsing ubx" << "\n"; -#include "packetStatistics.hpp" -#include "streamUbx.hpp" + while (inputStream) + { + int pos; + unsigned char c1 = 0; + unsigned char c2 = 0; + while (true) + { + pos = inputStream.tellg(); + // move c2 back one and replace, (check one byte at a time, not in pairs) + c1 = c2; + inputStream.read((char*)&c2, 1); -#define CLEAN_UP_AND_RETURN_ON_FAILURE \ - \ - if (inputStream.fail()) \ - { \ - inputStream.clear(); \ - inputStream.seekg(pos); \ - return; \ - } + if (inputStream) + { + if (c1 == UBX_PREAMBLE1 && c2 == UBX_PREAMBLE2) + { + break; + } + } + else + { + return; + } + nonFrameByteFound(c2); + } + CLEAN_UP_AND_RETURN_ON_FAILURE; + // account for 2 byte preamble + pos--; -void UbxParser::parse( - std::istream& inputStream) -{ -// std::cout << "Parsing ubx" << "\n"; - - while (inputStream) - { - int pos; - unsigned char c1 = 0; - unsigned char c2 = 0; - while (true) - { - pos = inputStream.tellg(); + preambleFound(); + + unsigned char ubxClass = 0; + inputStream.read((char*)&ubxClass, 1); + CLEAN_UP_AND_RETURN_ON_FAILURE; + unsigned char id = 0; + inputStream.read((char*)&id, 1); + CLEAN_UP_AND_RETURN_ON_FAILURE; + unsigned short int payload_length = 0; + inputStream.read((char*)&payload_length, 2); + CLEAN_UP_AND_RETURN_ON_FAILURE; + vector payload(payload_length); + inputStream.read((char*)payload.data(), payload_length); + CLEAN_UP_AND_RETURN_ON_FAILURE; // Read the frame data (include the header) + unsigned short int crcRead = 0; + inputStream.read((char*)&crcRead, 2); + CLEAN_UP_AND_RETURN_ON_FAILURE; + + // todo aaron calculate crcRead + if (0) + { + checksumFailure(); + + inputStream.seekg(pos + 1); + + continue; + } - //move c2 back one and replace, (check one byte at a time, not in pairs) - c1 = c2; - inputStream.read((char*)&c2, 1); + checksumSuccess(); - if (inputStream) - { - if ( c1 == UBX_PREAMBLE1 - &&c2 == UBX_PREAMBLE2) - { - break; - } - } - else - { - return; - } - nonFrameByteFound(c2); - } - CLEAN_UP_AND_RETURN_ON_FAILURE; + recordFrame(ubxClass, id, payload, crcRead); - //account for 2 byte preamble - pos--; - - preambleFound(); + decode(ubxClass, id, payload); - unsigned char ubxClass = 0; inputStream.read((char*)&ubxClass, 1); CLEAN_UP_AND_RETURN_ON_FAILURE; - unsigned char id = 0; inputStream.read((char*)&id, 1); CLEAN_UP_AND_RETURN_ON_FAILURE; - unsigned short int payload_length = 0; inputStream.read((char*)&payload_length, 2); CLEAN_UP_AND_RETURN_ON_FAILURE; - vector payload(payload_length); inputStream.read((char*)payload.data(), payload_length); CLEAN_UP_AND_RETURN_ON_FAILURE; // Read the frame data (include the header) - unsigned short int crcRead = 0; inputStream.read((char*)&crcRead, 2); CLEAN_UP_AND_RETURN_ON_FAILURE; - - //todo aaron calculate crcRead - if (0) - { - checksumFailure(); - - inputStream.seekg(pos + 1); - - continue; - } - - checksumSuccess(); - - recordFrame(ubxClass, id, payload, crcRead); - - decode(ubxClass, id, payload); - - if (obsListList.size() > 2) - { - return; - } - } - inputStream.clear(); + if (obsListList.size() > 2) + { + return; + } + } + inputStream.clear(); } - diff --git a/src/cpp/common/streamUbx.hpp b/src/cpp/common/streamUbx.hpp index 65c6c9c7e..bf342b921 100644 --- a/src/cpp/common/streamUbx.hpp +++ b/src/cpp/common/streamUbx.hpp @@ -1,23 +1,16 @@ - #pragma once - -#include "packetStatistics.hpp" -#include "streamParser.hpp" -#include "ubxDecoder.hpp" -#include "streamObs.hpp" +#include "common/packetStatistics.hpp" +#include "common/streamObs.hpp" +#include "common/streamParser.hpp" +#include "common/ubxDecoder.hpp" #define UBX_PREAMBLE1 0xB5 #define UBX_PREAMBLE2 0x62 struct UbxParser : Parser, UbxDecoder, PacketStatistics { - void parse( - std::istream& inputStream); - - string parserType() - { - return "UbxParser"; - } -}; + void parse(std::istream& inputStream); + string parserType() { return "UbxParser"; } +}; diff --git a/src/cpp/common/summary.cpp b/src/cpp/common/summary.cpp index bb6926057..30f4d68a5 100644 --- a/src/cpp/common/summary.cpp +++ b/src/cpp/common/summary.cpp @@ -1,75 +1,103 @@ - // #pragma GCC optimize ("O0") -#include "interactiveTerminal.hpp" -#include "receiver.hpp" -#include "summary.hpp" +#include "common/summary.hpp" +#include "common/acsConfig.hpp" +#include "common/receiver.hpp" void outputStatistics( - Trace& trace, - map& statisticsMap, - map& statisticsMapSum) + Trace& trace, + map& statisticsMap, + map& statisticsMapSum +) { - for (auto& [str, count] : statisticsMap) - { - statisticsMapSum[str] += count; - } + if (acsConfig.output_statistics == false) + { + return; + } + + for (auto& [str, count] : statisticsMap) + { + statisticsMapSum[str] += count; + } - { InteractiveTerminal ss( "Filter-Statistics/Epoch", trace); - Block block(ss, "Filter-Statistics/Epoch"); for (auto& [str, count] : statisticsMap) tracepdeex(0, ss, "! %-40s: %d\n", str.c_str(), count); } - { InteractiveTerminal ss( "Filter-Statistics/Total", trace); - Block block(ss, "Filter-Statistics/Total"); for (auto& [str, count] : statisticsMapSum) tracepdeex(0, ss, "! %-40s: %d\n", str.c_str(), count); } + { + Block block(trace, "Filter-Statistics/Epoch"); + for (auto& [str, count] : statisticsMap) + tracepdeex(0, trace, "! %-75s: %d\n", str.c_str(), count); + } + { + Block block(trace, "Filter-Statistics/Total"); + for (auto& [str, count] : statisticsMapSum) + tracepdeex(0, trace, "! %-75s: %d\n", str.c_str(), count); + } - statisticsMap.clear(); + statisticsMap.clear(); } /** Output statistics from each station. -* Including observation counts, slips, beginning and ending epochs*/ + * Including observation counts, slips, beginning and ending epochs*/ void outputSummaries( - Trace& trace, ///< Trace stream to output to - ReceiverMap& receiverMap) ///< Map of stations used throughout the program. + Trace& trace, ///< Trace stream to output to + ReceiverMap& receiverMap ///< Map of stations used throughout the program. +) { - trace << "\n" << "--------------- SUMMARIES ------------------- " << "\n"; + if (acsConfig.output_summaries == false) + { + return; + } + + trace << "\n" + << "--------------- SUMMARIES ------------------- " << "\n"; - for (auto& [id, rec] : receiverMap) - { - trace << "\n" << "------------------- " << rec.id << " --------------------"; - auto a = boost::posix_time::from_time_t((time_t)rec.firstEpoch.bigTime); - auto b = boost::posix_time::from_time_t((time_t)rec.lastEpoch. bigTime); - auto ab = b-a; + for (auto& [id, rec] : receiverMap) + { + trace << "\n" + << "------------------- " << rec.id << " --------------------"; + auto a = boost::posix_time::from_time_t((time_t)rec.firstEpoch.bigTime); + auto b = boost::posix_time::from_time_t((time_t)rec.lastEpoch.bigTime); + auto ab = b - a; - trace << "\n" << "First Epoch : " << a; - trace << "\n" << "Last Epoch : " << b; - trace << "\n" << "Epoch Count : " << rec.epochCount; - if (rec.epochCount > 1) - trace << "\n" << "Epoch Step : " << ab / (rec.epochCount - 1); - trace << "\n" << "Duration : " << ab; - trace << "\n" << "Observations: " << rec.obsCount; + trace << "\n" + << "First Epoch : " << a; + trace << "\n" + << "Last Epoch : " << b; + trace << "\n" + << "Epoch Count : " << rec.epochCount; + if (rec.epochCount > 1) + trace << "\n" + << "Epoch Step : " << ab / (rec.epochCount - 1); + trace << "\n" + << "Duration : " << ab; + trace << "\n" + << "Observations: " << rec.obsCount; - bool first = true; - trace << "\n" << "By Code : "; - for (auto& [code, count] : rec.codeCount) - { - if (first) - first = false; - else - trace << " | "; + bool first = true; + trace << "\n" + << "By Code : "; + for (auto& [code, count] : rec.codeCount) + { + if (first) + first = false; + else + trace << " | "; - trace << code._to_string() << " : " << count; - } + trace << enum_to_string(code) << " : " << count; + } - first = true; - trace << "\n" << "By Satellite: "; - for (auto& [sat, count] : rec.satCount) - { - if (first) - first = false; - else - trace << " | "; + first = true; + trace << "\n" + << "By Satellite: "; + for (auto& [sat, count] : rec.satCount) + { + if (first) + first = false; + else + trace << " | "; - trace << sat << " : " << count; - } - trace << "\n" << "GObs/Slips : " << rec.obsCount / (rec.slipCount + 1); - trace << "\n"; - } + trace << sat << " : " << count; + } + trace << "\n" + << "GObs/Slips : " << rec.obsCount / (rec.slipCount + 1); + trace << "\n"; + } } diff --git a/src/cpp/common/summary.hpp b/src/cpp/common/summary.hpp index bd5a01e8d..984b37cd8 100644 --- a/src/cpp/common/summary.hpp +++ b/src/cpp/common/summary.hpp @@ -1,19 +1,16 @@ - #pragma once #include +#include "common/trace.hpp" using std::map; -#include "trace.hpp" - struct ReceiverMap; void outputStatistics( - Trace& trace, - map& statisticsMap, - map& statisticsMapSum); + Trace& trace, + map& statisticsMap, + map& statisticsMapSum +); -void outputSummaries( - Trace& trace, - ReceiverMap& receiverMap); +void outputSummaries(Trace& trace, ReceiverMap& receiverMap); diff --git a/src/cpp/common/tcpSocket.cpp b/src/cpp/common/tcpSocket.cpp index 4924f40e0..56942b42d 100644 --- a/src/cpp/common/tcpSocket.cpp +++ b/src/cpp/common/tcpSocket.cpp @@ -1,595 +1,622 @@ - // #pragma GCC optimize ("O0") - // #define BSONCXX_POLY_USE_MNMLSTC // #define BSONCXX_POLY_USE_SYSTEM_MNMLSTC -#include -#include - +#include "common/tcpSocket.hpp" +#include #include - #include - - -#include "streamNtrip.hpp" -#include "tcpSocket.hpp" -#include "acsConfig.hpp" - - -using std::chrono::system_clock; -using bsoncxx::builder::basic::kvp; +#include "common/acsConfig.hpp" +#include "common/streamNtrip.hpp" namespace bp = boost::asio::placeholders; -B_asio::io_service TcpSocket::ioService; +using std::chrono::system_clock; +B_asio::io_context TcpSocket::ioContext; void TcpSocket::logChunkError() { -// if (numberValidChunks == 0) -// return; - - numberErroredChunks++; - - std::stringstream message; - message << "HTTP chunk error, number of errors : "; - message << numberErroredChunks; -// message << ", ratio (error/total) : " << (double)numberErroredChunks /(double)(numberErroredChunks+numberValidChunks); - - std::cout << message.str() << "\n"; - messageChunkLog(message.str()); - - //todo aaron -// std::ofstream outStream(rtcmTraceFilename, std::ios::app); -// if (!outStream) -// { -// std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; -// return; -// } -// -// outStream << timeGet(); -// outStream << " messageChunkLog" << message << "\n"; + // if (numberValidChunks == 0) + // return; + + numberErroredChunks++; + + std::stringstream message; + message << "HTTP chunk error, number of errors : "; + message << numberErroredChunks; + // message << ", ratio (error/total) : " << (double)numberErroredChunks + // /(double)(numberErroredChunks+numberValidChunks); + + std::cout << message.str() << "\n"; + messageChunkLog(message.str()); + + // todo aaron + // std::ofstream outStream(rtcmTraceFilename, std::ios::app); + // if (!outStream) + // { + // std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << + // "\n"; return; + // } + // + // outStream << timeGet(); + // outStream << " messageChunkLog" << message << "\n"; } - -void TcpSocket::startRead( - bool chunked) +void TcpSocket::startRead(bool chunked) { - auto function_ptr = &TcpSocket::readHandlerContent; - - if (chunked) function_ptr = &TcpSocket::readHandlerChunked; - else function_ptr = &TcpSocket::readHandlerContent; - - //BOOST_LOG_TRIVIAL(debug) << "TcpSocket::start_read\n"; - //BOOST_LOG_TRIVIAL(debug) << "Downloading, length : " << content_length << "\n"; - //BOOST_LOG_TRIVIAL(debug) << "downloadBuf.size() : " << downloadBuf.size() << "\n"; - - // Start reading remaining data until EOF. - if (url.protocol == "https") - { - boost::asio::async_read(*_sslsocket, downloadBuf, boost::asio::transfer_at_least(1), boost::bind(function_ptr, this, bp::error)); - } - else - { - boost::asio::async_read(*_socket, downloadBuf, boost::asio::transfer_at_least(1), boost::bind(function_ptr, this, bp::error)); - } + auto function_ptr = &TcpSocket::readHandlerContent; + + if (chunked) + function_ptr = &TcpSocket::readHandlerChunked; + else + function_ptr = &TcpSocket::readHandlerContent; + + // BOOST_LOG_TRIVIAL(debug) << "TcpSocket::start_read\n"; + // BOOST_LOG_TRIVIAL(debug) << "Downloading, length : " << content_length << "\n"; + // BOOST_LOG_TRIVIAL(debug) << "downloadBuf.size() : " << downloadBuf.size() << "\n"; + + // Start reading remaining data until EOF. + if (url.protocol == "https") + { + boost::asio::async_read( + *_sslsocket, + downloadBuf, + boost::asio::transfer_at_least(1), + boost::bind(function_ptr, this, bp::error) + ); + } + else + { + boost::asio::async_read( + *_socket, + downloadBuf, + boost::asio::transfer_at_least(1), + boost::bind(function_ptr, this, bp::error) + ); + } } - -void TcpSocket::readHandlerContent( - const boost::system::error_code& err) +void TcpSocket::readHandlerContent(const boost::system::error_code& err) { - if (err) - { - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } + if (err) + { + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } - //BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; - //BOOST_LOG_TRIVIAL(debug) << "Downloading, length : " << content_length; - //BOOST_LOG_TRIVIAL(debug) << "downloadBuf.size() : " << downloadBuf.size(); - if (downloadBuf.size() == content_length) - { - vector content(downloadBuf.size()); - buffer_copy(boost::asio::buffer(content), downloadBuf.data()); + // BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + // BOOST_LOG_TRIVIAL(debug) << "Downloading, length : " << content_length; + // BOOST_LOG_TRIVIAL(debug) << "downloadBuf.size() : " << downloadBuf.size(); + if (downloadBuf.size() == content_length) + { + vector content(downloadBuf.size()); + buffer_copy(boost::asio::buffer(content), downloadBuf.data()); - readContentDownloaded(content); + readContentDownloaded(content); - return; - } + return; + } - startRead(false); + startRead(false); } - -void TcpSocket::readHandlerChunked( - const boost::system::error_code& err) +void TcpSocket::readHandlerChunked(const boost::system::error_code& err) { - if (err) - { - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } - - // Note downloadBuf can be filled with data past the "\r\n" termination. - // We read whole lines only into receivedBuffer. - // Messages are chuncked NTRIP Version 2 see RCTM NTRIP document. - - // The message should begin with the message header containing the length in hexadecimal - // ascii charaters followed by carriage return and line feed. - // "AE", this routine recurses ancyronously searching the stream for a valid header. - // If there is error it starts searching again. Once a valided header is found it places - // the RTCM message in the buffer provided this can be done without error. - - // The async_read invokes this function every time it reads "", although - // it can read more data as so downloadBuf may not end with "". - - // If there is a problem with the chunking attempt is made to pass the message to the - // RTCM parser. The RTCM parser has further error checking and may recover some messages. - - onChunkReceivedStatistics(); - -// std::istream messStream(&downloadBuf); -// unsigned int sz = downloadBuf.size(); - -// std::cout << " size" << downloadBuf.size() << "\n"; - int oldSize = receivedTcpData .size(); - int extraSize = downloadBuf .size(); - - receivedTcpData.resize(oldSize + extraSize); - auto destination = boost::asio::buffer(&receivedTcpData[oldSize], extraSize); - buffer_copy(destination, downloadBuf.data()); - downloadBuf.consume(extraSize); - - int last = receivedTcpData.size(); - - int start = 0; - if (receivedTcpData.empty() == false) - while (true) - { - int endOfLength = 0; - int endOfHeader; - for (endOfHeader = start + 1; endOfHeader < last; endOfHeader++) - { - unsigned char c1 = receivedTcpData[endOfHeader - 1]; - unsigned char c2 = receivedTcpData[endOfHeader]; - - if ( c1 == ';') - { - endOfLength = endOfHeader - 1; - } - - if ( c1 == '\r' - &&c2 == '\n') - { - break; - } - } - - if (endOfHeader >= last) - { - break; - } - - if (endOfLength == 0) - { - endOfLength = endOfHeader - 2; - } - - if (endOfLength < 0) - { - start = 2; - continue; - } - - string hexLength(&receivedTcpData[start], &receivedTcpData[endOfLength + 1]); -// std::cout << "\nhexLength: " << hexLength << "\n"; - - int messageLength; - try - { - messageLength = std::stoi(hexLength, 0, 16); - } - catch (std::exception& e) - { - BOOST_LOG_TRIVIAL(warning) << "\nError Message Header, string not an integer: " << hexLength; - logChunkError(); - start = endOfHeader; - continue; - } - - if (messageLength > 10000) - { - BOOST_LOG_TRIVIAL(warning) << "\nError Message Header, Body too Long: " << messageLength; - logChunkError(); - start = endOfHeader; - continue; - } - - int startOfMessage = endOfHeader + 1; - int endOfMessage = startOfMessage + messageLength - 1; - - if (endOfMessage + 2 >= receivedTcpData.size()) - { - //not enough data, continue from this start later - break; - } - - - char postAmble1 = receivedTcpData[endOfMessage + 1]; - char postAmble2 = receivedTcpData[endOfMessage + 2]; - - if ( postAmble1 != '\r' - ||postAmble2 != '\n') - { - BOOST_LOG_TRIVIAL(warning) << "\nMissing Termination of Chunk Message.\n"; - start = endOfHeader; - continue; - } -// printf("\npostamble: %02x %02x\n", postAmble1, postAmble2); - - vector chunk(&receivedTcpData[startOfMessage], &receivedTcpData[endOfMessage] + 1); -// printHex(std::cout, chunk); - dataChunkDownloaded(chunk); - - start = endOfMessage + 2; - } - - if (start > 0) - { - receivedTcpData.erase(receivedTcpData.begin(), receivedTcpData.begin() + start); - } - - startRead(true); + if (err) + { + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } + + // Note downloadBuf can be filled with data past the "\r\n" termination. + // We read whole lines only into receivedBuffer. + // Messages are chuncked NTRIP Version 2 see RCTM NTRIP document. + + // The message should begin with the message header containing the length in hexadecimal + // ascii charaters followed by carriage return and line feed. + // "AE", this routine recurses ancyronously searching the stream for a valid header. + // If there is error it starts searching again. Once a valided header is found it places + // the RTCM message in the buffer provided this can be done without error. + + // The async_read invokes this function every time it reads "", although + // it can read more data as so downloadBuf may not end with "". + + // If there is a problem with the chunking attempt is made to pass the message to the + // RTCM parser. The RTCM parser has further error checking and may recover some messages. + + onChunkReceivedStatistics(); + + // std::istream messStream(&downloadBuf); + // unsigned int sz = downloadBuf.size(); + + // std::cout << " size" << downloadBuf.size() << "\n"; + int oldSize = receivedTcpData.size(); + int extraSize = downloadBuf.size(); + + receivedTcpData.resize(oldSize + extraSize); + auto destination = boost::asio::buffer(&receivedTcpData[oldSize], extraSize); + buffer_copy(destination, downloadBuf.data()); + downloadBuf.consume(extraSize); + + int last = receivedTcpData.size(); + + int start = 0; + if (receivedTcpData.empty() == false) + while (true) + { + int endOfLength = 0; + int endOfHeader; + for (endOfHeader = start + 1; endOfHeader < last; endOfHeader++) + { + unsigned char c1 = receivedTcpData[endOfHeader - 1]; + unsigned char c2 = receivedTcpData[endOfHeader]; + + if (c1 == ';') + { + endOfLength = endOfHeader - 1; + } + + if (c1 == '\r' && c2 == '\n') + { + break; + } + } + + if (endOfHeader >= last) + { + break; + } + + if (endOfLength == 0) + { + endOfLength = endOfHeader - 2; + } + + if (endOfLength < 0) + { + start = 2; + continue; + } + + string hexLength(&receivedTcpData[start], &receivedTcpData[endOfLength + 1]); + // std::cout << "\nhexLength: " << hexLength << "\n"; + + int messageLength; + try + { + messageLength = std::stoi(hexLength, 0, 16); + } + catch (std::exception& e) + { + BOOST_LOG_TRIVIAL(warning) + << "Error Message Header, string not an integer: " << hexLength; + logChunkError(); + start = endOfHeader; + continue; + } + + if (messageLength > 10000) + { + BOOST_LOG_TRIVIAL(warning) + << "Error Message Header, Body too Long: " << messageLength; + logChunkError(); + start = endOfHeader; + continue; + } + + int startOfMessage = endOfHeader + 1; + int endOfMessage = startOfMessage + messageLength - 1; + + if (endOfMessage + 2 >= receivedTcpData.size()) + { + // not enough data, continue from this start later + break; + } + + char postAmble1 = receivedTcpData[endOfMessage + 1]; + char postAmble2 = receivedTcpData[endOfMessage + 2]; + + if (postAmble1 != '\r' || postAmble2 != '\n') + { + BOOST_LOG_TRIVIAL(warning) << "Missing Termination of Chunk Message.\n"; + start = endOfHeader; + continue; + } + // printf("\npostamble: %02x %02x\n", postAmble1, postAmble2); + + vector chunk( + &receivedTcpData[startOfMessage], + &receivedTcpData[endOfMessage] + 1 + ); + // printHex(std::cout, chunk); + dataChunkDownloaded(chunk); + + start = endOfMessage + 2; + } + + if (start > 0) + { + receivedTcpData.erase(receivedTcpData.begin(), receivedTcpData.begin() + start); + } + + startRead(true); } - -void TcpSocket::reconnectTimerHandler( - const boost::system::error_code& err) +void TcpSocket::reconnectTimerHandler(const boost::system::error_code& err) { - if (err) - { - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } + if (err) + { + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } - connect(); + connect(); } -void TcpSocket::timeoutHandler( - const boost::system::error_code& err) +void TcpSocket::timeoutHandler(const boost::system::error_code& err) { - if (err) - { -// ERROR_OUTPUT_RECONNECT_AND_RETURN; - return; - } - - if (isConnected == false) - { - BOOST_LOG_TRIVIAL(error) << "Error: " << url.sanitised() <<" connection timed out, check paths, usernames + passwords, and ports"; - delayedReconnect(); - } + if (err) + { + // ERROR_OUTPUT_RECONNECT_AND_RETURN; + return; + } + + if (isConnected == false) + { + BOOST_LOG_TRIVIAL( + error + ) << url.sanitised() + << " connection timed out, check paths, usernames + passwords, and ports"; + delayedReconnect(); + } } void TcpSocket::delayedReconnect() { - if (isConnected) - { - isConnected = false; - disconnectionCount++; - - networkLog(""); - } - else - { - // If the network does not connect after 10 seconds print the HTTP request and receive. - if (logHttpSentReceived == false) - { - logHttpSentReceived = true; - } - } - - disconnect(); - - //BOOST_LOG_TRIVIAL(debug) << " " << __FUNCTION__ << " " << url.sanitised() << " Started Timer.\n"; - - // Delay and attempt reconnect, this prevents server abuse. - timer.expires_from_now(boost::posix_time::seconds((int)reconnectDelay)); - - //wait a little longer next time; - reconnectDelay *= 2; - - timer.async_wait(boost::bind(&TcpSocket::reconnectTimerHandler, this, bp::error)); + if (isConnected) + { + isConnected = false; + disconnectionCount++; + + networkLog(""); + } + else + { + // If the network does not connect after 10 seconds print the HTTP request and receive. + if (logHttpSentReceived == false) + { + logHttpSentReceived = true; + } + } + + disconnect(); + + // BOOST_LOG_TRIVIAL(debug) << " " << __FUNCTION__ << " " << url.sanitised() << " Started + // Timer.\n"; + + // Delay and attempt reconnect, this prevents server abuse. + timer.expires_from_now(boost::posix_time::seconds((int)reconnectDelay)); + + // wait a little longer next time; + reconnectDelay *= 2; + + timer.async_wait(boost::bind(&TcpSocket::reconnectTimerHandler, this, bp::error)); } -void NtripStream::requestResponseHandler( - const boost::system::error_code& err) +void NtripResponder::requestResponseHandler(const boost::system::error_code& err) { - if (err) - { - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } - - onChunkReceivedStatistics(); - - vector responseVec; - responseVec.resize(downloadBuf.size()); - buffer_copy(boost::asio::buffer(responseVec), downloadBuf.data()); + if (err) + { + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } - responseString.assign(responseVec.begin(), responseVec.end()); + onChunkReceivedStatistics(); - size_t pos = responseString.find("\r\n\r\n"); + vector responseVec; + responseVec.resize(downloadBuf.size()); + buffer_copy(boost::asio::buffer(responseVec), downloadBuf.data()); - if (pos == string::npos) - { - BOOST_LOG_TRIVIAL(error) << "Error handle_request_response : Invalid Server Response"; - responseString = ""; - delayedReconnect(); - return; - } + responseString.assign(responseVec.begin(), responseVec.end()); - responseString = responseString.substr(0, pos); - responseString += "\r\n\r\n"; + size_t pos = responseString.find("\r\n\r\n"); - // Note read buffer can be longer than the supplied delimiter. - downloadBuf.consume(pos+4); + if (pos == string::npos) + { + BOOST_LOG_TRIVIAL(error) << "Error handle_request_response : Invalid Server Response"; + responseString = ""; + delayedReconnect(); + return; + } + responseString = responseString.substr(0, pos); + responseString += "\r\n\r\n"; - // Check that response is OK. - string bufStr = responseString; - std::stringstream responseStream(bufStr); + // Note read buffer can be longer than the supplied delimiter. + downloadBuf.consume(pos + 4); - string httpVersion; responseStream >> httpVersion; - unsigned int statusCode; responseStream >> statusCode; - string statusMessage; std::getline( responseStream, statusMessage); + // Check that response is OK. + string bufStr = responseString; + std::stringstream responseStream(bufStr); - serverResponse(statusCode, httpVersion); + string httpVersion; + responseStream >> httpVersion; + unsigned int statusCode; + responseStream >> statusCode; + string statusMessage; + std::getline(responseStream, statusMessage); - if ( !responseStream - || httpVersion.substr(0, 5) != "HTTP/" - || statusCode != 200) - { - if (logHttpSentReceived) - { - std::stringstream message; - message << "\nHTTP Sent : "; - message << requestString; - message << "\nHTTP Received : "; - message << responseString; + serverResponse(statusCode, httpVersion); - networkLog(message.str()); - } + if (!responseStream || httpVersion.substr(0, 5) != "HTTP/" || statusCode != 200) + { + if (logHttpSentReceived) + { + std::stringstream message; + message << "\nHTTP Sent : "; + message << requestString; + message << "\nHTTP Received : "; + message << responseString; - std::erase(statusMessage, '\r'); - std::erase(statusMessage, '\n'); + networkLog(message.str()); + } - BOOST_LOG_TRIVIAL(error) - << "Error: NTRIP - " << statusCode << " " << statusMessage - << " in " << __FUNCTION__ << " for " << url.sanitised() - << ", reconnecting in " << reconnectDelay; + std::erase(statusMessage, '\r'); + std::erase(statusMessage, '\n'); - delayedReconnect(); + BOOST_LOG_TRIVIAL(error) << "NTRIP - " << statusCode << " " << statusMessage << " in " + << __FUNCTION__ << " for " << url.sanitised() + << ", reconnecting in " << reconnectDelay; - return; - } + delayedReconnect(); + return; + } - //BOOST_LOG_TRIVIAL(debug) << "**********************************************\n"; - BOOST_LOG_TRIVIAL(debug) << "Connected " << url.sanitised(); + // BOOST_LOG_TRIVIAL(debug) << "**********************************************\n"; + BOOST_LOG_TRIVIAL(debug) << "Connected " << url.sanitised(); - //conneccted, turn the delay back down. - reconnectDelay = 1; + // conneccted, turn the delay back down. + reconnectDelay = 1; - isConnected = true; + isConnected = true; - if (disconnectionCount == 0) - { - networkLog("Initial Connection."); - } - else - { - std::stringstream message; + if (disconnectionCount == 0) + { + networkLog("Initial Connection."); + } + else + { + std::stringstream message; - networkLog(message.str()); + networkLog(message.str()); - //BOOST_LOG_TRIVIAL(debug) << message.str(); - } + // BOOST_LOG_TRIVIAL(debug) << message.str(); + } - boost::asio::socket_base::keep_alive option(true); - socket_ptr->set_option(option); + boost::asio::socket_base::keep_alive option(true); + socket_ptr->set_option(option); - connected(); + connected(); } - -void TcpSocket::writeRequestHandler( - const boost::system::error_code& err) +void TcpSocket::writeRequestHandler(const boost::system::error_code& err) { - if (err) - { - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } - - onChunkSentStatistics(); - - //prepare a timeout because the read_until call doesnt seem to return on bad requests. - timer.expires_from_now(boost::posix_time::seconds(10)); - timer.async_wait(boost::bind(&TcpSocket::timeoutHandler, this, bp::error)); - - // Read the response status line. - if (url.protocol == "https") - { - boost::asio::async_read_until(*_sslsocket, downloadBuf, readUntilString, boost::bind(&TcpSocket::requestResponseHandler, this, bp::error)); - } - else - { - boost::asio::async_read_until(*_socket, downloadBuf, readUntilString, boost::bind(&TcpSocket::requestResponseHandler, this, bp::error)); - } + if (err) + { + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } + + onChunkSentStatistics(); + + // prepare a timeout because the read_until call doesnt seem to return on bad requests. + timer.expires_from_now(boost::posix_time::seconds(10)); + timer.async_wait(boost::bind(&TcpSocket::timeoutHandler, this, bp::error)); + + // Read the response status line. + if (url.protocol == "https") + { + boost::asio::async_read_until( + *_sslsocket, + downloadBuf, + readUntilString, + boost::bind(&TcpSocket::requestResponseHandler, this, bp::error) + ); + } + else + { + boost::asio::async_read_until( + *_socket, + downloadBuf, + readUntilString, + boost::bind(&TcpSocket::requestResponseHandler, this, bp::error) + ); + } } -void TcpSocket::sslHandshakeHandler( - const boost::system::error_code& err) +void TcpSocket::sslHandshakeHandler(const boost::system::error_code& err) { - if (err) - { - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } - - //BOOST_LOG_TRIVIAL(debug) << "SSL Handshake Completed.\n"; - - // The connection was successful. Send the request. - boost::asio::async_write(*_sslsocket, request, boost::bind(&TcpSocket::writeRequestHandler, this, bp::error)); + if (err) + { + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } + + // BOOST_LOG_TRIVIAL(debug) << "SSL Handshake Completed.\n"; + + // The connection was successful. Send the request. + boost::asio::async_write( + *_sslsocket, + request, + boost::bind(&TcpSocket::writeRequestHandler, this, bp::error) + ); } void TcpSocket::connectHandler( - const boost::system::error_code& err, - tcp::resolver::iterator endpointIterator) + const boost::system::error_code& err, + tcp::resolver::results_type::iterator it, + tcp::resolver::results_type::iterator end, + const tcp::resolver::results_type& endpoints // Keeps range alive +) { - if (err) - { - if (endpointIterator != tcp::resolver::iterator()) - { - // The connection failed. Try the next endpoint in the list. - tcp::endpoint endpoint = *endpointIterator; - socket_ptr->async_connect(*endpointIterator, boost::bind(&TcpSocket::connectHandler, this, bp::error, ++endpointIterator)); - - return; - } - else - { - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } - } - - onConnectedStatistics(); - - //BOOST_LOG_TRIVIAL(debug) << "Connect Completed.\n"; - - if (url.protocol == "https") - { - _sslsocket->async_handshake(boost::asio::ssl::stream_base::client, boost::bind(&TcpSocket::sslHandshakeHandler, this, bp::error)); - - return; - } - - // The connection was successful. Send the request. - boost::asio::async_write(*_socket, request, boost::bind(&TcpSocket::writeRequestHandler, this, bp::error)); + if (err) + { + if (it == end) + { + ERROR_OUTPUT_RECONNECT_AND_RETURN; + return; + } + + // Choose current and compute next BEFORE the async call + auto curr = it; + auto next = std::next(it); + + socket_ptr->async_connect( + *curr, + boost::bind(&TcpSocket::connectHandler, this, bp::error, next, end, endpoints) + ); + + return; + } + + onConnectedStatistics(); + + // BOOST_LOG_TRIVIAL(debug) << "Connect Completed.\n"; + + if (url.protocol == "https") + { + _sslsocket->async_handshake( + boost::asio::ssl::stream_base::client, + boost::bind(&TcpSocket::sslHandshakeHandler, this, bp::error) + ); + + return; + } + + // The connection was successful. Send the request. + boost::asio::async_write( + *_socket, + request, + boost::bind(&TcpSocket::writeRequestHandler, this, bp::error) + ); } - void TcpSocket::resolveHandler( - const boost::system::error_code& err, - tcp::resolver::iterator endpointIterator) + const boost::system::error_code& err, + const tcp::resolver::results_type& results +) { - if (err) - { - BOOST_LOG_TRIVIAL(error) << "Error: check url and any usernames/passwords"; - ERROR_OUTPUT_RECONNECT_AND_RETURN; - } + if (err) + { + BOOST_LOG_TRIVIAL(error) << "Check url and any usernames/passwords"; + ERROR_OUTPUT_RECONNECT_AND_RETURN; + } - //BOOST_LOG_TRIVIAL(debug) << "Resolve Completed.\n"; + // BOOST_LOG_TRIVIAL(debug) << "Resolve Completed.\n"; - // Attempt a connection to the first endpoint in the list. Each endpoint will be tried until we successfully establish a connection. + // Attempt a connection to the first endpoint in the list. Each endpoint will be tried until we + // successfully establish a connection. - tcp::endpoint endpoint = *endpointIterator; - socket_ptr->async_connect(endpoint, boost::bind(&TcpSocket::connectHandler, this, bp::error, ++endpointIterator)); + auto begin = results.begin(); + auto next = std::next(begin); + auto end = results.end(); + + socket_ptr->async_connect( + *begin, + boost::bind(&TcpSocket::connectHandler, this, bp::error, next, end, results) + ); } void TcpSocket::connect() { - // Pointers are required as objects may need to be destroyed to full recover - // socket error. - _socket = std::make_shared (ioService); - _sslsocket = std::make_shared (ioService, sslContext); - _resolver = std::make_shared (ioService); - - BOOST_LOG_TRIVIAL(debug) << "(Re)connecting " << url.sanitised(); - - // The socket_ptr reduces some code, although the async_read and async_right - // must be called using _sslsocket in order to work correctly. - if (url.protocol == "https") socket_ptr = &_sslsocket->next_layer(); - else socket_ptr = _socket.get(); - - std::ostream requestStream(&request); - requestStream << requestString; - - //tcp::resolver::query query(url.host, url.portStr, boost::asio::ip::resolver_query_base::numeric_service); - tcp::resolver::query query(boost::asio::ip::tcp::v4(), url.host, url.portStr); - - _resolver->async_resolve(query, boost::bind(&TcpSocket::resolveHandler, this, bp::error, bp::iterator)); + // Pointers are required as objects may need to be destroyed to full recover + // socket error. + _socket = std::make_shared(ioContext); + _sslsocket = std::make_shared(ioContext, sslContext); + _resolver = std::make_shared(ioContext); + + BOOST_LOG_TRIVIAL(debug) << "(Re)connecting " << url.sanitised(); + + // The socket_ptr reduces some code, although the async_read and async_right + // must be called using _sslsocket in order to work correctly. + if (url.protocol == "https") + socket_ptr = &_sslsocket->next_layer(); + else + socket_ptr = _socket.get(); + + std::ostream requestStream(&request); + requestStream << requestString; + + // tcp::resolver::query query(url.host, url.portStr, + // boost::asio::ip::resolver_query_base::numeric_service); + _resolver->async_resolve( + url.host, + url.portStr, + boost::bind(&TcpSocket::resolveHandler, this, bp::error, bp::iterator) + ); } void TcpSocket::disconnect() { - onDisconnectedStatistics(); - - try - { - socket_ptr->shutdown(boost::asio::ip::tcp::socket::shutdown_both); - socket_ptr->close(); - } - catch (...) - { - - } - - // Clear the buffers except receivedBuffer that could possibly retain valid RTCM messages. - request .consume(request .size()); - downloadBuf .consume(downloadBuf.size()); + onDisconnectedStatistics(); + + try + { + socket_ptr->shutdown(boost::asio::ip::tcp::socket::shutdown_both); + socket_ptr->close(); + } + catch (...) + { + } + + // Clear the buffers except receivedBuffer that could possibly retain valid RTCM messages. + request.consume(request.size()); + downloadBuf.consume(downloadBuf.size()); } - -void TcpSocket::connectionError( - const boost::system::error_code& err, - string operation) +void TcpSocket::connectionError(const boost::system::error_code& err, string operation) { - if (acsConfig.output_ntrip_log == false) - return; + if (acsConfig.output_ntrip_log == false) + return; - std::ofstream logStream(networkTraceFilename, std::ofstream::app); + std::ofstream logStream(networkTraceFilename, std::ofstream::app); - if (!logStream) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Error opening log file.\n"; - return; - } + if (!logStream) + { + BOOST_LOG_TRIVIAL(warning) << "Error opening log file.\n"; + return; + } - GTime time = timeGet(); + GTime time = timeGet(); - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("label", "connectionError")); - doc.append(kvp("Stream", url.path.substr(1, url.path.length()))); - doc.append(kvp("Time", time.to_string())); - doc.append(kvp("BoostSysErrCode", err.value())); - doc.append(kvp("BoostSysErrMess", err.message())); - doc.append(kvp("SocketOperation", operation)); + boost::json::object doc = {}; + doc["label"] = "connectionError"; + doc["Stream"] = url.path.substr(1, url.path.length()); + doc["Time"] = time.to_string(); + doc["BoostSysErrCode"] = err.value(); + doc["BoostSysErrMess"] = err.message(); + doc["SocketOperation"] = operation; - logStream << bsoncxx::to_json(doc) << "\n"; + logStream << boost::json::serialize(doc) << "\n"; } -void NtripStream::serverResponse( - unsigned int statusCode, - string httpVersion) +void NtripStream::serverResponse(unsigned int statusCode, string httpVersion) { - if (acsConfig.output_ntrip_log == false) - return; + if (acsConfig.output_ntrip_log == false) + return; - std::ofstream logStream(networkTraceFilename, std::ofstream::app); + std::ofstream logStream(networkTraceFilename, std::ofstream::app); - if (!logStream) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Error opening log file.\n"; - return; - } + if (!logStream) + { + BOOST_LOG_TRIVIAL(warning) << "Error opening log file.\n"; + return; + } - GTime time = timeGet(); + GTime time = timeGet(); - bsoncxx::builder::basic::document doc = {}; - doc.append(kvp("label", __FUNCTION__)); - doc.append(kvp("Stream", url.path.substr(1, url.path.length()))); - doc.append(kvp("Time", time.to_string())); - doc.append(kvp("ServerStatus", (int)statusCode)); - doc.append(kvp("VersionHTTP", httpVersion)); + boost::json::object doc = {}; + doc["label"] = __FUNCTION__; + doc["Stream"] = url.path.substr(1, url.path.length()); + doc["Time"] = time.to_string(); + doc["ServerStatus"] = (int)statusCode; + doc["VersionHTTP"] = httpVersion; - logStream << bsoncxx::to_json(doc) << "\n"; + logStream << boost::json::serialize(doc) << "\n"; } - diff --git a/src/cpp/common/tcpSocket.hpp b/src/cpp/common/tcpSocket.hpp index be223408e..0bc97e3c8 100644 --- a/src/cpp/common/tcpSocket.hpp +++ b/src/cpp/common/tcpSocket.hpp @@ -1,331 +1,339 @@ - #pragma once -#include -#include -#include - -using std::string; -using std::vector; - +#include +#include +#include +#include +#include #include +#include #include -#include -#include #include #include #include -#include -#include -#include #include -#include -#include +#include +#include +#include +#include +#include +#include "common/ntripTrace.hpp" +#include "common/streamSerial.hpp" -#include "streamSerial.hpp" -#include "ntripTrace.hpp" +namespace B_asio = boost::asio; +namespace ip = B_asio::ip; +namespace ssl = B_asio::ssl; +using std::string; +using std::vector; +using tcp = ip::tcp; +using error_code = boost::system::error_code; +using ssl_socket = ssl::stream; /* Interface to be used for NTRIP version 2, streams for downloading messages. -* The NtripSteam was ugraded by Alex and now connects and download ansyncronously -* in the background on a boost service with it's own thread created in the constuctor. -* The message chunking of NTRIP version 2 has been implemented and receivedDataBuffer -* is filled with valid RCTM messages in the background without the chunked encoding data. -* During testing there is some data downloading that is invalid, being not complient with -* the chunked protocol. This data is not transfered to receivedDataBuffer. -* As in the previous implementation getData() is called once per epoch of the main loop -* and the completed and checked RTCM messages are placed in receivedData in a thread safe -* way. A destructor was added to correctly close socket when the program exits. -* Addition ansyncronous code was added to connect reconnect and establish a data stream. -* An example for the boost library documenation was used; -* https://www.boost.org/doc/libs/1_40_0/doc/html/boost_asio/example/http/client/async_client.cpp -*/ -namespace B_asio = boost::asio; -namespace ip = B_asio::ip; -namespace ssl = B_asio::ssl; -using tcp = ip::tcp; -using error_code = boost::system::error_code; -using ssl_socket = ssl::stream; - - -#define ERROR_OUTPUT_RECONNECT_AND_RETURN \ -{ \ - string message = err.message(); \ - std::erase(message, '\r'); \ - std::erase(message, '\n'); \ - onErrorStatistics(err, __FUNCTION__); \ - BOOST_LOG_TRIVIAL(error) << "Error: NTRIP - " << message << " in " << __FUNCTION__ << " for " << url.sanitised(); \ - \ - if (err != boost::asio::error::operation_aborted) \ - delayedReconnect(); \ - \ - return; \ -} - + * The NtripSteam was ugraded by Alex and now connects and download ansyncronously + * in the background on a boost service with it's own thread created in the constuctor. + * The message chunking of NTRIP version 2 has been implemented and receivedDataBuffer + * is filled with valid RCTM messages in the background without the chunked encoding data. + * During testing there is some data downloading that is invalid, being not complient with + * the chunked protocol. This data is not transfered to receivedDataBuffer. + * As in the previous implementation getData() is called once per epoch of the main loop + * and the completed and checked RTCM messages are placed in receivedData in a thread safe + * way. A destructor was added to correctly close socket when the program exits. + * Addition ansyncronous code was added to connect reconnect and establish a data stream. + * An example for the boost library documenation was used; + * https://www.boost.org/doc/libs/1_40_0/doc/html/boost_asio/example/http/client/async_client.cpp + */ + +#define ERROR_OUTPUT_RECONNECT_AND_RETURN \ + { \ + string message = err.message(); \ + std::erase(message, '\r'); \ + std::erase(message, '\n'); \ + onErrorStatistics(err, __FUNCTION__); \ + BOOST_LOG_TRIVIAL(error) << "NTRIP - " << message << " in " << __FUNCTION__ << " for " \ + << url.sanitised(); \ + \ + if (err != boost::asio::error::operation_aborted) \ + delayedReconnect(); \ + \ + return; \ + } struct Base64 { - static string encode(string in) - { - return encode(in.c_str(), in.length()); - } - - static string encode(const char * in, std::size_t len) - { - string out; - const char constexpr tab[] = - { - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/" - }; - - for (auto n = len / 3; n--; ) - { - out += tab[ (in[0] & 0xfc) >> 2]; - out += tab[((in[0] & 0x03) << 4) + ((in[1] & 0xf0) >> 4)]; - out += tab[((in[2] & 0xc0) >> 6) + ((in[1] & 0x0f) << 2)]; - out += tab[ in[2] & 0x3f]; - in += 3; - } - - switch (len % 3) - { - case 2: - out += tab[ (in[0] & 0xfc) >> 2]; - out += tab[((in[0] & 0x03) << 4) + ((in[1] & 0xf0) >> 4)]; - out += tab[ (in[1] & 0x0f) << 2]; - out += '='; - break; - - case 1: - out += tab[ (in[0] & 0xfc) >> 2]; - out += tab[((in[0] & 0x03) << 4)]; - out += '='; - out += '='; - break; - - case 0: - break; - } - - return out; - } + static string encode(string in) { return encode(in.c_str(), in.length()); } + + static string encode(const char* in, std::size_t len) + { + string out; + const char constexpr tab[] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/" + }; + + for (auto n = len / 3; n--;) + { + out += tab[(in[0] & 0xfc) >> 2]; + out += tab[((in[0] & 0x03) << 4) + ((in[1] & 0xf0) >> 4)]; + out += tab[((in[2] & 0xc0) >> 6) + ((in[1] & 0x0f) << 2)]; + out += tab[in[2] & 0x3f]; + in += 3; + } + + switch (len % 3) + { + case 2: + out += tab[(in[0] & 0xfc) >> 2]; + out += tab[((in[0] & 0x03) << 4) + ((in[1] & 0xf0) >> 4)]; + out += tab[(in[1] & 0x0f) << 2]; + out += '='; + break; + + case 1: + out += tab[(in[0] & 0xfc) >> 2]; + out += tab[((in[0] & 0x03) << 4)]; + out += '='; + out += '='; + break; + + case 0: + break; + } + + return out; + } }; - - struct URL { - string url; - string protocol; - string portStr; - string user; - string pass; - string host; - int port; - string path; - map params; - - URL() - { - - } - - URL(const string& url) - { - this->url = url; - - bool fail = false; - - string subUrl; - auto protocolPos = url .find("://"); - if (protocolPos == string::npos) { protocol = "file"; subUrl = url; } - else { protocol = url .substr(0, protocolPos); subUrl = url .substr(protocolPos + 3); } - - string serverStr; - string pathNParams; - auto slashPos = subUrl .find("/"); - if (slashPos == string::npos) { fail = true; } - else { serverStr = subUrl .substr(0, slashPos); pathNParams = subUrl .substr(slashPos + 1); } - - string passNUrl; - auto colonPos = serverStr .find(":"); - if (colonPos == string::npos) { } - else { user = serverStr .substr(0, colonPos); passNUrl = serverStr .substr(colonPos + 1); } - - string justUrl; - auto atPos = passNUrl .find("@"); - if (atPos == string::npos) { } - else { pass = passNUrl .substr(0, atPos); justUrl = passNUrl .substr(atPos + 1); } - - - colonPos = justUrl .find(":"); - if (colonPos == string::npos) { host = justUrl; } - else { host = justUrl .substr(0, colonPos); portStr = justUrl .substr(colonPos + 1); } - - size_t questionPos = pathNParams.find("?"); - path = ((string)"/") + pathNParams.substr(0, questionPos); - - while (questionPos != string::npos) - { - pathNParams = pathNParams.substr(questionPos + 1); colonPos = pathNParams.find(":"); string paramName = pathNParams.substr(0, colonPos); - pathNParams = pathNParams.substr(colonPos + 1); questionPos = pathNParams.find("?"); string paramValue = pathNParams.substr(0, questionPos); - - params[paramName] = paramValue; - } - - if (fail) - { - BOOST_LOG_TRIVIAL(debug) << "Invalid URL [" << url << "]"; - *this = URL(); - return; - } - - if (protocol.empty()) - protocol = "http"; - - if (portStr.empty()) - { - if (protocol == "https") portStr = "443"; - else portStr = "2101"; - } - port = std::stoi(portStr); - } - - string sanitised() - { - return protocol + ":" + "//" + host + (port > 0 ? (":" + std::to_string(port)) : "") + path; - } + string url; + string protocol; + string portStr; + string user; + string pass; + string host; + int port; + string path; + map params; + + URL() {} + + URL(const string& url) + { + this->url = url; + + bool fail = false; + + string subUrl; + auto protocolPos = url.find("://"); + if (protocolPos == string::npos) + { + protocol = "file"; + subUrl = url; + } + else + { + protocol = url.substr(0, protocolPos); + subUrl = url.substr(protocolPos + 3); + } + + string serverStr; + string pathNParams; + auto slashPos = subUrl.find("/"); + if (slashPos == string::npos) + { + fail = true; + } + else + { + serverStr = subUrl.substr(0, slashPos); + pathNParams = subUrl.substr(slashPos + 1); + } + + string passNUrl; + auto colonPos = serverStr.find(":"); + if (colonPos == string::npos) + { + } + else + { + user = serverStr.substr(0, colonPos); + passNUrl = serverStr.substr(colonPos + 1); + } + + string justUrl; + auto atPos = passNUrl.find("@"); + if (atPos == string::npos) + { + } + else + { + pass = passNUrl.substr(0, atPos); + justUrl = passNUrl.substr(atPos + 1); + } + + colonPos = justUrl.find(":"); + if (colonPos == string::npos) + { + host = justUrl; + } + else + { + host = justUrl.substr(0, colonPos); + portStr = justUrl.substr(colonPos + 1); + } + + size_t questionPos = pathNParams.find("?"); + path = ((string) "/") + pathNParams.substr(0, questionPos); + + while (questionPos != string::npos) + { + pathNParams = pathNParams.substr(questionPos + 1); + colonPos = pathNParams.find(":"); + string paramName = pathNParams.substr(0, colonPos); + pathNParams = pathNParams.substr(colonPos + 1); + questionPos = pathNParams.find("?"); + string paramValue = pathNParams.substr(0, questionPos); + + params[paramName] = paramValue; + } + + if (fail) + { + BOOST_LOG_TRIVIAL(debug) << "Invalid URL [" << url << "]"; + *this = URL(); + return; + } + + if (protocol.empty()) + protocol = "http"; + + if (portStr.empty()) + { + if (protocol == "https") + portStr = "443"; + else + portStr = "2101"; + } + port = std::stoi(portStr); + } + + string sanitised() + { + return protocol + ":" + "//" + host + (port > 0 ? (":" + std::to_string(port)) : "") + path; + } }; struct TcpSocket : NetworkStatistics, SerialStream { -protected: - std::shared_ptr _socket; - tcp::socket* socket_ptr; - std::shared_ptr _sslsocket; - std::shared_ptr _resolver; - - boost::asio::deadline_timer timer; - - string readUntilString; - string requestString; - string responseString; - - boost::asio::streambuf request; - boost::asio::streambuf downloadBuf; - - vector receivedTcpData; + protected: + std::shared_ptr _socket; + tcp::socket* socket_ptr; + std::shared_ptr _sslsocket; + std::shared_ptr _resolver; -public: - URL url; + boost::asio::deadline_timer timer; - double reconnectDelay = 1; - int disconnectionCount = 0; - bool isConnected = false; + string readUntilString; + string requestString; + string responseString; - int numberErroredChunks = 0; - bool logHttpSentReceived = false; + boost::asio::streambuf request; + boost::asio::streambuf downloadBuf; - unsigned int content_length = 0; + vector receivedTcpData; - TcpSocket(const string& url_str, - const string& readUntil = "\r\n\r\n") - : timer (ioService), - readUntilString (readUntil), - sslContext (ssl::context::sslv23_client) - { - url = URL(url_str); - streamName = url.path; - } + public: + URL url; - void setUrl(const string& url_str) - { - url = URL(url_str); - streamName = url.path; - } + double reconnectDelay = 1; + int disconnectionCount = 0; + bool isConnected = false; - void connect(); - void disconnect(); + int numberErroredChunks = 0; + bool logHttpSentReceived = false; + unsigned int content_length = 0; - void startRead(bool chunked); + TcpSocket(const string& url_str, const string& readUntil = "\r\n\r\n") + : timer(ioContext), readUntilString(readUntil), sslContext(ssl::context::sslv23_client) + { + url = URL(url_str); + streamName = url.path; + } - void timeoutHandler (const boost::system::error_code& err); - void delayedReconnect(); - void connectHandler (const boost::system::error_code& err, tcp::resolver::iterator endpoint_iterator); - virtual void requestResponseHandler (const boost::system::error_code& err) {}; + void setUrl(const string& url_str) + { + url = URL(url_str); + streamName = url.path; + } -private: + void connect(); + void disconnect(); - std::mutex receivedDataBufferMtx; //< This mutex ensures that the main thread and the io_service thread dont alter the receivedDataBuffer buffer at the same time. - vector> chunkList; + void startRead(bool chunked); - // These functions manage the connection using the boost service and - // asyncronous function calls. - void resolveHandler (const boost::system::error_code& err, tcp::resolver::iterator endpoint_iterator); - void sslHandshakeHandler (const boost::system::error_code& err); - void reconnectTimerHandler (const boost::system::error_code& err); + void timeoutHandler(const boost::system::error_code& err); + void delayedReconnect(); + void connectHandler( + const boost::system::error_code& err, + tcp::resolver::results_type::iterator it, + tcp::resolver::results_type::iterator end, + const tcp::resolver::results_type& endpoints + ); + virtual void requestResponseHandler(const boost::system::error_code& err) {}; - void readHandlerContent (const boost::system::error_code& err); - void readHandlerChunked (const boost::system::error_code& err); - void writeRequestHandler (const boost::system::error_code& err); + private: + std::mutex receivedDataBufferMtx; //< This mutex ensures that the main thread and the + // io_context thread dont alter + // the receivedDataBuffer buffer at the same time. + vector> chunkList; -public: - void logChunkError(); + // These functions manage the connection using the boost service and + // asyncronous function calls. + void resolveHandler( + const boost::system::error_code& err, + const tcp::resolver::results_type& results + ); + void sslHandshakeHandler(const boost::system::error_code& err); + void reconnectTimerHandler(const boost::system::error_code& err); + void readHandlerContent(const boost::system::error_code& err); + void readHandlerChunked(const boost::system::error_code& err); + void writeRequestHandler(const boost::system::error_code& err); - void dataChunkDownloaded( - vector& dataChunk); + public: + void logChunkError(); - //content from a one-shot request has been received - virtual void readContentDownloaded( - vector content) - { + void dataChunkDownloaded(vector& dataChunk); - } + // content from a one-shot request has been received + virtual void readContentDownloaded(vector content) {} - virtual void connected() - { - startRead(true); - } + virtual void connected() { startRead(true); } - virtual void messageChunkLog( - string message) - { + virtual void messageChunkLog(string message) {} - } + virtual void networkLog(string message) {} - virtual void networkLog( - string message) - { + void getData() override; - } + void connectionError(const boost::system::error_code& err, string operation); - void getData() - override; + B_asio::ssl::context sslContext; - void connectionError( - const boost::system::error_code& err, - string operation); + static B_asio::io_context ioContext; - B_asio::ssl::context sslContext; + static void runService() + { + B_asio::executor_work_guard work = + B_asio::make_work_guard(ioContext); + ioContext.run(); + } - static B_asio::io_service ioService; + static void startClients() { std::thread(TcpSocket::runService).detach(); } - static void runService() - { - B_asio::io_service::work work(ioService); - ioService.run(); - } - - static void startClients() - { - std::thread(TcpSocket::runService).detach(); - } - - virtual ~TcpSocket() = default; + virtual ~TcpSocket() = default; }; - - diff --git a/src/cpp/common/testUtils.cpp b/src/cpp/common/testUtils.cpp index c9a313e22..5ae25184c 100644 --- a/src/cpp/common/testUtils.cpp +++ b/src/cpp/common/testUtils.cpp @@ -1,97 +1,104 @@ - // #pragma GCC optimize ("O0") +#include "common/testUtils.hpp" #include +// #include #include +#include "common/acsConfig.hpp" +#include "common/biases.hpp" +#include "common/common.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/streamNtrip.hpp" +#include "common/streamParser.hpp" +#include "common/streamRtcm.hpp" -#include "testUtils.hpp" -#include "acsConfig.hpp" -#include "common.hpp" - +using LogSink = sinks::synchronous_sink; void ErrorExit::consume( - boost::log::record_view const& rec, - sinks::basic_formatted_sink_backend::string_type const& log_string) + boost::log::record_view const& rec, + sinks::basic_formatted_sink_backend::string_type const& + log_string +) { - int logLevel = 0; - auto attrs = rec.attribute_values(); - auto sev = attrs[boost::log::trivial::severity].get(); - switch (sev) - { - case boost::log::trivial::trace: logLevel = 5; break; - case boost::log::trivial::debug: logLevel = 4; break; - case boost::log::trivial::info: logLevel = 3; break; - case boost::log::trivial::warning: logLevel = 2; break; - case boost::log::trivial::error: logLevel = 1; break; - case boost::log::trivial::fatal: logLevel = 0; break; - } - - if (logLevel <= acsConfig.fatal_level) - { - std::cout << "\n" << "Message met fatal_message_level condition for exit.\nExiting...\n\n"; - exit(0); - } + int logLevel = 0; + auto attrs = rec.attribute_values(); + auto sev = attrs[boost::log::trivial::severity].get(); + switch (sev) + { + case boost::log::trivial::trace: + logLevel = 5; + break; + case boost::log::trivial::debug: + logLevel = 4; + break; + case boost::log::trivial::info: + logLevel = 3; + break; + case boost::log::trivial::warning: + logLevel = 2; + break; + case boost::log::trivial::error: + logLevel = 1; + break; + case boost::log::trivial::fatal: + logLevel = 0; + break; + } + + if (logLevel <= acsConfig.fatal_level) + { + std::cout << "\n" + << "Message met fatal_message_level condition for exit.\nExiting...\n\n"; + exit(0); + } } - void exitOnErrors() { - // Construct the sink - using LogSink = sinks::synchronous_sink; + // Construct the sink - boost::shared_ptr logSink = boost::make_shared(); + boost::shared_ptr logSink = boost::make_shared(); - // Register the sink in the logging core - boost::log::core::get()->add_sink(logSink); + // Register the sink in the logging core + boost::log::core::get()->add_sink(logSink); } - #ifdef PLUMBER size_t bucket = 0; -#include - -#include "streamParser.hpp" -#include "streamNtrip.hpp" -#include "navigation.hpp" -#include "streamRtcm.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "biases.hpp" - static void* plumber_hook(size_t size, const void* caller); static void* plumber_hook(size_t size, const void* caller) { - void* result; + void* result; - /* Restore all old hooks */ - /* Call recursively */ - __malloc_hook = 0; - { - result = malloc(size); - } - __malloc_hook = plumber_hook; + /* Restore all old hooks */ + /* Call recursively */ + __malloc_hook = 0; + { + result = malloc(size); + } + __malloc_hook = plumber_hook; - bucket += size; + bucket += size; - return result; + return result; } - -template +template size_t plumberTest(T& t) { - //begin plumbing - bucket = 0; + // begin plumbing + bucket = 0; - __malloc_hook = plumber_hook; - { - T newT = t; - } - __malloc_hook = 0; + __malloc_hook = plumber_hook; + { + T newT = t; + } + __malloc_hook = 0; - return bucket; + return bucket; } #endif @@ -99,42 +106,104 @@ size_t plumberTest(T& t) void plumber() { #ifdef PLUMBER - static map plumberMap; - - size_t New; - string v; - - printf("Checking plumbing:\n"); - for (auto& [id, satNav] : nav.satNavMap) - { - v = id.id(); New = plumberTest(satNav ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - } - - v = "biasMaps"; New = plumberTest(biasMaps ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "receiverMap"; New = plumberTest(receiverMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "nav"; New = plumberTest(nav ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "ephMap"; New = plumberTest(nav.ephMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "gephMap"; New = plumberTest(nav.gephMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "sephMap"; New = plumberTest(nav.sephMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "pephMap"; New = plumberTest(nav.pephMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "pclkMap"; New = plumberTest(nav.pclkMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "satNavMap"; New = plumberTest(nav.satNavMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "tecMap"; New = plumberTest(nav.tecMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "pcoMap"; New = plumberTest(nav.pcoMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; -// v = "pcoMap"; New = plumberTest(nav.pcoMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - - v = "satOptsMap"; New = plumberTest(acsConfig.satOptsMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "recOptsMap"; New = plumberTest(acsConfig.recOptsMap ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "yamls"; New = plumberTest(acsConfig.yamls ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "ionoOpts"; New = plumberTest(acsConfig.ionoOpts ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "pppOpts"; New = plumberTest(acsConfig.pppOpts ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "minconOpts"; New = plumberTest(acsConfig.minconOpts ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "localMongo"; New = plumberTest(acsConfig.localMongo ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "remoteMongo"; New = plumberTest(acsConfig.remoteMongo ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "slrOpts"; New = plumberTest(acsConfig.slrOpts ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "netOpts"; New = plumberTest(acsConfig.netOpts ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - v = "availableOptions"; New = plumberTest(acsConfig.availableOptions ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; - - printf("\n"); + static map plumberMap; + + size_t New; + string v; + + printf("Checking plumbing:\n"); + for (auto& [id, satNav] : nav.satNavMap) + { + v = id.id(); + New = plumberTest(satNav); + printf( + "%15s has %15ld drops added, %15ld in bucket\n", + v.c_str(), + (New - plumberMap[v]), + New + ); + plumberMap[v] = New; + } + + v = "biasMaps"; + New = plumberTest(biasMaps); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "receiverMap"; + New = plumberTest(receiverMap); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "nav"; + New = plumberTest(nav); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + // v = "ephMap"; New = plumberTest(nav.ephMap ); printf("%15s + // has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + // plumberMap[v] = New; v = "gephMap"; New = plumberTest(nav.gephMap ); + // printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), + // New); plumberMap[v] = New; v = "sephMap"; New = plumberTest(nav.sephMap + // ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - + // plumberMap[v]), New); plumberMap[v] = New; v = "pephMap"; New = + // plumberTest(nav.pephMap + // ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - + // plumberMap[v]), New); plumberMap[v] = New; v = "pclkMap"; New = + // plumberTest(nav.pclkMap ); printf("%15s has %15ld drops added, %15ld in + // bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; v = "satNavMap"; + // New = plumberTest(nav.satNavMap ); printf("%15s has %15ld drops added, + // %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; v = + // "tecMap"; New = plumberTest(nav.tecMap ); printf("%15s has + // %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] + // = New; v = "pcoMap"; New = plumberTest(nav.pcoMap + // ); printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - + // plumberMap[v]), New); plumberMap[v] = New; v = "pcoMap"; New = + // plumberTest(nav.pcoMap ); printf("%15s has %15ld drops added, %15ld in + // bucket\n", v.c_str(), (New - plumberMap[v]), New); plumberMap[v] = New; + + v = "satOptsMap"; + New = plumberTest(acsConfig.satOptsMap); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "recOptsMap"; + New = plumberTest(acsConfig.recOptsMap); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "yamls"; + New = plumberTest(acsConfig.yamls); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "ionoOpts"; + New = plumberTest(acsConfig.ionoOpts); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "pppOpts"; + New = plumberTest(acsConfig.pppOpts); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "minconOpts"; + New = plumberTest(acsConfig.minconOpts); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "localMongo"; + New = plumberTest(acsConfig.localMongo); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "remoteMongo"; + New = plumberTest(acsConfig.remoteMongo); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "slrOpts"; + New = plumberTest(acsConfig.slrOpts); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "netOpts"; + New = plumberTest(acsConfig.netOpts); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + v = "availableOptions"; + New = plumberTest(acsConfig.availableOptions); + printf("%15s has %15ld drops added, %15ld in bucket\n", v.c_str(), (New - plumberMap[v]), New); + plumberMap[v] = New; + + printf("\n"); #endif } diff --git a/src/cpp/common/testUtils.hpp b/src/cpp/common/testUtils.hpp index 243d72b39..af2371a31 100644 --- a/src/cpp/common/testUtils.hpp +++ b/src/cpp/common/testUtils.hpp @@ -1,20 +1,19 @@ - #pragma once #include #include #include +#include "common/eigenIncluder.hpp" namespace sinks = boost::log::sinks; - -#include "eigenIncluder.hpp" - struct ErrorExit : public sinks::basic_formatted_sink_backend { - // The function consumes the log records that come from the frontend - void consume( - boost::log::record_view const& rec, - sinks::basic_formatted_sink_backend::string_type const& log_string); + // The function consumes the log records that come from the frontend + void consume( + boost::log::record_view const& rec, + sinks::basic_formatted_sink_backend::string_type const& + log_string + ); }; void exitOnErrors(); @@ -23,19 +22,15 @@ void stacktrace(); struct TempDisabler { - bool oldVal = false; - bool* bool_ptr; + bool oldVal = false; + bool* bool_ptr; - TempDisabler( - bool& disable) - { - oldVal = disable; - disable = false; - bool_ptr = &disable; - } + TempDisabler(bool& disable) + { + oldVal = disable; + disable = false; + bool_ptr = &disable; + } - ~TempDisabler() - { - *bool_ptr = oldVal; - } + ~TempDisabler() { *bool_ptr = oldVal; } }; diff --git a/src/cpp/common/tides.cpp b/src/cpp/common/tides.cpp index 1eaa1f907..0a7f1fe10 100644 --- a/src/cpp/common/tides.cpp +++ b/src/cpp/common/tides.cpp @@ -1,861 +1,992 @@ - // #pragma GCC optimize ("O0") -/** \file -* ###References: -* -* 1. G.Petit and B.Luzum (eds), IERS Technical Note No. 36, IERS Conventions (2010), 2010 -* 2.1 IERS Conventions (2010) Working Version 1.3.0 — Most Recent, https://iers-conventions.obspm.fr/conventions_material.php -* 2.2 IERS Conventions (2010) Working Version 1.3.0 — Chapter 7, https://iers-conventions.obspm.fr/content/chapter7/icc7.pdf -* 3. DEHANTTIDEINEL, https://iers-conventions.obspm.fr/content/chapter7/software/dehanttideinel -* 4. ARG2.F, https://iers-conventions.obspm.fr/content/chapter7/software/ARG2.F -* 5. HARDISP, https://iers-conventions.obspm.fr/content/chapter7/software/hardisp/ -* 6. H.-G. Scherneck, Explanatory Supplement to the Section “Local Site Displacement due to Ocean loading” of the IERS Conventions (1996), 1999 -* 7. libiers10++, https://github.com/xanthospap/iers2010 -*/ - +#include "common/tides.hpp" #include -#include #include +#include #include +#include "3rdparty/iers2010/iers2010.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/ephPrecise.hpp" +#include "common/erp.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/iers2010.hpp" +#include "orbprop/planets.hpp" using boost::algorithm::to_lower_copy; -using std::ifstream; - -#include "coordinates.hpp" -#include "ephPrecise.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "iers2010.hpp" -#include "receiver.hpp" -#include "planets.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "tides.hpp" -#include "trace.hpp" -#include "erp.hpp" - using iers2010::hisp::ntin; +using std::ifstream; -const double mjd1975 = 42413; ///< MJD of 1975.0 - -OceanPoleGrid oceanPoleGrid; ///< Grid map of ocean pole load tide - -map blqSignMap = ///< Map of signs of BLQ records (positive east, north and upwards) -{ - {E_TidalComponent::EAST, +1}, - {E_TidalComponent::WEST, -1}, - {E_TidalComponent::NORTH, +1}, - {E_TidalComponent::SOUTH, -1}, - {E_TidalComponent::UP, +1}, - {E_TidalComponent::DOWN, -1}, +/** \file + * ###References: + * + * 1. G.Petit and B.Luzum (eds), IERS Technical Note No. 36, IERS Conventions (2010), 2010 + * 2.1 IERS Conventions (2010) Working Version 1.3.0 — Most Recent, + * https://iers-conventions.obspm.fr/conventions_material.php 2.2 IERS Conventions (2010) Working + * Version 1.3.0 — Chapter 7, https://iers-conventions.obspm.fr/content/chapter7/icc7.pdf + * 3. DEHANTTIDEINEL, https://iers-conventions.obspm.fr/content/chapter7/software/dehanttideinel + * 4. ARG2.F, https://iers-conventions.obspm.fr/content/chapter7/software/ARG2.F + * 5. HARDISP, https://iers-conventions.obspm.fr/content/chapter7/software/hardisp/ + * 6. H.-G. Scherneck, Explanatory Supplement to the Section “Local Site Displacement due to Ocean + * loading” of the IERS Conventions (1996), 1999 + * 7. libiers10++, https://github.com/xanthospap/iers2010 + */ + +const double mjd1975 = 42413; ///< MJD of 1975.0 + +OceanPoleGrid oceanPoleGrid; ///< Grid map of ocean pole load tide + +map + blqSignMap = ///< Map of signs of BLQ records (positive east, north and upwards) + { + {E_TidalComponent::EAST, +1}, + {E_TidalComponent::WEST, -1}, + {E_TidalComponent::NORTH, +1}, + {E_TidalComponent::SOUTH, -1}, + {E_TidalComponent::UP, +1}, + {E_TidalComponent::DOWN, -1}, }; -map blqIndexMap = ///< Map of indexes of BLQ records -{ - {E_TidalComponent::EAST, 0}, - {E_TidalComponent::WEST, 0}, - {E_TidalComponent::NORTH, 1}, - {E_TidalComponent::SOUTH, 1}, - {E_TidalComponent::UP, 2}, - {E_TidalComponent::DOWN, 2}, +map blqIndexMap = ///< Map of indexes of BLQ records + { + {E_TidalComponent::EAST, 0}, + {E_TidalComponent::WEST, 0}, + {E_TidalComponent::NORTH, 1}, + {E_TidalComponent::SOUTH, 1}, + {E_TidalComponent::UP, 2}, + {E_TidalComponent::DOWN, 2}, }; +map otlDisplacementMap; +map atlDisplacementMap; + /** Read BLQ record for a single station -*/ + */ bool readBlqRecord( - ifstream& fileStream, ///< Stream to read content from - vector waveList, ///< List of tidal constituents - vector componentList, ///< List of components {E/W, N/S, U/D} - TideMap& tideLoading) ///< Map of ocean/atmospheric tide loading displacements in amplitude and phase + ifstream& fileStream, ///< Stream to read content from + vector waveList, ///< List of tidal constituents + vector componentList, ///< List of components {E/W, N/S, U/D} + TideMap& + tideLoading ///< Map of ocean/atmospheric tide loading displacements in amplitude and phase +) { - double v[11]; - int n = 0; - - while (fileStream) - { - string line; - getline(fileStream, line); - - if (line.substr(0, 2) == "$$") - continue; - - char* buff = &line[0]; - int found = sscanf(buff, "%lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", - &v[0], - &v[1], - &v[2], - &v[3], - &v[4], - &v[5], - &v[6], - &v[7], - &v[8], - &v[9], - &v[10]); - - auto component = componentList[n % 3]; - int index = blqIndexMap[component]; - int sign = blqSignMap [component]; - - int i = 0; - for (auto wave : waveList) - { - if (n < 3) tideLoading[wave].amplitude [index] = v[i] * sign; // Positive east, north and upwards - else tideLoading[wave].phase [index] = v[i]; // Phase signs won't change - i++; - } - - n++; - - if (n == 6) - return true; - } - - return false; + double v[11]; + int n = 0; + + while (fileStream) + { + string line; + getline(fileStream, line); + + if (line.substr(0, 2) == "$$") + continue; + + char* buff = &line[0]; + int found = sscanf( + buff, + "%lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf", + &v[0], + &v[1], + &v[2], + &v[3], + &v[4], + &v[5], + &v[6], + &v[7], + &v[8], + &v[9], + &v[10] + ); + + auto component = componentList[n % 3]; + int index = blqIndexMap[component]; + int sign = blqSignMap[component]; + + int i = 0; + for (auto wave : waveList) + { + if (n < 3) + tideLoading[wave].amplitude[index] = + v[i] * sign; // Positive east, north and upwards + else + tideLoading[wave].phase[index] = v[i]; // Phase signs won't change + i++; + } + + n++; + + if (n == 6) + return true; + } + + return false; } /** Read BLQ ocean/atmospheric tide loading parameters -*/ + */ bool readBlq( - string file, ///< BLQ ocean tide loading parameter file - Receiver& rec, ///< Receiver - E_LoadingType type) ///< Type of loading (ocean, atmospheric) + string file, ///< BLQ ocean tide loading parameter file + E_LoadingType type ///< Type of loading (ocean, atmospheric) +) { - ifstream fileStream(file); - if (!fileStream) - { - BOOST_LOG_TRIVIAL(error) - << "BLQ file open error " << file; - - return false; - } - - vector waveList; - - if (type == +E_LoadingType::OCEAN) { waveList = acsConfig.otl_blq_col_order; } - else if (type == +E_LoadingType::ATMOSPHERIC) { waveList = acsConfig.atl_blq_col_order; } - - // station ID to upper case - string id = rec.id; - boost::to_upper(id); - - bool columnOrderFound = false; - while (fileStream) - { - string line; - getline(fileStream, line); - - char* buff = &line[0]; - - if (line.substr(0, 16) == "$$ COLUMN ORDER:") - { - string str = line.substr(16); - boost::trim(str); - vector waveNames; - boost::split(waveNames, str, boost::is_any_of(" "), boost::token_compress_on); - - waveList.clear(); - - for (auto& waveName : waveNames) - { - try - { - E_TidalConstituent constituent = E_TidalConstituent::_from_string(waveName.c_str()); - waveList.push_back(constituent); - } - catch (...) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Unknown tidal constituent in BLQ file header: " << waveName << "\n"; - } - } - - columnOrderFound = true; - - continue; - } - else if (line.substr(0, 2) == "$$" - ||line.size() < 2) - { - continue; - } - - if (!columnOrderFound) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Column order information not found in BLQ file header, (default) config is used" << "\n"; - } - - string name = line.substr(2, 4); - boost::to_upper(name); - if (name != id) - continue; - - // read BLQ record for the station - if (type == +E_LoadingType::OCEAN) { auto componentList = acsConfig.otl_blq_row_order; return readBlqRecord(fileStream, waveList, componentList, rec.otlDisplacement); } - else if (type == +E_LoadingType::ATMOSPHERIC) { auto componentList = acsConfig.atl_blq_row_order; return readBlqRecord(fileStream, waveList, componentList, rec.atlDisplacement); } - else - { - BOOST_LOG_TRIVIAL(error) - << __FUNCTION__ << ": Unspported file type" << "\n"; - - return false; - } - } - - BOOST_LOG_TRIVIAL(warning) - << "Warning: No otl parameters found for " << rec.id << " in " << file; - - return false; + ifstream fileStream(file); + if (!fileStream) + { + BOOST_LOG_TRIVIAL(error) << "BLQ file open error " << file; + + return false; + } + + vector waveList; + + if (type == E_LoadingType::OCEAN) + { + waveList = acsConfig.otl_blq_col_order; + } + else if (type == E_LoadingType::ATMOSPHERIC) + { + waveList = acsConfig.atl_blq_col_order; + } + + bool columnOrderFound = false; + while (fileStream) + { + string line; + getline(fileStream, line); + + char* buff = &line[0]; + + if (line.substr(0, 16) == "$$ COLUMN ORDER:") + { + string str = line.substr(16); + boost::trim(str); + vector waveNames; + boost::split(waveNames, str, boost::is_any_of(" "), boost::token_compress_on); + + waveList.clear(); + + for (auto& waveName : waveNames) + { + try + { + E_TidalConstituent constituent = + string_to_enum(waveName.c_str()); + waveList.push_back(constituent); + } + catch (...) + { + BOOST_LOG_TRIVIAL(warning) + << "Unknown tidal constituent in BLQ file header: " << waveName << "\n"; + } + } + + columnOrderFound = true; + + continue; + } + else if (line.substr(0, 2) == "$$" || line.size() < 2) + { + continue; + } + + if (!columnOrderFound) + { + BOOST_LOG_TRIVIAL(warning) << "Column order information not found in BLQ file " + "header, (default) config is used" + << "\n"; + } + + string name = line.substr(2, 4); + boost::to_upper(name); + + // read BLQ record for the station + if (type == E_LoadingType::OCEAN) + { + auto componentList = acsConfig.otl_blq_row_order; + readBlqRecord(fileStream, waveList, componentList, otlDisplacementMap[name]); + } + else if (type == E_LoadingType::ATMOSPHERIC) + { + auto componentList = acsConfig.atl_blq_row_order; + readBlqRecord(fileStream, waveList, componentList, atlDisplacementMap[name]); + } + else + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Unspported file type" << "\n"; + + return false; + } + } + + return true; } /** Read ocean pole load tide coefficients -*/ -bool readOceanPoleCoeff( - string file) ///< Ocean pole tide coefficient file + */ +bool readOceanPoleCoeff(string file) ///< Ocean pole tide coefficient file { - ifstream fileStream(file); - if (!fileStream) - { - BOOST_LOG_TRIVIAL(error) - << "Ocean pole tide coefficient file open error " << file; - - return false; - } - - double v[11]; - OceanPoleCoeff oceanPoleCoeff; - - oceanPoleGrid.grid.clear(); - - bool headerDone = false; - - while (fileStream) - { - string line; - getline(fileStream, line); - - char* buff = &line[0]; - - if (headerDone == false) - { - if (to_lower_copy(line.substr(0, 30)) == "number_longitude_grid_points =") { oceanPoleGrid.numLonGrid = str2num(buff, 30, 10); continue; } - else if (to_lower_copy(line.substr(0, 30)) == "first_longitude_degrees =") { oceanPoleGrid.firstLonDeg = str2num(buff, 30, 10); continue; } - else if (to_lower_copy(line.substr(0, 30)) == "last_longitude_degrees =") { oceanPoleGrid.lastLonDeg = str2num(buff, 30, 10); continue; } - else if (to_lower_copy(line.substr(0, 30)) == "longitude_step_degrees =") { oceanPoleGrid.lonStepDeg = str2num(buff, 30, 10); continue; } - else if (to_lower_copy(line.substr(0, 30)) == "number_latitude_grid_points =") { oceanPoleGrid.numLatGrid = str2num(buff, 30, 10); continue; } - else if (to_lower_copy(line.substr(0, 30)) == "first_latitude_degrees =") { oceanPoleGrid.firstLatDeg = str2num(buff, 30, 10); continue; } - else if (to_lower_copy(line.substr(0, 30)) == "last_latitude_degrees =") { oceanPoleGrid.lastLatDeg = str2num(buff, 30, 10); continue; } - else if (to_lower_copy(line.substr(0, 30)) == "latitude_step_degrees =") { oceanPoleGrid.latStepDeg = str2num(buff, 30, 10); continue; } - } - - int found = sscanf(buff, "%lf %lf %lf %lf %lf %lf %lf %lf", - &v[0], - &v[1], - &v[2], - &v[3], - &v[4], - &v[5], - &v[6], - &v[7]); - - if (found != 8) - continue; - - headerDone = true; - - oceanPoleCoeff.lon = v[0]; - oceanPoleCoeff.lat = v[1]; - oceanPoleCoeff.uR.u() = v[2]; - oceanPoleCoeff.uI.u() = v[3]; - oceanPoleCoeff.uR.n() = v[4]; - oceanPoleCoeff.uI.n() = v[5]; - oceanPoleCoeff.uR.e() = v[6]; - oceanPoleCoeff.uI.e() = v[7]; - - oceanPoleGrid.grid.push_back(oceanPoleCoeff); - } - - return true; + ifstream fileStream(file); + if (!fileStream) + { + BOOST_LOG_TRIVIAL(error) << "Ocean pole tide coefficient file open error " << file; + + return false; + } + + double v[11]; + OceanPoleCoeff oceanPoleCoeff; + + oceanPoleGrid.grid.clear(); + + bool headerDone = false; + + while (fileStream) + { + string line; + getline(fileStream, line); + + char* buff = &line[0]; + + if (headerDone == false) + { + if (to_lower_copy(line.substr(0, 30)) == "number_longitude_grid_points =") + { + oceanPoleGrid.numLonGrid = str2num(buff, 30, 10); + continue; + } + else if (to_lower_copy(line.substr(0, 30)) == "first_longitude_degrees =") + { + oceanPoleGrid.firstLonDeg = str2num(buff, 30, 10); + continue; + } + else if (to_lower_copy(line.substr(0, 30)) == "last_longitude_degrees =") + { + oceanPoleGrid.lastLonDeg = str2num(buff, 30, 10); + continue; + } + else if (to_lower_copy(line.substr(0, 30)) == "longitude_step_degrees =") + { + oceanPoleGrid.lonStepDeg = str2num(buff, 30, 10); + continue; + } + else if (to_lower_copy(line.substr(0, 30)) == "number_latitude_grid_points =") + { + oceanPoleGrid.numLatGrid = str2num(buff, 30, 10); + continue; + } + else if (to_lower_copy(line.substr(0, 30)) == "first_latitude_degrees =") + { + oceanPoleGrid.firstLatDeg = str2num(buff, 30, 10); + continue; + } + else if (to_lower_copy(line.substr(0, 30)) == "last_latitude_degrees =") + { + oceanPoleGrid.lastLatDeg = str2num(buff, 30, 10); + continue; + } + else if (to_lower_copy(line.substr(0, 30)) == "latitude_step_degrees =") + { + oceanPoleGrid.latStepDeg = str2num(buff, 30, 10); + continue; + } + } + + int found = sscanf( + buff, + "%lf %lf %lf %lf %lf %lf %lf %lf", + &v[0], + &v[1], + &v[2], + &v[3], + &v[4], + &v[5], + &v[6], + &v[7] + ); + + if (found != 8) + continue; + + headerDone = true; + + oceanPoleCoeff.lon = v[0]; + oceanPoleCoeff.lat = v[1]; + oceanPoleCoeff.uR.u() = v[2]; + oceanPoleCoeff.uI.u() = v[3]; + oceanPoleCoeff.uR.n() = v[4]; + oceanPoleCoeff.uI.n() = v[5]; + oceanPoleCoeff.uR.e() = v[6]; + oceanPoleCoeff.uI.e() = v[7]; + + oceanPoleGrid.grid.push_back(oceanPoleCoeff); + } + + return true; } -/** Step 1 of solid Earth tide computation: Corrections to be computed in the time domain (solar/lunar tides) -* See ref [1] 7.1.1 -*/ +/** Step 1 of solid Earth tide computation: Corrections to be computed in the time domain + * (solar/lunar tides) See ref [1] 7.1.1 + */ Vector3d tideTimeDomain( - Trace& trace, ///< Trace to output to - const Vector3d& eu, ///< Unit vector of Up component - const Vector3d& rp, ///< Sun/Moon position vector in ECEF (m) - double GMp, ///< Sun/Moon gravitational constant - const VectorPos& pos) ///< Geodetic position of station {lat,lon} (rad) + Trace& trace, ///< Trace to output to + const Vector3d& eu, ///< Unit vector of Up component + const Vector3d& rp, ///< Sun/Moon position vector in ECEF (m) + double GMp, ///< Sun/Moon gravitational constant + const VectorPos& pos ///< Geodetic position of station {lat,lon} (rad) +) { - const double H3 = 0.292; - const double L3 = 0.015; + const double H3 = 0.292; + const double L3 = 0.015; - tracepdeex(4, trace, "\n%s: pos=%.3f %.3f\n", __FUNCTION__, pos.latDeg(), pos.lonDeg()); + tracepdeex(4, trace, "\n%s: pos=%.3f %.3f\n", __FUNCTION__, pos.latDeg(), pos.lonDeg()); - double r = rp.norm(); - if (r == 0) - return Vector3d::Zero(); + double r = rp.norm(); + if (r == 0) + return Vector3d::Zero(); - Vector3d ep = rp.normalized(); + Vector3d ep = rp.normalized(); - double K2 = GMp / GM_Earth * SQR(RE_WGS84) * SQR(RE_WGS84) / (r * r * r); - double K3 = K2 * RE_WGS84 / r; + double K2 = GMp / GM_Earth * SQR(RE_WGS84) * SQR(RE_WGS84) / (r * r * r); + double K3 = K2 * RE_WGS84 / r; - double latp = asin(ep[2]); - double lonp = atan2(ep[1], ep[0]); + double latp = asin(ep[2]); + double lonp = atan2(ep[1], ep[0]); - double cosp = cos(latp); - double sinl = sin(pos.lat()); - double cosl = cos(pos.lat()); + double cosp = cos(latp); + double sinl = sin(pos.lat()); + double cosl = cos(pos.lat()); - // step 1: in phase (degree 2) - double p = (3 * SQR(sinl) - 1) / 2; - double H2 = 0.6078 - 0.0006 * p; - double L2 = 0.0847 + 0.0002 * p; - double a = ep.dot(eu); - double dp = K2 * 3 * L2 * a; - double du = K2 * (H2 * (1.5 * a * a - 0.5) - 3 * L2 * SQR(a)); + // step 1: in phase (degree 2) + double p = (3 * SQR(sinl) - 1) / 2; + double H2 = 0.6078 - 0.0006 * p; + double L2 = 0.0847 + 0.0002 * p; + double a = ep.dot(eu); + double dp = K2 * 3 * L2 * a; + double du = K2 * (H2 * (1.5 * a * a - 0.5) - 3 * L2 * SQR(a)); - // step 1: in phase (degree 3) - dp += K3 * L3 * (7.5 * a * a - 1.5); - du += K3 * (H3 * (2.5 * a * a * a - 1.5 * a) - L3 * (7.5 * a * a - 1.5) * a); + // step 1: in phase (degree 3) + dp += K3 * L3 * (7.5 * a * a - 1.5); + du += K3 * (H3 * (2.5 * a * a * a - 1.5 * a) - L3 * (7.5 * a * a - 1.5) * a); - // step 1: out-of-phase (only radial) - du += 3.0 / 4.0 * 0.0025 * K2 * sin(2 * latp) * sin(2 * pos.lat()) * sin( pos.lon() - lonp); - du += 3.0 / 4.0 * 0.0022 * K2 * cosp * cosp * cosl * cosl * sin(2 * ( pos.lon() - lonp)); + // step 1: out-of-phase (only radial) + du += 3.0 / 4.0 * 0.0025 * K2 * sin(2 * latp) * sin(2 * pos.lat()) * sin(pos.lon() - lonp); + du += 3.0 / 4.0 * 0.0022 * K2 * cosp * cosp * cosl * cosl * sin(2 * (pos.lon() - lonp)); - Vector3d dr = dp * ep - + du * eu; + Vector3d dr = dp * ep + du * eu; - tracepdeex(5, trace, "%s: dr=%.3f %.3f %.3f\n", __FUNCTION__, dr[0], dr[1], dr[2]); + tracepdeex(5, trace, "%s: dr=%.3f %.3f %.3f\n", __FUNCTION__, dr[0], dr[1], dr[2]); - return dr; + return dr; } /** Displacement by solid Earth tide -* See ref [1] 7.1.1 -* Note: permanent deformation not removed, i.e. the tidal model "in principle contains a time-independent part so that -* the coordinates obtained by taking into account this model in the analysis will be 'conventional tide free' values." -* See ref above for details -*/ + * See ref [1] 7.1.1 + * Note: permanent deformation not removed, i.e. the tidal model "in principle contains a + * time-independent part so that the coordinates obtained by taking into account this model in the + * analysis will be 'conventional tide free' values." See ref above for details + */ Vector3d tideSolidEarth( - Trace& trace, ///< Trace to output to - GTime time, ///< GPS time - MjDateUt1 mjdUt1, ///< UT1 time in MJD - const Vector3d& rsun, ///< Sun position vector in ECEF (m) - const Vector3d& rmoon, ///< Moon position vector in ECEF (m) - const VectorPos& pos) ///< Geodetic position of station {lat,lon} (rad) + Trace& trace, ///< Trace to output to + GTime time, ///< GPS time + MjDateUt1 mjdUt1, ///< UT1 time in MJD + const Vector3d& rsun, ///< Sun position vector in ECEF (m) + const Vector3d& rmoon, ///< Moon position vector in ECEF (m) + const VectorPos& pos ///< Geodetic position of station {lat,lon} (rad) +) { - tracepdeex(4, trace, "\n%s: pos=%.3f %.3f\n", __FUNCTION__, pos.latDeg(), pos.lonDeg()); + tracepdeex(4, trace, "\n%s: pos=%.3f %.3f\n", __FUNCTION__, pos.latDeg(), pos.lonDeg()); - // step 1: time domain - Matrix3d E; - pos2enu(pos, E.data()); + // step 1: time domain + Matrix3d E; + pos2enu(pos, E.data()); - Vector3d eu = E.row(2); + Vector3d eu = E.row(2); - Vector3d dr1 = tideTimeDomain(trace, eu, rsun, GM_Sun, pos); - Vector3d dr2 = tideTimeDomain(trace, eu, rmoon, GM_Moon, pos); + Vector3d dr1 = tideTimeDomain(trace, eu, rsun, GM_Sun, pos); + Vector3d dr2 = tideTimeDomain(trace, eu, rmoon, GM_Moon, pos); - // step 2: frequency domain, only K1 radial - double sin2l = sin(2 * pos.lat()); - double gmst = Sofa::iauGmst(mjdUt1, time); - double du = -0.012 * sin2l * sin(gmst + pos.lon()); + // step 2: frequency domain, only K1 radial + double sin2l = sin(2 * pos.lat()); + double gmst = Sofa::iauGmst(mjdUt1, time); + double du = -0.012 * sin2l * sin(gmst + pos.lon()); - Vector3d dr = dr1 + dr2 + du * eu; + Vector3d dr = dr1 + dr2 + du * eu; - // permanent deformation not removed + // permanent deformation not removed - tracepdeex(5, trace, "%s: dr=%.3f %.3f %.3f\n", __FUNCTION__, dr[0], dr[1], dr[2]); + tracepdeex(5, trace, "%s: dr=%.3f %.3f %.3f\n", __FUNCTION__, dr[0], dr[1], dr[2]); - return dr; + return dr; } /** Displacement by solid Earth tide with DEHANTTIDEINEL * See ref [1] 7.1.1, [3], [7] -*/ + */ Vector3d tideSolidEarthDehant( - Trace& trace, ///< Trace to output to - GTime time, ///< GPS time - const Vector3d& rsun, ///< Sun position vector in ECEF (m) - const Vector3d& rmoon, ///< Moon position vector in ECEF (m) - const Vector3d& recPos) ///< Receiver position in ECEF (m) + Trace& trace, ///< Trace to output to + GTime time, ///< GPS time + const Vector3d& rsun, ///< Sun position vector in ECEF (m) + const Vector3d& rmoon, ///< Moon position vector in ECEF (m) + const Vector3d& recPos ///< Receiver position in ECEF (m) +) { - tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); + tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); - MjDateUtc mjdUtc(time); - MjDateTT mjdTT(time); - double mjd = mjdUtc.to_double(); - int mjdInt = (int) mjd; - const double fhr = (mjd - mjdInt) * 24; - const double t = mjdTT.to_j2000() / 36525e0; + MjDateUtc mjdUtc(time); + MjDateTT mjdTT(time); + double mjd = mjdUtc.to_double(); + int mjdInt = (int)mjd; + const double fhr = (mjd - mjdInt) * 24; + const double t = mjdTT.to_j2000() / 36525e0; - vector recPosVec; - recPosVec.push_back(recPos); + vector recPosVec; + recPosVec.push_back(recPos); - vector drVec; + vector drVec; - iers2010::dehanttideinel_impl(t, fhr, rsun, rmoon, recPosVec, drVec); + iers2010::dehanttideinel_impl(t, fhr, rsun, rmoon, recPosVec, drVec); - Vector3d dr = drVec.front(); + Vector3d dr = drVec.front(); - tracepdeex(5, trace, "%s: dr=%.3f %.3f %.3f\n", __FUNCTION__, dr[0], dr[1], dr[2]); + tracepdeex(5, trace, "%s: dr=%.3f %.3f %.3f\n", __FUNCTION__, dr[0], dr[1], dr[2]); - return dr; + return dr; } /** Displacement by ocean tide loading -* See ref [1] 7.1.2, [4] -*/ + * See ref [1] 7.1.2, [4] + */ VectorEnu tideOceanLoad( - Trace& trace, ///< Trace to output to - MjDateUt1 mjdUt1, ///< UT1 time in MJD - TideMap& otlDisplacement) ///< OTL displacements in amplitude and phase + Trace& trace, ///< Trace to output to + MjDateUt1 mjdUt1, ///< UT1 time in MJD + TideMap& otlDisplacement ///< OTL displacements in amplitude and phase +) { - map> args = - { - {E_TidalConstituent::M2, {1.40519E-4, 2.0, -2.0, 0.0, 0.00}}, // M2 - {E_TidalConstituent::S2, {1.45444E-4, 0.0, 0.0, 0.0, 0.00}}, // S2 - {E_TidalConstituent::N2, {1.37880E-4, 2.0, -3.0, 1.0, 0.00}}, // N2 - {E_TidalConstituent::K2, {1.45842E-4, 2.0, 0.0, 0.0, 0.00}}, // K2 - {E_TidalConstituent::K1, {0.72921E-4, 1.0, 0.0, 0.0, 0.25}}, // K1 - {E_TidalConstituent::O1, {0.67598E-4, 1.0, -2.0, 0.0, -0.25}}, // O1 - {E_TidalConstituent::P1, {0.72523E-4, -1.0, 0.0, 0.0, -0.25}}, // P1 - {E_TidalConstituent::Q1, {0.64959E-4, 1.0, -3.0, 1.0, -0.25}}, // Q1 - {E_TidalConstituent::MF, {0.53234E-5, 0.0, 2.0, 0.0, 0.00}}, // Mf - {E_TidalConstituent::MM, {0.26392E-5, 0.0, 1.0, -1.0, 0.00}}, // Mm - {E_TidalConstituent::SSA, {0.03982E-5, 2.0, 0.0, 0.0, 0.00}} // Ssa - }; - - tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); - - // angular argument, see ref [4] ARG2.F - - double mjd = mjdUt1.to_double(); - int mjdInt = (int) mjd; - double sod = (mjd - mjdInt) * S_IN_DAY; - - double days = mjdInt - mjd1975 + 1; - double t = (27392.500528 + 1.000000035 * days) / 36525.0; - double t2 = t * t; - double t3 = t * t * t; - - double a[5]; - a[0]= sod; - a[1]= (279.69668 + 36000.768930485 * t + 3.03E-4 * t2) * D2R; // H0 - a[2]= (270.434358 + 481267.88314137 * t - 0.001133 * t2 + 1.9E-6 * t3) * D2R; // S0 - a[3]= (334.329653 + 4069.0340329577 * t - 0.010325 * t2 - 1.2E-5 * t3) * D2R; // P0 - a[4]= 2 * PI; - - - // displacements by 11 constituents - - VectorEnu denu; - for (auto& [wave, disp] : otlDisplacement) - { - double ang = 0; - for (int i = 0; i < 5; i++) ang += a[i] * args[wave][i]; - for (int j = 0; j < 3; j++) denu[j] += disp.amplitude[j] * cos(ang - disp.phase[j] * D2R); - } - - tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); - - return denu; + map> args = { + {E_TidalConstituent::M2, {1.40519E-4, 2.0, -2.0, 0.0, 0.00}}, // M2 + {E_TidalConstituent::S2, {1.45444E-4, 0.0, 0.0, 0.0, 0.00}}, // S2 + {E_TidalConstituent::N2, {1.37880E-4, 2.0, -3.0, 1.0, 0.00}}, // N2 + {E_TidalConstituent::K2, {1.45842E-4, 2.0, 0.0, 0.0, 0.00}}, // K2 + {E_TidalConstituent::K1, {0.72921E-4, 1.0, 0.0, 0.0, 0.25}}, // K1 + {E_TidalConstituent::O1, {0.67598E-4, 1.0, -2.0, 0.0, -0.25}}, // O1 + {E_TidalConstituent::P1, {0.72523E-4, -1.0, 0.0, 0.0, -0.25}}, // P1 + {E_TidalConstituent::Q1, {0.64959E-4, 1.0, -3.0, 1.0, -0.25}}, // Q1 + {E_TidalConstituent::MF, {0.53234E-5, 0.0, 2.0, 0.0, 0.00}}, // Mf + {E_TidalConstituent::MM, {0.26392E-5, 0.0, 1.0, -1.0, 0.00}}, // Mm + {E_TidalConstituent::SSA, {0.03982E-5, 2.0, 0.0, 0.0, 0.00}} // Ssa + }; + + tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); + + // angular argument, see ref [4] ARG2.F + + double mjd = mjdUt1.to_double(); + int mjdInt = (int)mjd; + double sod = (mjd - mjdInt) * S_IN_DAY; + + double days = mjdInt - mjd1975 + 1; + double t = (27392.500528 + 1.000000035 * days) / 36525.0; + double t2 = t * t; + double t3 = t * t * t; + + double a[5]; + a[0] = sod; + a[1] = (279.69668 + 36000.768930485 * t + 3.03E-4 * t2) * D2R; // H0 + a[2] = (270.434358 + 481267.88314137 * t - 0.001133 * t2 + 1.9E-6 * t3) * D2R; // S0 + a[3] = (334.329653 + 4069.0340329577 * t - 0.010325 * t2 - 1.2E-5 * t3) * D2R; // P0 + a[4] = 2 * PI; + + // displacements by 11 constituents + + VectorEnu denu; + for (auto& [wave, disp] : otlDisplacement) + { + double ang = 0; + for (int i = 0; i < 5; i++) + ang += a[i] * args[wave][i]; + for (int j = 0; j < 3; j++) + denu[j] += disp.amplitude[j] * cos(ang - disp.phase[j] * D2R); + } + + tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); + + return denu; } /** Displacement by ocean tide loading - adjustments -* See ref [1] eq 7.16, [6] -* Note: This model/function does not work well -*/ + * See ref [1] eq 7.16, [6] + * Note: This model/function does not work well + */ VectorEnu tideOceanLoadAdjusted( - Trace& trace, ///< Trace to output to - GTime time, ///< GPS time - MjDateUt1 mjdUt1, ///< UT1 time in MJD - TideMap& otlDisplacement) ///< OTL displacements in amplitude and phase + Trace& trace, ///< Trace to output to + GTime time, ///< GPS time + MjDateUt1 mjdUt1, ///< UT1 time in MJD + TideMap& otlDisplacement ///< OTL displacements in amplitude and phase +) { - map> q = - { - {E_TidalConstituent::M2, { 2, 0, 0, 0, 0, 0, 0}}, // M2 - {E_TidalConstituent::S2, { 2, 2, -2, 0, 0, 0, 0}}, // S2 - {E_TidalConstituent::N2, { 2, -1, 0, 1, 0, 0, 0}}, // N2 - {E_TidalConstituent::K2, { 2, 2, 0, 0, 0, 0, 0}}, // K2 - {E_TidalConstituent::K1, { 1, 1, 0, 0, 0, 0, 1}}, // K1 - {E_TidalConstituent::O1, { 1, -1, 0, 0, 0, 0, -1}}, // O1 - {E_TidalConstituent::P1, { 1, 1, -2, 0, 0, 0, -1}}, // P1 - {E_TidalConstituent::Q1, { 1, -2, 0, 1, 0, 0, -1}}, // Q1 - {E_TidalConstituent::MF, { 0, 2, 0, 0, 0, 0, 0}}, // Mf - {E_TidalConstituent::MM, { 0, 1, 0, -1, 0, 0, 0}}, // Mm - {E_TidalConstituent::SSA, { 0, 0, 2, 0, 0, 0, 0}} // Ssa - }; - - map> q5 = - { - {E_TidalConstituent::M2, {-1}}, // M2 - {E_TidalConstituent::S2, { 0}}, // S2 - {E_TidalConstituent::N2, { 0}}, // N2 - {E_TidalConstituent::K2, { 1}}, // K2 - {E_TidalConstituent::K1, { 1, -1}}, // K1 - {E_TidalConstituent::O1, {-1}}, // O1 - {E_TidalConstituent::P1, { 0}}, // P1 - {E_TidalConstituent::Q1, {-1}}, // Q1 - {E_TidalConstituent::MF, { 1}}, // Mf - {E_TidalConstituent::MM, { 0}}, // Mm - {E_TidalConstituent::SSA, { 0}} // Ssa - }; - - map> Q = - { - {E_TidalConstituent::M2, {-0.037}}, // M2 - {E_TidalConstituent::S2, { 0.000}}, // S2 - {E_TidalConstituent::N2, { 0.000}}, // N2 - {E_TidalConstituent::K2, { 0.298}}, // K2 - {E_TidalConstituent::K1, { 0.136, -0.020}}, // K1 - {E_TidalConstituent::O1, { 0.189}}, // O1 - {E_TidalConstituent::P1, { 0.000}}, // P1 - {E_TidalConstituent::Q1, { 0.189}}, // Q1 - {E_TidalConstituent::MF, { 0.415}}, // Mf - {E_TidalConstituent::MM, { 0.000}}, // Mm - {E_TidalConstituent::SSA, { 0.000}} // Ssa - }; - - tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); - - double mjd = mjdUt1.to_double(); - int mjdInt = (int) mjd; - double sod = (mjd - mjdInt) * S_IN_DAY; - - MjDateUtc mjdUtc(time); - double U = mjdUtc.to_double() / 36525.0; - double U2 = U * U; - double U3 = U * U * U; - - MjDateTT mjdTT(time); - double T = mjdTT.to_double() / 36525.0; - double T2 = T * T; - - double a[7]; - a[1] = (218.3166560 + 481267.881342 * T - 0.00133000 * T2 + 0.0040 * cos((133 * T + 29) * D2R)) * D2R; // s - a[2] = (280.4664490 + 36000.769822 * T + 0.00030360 * T2 + 0.0018 * cos(( 19 * T + 159) * D2R)) * D2R; // h - a[3] = ( 83.3532430 + 4069.013711 * T - 0.01032400 * T2) * D2R; // p - a[4] = (234.9554440 + 1934.136185 * T - 0.00207600 * T2) * D2R; // N - a[5] = (282.9373480 + 1.719533 * T + 0.00045970 * T2) * D2R; // ps - a[6] = PI / 2; - a[0] = (280.4606184 + 36000.7700536 * U + 0.00038793 * U2 - 0.0000000258 * U3 + sod / 240.0) * D2R - a[1]; // tao - - VectorEnu denu; - for (auto& [wave, disp] : otlDisplacement) - { - int n = Q[wave].size(); - double f = 1; - double u = 0; - double ang = 0; - - for (int i = 0; i < 7; i++) ang += fmod(a[i], PI2) * q[wave][i]; - for (int j = 0; j < n; j++) { f *= (1 + Q[wave][j] * cos(a[4])); - u += Q[wave][j] * sin(q5[wave][j] * a[4]); } - for (int k = 0; k < 3; k++) denu[k] += disp.amplitude[k] * f * cos(ang + u - disp.phase[k] * D2R); - } - - tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); - - return denu; + map> q = { + {E_TidalConstituent::M2, {2, 0, 0, 0, 0, 0, 0}}, // M2 + {E_TidalConstituent::S2, {2, 2, -2, 0, 0, 0, 0}}, // S2 + {E_TidalConstituent::N2, {2, -1, 0, 1, 0, 0, 0}}, // N2 + {E_TidalConstituent::K2, {2, 2, 0, 0, 0, 0, 0}}, // K2 + {E_TidalConstituent::K1, {1, 1, 0, 0, 0, 0, 1}}, // K1 + {E_TidalConstituent::O1, {1, -1, 0, 0, 0, 0, -1}}, // O1 + {E_TidalConstituent::P1, {1, 1, -2, 0, 0, 0, -1}}, // P1 + {E_TidalConstituent::Q1, {1, -2, 0, 1, 0, 0, -1}}, // Q1 + {E_TidalConstituent::MF, {0, 2, 0, 0, 0, 0, 0}}, // Mf + {E_TidalConstituent::MM, {0, 1, 0, -1, 0, 0, 0}}, // Mm + {E_TidalConstituent::SSA, {0, 0, 2, 0, 0, 0, 0}} // Ssa + }; + + map> q5 = { + {E_TidalConstituent::M2, {-1}}, // M2 + {E_TidalConstituent::S2, {0}}, // S2 + {E_TidalConstituent::N2, {0}}, // N2 + {E_TidalConstituent::K2, {1}}, // K2 + {E_TidalConstituent::K1, {1, -1}}, // K1 + {E_TidalConstituent::O1, {-1}}, // O1 + {E_TidalConstituent::P1, {0}}, // P1 + {E_TidalConstituent::Q1, {-1}}, // Q1 + {E_TidalConstituent::MF, {1}}, // Mf + {E_TidalConstituent::MM, {0}}, // Mm + {E_TidalConstituent::SSA, {0}} // Ssa + }; + + map> Q = { + {E_TidalConstituent::M2, {-0.037}}, // M2 + {E_TidalConstituent::S2, {0.000}}, // S2 + {E_TidalConstituent::N2, {0.000}}, // N2 + {E_TidalConstituent::K2, {0.298}}, // K2 + {E_TidalConstituent::K1, {0.136, -0.020}}, // K1 + {E_TidalConstituent::O1, {0.189}}, // O1 + {E_TidalConstituent::P1, {0.000}}, // P1 + {E_TidalConstituent::Q1, {0.189}}, // Q1 + {E_TidalConstituent::MF, {0.415}}, // Mf + {E_TidalConstituent::MM, {0.000}}, // Mm + {E_TidalConstituent::SSA, {0.000}} // Ssa + }; + + tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); + + double mjd = mjdUt1.to_double(); + int mjdInt = (int)mjd; + double sod = (mjd - mjdInt) * S_IN_DAY; + + MjDateUtc mjdUtc(time); + double U = mjdUtc.to_double() / 36525.0; + double U2 = U * U; + double U3 = U * U * U; + + MjDateTT mjdTT(time); + double T = mjdTT.to_double() / 36525.0; + double T2 = T * T; + + double a[7]; + a[1] = + (218.3166560 + 481267.881342 * T - 0.00133000 * T2 + 0.0040 * cos((133 * T + 29) * D2R)) * + D2R; // s + a[2] = (280.4664490 + 36000.769822 * T + 0.00030360 * T2 + 0.0018 * cos((19 * T + 159) * D2R)) * + D2R; // h + a[3] = (83.3532430 + 4069.013711 * T - 0.01032400 * T2) * D2R; // p + a[4] = (234.9554440 + 1934.136185 * T - 0.00207600 * T2) * D2R; // N + a[5] = (282.9373480 + 1.719533 * T + 0.00045970 * T2) * D2R; // ps + a[6] = PI / 2; + a[0] = (280.4606184 + 36000.7700536 * U + 0.00038793 * U2 - 0.0000000258 * U3 + sod / 240.0) * + D2R - + a[1]; // tao + + VectorEnu denu; + for (auto& [wave, disp] : otlDisplacement) + { + int n = Q[wave].size(); + double f = 1; + double u = 0; + double ang = 0; + + for (int i = 0; i < 7; i++) + ang += fmod(a[i], PI2) * q[wave][i]; + for (int j = 0; j < n; j++) + { + f *= (1 + Q[wave][j] * cos(a[4])); + u += Q[wave][j] * sin(q5[wave][j] * a[4]); + } + for (int k = 0; k < 3; k++) + denu[k] += disp.amplitude[k] * f * cos(ang + u - disp.phase[k] * D2R); + } + + tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); + + return denu; } /** Displacement by ocean tide loading with HARDISP -* See ref [1] 7.1.2, [5], [7] -*/ + * See ref [1] 7.1.2, [5], [7] + */ VectorEnu tideOceanLoadHardisp( - Trace& trace, ///< Trace to output to - GTime time, ///< GPS time - TideMap& otlDisplacement) ///< OTL displacements in amplitude and phase + Trace& trace, ///< Trace to output to + GTime time, ///< GPS time + TideMap& otlDisplacement ///< OTL displacements in amplitude and phase +) { - tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); + if (otlDisplacement.empty()) + { + return VectorEnu(); + } - double tamp [3][ntin]; - double tph [3][ntin]; + tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); - int i = 0; - 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 - tamp[1][i] = otlDisplacement[wave].amplitude .e(); - tph [1][i] = -otlDisplacement[wave].phase .e(); - tamp[2][i] = otlDisplacement[wave].amplitude .n(); - tph [2][i] = -otlDisplacement[wave].phase .n(); + double tamp[3][ntin]; + double tph[3][ntin]; - if (i++ >= ntin) - break; - } + int i = 0; + 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 + tamp[1][i] = otlDisplacement[wave].amplitude.e(); + tph[1][i] = -otlDisplacement[wave].phase.e(); + tamp[2][i] = otlDisplacement[wave].amplitude.n(); + tph[2][i] = -otlDisplacement[wave].phase.n(); - double du[1]; - double dn[1]; - double de[1]; + if (i++ >= ntin) + break; + } - iers2010::hisp::hardisp_impl(1, 0, tamp, tph, time, du, dn, de); + double du[1]; + double dn[1]; + double de[1]; - VectorEnu denu; - denu.e() = de[0]; - denu.n() = dn[0]; - denu.u() = du[0]; + iers2010::hisp::hardisp_impl(1, 0, tamp, tph, time, du, dn, de); - tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); + VectorEnu denu; + denu.e() = de[0]; + denu.n() = dn[0]; + denu.u() = du[0]; - return denu; + tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); + + return denu; } /** Displacement by atmospheric tide loading -* See ref [1] 7.1.3 -*/ + * See ref [1] 7.1.3 + */ VectorEnu tideAtmosLoad( - Trace& trace, ///< Trace to output to - MjDateUt1 mjdUt1, ///< UT1 time in MJD - TideMap& atlDisplacement) ///< ATL displacements in amplitude and phase + Trace& trace, ///< Trace to output to + MjDateUt1 mjdUt1, ///< UT1 time in MJD + TideMap& atlDisplacement ///< ATL displacements in amplitude and phase +) { - map args = - { - {E_TidalConstituent::S2, 1.45444E-4}, // S2: 2 cycles/day == 4*PI/86400 - {E_TidalConstituent::S1, 0.72722E-4}, // S1: 1 cycle /day == 2*PI/86400 - }; + if (atlDisplacement.empty()) + { + return VectorEnu(); + } - tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); + map args = { + {E_TidalConstituent::S2, 1.45444E-4}, // S2: 2 cycles/day == 4*PI/86400 + {E_TidalConstituent::S1, 0.72722E-4}, // S1: 1 cycle /day == 2*PI/86400 + }; - double mjd = mjdUt1.to_double(); - int mjdInt = (int) mjd; - double sod = (mjd - mjdInt) * S_IN_DAY; + tracepdeex(4, trace, "\n%s:\n", __FUNCTION__); - // displacements by 2 constituents - equation rederived w/ amplitude and phase + double mjd = mjdUt1.to_double(); + int mjdInt = (int)mjd; + double sod = (mjd - mjdInt) * S_IN_DAY; - VectorEnu denu = {}; - for (auto& [wave, disp] : atlDisplacement) - { - double ang = sod * args[wave]; - for (int j = 0; j < 3; j++) - { - denu[j] += disp.amplitude[j] * cos(ang - disp.phase[j] * D2R); - } - } + // displacements by 2 constituents - equation rederived w/ amplitude and phase - tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); + VectorEnu denu = {}; + for (auto& [wave, disp] : atlDisplacement) + { + double ang = sod * args[wave]; + for (int j = 0; j < 3; j++) + { + denu[j] += disp.amplitude[j] * cos(ang - disp.phase[j] * D2R); + } + } - return denu; -} + tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); -/** IERS mean pole -* See ref [2.2] eq.21 -* Eugene: This function will be gone in the future as IERS2010::meanPole() does the same thing -*/ -void iersMeanPole( - MjDateUt1 mjdUt1, ///< UT1 time in MJD - double& xp_bar, ///< Mean pole xp (mas) - double& yp_bar) ///< Mean pole yp (mas) -{ - double y = mjdUt1.to_j2000() / 365.25; - - //* Note: The cubic + linear mean pole model is obsolete, and a secular polar motion is adopted as recommended by IERS Conventions (2010) Working Version 1.3.0 - if (0) - { - double y2 = y * y; - double y3 = y * y * y; - - if (y < 3653.0 / 365.25) - { - /* until 2010.0 */ - xp_bar = 55.974 + 1.8243 * y + 0.18413 * y2 + 0.007024 * y3; // (mas) - yp_bar = 346.346 + 1.7896 * y - 0.10729 * y2 - 0.000908 * y3; - } - else - { - /* after 2010.0 */ - xp_bar = 23.513 + 7.6141 * y; // (mas) - yp_bar = 358.891 - 0.6287 * y; - } - } - - xp_bar = 55.0 + 1.677 * y; - yp_bar = 320.5 - 3.460 * y; + return denu; } /** Displacement by solid Earth pole tide -* See ref [1] 7.1.4 -*/ + * See ref [1] 7.1.4 + */ VectorEnu tideSolidPole( - Trace& trace, ///< Trace to output to - MjDateUt1 mjdUt1, ///< UT1 time in MJD - const VectorPos& pos, ///< Geodetic position of station {lat,lon} (rad) - ERPValues& erpv) ///< ERP values + Trace& trace, ///< Trace to output to + MjDateTT mjdTT, ///< TT time in MJD + const VectorPos& pos, ///< Geodetic position of station {lat,lon} (rad) + ERPValues& erpv ///< ERP values +) { - tracepdeex(4, trace, "\n%s: pos=%.3f %.3f\n", __FUNCTION__, pos.latDeg(), pos.lonDeg()); + tracepdeex(4, trace, "\n%s: pos=%.3f %.3f\n", __FUNCTION__, pos.latDeg(), pos.lonDeg()); - // IERS mean pole (mas) - double xp_bar; - double yp_bar; - iersMeanPole(mjdUt1, xp_bar, yp_bar); + // IERS mean pole (mas) + double xp_bar; + double yp_bar; + IERS2010::secularPole(mjdTT, xp_bar, yp_bar); - // ref [1] eq.7.24 - double m1 = + erpv.xp / AS2R - xp_bar * 1E-3; // (arcsec) - double m2 = - erpv.yp / AS2R + yp_bar * 1E-3; + // ref [1] eq.7.24 + double m1 = +erpv.xp / AS2R - xp_bar * 1E-3; // (arcsec) + double m2 = -erpv.yp / AS2R + yp_bar * 1E-3; - double theta = PI / 2 - pos.lat(); // co-latitude - double cosl = cos(pos.lon()); - double sinl = sin(pos.lon()); + double theta = PI / 2 - pos.lat(); // co-latitude + double cosl = cos(pos.lon()); + double sinl = sin(pos.lon()); - VectorEnu denu; - denu[0] = + 9E-3 * cos(1 * theta) * (m1 * sinl - m2 * cosl); // de = +Slambda (m), Slambda positive east - denu[1] = + 9E-3 * cos(2 * theta) * (m1 * cosl + m2 * sinl); // dn = -Stheta (m), Stheta positive south - denu[2] = -33E-3 * sin(2 * theta) * (m1 * cosl + m2 * sinl); // du = +Sr (m), Sr positive upwards + VectorEnu denu; + denu[0] = +9E-3 * cos(1 * theta) * + (m1 * sinl - m2 * cosl); // de = +Slambda (m), Slambda positive east + denu[1] = +9E-3 * cos(2 * theta) * + (m1 * cosl + m2 * sinl); // dn = -Stheta (m), Stheta positive south + denu[2] = -33E-3 * sin(2 * theta) * + (m1 * cosl + m2 * sinl); // du = +Sr (m), Sr positive upwards - tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); + tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); - return denu; + return denu; } /** Displacement by ocean pole tide -* See ref [1] 7.1.5 -*/ + * See ref [1] 7.1.5 + */ VectorEnu tideOceanPole( - Trace& trace, ///< Trace to output to - MjDateUt1 mjdUt1, ///< UT1 time in MJD - const VectorPos& pos, ///< Geodetic position of station {lat,lon} (rad) - ERPValues& erpv) ///< ERP values + Trace& trace, ///< Trace to output to + MjDateTT mjdTT, ///< UT1 time in MJD + const VectorPos& pos, ///< Geodetic position of station {lat,lon} (rad) + ERPValues& erpv ///< ERP values +) { - double latDeg = pos.latDeg(); - double lonDeg = pos.lonDeg(); - tracepdeex(4, trace, "\n%s: pos=%.3f %.3f\n", __FUNCTION__, latDeg, lonDeg); - - if (oceanPoleGrid.grid.empty()) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Ocean pole tide coefficients not available" << "\n"; - - return VectorEnu(); - } - - int ilat = floor((latDeg - oceanPoleGrid.firstLatDeg) / oceanPoleGrid.latStepDeg); - int ilon = floor((lonDeg - oceanPoleGrid.firstLonDeg) / oceanPoleGrid.lonStepDeg); - double lat1 = ilat * oceanPoleGrid.latStepDeg + oceanPoleGrid.firstLatDeg; - double lat2 = lat1 + oceanPoleGrid.latStepDeg; - double lon1 = ilon * oceanPoleGrid.lonStepDeg + oceanPoleGrid.firstLonDeg; - double lon2 = lon1 + oceanPoleGrid.lonStepDeg; - - vector dlat; - vector dlon; - dlat.push_back(lat1 - latDeg); dlon.push_back(lon1 - lonDeg); - dlat.push_back(lat2 - latDeg); dlon.push_back(lon2 - lonDeg); - - double index[4] = {}; - index[0] = ilat * oceanPoleGrid.numLonGrid + ilon; - index[1] = index[0] + 1; - index[2] = index[0] + oceanPoleGrid.numLonGrid; - index[3] = index[2] + 1; - - // wrap grid points along longitude (outside of grid) - if (lon1 < oceanPoleGrid.firstLonDeg) { index[0] += oceanPoleGrid.numLonGrid; index[2] += oceanPoleGrid.numLonGrid; } - if (lon2 > oceanPoleGrid. lastLonDeg) { index[1] -= oceanPoleGrid.numLonGrid; index[3] -= oceanPoleGrid.numLonGrid; } - - // nearest-neighbour extrapolation along latitude (outside of grid) - if (lat1 < oceanPoleGrid.firstLatDeg) { index[0] = index[2]; index[1] = index[3]; } - if (lat2 > oceanPoleGrid. lastLatDeg) { index[2] = index[0]; index[3] = index[1]; } - - vector uRLon1; - vector uILon1; - vector uRLon2; - vector uILon2; - for (int i = 0; i < 2; i++) - { - OceanPoleCoeff coeff1 = oceanPoleGrid.grid.at(index[i]); - OceanPoleCoeff coeff2 = oceanPoleGrid.grid.at(index[i + 2]); - - uRLon1.push_back(coeff1.uR); uRLon2.push_back(coeff2.uR); - uILon1.push_back(coeff1.uI); uILon2.push_back(coeff2.uI); - } - - // linear interpolation along longitude - vector uRLat; - vector uILat; - uRLat.push_back(interpolate(dlon, uRLon1)); uILat.push_back(interpolate(dlon, uILon1)); - uRLat.push_back(interpolate(dlon, uRLon2)); uILat.push_back(interpolate(dlon, uILon2)); - - // linear interpolation along latitude - VectorEnu uR = interpolate(dlat, uRLat); - VectorEnu uI = interpolate(dlat, uILat); - - // IERS mean pole (mas) - double xp_bar; - double yp_bar; - iersMeanPole(mjdUt1, xp_bar, yp_bar); - - // ref [1] eq.7.24 - double m1 = + erpv.xp / AS2R - xp_bar * 1E-3; // (arcsec) - double m2 = - erpv.yp / AS2R + yp_bar * 1E-3; - - double K = 5.3394043696E+03; // K = 4 * pi * G * aE * rhow * Hp / (3 * ge), Hp = sqrt(8 * pi / 15) * OmgE^2 * aE^4 / GM; - double gamma2R = 0.6870; - double gamma2I = 0.0036; - - - VectorEnu denu = ( uR * (m1 * gamma2R + m2 * gamma2I) - + uI * (m2 * gamma2R - m1 * gamma2I)) * AS2R * K; - - tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); - - return denu; + double latDeg = pos.latDeg(); + double lonDeg = pos.lonDeg(); + tracepdeex(4, trace, "\n%s: pos=%.3f %.3f\n", __FUNCTION__, latDeg, lonDeg); + + if (oceanPoleGrid.grid.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "Ocean pole tide coefficients not available" << "\n"; + + return VectorEnu(); + } + + int ilat = floor((latDeg - oceanPoleGrid.firstLatDeg) / oceanPoleGrid.latStepDeg); + int ilon = floor((lonDeg - oceanPoleGrid.firstLonDeg) / oceanPoleGrid.lonStepDeg); + double lat1 = ilat * oceanPoleGrid.latStepDeg + oceanPoleGrid.firstLatDeg; + double lat2 = lat1 + oceanPoleGrid.latStepDeg; + double lon1 = ilon * oceanPoleGrid.lonStepDeg + oceanPoleGrid.firstLonDeg; + double lon2 = lon1 + oceanPoleGrid.lonStepDeg; + + vector dlat; + vector dlon; + dlat.push_back(lat1 - latDeg); + dlon.push_back(lon1 - lonDeg); + dlat.push_back(lat2 - latDeg); + dlon.push_back(lon2 - lonDeg); + + double index[4] = {}; + index[0] = ilat * oceanPoleGrid.numLonGrid + ilon; + index[1] = index[0] + 1; + index[2] = index[0] + oceanPoleGrid.numLonGrid; + index[3] = index[2] + 1; + + // wrap grid points along longitude (outside of grid) + if (lon1 < oceanPoleGrid.firstLonDeg) + { + index[0] += oceanPoleGrid.numLonGrid; + index[2] += oceanPoleGrid.numLonGrid; + } + if (lon2 > oceanPoleGrid.lastLonDeg) + { + index[1] -= oceanPoleGrid.numLonGrid; + index[3] -= oceanPoleGrid.numLonGrid; + } + + // nearest-neighbour extrapolation along latitude (outside of grid) + if (lat1 < oceanPoleGrid.firstLatDeg) + { + index[0] = index[2]; + index[1] = index[3]; + } + if (lat2 > oceanPoleGrid.lastLatDeg) + { + index[2] = index[0]; + index[3] = index[1]; + } + + vector uRLon1; + vector uILon1; + vector uRLon2; + vector uILon2; + for (int i = 0; i < 2; i++) + { + OceanPoleCoeff coeff1 = oceanPoleGrid.grid.at(index[i]); + OceanPoleCoeff coeff2 = oceanPoleGrid.grid.at(index[i + 2]); + + uRLon1.push_back(coeff1.uR); + uRLon2.push_back(coeff2.uR); + uILon1.push_back(coeff1.uI); + uILon2.push_back(coeff2.uI); + } + + // linear interpolation along longitude + vector uRLat; + vector uILat; + uRLat.push_back(interpolate(dlon, uRLon1)); + uILat.push_back(interpolate(dlon, uILon1)); + uRLat.push_back(interpolate(dlon, uRLon2)); + uILat.push_back(interpolate(dlon, uILon2)); + + // linear interpolation along latitude + VectorEnu uR = interpolate(dlat, uRLat); + VectorEnu uI = interpolate(dlat, uILat); + + // IERS mean pole (mas) + double xp_bar; + double yp_bar; + IERS2010::secularPole(mjdTT, xp_bar, yp_bar); + + // ref [1] eq.7.24 + double m1 = +erpv.xp / AS2R - xp_bar * 1E-3; // (arcsec) + double m2 = -erpv.yp / AS2R + yp_bar * 1E-3; + + double K = 5.3394043696E+03; // K = 4 * pi * G * aE * rhow * Hp / (3 * ge), Hp = sqrt(8 * pi / + // 15) * OmgE^2 * aE^4 / GM; + double gamma2R = 0.6870; + double gamma2I = 0.0036; + + VectorEnu denu = + (uR * (m1 * gamma2R + m2 * gamma2I) + uI * (m2 * gamma2R - m1 * gamma2I)) * AS2R * K; + + tracepdeex(5, trace, "%s: denu=%.3f %.3f %.3f\n", __FUNCTION__, denu[0], denu[1], denu[2]); + + return denu; } /* Tidal displacement by Earth tides -* See ref [1] 7.1 -*/ + * See ref [1] 7.1 + */ void tideDisp( - Trace& trace, ///< Trace to output to - GTime time, ///< GPS time - Receiver& rec, ///< Receiver - Vector3d& recPos, ///< Receiver position in ECEF (m) - Vector3d& solid, ///< Displacement by solid Earth tide - Vector3d& otl, ///< Displacement by ocean tide - Vector3d& atl, ///< Displacement by atmospheric tide - Vector3d& spole, ///< Displacement by solid Earth pole tide - Vector3d& opole) ///< Displacement by ocean pole tide + Trace& trace, ///< Trace to output to + GTime time, ///< GPS time + string id, ///< Receiver id + Vector3d& recPos, ///< Receiver position in ECEF (m) + Vector3d& solid, ///< Displacement by solid Earth tide + Vector3d& otl, ///< Displacement by ocean tide + Vector3d& atl, ///< Displacement by atmospheric tide + Vector3d& spole, ///< Displacement by solid Earth pole tide + Vector3d& opole ///< Displacement by ocean pole tide +) { - int lv = 3; - - string timeStr = time.to_string(); - - tracepdeex(lv, trace, "\n\n%s: time=%s", __FUNCTION__, time.to_string().c_str()); - - ERPValues erpv = getErp(nav.erp, time); - - MjDateUt1 mjdUt1(time, erpv.ut1Utc); - - if (recPos.isZero()) - return; - - VectorPos pos = ecef2pos(recPos); - - auto& recOpts = acsConfig.getRecOpts(rec.id); - - VectorEcef rSun; - VectorEcef rMoon; - - if (recOpts.tideModels.solid) - { - // Sun and Moon positions in ECEF - planetPosEcef(time, E_ThirdBody::MOON, rMoon, erpv); - planetPosEcef(time, E_ThirdBody::SUN, rSun, erpv); - } - - if (recOpts.tideModels.solid) { solid = tideSolidEarthDehant(trace, time, rSun, rMoon, recPos); } - if (recOpts.tideModels.otl &&rec.otlDisplacement.empty() == false) { VectorEnu denu = tideOceanLoadHardisp (trace, time, rec.otlDisplacement); otl = (Vector3d) enu2ecef(pos, denu); } - if (recOpts.tideModels.atl &&rec.atlDisplacement.empty() == false) { VectorEnu denu = tideAtmosLoad (trace, mjdUt1, rec.atlDisplacement); atl = (Vector3d) enu2ecef(pos, denu); } - if (recOpts.tideModels.spole) { VectorEnu denu = tideSolidPole (trace, mjdUt1, pos, erpv); spole = (Vector3d) enu2ecef(pos, denu); } - if (recOpts.tideModels.opole) { VectorEnu denu = tideOceanPole (trace, mjdUt1, pos, erpv); opole = (Vector3d) enu2ecef(pos, denu); } - - tracepdeex(lv, trace, "\n%s SOLID %14.6f %14.6f %14.6f", timeStr.c_str(), solid [0], solid [1], solid [2]); - tracepdeex(lv, trace, "\n%s OCEAN %14.6f %14.6f %14.6f", timeStr.c_str(), otl [0], otl [1], otl [2]); - tracepdeex(lv, trace, "\n%s ATMOSPHERIC %14.6f %14.6f %14.6f", timeStr.c_str(), atl [0], atl [1], atl [2]); - tracepdeex(lv, trace, "\n%s SOLID POLE %14.6f %14.6f %14.6f", timeStr.c_str(), spole [0], spole [1], spole [2]); - tracepdeex(lv, trace, "\n%s OCEAN POLE %14.6f %14.6f %14.6f", timeStr.c_str(), opole [0], opole [1], opole [2]); + int lv = 3; + + string timeStr = time.to_string(); + + tracepdeex(lv, trace, "\n\n%s: time=%s", __FUNCTION__, time.to_string().c_str()); + + ERPValues erpv = getErp(nav.erp, time); + + MjDateTT mjdTT(time); + MjDateUt1 mjdUt1(time, erpv.ut1Utc); + if (recPos.isZero()) + return; + + VectorPos pos = ecef2pos(recPos); + + auto& recOpts = acsConfig.getRecOpts(id); + + VectorEcef rSun; + VectorEcef rMoon; + + if (recOpts.tideModels.solid) + { + // Sun and Moon positions in ECEF + planetPosEcef(time, E_ThirdBody::MOON, rMoon, erpv); + planetPosEcef(time, E_ThirdBody::SUN, rSun, erpv); + } + + auto& otlMap = otlDisplacementMap[id]; + auto& atlMap = atlDisplacementMap[id]; + + if (recOpts.tideModels.otl && otlMap.empty()) + BOOST_LOG_TRIVIAL(warning) << "No otl parameters found for " << id; + if (recOpts.tideModels.atl && atlMap.empty()) + BOOST_LOG_TRIVIAL(warning) << "No atl parameters found for " << id; + + if (recOpts.tideModels.solid) + { + solid = tideSolidEarthDehant(trace, time, rSun, rMoon, recPos); + } + if (recOpts.tideModels.otl) + { + VectorEnu denu = tideOceanLoadHardisp(trace, time, otlMap); + otl = (Vector3d)enu2ecef(pos, denu); + } + if (recOpts.tideModels.atl) + { + VectorEnu denu = tideAtmosLoad(trace, mjdUt1, atlMap); + atl = (Vector3d)enu2ecef(pos, denu); + } + if (recOpts.tideModels.spole) + { + VectorEnu denu = tideSolidPole(trace, mjdTT, pos, erpv); + spole = (Vector3d)enu2ecef(pos, denu); + } + if (recOpts.tideModels.opole) + { + VectorEnu denu = tideOceanPole(trace, mjdTT, pos, erpv); + opole = (Vector3d)enu2ecef(pos, denu); + } + + tracepdeex( + lv, + trace, + "\n%s SOLID %14.6f %14.6f %14.6f", + timeStr.c_str(), + solid[0], + solid[1], + solid[2] + ); + tracepdeex( + lv, + trace, + "\n%s OCEAN %14.6f %14.6f %14.6f", + timeStr.c_str(), + otl[0], + otl[1], + otl[2] + ); + tracepdeex( + lv, + trace, + "\n%s ATMOSPHERIC %14.6f %14.6f %14.6f", + timeStr.c_str(), + atl[0], + atl[1], + atl[2] + ); + tracepdeex( + lv, + trace, + "\n%s SOLID POLE %14.6f %14.6f %14.6f", + timeStr.c_str(), + spole[0], + spole[1], + spole[2] + ); + tracepdeex( + lv, + trace, + "\n%s OCEAN POLE %14.6f %14.6f %14.6f", + timeStr.c_str(), + opole[0], + opole[1], + opole[2] + ); } diff --git a/src/cpp/common/tides.hpp b/src/cpp/common/tides.hpp index bd057485c..b69c4f650 100644 --- a/src/cpp/common/tides.hpp +++ b/src/cpp/common/tides.hpp @@ -1,105 +1,101 @@ - #pragma once -#include "eigenIncluder.hpp" -#include "gTime.hpp" -#include "trace.hpp" +#include +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/trace.hpp" +using std::map; -//forward declaration +// forward declaration struct ERP; struct TideMap; +struct ERPValues; /** Structure for grid points of ocean pole load tide coefficients -*/ + */ struct OceanPoleCoeff { - double lat = 0; ///< latitude of the grid point - double lon = 0; ///< longitude of the grid point - VectorEnu uR = {}; ///< the real part of the ocean pole load tide coefficients - VectorEnu uI = {}; ///< the imaginary part of the ocean pole load tide coefficients + double lat = 0; ///< latitude of the grid point + double lon = 0; ///< longitude of the grid point + VectorEnu uR = {}; ///< the real part of the ocean pole load tide coefficients + VectorEnu uI = {}; ///< the imaginary part of the ocean pole load tide coefficients }; /** Structure for grid map of ocean pole load tide coefficients -*/ + */ struct OceanPoleGrid { - int numLonGrid = 0; ///< number of longitude grids - int numLatGrid = 0; ///< number of latitude grids - double firstLonDeg = 0; ///< longitude of first grid point (degree) - double firstLatDeg = 0; ///< latitude of first grid point (degree) - double lastLonDeg = 0; ///< longitude of last grid point (degree) - double lastLatDeg = 0; ///< latitude of last grid point (degree) - double lonStepDeg = 0; ///< step size of longitude grids - double latStepDeg = 0; ///< step size of latitude grids - vector grid = {}; ///< grid map of ocean pole load tide coefficients + int numLonGrid = 0; ///< number of longitude grids + int numLatGrid = 0; ///< number of latitude grids + double firstLonDeg = 0; ///< longitude of first grid point (degree) + double firstLatDeg = 0; ///< latitude of first grid point (degree) + double lastLonDeg = 0; ///< longitude of last grid point (degree) + double lastLatDeg = 0; ///< latitude of last grid point (degree) + double lonStepDeg = 0; ///< step size of longitude grids + double latStepDeg = 0; ///< step size of latitude grids + vector grid = {}; ///< grid map of ocean pole load tide coefficients +}; + +/** Structure of ocean/atmospheric tide loading displacements in amplitude and phase + */ +struct TidalDisplacement +{ + VectorEnu amplitude; + VectorEnu phase; +}; + +/** Map of ocean/atmospheric tide loading displacements + */ +struct TideMap : map +{ }; -bool readBlq( - string file, - Receiver& rec, - E_LoadingType type); +extern map otlDisplacementMap; ///< ocean tide loading parameters +extern map atlDisplacementMap; ///< atmospheric tide loading parameters -bool readOceanPoleCoeff( - string file); +bool readBlq(string file, E_LoadingType type); +bool readOceanPoleCoeff(string file); Vector3d tideSolidEarth( - Trace& trace, - GTime time, - MjDateUt1 mjdUt1, - const Vector3d& rsun, - const Vector3d& rmoon, - const VectorPos& pos); + Trace& trace, + GTime time, + MjDateUt1 mjdUt1, + const Vector3d& rsun, + const Vector3d& rmoon, + const VectorPos& pos +); Vector3d tideSolidEarthDehant( - Trace& trace, - GTime time, - const Vector3d& rsun, - const Vector3d& rmoon, - const Vector3d& recPos); - -VectorEnu tideOceanLoad( - Trace& trace, - MjDateUt1 mjdUt1, - TideMap& otlDisplacement); - -VectorEnu tideOceanLoadAdjusted( - Trace& trace, - GTime time, - MjDateUt1 mjdUt1, - TideMap& otlDisplacement); - -VectorEnu tideOceanLoadHardisp( - Trace& trace, - GTime time, - TideMap& otlDisplacement); - -VectorEnu tideAtmosLoad( - Trace& trace, - MjDateUt1 mjdUt1, - TideMap& atlDisplacement); - -VectorEnu tideSolidPole( - Trace& trace, - MjDateUt1 mjdUt1, - const VectorPos& pos, - ERPValues& erpv); - -VectorEnu tideOceanPole( - Trace& trace, - MjDateUt1 mjdUt1, - const VectorPos& pos, - ERPValues& erpv); + Trace& trace, + GTime time, + const Vector3d& rsun, + const Vector3d& rmoon, + const Vector3d& recPos +); -void tideDisp( - Trace& trace, - GTime time, - Receiver& rec, - Vector3d& recPos, - Vector3d& solid, - Vector3d& olt, - Vector3d& alt, - Vector3d& spole, - Vector3d& opole); +VectorEnu tideOceanLoad(Trace& trace, MjDateUt1 mjdUt1, TideMap& otlDisplacement); + +VectorEnu +tideOceanLoadAdjusted(Trace& trace, GTime time, MjDateUt1 mjdUt1, TideMap& otlDisplacement); + +VectorEnu tideOceanLoadHardisp(Trace& trace, GTime time, TideMap& otlDisplacement); +VectorEnu tideAtmosLoad(Trace& trace, MjDateUt1 mjdUt1, TideMap& atlDisplacement); +VectorEnu tideSolidPole(Trace& trace, MjDateTT mjdTT, const VectorPos& pos, ERPValues& erpv); + +VectorEnu tideOceanPole(Trace& trace, MjDateTT mjdTT, const VectorPos& pos, ERPValues& erpv); + +void tideDisp( + Trace& trace, + GTime time, + string id, + Vector3d& recPos, + Vector3d& solid, + Vector3d& olt, + Vector3d& alt, + Vector3d& spole, + Vector3d& opole +); diff --git a/src/cpp/common/trace.cpp b/src/cpp/common/trace.cpp index ec78dbc01..ba2347305 100644 --- a/src/cpp/common/trace.cpp +++ b/src/cpp/common/trace.cpp @@ -1,255 +1,268 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -#include +#include "common/trace.hpp" +#include +#include +#include +#include +#include #include #include -#include +#include +#include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/gTime.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "pea/inputsOutputs.hpp" +#include "pea/peaCommitStrings.hpp" using std::unordered_map; -#include -#include - -#include "interactiveTerminal.hpp" -#include "peaCommitStrings.hpp" -#include "inputsOutputs.hpp" -#include "observations.hpp" -#include "mongoWrite.hpp" -#include "navigation.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "trace.hpp" - boost::iostreams::stream nullStream((boost::iostreams::null_sink())); - -bool ConsoleLog::useInteractive = false; - - - /** Semi-formatted text-based outputs. * Trace files are the best record of the processing that occurs within the Pea. * * The level of trace may be set numerically by configuration. - * It will enable more or fewer lines of detail, and in some cases, the number of columns to be included in formatted sections. + * It will enable more or fewer lines of detail, and in some cases, the number of columns to be + * included in formatted sections. * - * Receivers and Satellites may have trace file outputs configured, which will include details specific to their processing, such as any preprocessing, - * and details about measurements that are being computed. + * Receivers and Satellites may have trace file outputs configured, which will include details + * specific to their processing, such as any preprocessing, and details about measurements that are + * being computed. * - * Calculated satellite and receiver positions, and reasons for the exclusion of any measurements are recorded in the receiver trace files. + * Calculated satellite and receiver positions, and reasons for the exclusion of any measurements + * are recorded in the receiver trace files. * - * When satellite orbit propagation is enabled, some details about modelled forces may be available in satellite trace files. + * When satellite orbit propagation is enabled, some details about modelled forces may be available + * in satellite trace files. * - * The receiver files may be configured with residual chain outputs on, which will produce formatted output of each modelled component as they are subtracted from the measured quantity. - * Depending on the possibility of chunking, receiver trace files may also include states that are usually only recorded in the network trace files. + * The receiver files may be configured with residual chain outputs on, which will produce formatted + * output of each modelled component as they are subtracted from the measured quantity. Depending on + * the possibility of chunking, receiver trace files may also include states that are usually only + * recorded in the network trace files. * - * Once the list of measurements is aggregated and passed to the main filter, trace outputs are written to the 'network' trace file. - * This contains any filter states, measurement residuals, state removals or reinitialsations, and iteration details. + * Once the list of measurements is aggregated and passed to the main filter, trace outputs are + * written to the 'network' trace file. This contains any filter states, measurement residuals, + * state removals or reinitialsations, and iteration details. * - * The filter outputs in the network trace file include blocks that are formatted with SINEX-style +BLOCK...-BLOCK sections, - * which may allow for easier post-processing by eliminating any information outside of the required section. + * The filter outputs in the network trace file include blocks that are formatted with SINEX-style + * +BLOCK...-BLOCK sections, which may allow for easier post-processing by eliminating any + * information outside of the required section. */ -FileType Trace_Files__() -{ - -} +FileType Trace_Files__() {} boost::log::trivial::severity_level acsSeverity = boost::log::trivial::info; void ConsoleLog::consume( - boost::log::record_view const& rec, - sinks::basic_formatted_sink_backend::string_type const& logString) + boost::log::record_view const& rec, + sinks::basic_formatted_sink_backend::string_type const& + logString +) { - static unordered_map warnedMap; - - auto attrs = rec.attribute_values(); - auto sev = attrs[boost::log::trivial::severity].get(); - - if ( sev == boost::log::trivial::warning - && acsConfig.warn_once) - { - auto& warned = warnedMap[std::hash{}(logString)]; - if (warned) - { - return; - } - - warned = true; - } - - - string output; - - output += "\r\n"; - if (acsConfig.colourise_terminal) - { - if (sev == boost::log::trivial::warning) output += "\x1B[1;93m"; - if (sev == boost::log::trivial::error) output += "\x1B[101m"; - } - output += logString; - - if (acsConfig.colourise_terminal) - { - output += "\x1B[0m"; - } - - if (useInteractive) - { - InteractiveTerminal::addString("Messages/All", logString); - - if (sev == boost::log::trivial::info) InteractiveTerminal::addString("Messages/Info", logString); - else if (sev == boost::log::trivial::warning) InteractiveTerminal::addString("Messages/Warnings", logString); - else if (sev == boost::log::trivial::error) InteractiveTerminal::addString("Messages/Errors", logString); - else if (sev == boost::log::trivial::debug) InteractiveTerminal::addString("Messages/Debug", logString); - - // std::cerr << output << std::flush; - } - else - { - std::cout << output << std::flush; - } + static unordered_map warnedMap; + + auto attrs = rec.attribute_values(); + auto sev = attrs[boost::log::trivial::severity].get(); + + if (sev == boost::log::trivial::warning && acsConfig.warn_once) + { + auto& warned = warnedMap[std::hash{}(logString)]; + if (warned) + { + return; + } + + warned = true; + } + + string output = ""; + + string ending = ""; + if (acsConfig.colourise_terminal) + { + if (sev == boost::log::trivial::warning) + output += "\x1B[1;93m"; + if (sev >= boost::log::trivial::error) + output += "\x1B[101m"; + + if (sev >= boost::log::trivial::warning) + ending = "\x1B[0m"; + } + + if (acsConfig.timestamp_console_logs && boost::trim_copy(logString).empty() == false) + { + string timestamp = "[" + timeGet().to_string() + "]\t"; + output += timestamp; + } + + if (sev == boost::log::trivial::warning) + output += "Warning: "; + if (sev >= boost::log::trivial::error) + output += "Error: "; + + output += logString; + output += ending; + + output += "\r\n"; + std::cout << output << std::flush; } - -int traceLevel = 0; ///< level of trace +int traceLevel = 0; ///< level of trace void traceFormatedFloat(Trace& trace, double val, string formatStr) { - // If someone knows how to make C++ print with just one digit as exponent... - int exponent = 0; - double base = 0; - - if (val != 0) - { - exponent = (int)floor(log10(fabs(val))); - base = val * pow(10, -1 * exponent); - } - tracepdeex(0, trace, formatStr.c_str(), base, exponent); + // If someone knows how to make C++ print with just one digit as exponent... + int exponent = 0; + double base = 0; + + if (val != 0) + { + exponent = (int)floor(log10(fabs(val))); + base = val * pow(10, -1 * exponent); + } + tracepdeex(0, trace, formatStr.c_str(), base, exponent); } -void printHex( - Trace& trace, - vector& chunk) +void printHex(Trace& trace, vector& chunk) { - trace << "\nHex Data : " << chunk.size(); - - for (int i = 0; i < chunk.size(); i++) - { - if (i % 40 == 0) - trace << "\n"; - - if (i % 10 == 0) - trace << " "; - char hex[3]; - snprintf(hex, sizeof(hex),"%02x", chunk[i]); - tracepdeex(0, trace, "%s ", hex); - } - trace << "\n"; + trace << "\nHex Data : " << chunk.size(); + + for (int i = 0; i < chunk.size(); i++) + { + if (i % 40 == 0) + trace << "\n"; + + if (i % 10 == 0) + trace << " "; + char hex[3]; + snprintf(hex, sizeof(hex), "%02x", chunk[i]); + tracepdeex(0, trace, "%s ", hex); + } + trace << "\n"; } - -void traceJson_( - Trace& trace, - GTime& time, - vector id, - vector val) +void traceJson_(Trace& trace, GTime& time, vector id, vector val) { - GEpoch ep(time); - - char timeBuff[64]; - snprintf(timeBuff, sizeof(timeBuff),"%04.0f-%02.0f-%02.0fT%02.0f:%02.0f:%06.3fZ", - ep.year, - ep.month, - ep.day, - ep.hour, - ep.min, - ep.sec); - - string json = (string) "{ \"Epoch\":{ \"$date\":\"" + timeBuff + "\"}, \"id\":{"; - - for (auto& thing : id) - { - json += "\"" + thing.name + "\":" + thing.value() + ","; - } - json = json.substr(0, json.length() - 1); - - json += "}, \"val\":{"; - - for (auto& thing : val) - { - json += "\"" + thing.name + "\":" + thing.value() + ","; - } - json = json.substr(0, json.length() - 1); - json += "} }"; - - if (acsConfig.output_json_trace) - { - trace << "\n - " + json; - } - if (acsConfig.mongoOpts.output_trace) - { - mongoTrace({json}, acsConfig.mongoOpts.queue_outputs); - } + GEpoch ep(time); + + char timeBuff[64]; + snprintf( + timeBuff, + sizeof(timeBuff), + "%04.0f-%02.0f-%02.0fT%02.0f:%02.0f:%06.3fZ", + ep.year, + ep.month, + ep.day, + ep.hour, + ep.min, + ep.sec + ); + + string json = (string) "{ \"Epoch\":{ \"$date\":\"" + timeBuff + "\"}, \"id\":{"; + + for (auto& thing : id) + { + json += "\"" + thing.name + "\":" + thing.value() + ","; + } + json = json.substr(0, json.length() - 1); + + json += "}, \"val\":{"; + + for (auto& thing : val) + { + json += "\"" + thing.name + "\":" + thing.value() + ","; + } + json = json.substr(0, json.length() - 1); + json += "} }"; + + if (acsConfig.output_json_trace) + { + trace << "\n - " + json; + } + if (acsConfig.mongoOpts.output_trace != E_Mongo::NONE) + { + mongoTrace({json}, acsConfig.mongoOpts.queue_outputs); + } } bool createNewTraceFile( - const string id, - boost::posix_time::ptime logptime, - string new_path_trace, - string& old_path_trace, - bool outputHeader, - bool outputConfig) + const string id, + const string& source, + boost::posix_time::ptime logptime, + string new_path_trace, + string& old_path_trace, + bool outputHeader, + bool outputConfig +) { - replaceString(new_path_trace, "", id); - replaceTimes (new_path_trace, logptime); - - if (new_path_trace == acsConfig.pppOpts.rts_smoothed_suffix) - { - return false; - } - - // Create the trace file if its a new filename, otherwise, keep the old one - if ( new_path_trace == old_path_trace - ||new_path_trace.empty()) - { - //the filename is the same, keep using the old ones - return false; - } - - old_path_trace = new_path_trace; - - BOOST_LOG_TRIVIAL(debug) - << "Creating new file for " << id << " at " << old_path_trace; - - std::ofstream trace(old_path_trace); - if (!trace) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Could not create file for " << id << " at " << old_path_trace; - - return false; - } - - // Trace file head - if (outputHeader) - { - trace << "station : " << id << "\n"; - trace << "start_epoch: " << acsConfig.start_epoch << "\n"; - trace << "end_epoch : " << acsConfig.end_epoch << "\n"; - trace << "trace_level: " << acsConfig.trace_level << "\n"; - trace << "pea_version: " << ginanCommitVersion() << "\n"; -// trace << "rts_lag : " << acsConfig.pppOpts.rts_lag << "\n"; - } - - if (outputConfig) - { - dumpConfig(trace); - } - - return true; + int lastSlash = source.find_last_of('/'); + + if (lastSlash == string::npos) + { + lastSlash = 0; + } + + string shortSource = source.substr(lastSlash); + if (shortSource.empty()) + { + shortSource = id; + } + + replaceString(new_path_trace, "", shortSource); + replaceString(new_path_trace, "", shortSource); + replaceString(new_path_trace, "", id); + replaceTimes(new_path_trace, logptime); + + if (new_path_trace == acsConfig.pppOpts.rts_smoothed_suffix) + { + return false; + } + + // Create the trace file if its a new filename, otherwise, keep the old one + if (new_path_trace == old_path_trace || new_path_trace.empty()) + { + // the filename is the same, keep using the old ones + return false; + } + + if (old_path_trace.empty() == false && std::filesystem::file_size(old_path_trace) == 0) + { + // the previous file wasnt used before changing the name, remove it + std::remove(old_path_trace.c_str()); + } + + old_path_trace = new_path_trace; + + BOOST_LOG_TRIVIAL(debug) << "Creating new file for " << id << " at " << old_path_trace; + + std::ofstream trace(old_path_trace); + if (!trace) + { + BOOST_LOG_TRIVIAL(error) << "Could not create file for " << id << " at " << old_path_trace; + + return false; + } + + // Trace file head + if (outputHeader) + { + trace << "station : " << id << "\n"; + trace << "start_epoch: " << acsConfig.start_epoch << "\n"; + trace << "end_epoch : " << acsConfig.end_epoch << "\n"; + trace << "trace_level: " << acsConfig.trace_level << "\n"; + trace << "pea_version: " << ginanCommitVersion() << "\n"; + // trace << "rts_lag : " << acsConfig.pppOpts.rts_lag << "\n"; + } + + if (outputConfig) + { + dumpConfig(trace); + } + + return true; } diff --git a/src/cpp/common/trace.hpp b/src/cpp/common/trace.hpp index baef524d6..4016d6e91 100644 --- a/src/cpp/common/trace.hpp +++ b/src/cpp/common/trace.hpp @@ -1,198 +1,211 @@ - - #pragma once -#include +#include +#include +#include +#include +#include +#include #include #include +#include #include #include - -#include -#include -#include +#include "common/eigenIncluder.hpp" namespace sinks = boost::log::sinks; - -#include -#include -#include - - -using std::vector; using std::string; +using std::vector; +using Trace = std::ostream; struct GTime; -extern boost::iostreams::stream< boost::iostreams::null_sink> nullStream; - - -using Trace = std::ostream; - -#include "eigenIncluder.hpp" +extern boost::iostreams::stream nullStream; struct ConsoleLog : public sinks::basic_formatted_sink_backend { - static bool useInteractive; - - // The function consumes the log records that come from the frontend - void consume( - boost::log::record_view const& rec, - sinks::basic_formatted_sink_backend::string_type const& log_string); + // The function consumes the log records that come from the frontend + void consume( + boost::log::record_view const& rec, + sinks::basic_formatted_sink_backend::string_type const& + log_string + ); }; - extern int traceLevel; -template -void tracepdeex_( - Trace& stream, - string const& fmt, - Arguments&&... args) +template +void tracepdeex_(Trace& stream, string const& fmt, Arguments&&... args) { - boost::format f(fmt); - int unroll[] {0, (f % std::forward(args), 0)...}; - stream << boost::str(f); + boost::format f(fmt); + int unroll[]{0, (f % std::forward(args), 0)...}; + stream << boost::str(f); } -templatevoid traceTrivialDebug_ (string const& fmt, Args&&... args){ boost::format f(fmt); int unroll[] {0, (f % std::forward(args), 0)...}; BOOST_LOG_TRIVIAL(debug) << boost::str(f);} -templatevoid traceTrivialInfo_ (string const& fmt, Args&&... args){ boost::format f(fmt); int unroll[] {0, (f % std::forward(args), 0)...}; BOOST_LOG_TRIVIAL(info) << boost::str(f);} +template +void traceTrivialDebug_(string const& fmt, Args&&... args) +{ + boost::format f(fmt); + int unroll[]{0, (f % std::forward(args), 0)...}; + BOOST_LOG_TRIVIAL(debug) << boost::str(f); +} +template +void traceTrivialInfo_(string const& fmt, Args&&... args) +{ + boost::format f(fmt); + int unroll[]{0, (f % std::forward(args), 0)...}; + BOOST_LOG_TRIVIAL(info) << boost::str(f); +} +template +void traceTrivialTrace_(string const& fmt, Args&&... args) +{ + boost::format f(fmt); + int unroll[]{0, (f % std::forward(args), 0)...}; + BOOST_LOG_TRIVIAL(trace) << boost::str(f); +} -template -std::ofstream getTraceFile( - T& thing, - bool json = false) +template +std::ofstream getTraceFile(T& thing, bool json = false) { - string traceFilename; - if (json) traceFilename = thing.jsonTraceFilename; - else traceFilename = thing.traceFilename; - - if (traceFilename.empty()) - { - return std::ofstream(); - } - - std::ofstream trace(traceFilename, std::ios::app); - if (!trace) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Could not open trace file for " << thing.id << " at " << traceFilename; - } - - return trace; + string traceFilename; + if (json) + traceFilename = thing.jsonTraceFilename; + else + traceFilename = thing.traceFilename; + + if (traceFilename.empty()) + { + return std::ofstream(); + } + + std::ofstream trace(traceFilename, std::ios::app); + if (!trace) + { + BOOST_LOG_TRIVIAL(error) << "Could not open trace file for " << thing.id << " at " + << traceFilename; + } + + return trace; } -void printHex( - Trace& trace, - vector& chunk); +void printHex(Trace& trace, vector& chunk); void traceFormatedFloat(Trace& trace, double val, string formatStr); struct Block { - Trace& trace; - string blockName; - - Block( - Trace& trace, - string blockName) - : trace {trace}, - blockName {blockName} - { - trace << "\n" << "+" << blockName << "\n"; - } - - ~Block() - { - trace << "-" << blockName << "\n"; - } + Trace& trace; + string blockName; + + Block(Trace& trace, string blockName) : trace{trace}, blockName{blockName} + { + trace << "\n" + << "+" << blockName << "\n"; + } + + ~Block() { trace << "-" << blockName << "\n"; } }; struct ArbitraryKVP { - string name; - string str; - double num = 0; - long int integer = 0; - int type = 0; - - ArbitraryKVP(string name, string str) : name {name}, str {str } { type = 0; } - ArbitraryKVP(string name, double num) : name {name}, num {num } { type = 1; } - ArbitraryKVP(string name, int integer) : name {name}, integer {integer } { type = 2; } - ArbitraryKVP(string name, long int integer) : name {name}, integer {integer } { type = 2; } - - string value() - { - if (isnan(num)) - { - num = -1; - } - - if (type == 0) return "\"" + str + "\""; - else if (type == 1) return std::to_string(num); - else if (type == 2) return std::to_string(integer); - else return ""; - } + string name; + string str; + double num = 0; + long int integer = 0; + int type = 0; + + ArbitraryKVP(string name, const char* str) : name{name}, str{str} { type = 0; } + ArbitraryKVP(string name, string str) : name{name}, str{str} { type = 0; } + ArbitraryKVP(string name, double num) : name{name}, num{num} { type = 1; } + ArbitraryKVP(string name, int integer) : name{name}, integer{integer} { type = 2; } + ArbitraryKVP(string name, long int integer) : name{name}, integer{integer} { type = 2; } + ArbitraryKVP(string name, bool integer) : name{name}, integer{integer} { type = 3; } + + bool isBool() const { return type == 3; } + + string value() + { + if (isnan(num)) + { + num = -1; + } + + if (type == 0) + return "\"" + str + "\""; + else if (type == 1) + return std::to_string(num); + else if (type == 2) + return std::to_string(integer); + else if (type == 3) + return std::to_string(integer); + else + return ""; + } }; -void traceJson_( - Trace& trace, - GTime& time, - vector id, - vector val); - +void traceJson_(Trace& trace, GTime& time, vector id, vector val); bool createNewTraceFile( - const string id, - boost::posix_time::ptime logptime, - string new_path_trace, - string& old_path_trace, - bool outputHeader = false, - bool outputConfig = false); - + const string id, + const string& source, + boost::posix_time::ptime logptime, + string new_path_trace, + string& old_path_trace, + bool outputHeader = false, + bool outputConfig = false +); extern boost::log::trivial::severity_level acsSeverity; -//wrap trace functions to lazily execute their parameter evaluations. - -#define traceTrivialDebug(...) \ -do \ -{ \ - if (acsSeverity > boost::log::trivial::debug) \ - continue; \ - \ - traceTrivialDebug_ (__VA_ARGS__); \ -} while (false) - -#define traceTrivialInfo(...) \ -do \ -{ \ - if (acsSeverity > boost::log::trivial::info) \ - continue; \ - \ - traceTrivialInfo_ (__VA_ARGS__); \ -} while (false) - -#define tracepdeex(level, ...) \ -do \ -{ \ - if (level > traceLevel) \ - continue; \ - \ - tracepdeex_ (__VA_ARGS__); \ -} while (false) - -#define traceJson(level, ...) \ -do \ -{ \ - if (level > traceLevel) \ - continue; \ - \ - if ( acsConfig.output_json_trace == false \ - &&acsConfig.mongoOpts.output_trace == false) \ - { \ - continue; \ - } \ - traceJson_ (__VA_ARGS__); \ -} while (false) +// wrap trace functions to lazily execute their parameter evaluations. + +#define traceTrivialDebug(...) \ + do \ + { \ + if (acsSeverity > boost::log::trivial::debug) \ + continue; \ + \ + traceTrivialDebug_(__VA_ARGS__); \ + } while (false) + +#define traceTrivialInfo(...) \ + do \ + { \ + if (acsSeverity > boost::log::trivial::info) \ + continue; \ + \ + traceTrivialInfo_(__VA_ARGS__); \ + } while (false) + +#define traceTrivialTrace(...) \ + do \ + { \ + if (acsSeverity > boost::log::trivial::trace) \ + continue; \ + \ + traceTrivialTrace_(__VA_ARGS__); \ + } while (false) + +#define tracepdeex(level, ...) \ + do \ + { \ + if (level > traceLevel) \ + continue; \ + \ + tracepdeex_(__VA_ARGS__); \ + } while (false) + +#define traceJson(level, ...) \ + do \ + { \ + if (level > traceLevel) \ + continue; \ + \ + if (acsConfig.output_json_trace == false && \ + acsConfig.mongoOpts.output_trace == E_Mongo::NONE) \ + { \ + continue; \ + } \ + traceJson_(__VA_ARGS__); \ + } while (false) diff --git a/src/cpp/common/tropSinex.cpp b/src/cpp/common/tropSinex.cpp index 887fbb748..50f493538 100644 --- a/src/cpp/common/tropSinex.cpp +++ b/src/cpp/common/tropSinex.cpp @@ -1,643 +1,910 @@ #include - -#include "eigenIncluder.hpp" -#include "inputsOutputs.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "tropModels.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "common.hpp" -#include "trace.hpp" -#include "gTime.hpp" -#include "sinex.hpp" -#include "EGM96.h" +#include "3rdparty/egm96/EGM96.h" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/sinex.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "pea/inputsOutputs.hpp" +#include "pea/peaCommitStrings.hpp" +#include "trop/tropModels.hpp" using std::ofstream; - /** Reset comments to their default values */ void resetCommentsToDefault() { - if (!theSinex.tropDesc .isEmpty) {theSinex.blockComments["TROP/DESCRIPTION"] .clear(); theSinex.blockComments["TROP/DESCRIPTION"] .push_back("*_________KEYWORD_____________ __VALUE(S)_______________________________________");} - if (!theSinex.mapsiteids .empty()) {theSinex.blockComments["SITE/ID"] .clear(); theSinex.blockComments["SITE/ID"] .push_back("*STATION__ PT __DOMES__ T _STATION_DESCRIPTION__ _LONGITUDE _LATITUDE_ _HGT_ELI_ HGT_GEOID");} - if (!theSinex.mapreceivers .empty()) {theSinex.blockComments["SITE/RECEIVER"] .clear(); theSinex.blockComments["SITE/RECEIVER"] .push_back("*STATION__ PT SOLN T DATA_START____ DATA_END______ DESCRIPTION_________ S/N_________________ FIRMWARE___");} - if (!theSinex.mapantennas .empty()) {theSinex.blockComments["SITE/ANTENNA"] .clear(); theSinex.blockComments["SITE/ANTENNA"] .push_back("*STATION__ PT SOLN T DATA_START____ DATA_END______ DESCRIPTION_________ S/N_________________ PCV_MODEL_");} - if (!theSinex.mapeccentricities .empty()) {theSinex.blockComments["SITE/ECCENTRICITY"].clear(); theSinex.blockComments["SITE/ECCENTRICITY"] .push_back("* _UP_____ _NORTH__ _EAST___"); - theSinex.blockComments["SITE/ECCENTRICITY"] .push_back("*STATION__ PT SOLN T DATA_START____ DATA_END______ REF _MARKER->ARP(m)____________");} - if (!theSinex.tropSiteCoordMapMap .empty()) {theSinex.blockComments["SITE/COORDINATES"] .clear(); theSinex.blockComments["SITE/COORDINATES"] .push_back("*STATION__ PT SOLN T __DATA_START__ __DATA_END____ __STA_X_____ __STA_Y_____ __STA_Z_____ SYSTEM REMRK");} + if (!theSinex.tropDesc.isEmpty) + { + theSinex.blockComments["TROP/DESCRIPTION"].clear(); + theSinex.blockComments["TROP/DESCRIPTION"].push_back( + "*_________KEYWORD_____________ __VALUE(S)_______________________________________" + ); + } + if (!theSinex.mapsiteids.empty()) + { + theSinex.blockComments["SITE/ID"].clear(); + theSinex.blockComments["SITE/ID"].push_back( + "*STATION__ PT __DOMES__ T _STATION_DESCRIPTION__ _LONGITUDE _LATITUDE_ _HGT_ELI_ " + "HGT_GEOID" + ); + } + if (!theSinex.mapreceivers.empty()) + { + theSinex.blockComments["SITE/RECEIVER"].clear(); + theSinex.blockComments["SITE/RECEIVER"].push_back( + "*STATION__ PT SOLN T DATA_START____ DATA_END______ DESCRIPTION_________ " + "S/N_________________ FIRMWARE___" + ); + } + if (!theSinex.mapantennas.empty()) + { + theSinex.blockComments["SITE/ANTENNA"].clear(); + theSinex.blockComments["SITE/ANTENNA"].push_back( + "*STATION__ PT SOLN T DATA_START____ DATA_END______ DESCRIPTION_________ " + "S/N_________________ PCV_MODEL_" + ); + } + if (!theSinex.mapeccentricities.empty()) + { + theSinex.blockComments["SITE/ECCENTRICITY"].clear(); + theSinex.blockComments["SITE/ECCENTRICITY"].push_back( + "* _UP_____ _NORTH__ _EAST___" + ); + theSinex.blockComments["SITE/ECCENTRICITY"].push_back( + "*STATION__ PT SOLN T DATA_START____ DATA_END______ REF _MARKER->ARP(m)____________" + ); + } + if (!theSinex.tropSiteCoordMapMap.empty()) + { + theSinex.blockComments["SITE/COORDINATES"].clear(); + theSinex.blockComments["SITE/COORDINATES"].push_back( + "*STATION__ PT SOLN T __DATA_START__ __DATA_END____ __STA_X_____ __STA_Y_____ " + "__STA_Z_____ SYSTEM REMRK" + ); + } } /** Write out trop sinex file header */ -void writeTropHeader( - ofstream& out) ///< stream to write out +void writeTropHeader(ofstream& out) ///< stream to write out { - tracepdeex(0, out, "%%=TRO %4.2lf %3s %04d:%03d:%05d %3s %04d:%03d:%05d %04d:%03d:%05d %c %4s\n", - theSinex.ver, - theSinex.createagc, - theSinex.filedate[0], - theSinex.filedate[1], - theSinex.filedate[2], - theSinex.dataagc, - theSinex.solutionstartdate[0], - theSinex.solutionstartdate[1], - theSinex.solutionstartdate[2], - theSinex.solutionenddate[0], - theSinex.solutionenddate[1], - theSinex.solutionenddate[2], - theSinex.obsCode, - theSinex.markerName); + tracepdeex( + 0, + out, + "%%=TRO %4.2lf %3s %04d:%03d:%05d %3s %04d:%03d:%05d %04d:%03d:%05d %c %4s\n", + theSinex.ver, + theSinex.createagc, + theSinex.filedate[0], + theSinex.filedate[1], + theSinex.filedate[2], + theSinex.dataagc, + theSinex.solutionstartdate[0], + theSinex.solutionstartdate[1], + theSinex.solutionstartdate[2], + theSinex.solutionenddate[0], + theSinex.solutionenddate[1], + theSinex.solutionenddate[2], + theSinex.obsCode, + theSinex.markerName + ); } /** Set data for trop description block */ -void setDescription( - bool isSmoothed) ///< if solution is smoothed (RTS or fixed-lag) +void setDescription(bool isSmoothed) ///< if solution is smoothed (RTS or fixed-lag) { - string tropEstMethod; - if (isSmoothed) tropEstMethod = "Smoother"; - else tropEstMethod = "Filter"; - - auto& recOpts = acsConfig.getRecOpts("global"); - - theSinex.tropDesc.strings ["TROPO MODELING METHOD"] = tropEstMethod; - theSinex.tropDesc.strings ["TIME SYSTEM"] = acsConfig.time_system; - theSinex.tropDesc.strings ["OCEAN TIDE LOADING MODEL"] = acsConfig.ocean_tide_loading_model; - theSinex.tropDesc.strings ["ATMOSPH TIDE LOADING MODEL"] = acsConfig.atmospheric_tide_loading_model; - theSinex.tropDesc.strings ["GEOID MODEL"] = acsConfig.geoid_model; - theSinex.tropDesc.ints ["TROPO SAMPLING INTERVAL"] = acsConfig.epoch_interval; - theSinex.tropDesc.strings ["A PRIORI TROPOSPHERE"] = recOpts.tropModel.models.front()._to_string(); - theSinex.tropDesc.strings ["TROPO MAPPING FUNCTION"] = recOpts.tropModel.models.front()._to_string(); - theSinex.tropDesc.strings ["GRADS MAPPING FUNCTION"] = acsConfig.gradient_mapping_function; - theSinex.tropDesc.ints ["ELEVATION CUTOFF ANGLE"] = recOpts.elevation_mask_deg; - theSinex.tropDesc.vecStrings["TROPO PARAMETER NAMES"].clear(); - theSinex.tropDesc.vecStrings["TROPO PARAMETER UNITS"].clear(); - theSinex.tropDesc.vecStrings["TROPO PARAMETER WIDTH"].clear(); - - if (theSinex.tropSolList.empty() == false) - for (auto& entry : theSinex.tropSolList.front().solutions) - { - theSinex.tropDesc.vecStrings["TROPO PARAMETER NAMES"].push_back(entry.type); - std::ostringstream unitsSs; - unitsSs << std::scientific << std::setprecision(0) << entry.units; // get into scientific format - theSinex.tropDesc.vecStrings["TROPO PARAMETER UNITS"].push_back(unitsSs.str()); - theSinex.tropDesc.vecStrings["TROPO PARAMETER WIDTH"].push_back(std::to_string(entry.width)); - } - theSinex.tropDesc.isEmpty = false; + string tropEstMethod; + if (isSmoothed) + tropEstMethod = "Smoother"; + else + tropEstMethod = "Filter"; + + auto& recOpts = acsConfig.getRecOpts("global"); + + theSinex.tropDesc.strings["TROPO MODELING METHOD"] = tropEstMethod; + theSinex.tropDesc.strings["TIME SYSTEM"] = acsConfig.time_system; + theSinex.tropDesc.strings["OCEAN TIDE LOADING MODEL"] = acsConfig.ocean_tide_loading_model; + theSinex.tropDesc.strings["ATMOSPH TIDE LOADING MODEL"] = + acsConfig.atmospheric_tide_loading_model; + theSinex.tropDesc.strings["GEOID MODEL"] = acsConfig.geoid_model; + theSinex.tropDesc.ints["TROPO SAMPLING INTERVAL"] = acsConfig.epoch_interval; + theSinex.tropDesc.strings["A PRIORI TROPOSPHERE"] = + enum_to_string(recOpts.tropModel.models.front()); + theSinex.tropDesc.strings["TROPO MAPPING FUNCTION"] = + enum_to_string(recOpts.tropModel.models.front()); + theSinex.tropDesc.strings["GRADS MAPPING FUNCTION"] = acsConfig.gradient_mapping_function; + theSinex.tropDesc.ints["ELEVATION CUTOFF ANGLE"] = recOpts.elevation_mask_deg; + theSinex.tropDesc.vecStrings["TROPO PARAMETER NAMES"].clear(); + theSinex.tropDesc.vecStrings["TROPO PARAMETER UNITS"].clear(); + theSinex.tropDesc.vecStrings["TROPO PARAMETER WIDTH"].clear(); + + if (theSinex.tropSolList.empty() == false) + for (auto& entry : theSinex.tropSolList.front().solutions) + { + theSinex.tropDesc.vecStrings["TROPO PARAMETER NAMES"].push_back(entry.type); + std::ostringstream unitsSs; + unitsSs << std::scientific << std::setprecision(0) + << entry.units; // get into scientific format + theSinex.tropDesc.vecStrings["TROPO PARAMETER UNITS"].push_back(unitsSs.str()); + theSinex.tropDesc.vecStrings["TROPO PARAMETER WIDTH"].push_back( + std::to_string(entry.width) + ); + } + theSinex.tropDesc.isEmpty = false; } /** Write TROP/DESCRIPTION block */ -void writeTropDesc( - ofstream& out) ///< stream to write out +void writeTropDesc(ofstream& out) ///< stream to write out { - Block block(out, "TROP/DESCRIPTION"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [key, entry] : theSinex.tropDesc.strings) tracepdeex(0, out, " %-29s %22s\n", key, entry); - for (auto& [key, entry] : theSinex.tropDesc.ints) tracepdeex(0, out, " %-29s %22d\n", key, entry); - for (auto& [key, entry] : theSinex.tropDesc.doubles) tracepdeex(0, out, " %-29s %22f\n", key, entry); - for (auto& [key, entry] : theSinex.tropDesc.vecStrings) - { - tracepdeex(0, out, " %-29s", key); - for (auto& str : entry) tracepdeex(0, out, " %6s", str); - tracepdeex(0, out, "\n"); - } + Block block(out, "TROP/DESCRIPTION"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [key, entry] : theSinex.tropDesc.strings) + tracepdeex(0, out, " %-29s %22s\n", key, entry); + for (auto& [key, entry] : theSinex.tropDesc.ints) + tracepdeex(0, out, " %-29s %22d\n", key, entry); + for (auto& [key, entry] : theSinex.tropDesc.doubles) + tracepdeex(0, out, " %-29s %22f\n", key, entry); + for (auto& [key, entry] : theSinex.tropDesc.vecStrings) + { + tracepdeex(0, out, " %-29s", key); + for (auto& str : entry) + tracepdeex(0, out, " %6s", str); + tracepdeex(0, out, "\n"); + } } /** Write SITE/ID block */ void writeTropSiteId( - ofstream& out, ///< stream to write out - string markerName) ///< recID if individual rec used for soln, else blank + ofstream& out, ///< stream to write out + string markerName ///< recID if individual rec used for soln, else blank +) { - Block block(out, "SITE/ID"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [id, ssi] : theSinex.mapsiteids) - { - if ( markerName != "MIX" - &&id != markerName) - { - continue; - } - if (ssi.used == false) - continue; - - // Retrieve rec pos - VectorPos pos; - pos.lat() = ssi.lat_deg + ssi.lat_min / 60.0 + ssi.lat_sec / 60.0 / 60.0; - pos.lon() = ssi.lon_deg + ssi.lon_min / 60.0 + ssi.lon_sec / 60.0 / 60.0; - pos.hgt() = ssi.height; - - Vector3d rRec; - auto it = theSinex.tropSiteCoordMapMap.find(id); - if (it != theSinex.tropSiteCoordMapMap.end()) - { - rRec = theSinex.tropSiteCoordMapMap[id]; - } - else - { - rRec = pos2ecef(pos); - } - - // Calc ant offset (ECEF) - SinexRecData stationSinex; - - auto result = getRecSnx(id, theSinex.solutionenddate, stationSinex); - - 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 other function - - VectorEnu& antdel = stationSinex.ecc_ptr->ecc; - Vector3d dr1 = enu2ecef(pos, antdel); - - // Calc ant pos (ECEF), convert to lat/lon/ht - rRec += dr1; - pos = ecef2pos(rRec); - double lat = pos.latDeg(); - double lon = pos.lonDeg(); - double hgt = pos.hgt(); - - while (lon < 0) - lon += 360; - - double offset = egm96_compute_altitude_offset(lat, lon); - - tracepdeex(0, out, " %-9s %2s %9s %c %22s %10.6lf %10.6lf %9.3lf %9.3lf\n", - ssi.sitecode, - ssi.ptcode, - ssi.domes, - ssi.typecode, - ssi.desc, - lon, - lat, - hgt, - hgt - offset); - } + Block block(out, "SITE/ID"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [id, entry] : theSinex.tropSiteCoordMapMap) + { + if (markerName != "MIX" && id != markerName) + { + 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; + + // Retrieve rec pos + VectorPos pos; + pos.lat() = ssi.lat_deg + ssi.lat_min / 60.0 + ssi.lat_sec / 60.0 / 60.0; + pos.lon() = ssi.lon_deg + ssi.lon_min / 60.0 + ssi.lon_sec / 60.0 / 60.0; + pos.hgt() = ssi.height; + + Vector3d rRec; + rRec = entry; + + // Calc ant offset (ECEF) + SinexRecData stationSinex; + + auto result = getRecSnx(id, theSinex.solutionenddate, stationSinex); + + 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 + // other function + + VectorEnu& antdel = stationSinex.ecc_ptr->ecc; + Vector3d dr1 = enu2ecef(pos, antdel); + + // Calc ant pos (ECEF), convert to lat/lon/ht + rRec += dr1; + pos = ecef2pos(rRec); + double lat = pos.latDeg(); + double lon = pos.lonDeg(); + double hgt = pos.hgt(); + + while (lon < 0) + lon += 360; + + double offset = egm96_compute_altitude_offset(lat, lon); + + tracepdeex( + 0, + out, + " %-9s %2s %9s %c %22s %10.6lf %10.6lf %9.3lf %9.3lf\n", + ssi.sitecode, + ssi.ptcode, + ssi.domes, + ssi.typecode, + ssi.desc, + lon, + lat, + hgt, + hgt - offset + ); + } + else + { + auto rec = acsConfig.getRecOpts(id); + string domes_number = rec.domes_number.empty() ? "----" : rec.domes_number; + string site_description = rec.site_description.empty() ? "----" : rec.site_description; + Vector3d rRec; + auto it = theSinex.tropSiteCoordMapMap.find(id); + if (it != theSinex.tropSiteCoordMapMap.end()) + { + rRec = theSinex.tropSiteCoordMapMap[id]; + } + else + { + rRec = rec.apriori_pos; + } + VectorEnu antdel = rec.eccentricityModel.eccentricity; + VectorPos pos = ecef2pos(rRec); + Vector3d dr1 = enu2ecef(pos, antdel); + + // Calc ant pos (ECEF), convert to lat/lon/ht + rRec += dr1; + pos = ecef2pos(rRec); + double lat = pos.latDeg(); + double lon = pos.lonDeg(); + double hgt = pos.hgt(); + + while (lon < 0) + lon += 360; + + double offset = egm96_compute_altitude_offset(lat, lon); + tracepdeex( + 0, + out, + " %-9s %2s %-9s %c %-22s %10.6lf %10.6lf %9.3lf %9.3lf\n", + id, + "A", + domes_number, + 'P', + site_description, + lon, + lat, + hgt, + hgt - offset + ); + } + } + // exit(0); } /** Write SITE/RECEIVER block */ void writeTropSiteRec( - ofstream& out, ///< stream to write out - string markerName) ///< recID if individual rec used for soln, else blank + ofstream& out, ///< stream to write out + string markerName ///< recID if individual rec used for soln, else blank +) { - Block block(out, "SITE/RECEIVER"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [id, timemap] : theSinex.mapreceivers) - for (auto it = timemap.rbegin(); it != timemap.rend(); it++) - { - auto& [time, receiver] = *it; - - if ( markerName != "MIX" - &&id != markerName) - { - continue; - } - - if (receiver.used == false) - continue; - - if (receiver.sn. empty()) receiver.sn = "-----"; - if (receiver.firm. empty()) receiver.firm = "-----"; - - tracepdeex(0, out, " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %20s %-20s %s\n", - receiver.sitecode .c_str(), - receiver.ptcode .c_str(), - receiver.solnid .c_str(), - receiver.typecode, - receiver.start[0], - receiver.start[1], - receiver.start[2], - receiver.end[0], - receiver.end[1], - receiver.end[2], - receiver.type .c_str(), - receiver.sn .c_str(), - receiver.firm .c_str()); - } + Block block(out, "SITE/RECEIVER"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [id, entry] : theSinex.tropSiteCoordMapMap) + { + if (markerName != "MIX" && id != markerName) + { + continue; + } + auto timemap_ = theSinex.mapreceivers.find(id); + if (timemap_ != theSinex.mapreceivers.end()) + { + auto& timeMap = timemap_->second; + for (auto it = timeMap.rbegin(); it != timeMap.rend(); it++) + { + auto& [time, receiver] = *it; + + if (receiver.used == false) + continue; + + if (receiver.sn.empty()) + receiver.sn = "-----"; + if (receiver.firm.empty()) + receiver.firm = "-----"; + + tracepdeex( + 0, + out, + " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %20s %-20s %s\n", + receiver.sitecode.c_str(), + receiver.ptcode.c_str(), + receiver.solnid.c_str(), + receiver.typecode, + receiver.start[0], + receiver.start[1], + receiver.start[2], + receiver.end[0], + receiver.end[1], + receiver.end[2], + receiver.type.c_str(), + receiver.sn.c_str(), + receiver.firm.c_str() + ); + } + } + else + { + auto rec = acsConfig.getRecOpts(id); + tracepdeex( + 0, + out, + " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %-20s %-20s %s\n", + id, + "A", + "----", + "P", + theSinex.solutionstartdate[0], + theSinex.solutionstartdate[1], + theSinex.solutionstartdate[2], + theSinex.solutionenddate[0], + theSinex.solutionenddate[1], + theSinex.solutionenddate[2], + rec.receiver_type.c_str(), + "-----", + "-----" + ); + } + } } /** Set antenna calibration model data for SITE/ANTENNA block */ void setSiteAntCalib() { - string defaultStr = "-----"; - for (auto& [site, antmap] : theSinex.mapantennas) - for (auto it = antmap.rbegin(); it != antmap.rend(); it++) - { - auto& [time, ant] = *it; - if ( ant.calibModel.empty() - ||ant.calibModel == defaultStr) - { - ant.calibModel = defaultStr; - PhaseCenterData* pcd_ptr; - bool pass = findAntenna(ant.type, E_Sys::GPS, time, nav, F1, &pcd_ptr); - if (pass) - ant.calibModel = pcd_ptr->calibModel; - } - } + string defaultStr = "-----"; + for (auto& [site, antmap] : theSinex.mapantennas) + for (auto it = antmap.rbegin(); it != antmap.rend(); it++) + { + auto& [time, ant] = *it; + if (ant.calibModel.empty() || ant.calibModel == defaultStr) + { + ant.calibModel = defaultStr; + PhaseCenterData* pcd_ptr; + bool pass = findAntenna(ant.type, E_Sys::GPS, time, nav, F1, &pcd_ptr); + if (pass) + ant.calibModel = pcd_ptr->calibModel; + } + } } /** Write SITE/ANTENNA block */ void writeTropSiteAnt( - ofstream& out, ///< stream to write out - string markerName) ///< recID if individual rec used for soln, else blank + ofstream& out, ///< stream to write out + string markerName ///< recID if individual rec used for soln, else blank +) { - Block block(out, "SITE/ANTENNA"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [id, antmap] : theSinex.mapantennas) - for (auto it = antmap.rbegin(); it != antmap.rend(); it++) - { - auto& [time, ant] = *it; - - if ( markerName != "MIX" - &&id != markerName) - { - continue; - } - if (ant.used == false) - continue; - - tracepdeex(0, out, " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %20s %-20s %-10s\n", - ant.sitecode, - ant.ptcode, - ant.solnnum, - ant.typecode, - ant.start[0], - ant.start[1], - ant.start[2], - ant.end[0], - ant.end[1], - ant.end[2], - ant.type, - ant.sn, - ant.calibModel); - } + Block block(out, "SITE/ANTENNA"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [id, entry] : theSinex.tropSiteCoordMapMap) + { + if (markerName != "MIX" && id != markerName) + { + continue; + } + auto timeMap_it = theSinex.mapantennas.find(id); + if (timeMap_it != theSinex.mapantennas.end()) + { + auto& timeMap = timeMap_it->second; + for (auto it = timeMap.rbegin(); it != timeMap.rend(); it++) + { + auto& [time, ant] = *it; + + if (ant.used == false) + continue; + tracepdeex( + 0, + out, + " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %20s %-20s %-10s\n", + ant.sitecode, + ant.ptcode, + ant.solnnum, + ant.typecode, + ant.start[0], + ant.start[1], + ant.start[2], + ant.end[0], + ant.end[1], + ant.end[2], + ant.type, + ant.sn, + ant.calibModel + ); + } + } + else + { + auto rec = acsConfig.getRecOpts(id); + auto ecc = rec.antenna_type; + string sn = "-----"; + string calib = "-----"; + tracepdeex( + 0, + out, + " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %20s %-20s %-10s\n", + id, + "A", + "----", + "P", + theSinex.solutionstartdate[0], + theSinex.solutionstartdate[1], + theSinex.solutionstartdate[2], + theSinex.solutionenddate[0], + theSinex.solutionenddate[1], + theSinex.solutionenddate[2], + rec.antenna_type, + sn, + calib + ); + } + } } /** Write SITE/ECCENTRICITY block */ void writeTropSiteEcc( - ofstream& out, ///< stream to write out - string markerName) ///< recID if individual rec used for soln, else blank + ofstream& out, ///< stream to write out + string markerName ///< recID if individual rec used for soln, else blank +) { - Block block(out, "SITE/ECCENTRICITY"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [id, setMap] : theSinex.mapeccentricities) - for (auto it = setMap.rbegin(); it != setMap.rend(); it++) - { - auto& [time, set] = *it; - - if ( markerName != "MIX" - &&id != markerName) - { - continue; - } - if (set.used == false) - continue; - - tracepdeex(0, out, " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %3s %8.4lf %8.4lf %8.4lf\n", - set.sitecode, - set.ptcode, - set.solnnum, - set.typecode, - set.start[0], - set.start[1], - set.start[2], - set.end[0], - set.end[1], - set.end[2], - set.rs, - set.ecc[2], - set.ecc[1], - set.ecc[0]); - } + Block block(out, "SITE/ECCENTRICITY"); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [id, entry] : theSinex.tropSiteCoordMapMap) + { + if (markerName != "MIX" && id != markerName) + { + continue; + } + auto timeMap_it = theSinex.mapeccentricities.find(id); + if (timeMap_it != theSinex.mapeccentricities.end()) + { + auto& timeMap = timeMap_it->second; + for (auto it = timeMap.rbegin(); it != timeMap.rend(); it++) + { + auto& [time, set] = *it; + + if (set.used == false) + continue; + + tracepdeex( + 0, + out, + " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %3s %8.4lf %8.4lf %8.4lf\n", + set.sitecode, + set.ptcode, + set.solnnum, + set.typecode, + set.start[0], + set.start[1], + set.start[2], + set.end[0], + set.end[1], + set.end[2], + set.rs, + set.ecc[2], + set.ecc[1], + set.ecc[0] + ); + } + } + else + { + auto rec = acsConfig.getRecOpts(id); + auto ecc = rec.eccentricityModel.eccentricity; + tracepdeex( + 0, + out, + " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %3s %8.4lf %8.4lf %8.4lf\n", + id, + "A", + "----", + "P", + theSinex.solutionstartdate[0], + theSinex.solutionstartdate[1], + theSinex.solutionstartdate[2], + theSinex.solutionenddate[0], + theSinex.solutionenddate[1], + theSinex.solutionenddate[2], + "UNE", + ecc[2], + ecc[1], + ecc[0] + ); + } + } } /** Write SITE/COORDINATES block */ void writeTropSiteCoord( - ofstream& out, ///< stream to write out - string markerName, ///< recID if individual rec used for soln, else blank - string filename) ///< filename of file to write out + ofstream& out, ///< stream to write out + string markerName, ///< recID if individual rec used for soln, else blank + string filename ///< filename of file to write out +) { - long int pos = theSinex.tropSiteCoordBodyFPosMap[filename]; - if (pos == 0) - { - out << "+SITE/COORDINATES" << "\n"; - - writeAsComments(out, theSinex.blockComments["SITE/COORDINATES"]); - - pos = out.tellp(); - - theSinex.tropSiteCoordBodyFPosMap[filename] = pos; - } - - out.seekp(pos); // Overwrite previous body entries - - for (auto& [id, entry] : theSinex.tropSiteCoordMapMap) - { - if ( markerName != "MIX" - &&id != markerName) - { - continue; - } - - tracepdeex(0, out, " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %12.3lf %12.3lf %12.3lf %6s %5s\n", - id, - theSinex.mapsiteids[id].ptcode, - 1, - 'P', //note: adjust if station is non-GNSS - see sinex_v201_appendix1_doc for other obs types (e.g. SLR) - theSinex.solutionstartdate[0], - theSinex.solutionstartdate[1], - theSinex.solutionstartdate[2], - theSinex.solutionenddate[0], - theSinex.solutionenddate[1], - theSinex.solutionenddate[2], - entry[0], - entry[1], - entry[2], - acsConfig.reference_system, - acsConfig.analysis_agency); - } - - out << "-SITE/COORDINATES" << "\n" << "\n"; + long int pos = theSinex.tropSiteCoordBodyFPosMap[filename]; + if (pos == 0) + { + out << "+SITE/COORDINATES" << "\n"; + + writeAsComments(out, theSinex.blockComments["SITE/COORDINATES"]); + + pos = out.tellp(); + + theSinex.tropSiteCoordBodyFPosMap[filename] = pos; + } + + out.seekp(pos); // Overwrite previous body entries + + for (auto& [id, entry] : theSinex.tropSiteCoordMapMap) + { + if (markerName != "MIX" && id != markerName) + { + continue; + } + + tracepdeex( + 0, + out, + " %-9s %2s %4s %c %04d:%03d:%05d %04d:%03d:%05d %12.3lf %12.3lf %12.3lf %6s %5s\n", + id, + theSinex.mapsiteids[id].ptcode, + 1, + 'P', // note: adjust if station is non-GNSS - see sinex_v201_appendix1_doc for other + // obs types (e.g. SLR) + theSinex.solutionstartdate[0], + theSinex.solutionstartdate[1], + theSinex.solutionstartdate[2], + theSinex.solutionenddate[0], + theSinex.solutionenddate[1], + theSinex.solutionenddate[2], + entry[0], + entry[1], + entry[2], + acsConfig.reference_system, + acsConfig.analysis_agency + ); + } + + out << "-SITE/COORDINATES" << "\n" + << "\n"; } /** Set troposphere solution data from filter */ -void setTropSolFromFilter( - KFState& kfState) ///< KF state +void setTropSolFromFilter(KFState& kfState) ///< KF state { - // Retrieve & accumulate KF & preprocessor values - struct State - { - double x = 0; - double var = 0; - }; - - map> tropSumMap; //for summing similar components - eg trop and trop_gm - - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( key.type != KF::TROP - &&key.type != KF::TROP_GRAD) - { - continue; - } - - string type; - if (key.type == KF::TROP) type = "TRO"; //zenith - else if (key.type == KF::TROP_GRAD && key.num == 0) type = "TGN"; //N gradient - else if (key.type == KF::TROP_GRAD && key.num == 1) type = "TGE"; //E gradient - - string typeWet = type + "WET"; - string typeTot = type + "TOT"; - - string id = theSinex.mapsiteids[key.str].sitecode; - - double x = 0; - double var = 0; - 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 - - // Add on filter estimates - if (key.type == KF::TROP) { tropSumMap[id][typeTot].x += x; tropSumMap[id][typeTot].var = newVar; } - { tropSumMap[id][typeWet].x += x; tropSumMap[id][typeWet].var = newVar; } - } - - auto& recOpts = acsConfig.getRecOpts("global"); - - // Store accumulated trop values for writing out later - for (auto& [id, entries] : tropSumMap) - { - SinexTropSol stationEntry; - stationEntry.site = id; - stationEntry.yds = theSinex.solutionenddate; - - for (auto& [type, entry] : entries) - { - double units = 1; - bool wet = false; - - if (type.substr(type.size() - 3) == "WET") { units = 1e3; wet = true; } - else if (type.substr(type.size() - 3) == "TOT") { units = 1e3; } - - if ( wet - &&type.substr(0,3) == "TRO") - { - auto it = theSinex.tropSiteCoordMapMap.find(id); - if (it == theSinex.tropSiteCoordMapMap.end()) - { - BOOST_LOG_TRIVIAL(error) - << "Error: theSinex.tropSiteCoordMapMap has no entry for " << id; - - continue; - } - - VectorEcef ecef = theSinex.tropSiteCoordMapMap[id]; - - VectorPos pos = ecef2pos(ecef); - - double modelledZhd = tropDryZTD(nullStream, recOpts.tropModel.models, kfState.time, pos); - - entry.x -= modelledZhd; - } - - 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}); - } - theSinex.tropSolList.push_back(stationEntry); - } + // Retrieve & accumulate KF & preprocessor values + struct State + { + double x = 0; + double var = 0; + }; + + map> + tropSumMap; // for summing similar components - eg trop and trop_gm + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::TROP && key.type != KF::TROP_GRAD) + { + continue; + } + string type; + if (key.type == KF::TROP) + type = "TRO"; // zenith + else if (key.type == KF::TROP_GRAD && key.num == 0) + type = "TGN"; // N gradient + else if (key.type == KF::TROP_GRAD && key.num == 1) + type = "TGE"; // E gradient + + string typeWet = type + "WET"; + string typeTot = type + "TOT"; + + string id = theSinex.mapsiteids[key.str].sitecode; + if (id.empty()) + { + id = key.str; + } + + double x = 0; + double var = 0; + 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 + + // Add on filter estimates + if (key.type == KF::TROP) + { + tropSumMap[id][typeTot].x += x; + tropSumMap[id][typeTot].var = newVar; + } + { + tropSumMap[id][typeWet].x += x; + tropSumMap[id][typeWet].var = newVar; + } + } + + auto& recOpts = acsConfig.getRecOpts("global"); + + // Store accumulated trop values for writing out later + for (auto& [id, entries] : tropSumMap) + { + SinexTropSol stationEntry; + stationEntry.site = id; + stationEntry.yds = theSinex.solutionenddate; + + for (auto& [type, entry] : entries) + { + double units = 1; + bool wet = false; + + if (type.substr(type.size() - 3) == "WET") + { + units = 1e3; + wet = true; + } + else if (type.substr(type.size() - 3) == "TOT") + { + units = 1e3; + } + + if (wet && type.substr(0, 3) == "TRO") + { + auto it = theSinex.tropSiteCoordMapMap.find(id); + if (it == theSinex.tropSiteCoordMapMap.end()) + { + BOOST_LOG_TRIVIAL(error) + << "theSinex.tropSiteCoordMapMap has no entry for " << id; + + continue; + } + + VectorEcef ecef = theSinex.tropSiteCoordMapMap[id]; + + VectorPos pos = ecef2pos(ecef); + + double modelledZhd = + tropDryZTD(nullStream, recOpts.tropModel.models, kfState.time, pos); + + entry.x -= modelledZhd; + } + + 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}); + } + theSinex.tropSolList.push_back(stationEntry); + } } /** Set troposphere solution header comment */ void setTropSolCommentList() { - // Adjust trop sol header fields - std::ostringstream headerFields; - - if (theSinex.tropSolList.empty() == false) - for (auto& entry : theSinex.tropSolList.front().solutions) - { - headerFields << " " << std::setw(entry.width) << entry.type; - } - - theSinex.blockComments["TROP/SOLUTION"].clear(); - theSinex.blockComments["TROP/SOLUTION"].push_back("*STATION__ ____EPOCH_____" + headerFields.str()); + // Adjust trop sol header fields + std::ostringstream headerFields; + + if (theSinex.tropSolList.empty() == false) + for (auto& entry : theSinex.tropSolList.front().solutions) + { + headerFields << " " << std::setw(entry.width) << entry.type; + } + + theSinex.blockComments["TROP/SOLUTION"].clear(); + theSinex.blockComments["TROP/SOLUTION"].push_back( + "*STATION__ ____EPOCH_____" + headerFields.str() + ); } /** Set troposphere solution data */ -void setTropSol( - KFState& kfState) ///< KF state +void setTropSol(KFState& kfState) ///< KF state { - auto source = acsConfig.trop_sinex_data_sources.front(); - switch (source) - { - case E_Source::KALMAN: - { - setTropSolFromFilter(kfState); - break; - } - default: - { - BOOST_LOG_TRIVIAL(error) - << "Error: Unrecognised troposphere delay source " << source; - } - } - setTropSolCommentList(); + auto source = acsConfig.trop_sinex_data_sources.front(); + switch (source) + { + case E_Source::KALMAN: + { + setTropSolFromFilter(kfState); + break; + } + default: + { + BOOST_LOG_TRIVIAL(error) << "Unrecognised troposphere delay source " << source; + } + } + setTropSolCommentList(); } /** Write TROP/SOLUTION block */ void writeTropSol( - ofstream& out, ///< stream to write out - string markerName, ///< recID if individual rec used for soln, else blank - string filename) ///< filename of file to write out + ofstream& out, ///< stream to write out + string markerName, ///< recID if individual rec used for soln, else blank + string filename ///< filename of file to write out +) { - long int pos = theSinex.tropSolFootFPosMap[filename]; - if (pos == 0) - { - out << "+TROP/SOLUTION" << "\n"; - writeAsComments(out, theSinex.blockComments["TROP/SOLUTION"]); - - pos = out.tellp(); - theSinex.tropSolFootFPosMap[filename] = pos; - } - - out.seekp(pos); // Append body entries each epoch, overwriting previous footer - - for (auto& entry : theSinex.tropSolList) - { - if ( markerName != "MIX" - &&entry.site != markerName) - { - continue; - } - - tracepdeex(0, out, " %-9s %04d:%03d:%05d", - entry.site, - entry.yds[0], - entry.yds[1], - entry.yds[2]); - - for (auto& solution : entry.solutions) - { - out << std::fixed << std::setprecision(2); // set number of decimal digits to 2 - out << " " << std::setw(solution.width) << solution.value * solution.units; - } - out << "\n"; - } - - pos = out.tellp(); - - theSinex.tropSolList.clear(); - theSinex.tropSolFootFPosMap[filename] = pos; - - out << "-TROP/SOLUTION" << "\n" << "\n"; + long int pos = theSinex.tropSolFootFPosMap[filename]; + if (pos == 0) + { + out << "+TROP/SOLUTION" << "\n"; + writeAsComments(out, theSinex.blockComments["TROP/SOLUTION"]); + + pos = out.tellp(); + theSinex.tropSolFootFPosMap[filename] = pos; + } + + out.seekp(pos); // Append body entries each epoch, overwriting previous footer + + for (auto& entry : theSinex.tropSolList) + { + if (markerName != "MIX" && entry.site != markerName) + { + continue; + } + + tracepdeex( + 0, + out, + " %-9s %04d:%03d:%05d", + entry.site, + entry.yds[0], + entry.yds[1], + entry.yds[2] + ); + + for (auto& solution : entry.solutions) + { + out << std::fixed << std::setprecision(2); // set number of decimal digits to 2 + out << " " << std::setw(solution.width) << solution.value * solution.units; + } + out << "\n"; + } + + pos = out.tellp(); + + theSinex.tropSolList.clear(); + theSinex.tropSolFootFPosMap[filename] = pos; + + out << "-TROP/SOLUTION" << "\n" + << "\n"; } /** Write troposphere Sinex data to file */ -void writeTropSinexToFile( - string filename, ///< filename of file to write out - string markerName) ///< recID if individual rec used for soln, else blank +void writeTropSinexToFile( + string filename, ///< filename of file to write out + string markerName ///< recID if individual rec used for soln, else blank +) { - ofstream fout(filename, std::fstream::in | std::fstream::out); - if (!fout) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Could not open " << filename << " for writing trop sinex"; - - return; - } - - // Write header if required - fout.seekp(0, fout.end); // seek to end of file - - long int pos = fout.tellp(); - - if (pos == 0) - { - writeTropHeader(fout); - - if (!theSinex.refstrings. empty()) { writeSnxReference (fout); } - if (!theSinex.tropDesc. isEmpty) { writeTropDesc (fout); } - if (!theSinex.mapsiteids. empty()) { writeTropSiteId (fout, markerName); } - if (!theSinex.mapreceivers. empty()) { writeTropSiteRec (fout, markerName); } - if (!theSinex.mapantennas. empty()) { writeTropSiteAnt (fout, markerName); } - if (!theSinex.mapeccentricities. empty()) { writeTropSiteEcc (fout, markerName); } - } - if ( !theSinex.tropSiteCoordMapMap. empty()) { writeTropSiteCoord (fout, markerName, filename); } - if ( !theSinex.tropSolList. empty()) { writeTropSol (fout, markerName, filename); } - - fout << "%=ENDTRO" << "\n"; + ofstream fout(filename, std::fstream::in | std::fstream::out); + if (!fout) + { + BOOST_LOG_TRIVIAL(warning) << "Could not open " << filename << " for writing trop sinex"; + + return; + } + + // Write header if required + fout.seekp(0, fout.end); // seek to end of file + + long int pos = fout.tellp(); + + if (pos == 0) + { + writeTropHeader(fout); + + if (!theSinex.refstrings.empty()) + { + writeSnxReference(fout); + } + if (!theSinex.tropDesc.isEmpty) + { + writeTropDesc(fout); + } + if (!theSinex.mapsiteids.empty()) + { + writeTropSiteId(fout, markerName); + } + if (!theSinex.mapreceivers.empty()) + { + writeTropSiteRec(fout, markerName); + } + if (!theSinex.mapantennas.empty()) + { + writeTropSiteAnt(fout, markerName); + } + if (!theSinex.mapeccentricities.empty()) + { + writeTropSiteEcc(fout, markerName); + } + } + if (!theSinex.tropSiteCoordMapMap.empty()) + { + writeTropSiteCoord(fout, markerName, filename); + } + if (!theSinex.tropSolList.empty()) + { + writeTropSol(fout, markerName, filename); + } + + fout << "%=ENDTRO" << "\n"; } /** Output troposphere SINEX data */ void outputTropSinex( - string filename, ///< filename of file to write out - GTime time, ///< epoch of solution - KFState& kfState, ///< KF state - string markerName, ///< name of station to use ("MIX" for all) - bool isSmoothed) ///< if solution is smoothed (RTS or fixed-lag) + string filename, ///< filename of file to write out + GTime time, ///< epoch of solution + KFState& kfState, ///< KF state + string markerName, ///< name of station to use ("MIX" for all) + bool isSmoothed ///< if solution is smoothed (RTS or fixed-lag) +) { - theSinex.markerName = markerName.substr(0,4); - sinexCheckAddGaReference( acsConfig.trop_sinex_sol_type, - acsConfig.analysis_software_version, - true); + theSinex.markerName = markerName.substr(0, 4); + sinexCheckAddGaReference(acsConfig.trop_sinex_sol_type, ginanCommitVersion(), true); - KFState sinexSubstate = mergeFilters({&kfState}, {KF::ONE, KF::REC_POS, KF::REC_POS_RATE, KF::TROP, KF::TROP_GRAD}); + KFState sinexSubstate = + mergeFilters({&kfState}, {KF::ONE, KF::REC_POS, KF::REC_POS_RATE, KF::TROP, KF::TROP_GRAD}); - PTime startTime; - startTime.bigTime = boost::posix_time::to_time_t(acsConfig.start_epoch); - string dataAgc; - string contents; - updateSinexHeader( acsConfig.analysis_agency, - dataAgc, - (GTime) startTime, - time, - acsConfig.trop_sinex_obs_code, - acsConfig.trop_sinex_const_code, - contents, - sinexSubstate.x.rows() - 1, - acsConfig.trop_sinex_version); + PTime startTime; + startTime.bigTime = boost::posix_time::to_time_t(acsConfig.start_epoch); + string dataAgc; + string contents; + updateSinexHeader( + acsConfig.analysis_agency, + dataAgc, + (GTime)startTime, + time, + acsConfig.trop_sinex_obs_code, + acsConfig.trop_sinex_const_code, + contents, + sinexSubstate.x.rows() - 1, + acsConfig.trop_sinex_version + ); - for (auto& [key, index] : sinexSubstate.kfIndexMap) - { - if (key.type != KF::REC_POS) - continue; + for (auto& [key, index] : sinexSubstate.kfIndexMap) + { + if (key.type != KF::REC_POS) + continue; - sinexSubstate.getKFValue(key, theSinex.tropSiteCoordMapMap[key.str][key.num]); - } + sinexSubstate.getKFValue(key, theSinex.tropSiteCoordMapMap[key.str][key.num]); + } - setSiteAntCalib(); + setSiteAntCalib(); - setTropSol(sinexSubstate); + setTropSol(sinexSubstate); - setDescription(isSmoothed); + setDescription(isSmoothed); - replaceTimes(filename, acsConfig.start_epoch); + replaceTimes(filename, acsConfig.start_epoch); - resetCommentsToDefault(); + resetCommentsToDefault(); - writeTropSinexToFile(filename, markerName); + writeTropSinexToFile(filename, markerName); } diff --git a/src/cpp/common/ubxDecoder.cpp b/src/cpp/common/ubxDecoder.cpp index 86112c247..2ab8d54c8 100644 --- a/src/cpp/common/ubxDecoder.cpp +++ b/src/cpp/common/ubxDecoder.cpp @@ -1,358 +1,348 @@ - - // #pragma GCC optimize ("O0") +#include "common/ubxDecoder.hpp" +#include #include "architectureDocs.hpp" - -FileType UBX__() -{ - -} - -#include "observations.hpp" -#include "navigation.hpp" -#include "ubxDecoder.hpp" -#include "icdDecoder.hpp" -#include "streamUbx.hpp" -#include "constants.hpp" -#include "gTime.hpp" -#include "enums.h" - - -map ubxSysMap = -{ - {0, E_Sys::GPS}, - {1, E_Sys::SBS}, - {2, E_Sys::GAL}, - {3, E_Sys::BDS}, - {4, E_Sys::IMS}, - {5, E_Sys::QZS}, - {6, E_Sys::GLO} +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/icdDecoder.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/streamUbx.hpp" + +FileType UBX__() {} + +map ubxSysMap = { + {0, E_Sys::GPS}, + {1, E_Sys::SBS}, + {2, E_Sys::GAL}, + {3, E_Sys::BDS}, + {4, E_Sys::IMS}, + {5, E_Sys::QZS}, + {6, E_Sys::GLO} }; -map> ubxSysObsCodeMap = -{ - { E_Sys::GPS, - { - {0, E_ObsCode::L1C}, - {3, E_ObsCode::L2C}, - {4, E_ObsCode::L2C}, - {6, E_ObsCode::L5I}, - {7, E_ObsCode::L5Q} - } - } +map> ubxSysObsCodeMap = { + {E_Sys::GPS, + {{0, E_ObsCode::L1C}, + {3, E_ObsCode::L2C}, + {4, E_ObsCode::L2C}, + {6, E_ObsCode::L5I}, + {7, E_ObsCode::L5Q}}} }; -map>> UbxDecoder::gyroDataMaps; -map>> UbxDecoder::acclDataMaps; -map>> UbxDecoder::tempDataMaps; +map>> UbxDecoder::gyroDataMaps; +map>> UbxDecoder::acclDataMaps; +map>> UbxDecoder::tempDataMaps; -void UbxDecoder::decodeRAWX( - vector& payload) +void UbxDecoder::decodeRAWX(vector& payload) { -// std::cout << "Recieved RAWX message" << "\n"; - - double rcvTow = *((double*) &payload[0]); - short unsigned int week = *((short unsigned int*) &payload[8]); - char leapS = *((char*) &payload[10]); - unsigned char numMeas = payload[11]; - - if (payload.size() != 16 + 32 * numMeas) - { - return; - } - -// std::cout << "\n" << "Recieved RAWX message has " << numMeas << " measurements" << "\n"; - - map obsMap; - - for (int i = 0; i < numMeas; i++) - { - unsigned char* measPayload = &payload[i*32]; //below offsets dont start at zero, this matches spec - - double pr = *((double*) &measPayload[16]); - double cp = *((double*) &measPayload[24]); - float dop = *((float*) &measPayload[32]); - int gnssId = measPayload[36]; - int satId = measPayload[37]; - int sigId = measPayload[38]; - - E_Sys sys = ubxSysMap[gnssId]; - - if (sys == +E_Sys::NONE) - continue; - - E_ObsCode obsCode = ubxSysObsCodeMap[sys][sigId]; - - if (obsCode == +E_ObsCode::NONE) - continue; - - Sig sig; - sig.code = obsCode; - sig.L = cp; - sig.P = pr; - sig.D = dop; - - SatSys Sat(sys, satId); - auto& obs = obsMap[Sat]; - obs.Sat = Sat; - obs.time = gpst2time(week, rcvTow); - - printf("meas %s %s %s %14.3lf %14.3lf\n", obs.time.to_string().c_str(), Sat.id().c_str(), obsCode._to_string(), pr, cp); - auto ft = code2Freq[sys][obsCode]; - obs.sigsLists[ft].push_back(sig); - } - - ObsList obsList; - - for (auto& [Sat, obs] : obsMap) - { - obsList.push_back((shared_ptr)obs); - } - - obsListList.push_back(obsList); - - lastTimeTag = 0; - lastTime = gpst2time(week, rcvTow); + // std::cout << "Recieved RAWX message" << "\n"; + + double rcvTow = *((double*)&payload[0]); + short unsigned int week = *((short unsigned int*)&payload[8]); + char leapS = *((char*)&payload[10]); + unsigned char numMeas = payload[11]; + + if (payload.size() != 16 + 32 * numMeas) + { + return; + } + + // std::cout << "\n" << "Recieved RAWX message has " << numMeas << " measurements" << "\n"; + + map obsMap; + + for (int i = 0; i < numMeas; i++) + { + unsigned char* measPayload = + &payload[i * 32]; // below offsets dont start at zero, this matches spec + + double pr = *((double*)&measPayload[16]); + double cp = *((double*)&measPayload[24]); + float dop = *((float*)&measPayload[32]); + int gnssId = measPayload[36]; + int satId = measPayload[37]; + int sigId = measPayload[38]; + + E_Sys sys = ubxSysMap[gnssId]; + + if (sys == E_Sys::NONE) + continue; + + E_ObsCode obsCode = ubxSysObsCodeMap[sys][sigId]; + + if (obsCode == E_ObsCode::NONE) + continue; + + Sig sig; + sig.code = obsCode; + sig.L = cp; + sig.P = pr; + sig.D = dop; + + SatSys Sat(sys, satId); + auto& obs = obsMap[Sat]; + obs.Sat = Sat; + obs.time = gpst2time(week, rcvTow); + + printf( + "meas %s %s %s %14.3lf %14.3lf\n", + obs.time.to_string().c_str(), + Sat.id().c_str(), + enum_to_string(obsCode).c_str(), + pr, + cp + ); + auto ft = code2Freq[sys][obsCode]; + obs.sigsLists[ft].push_back(sig); + } + + ObsList obsList; + + for (auto& [Sat, obs] : obsMap) + { + obsList.push_back((shared_ptr)obs); + } + + obsListList.push_back(obsList); + + lastTimeTag = 0; + lastTime = gpst2time(week, rcvTow); } - -void UbxDecoder::decodeMEAS( - vector& payload) +void UbxDecoder::decodeMEAS(vector& payload) { - unsigned int timeTag = *((unsigned int*) &payload[0]); - short unsigned int flags = *((short unsigned int*) &payload[4]); - short unsigned int id = *((short unsigned int*) &payload[6]); - - int numMeas = flags >> 11; - - //adjust time tags - if (lastTimeTag == 0) - { - lastTimeTag = timeTag; - } - - double timeOffset = ((signed int)(timeTag - lastTimeTag)) * 1e-3; - -// std::cout << "\n" << "Recieved MEAS message has " << numMeas << " measurements at " << timeOffset << "\n"; - - for (int i = 0; i < numMeas; i++) - { - unsigned int data = *((unsigned int*) &payload[8 + 4 * i]); - - data &= 0x3fffffff; - - unsigned int dataType = data >> 24; - int dataField = data &= 0x00ffffff; - - dataField <<= 8; //get leading ones - dataField >>= 8; - - E_MEASDataType measDataType = E_MEASDataType::_from_integral(dataType); - - switch (measDataType) - { - default: - { -// std::cout << "\n" << measDataType._to_string(); - break; - } - case E_MEASDataType::GYRO_X: - case E_MEASDataType::GYRO_Y: - case E_MEASDataType::GYRO_Z: - { - double gyro = dataField * P2_12; -// std::cout << "\n" << measDataType._to_string() << " : " << gyro; - - int index = 0; - if (measDataType == +E_MEASDataType::GYRO_X) index = 0; //ubx indices are dumb and not ordered - else if (measDataType == +E_MEASDataType::GYRO_Y) index = 1; - else if (measDataType == +E_MEASDataType::GYRO_Z) index = 2; - - gyroDataMaps[recId][lastTime + timeOffset][index] = gyro; - - break; - } - case E_MEASDataType::ACCL_X: - case E_MEASDataType::ACCL_Y: - case E_MEASDataType::ACCL_Z: - { - double accl = dataField * P2_10; -// std::cout << "\n" << measDataType._to_string() << " : " << accl; - - int index = 0; - if (measDataType == +E_MEASDataType::ACCL_X) index = 0; - else if (measDataType == +E_MEASDataType::ACCL_Y) index = 1; - else if (measDataType == +E_MEASDataType::ACCL_Z) index = 2; - - acclDataMaps[recId][lastTime + timeOffset][index] = accl; - - break; - } - case E_MEASDataType::GYRO_TEMP: - { - double temp = dataField * 1e-2; -// std::cout << "\n" << measDataType._to_string() << " : " << temp; - - tempDataMaps[recId][lastTime + timeOffset] = temp; - - break; - } - } - } + unsigned int timeTag = *((unsigned int*)&payload[0]); + short unsigned int flags = *((short unsigned int*)&payload[4]); + short unsigned int id = *((short unsigned int*)&payload[6]); + + int numMeas = flags >> 11; + + // adjust time tags + if (lastTimeTag == 0) + { + lastTimeTag = timeTag; + } + + double timeOffset = ((signed int)(timeTag - lastTimeTag)) * 1e-3; + + // std::cout << "\n" << "Recieved MEAS message has " << numMeas << " measurements at " << + // timeOffset << "\n"; + + for (int i = 0; i < numMeas; i++) + { + unsigned int data = *((unsigned int*)&payload[8 + 4 * i]); + + data &= 0x3fffffff; + + unsigned int dataType = data >> 24; + int dataField = data &= 0x00ffffff; + + dataField <<= 8; // get leading ones + dataField >>= 8; + + E_MEASDataType measDataType = int_to_enum(dataType); + + switch (measDataType) + { + default: + { + // std::cout << "\n" << enum_to_string(measDataType); + break; + } + case E_MEASDataType::GYRO_X: + case E_MEASDataType::GYRO_Y: + case E_MEASDataType::GYRO_Z: + { + double gyro = dataField * P2_12; + // std::cout << "\n" << enum_to_string(measDataType) << " : " << gyro; + + int index = 0; + if (measDataType == E_MEASDataType::GYRO_X) + index = 0; // ubx indices are dumb and not ordered + else if (measDataType == E_MEASDataType::GYRO_Y) + index = 1; + else if (measDataType == E_MEASDataType::GYRO_Z) + index = 2; + + gyroDataMaps[recId][lastTime + timeOffset][index] = gyro; + + break; + } + case E_MEASDataType::ACCL_X: + case E_MEASDataType::ACCL_Y: + case E_MEASDataType::ACCL_Z: + { + double accl = dataField * P2_10; + // std::cout << "\n" << enum_to_string(measDataType) << " : " << accl; + + int index = 0; + if (measDataType == E_MEASDataType::ACCL_X) + index = 0; + else if (measDataType == E_MEASDataType::ACCL_Y) + index = 1; + else if (measDataType == E_MEASDataType::ACCL_Z) + index = 2; + + acclDataMaps[recId][lastTime + timeOffset][index] = accl; + + break; + } + case E_MEASDataType::GYRO_TEMP: + { + double temp = dataField * 1e-2; + // std::cout << "\n" << enum_to_string(measDataType) << " : " << temp; + + tempDataMaps[recId][lastTime + timeOffset] = temp; + + break; + } + } + } } -signed int gpsBitSFromWord( - vector& words, - int wordNum, - int offset, - int len) +signed int gpsBitSFromWord(vector& words, int wordNum, int offset, int len) { - signed int word = words[wordNum-1]; - offset -= 1; //icd counts from 1 - offset %= 30; //icd words have indices like 31.. - word <<= offset; - word >>= 32 - len; + signed int word = words[wordNum - 1]; + offset -= 1; // icd counts from 1 + offset %= 30; // icd words have indices like 31.. + word <<= offset; + word >>= 32 - len; - return word; + return word; } -unsigned int gpsBitUFromWord( - vector& words, - int wordNum, - int offset, - int len) +unsigned int gpsBitUFromWord(vector& words, int wordNum, int offset, int len) { - unsigned int word = words[wordNum-1]; - offset -= 1; //icd counts from 1 - offset %= 30; //icd words have indices like 31.. - word <<= offset; - word >>= 32 - len; + unsigned int word = words[wordNum - 1]; + offset -= 1; // icd counts from 1 + offset %= 30; // icd words have indices like 31.. + word <<= offset; + word >>= 32 - len; - return word; + return word; } -#include - -void UbxDecoder::decodeEphFrames( - SatSys Sat) +void UbxDecoder::decodeEphFrames(SatSys Sat) { - Eph eph; - bool pass = true; - - pass &= decodeGpsSubframe(subframeMap[Sat][1], eph); - pass &= decodeGpsSubframe(subframeMap[Sat][2], eph); - pass &= decodeGpsSubframe(subframeMap[Sat][3], eph); - - if (pass) - { - std::cout << "\n" << "*"; - eph.Sat = Sat; - eph.type = E_NavMsgType::LNAV; - nav.ephMap[eph.Sat][eph.type][eph.toe] = eph; - - - bsoncxx::builder::basic::document doc = {}; - - traceBrdcEphBody(doc, eph); - - std::cout << bsoncxx::to_json(doc) << "\n"; -// -// if (acsConfig.output_decoded_rtcm_json) -// traceBrdcEph(RtcmMessageType::GPS_EPHEMERIS, eph); -// -// if (acsConfig.localMongo.output_rtcm_messages) 11, iode27 -// mongoBrdcEph(eph); - } + Eph eph; + bool pass = true; + + pass &= decodeGpsSubframe(subframeMap[Sat][1], eph); + pass &= decodeGpsSubframe(subframeMap[Sat][2], eph); + pass &= decodeGpsSubframe(subframeMap[Sat][3], eph); + + if (pass) + { + std::cout << "\n" + << "*"; + eph.Sat = Sat; + eph.type = E_NavMsgType::LNAV; + nav.ephMap[eph.Sat][eph.type][eph.toe] = eph; + + boost::json::object doc = {}; + + traceBrdcEphBody(doc, eph); + + std::cout << boost::json::serialize(doc) << "\n"; + // + // if (acsConfig.output_decoded_rtcm_json) + // traceBrdcEph(RtcmMessageType::GPS_EPHEMERIS, eph); + // + // if (acsConfig.localMongo.output_rtcm_messages) 11, iode27 + // mongoBrdcEph(eph); + } } -void UbxDecoder::decodeSFRBX( - vector& payload) +void UbxDecoder::decodeSFRBX(vector& payload) { -// std::cout << "Recieved SFRBX message" << "\n"; - if (payload.size() < 5) - return; - - int gnssId = payload[0]; - int satId = payload[1]; - int frameLen = payload[4]; - - if (frameLen != (payload.size() - 8) / 4.0) - return; - - E_Sys sys = ubxSysMap[gnssId]; - - if (sys == +E_Sys::NONE) - return; - - if (sys != +E_Sys::GPS) - return; - - SatSys Sat(sys, satId); - -// printf("\n %s ", Sat.id().c_str()); - - for (int b = 0; b < 8; b++) - { - auto byte = payload[b]; -// if (b % 4 == 0) -// printf("--- "); -// printf("%02x ", byte); - } - - vector frameWords; - - int* words = (int*) &payload.data()[8]; - for (int f = 0; f < frameLen; f++) - { - int word = words[f] << 2; - - frameWords.push_back(word); - -// printf("%08x ", word); - } - - int preamble = gpsBitUFromWord(frameWords, 1, 1, 8); - int subFrameId = gpsBitUFromWord(frameWords, 2, 20, 3); - - if (preamble != 0x8b) - return; - -// printf("\n preamble : %02x - subFrameId : %02x - ", preamble, subFrameId); - - if ( subFrameId <= 0 - &&subFrameId >= 4) - { - return; - } - -// vector subFrame; -// int byteBits = 0; -// unsigned char byte; -// for (auto& word : frameWords) -// for (int j = 23; j >= 0; j--) -// { -// byte <<= 1; -// byte += (word >> j) & 1; -// -// byteBits++; -// if (byteBits == 8) -// { -// byteBits = 0; -// subFrame.push_back(byte); -// } -// } - - if (1) - { - subframeMap[Sat][subFrameId] = frameWords; - } - - switch (subFrameId) - { - default: break; - case 2: - case 3: decodeEphFrames(Sat); break; - } + // std::cout << "Recieved SFRBX message" << "\n"; + if (payload.size() < 5) + return; + + int gnssId = payload[0]; + int satId = payload[1]; + int frameLen = payload[4]; + + if (frameLen != (payload.size() - 8) / 4.0) + return; + + E_Sys sys = ubxSysMap[gnssId]; + + if (sys == E_Sys::NONE) + return; + + if (sys != E_Sys::GPS) + return; + + SatSys Sat(sys, satId); + + // printf("\n %s ", Sat.id().c_str()); + + for (int b = 0; b < 8; b++) + { + auto byte = payload[b]; + // if (b % 4 == 0) + // printf("--- "); + // printf("%02x ", byte); + } + + vector frameWords; + + int* words = (int*)&payload.data()[8]; + for (int f = 0; f < frameLen; f++) + { + int word = words[f] << 2; + + frameWords.push_back(word); + + // printf("%08x ", word); + } + + int preamble = gpsBitUFromWord(frameWords, 1, 1, 8); + int subFrameId = gpsBitUFromWord(frameWords, 2, 20, 3); + + if (preamble != 0x8b) + return; + + // printf("\n preamble : %02x - subFrameId : %02x - ", preamble, subFrameId); + + if (subFrameId <= 0 && subFrameId >= 4) + { + return; + } + + // vector subFrame; + // int byteBits = 0; + // unsigned char byte; + // for (auto& word : frameWords) + // for (int j = 23; j >= 0; j--) + // { + // byte <<= 1; + // byte += (word >> j) & 1; + // + // byteBits++; + // if (byteBits == 8) + // { + // byteBits = 0; + // subFrame.push_back(byte); + // } + // } + + if (1) + { + subframeMap[Sat][subFrameId] = frameWords; + } + + switch (subFrameId) + { + default: + break; + case 2: + case 3: + decodeEphFrames(Sat); + break; + } } - diff --git a/src/cpp/common/ubxDecoder.hpp b/src/cpp/common/ubxDecoder.hpp index e567dacd6..3220b7918 100644 --- a/src/cpp/common/ubxDecoder.hpp +++ b/src/cpp/common/ubxDecoder.hpp @@ -1,130 +1,140 @@ - #pragma once - // #pragma GCC optimize ("O0") -#include #include +#include +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/icdDecoder.hpp" +#include "common/streamObs.hpp" -using std::vector; using std::map; +using std::vector; -#include "icdDecoder.hpp" -#include "streamObs.hpp" -#include "gTime.hpp" -#include "enums.h" - -extern map ubxSysMap; -extern map> ubxSysObsCodeMap; +extern map ubxSysMap; +extern map> ubxSysObsCodeMap; struct UbxDecoder : ObsLister, IcdDecoder { - static map>> gyroDataMaps; - static map>> acclDataMaps; - static map>> tempDataMaps; - - unsigned int lastTimeTag = 0; - GTime lastTime; - - string recId; - - string raw_ubx_filename; - - void decodeEphFrames( - SatSys Sat); - - void decodeRAWX( - vector& payload); - - void decodeSFRBX( - vector& payload); - - void decodeMEAS( - vector& payload); - - void decodeRXM( - vector& payload, - unsigned char id) - { -// printf("\nReceived RXM-0x%02x message", id); - switch (id) - { - default: - { - break; - } - case E_RXMId::RAWX: { decodeRAWX (payload); break; } - case E_RXMId::SFRBX: { decodeSFRBX (payload); break; } - } - } - - void decodeESF( - vector& payload, - unsigned char id) - { -// printf("\nReceived ESF-0x%02x message", id); - switch (id) - { - default: - { - break; - } - case E_ESFId::MEAS: { decodeMEAS (payload); break; } - } - } - - void decode( - unsigned char ubxClass, - unsigned char id, - vector& payload) - { -// printf("\nReceived ubx: 0x%02x : 0x%02x > %ld bytes", ubxClass, id, payload.size()); - - switch (ubxClass) - { - default: { break; } - case E_UBXClass::RXM: { decodeRXM(payload, id); break; } - case E_UBXClass::ESF: { decodeESF(payload, id); break; } - } - } - - void recordFrame( - unsigned char ubxClass, - unsigned char id, - vector& data, - unsigned short int crcRead) - { - if (raw_ubx_filename.empty()) - { - return; - } - - std::ofstream ofs(raw_ubx_filename, std::ofstream::app); - - if (!ofs) - { - return; - } - -// //Write the custom time stamp message. -// RtcmEncoder encoder; -// encoder.rtcmTraceFilename = rtcmTraceFilename; -// -// auto buffer = encoder.encodeTimeStampRTCM(); -// bool write = encoder.encodeWriteMessageToBuffer(buffer); -// -// if (write) -// { -// encoder.encodeWriteMessages(ofs); -// } - - //copy the message to the output file too - unsigned short int payloadLength = data.size(); - - ofs.write((char *)&ubxClass, 1); - ofs.write((char *)&id, 1); - ofs.write((char *)&payloadLength, 2); - ofs.write((char *)data.data(), data.size()); - ofs.write((char *)&crcRead, 3); - } + static map>> gyroDataMaps; + static map>> acclDataMaps; + static map>> tempDataMaps; + + unsigned int lastTimeTag = 0; + GTime lastTime; + + string recId; + + string raw_ubx_filename; + + void decodeEphFrames(SatSys Sat); + + void decodeRAWX(vector& payload); + + void decodeSFRBX(vector& payload); + + void decodeMEAS(vector& payload); + + void decodeRXM(vector& payload, unsigned char id) + { + // printf("\nReceived RXM-0x%02x message", id); + switch (id) + { + default: + { + break; + } + case static_cast(E_RXMId::RAWX): + { + decodeRAWX(payload); + break; + } + case static_cast(E_RXMId::SFRBX): + { + decodeSFRBX(payload); + break; + } + } + } + + void decodeESF(vector& payload, unsigned char id) + { + // printf("\nReceived ESF-0x%02x message", id); + switch (id) + { + default: + { + break; + } + case static_cast(E_ESFId::MEAS): + { + decodeMEAS(payload); + break; + } + } + } + + void decode(unsigned char ubxClass, unsigned char id, vector& payload) + { + // printf("\nReceived ubx: 0x%02x : 0x%02x > %ld bytes", ubxClass, id, payload.size()); + + switch (ubxClass) + { + default: + { + break; + } + case static_cast(E_UBXClass::RXM): + { + decodeRXM(payload, id); + break; + } + case static_cast(E_UBXClass::ESF): + { + decodeESF(payload, id); + break; + } + } + } + + void recordFrame( + unsigned char ubxClass, + unsigned char id, + vector& data, + unsigned short int crcRead + ) + { + if (raw_ubx_filename.empty()) + { + return; + } + + std::ofstream ofs(raw_ubx_filename, std::ofstream::app); + + if (!ofs) + { + return; + } + + // //Write the custom time stamp message. + // RtcmEncoder encoder; + // encoder.rtcmTraceFilename = rtcmTraceFilename; + // + // auto buffer = encoder.encodeTimeStampRTCM(); + // bool write = encoder.encodeWriteMessageToBuffer(buffer); + // + // if (write) + // { + // encoder.encodeWriteMessages(ofs); + // } + + // copy the message to the output file too + unsigned short int payloadLength = data.size(); + + ofs.write((char*)&ubxClass, 1); + ofs.write((char*)&id, 1); + ofs.write((char*)&payloadLength, 2); + ofs.write((char*)data.data(), data.size()); + ofs.write((char*)&crcRead, 3); + } }; diff --git a/src/cpp/common/walkthrough.cpp b/src/cpp/common/walkthrough.cpp deleted file mode 100644 index 7f63e6987..000000000 --- a/src/cpp/common/walkthrough.cpp +++ /dev/null @@ -1,435 +0,0 @@ - -// #pragma GCC optimize ("O0") - -#include "algebra.hpp" -#include "common.hpp" -#include "trace.hpp" -#include "ppp.hpp" - -#include -#include - - -string noteONE = R"raw( -This is the basic form of the outputStates function's output, describing the times, keys, values, variances, and adjustments of everything in the filter. -Note that the filter is not empty upon initialisation, it contains a ONE element that is always present with state value 1, and used for internal calculations. -Get used to ignoring this element. - )raw"; - -string noteInit = R"raw( -When the stateTransition function is called, any kfKeys that were defined and used in measurements are automatically added as states. -They contain the values as defined in the InitialState that may have been used. - )raw"; - -string noteAddMeas = R"raw( -Here the code creates several KFMeasEntrys and adds them to a list - )raw"; - -string noteMeas = R"raw( -When the combineKFMeasList function is called, finalised matrices of the measurements are generated, ready for filtering -They can be viewed with the outputMeasurements function, which displays the measurements keys down the left, states across the top, OMCs on the right, and non zero entries of the design matrix between. -(Its a lot more obvious whats going on when there are more observations and states, and this output is generally unnecessary) - )raw"; - -string noteResiduals = R"raw( -When the filterKalman function is called, standard ekf processes are used to update the states and covariances of the filter. -During the process, the residuals and statistics may be output, showing the fit achieved to the measurements. -Here we see a significant reduction in pre->post fit residuals, indicating the adjustments have improved the fit to these measurements. - )raw"; - - -string noteIntro1 = R"raw( -This walkthrough demonstrates the basics of Ginan's kalman filter. -Its target is an object having its position measured in 1D, by measurements over several epochs. -It's expected that the comments in the code, and the descriptions in the outputs below will give a good basis for starting to work with Ginan's kalman filter. - )raw"; - -string notePostFilter1 = R"raw( -Here we see that the "Walk" REC_POS state value after the filter has moved substantially toward the true value (10), and its variance has decreased from its previous value. - )raw"; - -string noteProcNoise = R"raw( -In the second epoch, after some time has elapsed, the stateTransition function has added process noise according to the value specified by InitialState. -We see a large increase in the variance. - )raw"; - -string noteReject = R"raw( -During this epoch, one of the measurements encounters an outlier, with some large glitch in the observed term. -During processing of the filter the consistency of measurements' and states' variances with each other is confirmed, and outliers detected. -In the case that this consistency check fails, rejection callbacks may be defined, which may trigger appropriate actions. -Here we see that the measurement with the glitch has its measurement variance deweighted by a factor of 10000 such that it doesnt corrupt the filter states. -In the first filter residuals block, both states have poor postfit residuals, but upon iteration when the second measurement is deweighted, the remaining first measurement achieves a good fit. - )raw"; - -string noteIntro2 = R"raw( -Beginning of walkthrough section 2 - -This walkthrough demonstrates the basics of using rate terms in Ginan's kalman filter (velocities, accelerations) - -Its target is an object having its position measured in 1D, by measurements over several epochs. -It's expected that the comments in the code, and the descriptions in the outputs below will give a good basis for starting to work with Ginan's kalman filter. - )raw"; - -string noteRemove = R"raw( -The states from section 1 remain in the filter. -At this point the code removes all states, but this wont be seen until the next stateTransition call - )raw"; - -string noteMeas2 = R"raw( -Here we see the design matrix created by the code - there are 6 states that have been automatically added to the filter by 3 measurements. -1 - This measurement measures the state with num = 1 directly. There is no rate associated with this state -2 - This measurement measures the position state with num = 2 directly. There is a velocity term associated with this rate, but it cant be measured directly with a position observation. -3 - This measurement measures the position state with num = 3 directly. There are velocity and acceleration terms associated with this rate, but they also cant be measured directly. - -Thus the only entries in the design matrix are those corresponding to the position states. - )raw"; - -string noteFirstMeas = R"raw( -Here we see that after one measurement the variance of the position state has reduced substantially, and the position state value quite close to the actual x value. -The estimate for the velocity and acceleration remain unchanged and with large variances, since there is not enough data for them to update.. yet. - )raw"; - -string noteCorrelations = R"raw( -We also see that the velocity terms have began to become corellated with their corresponding position terms. - )raw"; - - -string noteSecondMeas = R"raw( -After a second measurement the velocity terms begin to adjust to the measurements. -This takes place purely due to corellations with the position terms, caused by the propagation of the state transition matrix. - )raw"; - -string noteThirdMeas = R"raw( -After a third measurement the acceleration term also begins to adjust to the measurements. - )raw"; - -string note1Second = R"raw( -Over the next 1 second's worth of measurements, the velocities and acceleration terms have converged toward the correct values, -initially overshooting before oscillating around the correct value (a = -9.8) -The postfit residuals for most measurements have reduced significantly, despite the very poor initilisation conditions. -(The relatively good prefit residual for the num=1 measurement (the one with no velocity or acceleration integration) -is only due to the lack of motion at this point in time. The correct value is relatively close to the previous value, see below) - )raw"; - -string note2Seconds = R"raw( -Over the next second's worth of measurements, the velocities and acceleration terms have further converged toward the correct values -Now however the position state with no velocity term has relatively poor residuals, due to the fast unmodelled kinematics, as compared with the state that is estimating and modelling a linear acceleration - )raw"; - - -void printWait( - string str) -{ - std::cout << "\n\n\n\n\n" << str; - std::cout << "(continue)\n"; - - getline(std::cin, str); - std::cout << "\n\n\n\n\n"; -} - -/** Ginan kalman filter basics - * The basic structure for kalman filtering in ginan is the KFState struct, it is required for all filtering. - * The KFKey struct is used to define a state element within the filter. These are used rather than row/column indexing to eliminate manual bookkeeping - * State elements are generally initialised using an InitialState object, which defines the initial value, sigma, and process noise to be applied between epochs. - * Measurements are defined individually as KFMeasEntry objects and added to a list, before being combined to create a KFMeas object that contains the aggregated measurement list in matrix form. - * The measurement's design matrix is set by defining the sensitivity of the measurement to state elements, and its innovation (OMC) and noise are set with helper functions. - * State elements are automatically added to the filter when a stateTransition occurs after referencing a KFKey with one of the helper functions. - */ -KFState walkthrough1() -{ - //unrequired preparation - double actualPosition = 10; - double measVar = 0.21; - std::random_device randoDev; - std::mt19937 randoGen(randoDev()); - std::normal_distribution rando(0, SQRT(measVar)); - - //prepare the filter - KFState kfState; - kfState.output_residuals = true; //turn on nice outputs for pre/postfit residuals when filtering - kfState.measRejectCallbacks.push_back(deweightMeas); //turn on a rejection callback - kfState.prefitOpts. max_iterations = 4; - kfState.postfitOpts.max_iterations = 4; - - //Prepare the initial state for whenever this element is eventaully initialised - InitialState posInit; - posInit.x = 0.0123; //initial estimate of the state - posInit.P = 900; //initial variance of the state - posInit.Q = 6; //process noise per second - - GTime time; - - printWait(noteIntro1); - - - kfState.outputStates(std::cout, " - BEFORE ANYTHING HAPPENS"); - printWait(noteONE); - - for (int epoch = 1; epoch <= 2; epoch++) - { - std::cout << "\n" << "Begin Epoch " << epoch << "\n"; - time += 60; - - KFMeasEntryList kfMeasEntryList; - - KFKey posKey; - posKey.type = KF::REC_POS; - posKey.str = "Walk"; - - if (epoch == 1) printWait(noteAddMeas); - { - //get the computed (usually just current) state - double stateVal = posInit.x; //inital computed state is whatever we are going to initialise it as - kfState.getKFValue(posKey, stateVal); //override that with whatever is in the kalman filter, if there is something there, that is. - - double computed = stateVal; - double observed = actualPosition + rando(randoDev); //the measurement is the actual position +- some error noise - - double omc = observed - computed; - - KFMeasEntry measEntry(&kfState); - measEntry.addDsgnEntry(posKey, 1, posInit); //this measEntry is a direct measurement of the state with key posKey -> sensitivity is 1, create and initialise the state with posInit if required. - measEntry.setInnov(omc); - measEntry.setNoise(measVar); - - //this is optional, but makes the output easier to read. Usually this would be passed in through KFMeasEntry's constructor - if (1) - { - KFKey obsKey; - obsKey.str = "Meas"; - obsKey.num = 1337; - measEntry.obsKey = obsKey; - } - - kfMeasEntryList.push_back(measEntry); - } - - { - //get the computed (usually just current) state - double stateVal = posInit.x; //inital computed state is whatever we are going to initialise it as - kfState.getKFValue(posKey, stateVal); //override that with whatever is in the kalman filter, if there is something there, that is. - - double computed = stateVal / 2.54; //this measurement is in inches, this time the 'computation' is needed before use. - double observed = actualPosition / 2.54; //the measurement is the actual position if it were measured in inches. - - if (epoch == 2) observed += 14; - - double omc = observed - computed; - - KFMeasEntry measEntry(&kfState); - measEntry.addDsgnEntry(posKey, 1 / 2.54, posInit); //this measEntry is a scaled measurement of the state with key posKey -> sensitivity of measuremet to change in state is 1/2.54, create and initialise the state with posInit if required. - measEntry.setInnov(omc); //error between the current state value, and what we think it should be (observed minus computed) - measEntry.setNoise(measVar); //how much variance we expect in the measurement - our rulers are not perfect - - //this is optional, but makes the output easier to read. Usually this would be passed in through KFMeasEntry's constructor - if (1) - { - KFKey obsKey; - obsKey.str = "Inch"; - measEntry.obsKey = obsKey; - } - - kfMeasEntryList.push_back(measEntry); - } - - - kfState.stateTransition(std::cout, time); //the state transition to t = time initialises newly created states, and adds process noise to existing ones as per their initialisation - - kfState.outputStates(std::cout, " - POST STATE TRANSITION"); - if (epoch == 1) printWait(noteInit); - if (epoch == 2) printWait(noteProcNoise); - - KFMeas kfMeas(kfState, kfMeasEntryList, time); //filterKalman requires a consolidated KFMeas input, rather than a list of individial KFMeasEntrys, create it here - - if (1) - { - if (epoch == 1) - kfState.outputMeasurements(std::cout, kfMeas); - if (epoch == 1) printWait(noteMeas); - } - - - kfState.filterKalman(std::cout, kfMeas, "", true); //perform the kalman filter operation (the true indicates this is an extended kalman filter using precalculated OMCs, this should generally be true in ginan) - if (epoch == 1) printWait(noteResiduals); - if (epoch == 2) printWait(noteReject); - - - kfState.outputStates(std::cout, " - POST FILTER"); - if (epoch == 1) printWait(notePostFilter1); - } - - printWait("End of walkthrough section 1\n\t"); - - return kfState; -} - - -void walkthrough2( - KFState& kfState) -{ - //unrequired preparation - double measVar = 1; - std::random_device randoDev; - std::mt19937 randoGen(randoDev()); - std::normal_distribution rando(0, SQRT(measVar)); - double v0 = 10; - double x0 = 30; - double a = -9.8; - double dt = 0.02; - GTime time = kfState.time + 1000; - - - - InitialState posInit; //initial values for pos, vel, and acc. - posInit.P = 10000; //large P values indicate large uncertainty in initial position - //initial x value defaults to 0 if undefined, substantially different from actual initial values defined above - - InitialState velInit; - velInit.P = 10000; - - InitialState accInit; - accInit.P = 10000; - - printWait(noteIntro2); - - KFKey posKey; //keys for identifying the states added to the filter - posKey.type = KF::POS; - posKey.str = "Obj"; - - KFKey velKey; - velKey.type = KF::VEL; - velKey.str = "Obj"; - - KFKey accKey; - accKey.type = KF::ACC; - accKey.str = "Obj"; - - - kfState.outputStates(std::cout, " - BEFORE ANYTHING HAPPENS"); - - printWait(noteRemove); - - //idiomatic procedure for iterating over things in the filter - for (auto& [key, index] : kfState.kfIndexMap) - { - //dont remove ONE, we need that! - if (key.type == KF::ONE) - { - continue; - } - - //remove all other states - kfState.removeState(key); - } - - - for (double t = 0; t <= 2; t += dt) - { - double actualPosition = x0 //compute positions and velocities of a simulated ball in gravity - + v0 * t - + a * t * t / 2; - - double actualVelocity = v0 - + a * t; - - std::cout << "\n" << "\n\n\n\nEpoch " - << "\n t = " << t - << "\n x = " << actualPosition - << "\n v = " << actualVelocity << "\n"; - time += dt; //generally we wouldnt be iterating over t, dt like this, we would be given a time from some external observation source and we'd just use it - - KFMeasEntryList kfMeasEntryList; - - kfState.stateTransition(std::cout, time); //since we're using rates we need to compute the current value of the states since they have changed over the period since the stateTransition at the end of this loop was last called, (at t-dt) - //call stateTransition to do this computation - - - - - if (t == 0) - kfState.outputStates(std::cout, " - POST FIRST STATE TRANSITION"); - if (t == 0) printWait(noteAddMeas + "\nAlso note that the current true value of the x,v,t at each epoch will be shown as above\n\t"); - - double observed = actualPosition; //the measurement is the actual position - - //create three types of measurements for the same object, with more or less kinematics involved - for (int i = 1; i <= 3; i++) - { - posKey.num = i; - velKey.num = i; - accKey.num = i; - - if (i == 1) { posInit.Q = SQR(10); } - else if (i == 2) { posInit.Q = 0; velInit.Q = SQR(10); } //The Q value (process noise) should be 0 for elements that have rates applied - eg, for num==2 a vel term is the derivative of pos, so posInit.Q = 0 - else if (i == 3) { posInit.Q = 0; velInit.Q = 0; } //only the highest derivative should get process noise. - - //get the computed (usually just current) state - double stateVal = posInit.x; //inital computed state is whatever we are going to initialise it as - kfState.getKFValue(posKey, stateVal); //override that with whatever is in the kalman filter, if there is something there, that is. - - double computed = stateVal; - - double omc = observed - computed; - - KFKey obsKey; //give the measEntry a KFKey just so the residuals/measurements outputs are easier to read (optional) - obsKey.str = "MEAS"; - obsKey.num = i; - - KFMeasEntry measEntry(&kfState, obsKey); - measEntry.addDsgnEntry(posKey, 1, posInit); //this measEntry is a direct measurement of the state with key posKey -> sensitivity is 1, create and initialise the state with posInit if required. - measEntry.setInnov(omc); - measEntry.setNoise(measVar); - - if (i >= 2) - kfState.setKFTransRate(posKey, velKey, 1, velInit); //here we define that velKey is the rate derivative of posKey, with scaling 1, and some initial velocity state - - if (i == 3) - kfState.setAccelerator(posKey, velKey, accKey, 1, accInit); //here we define that accKey is an accelerator of posKey, with velKey being an intermediate derivative between that and this. - - - kfMeasEntryList.push_back(measEntry); - } - - - kfState.stateTransition(std::cout, time); //since we called stateTransition with t = time above, this will only initialise new states - no time has passed to change state values or process noises - - if (t < dt*3) - kfState.outputStates(std::cout, " - POST SECOND STATE TRANSITION"); - - - KFMeas kfMeas(kfState, kfMeasEntryList, time); - - - if (1) - { - if (t == 0) - kfState.outputMeasurements(std::cout, kfMeas); - if (t == 0) printWait(noteMeas2); - - - if (t == dt) - kfState.outputCorrelations(std::cout); - if (t == dt) printWait(noteCorrelations); - } - - - kfState.filterKalman(std::cout, kfMeas, "", true); - - - kfState.outputStates(std::cout, " - POST FILTER"); - if (t == 0) printWait(noteFirstMeas); - if (t == dt) printWait(noteSecondMeas); - if (t == dt*2) printWait(noteThirdMeas); - if (t > 1) printWait(note1Second); - } - - printWait(note2Seconds); - printWait("End of walkthrough section 2"); -} - -void walkthrough() -{ - KFState kfState = walkthrough1(); - walkthrough2(kfState); - exit(1); -} diff --git a/src/cpp/configurator/htmlFooterTemplate.html b/src/cpp/configurator/htmlFooterTemplate.hpp similarity index 96% rename from src/cpp/configurator/htmlFooterTemplate.html rename to src/cpp/configurator/htmlFooterTemplate.hpp index 5df4c0f31..f2bc80680 100644 --- a/src/cpp/configurator/htmlFooterTemplate.html +++ b/src/cpp/configurator/htmlFooterTemplate.hpp @@ -1,15 +1,12 @@ -R"HTMLTEMPLATE( - +#pragma once +// clang-format off +static const char* htmlFooterTemplate = R"HTMLTEMPLATE(

    -
    -
    - - - -)HTMLTEMPLATE" +)HTMLTEMPLATE"; +// clang-format on \ No newline at end of file diff --git a/src/cpp/configurator/htmlHeaderTemplate.html b/src/cpp/configurator/htmlHeaderTemplate.hpp similarity index 94% rename from src/cpp/configurator/htmlHeaderTemplate.html rename to src/cpp/configurator/htmlHeaderTemplate.hpp index 3e303f104..fe909a6e5 100644 --- a/src/cpp/configurator/htmlHeaderTemplate.html +++ b/src/cpp/configurator/htmlHeaderTemplate.hpp @@ -1,41 +1,35 @@ -R"HTMLTEMPLATE( - +#pragma once +// clang-format off +static const char* htmlHeaderTemplate = 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. @@ -124,6 +107,5 @@

    Ginan YAML Inspector

    - - -)HTMLTEMPLATE" +)HTMLTEMPLATE"; +// clang-format on \ No newline at end of file diff --git a/src/cpp/inertial/posProp.cpp b/src/cpp/inertial/posProp.cpp index 4e3941b5f..72a8a3d14 100644 --- a/src/cpp/inertial/posProp.cpp +++ b/src/cpp/inertial/posProp.cpp @@ -1,441 +1,506 @@ - - // #pragma GCC optimize ("O0") +#include "inertial/posProp.hpp" #include #include #include +#include "common/eigenIncluder.hpp" +#include "common/ubxDecoder.hpp" +#include "orbprop/acceleration.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/planets.hpp" using std::deque; using std::map; -#include "eigenIncluder.hpp" -#include "acceleration.hpp" -#include "coordinates.hpp" -#include "ubxDecoder.hpp" -#include "posProp.hpp" -#include "planets.hpp" - void propLinear( - double dt, - string& id, - GTime time, - Vector3d& r, - Vector3d& v, - Vector4d& q, - const Vector3d& gyroScale, - const Vector3d& acclScale, - const Vector3d& gyroBias, - const Vector3d& acclBias) + double dt, + string& id, + GTime time, + Vector3d& r, + Vector3d& v, + Vector4d& q, + const Vector3d& gyroScale, + const Vector3d& acclScale, + const Vector3d& gyroBias, + const Vector3d& acclBias +) { - Quaterniond Q; - Q.w() = q(0); - Q.x() = q(1); - Q.y() = q(2); - Q.z() = q(3); + Quaterniond Q; + Q.w() = q(0); + Q.x() = q(1); + Q.y() = q(2); + Q.z() = q(3); - Vector3d acclBody = Vector3d::Zero(); - { - auto it = UbxDecoder::acclDataMaps[id].lower_bound(time); + Vector3d acclBody = Vector3d::Zero(); + { + auto it = UbxDecoder::acclDataMaps[id].lower_bound(time); - auto& [foundTime, accl] = *it; + auto& [foundTime, accl] = *it; - acclBody = ((accl - acclBias).array() * acclScale.array()).matrix(); + acclBody = ((accl - acclBias).array() * acclScale.array()).matrix(); -// Vector3d other = quat * accl; + // Vector3d other = quat * accl; -// double temp = other.y(); -// other.y() = other.z(); -// other.z() = temp; + // double temp = other.y(); + // other.y() = other.z(); + // other.z() = temp; -// Vector3d acclEcef = (accl - inertialInit.acclBias)[2] * rRec.normalized(); //todo aaron, rotate to ecef + // Vector3d acclEcef = (accl - inertialInit.acclBias)[2] * rRec.normalized(); + // //todo aaron, rotate to ecef + } - } + Vector3d accCF = accelCentralForce(r, GM_values[E_ThirdBody::EARTH]); + Vector3d accAccl = Q * acclBody; - Vector3d accCF = accelCentralForce(r, GM_values[E_ThirdBody::EARTH]); - Vector3d accAccl = Q * acclBody; + Vector3d a = accAccl + accCF; - Vector3d a = accAccl + accCF; + // std::cout << "\n" << " " << accCF .transpose(); + // std::cout << "\n" << " " << accAccl .transpose(); + // std::cout << "\n" << "Using " << a.transpose(); -// std::cout << "\n" << " " << accCF .transpose(); -// std::cout << "\n" << " " << accAccl .transpose(); -// std::cout << "\n" << "Using " << a.transpose(); + Vector3d gyroBody; + { + auto it = UbxDecoder::gyroDataMaps[id].lower_bound(time); - Vector3d gyroBody; - { - auto it = UbxDecoder::gyroDataMaps[id].lower_bound(time); + auto& [foundTime, gyro] = *it; - auto& [foundTime, gyro] = *it; + gyroBody = ((gyro - gyroBias).array() * gyroScale.array()).matrix() * PI / 180; + // std::cout << "\n" << " " << gyro .transpose(); + // std::cout << "\n" << " " << gyroBias + // .transpose(); + } - gyroBody = ((gyro - gyroBias).array() * gyroScale.array()).matrix() * PI / 180; -// std::cout << "\n" << " " << gyro .transpose(); -// std::cout << "\n" << " " << gyroBias .transpose(); - } + // std::cout << "\n" << "Guyo " << gyroBody.transpose(); + Quaterniond qBody(Eigen::AngleAxis(gyroBody.norm() * dt, gyroBody.normalized())); -// std::cout << "\n" << "Guyo " << gyroBody.transpose(); - Quaterniond qBody(Eigen::AngleAxis(gyroBody.norm() * dt, gyroBody.normalized())); + Vector3d rPlus = r + v * dt; + Vector3d vPlus = v + a * dt; + ; - Vector3d rPlus = r + v * dt; - Vector3d vPlus = v + a * dt;; + Quaterniond qPlus = Q * qBody; // todo aaron, check ordering of this - Quaterniond qPlus = Q * qBody; //todo aaron, check ordering of this + r = rPlus; + v = vPlus; - r = rPlus; - v = vPlus; - - q(0) = qPlus.w(); - q(1) = qPlus.x(); - q(2) = qPlus.y(); - q(3) = qPlus.z(); + q(0) = qPlus.w(); + q(1) = qPlus.x(); + q(2) = qPlus.y(); + q(3) = qPlus.z(); } - void InertialIntegrator::operator()( - const Inertials& inertialInits, - Inertials& derivatives, - const double timeOffset) + const Inertials& inertialInits, + Inertials& derivatives, + const double timeOffset +) { - GTime time = timeInit + timeOffset; - - for (int i = 0; i < inertialInits.size(); i++) - { - auto& inertialInit = inertialInits [i]; - auto& derivative = derivatives [i]; - - if (inertialInit.exclude) - { - continue; - } - - string id = inertialInit.str; - - int numParams = inertialInit.numParams; - - Vector3d acc = Vector3d::Zero(); - Vector4d dQuat = Vector4d::Zero(); - Matrix3d dAdPos = Matrix3d::Zero(); - Matrix3d dAdVel = Matrix3d::Zero(); - MatrixXd dAdQuat = MatrixXd::Zero(3, 4); - MatrixXd dAdParam = MatrixXd::Zero(3, numParams); - - Vector3d rRec = inertialInit.pos; - Vector3d vRec = inertialInit.vel; - Vector4d quat = inertialInit.quat.normalized(); - - const double offset = 1e-6; - const double dt = 0.1; - - Vector3d r0 = inertialInit.pos; - Vector3d v0 = inertialInit.vel; - Vector4d q0 = inertialInit.quat; - Vector3d r_; - Vector3d v_; - Vector4d q_; - MatrixXd A = MatrixXd::Zero(inertialInit.subState.x.rows(), inertialInit.subState.x.rows()); - - //get propagated values with no offsets - { - Vector3d r = inertialInit.pos; - Vector3d v = inertialInit.vel; - Vector4d q = inertialInit.quat; - - Vector3d gs = inertialInit.gyroScale; - Vector3d as = inertialInit.acclScale; - Vector3d gb = inertialInit.gyroBias; - Vector3d ab = inertialInit.acclBias; - - propLinear(dt, id, time, r, v, q, gs, as, gb, ab); - - r_ = r; - v_ = v; - q_ = q; - - derivative.pos = (r - r0) / dt; - derivative.vel = (v - v0) / dt; - derivative.quat = (q - q0) / dt; - } - - //get propagated values with offsets to determine sensitivtiy - for (auto& [key, index] : inertialInit.subState.kfIndexMap) - { - Vector3d r = inertialInit.pos; - Vector3d v = inertialInit.vel; - Vector4d q = inertialInit.quat; - - Vector3d gs = inertialInit.gyroScale; - Vector3d as = inertialInit.acclScale; - Vector3d gb = inertialInit.gyroBias; - Vector3d ab = inertialInit.acclBias; - - if (key.type == KF::REC_POS) { r (key.num) += offset; } - if (key.type == KF::REC_POS_RATE) { v (key.num) += offset; } - if (key.type == KF::ORIENTATION) { q (key.num) += offset; } - if (key.type == KF::GYRO_SCALE) { gs (key.num) += offset; } - if (key.type == KF::ACCL_SCALE) { as (key.num) += offset; } - if (key.type == KF::GYRO_BIAS) { gb (key.num) += offset; } - if (key.type == KF::ACCL_BIAS) { ab (key.num) += offset; } - - Vector3d r__ = r; - Vector3d v__ = v; - Vector4d q__ = q; - - propLinear(dt, id, time, r, v, q, gs, as, gb, ab); - - Vector3d deltaR = (r - r__) - (r_ - r0); A.col(index).segment(0, 3) = deltaR / dt / offset; - Vector3d deltaV = (v - v__) - (v_ - v0); A.col(index).segment(3, 3) = deltaV / dt / offset; - Vector4d deltaQ = (q - q__) - (q_ - q0); A.col(index).segment(6, 4) = deltaQ / dt / offset; - } - -// std::cout << "\n" << "A" << "\n" << A << "\n"; - - derivative.posVelQuatSTM = A * inertialInit.posVelQuatSTM; - -// derivative.posVelQuatSTM.bottomRightCorner(3, numParams) += dAdParam; //this is basically assuming params are unchanged and identity matrix? - } + GTime time = timeInit + timeOffset; + + for (int i = 0; i < inertialInits.size(); i++) + { + auto& inertialInit = inertialInits[i]; + auto& derivative = derivatives[i]; + + if (inertialInit.exclude) + { + continue; + } + + string id = inertialInit.str; + + int numParams = inertialInit.numParams; + + Vector3d acc = Vector3d::Zero(); + Vector4d dQuat = Vector4d::Zero(); + Matrix3d dAdPos = Matrix3d::Zero(); + Matrix3d dAdVel = Matrix3d::Zero(); + MatrixXd dAdQuat = MatrixXd::Zero(3, 4); + MatrixXd dAdParam = MatrixXd::Zero(3, numParams); + + Vector3d rRec = inertialInit.pos; + Vector3d vRec = inertialInit.vel; + Vector4d quat = inertialInit.quat.normalized(); + + const double offset = 1e-6; + const double dt = 0.1; + + Vector3d r0 = inertialInit.pos; + Vector3d v0 = inertialInit.vel; + Vector4d q0 = inertialInit.quat; + Vector3d r_; + Vector3d v_; + Vector4d q_; + MatrixXd A = MatrixXd::Zero(inertialInit.subState.x.rows(), inertialInit.subState.x.rows()); + + // get propagated values with no offsets + { + Vector3d r = inertialInit.pos; + Vector3d v = inertialInit.vel; + Vector4d q = inertialInit.quat; + + Vector3d gs = inertialInit.gyroScale; + Vector3d as = inertialInit.acclScale; + Vector3d gb = inertialInit.gyroBias; + Vector3d ab = inertialInit.acclBias; + + propLinear(dt, id, time, r, v, q, gs, as, gb, ab); + + r_ = r; + v_ = v; + q_ = q; + + derivative.pos = (r - r0) / dt; + derivative.vel = (v - v0) / dt; + derivative.quat = (q - q0) / dt; + } + + // get propagated values with offsets to determine sensitivtiy + for (auto& [key, index] : inertialInit.subState.kfIndexMap) + { + Vector3d r = inertialInit.pos; + Vector3d v = inertialInit.vel; + Vector4d q = inertialInit.quat; + + Vector3d gs = inertialInit.gyroScale; + Vector3d as = inertialInit.acclScale; + Vector3d gb = inertialInit.gyroBias; + Vector3d ab = inertialInit.acclBias; + + if (key.type == KF::REC_POS) + { + r(key.num) += offset; + } + if (key.type == KF::REC_POS_RATE) + { + v(key.num) += offset; + } + if (key.type == KF::ORIENTATION) + { + q(key.num) += offset; + } + if (key.type == KF::GYRO_SCALE) + { + gs(key.num) += offset; + } + if (key.type == KF::ACCL_SCALE) + { + as(key.num) += offset; + } + if (key.type == KF::GYRO_BIAS) + { + gb(key.num) += offset; + } + if (key.type == KF::ACCL_BIAS) + { + ab(key.num) += offset; + } + + Vector3d r__ = r; + Vector3d v__ = v; + Vector4d q__ = q; + + propLinear(dt, id, time, r, v, q, gs, as, gb, ab); + + Vector3d deltaR = (r - r__) - (r_ - r0); + A.col(index).segment(0, 3) = deltaR / dt / offset; + Vector3d deltaV = (v - v__) - (v_ - v0); + A.col(index).segment(3, 3) = deltaV / dt / offset; + Vector4d deltaQ = (q - q__) - (q_ - q0); + A.col(index).segment(6, 4) = deltaQ / dt / offset; + } + + // std::cout << "\n" << "A" << "\n" << A << "\n"; + + derivative.posVelQuatSTM = A * inertialInit.posVelQuatSTM; + + // derivative.posVelQuatSTM.bottomRightCorner(3, numParams) += dAdParam; + // //this is basically assuming params are unchanged and identity matrix? + } }; - void integrateInertials( - InertialIntegrator& inertialPropagator, - Inertials& inertials, - double integrationPeriod, - double dtRequested) + InertialIntegrator& inertialPropagator, + Inertials& inertials, + double integrationPeriod, + double dtRequested +) { - if ( inertials.empty() - ||integrationPeriod == 0) - { - return; - } - - double dt = dtRequested; - int steps = round(integrationPeriod / dt); - double remainder = fmod (integrationPeriod, dtRequested); - - if (steps == 0) - { - steps = 1; - } - - if (remainder != 0) - { - double newDt = integrationPeriod / steps; - - BOOST_LOG_TRIVIAL(warning) << "Warning: Time step adjusted from " << dt << " to " << newDt; - - dt = newDt; - } - - for (int i = 0; i < steps; i++) - { - double initTime = i * dt; - - Inertials errors; - inertialPropagator.odeIntegrator.do_step(boost::ref(inertialPropagator), inertials, initTime, dt, errors); - - for (auto error : errors) - { - double errorMag = error.pos.norm(); - if (errorMag > 0.001) - { - BOOST_LOG_TRIVIAL(warning) << " Integrator error " << errorMag << " greater than 1mm for " << error.Sat << " " << error.str; - } - } - } + if (inertials.empty() || integrationPeriod == 0) + { + return; + } + + double dt = dtRequested; + int steps = round(integrationPeriod / dt); + double remainder = fmod(integrationPeriod, dtRequested); + + if (steps == 0) + { + steps = 1; + } + + if (remainder != 0) + { + double newDt = integrationPeriod / steps; + + BOOST_LOG_TRIVIAL(warning) << "Time step adjusted from " << dt << " to " << newDt; + + dt = newDt; + } + + for (int i = 0; i < steps; i++) + { + double initTime = i * dt; + + Inertials errors; + inertialPropagator.odeIntegrator + .do_step(boost::ref(inertialPropagator), inertials, initTime, dt, errors); + + for (auto error : errors) + { + double errorMag = error.pos.norm(); + if (errorMag > 0.001) + { + BOOST_LOG_TRIVIAL(warning) + << "Integrator error " << errorMag << " greater than 1mm for " << error.Sat + << " " << error.str; + } + } + } } /** Get the estimated elements for a single satellite's orbit */ -KFState getInertialFromState( - Trace& trace, - string str, - const KFState& kfState) +KFState getInertialFromState(Trace& trace, string str, const KFState& kfState) { - map kfKeyMap; - - int index = 0; - - //find all satellite orbit related states related to this sat/str pair - for (auto& [kfKey, unused] : kfState.kfIndexMap) - { - if ( kfKey.str != str) - { - continue; - } - - if ( kfKey.type == KF::REC_POS - || kfKey.type == KF::REC_POS_RATE - || kfKey.type == KF::ORIENTATION - ||( kfKey.type >= KF::BEGIN_INERTIAL_STATES - &&kfKey.type <= KF::END_INERTIAL_STATES)) - { - kfKeyMap[kfKey] = index; - index++; - } - } - - KFState subState; - kfState.getSubState(kfKeyMap, subState); - - return subState; + map kfKeyMap; + + int index = 0; + + // find all satellite orbit related states related to this sat/str pair + for (auto& [kfKey, unused] : kfState.kfIndexMap) + { + if (kfKey.str != str) + { + continue; + } + + if (kfKey.type == KF::REC_POS || kfKey.type == KF::REC_POS_RATE || + kfKey.type == KF::ORIENTATION || + (kfKey.type >= KF::BEGIN_INERTIAL_STATES && kfKey.type <= KF::END_INERTIAL_STATES)) + { + kfKeyMap[kfKey] = index; + index++; + } + } + + KFState subState; + kfState.getSubState(kfKeyMap, subState); + + return subState; } -Inertials prepareInertials( - Trace& trace, - const KFState& kfState) +Inertials prepareInertials(Trace& trace, const KFState& kfState) { - Inertials inertials; - - for (auto& [kfKey, index] : kfState.kfIndexMap) - { - if ( kfKey.type != KF::ORIENTATION - || kfKey.num != 0) - { - continue; - } - - inertials.emplace_back(InertialState{.str = kfKey.str}); - } - - for (auto& inertial : inertials) - { - auto& subState = inertial.subState; - - subState = getInertialFromState(trace, inertial.str, kfState); - - for (auto& [key, index] : subState.kfIndexMap) - { - if (key.type == KF::REC_POS) { inertial.pos (key.num) = inertial.subState.x[index]; } - if (key.type == KF::REC_POS_RATE) { inertial.vel (key.num) = inertial.subState.x[index]; } - if (key.type == KF::ORIENTATION) { inertial.quat (key.num) = inertial.subState.x[index]; } - if (key.type == KF::GYRO_SCALE) { inertial.gyroScale (key.num) = inertial.subState.x[index]; } - if (key.type == KF::ACCL_SCALE) { inertial.acclScale (key.num) = inertial.subState.x[index]; } - if (key.type == KF::GYRO_BIAS) { inertial.gyroBias (key.num) = inertial.subState.x[index]; } - if (key.type == KF::ACCL_BIAS) { inertial.acclBias (key.num) = inertial.subState.x[index]; } - } - - if (inertial.pos.isZero()) - { - inertial.exclude = true; - continue; - } - - inertial.posVelQuatSTM = MatrixXd::Identity(inertial.subState.kfIndexMap.size(), inertial.subState.kfIndexMap.size()); - } - - return inertials; + Inertials inertials; + + for (auto& [kfKey, index] : kfState.kfIndexMap) + { + if (kfKey.type != KF::ORIENTATION || kfKey.num != 0) + { + continue; + } + + inertials.emplace_back(InertialState{.str = kfKey.str}); + } + + for (auto& inertial : inertials) + { + auto& subState = inertial.subState; + + subState = getInertialFromState(trace, inertial.str, kfState); + + for (auto& [key, index] : subState.kfIndexMap) + { + if (key.type == KF::REC_POS) + { + inertial.pos(key.num) = inertial.subState.x[index]; + } + if (key.type == KF::REC_POS_RATE) + { + inertial.vel(key.num) = inertial.subState.x[index]; + } + if (key.type == KF::ORIENTATION) + { + inertial.quat(key.num) = inertial.subState.x[index]; + } + if (key.type == KF::GYRO_SCALE) + { + inertial.gyroScale(key.num) = inertial.subState.x[index]; + } + if (key.type == KF::ACCL_SCALE) + { + inertial.acclScale(key.num) = inertial.subState.x[index]; + } + if (key.type == KF::GYRO_BIAS) + { + inertial.gyroBias(key.num) = inertial.subState.x[index]; + } + if (key.type == KF::ACCL_BIAS) + { + inertial.acclBias(key.num) = inertial.subState.x[index]; + } + } + + if (inertial.pos.isZero()) + { + inertial.exclude = true; + continue; + } + + inertial.posVelQuatSTM = MatrixXd::Identity( + inertial.subState.kfIndexMap.size(), + inertial.subState.kfIndexMap.size() + ); + } + + return inertials; } /** Apply the prediction using the filter's state transition */ -void applyInertials( - Trace& trace, - Inertials& inertials, - const KFState& kfState, - GTime time, - double tgap) +void applyInertials(Trace& trace, Inertials& inertials, KFState& kfState, GTime time, double tgap) { - for (auto& inertial : inertials) - { - if (inertial.exclude) - { - continue; - } - - auto& subState = inertial.subState; - - Vector10d inertialState; - inertialState << inertial.pos, inertial.vel, inertial.quat; - - //Convert the absolute transition matrix to an identity matrix (already populated elsewhere) and stm-per-time matrix - MatrixXd transition = inertial.posVelQuatSTM - MatrixXd::Identity(inertial.posVelQuatSTM.rows(), inertial.posVelQuatSTM.cols()); - transition /= tgap; - - KFKey keyI; - keyI.str = inertial.str; - - //use the calculated transition matrix for the main filter, and a also a test filter (substate) - for (int i = 0; i < 10; i++) - for (auto& [keyJ, indexJ] : subState.kfIndexMap) - { - if (i >= 0 && i < 3) { keyI.type = KF::REC_POS; keyI.num = i - 0; } - if (i >= 3 && i < 6) { keyI.type = KF::REC_POS_RATE; keyI.num = i - 3; } - if (i >= 6 && i < 10) { keyI.type = KF::ORIENTATION; keyI.num = i - 6; } - - double transferIJ = transition(i, indexJ); - - kfState .setKFTransRate(keyI, keyJ, transferIJ); - subState.setKFTransRate(keyI, keyJ, transferIJ); - } - - //run a test run of the transition on the substate to see how much we miss by - subState.stateTransition(trace, time); - - //calculate the state error between the linearly transitioned filter state, and the orbit propagated states. - Vector10d deltaState = inertialState - - subState.x.head(10); - - //We should ensure that the state transition is smooth, without bulk adjustments or 'setting' the state - //add per-time adjusting state transitions to implement an addition of the shortfall delta calculated above - Vector10d deltaStatePerSec = deltaState / tgap; - - KFKey kfKey; - kfKey.str = inertial.str; - - for (int i = 0; i < 6; i++) - { - if (i >= 0 && i < 3) { kfKey.type = KF::REC_POS; kfKey.num = i - 0; } - if (i >= 3 && i < 6) { kfKey.type = KF::REC_POS_RATE; kfKey.num = i - 3; } - if (i >= 6 && i < 10) { kfKey.type = KF::ORIENTATION; kfKey.num = i - 6; } - - kfState.setKFTransRate(kfKey, KFState::oneKey, deltaStatePerSec(i)); - } - } + for (auto& inertial : inertials) + { + if (inertial.exclude) + { + continue; + } + + auto& subState = inertial.subState; + + Vector10d inertialState; + inertialState << inertial.pos, inertial.vel, inertial.quat; + + // Convert the absolute transition matrix to an identity matrix (already populated + // elsewhere) and stm-per-time matrix + MatrixXd transition = + inertial.posVelQuatSTM - + MatrixXd::Identity(inertial.posVelQuatSTM.rows(), inertial.posVelQuatSTM.cols()); + transition /= tgap; + + KFKey keyI; + keyI.str = inertial.str; + + // use the calculated transition matrix for the main filter, and a also a test filter + // (substate) + for (int i = 0; i < 10; i++) + for (auto& [keyJ, indexJ] : subState.kfIndexMap) + { + if (i >= 0 && i < 3) + { + keyI.type = KF::REC_POS; + keyI.num = i - 0; + } + if (i >= 3 && i < 6) + { + keyI.type = KF::REC_POS_RATE; + keyI.num = i - 3; + } + if (i >= 6 && i < 10) + { + keyI.type = KF::ORIENTATION; + keyI.num = i - 6; + } + + double transferIJ = transition(i, indexJ); + + kfState.setKFTransRate(keyI, keyJ, transferIJ); + subState.setKFTransRate(keyI, keyJ, transferIJ); + } + + // run a test run of the transition on the substate to see how much we miss by + subState.stateTransition(trace, time); + + // calculate the state error between the linearly transitioned filter state, and the orbit + // propagated states. + Vector10d deltaState = inertialState - subState.x.head(10); + + // We should ensure that the state transition is smooth, without bulk adjustments or + // 'setting' the state add per-time adjusting state transitions to implement an addition of + // the shortfall delta calculated above + Vector10d deltaStatePerSec = deltaState / tgap; + + KFKey kfKey; + kfKey.str = inertial.str; + + for (int i = 0; i < 6; i++) + { + if (i >= 0 && i < 3) + { + kfKey.type = KF::REC_POS; + kfKey.num = i - 0; + } + if (i >= 3 && i < 6) + { + kfKey.type = KF::REC_POS_RATE; + kfKey.num = i - 3; + } + if (i >= 6 && i < 10) + { + kfKey.type = KF::ORIENTATION; + kfKey.num = i - 6; + } + + kfState.setKFTransRate(kfKey, KFState::oneKey, deltaStatePerSec(i)); + } + } } -/** Use models to predict orbital motion and prepare state transition equations to implement those predictions in the filter +/** Use models to predict orbital motion and prepare state transition equations to implement those + * predictions in the filter */ -void predictInertials( - Trace& trace, - const KFState& kfState, - GTime time) +void predictInertials(Trace& trace, KFState& kfState, GTime time) { - double tgap = (time - kfState.time).to_double(); + double tgap = (time - kfState.time).to_double(); - if (tgap == 0) - { - return; - } + if (tgap == 0) + { + return; + } - Inertials inertials = prepareInertials(trace, kfState); + Inertials inertials = prepareInertials(trace, kfState); - if (inertials.empty()) - { - return; - } + if (inertials.empty()) + { + return; + } - BOOST_LOG_TRIVIAL(info) << " ------- PROPAGATING INERTIALS --------" << "\n"; + BOOST_LOG_TRIVIAL(info) << " ------- PROPAGATING INERTIALS --------" << "\n"; - InertialIntegrator integrator; - integrator.timeInit = kfState.time; -// integrator.propagationOptions = acsConfig.inertialPropagation; + InertialIntegrator integrator; + integrator.timeInit = kfState.time; + // integrator.propagationOptions = acsConfig.inertialPropagation; - double timestep = 0.1; - integrateInertials(integrator, inertials, tgap, timestep); + double timestep = 0.1; + integrateInertials(integrator, inertials, tgap, timestep); - applyInertials(trace, inertials, kfState, time, tgap); + applyInertials(trace, inertials, kfState, time, tgap); - for (auto& [id, timeMap] : UbxDecoder::acclDataMaps) - for (auto it = timeMap.begin(); it != timeMap.end(); ) - { - auto& [entryTime, thing] = *it; + for (auto& [id, timeMap] : UbxDecoder::acclDataMaps) + for (auto it = timeMap.begin(); it != timeMap.end();) + { + auto& [entryTime, thing] = *it; - if (entryTime < time - 1.0) - { - it = timeMap.erase(it); - } - else - { - ++it; - } - } + if (entryTime < time - 1.0) + { + it = timeMap.erase(it); + } + else + { + ++it; + } + } }; diff --git a/src/cpp/inertial/posProp.hpp b/src/cpp/inertial/posProp.hpp index 7164bc69b..5c7472faf 100644 --- a/src/cpp/inertial/posProp.hpp +++ b/src/cpp/inertial/posProp.hpp @@ -1,163 +1,146 @@ - #pragma once #include - #include -#include #include +#include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/erp.hpp" +#include "common/gTime.hpp" +#include "common/trace.hpp" -using std::vector; using std::map; - -#include "eigenIncluder.hpp" -#include "acsConfig.hpp" -#include "algebra.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" -#include "erp.hpp" - +using std::vector; using namespace boost::numeric::odeint; struct InertialState { - SatSys Sat; - string str; - - bool exclude = false; - - KFState subState; - -// vector empInput; - - map componentsMap; - - int numParams = 0; - Vector3d pos = Vector3d::Zero(); - Vector3d vel = Vector3d::Zero(); - Vector4d quat = Vector4d::Zero(); - MatrixXd posVelQuatSTM; - - Vector3d gyroBias = Vector3d::Zero(); - Vector3d acclBias = Vector3d::Zero(); - Vector3d gyroScale = Vector3d::Ones(); - Vector3d acclScale = Vector3d::Ones(); - - double posVar = 0; - - InertialState& operator+=(double rhs) - { - pos = (pos .array() + rhs).matrix(); - vel = (vel .array() + rhs).matrix(); - quat = (quat .array() + rhs).matrix(); - posVelQuatSTM = (posVelQuatSTM.array() + rhs).matrix(); - return *this; - } - - InertialState& operator*=(double rhs) - { - pos *= rhs; - vel *= rhs; - quat *= rhs; - posVelQuatSTM *= rhs; - return *this; - } - - InertialState operator+(double rhs) const - { - InertialState newState = *this; - newState += rhs; - return newState; - } - - InertialState operator+(const InertialState& rhs) const - { - InertialState newState = *this; - newState.pos += rhs.pos; - newState.vel += rhs.vel; - newState.quat += rhs.quat; - newState.posVelQuatSTM += rhs.posVelQuatSTM; - return newState; - } - - InertialState operator*(double rhs) const - { - InertialState newState = *this; - newState *= rhs; - return newState; - } + SatSys Sat; + string str; + + bool exclude = false; + + KFState subState; + + // vector empInput; + + map componentsMap; + + int numParams = 0; + Vector3d pos = Vector3d::Zero(); + Vector3d vel = Vector3d::Zero(); + Vector4d quat = Vector4d::Zero(); + MatrixXd posVelQuatSTM; + + Vector3d gyroBias = Vector3d::Zero(); + Vector3d acclBias = Vector3d::Zero(); + Vector3d gyroScale = Vector3d::Ones(); + Vector3d acclScale = Vector3d::Ones(); + + double posVar = 0; + + InertialState& operator+=(double rhs) + { + pos = (pos.array() + rhs).matrix(); + vel = (vel.array() + rhs).matrix(); + quat = (quat.array() + rhs).matrix(); + posVelQuatSTM = (posVelQuatSTM.array() + rhs).matrix(); + return *this; + } + + InertialState& operator*=(double rhs) + { + pos *= rhs; + vel *= rhs; + quat *= rhs; + posVelQuatSTM *= rhs; + return *this; + } + + InertialState operator+(double rhs) const + { + InertialState newState = *this; + newState += rhs; + return newState; + } + + InertialState operator+(const InertialState& rhs) const + { + InertialState newState = *this; + newState.pos += rhs.pos; + newState.vel += rhs.vel; + newState.quat += rhs.quat; + newState.posVelQuatSTM += rhs.posVelQuatSTM; + return newState; + } + + InertialState operator*(double rhs) const + { + InertialState newState = *this; + newState *= rhs; + return newState; + } }; typedef vector Inertials; -inline InertialState operator*( - const double lhs, - const InertialState& rhs) +inline InertialState operator*(const double lhs, const InertialState& rhs) { - return rhs * lhs; + return rhs * lhs; }; -inline Inertials operator+( - const Inertials& lhs, - const Inertials& rhs) +inline Inertials operator+(const Inertials& lhs, const Inertials& rhs) { - Inertials newState = lhs; - for (int i = 0; i < lhs.size(); i++) - { - newState[i] = lhs[i] + rhs[i]; - } - return newState; + Inertials newState = lhs; + for (int i = 0; i < lhs.size(); i++) + { + newState[i] = lhs[i] + rhs[i]; + } + return newState; } -inline Inertials operator*( - const Inertials& lhs, - const double rhs) +inline Inertials operator*(const Inertials& lhs, const double rhs) { - Inertials newState = lhs; - for (int i = 0; i < lhs.size(); i++) - { - newState[i] *= rhs; - } - return newState; + Inertials newState = lhs; + for (int i = 0; i < lhs.size(); i++) + { + newState[i] *= rhs; + } + return newState; } -inline Inertials operator*( - const double rhs, - const Inertials& lhs) +inline Inertials operator*(const double rhs, const Inertials& lhs) { - Inertials newState = lhs; - for (int i = 0; i < lhs.size(); i++) - { - newState[i] *= rhs; - } - return newState; + Inertials newState = lhs; + for (int i = 0; i < lhs.size(); i++) + { + newState[i] *= rhs; + } + return newState; } - struct InertialIntegrator { - GTime timeInit; -// InertialPropagation propagationOptions; - - runge_kutta_fehlberg78 odeIntegrator; - - void operator()( - const Inertials& inertialInit, - Inertials& inertialUpdate, - const double mjdSec); - - void computeDeltaInertial( - const InertialState& inertialInit, - Vector3d& acc, - Vector4d& dQuat, - Matrix3d& dAdPos, - Matrix3d& dAdVel, - Matrix4d& dAdQuat, - MatrixXd& dAdParam); + GTime timeInit; + // InertialPropagation propagationOptions; + + runge_kutta_fehlberg78 + odeIntegrator; + + void operator()(const Inertials& inertialInit, Inertials& inertialUpdate, const double mjdSec); + + void computeDeltaInertial( + const InertialState& inertialInit, + Vector3d& acc, + Vector4d& dQuat, + Matrix3d& dAdPos, + Matrix3d& dAdVel, + Matrix4d& dAdQuat, + MatrixXd& dAdParam + ); }; -void predictInertials( - Trace& trace, - const KFState& kfState, - GTime time); +void predictInertials(Trace& trace, KFState& kfState, GTime time); diff --git a/src/cpp/iono/geomagField.cpp b/src/cpp/iono/geomagField.cpp index e0e82bcc1..e624eb775 100644 --- a/src/cpp/iono/geomagField.cpp +++ b/src/cpp/iono/geomagField.cpp @@ -1,279 +1,296 @@ - /** References -* 1. P.Alken, E.Thébault, C.D.Beggan, H.Amit, J.Aubert, J.Baerenzung, T.N.Bondar, W.J.Brown, S.Califf, A.Chambodut & A.Chulliat, International geomagnetic reference field: the thirteenth generation. Earth, Planets and Space, 2021. -* 2. E.Thébault, C.C.Finlay, C.D.Beggan, P.Alken, J.Aubert, O.Barrois, F.Bertrand, T.Bondar, A.Boness, L.Brocco & E.Canet. International geomagnetic reference field: the 12th generation. Earth, Planets and Space, 2015. -* 3. D.E.Winch, D.J.Ivers, J.P.R.Turner & R.J.Stening. Geomagnetism and Schmidt quasi-normalization. Geophysical Journal International, 2005. -* 4. Wikipedia, Spherical harmonics. https://en.wikipedia.org/wiki/Spherical_harmonics#Orthogonality_and_normalization -*/ - - -#include + * 1. P.Alken, E.Thébault, C.D.Beggan, H.Amit, J.Aubert, J.Baerenzung, T.N.Bondar, W.J.Brown, + * S.Califf, A.Chambodut & A.Chulliat, International geomagnetic reference field: the thirteenth + * generation. Earth, Planets and Space, 2021. + * 2. E.Thébault, C.C.Finlay, C.D.Beggan, P.Alken, J.Aubert, O.Barrois, F.Bertrand, T.Bondar, + * A.Boness, L.Brocco & E.Canet. International geomagnetic reference field: the 12th generation. + * Earth, Planets and Space, 2015. + * 3. D.E.Winch, D.J.Ivers, J.P.R.Turner & R.J.Stening. Geomagnetism and Schmidt + * quasi-normalization. Geophysical Journal International, 2005. + * 4. Wikipedia, Spherical harmonics. + * https://en.wikipedia.org/wiki/Spherical_harmonics#Orthogonality_and_normalization + */ + +#include "iono/geomagField.hpp" +#include +#include +#include #include +#include #include +#include "common/common.hpp" +#include "common/constants.hpp" +#include "orbprop/acceleration.hpp" +#include "orbprop/coordinates.hpp" using std::vector; -#include -#include -#include - -#include "acceleration.hpp" -#include "coordinates.hpp" -#include "geomagField.hpp" -#include "constants.hpp" -#include "common.hpp" - -map igrfMFMap; ///< igrfMFMap[year] -GeomagSecularVariation igrfSV; +map igrfMFMap; ///< igrfMFMap[year] +GeomagSecularVariation igrfSV; /** Read the IGRF file -* Only support the latest generations of IGRF file from IGRF-11 -*/ -void readIGRF( - string filename) ///< IGRF file to read + * Only support the latest generations of IGRF file from IGRF-11 + */ +void readIGRF(string filename) ///< IGRF file to read { - if (filename.empty()) - { - return; - } - - std::ifstream infile(filename); - if (!infile) - { - return; - } - - string line; - vector yearList; - while (std::getline(infile, line)) - { - if (line[0] == '#') - { - continue; - } - - vector split; - boost::trim(line); - boost::algorithm::split(split, line, boost::algorithm::is_space(), boost::token_compress_on); - - if (split[0] == "g/h") - { - int i; - for (i = 3; i < split.size()-1; i++) - { - int year = stoi(split[i]); - int maxDegree = 13; - bool isDefinitive = false; - - if (year <= 1995) - maxDegree = 10; - - GeomagMainField& igrfMF = igrfMFMap[year]; - igrfMF.year = year; - igrfMF.maxDegree = maxDegree; - - yearList.push_back(year); - } - - // SV - igrfSV.year = stoi(split[i].substr(0,4)); - igrfSV.yearEnd = stoi(split[i].substr(5,2)) + 2000; - igrfSV.maxDegree = 8; - } - else if ( split[0] == "g" - && split.size() > 3) - { - int n = stoi(split[1]); - int m = stoi(split[2]); - - int i; - for (i = 3; i < split.size()-1; i++) - { - int year = yearList[i-3]; - auto& gnm = igrfMFMap[year].gnm; - gnm(n, m) = stod(split[i]); - } - - // SV - igrfSV.gnm(n, m) = stod(split[i]); - } - else if ( split[0] == "h" - && split.size() > 3) - { - int n = stoi(split[1]); - int m = stoi(split[2]); - - int i; - for (i = 3; i < split.size()-1; i++) - { - int year = yearList[i-3]; - auto& hnm = igrfMFMap[year].hnm; - hnm(n, m) = stod(split[i]); - } - - // SV - igrfSV.hnm(n, m) = stod(split[i]); - } - } + if (filename.empty()) + { + return; + } + + std::ifstream infile(filename); + if (!infile) + { + return; + } + + string line; + vector yearList; + while (std::getline(infile, line)) + { + if (line[0] == '#') + { + continue; + } + + vector split; + boost::trim(line); + boost::algorithm::split( + split, + line, + boost::algorithm::is_space(), + boost::token_compress_on + ); + + if (split[0] == "g/h") + { + int i; + for (i = 3; i < split.size() - 1; i++) + { + int year = stoi(split[i]); + int maxDegree = 13; + bool isDefinitive = false; + + if (year <= 1995) + maxDegree = 10; + + GeomagMainField& igrfMF = igrfMFMap[year]; + igrfMF.year = year; + igrfMF.maxDegree = maxDegree; + + yearList.push_back(year); + } + + // SV + igrfSV.year = stoi(split[i].substr(0, 4)); + igrfSV.yearEnd = stoi(split[i].substr(5, 2)) + 2000; + igrfSV.maxDegree = 8; + } + else if (split[0] == "g" && split.size() > 3) + { + int n = stoi(split[1]); + int m = stoi(split[2]); + + int i; + for (i = 3; i < split.size() - 1; i++) + { + int year = yearList[i - 3]; + auto& gnm = igrfMFMap[year].gnm; + gnm(n, m) = stod(split[i]); + } + + // SV + igrfSV.gnm(n, m) = stod(split[i]); + } + else if (split[0] == "h" && split.size() > 3) + { + int n = stoi(split[1]); + int m = stoi(split[2]); + + int i; + for (i = 3; i < split.size() - 1; i++) + { + int year = yearList[i - 3]; + auto& hnm = igrfMFMap[year].hnm; + hnm(n, m) = stod(split[i]); + } + + // SV + igrfSV.hnm(n, m) = stod(split[i]); + } + } } /** Convert time to decimal year -* For example: 2019-07-18 00:00:00 -> 2019.54247 -*/ -double decimalYear( - GTime time) ///< time + * For example: 2019-07-18 00:00:00 -> 2019.54247 + */ +double decimalYear(GTime time) ///< time { - GEpoch ep = time; - GEpoch ep0 = {ep.year, 1, 1, 0, 0, 0}; - GEpoch ep1 = {ep.year+1, 1, 1, 0, 0, 0}; + GEpoch ep = time; + GEpoch ep0 = {ep.year, 1, 1, 0, 0, 0}; + GEpoch ep1 = {ep.year + 1, 1, 1, 0, 0, 0}; - double secOfYear = ((GTime)ep - (GTime)ep0).to_double(); - double secInYear = ((GTime)ep1 - (GTime)ep0).to_double(); + double secOfYear = ((GTime)ep - (GTime)ep0).to_double(); + double secInYear = ((GTime)ep1 - (GTime)ep0).to_double(); - return ep.year + secOfYear/secInYear; + return ep.year + secOfYear / secInYear; } /** Calculate the SH coefficients at given epoch based on a piecewise linear model -* For years starting in 1900 and ending in 2020, the change rates should be calculated using the two nearest data points -* For the final 5 years of model validity (2020 to 2025 for IGRF-13), the Secular Variation coefficients are explicitly provided -* See ref [1] -*/ + * For years starting in 1900 and ending in 2020, the change rates should be calculated using the + * two nearest data points For the final 5 years of model validity (2020 to 2025 for IGRF-13), the + * Secular Variation coefficients are explicitly provided See ref [1] + */ bool getSHCoef( - GTime time, ///< time of IGRF coefficients to calculate - GeomagMainField& igrfMF) ///< map of the IGRF coefficients calculated + GTime time, ///< time of IGRF coefficients to calculate + GeomagMainField& igrfMF ///< map of the IGRF coefficients calculated +) { - if (igrfMFMap.empty()) - return false; - - double year = decimalYear(time); - - if ( year >= igrfSV.yearEnd - ||year < 1900) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Input epoch " << time.to_string() << " out of range covered by the IGRF file: 1900-" << igrfSV.yearEnd; - return false; - } - - int year0 = ((int)year / 5) * 5; - int year1 = year0 + 5; - double dt = year - year0; - - MatrixXd gnmDot = MatrixXd::Zero(14, 14); - MatrixXd hnmDot = MatrixXd::Zero(14, 14); - if (year >= igrfSV.year) - { - gnmDot = igrfSV.gnm; - hnmDot = igrfSV.hnm; - } - else - { - gnmDot = (igrfMFMap[year1].gnm - igrfMFMap[year0].gnm) / 5.0; - hnmDot = (igrfMFMap[year1].hnm - igrfMFMap[year0].hnm) / 5.0; - } - - igrfMF.year = year; - igrfMF.maxDegree = igrfMFMap[year0].maxDegree; - igrfMF.gnm = igrfMFMap[year0].gnm + gnmDot * dt; - igrfMF.hnm = igrfMFMap[year0].hnm + hnmDot * dt; - - return true; + if (igrfMFMap.empty()) + return false; + + double year = decimalYear(time); + + if (year >= igrfSV.yearEnd || year < 1900) + { + BOOST_LOG_TRIVIAL(warning) + << "Input epoch " << time.to_string() << " out of range covered by the IGRF file: 1900-" + << igrfSV.yearEnd; + return false; + } + + int year0 = ((int)year / 5) * 5; + int year1 = year0 + 5; + double dt = year - year0; + + MatrixXd gnmDot = MatrixXd::Zero(14, 14); + MatrixXd hnmDot = MatrixXd::Zero(14, 14); + if (year >= igrfSV.year) + { + gnmDot = igrfSV.gnm; + hnmDot = igrfSV.hnm; + } + else + { + gnmDot = (igrfMFMap[year1].gnm - igrfMFMap[year0].gnm) / 5.0; + hnmDot = (igrfMFMap[year1].hnm - igrfMFMap[year0].hnm) / 5.0; + } + + igrfMF.year = year; + igrfMF.maxDegree = igrfMFMap[year0].maxDegree; + igrfMF.gnm = igrfMFMap[year0].gnm + gnmDot * dt; + igrfMF.hnm = igrfMFMap[year0].hnm + hnmDot * dt; + + return true; } -/** Compute the geomagnetic field components in geocentric local reference system at given time and position -* See ref [1] [2] -* Note that the normalisation of associated Legendre functions for Geomagnetic field is different from that for Geogravitational field, i.e. -* Pnm_gra_norm(x) = sqrt((2-dm0)*(2*n+1) * (n-m)/(n+m)) * Pnm(x) -* Pnm_mag_norm(x) = sqrt((2-dm0)* (n-m)/(n+m)) * Pnm(x) -* where dm0 is the Kronecker delta which is equal to 1 for m==0 and 0 for m!=0 -* Thus we have Pnm_gra_norm(x) = sqrt(2*n+1) * Pnm_mag_norm(x) -* See ref [3] [4] -*/ +/** Compute the geomagnetic field components in geocentric local reference system at given time and + * position See ref [1] [2] Note that the normalisation of associated Legendre functions for + * Geomagnetic field is different from that for Geogravitational field, i.e. Pnm_gra_norm(x) = + * sqrt((2-dm0)*(2*n+1) * (n-m)/(n+m)) * Pnm(x) Pnm_mag_norm(x) = sqrt((2-dm0)* + * (n-m)/(n+m)) * Pnm(x) where dm0 is the Kronecker delta which is equal to 1 for m==0 and 0 for + * m!=0 Thus we have Pnm_gra_norm(x) = sqrt(2*n+1) * Pnm_mag_norm(x) See ref [3] [4] + */ VectorEnu getGeomagIntensity( - GTime time, ///< time of geomagnetic field to model - const VectorPos& pos) ///< position in geocentric spherical coordinates where geomagnetic field to model + GTime time, ///< time of geomagnetic field to model + const VectorPos& + pos ///< position in geocentric spherical coordinates where geomagnetic field to model +) { - VectorEnu intensityEnu; - - GeomagMainField igrfMF; - - bool pass = getSHCoef(time, igrfMF); - if (pass == false) - { - return intensityEnu; - } - - double lat = pos.lat(); - double lon = pos.lon(); - double radius = pos[2]; - - const double maxLat = PI/2 - 1E-6; - - // resolve singularity - if (pos.lat() > +maxLat) { lat = +maxLat; } - else if (pos.lat() < -maxLat) { lat = -maxLat; } - - double cosTheta = sin(lat); // theta is co-latitude - double sinTheta = cos(lat); - double cosPhi = cos(lon); - double sinPhi = sin(lon); - - VectorXd cosmPhi(igrfMF.maxDegree+1); - VectorXd sinmPhi(igrfMF.maxDegree+1); - cosmPhi(0) = 1; - sinmPhi(0) = 0; - cosmPhi(1) = cosPhi; - sinmPhi(1) = sinPhi; - for (int m = 2; m <= igrfMF.maxDegree; m++) - { - cosmPhi(m) = cosmPhi(m-1) * cosPhi - sinmPhi(m-1) * sinPhi; - sinmPhi(m) = sinmPhi(m-1) * cosPhi + cosmPhi(m-1) * sinPhi; - } - - Legendre leg(igrfMF.maxDegree); - leg.calculate(cosTheta); // Pnm_gra_norm(cos(theta)) - - double Btheta = 0; - double Bphi = 0; - double Br = 0; - - double radiusRatio = SQR(RE_IGRF / radius); - for (int n = 1; n <= igrfMF.maxDegree; n++) - { - radiusRatio *= RE_IGRF / radius; // (a/r)**n+2 - - double dVtheta = 0; - double dVphi = 0; - double dVr = 0; - - double factor = 1 / sqrt(2 * n + 1); // factor converting Pnm_gra_norm(x) to Pnm_mag_norm(x) - for (int m = 0; m <= n; m++) - { - dVtheta += leg.dPnm(n, m) * (igrfMF.gnm(n, m) * cosmPhi(m) + igrfMF.hnm(n, m) * sinmPhi(m)); // lat - dVphi += m * leg. Pnm(n, m) * (igrfMF.hnm(n, m) * cosmPhi(m) - igrfMF.gnm(n, m) * sinmPhi(m)); // lon - dVr += leg. Pnm(n, m) * (igrfMF.gnm(n, m) * cosmPhi(m) + igrfMF.hnm(n, m) * sinmPhi(m)); // r - } - - Btheta += factor * radiusRatio * dVtheta; - Bphi += factor * radiusRatio * dVphi; - Br += -(n+1) * factor * radiusRatio * dVr; - } - - intensityEnu.e() = -Bphi / sinTheta; - intensityEnu.n() = +Btheta; - intensityEnu.u() = -Br; - - return intensityEnu; + VectorEnu intensityEnu; + + GeomagMainField igrfMF; + + bool pass = getSHCoef(time, igrfMF); + if (pass == false) + { + return intensityEnu; + } + + double lat = pos.lat(); + double lon = pos.lon(); + double radius = pos[2]; + + const double maxLat = PI / 2 - 1E-6; + + // resolve singularity + if (pos.lat() > +maxLat) + { + lat = +maxLat; + } + else if (pos.lat() < -maxLat) + { + lat = -maxLat; + } + + double cosTheta = sin(lat); // theta is co-latitude + double sinTheta = cos(lat); + double cosPhi = cos(lon); + double sinPhi = sin(lon); + + VectorXd cosmPhi(igrfMF.maxDegree + 1); + VectorXd sinmPhi(igrfMF.maxDegree + 1); + cosmPhi(0) = 1; + sinmPhi(0) = 0; + cosmPhi(1) = cosPhi; + sinmPhi(1) = sinPhi; + for (int m = 2; m <= igrfMF.maxDegree; m++) + { + cosmPhi(m) = cosmPhi(m - 1) * cosPhi - sinmPhi(m - 1) * sinPhi; + sinmPhi(m) = sinmPhi(m - 1) * cosPhi + cosmPhi(m - 1) * sinPhi; + } + + Legendre leg(igrfMF.maxDegree); + leg.calculate(cosTheta); // Pnm_gra_norm(cos(theta)) + + double Btheta = 0; + double Bphi = 0; + double Br = 0; + + double radiusRatio = SQR(RE_IGRF / radius); + for (int n = 1; n <= igrfMF.maxDegree; n++) + { + radiusRatio *= RE_IGRF / radius; // (a/r)**n+2 + + double dVtheta = 0; + double dVphi = 0; + double dVr = 0; + + double factor = + 1 / sqrt(2 * n + 1); // factor converting Pnm_gra_norm(x) to Pnm_mag_norm(x) + for (int m = 0; m <= n; m++) + { + dVtheta += leg.dPnm(n, m) * + (igrfMF.gnm(n, m) * cosmPhi(m) + igrfMF.hnm(n, m) * sinmPhi(m)); // lat + dVphi += m * leg.Pnm(n, m) * + (igrfMF.hnm(n, m) * cosmPhi(m) - igrfMF.gnm(n, m) * sinmPhi(m)); // lon + dVr += leg.Pnm(n, m) * + (igrfMF.gnm(n, m) * cosmPhi(m) + igrfMF.hnm(n, m) * sinmPhi(m)); // r + } + + Btheta += factor * radiusRatio * dVtheta; + Bphi += factor * radiusRatio * dVphi; + Br += -(n + 1) * factor * radiusRatio * dVr; + } + + intensityEnu.e() = -Bphi / sinTheta; + intensityEnu.n() = +Btheta; + intensityEnu.u() = -Br; + + return intensityEnu; } /** Compute the geomagnetic field components in ECEF reference system at given time and position -*/ + */ VectorEcef getGeomagIntensityEcef( - GTime time, ///< time of geomagnetic field to model - const VectorPos& pos) ///< position in geocentric spherical coordinates where geomagnetic field to model + GTime time, ///< time of geomagnetic field to model + const VectorPos& + pos ///< position in geocentric spherical coordinates where geomagnetic field to model +) { - VectorEnu intensityEnu = getGeomagIntensity(time, pos); + VectorEnu intensityEnu = getGeomagIntensity(time, pos); - VectorEcef intensityEcef = enu2ecef(pos, intensityEnu); + VectorEcef intensityEcef = enu2ecef(pos, intensityEnu); - return intensityEcef; + return intensityEcef; } diff --git a/src/cpp/iono/geomagField.hpp b/src/cpp/iono/geomagField.hpp index a7eae002c..e17a3677c 100644 --- a/src/cpp/iono/geomagField.hpp +++ b/src/cpp/iono/geomagField.hpp @@ -1,53 +1,43 @@ - #pragma once -#include #include +#include +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" -using std::string; using std::map; - -#include "eigenIncluder.hpp" -#include "gTime.hpp" +using std::string; /** Structure for variables related to the International Geomagnetic Reference Field -* 13th Generation International Geomagnetic Reference Field Schmidt semi-normalised spherical harmonic coefficients, degree n=1,13 -* in units nanoTesla for IGRF and definitive DGRF main-field models (degree n=1,8 nanoTesla/year for secular variation (SV)) -*/ + * 13th Generation International Geomagnetic Reference Field Schmidt semi-normalised spherical + * harmonic coefficients, degree n=1,13 in units nanoTesla for IGRF and definitive DGRF main-field + * models (degree n=1,8 nanoTesla/year for secular variation (SV)) + */ struct GeomagMainField { - GeomagMainField() - { + GeomagMainField() { - }; + }; - int year; ///< model epoch - int maxDegree = 13; ///< maximum degree and order - MatrixXd gnm = MatrixXd::Zero(14, 14); ///< SH coefcients gnm - MatrixXd hnm = MatrixXd::Zero(14, 14); ///< SH coefcients hnm + int year; ///< model epoch + int maxDegree = 13; ///< maximum degree and order + MatrixXd gnm = MatrixXd::Zero(14, 14); ///< SH coefcients gnm + MatrixXd hnm = MatrixXd::Zero(14, 14); ///< SH coefcients hnm }; /** Structure for secular variation (SV) inherited from GeomagMainField -*/ + */ struct GeomagSecularVariation : GeomagMainField { - int yearEnd; ///< end of validity period == year+5 + int yearEnd; ///< end of validity period == year+5 }; -void readIGRF( - string filename); +void readIGRF(string filename); -double decimalYear( - GTime time); +double decimalYear(GTime time); -bool getSHCoef( - GTime time, - GeomagMainField& igrfMF); +bool getSHCoef(GTime time, GeomagMainField& igrfMF); -VectorEnu getGeomagIntensity( - GTime time, - const VectorPos& pos); +VectorEnu getGeomagIntensity(GTime time, const VectorPos& pos); -VectorEcef getGeomagIntensityEcef( - GTime time, - const VectorPos& pos); +VectorEcef getGeomagIntensityEcef(GTime time, const VectorPos& pos); diff --git a/src/cpp/iono/ionex.cpp b/src/cpp/iono/ionex.cpp index e4b721f0c..f2c6f09e3 100644 --- a/src/cpp/iono/ionex.cpp +++ b/src/cpp/iono/ionex.cpp @@ -1,481 +1,465 @@ -/**------------------------------------------------------------------------------ - -* references: -* [1] S.Schear, W.Gurtner and J.Feltens, IONEX: The IONosphere Map EXchange -* Format Version 1, February 25, 1998 -* [2] S.Schaer, R.Markus, B.Gerhard and A.S.Timon, Daily Global Ionosphere -* Maps based on GPS Carrier Phase Data Routinely producted by CODE -* Analysis Center, Proceeding of the IGS Analysis Center Workshop, 1996 -*-----------------------------------------------------------------------------*/ -#include - -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "biases.hpp" -#include "common.hpp" - -/* get index - */ -int getindex( - double value, - const double* range) -{ - if (range[2] == 0) return 0; - if (range[1] > 0 && (value < range[0] || range[1] < value)) return -1; - if (range[1] < 0 && (value < range[1] || range[0] < value)) return -1; - - return (int) floor((value - range[0]) / range[2] + 0.5); -} - -/* get number of items - */ -int nitem( - const double* range) -{ - return getindex(range[1], range) + 1; -} - -/* data index (i:lat,j:lon,k:hgt) - */ -int dataindex( - int i, - int j, - int k, - const int* ndata) //todo aaron, convert to maps -{ - if ( i < 0 || ndata[0] <= i - || j < 0 || ndata[1] <= j - || k < 0 || ndata[2] <= k) - { - return -1; - } - - return i + ndata[0] * (j + ndata[1] * k); -} - -/** read ionex dcb aux data - */ -void readionexdcb( - std::ifstream& in, - Navigation* navi) -{ - char buff[1024]; - BiasEntry entry; - bool refObs = false; - - entry.tini.bigTime = 2; - entry.measType = CODE; - entry.source = "ionex"; - - BOOST_LOG_TRIVIAL(debug) - << "readionexdcb:"; - - string line; - while (std::getline(in, line)) - { - char* buff = &line[0]; - - if (strlen(buff) < 60) - continue; - - char* label = buff + 60; - - SatSys Sat; - - string id; - if ( strstr(label, "COMMENT") == label - &&strstr(buff, "Reference observables")) - { - char* ptr = strchr(buff, ':'); - if (ptr != nullptr) - { - string cod1str(ptr + 2, 3); - string cod2str(ptr + 6, 3); - - E_MeasType dummy1; - entry.cod1 = str2code(cod1str, dummy1); - entry.cod2 = str2code(cod2str, dummy1); - - refObs = true; - } - - continue; - } - else if (strstr(label, "PRN / BIAS / RMS") == label) - { - string sat(buff + 3, 3); - Sat = SatSys(sat.c_str()); - entry.Sat = Sat; - entry.name = ""; - id = sat; - - if (!refObs) - { - if (Sat.sys == +E_Sys::GPS) - { - entry.cod1 = E_ObsCode::L1W; - entry.cod2 = E_ObsCode::L2W; - } - else if (Sat.sys == +E_Sys::GLO) - { - entry.cod1 = E_ObsCode::L1P; - entry.cod2 = E_ObsCode::L2P; - } - else - { - BOOST_LOG_TRIVIAL(debug) - << "ionex invalid satellite: " << id; - - continue; - } - } - - if (Sat) - { - entry.bias = str2num(buff, 6, 10) * CLIGHT * 1E-9; - entry.var = SQR(str2num(buff, 16, 10) * CLIGHT * 1E-9); - - BOOST_LOG_TRIVIAL(debug) - << id << entry.bias; - } - else - { - BOOST_LOG_TRIVIAL(debug) - << "ionex invalid satellite: " << id; - - continue; - } - - //fallthrough to after the ifs - } - else if (strstr(label, "STATION / BIAS / RMS") == label) - { - string sys (buff + 3, 1); - string name(buff + 6, 4); - Sat = SatSys(sys.c_str()); - entry.Sat = Sat; - entry.name = name; - id = name; - - if (!refObs) - { - if (Sat.sys == +E_Sys::GPS) - { - entry.cod1 = E_ObsCode::L1W; - entry.cod2 = E_ObsCode::L2W; - } - else if (Sat.sys == +E_Sys::GLO) - { - entry.cod1 = E_ObsCode::L1P; - entry.cod2 = E_ObsCode::L2P; - } - else - { - BOOST_LOG_TRIVIAL(debug) - << "ionex invalid satellite system: " << sys; - - continue; - } - } - - if (Sat) - { - entry.bias = str2num(buff, 26, 10) * CLIGHT * 1E-9; - entry.var = SQR(str2num(buff, 36, 10) * CLIGHT * 1E-9); - - BOOST_LOG_TRIVIAL(debug) - << id << entry.bias; - } - else - { - BOOST_LOG_TRIVIAL(debug) - << "ionex invalid station: " << id; - - continue; - } - - //fallthrough to after the ifs - } - else if (strstr(label, "END OF AUX DATA") == label) - break; - else - continue; - - entry.name = id; - - if ( Sat.sys == +E_Sys::GLO - &&Sat.prn == 0) - { - // this seems to be a receiver - // for ambiguous GLO receiver bias id (i.e. PRN not specified), duplicate bias entry for each satellite - for (int prn = 1; prn <= NSATGLO; prn++) - { - Sat.prn = prn; - id = entry.name + ":" + Sat.id(); - // entry.Sat = Sat; - pushBiasEntry(id, entry); - } - } - else if ( Sat.sys == +E_Sys::GLO - &&Sat.prn != 0) - { - // this can be a receiver or satellite - id = id + ":" + Sat.id(); - pushBiasEntry(id, entry); - } - else - { - // this can be a receiver or satellite - id = id + ":" + Sat.sysChar(); - pushBiasEntry(id, entry); - } - } -} - -/* read ionex header - */ -double readionexh( - std::ifstream& in, - double* lats, - double* lons, - double* hgts, - double& rb, - double& nexp, - Navigation* navi) -{ - double ver = 0; - - BOOST_LOG_TRIVIAL(debug) - << "readionexh:"; - - string line; - while (std::getline(in, line)) - { - char* buff = &line[0]; - - if (strlen(buff) < 60) - continue; - - char* label = buff + 60; - - if (strstr(label, "IONEX VERSION / TYPE") == label) - { - if (buff[20] == 'I') - ver = str2num(buff, 0, 8); - - BOOST_LOG_TRIVIAL(debug) - << " ver= " << ver; - } - else if (strstr(label, "BASE RADIUS") == label) - { - rb = str2num(buff, 0, 8); - - BOOST_LOG_TRIVIAL(debug) - << " rad= " << rb; - } - else if (strstr(label, "HGT1 / HGT2 / DHGT") == label) - { - hgts[0] = str2num(buff, 2, 6); - hgts[1] = str2num(buff, 8, 6); - hgts[2] = str2num(buff, 14, 6); - - BOOST_LOG_TRIVIAL(debug) - << " heights= " << hgts[0] << " " << hgts[1] << " " << hgts[2]; - } - else if (strstr(label, "LAT1 / LAT2 / DLAT") == label) - { - lats[0] = str2num(buff, 2, 6); - lats[1] = str2num(buff, 8, 6); - lats[2] = str2num(buff, 14, 6); - - BOOST_LOG_TRIVIAL(debug) - << " lats= " << lats[0] << " " << lats[1] << " " << lats[2]; - } - else if (strstr(label, "LON1 / LON2 / DLON") == label) - { - lons[0] = str2num(buff, 2, 6); - lons[1] = str2num(buff, 8, 6); - lons[2] = str2num(buff, 14, 6); - - BOOST_LOG_TRIVIAL(debug) - << " lons= " << lons[0] << " " << lons[1] << " " << lons[2]; - } - else if (strstr(label, "EXPONENT") == label) - { - nexp = str2num(buff, 0, 6); - } - else if ( strstr(label, "START OF AUX DATA") == label - &&strstr(buff, "DIFFERENTIAL CODE BIASES")) - { - readionexdcb(in, navi); - } - else if (strstr(label, "END OF HEADER") == label) - { - return ver; - } - } - - return 0; -} - -/* read ionex body -----------------------------------------------------------*/ -int readionexb( - std::ifstream& in, - const double* lats, - const double* lons, - const double* hgts, - double rb, - double nexp, - Navigation* navi) -{ - GTime time = {}; - int type = 0; - - // if (fdebug) - // fprintf(fdebug, "readionexb:\n"); - - string line; - while (std::getline(in, line)) - { - char* buff = &line[0]; - char* label = buff + 60; - - if (strlen(buff) < 60) - continue; - - if (strstr(label, "START OF TEC MAP") == label) - { - type = 1; - time.bigTime = 0; - - } - else if (strstr(label, "END OF TEC MAP") == label) - { - // if (fdebug) - // fprintf(fdebug, "%5ld data and %5ld rms entries for %s\n", navi->tecList[time.time].data.size(), navi->tecList[time.time].rms.size(), time.to_string(0).c_str()); - - type = 0; - } - else if (strstr(label, "START OF RMS MAP") == label) - { - type = 2; - time.bigTime = 0; - } - else if (strstr(label, "END OF RMS MAP") == label) - { - // if (fdebug) - // fprintf(fdebug, "%5ld data and %5ld rms entries for %s\n", navi->tecList[time.time].data.size(), navi->tecList[time.time].rms.size(), time.to_string().c_str()); - - type = 0; - } - else if (strstr(label, "EPOCH OF CURRENT MAP") == label) - { - if (str2time(buff, 0, 36, time)) - { - // fprintf(fdebug, "ionex epoch invalid: %-36.36s\n", buff); - continue; - } - - auto& epochTec = navi->tecMap[time]; - - if (type == 1) - { - epochTec.time = time; - epochTec.ndata[0] = nitem(lats); - epochTec.ndata[1] = nitem(lons); - epochTec.ndata[2] = nitem(hgts); - epochTec.rb = rb; - - for (int i = 0; i < 3; i++) - { - epochTec.lats[i] = lats[i]; - epochTec.lons[i] = lons[i]; - epochTec.hgts[i] = hgts[i]; - } - - epochTec.tecPointVector.resize(epochTec.ndata[0] * epochTec.ndata[1] * epochTec.ndata[2]); - - std::fill(epochTec.tecPointVector.begin(), epochTec.tecPointVector.end(), TECPoint{}); - } - } - else if ( strstr(label, "LAT/LON1/LON2/DLON/H") == label - && time.bigTime - && type) - { - double lon[3]; - double lat = str2num(buff, 2, 6); - lon[0] = str2num(buff, 8, 6); - lon[1] = str2num(buff, 14, 6); - lon[2] = str2num(buff, 20, 6); - double hgt = str2num(buff, 26, 6); - - int i = getindex(lat, lats); - int k = getindex(hgt, hgts); - int n = nitem(lon); - - auto& epochTec = navi->tecMap[time]; - - for (int m = 0; m < n; m++) - { - if ( m % 16 == 0 - && !std::getline(in, line)) - break; - - buff = &line[0]; - - int j = getindex(lon[0] + lon[2] * m, lons); - - int index = dataindex(i, j, k, epochTec.ndata); - if (index < 0) - continue; - - double x = str2num(buff, m % 16 * 5, 5); - if (x == 9999) - continue; - - if (type == 1) epochTec.tecPointVector[index].data = x * pow(10, nexp); - if (type == 2) epochTec.tecPointVector[index].rms = x * pow(10, nexp); - } - } - } - - return 1; -} - -/** read ionex tec grid file - */ -void readTec( - string file, - Navigation* navi) -{ - BOOST_LOG_TRIVIAL(debug) - << __FUNCTION__ << " : file=" << file; - - std::ifstream inputStream(file); - if (!inputStream) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: ionex file open error " << file; - - return; - } - - /* read ionex header */ - double nexp = -1; - double rb = 0; - double lats[3] = {}; - double lons[3] = {}; - double hgts[3] = {}; - double version = readionexh(inputStream, lats, lons, hgts, rb, nexp, navi); - if (version <= 0) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: ionex file format error " << file; - - return; - } - - /* read ionex body */ - readionexb(inputStream, lats, lons, hgts, rb, nexp, navi); -} +/**------------------------------------------------------------------------------ + * references: + * [1] S.Schear, W.Gurtner and J.Feltens, IONEX: The IONosphere Map EXchange + * Format Version 1, February 25, 1998 + * [2] S.Schaer, R.Markus, B.Gerhard and A.S.Timon, Daily Global Ionosphere + * Maps based on GPS Carrier Phase Data Routinely producted by CODE + * Analysis Center, Proceeding of the IGS Analysis Center Workshop, 1996 + *-----------------------------------------------------------------------------*/ + +#include +#include "common/acsConfig.hpp" +#include "common/biases.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/navigation.hpp" + +/* get index + */ +int getindex(double value, const double* range) +{ + if (range[2] == 0) + return 0; + if (range[1] > 0 && (value < range[0] || range[1] < value)) + return -1; + if (range[1] < 0 && (value < range[1] || range[0] < value)) + return -1; + + return (int)floor((value - range[0]) / range[2] + 0.5); +} + +/* get number of items + */ +int nitem(const double* range) +{ + return getindex(range[1], range) + 1; +} + +/* data index (i:lat,j:lon,k:hgt) + */ +int dataindex(int i, int j, int k, + const int* ndata) // todo aaron, convert to maps +{ + if (i < 0 || ndata[0] <= i || j < 0 || ndata[1] <= j || k < 0 || ndata[2] <= k) + { + return -1; + } + + return i + ndata[0] * (j + ndata[1] * k); +} + +/** read ionex dcb aux data + */ +void readionexdcb(std::ifstream& in, Navigation* navi) +{ + char buff[1024]; + BiasEntry entry; + bool refObs = false; + + entry.tini.bigTime = 2; + entry.measType = CODE; + entry.source = "ionex"; + + BOOST_LOG_TRIVIAL(debug) << "readionexdcb:"; + + string line; + while (std::getline(in, line)) + { + char* buff = &line[0]; + + if (strlen(buff) < 60) + continue; + + char* label = buff + 60; + + SatSys Sat; + + string id; + if (strstr(label, "COMMENT") == label && strstr(buff, "Reference observables")) + { + char* ptr = strchr(buff, ':'); + if (ptr != nullptr) + { + string cod1str(ptr + 2, 3); + string cod2str(ptr + 6, 3); + + E_MeasType dummy1; + entry.cod1 = str2code(cod1str, dummy1); + entry.cod2 = str2code(cod2str, dummy1); + + refObs = true; + } + + continue; + } + else if (strstr(label, "PRN / BIAS / RMS") == label) + { + string sat(buff + 3, 3); + Sat = SatSys(sat.c_str()); + entry.Sat = Sat; + entry.name = ""; + id = sat; + + if (!refObs) + { + if (Sat.sys == E_Sys::GPS) + { + entry.cod1 = E_ObsCode::L1W; + entry.cod2 = E_ObsCode::L2W; + } + else if (Sat.sys == E_Sys::GLO) + { + entry.cod1 = E_ObsCode::L1P; + entry.cod2 = E_ObsCode::L2P; + } + else + { + BOOST_LOG_TRIVIAL(debug) << "ionex invalid satellite: " << id; + + continue; + } + } + + if (Sat) + { + entry.bias = str2num(buff, 6, 10) * CLIGHT * 1E-9; + entry.var = SQR(str2num(buff, 16, 10) * CLIGHT * 1E-9); + + BOOST_LOG_TRIVIAL(debug) << id << entry.bias; + } + else + { + BOOST_LOG_TRIVIAL(debug) << "ionex invalid satellite: " << id; + + continue; + } + + // fallthrough to after the ifs + } + else if (strstr(label, "STATION / BIAS / RMS") == label) + { + string sys(buff + 3, 1); + string name(buff + 6, 4); + Sat = SatSys(sys.c_str()); + entry.Sat = Sat; + entry.name = name; + id = name; + + if (!refObs) + { + if (Sat.sys == E_Sys::GPS) + { + entry.cod1 = E_ObsCode::L1W; + entry.cod2 = E_ObsCode::L2W; + } + else if (Sat.sys == E_Sys::GLO) + { + entry.cod1 = E_ObsCode::L1P; + entry.cod2 = E_ObsCode::L2P; + } + else + { + BOOST_LOG_TRIVIAL(debug) << "ionex invalid satellite system: " << sys; + + continue; + } + } + + if (Sat) + { + entry.bias = str2num(buff, 26, 10) * CLIGHT * 1E-9; + entry.var = SQR(str2num(buff, 36, 10) * CLIGHT * 1E-9); + + BOOST_LOG_TRIVIAL(debug) << id << entry.bias; + } + else + { + BOOST_LOG_TRIVIAL(debug) << "ionex invalid station: " << id; + + continue; + } + + // fallthrough to after the ifs + } + else if (strstr(label, "END OF AUX DATA") == label) + break; + else + continue; + + entry.name = id; + + updateRefTime(entry); + + if (Sat.sys == E_Sys::GLO && Sat.prn == 0) + { + // this seems to be a receiver + // for ambiguous GLO receiver bias id (i.e. PRN not specified), duplicate bias entry for + // each satellite + for (int prn = 1; prn <= NSATGLO; prn++) + { + Sat.prn = prn; + id = entry.name + ":" + Sat.id(); + // entry.Sat = Sat; + pushBiasEntry(id, entry); + } + } + else if (Sat.sys == E_Sys::GLO && Sat.prn != 0) + { + // this can be a receiver or satellite + id = id + ":" + Sat.id(); + pushBiasEntry(id, entry); + } + else + { + // this can be a receiver or satellite + id = id + ":" + Sat.sysChar(); + pushBiasEntry(id, entry); + } + } +} + +/* read ionex header + */ +double readionexh( + std::ifstream& in, + double* lats, + double* lons, + double* hgts, + double& rb, + double& nexp, + Navigation* navi +) +{ + double ver = 0; + + BOOST_LOG_TRIVIAL(debug) << "readionexh:"; + + string line; + while (std::getline(in, line)) + { + char* buff = &line[0]; + + if (strlen(buff) < 60) + continue; + + char* label = buff + 60; + + if (strstr(label, "IONEX VERSION / TYPE") == label) + { + if (buff[20] == 'I') + ver = str2num(buff, 0, 8); + + BOOST_LOG_TRIVIAL(debug) << " ver= " << ver; + } + else if (strstr(label, "BASE RADIUS") == label) + { + rb = str2num(buff, 0, 8); + + BOOST_LOG_TRIVIAL(debug) << " rad= " << rb; + } + else if (strstr(label, "HGT1 / HGT2 / DHGT") == label) + { + hgts[0] = str2num(buff, 2, 6); + hgts[1] = str2num(buff, 8, 6); + hgts[2] = str2num(buff, 14, 6); + + BOOST_LOG_TRIVIAL(debug) << " heights= " << hgts[0] << " " << hgts[1] << " " << hgts[2]; + } + else if (strstr(label, "LAT1 / LAT2 / DLAT") == label) + { + lats[0] = str2num(buff, 2, 6); + lats[1] = str2num(buff, 8, 6); + lats[2] = str2num(buff, 14, 6); + + BOOST_LOG_TRIVIAL(debug) << " lats= " << lats[0] << " " << lats[1] << " " << lats[2]; + } + else if (strstr(label, "LON1 / LON2 / DLON") == label) + { + lons[0] = str2num(buff, 2, 6); + lons[1] = str2num(buff, 8, 6); + lons[2] = str2num(buff, 14, 6); + + BOOST_LOG_TRIVIAL(debug) << " lons= " << lons[0] << " " << lons[1] << " " << lons[2]; + } + else if (strstr(label, "EXPONENT") == label) + { + nexp = str2num(buff, 0, 6); + } + else if (strstr(label, "START OF AUX DATA") == label && + strstr(buff, "DIFFERENTIAL CODE BIASES")) + { + readionexdcb(in, navi); + } + else if (strstr(label, "END OF HEADER") == label) + { + return ver; + } + } + + return 0; +} + +/* read ionex body -----------------------------------------------------------*/ +int readionexb( + std::ifstream& in, + const double* lats, + const double* lons, + const double* hgts, + double rb, + double nexp, + Navigation* navi +) +{ + GTime time = {}; + int type = 0; + + // if (fdebug) + // fprintf(fdebug, "readionexb:\n"); + + string line; + while (std::getline(in, line)) + { + char* buff = &line[0]; + char* label = buff + 60; + + if (strlen(buff) < 60) + continue; + + if (strstr(label, "START OF TEC MAP") == label) + { + type = 1; + time.bigTime = 0; + } + else if (strstr(label, "END OF TEC MAP") == label) + { + // if (fdebug) + // fprintf(fdebug, "%5ld data and %5ld rms entries for %s\n", + // navi->tecList[time.time].data.size(), navi->tecList[time.time].rms.size(), + // time.to_string(0).c_str()); + + type = 0; + } + else if (strstr(label, "START OF RMS MAP") == label) + { + type = 2; + time.bigTime = 0; + } + else if (strstr(label, "END OF RMS MAP") == label) + { + // if (fdebug) + // fprintf(fdebug, "%5ld data and %5ld rms entries for %s\n", + // navi->tecList[time.time].data.size(), navi->tecList[time.time].rms.size(), + // time.to_string().c_str()); + + type = 0; + } + else if (strstr(label, "EPOCH OF CURRENT MAP") == label) + { + if (str2time(buff, 0, 36, time)) + { + // fprintf(fdebug, "ionex epoch invalid: %-36.36s\n", buff); + continue; + } + + auto& epochTec = navi->tecMap[time]; + + if (type == 1) + { + epochTec.time = time; + epochTec.ndata[0] = nitem(lats); + epochTec.ndata[1] = nitem(lons); + epochTec.ndata[2] = nitem(hgts); + epochTec.rb = rb; + + for (int i = 0; i < 3; i++) + { + epochTec.lats[i] = lats[i]; + epochTec.lons[i] = lons[i]; + epochTec.hgts[i] = hgts[i]; + } + + epochTec.tecPointVector.resize( + epochTec.ndata[0] * epochTec.ndata[1] * epochTec.ndata[2] + ); + + std::fill( + epochTec.tecPointVector.begin(), + epochTec.tecPointVector.end(), + TECPoint{} + ); + } + } + else if (strstr(label, "LAT/LON1/LON2/DLON/H") == label && time.bigTime && type) + { + double lon[3]; + double lat = str2num(buff, 2, 6); + lon[0] = str2num(buff, 8, 6); + lon[1] = str2num(buff, 14, 6); + lon[2] = str2num(buff, 20, 6); + double hgt = str2num(buff, 26, 6); + + int i = getindex(lat, lats); + int k = getindex(hgt, hgts); + int n = nitem(lon); + + auto& epochTec = navi->tecMap[time]; + + for (int m = 0; m < n; m++) + { + if (m % 16 == 0 && !std::getline(in, line)) + break; + + buff = &line[0]; + + int j = getindex(lon[0] + lon[2] * m, lons); + + int index = dataindex(i, j, k, epochTec.ndata); + if (index < 0) + continue; + + double x = str2num(buff, m % 16 * 5, 5); + if (x == 9999) + continue; + + if (type == 1) + epochTec.tecPointVector[index].data = x * pow(10, nexp); + if (type == 2) + epochTec.tecPointVector[index].rms = x * pow(10, nexp); + } + } + } + + return 1; +} + +/** read ionex tec grid file + */ +void readTec(string file, Navigation* navi) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " : file=" << file; + + std::ifstream inputStream(file); + if (!inputStream) + { + BOOST_LOG_TRIVIAL(warning) << "Ionex file open error " << file; + + return; + } + + /* read ionex header */ + double nexp = -1; + double rb = 0; + double lats[3] = {}; + double lons[3] = {}; + double hgts[3] = {}; + double version = readionexh(inputStream, lats, lons, hgts, rb, nexp, navi); + if (version <= 0) + { + BOOST_LOG_TRIVIAL(warning) << "Ionex file format error " << file; + + return; + } + + /* read ionex body */ + readionexb(inputStream, lats, lons, hgts, rb, nexp, navi); +} diff --git a/src/cpp/iono/ionexWrite.cpp b/src/cpp/iono/ionexWrite.cpp index 8d4331a75..c8a3cfe4e 100644 --- a/src/cpp/iono/ionexWrite.cpp +++ b/src/cpp/iono/ionexWrite.cpp @@ -1,24 +1,21 @@ - - // #pragma GCC optimize ("O0") +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/constants.hpp" +#include "common/satStat.hpp" +#include "common/satSys.hpp" +#include "iono/ionoModel.hpp" -#include "ionoModel.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "satStat.hpp" -#include "algebra.hpp" -#include "satSys.hpp" +constexpr int IONEX_NEXP = -1; +constexpr double SINGL_LAY_ERR = 0.3; -#define IONEX_NEXP -1 -#define SINGL_LAY_ERR 0.3 - -static double ionexLatmin = -55; -static double ionexLonmin = 90; -static double ionexLatinc = 5; -static double ionexLoninc = 5; -static int ionexLatres = 15; -static int ionexLonres = 19; +static double ionexLatmin = -55; +static double ionexLonmin = 90; +static double ionexLatinc = 5; +static double ionexLoninc = 5; +static int ionexLatres = 15; +static int ionexLonres = 19; static int lastIonex = -1; @@ -26,218 +23,285 @@ map endLinePos; static int ionexMapIndex = 0; -double ionVtec( - Trace& trace, - GTime time, - VectorPos& ionPP, - int layer, - double& vtecstd, - KFState& kfState) +double +ionVtec(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& vtecstd, KFState& kfState) { - switch (acsConfig.ionModelOpts.model) - { - case E_IonoModel::SPHERICAL_HARMONICS: return ionVtecSphhar(trace, time, ionPP, layer, vtecstd, kfState); - case E_IonoModel::SPHERICAL_CAPS: return ionVtecSphcap(trace, time, ionPP, layer, vtecstd, kfState); - case E_IonoModel::BSPLINE: return ionVtecBsplin(trace, time, ionPP, layer, vtecstd, kfState); - default: return 0; - } + switch (acsConfig.ionModelOpts.model) + { + case E_IonoModel::SPHERICAL_HARMONICS: + return ionVtecSphhar(trace, time, ionPP, layer, vtecstd, kfState); + case E_IonoModel::SPHERICAL_CAPS: + return ionVtecSphcap(trace, time, ionPP, layer, vtecstd, kfState); + case E_IonoModel::BSPLINE: + return ionVtecBsplin(trace, time, ionPP, layer, vtecstd, kfState); + default: + return 0; + } } -void writeIonexHead( - Trace& ionex) +void writeIonexHead(Trace& ionex) { - ionexMapIndex = 1; - - ionexLatinc = acsConfig.ionexGrid.lat_res; - ionexLoninc = acsConfig.ionexGrid.lon_res; - ionexLatres = floor(acsConfig.ionexGrid.lat_width / ionexLatinc) + 1; - ionexLonres = floor(acsConfig.ionexGrid.lon_width / ionexLoninc) + 1; - ionexLatmin = acsConfig.ionexGrid.lat_centre - ionexLatinc * (ionexLatres - 1) / 2; - ionexLonmin = acsConfig.ionexGrid.lon_centre - ionexLoninc * (ionexLonres - 1) / 2; - - if (acsConfig.ionModelOpts.layer_heights.empty()) - { - return; - } - - double hght1 = acsConfig.ionModelOpts.layer_heights.front() / 1000; - double hght2 = acsConfig.ionModelOpts.layer_heights.back() / 1000; - double dhght = (hght2 - hght1) / (acsConfig.ionModelOpts.layer_heights.size() - 1); - - if (acsConfig.ionModelOpts.layer_heights.size() == 1) - dhght = 0; - - auto& recOpts = acsConfig.getRecOpts("global"); - - tracepdeex(0, ionex, "%8.1f%12s%-20s%-20.20sIONEX VERSION / TYPE\n", 1.1, " ", "I", "GNS"); - tracepdeex(0, ionex, "%-20s%-20s%-20.20sPGM / RUN BY / DATE\n", acsConfig.analysis_software.c_str(), acsConfig.analysis_centre.c_str(), timeGet().to_string(0).c_str()); - tracepdeex(0, ionex, " %4s%54sMAPPING FUNCTION\n", "COSZ", ""); - tracepdeex(0, ionex, "%8.1f%52sELEVATION CUTOFF\n", recOpts.elevation_mask_deg, " "); - tracepdeex(0, ionex, "%8.1f%52sBASE RADIUS\n", RE_WGS84 / 1000.0, " "); - tracepdeex(0, ionex, "%6d%54sMAP DIMENSION\n", acsConfig.ionModelOpts.layer_heights.size() > 1 ? 3 : 2, " "); - tracepdeex(0, ionex, " %6.1f%6.1f%6.1f%40sHGT1 / HGT2 / DHGT\n", hght1, hght2, dhght, " "); - tracepdeex(0, ionex, " %6.1f%6.1f%6.1f%40sLAT1 / LAT2 / DLAT\n", ionexLatmin + ionexLatinc * (ionexLatres - 1), ionexLatmin, -ionexLatinc, " "); - tracepdeex(0, ionex, " %6.1f%6.1f%6.1f%40sLON1 / LON2 / DLON\n", ionexLonmin, ionexLonmin + ionexLoninc * (ionexLonres - 1), ionexLoninc, " "); - tracepdeex(0, ionex, "%6d%54sEXPONENT\n", IONEX_NEXP, ""); - tracepdeex(0, ionex, "%-60s%s\n", acsConfig.rinex_comment.c_str(), "COMMENT"); - tracepdeex(0, ionex, "%60sEND OF HEADER\n\n", " "); + ionexMapIndex = 1; + + ionexLatinc = acsConfig.ionexGrid.lat_res; + ionexLoninc = acsConfig.ionexGrid.lon_res; + ionexLatres = floor(acsConfig.ionexGrid.lat_width / ionexLatinc) + 1; + ionexLonres = floor(acsConfig.ionexGrid.lon_width / ionexLoninc) + 1; + ionexLatmin = acsConfig.ionexGrid.lat_centre - ionexLatinc * (ionexLatres - 1) / 2; + ionexLonmin = acsConfig.ionexGrid.lon_centre - ionexLoninc * (ionexLonres - 1) / 2; + + if (acsConfig.ionModelOpts.layer_heights.empty()) + { + return; + } + + double hght1 = acsConfig.ionModelOpts.layer_heights.front() / 1000; + double hght2 = acsConfig.ionModelOpts.layer_heights.back() / 1000; + double dhght = (hght2 - hght1) / (acsConfig.ionModelOpts.layer_heights.size() - 1); + + if (acsConfig.ionModelOpts.layer_heights.size() == 1) + dhght = 0; + + auto& recOpts = acsConfig.getRecOpts("global"); + + tracepdeex(0, ionex, "%8.1f%12s%-20s%-20.20sIONEX VERSION / TYPE\n", 1.1, " ", "I", "GNS"); + tracepdeex( + 0, + ionex, + "%-20s%-20s%-20.20sPGM / RUN BY / DATE\n", + acsConfig.analysis_software.c_str(), + acsConfig.analysis_centre.c_str(), + timeGet().to_string(0).c_str() + ); + tracepdeex(0, ionex, " %4s%54sMAPPING FUNCTION\n", "COSZ", ""); + tracepdeex(0, ionex, "%8.1f%52sELEVATION CUTOFF\n", recOpts.elevation_mask_deg, " "); + tracepdeex(0, ionex, "%8.1f%52sBASE RADIUS\n", RE_WGS84 / 1000.0, " "); + tracepdeex( + 0, + ionex, + "%6d%54sMAP DIMENSION\n", + acsConfig.ionModelOpts.layer_heights.size() > 1 ? 3 : 2, + " " + ); + tracepdeex(0, ionex, " %6.1f%6.1f%6.1f%40sHGT1 / HGT2 / DHGT\n", hght1, hght2, dhght, " "); + tracepdeex( + 0, + ionex, + " %6.1f%6.1f%6.1f%40sLAT1 / LAT2 / DLAT\n", + ionexLatmin + ionexLatinc * (ionexLatres - 1), + ionexLatmin, + -ionexLatinc, + " " + ); + tracepdeex( + 0, + ionex, + " %6.1f%6.1f%6.1f%40sLON1 / LON2 / DLON\n", + ionexLonmin, + ionexLonmin + ionexLoninc * (ionexLonres - 1), + ionexLoninc, + " " + ); + tracepdeex(0, ionex, "%6d%54sEXPONENT\n", IONEX_NEXP, ""); + tracepdeex(0, ionex, "%-60s%s\n", acsConfig.rinex_comment.c_str(), "COMMENT"); + tracepdeex(0, ionex, "%60sEND OF HEADER\n\n", " "); } -void writeIonexEpoch( - Trace& trace, - Trace& ionex, - GTime time, - KFState& kfState) +void writeIonexEpoch(Trace& trace, Trace& ionex, GTime time, KFState& kfState) { - GTow tow = time; - int timeseg = floor(tow / acsConfig.ionexGrid.time_res); - - if ( lastIonex >= 0 - && lastIonex == timeseg) - { - return; - } - - lastIonex = timeseg; - - GEpoch ep = time; - tracepdeex(4, trace, " ..Writing IONEX epoch:%6.0f%6.0f%6.0f%6.0f%6.0f%6.0f \n", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5]); - - tracepdeex(0, ionex, "%6d%54sSTART OF TEC MAP\n", ionexMapIndex, " "); - tracepdeex(0, ionex, "%6.0f%6.0f%6.0f%6.0f%6.0f%6.0f%24sEPOCH OF CURRENT MAP\n", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5], " "); - - vector tecrmsList; - - for (int ihgt = 0; ihgt < acsConfig.ionModelOpts.layer_heights.size(); ihgt++) - for (int ilat = 0; ilat < ionexLatres; ilat++) - { - VectorPos ipp; - ipp[0] = ionexLatmin + (ionexLatres - ilat - 1) * ionexLatinc; - - tracepdeex(0, ionex, " %6.1f%6.1f%6.1f%6.1f%6.1f%28sLAT/LON1/LON2/DLON/H", - ipp[0], - ionexLonmin, - ionexLonmin + (ionexLonres - 1) * ionexLoninc, - ionexLoninc, - acsConfig.ionModelOpts.layer_heights[ihgt] / 1000, " "); - - ipp[0] *= D2R; - - for (int ilon = 0; ilon < ionexLonres; ilon++) - { - if (ilon % 16 == 0) - tracepdeex(0, ionex, "\n"); - - ipp[1] = (ionexLonmin + ilon * ionexLoninc) * D2R; - - double var = 0; - double iono = ionVtec(trace, time, ipp, ihgt, var, kfState) / pow(10, IONEX_NEXP); - - tracepdeex(5, std::cout, "IPP: %8.4f,%9.4f; layr: %1d; delay: %12.6f; var: %.4e\n", - ipp[0]*R2D, - ipp[1]*R2D, - ihgt, - iono, - var); - - if (acsConfig.ionModelOpts.layer_heights.size() == 1) - var += SINGL_LAY_ERR; - - double tecrms = var / pow(10, 2 * IONEX_NEXP); - - if ( tecrms > 9999 - || tecrms <= 0 ) - { - tecrms = 9999; - } - - if ( iono > +9999 - || iono < -9999) - { - iono = 9999; - tecrms = 9999; - } - - tracepdeex(0, ionex, "%5.0f", iono); - tecrmsList.push_back(tecrms); - } - - tracepdeex(0, ionex, "\n"); - } - - tracepdeex(0, ionex, "%6d%54sEND OF TEC MAP\n", ionexMapIndex, " "); - tracepdeex(0, ionex, "%6d%54sSTART OF RMS MAP\n", ionexMapIndex, " "); - - tracepdeex(0, ionex, "%6.0f%6.0f%6.0f%6.0f%6.0f%6.0f%24sEPOCH OF CURRENT MAP\n", ep[0], ep[1], ep[2], ep[3], ep[4], ep[5], " "); - - auto it = tecrmsList.begin(); - - for (int ihgt = 0; ihgt < acsConfig.ionModelOpts.layer_heights.size(); ihgt++) - for (int ilat = 0; ilat < ionexLatres; ilat++) - { - tracepdeex(0, ionex, " %6.1f%6.1f%6.1f%6.1f%6.1f%28sLAT/LON1/LON2/DLON/H", - ionexLatmin + (ionexLatres - ilat - 1) * ionexLatinc, - ionexLonmin, - ionexLonmin + (ionexLonres - 1) * ionexLoninc, - ionexLoninc, - acsConfig.ionModelOpts.layer_heights[ihgt] / 1000, ""); - - for (int ilon = 0; ilon < ionexLonres; ilon++) - { - if (ilon % 16 == 0) - tracepdeex(0, ionex, "\n"); - - double tecrms = *it; - it++; - - tracepdeex(0, ionex, "%5.0f", tecrms); - } - - tracepdeex(0, ionex, "\n"); - } - - tracepdeex(0, ionex, "%6d%54sEND OF RMS MAP\n", ionexMapIndex, ""); - - ionexMapIndex++; + GTow tow = time; + int timeseg = floor(tow / acsConfig.ionexGrid.time_res); + + if (lastIonex >= 0 && lastIonex == timeseg) + { + return; + } + + lastIonex = timeseg; + + GEpoch ep = time; + tracepdeex( + 4, + trace, + " ..Writing IONEX epoch:%6.0f%6.0f%6.0f%6.0f%6.0f%6.0f \n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5] + ); + + tracepdeex(0, ionex, "%6d%54sSTART OF TEC MAP\n", ionexMapIndex, " "); + tracepdeex( + 0, + ionex, + "%6.0f%6.0f%6.0f%6.0f%6.0f%6.0f%24sEPOCH OF CURRENT MAP\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5], + " " + ); + + vector tecrmsList; + + for (int ihgt = 0; ihgt < acsConfig.ionModelOpts.layer_heights.size(); ihgt++) + for (int ilat = 0; ilat < ionexLatres; ilat++) + { + VectorPos ipp; + ipp[0] = ionexLatmin + (ionexLatres - ilat - 1) * ionexLatinc; + + tracepdeex( + 0, + ionex, + " %6.1f%6.1f%6.1f%6.1f%6.1f%28sLAT/LON1/LON2/DLON/H", + ipp[0], + ionexLonmin, + ionexLonmin + (ionexLonres - 1) * ionexLoninc, + ionexLoninc, + acsConfig.ionModelOpts.layer_heights[ihgt] / 1000, + " " + ); + + ipp[0] *= D2R; + + for (int ilon = 0; ilon < ionexLonres; ilon++) + { + if (ilon % 16 == 0) + tracepdeex(0, ionex, "\n"); + + ipp[1] = (ionexLonmin + ilon * ionexLoninc) * D2R; + + double var = 0; + double iono = ionVtec(trace, time, ipp, ihgt, var, kfState) / pow(10, IONEX_NEXP); + + tracepdeex( + 5, + std::cout, + "IPP: %8.4f,%9.4f; layr: %1d; delay: %12.6f; var: %.4e\n", + ipp[0] * R2D, + ipp[1] * R2D, + ihgt, + iono, + var + ); + + if (acsConfig.ionModelOpts.layer_heights.size() == 1) + var += SINGL_LAY_ERR; + + double tecrms = var / pow(10, 2 * IONEX_NEXP); + + if (tecrms > 9999 || tecrms <= 0) + { + tecrms = 9999; + } + + if (iono > +9999 || iono < -9999) + { + iono = 9999; + tecrms = 9999; + } + + tracepdeex(0, ionex, "%5.0f", iono); + tecrmsList.push_back(tecrms); + } + + tracepdeex(0, ionex, "\n"); + } + + tracepdeex(0, ionex, "%6d%54sEND OF TEC MAP\n", ionexMapIndex, " "); + tracepdeex(0, ionex, "%6d%54sSTART OF RMS MAP\n", ionexMapIndex, " "); + + tracepdeex( + 0, + ionex, + "%6.0f%6.0f%6.0f%6.0f%6.0f%6.0f%24sEPOCH OF CURRENT MAP\n", + ep[0], + ep[1], + ep[2], + ep[3], + ep[4], + ep[5], + " " + ); + + auto it = tecrmsList.begin(); + + for (int ihgt = 0; ihgt < acsConfig.ionModelOpts.layer_heights.size(); ihgt++) + for (int ilat = 0; ilat < ionexLatres; ilat++) + { + tracepdeex( + 0, + ionex, + " %6.1f%6.1f%6.1f%6.1f%6.1f%28sLAT/LON1/LON2/DLON/H", + ionexLatmin + (ionexLatres - ilat - 1) * ionexLatinc, + ionexLonmin, + ionexLonmin + (ionexLonres - 1) * ionexLoninc, + ionexLoninc, + acsConfig.ionModelOpts.layer_heights[ihgt] / 1000, + "" + ); + + for (int ilon = 0; ilon < ionexLonres; ilon++) + { + if (ilon % 16 == 0) + tracepdeex(0, ionex, "\n"); + + double tecrms = *it; + it++; + + tracepdeex(0, ionex, "%5.0f", tecrms); + } + + tracepdeex(0, ionex, "\n"); + } + + tracepdeex(0, ionex, "%6d%54sEND OF RMS MAP\n", ionexMapIndex, ""); + + ionexMapIndex++; } -void ionexFileWrite( - Trace& trace, - string filename, - GTime time, - KFState& kfState) +void ionexFileWrite(Trace& trace, string filename, GTime time, KFState& kfState) { - if (acsConfig.ionModelOpts.model == +E_IonoModel::NONE) return; - if (acsConfig.ionModelOpts.model == +E_IonoModel::MEAS_OUT) return; - if (acsConfig.ionModelOpts.layer_heights.size() < 1) return; - if (acsConfig.ionModelOpts.function_order < 1) return; + if (acsConfig.ionModelOpts.model == E_IonoModel::NONE) + return; + if (acsConfig.ionModelOpts.model == E_IonoModel::MEAS_OUT) + return; + if (acsConfig.ionModelOpts.layer_heights.size() < 1) + return; + if (acsConfig.ionModelOpts.function_order < 1) + return; - double sigma0 = 0; - KFKey std0Key; - std0Key.type = KF::IONOSPHERIC; - std0Key.num = 0; - if (kfState.getKFSigma(std0Key, sigma0) == false) return; - if (sigma0 > 1) return; + double sigma0 = 0; + KFKey std0Key; + std0Key.type = KF::IONOSPHERIC; + std0Key.num = 0; + if (kfState.getKFSigma(std0Key, sigma0) == false) + return; + if (sigma0 > 1) + return; - std::ofstream ionex(filename, std::fstream::in | std::fstream::out); + std::ofstream ionex(filename, std::fstream::in | std::fstream::out); - ionex.seekp(0, std::ios::end); + ionex.seekp(0, std::ios::end); - long endFilePos = ionex.tellp(); + long endFilePos = ionex.tellp(); - if (endFilePos == 0) - { - writeIonexHead(ionex); + if (endFilePos == 0) + { + writeIonexHead(ionex); - endLinePos[filename] = ionex.tellp(); - } + endLinePos[filename] = ionex.tellp(); + } - ionex.seekp(endLinePos[filename]); + ionex.seekp(endLinePos[filename]); - writeIonexEpoch(trace, ionex, time, kfState); + writeIonexEpoch(trace, ionex, time, kfState); - endLinePos[filename] = ionex.tellp(); + endLinePos[filename] = ionex.tellp(); - tracepdeex(0, ionex, "%60sEND OF FILE\n", " "); + tracepdeex(0, ionex, "%60sEND OF FILE\n", " "); } - diff --git a/src/cpp/iono/ionoBSplines.cpp b/src/cpp/iono/ionoBSplines.cpp index b919d0acc..4ec56ffa4 100644 --- a/src/cpp/iono/ionoBSplines.cpp +++ b/src/cpp/iono/ionoBSplines.cpp @@ -1,100 +1,104 @@ - -#include "ionoModel.hpp" -#include "observations.hpp" -#include "common.hpp" -#include "acsConfig.hpp" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/observations.hpp" +#include "iono/ionoModel.hpp" struct BspBasis { - int ind; /* layer number */ - double latDeg; /* latitude */ - double lonDeg; /* longitude */ + int ind; /* layer number */ + double latDeg; /* latitude */ + double lonDeg; /* longitude */ }; map bspBasisMap; -static double BSPLINE_LAT_CENTRE = 0; -static double BSPLINE_LON_CENTRE = 0; -static double BSPLINE_LAT_WIDTH = 0; -static double BSPLINE_LON_WIDTH = 0; -static double BSPLINE_LAT_INTERVAL = 0; -static double BSPLINE_LON_INTERVAL = 0; +static double BSPLINE_LAT_CENTRE = 0; +static double BSPLINE_LON_CENTRE = 0; +static double BSPLINE_LAT_WIDTH = 0; +static double BSPLINE_LON_WIDTH = 0; +static double BSPLINE_LAT_INTERVAL = 0; +static double BSPLINE_LON_INTERVAL = 0; /** Initializes grid map model - The following configursation parameters are used - - acsConfig.ionFilterOpts.lat_center: latitude of map centre - - acsConfig.ionFilterOpts.lon_center: longitude of map centre - - acsConfig.ionFilterOpts.lat_width: latitude width of maps - - acsConfig.ionFilterOpts.lon_width: longitude width of maps - - acsConfig.ionFilterOpts.lat_res: latitude resolution of gridmap - - acsConfig.ionFilterOpts.lon_res: longitude resolution of gridmap - - acsConfig.ionFilterOpts.layer_heights: Ionosphere layer Heights + The following configursation parameters are used + - acsConfig.ionFilterOpts.lat_center: latitude of map centre + - acsConfig.ionFilterOpts.lon_center: longitude of map centre + - acsConfig.ionFilterOpts.lat_width: latitude width of maps + - acsConfig.ionFilterOpts.lon_width: longitude width of maps + - acsConfig.ionFilterOpts.lat_res: latitude resolution of gridmap + - acsConfig.ionFilterOpts.lon_res: longitude resolution of gridmap + - acsConfig.ionFilterOpts.layer_heights: Ionosphere layer Heights ----------------------------------------------------------------------------*/ -int configIonModelBsplin( - Trace& trace) +int configIonModelBsplin(Trace& trace) { - bspBasisMap.clear(); - if ( acsConfig.ionexGrid.lat_width > 180 - || acsConfig.ionexGrid.lat_width < 0 - || acsConfig.ionexGrid.lon_width > 360 - || acsConfig.ionexGrid.lon_width < 0) - { - std::cout << "Wrongly sized gridmaps revise lat and lon width parameters..."; - return 0; - } - - BSPLINE_LAT_CENTRE = acsConfig.ionexGrid.lat_centre; - BSPLINE_LON_CENTRE = acsConfig.ionexGrid.lon_centre; - BSPLINE_LAT_INTERVAL = acsConfig.ionexGrid.lat_res; - BSPLINE_LON_INTERVAL = acsConfig.ionexGrid.lon_res; - - int latnum = (int)(acsConfig.ionexGrid.lat_width / acsConfig.ionexGrid.lat_res) + 1; - int lonnum = (int)(acsConfig.ionexGrid.lon_width / acsConfig.ionexGrid.lon_res) + 1; - - BSPLINE_LAT_WIDTH = BSPLINE_LAT_INTERVAL * (latnum - 1) / 2; - BSPLINE_LON_WIDTH = BSPLINE_LON_INTERVAL * (lonnum - 1) / 2; - - double latmin = BSPLINE_LAT_CENTRE - BSPLINE_LAT_WIDTH; - double lonmin = BSPLINE_LON_CENTRE - BSPLINE_LON_WIDTH; - - if ( (latmin < -90) - ||((BSPLINE_LAT_CENTRE + BSPLINE_LAT_WIDTH) > 90)) - { - std::cout << "Gridmap model does not work on polar regions, select other mapping methods"; - return 0; - } - - - BspBasis basis; - int ind = 0; - - for (int layN = 0; layN < acsConfig.ionModelOpts.layer_heights.size(); layN++) - for (int latN = 0; latN < latnum; latN++) - for (int lonN = 0; lonN < lonnum; lonN++) - { - basis.ind = layN; - basis.latDeg = latmin + latN * BSPLINE_LAT_INTERVAL; - double lonmap = lonmin + lonN * BSPLINE_LON_INTERVAL; - - if (lonmap < -180) lonmap += 360; - if (lonmap > 180) lonmap -= 360; - - basis.lonDeg = lonmap; - - bspBasisMap[ind] = basis; - ind++; - } - - acsConfig.ionModelOpts.numBasis = ind; - - tracepdeex(2,trace, "\nIONO_BASIS ind lay latitude longitude"); - - for (auto& [j, basis2] : bspBasisMap) - tracepdeex(2, trace, "\nIONO_BASIS %3d %3d %8.4f %8.4f ", j, basis.ind, basis.latDeg, basis.lonDeg); - - tracepdeex(2,trace, "\n"); - - return ind; + bspBasisMap.clear(); + if (acsConfig.ionexGrid.lat_width > 180 || acsConfig.ionexGrid.lat_width < 0 || + acsConfig.ionexGrid.lon_width > 360 || acsConfig.ionexGrid.lon_width < 0) + { + std::cout << "Wrongly sized gridmaps revise lat and lon width parameters..."; + return 0; + } + + BSPLINE_LAT_CENTRE = acsConfig.ionexGrid.lat_centre; + BSPLINE_LON_CENTRE = acsConfig.ionexGrid.lon_centre; + BSPLINE_LAT_INTERVAL = acsConfig.ionexGrid.lat_res; + BSPLINE_LON_INTERVAL = acsConfig.ionexGrid.lon_res; + + int latnum = (int)(acsConfig.ionexGrid.lat_width / acsConfig.ionexGrid.lat_res) + 1; + int lonnum = (int)(acsConfig.ionexGrid.lon_width / acsConfig.ionexGrid.lon_res) + 1; + + BSPLINE_LAT_WIDTH = BSPLINE_LAT_INTERVAL * (latnum - 1) / 2; + BSPLINE_LON_WIDTH = BSPLINE_LON_INTERVAL * (lonnum - 1) / 2; + + double latmin = BSPLINE_LAT_CENTRE - BSPLINE_LAT_WIDTH; + double lonmin = BSPLINE_LON_CENTRE - BSPLINE_LON_WIDTH; + + if ((latmin < -90) || ((BSPLINE_LAT_CENTRE + BSPLINE_LAT_WIDTH) > 90)) + { + std::cout << "Gridmap model does not work on polar regions, select other mapping methods"; + return 0; + } + + BspBasis basis; + int ind = 0; + + for (int layN = 0; layN < acsConfig.ionModelOpts.layer_heights.size(); layN++) + for (int latN = 0; latN < latnum; latN++) + for (int lonN = 0; lonN < lonnum; lonN++) + { + basis.ind = layN; + basis.latDeg = latmin + latN * BSPLINE_LAT_INTERVAL; + double lonmap = lonmin + lonN * BSPLINE_LON_INTERVAL; + + if (lonmap < -180) + lonmap += 360; + if (lonmap > 180) + lonmap -= 360; + + basis.lonDeg = lonmap; + + bspBasisMap[ind] = basis; + ind++; + } + + acsConfig.ionModelOpts.numBasis = ind; + + tracepdeex(2, trace, "\nIONO_BASIS ind lay latitude longitude"); + + for (auto& [j, basis2] : bspBasisMap) + tracepdeex( + 2, + trace, + "\nIONO_BASIS %3d %3d %8.4f %8.4f ", + j, + basis.ind, + basis.latDeg, + basis.lonDeg + ); + + tracepdeex(2, trace, "\n"); + + return ind; } /** checks if the Ionosphere Piercing Point falls in area of coverage @@ -106,122 +110,117 @@ Author: Ken Harima @ RMIT 04 August 2020 -----------------------------------------------------*/ bool ippCheckBsplin(GTime time, VectorPos& Ion_pp) { - if (fabs(BSPLINE_LAT_CENTRE - Ion_pp.latDeg()) > BSPLINE_LAT_WIDTH) - return false; + if (fabs(BSPLINE_LAT_CENTRE - Ion_pp.latDeg()) > BSPLINE_LAT_WIDTH) + return false; - double londiff = BSPLINE_LON_CENTRE - Ion_pp.lonDeg(); + double londiff = BSPLINE_LON_CENTRE - Ion_pp.lonDeg(); - if (londiff < -180) londiff += 360; - if (londiff > 180) londiff -= 360; + if (londiff < -180) + londiff += 360; + if (londiff > 180) + londiff -= 360; - if (fabs(londiff) > BSPLINE_LON_WIDTH) - return false; + if (fabs(londiff) > BSPLINE_LON_WIDTH) + return false; - return true; + return true; } /** Evaluates B-splines basis functions - int ind I Basis function number - meas I Ionosphere measurement struct - latIPP - Latitude of Ionosphere Piercing Point - lonIPP - Longitude of Ionosphere Piercing Point - angIPP - Angular gain for Ionosphere Piercing Point - bool slant I state to delay gain; false: state to VTEC gain - - BSPLINE_LATINT and BSPLINE_LONINT needs to be set before calling this function + int ind I Basis function number + meas I Ionosphere measurement struct + latIPP - Latitude of Ionosphere Piercing Point + lonIPP - Longitude of Ionosphere Piercing Point + angIPP - Angular gain for Ionosphere Piercing Point + bool slant I state to delay gain; false: state to VTEC gain + + BSPLINE_LATINT and BSPLINE_LONINT needs to be set before calling this function ----------------------------------------------------------------------------*/ -double ionCoefBsplin( - Trace& trace, - int ind, - IonoObs& obs, - bool slant) +double ionCoefBsplin(Trace& trace, int ind, IonoObs& obs, bool slant) { - if (ind >= bspBasisMap.size()) - return 0; + if (ind >= bspBasisMap.size()) + return 0; - auto& basis = bspBasisMap[ind]; + auto& basis = bspBasisMap[ind]; - double latdiff = (obs.ippMap[basis.ind].latDeg - basis.latDeg) / BSPLINE_LAT_INTERVAL; + double latdiff = (obs.ippMap[basis.ind].latDeg - basis.latDeg) / BSPLINE_LAT_INTERVAL; - if ( latdiff <= -1 - || latdiff >= +1) - { - return 0; - } + if (latdiff <= -1 || latdiff >= +1) + { + return 0; + } - double londiff = (obs.ippMap[basis.ind].lonDeg - basis.lonDeg); + double londiff = (obs.ippMap[basis.ind].lonDeg - basis.lonDeg); - if (londiff < -180) londiff += 360; - if (londiff > 180) londiff -= 360; + if (londiff < -180) + londiff += 360; + if (londiff > 180) + londiff -= 360; - londiff /= BSPLINE_LAT_INTERVAL; + londiff /= BSPLINE_LAT_INTERVAL; - if ( londiff <= -1 - || londiff >= +1) - { - return 0; - } + if (londiff <= -1 || londiff >= +1) + { + return 0; + } - double out = latdiff < 0 ? (1 + latdiff) : (1 - latdiff); + double out = latdiff < 0 ? (1 + latdiff) : (1 - latdiff); - if (londiff < 0) out *= 1 + londiff; - else out *= 1 - londiff; + if (londiff < 0) + out *= 1 + londiff; + else + out *= 1 - londiff; - if (slant) - { - out *= obs.ippMap[basis.ind].slantFactor * obs.stecToDelay; - } + if (slant) + { + out *= obs.ippMap[basis.ind].slantFactor * obs.stecToDelay; + } - return out; + return out; } /** Estimate Ionosphere VTEC using Ionospheric gridmaps - Ion_pp I Ionosphere Piercing Point - layer I Layer number - vari O variance of VTEC + Ion_pp I Ionosphere Piercing Point + layer I Layer number + vari O variance of VTEC returns: VETC at piercing point */ -double ionVtecBsplin( - Trace& trace, - GTime time, - VectorPos& ionPP, - int layer, - double& var, - KFState& kfState) +double +ionVtecBsplin(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState) { - var = 0; + var = 0; - if (ippCheckBsplin(time, ionPP) == false) - { - return 0; - } + if (ippCheckBsplin(time, ionPP) == false) + { + return 0; + } - double iono = 0; - GObs tmpobs; - tmpobs.ippMap[layer].latDeg = ionPP.latDeg(); - tmpobs.ippMap[layer].lonDeg = ionPP.lonDeg(); - tmpobs.ippMap[layer].slantFactor = 1; + double iono = 0; + GObs tmpobs; + tmpobs.ippMap[layer].latDeg = ionPP.latDeg(); + tmpobs.ippMap[layer].lonDeg = ionPP.lonDeg(); + tmpobs.ippMap[layer].slantFactor = 1; - for (int ind = 0; ind < acsConfig.ionModelOpts.numBasis; ind++) - { - auto& basis = bspBasisMap[ind]; + for (int ind = 0; ind < acsConfig.ionModelOpts.numBasis; ind++) + { + auto& basis = bspBasisMap[ind]; - if (basis.ind != layer) - continue; + if (basis.ind != layer) + continue; - double coef = ionCoefBsplin(trace, ind, tmpobs, false); + double coef = ionCoefBsplin(trace, ind, tmpobs, false); - KFKey keyC; - keyC.type = KF::IONOSPHERIC; - keyC.num = ind; + KFKey keyC; + keyC.type = KF::IONOSPHERIC; + keyC.num = ind; - double kfval = 0; - double kfvar = 0; - kfState.getKFValue(keyC, kfval, &kfvar); + double kfval = 0; + double kfvar = 0; + kfState.getKFValue(keyC, kfval, &kfvar); - iono += coef * kfval; - var += SQR(coef) * kfvar; - } + iono += coef * kfval; + var += SQR(coef) * kfvar; + } - return iono; + return iono; } diff --git a/src/cpp/iono/ionoLocalSTEC.cpp b/src/cpp/iono/ionoLocalSTEC.cpp index eaa564fd7..48f666d0b 100644 --- a/src/cpp/iono/ionoLocalSTEC.cpp +++ b/src/cpp/iono/ionoLocalSTEC.cpp @@ -1,537 +1,600 @@ -#include "observations.hpp" -#include "coordinates.hpp" -#include "ionoModel.hpp" -#include "acsConfig.hpp" -#include "common.hpp" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/observations.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/coordinates.hpp" -#define IONO_OUT_THRESHOLD 120 -#define DEFAULT_STEC_POLY_ACC 0.5 +constexpr int IONO_OUT_THRESHOLD = 120; +constexpr double DEFAULT_STEC_POLY_ACC = 0.5; double currRecLatDeg = 0; double currRecLonDeg = 0; -map> irrGrdCoef; +map> irrGrdCoef; struct LocalBasis { - int regionID; ///< Atmospheric region ID - SatSys Sat; ///< Satellite System - E_BasisType type; ///< parameter type - int index; ///< parameter index + int regionID; ///< Atmospheric region ID + SatSys Sat; ///< Satellite System + E_BasisType type; ///< parameter type + int index; ///< parameter index }; -map localBasisVec; +map localBasisVec; -double IrregGridCoef( - int Index, - double recLatDeg, - double recLonDeg) +double IrregGridCoef(int Index, double recLatDeg, double recLonDeg) { - if ( recLatDeg != currRecLatDeg - || recLonDeg != currRecLonDeg) - { - double acum = 0; - SatSys sat0 = localBasisVec[0].Sat; + if (recLatDeg != currRecLatDeg || recLonDeg != currRecLonDeg) + { + double acum = 0; + SatSys sat0 = localBasisVec[0].Sat; - map> coefMap; + map> coefMap; - for (auto& [ind, basis] : localBasisVec) - { - if (basis.Sat != sat0) - break; + for (auto& [ind, basis] : localBasisVec) + { + if (basis.Sat != sat0) + break; - if (basis.type != +E_BasisType::GRIDPOINT) - continue; + if (basis.type != E_BasisType::GRIDPOINT) + continue; - auto& atmReg = nav.ssrAtm.atmosRegionsMap[basis.regionID]; + auto& atmReg = nav.ssrAtm.atmosRegionsMap[basis.regionID]; - double dlat = fabs(recLatDeg - atmReg.gridLatDeg[basis.index]); - double dlon = fabs(recLonDeg - atmReg.gridLonDeg[basis.index]); + double dlat = fabs(recLatDeg - atmReg.gridLatDeg[basis.index]); + double dlon = fabs(recLonDeg - atmReg.gridLonDeg[basis.index]); - if (dlat > atmReg.intLatDeg || atmReg.intLatDeg == 0) continue; - if (dlon > atmReg.intLonDeg || atmReg.intLonDeg == 0) continue; + if (dlat > atmReg.intLatDeg || atmReg.intLatDeg == 0) + continue; + if (dlon > atmReg.intLonDeg || atmReg.intLonDeg == 0) + continue; - coefMap[basis.regionID][basis.index] = (1 - dlat / atmReg.intLatDeg) * (1 - dlon / atmReg.intLonDeg); + coefMap[basis.regionID][basis.index] = + (1 - dlat / atmReg.intLatDeg) * (1 - dlon / atmReg.intLonDeg); - acum += coefMap[basis.regionID][basis.index]; - } + acum += coefMap[basis.regionID][basis.index]; + } - irrGrdCoef.clear(); + irrGrdCoef.clear(); - for (auto& [regId, coef] : coefMap) - for (auto& [grdId, c] : coef) - irrGrdCoef[regId][grdId] = c / acum; + for (auto& [regId, coef] : coefMap) + for (auto& [grdId, c] : coef) + irrGrdCoef[regId][grdId] = c / acum; - currRecLatDeg = recLatDeg; - currRecLonDeg = recLonDeg; - } + currRecLatDeg = recLatDeg; + currRecLonDeg = recLonDeg; + } - auto& basis = localBasisVec[Index]; + auto& basis = localBasisVec[Index]; - if (irrGrdCoef.find(basis.regionID) == irrGrdCoef.end()) - return 0; + if (irrGrdCoef.find(basis.regionID) == irrGrdCoef.end()) + return 0; - if (irrGrdCoef[basis.regionID].find(basis.index) == irrGrdCoef[basis.regionID].end()) - return 0; + if (irrGrdCoef[basis.regionID].find(basis.index) == irrGrdCoef[basis.regionID].end()) + return 0; - return irrGrdCoef[basis.regionID][basis.index]; + return irrGrdCoef[basis.regionID][basis.index]; } -int configIonModelLocal_( - Trace& trace) +int configIonModelLocal_(Trace& trace) { - localBasisVec.clear(); - - int ind = 0; - - for (auto& [Sat, satNav] : nav.satNavMap) - for (auto& [iatm, atmReg] : nav.ssrAtm.atmosRegionsMap) - { - if ( atmReg.gridType >= 0 - && atmReg.ionoGrid) - for (auto& [igrid, latgrid] : atmReg.gridLatDeg) - { - LocalBasis basis; - basis.regionID = iatm; - basis.Sat = Sat; - basis.type = E_BasisType::GRIDPOINT; - basis.index = igrid; - - localBasisVec[ind] = basis; - ind++; - } - else - for (int i = 0; i < atmReg.ionoPolySize; i++) - { - LocalBasis basis; - basis.regionID = iatm; - basis.Sat = Sat; - basis.type = E_BasisType::POLYNOMIAL; - basis.index = i; - - localBasisVec[ind] = basis; - ind++; - } - } - - if (localBasisVec.empty()) - return 0; - - acsConfig.ionModelOpts.numBasis = localBasisVec.size(); - acsConfig.ionModelOpts.estimate_sat_dcb = false; - acsConfig.ionModelOpts.layer_heights.clear(); - acsConfig.ionModelOpts.layer_heights.push_back(0); - - // tracepdeex(2,trace, "\nIONO_BASIS ind reg sat type ind"); - // for (auto [j, basis] : localBasisVec) - // tracepdeex(2,trace, "\nIONO_BASIS %3d %2d %s %s %3d ", j, basis.regionID, basis.Sat.id().c_str(), (basis.type==+E_BasisType::POLYNOMIAL)?"poly":"grid", basis.index); - // tracepdeex(2,trace, "\n"); - - return acsConfig.ionModelOpts.numBasis; + localBasisVec.clear(); + + int ind = 0; + + for (auto& [Sat, satNav] : nav.satNavMap) + for (auto& [iatm, atmReg] : nav.ssrAtm.atmosRegionsMap) + { + if (atmReg.gridType >= 0 && atmReg.ionoGrid) + for (auto& [igrid, latgrid] : atmReg.gridLatDeg) + { + LocalBasis basis; + basis.regionID = iatm; + basis.Sat = Sat; + basis.type = E_BasisType::GRIDPOINT; + basis.index = igrid; + + localBasisVec[ind] = basis; + ind++; + } + else + for (int i = 0; i < atmReg.ionoPolySize; i++) + { + LocalBasis basis; + basis.regionID = iatm; + basis.Sat = Sat; + basis.type = E_BasisType::POLYNOMIAL; + basis.index = i; + + localBasisVec[ind] = basis; + ind++; + } + } + + if (localBasisVec.empty()) + return 0; + + acsConfig.ionModelOpts.numBasis = localBasisVec.size(); + acsConfig.ionModelOpts.estimate_sat_dcb = false; + acsConfig.ionModelOpts.layer_heights.clear(); + acsConfig.ionModelOpts.layer_heights.push_back(0); + + // tracepdeex(2,trace, "\nIONO_BASIS ind reg sat type ind"); + // for (auto [j, basis] : localBasisVec) + // tracepdeex(2,trace, "\nIONO_BASIS %3d %2d %s %s %3d ", j, basis.regionID, + // basis.Sat.id().c_str(), (basis.type==E_BasisType::POLYNOMIAL)?"poly":"grid", basis.index); + // tracepdeex(2,trace, "\n"); + + return acsConfig.ionModelOpts.numBasis; } /** Checks if the Ionosphere Piercing Point falls in area of coverage. Return true if there is a region containing the IPP, false if out of coverage */ bool ippCheckLocal( - GTime time, ///< time of observations (not used) - VectorPos& ionPP) ///< Ionospheric piercing point to be updated + GTime time, ///< time of observations (not used) + VectorPos& ionPP ///< Ionospheric piercing point to be updated +) { - auto& RegMaps = nav.ssrAtm.atmosRegionsMap; + auto& RegMaps = nav.ssrAtm.atmosRegionsMap; - for (auto& [iatm,atmReg] : RegMaps) - { - if (ionPP.latDeg() < atmReg.minLatDeg) continue; - if (ionPP.latDeg() > atmReg.maxLatDeg) continue; + for (auto& [iatm, atmReg] : RegMaps) + { + if (ionPP.latDeg() < atmReg.minLatDeg) + continue; + if (ionPP.latDeg() > atmReg.maxLatDeg) + continue; - double recLonDeg = ionPP.lonDeg(); - double midLonDeg = (atmReg.minLonDeg + atmReg.maxLonDeg) / 2; + double recLonDeg = ionPP.lonDeg(); + double midLonDeg = (atmReg.minLonDeg + atmReg.maxLonDeg) / 2; - if ((recLonDeg - midLonDeg) > 180) recLonDeg -= 360; - else if ((recLonDeg - midLonDeg) <-180) recLonDeg += 360; + if ((recLonDeg - midLonDeg) > 180) + recLonDeg -= 360; + else if ((recLonDeg - midLonDeg) < -180) + recLonDeg += 360; - if (recLonDeg > atmReg.maxLonDeg) continue; - if (recLonDeg < atmReg.minLonDeg) continue; + if (recLonDeg > atmReg.maxLonDeg) + continue; + if (recLonDeg < atmReg.minLonDeg) + continue; - return true; - } + return true; + } - return false; + return false; } -double ionCoefPolynomial( - Trace& trace, - SSRAtmRegion& atmReg, - double latDeg, - double lonDeg, - int ind) +double ionCoefPolynomial(Trace& trace, SSRAtmRegion& atmReg, double latDeg, double lonDeg, int ind) { - if (atmReg.maxLatDeg <= atmReg.minLatDeg) return 0; - if (atmReg.maxLonDeg <= atmReg.minLonDeg) return 0; - if (latDeg > atmReg.maxLatDeg) return 0; - if (latDeg < atmReg.minLatDeg) return 0; - - double recLonDeg = lonDeg; - double midLonDeg = (atmReg.minLonDeg + atmReg.maxLonDeg) / 2; - if ((recLonDeg - midLonDeg) > 180) recLonDeg -= 360; - else if ((recLonDeg - midLonDeg) < -180) recLonDeg += 360; - - if (recLonDeg > atmReg.maxLonDeg) return 0; - if (recLonDeg < atmReg.minLonDeg) return 0; - - double latdiff = (latDeg - atmReg.gridLatDeg[0])/(atmReg.maxLatDeg-atmReg.minLatDeg); - double londiff = (recLonDeg - atmReg.gridLonDeg[0])/(atmReg.maxLonDeg-atmReg.minLonDeg); - - tracepdeex (4, trace, "\nPolinomial basis %d: %.4f, %.4f", ind, atmReg.gridLatDeg[0], atmReg.gridLonDeg[0]); - - switch (ind) - { - case 0: return 1; - case 1: return 2*latdiff; - case 2: return 2*londiff; - case 3: return 4*latdiff * londiff; - case 4: return 3*latdiff * latdiff; - case 5: return 3*londiff * londiff; - default: return 0; - } + if (atmReg.maxLatDeg <= atmReg.minLatDeg) + return 0; + if (atmReg.maxLonDeg <= atmReg.minLonDeg) + return 0; + if (latDeg > atmReg.maxLatDeg) + return 0; + if (latDeg < atmReg.minLatDeg) + return 0; + + double recLonDeg = lonDeg; + double midLonDeg = (atmReg.minLonDeg + atmReg.maxLonDeg) / 2; + if ((recLonDeg - midLonDeg) > 180) + recLonDeg -= 360; + else if ((recLonDeg - midLonDeg) < -180) + recLonDeg += 360; + + if (recLonDeg > atmReg.maxLonDeg) + return 0; + if (recLonDeg < atmReg.minLonDeg) + return 0; + + double latdiff = (latDeg - atmReg.gridLatDeg[0]) / (atmReg.maxLatDeg - atmReg.minLatDeg); + double londiff = (recLonDeg - atmReg.gridLonDeg[0]) / (atmReg.maxLonDeg - atmReg.minLonDeg); + + tracepdeex( + 4, + trace, + "\nPolinomial basis %d: %.4f, %.4f", + ind, + atmReg.gridLatDeg[0], + atmReg.gridLonDeg[0] + ); + + switch (ind) + { + case 0: + return 1; + case 1: + return 2 * latdiff; + case 2: + return 2 * londiff; + case 3: + return 4 * latdiff * londiff; + case 4: + return 3 * latdiff * latdiff; + case 5: + return 3 * londiff * londiff; + default: + return 0; + } } /** calcuates the partials of observations with respect to basis functions */ double ionCoefLocal( - Trace& trace, - int ind, ///< Basis function number - IonoObs& obs) ///< Metadata containing piercing points + Trace& trace, + int ind, ///< Basis function number + IonoObs& obs ///< Metadata containing piercing points +) { - if (ind >= localBasisVec.size()) return 0; - - auto& basis = localBasisVec[ind]; - - if (obs.ionoSat != basis.Sat) return 0; - - auto& atmReg = nav.ssrAtm.atmosRegionsMap[basis.regionID]; - - double recLatDeg = obs.ippMap[0].latDeg; - double recLonDeg = obs.ippMap[0].lonDeg; - - switch (basis.type) - { - case +E_BasisType::POLYNOMIAL: return ionCoefPolynomial (trace, atmReg, recLatDeg, recLonDeg, basis.index); - case +E_BasisType::GRIDPOINT: - { - if (atmReg.gridType == 0) - return IrregGridCoef(ind, recLatDeg, recLonDeg); - - double dlatDeg = fabs(recLatDeg - atmReg.gridLatDeg[basis.index]); - double dlonDeg = fabs(recLonDeg - atmReg.gridLonDeg[basis.index]); - - if (dlatDeg > atmReg.intLatDeg || atmReg.intLatDeg == 0) return 0; - if (dlonDeg > atmReg.intLonDeg || atmReg.intLonDeg == 0) return 0; - - tracepdeex (4, trace, "\nGridded basis: %.4f, %.4f", atmReg.gridLatDeg[basis.index], atmReg.gridLonDeg[basis.index]); - - return (1 - dlatDeg / atmReg.intLatDeg) * (1 - dlonDeg / atmReg.intLonDeg); //todo aaron use bilinear interpolation function? - } - default: - { - return 0; - } - } + if (ind >= localBasisVec.size()) + return 0; + + auto& basis = localBasisVec[ind]; + + if (obs.ionoSat != basis.Sat) + return 0; + + auto& atmReg = nav.ssrAtm.atmosRegionsMap[basis.regionID]; + + double recLatDeg = obs.ippMap[0].latDeg; + double recLonDeg = obs.ippMap[0].lonDeg; + + switch (basis.type) + { + case E_BasisType::POLYNOMIAL: + return ionCoefPolynomial(trace, atmReg, recLatDeg, recLonDeg, basis.index); + case E_BasisType::GRIDPOINT: + { + if (atmReg.gridType == 0) + return IrregGridCoef(ind, recLatDeg, recLonDeg); + + double dlatDeg = fabs(recLatDeg - atmReg.gridLatDeg[basis.index]); + double dlonDeg = fabs(recLonDeg - atmReg.gridLonDeg[basis.index]); + + if (dlatDeg > atmReg.intLatDeg || atmReg.intLatDeg == 0) + return 0; + if (dlonDeg > atmReg.intLonDeg || atmReg.intLonDeg == 0) + return 0; + + tracepdeex( + 4, + trace, + "\nGridded basis: %.4f, %.4f", + atmReg.gridLatDeg[basis.index], + atmReg.gridLonDeg[basis.index] + ); + + return (1 - dlatDeg / atmReg.intLatDeg) * + (1 - dlonDeg / atmReg.intLonDeg + ); // todo aaron use bilinear interpolation function? + } + default: + { + return 0; + } + } } -void ionOutputLocal( - Trace& trace, - KFState& kfState) +void ionOutputLocal(Trace& trace, KFState& kfState) { - //Discard old data - for (auto& [regID,regData] : nav.ssrAtm.atmosRegionsMap) - for (auto& [sat ,satData] : regData.stecData) - for (auto it = satData.begin(); it != satData.end();) - { - double dt = (kfState.time - it->first).to_double(); - if(abs(dt) > acsConfig.ssrInOpts.local_stec_valid_time) - it = satData.erase(it); - else - it++; - } - - // Copy new data - for (auto [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::IONOSPHERIC) - continue; - - auto& basis = localBasisVec[key.num]; - auto& atmReg = nav.ssrAtm.atmosRegionsMap[basis.regionID]; - atmReg.stecUpdateTime = kfState.time; - auto& stecRecord = atmReg.stecData[basis.Sat][kfState.time]; - - switch (basis.type) - { - case E_BasisType::POLYNOMIAL: - { - double val; - double var; - kfState.getKFValue(key, val, &var); - - double fact = 1; - double latFact = (atmReg.maxLatDeg-atmReg.minLatDeg); - double lonFact = (atmReg.maxLonDeg-atmReg.minLonDeg); - switch (basis.index) - { - case 0: break; - case 1: fact = latFact/2; break; - case 2: fact = lonFact/2; break; - case 3: fact = latFact*lonFact/4; break; - case 4: fact = latFact*latFact/3; break; - case 5: fact = lonFact*lonFact/3; break; - default: var = 1e6; break; - } - - if (var > 100*SQR(acsConfig.ssrOpts.max_stec_sigma)) - { - stecRecord.poly[basis.index] = -9999; - stecRecord.sigma = 1e6; - } - else - { - stecRecord.poly[basis.index] = fact * val; - if (stecRecord.sigma < 1e6) - stecRecord.sigma = acsConfig.ssrOpts.max_stec_sigma; - } - - break; - } - case E_BasisType::GRIDPOINT: - { - double val; - double var; - kfState.getKFValue(key, val,&var); - - double sigma = sqrt(var); - if (sigma < acsConfig.ssrOpts.max_stec_sigma) - { - stecRecord.grid[basis.index] = val; - - if (sigma > stecRecord.sigma) - stecRecord.sigma = sigma; - } - break; - } - } - } - - // Divide between gridmap and polynomial, if necessary - for (auto& [regID, regData] : nav.ssrAtm.atmosRegionsMap) - { - if (regData.gridType < 0) continue; - if (!regData.ionoGrid) continue; - if (regData.ionoPolySize <= 0) continue; - - // Grid plus polynomial corrections - for (auto& [sat, satData] : regData.stecData) - { - if (satData.find(kfState.time) == satData.end()) - continue; - auto& stecData = satData[kfState.time]; - - stecData.poly.clear(); - - int ngrid = stecData.grid.size(); - if ( ngrid <= 1) - continue; - - int npoly = (regData.ionoPolySize < (ngrid - 1)) ? regData.ionoPolySize : (ngrid - 1); - if (npoly == 2) npoly = 1; - if (npoly == 5) npoly = 4; - - VectorXd y = VectorXd::Zero(ngrid); - MatrixXd H = MatrixXd::Zero(ngrid, npoly); - - int nind = 0; - map gridMap; - for (auto& [ind, stec] : stecData.grid) - { - y(nind)=stec; - H(nind, 0) = 1; - if (npoly == 1) - continue; - - double dLat = regData.gridLatDeg[ind] - regData.gridLatDeg[0]; - double dLon = regData.gridLonDeg[ind] - regData.gridLonDeg[0]; - H(nind, 1) = dLat; - H(nind, 2) = dLon; - if (npoly < 4) - continue; - - H(nind, 3) = dLat * dLon; - if (npoly < 6) - continue; - - H(nind, 4) = dLat * dLat; - H(nind, 5) = dLon * dLon; - - gridMap[ind] = nind; - nind++; - } - - MatrixXd Q = (H.transpose()*H).inverse(); - VectorXd x = Q * H.transpose() * y; - VectorXd v = y - H * x; - - for (int i=0; i < npoly; i++) - stecData.poly[i] = x[i]; - - for (auto& [ind, stec] : stecData.grid) - stec = v[gridMap[ind]]; - } - } + // Discard old data + for (auto& [regID, regData] : nav.ssrAtm.atmosRegionsMap) + for (auto& [sat, satData] : regData.stecData) + for (auto it = satData.begin(); it != satData.end();) + { + double dt = (kfState.time - it->first).to_double(); + if (abs(dt) > acsConfig.ssrInOpts.local_stec_valid_time) + it = satData.erase(it); + else + it++; + } + + // Copy new data + for (auto [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::IONOSPHERIC) + continue; + + auto& basis = localBasisVec[key.num]; + auto& atmReg = nav.ssrAtm.atmosRegionsMap[basis.regionID]; + atmReg.stecUpdateTime = kfState.time; + auto& stecRecord = atmReg.stecData[basis.Sat][kfState.time]; + + switch (basis.type) + { + case E_BasisType::POLYNOMIAL: + { + double val; + double var; + kfState.getKFValue(key, val, &var); + + double fact = 1; + double latFact = (atmReg.maxLatDeg - atmReg.minLatDeg); + double lonFact = (atmReg.maxLonDeg - atmReg.minLonDeg); + switch (basis.index) + { + case 0: + break; + case 1: + fact = latFact / 2; + break; + case 2: + fact = lonFact / 2; + break; + case 3: + fact = latFact * lonFact / 4; + break; + case 4: + fact = latFact * latFact / 3; + break; + case 5: + fact = lonFact * lonFact / 3; + break; + default: + var = 1e6; + break; + } + + if (var > 100 * SQR(acsConfig.ssrOpts.max_stec_sigma)) + { + stecRecord.poly[basis.index] = -9999; + stecRecord.sigma = 1e6; + } + else + { + stecRecord.poly[basis.index] = fact * val; + if (stecRecord.sigma < 1e6) + stecRecord.sigma = acsConfig.ssrOpts.max_stec_sigma; + } + + break; + } + case E_BasisType::GRIDPOINT: + { + double val; + double var; + kfState.getKFValue(key, val, &var); + + double sigma = sqrt(var); + if (sigma < acsConfig.ssrOpts.max_stec_sigma) + { + stecRecord.grid[basis.index] = val; + + if (sigma > stecRecord.sigma) + stecRecord.sigma = sigma; + } + break; + } + } + } + + // Divide between gridmap and polynomial, if necessary + for (auto& [regID, regData] : nav.ssrAtm.atmosRegionsMap) + { + if (regData.gridType < 0) + continue; + if (!regData.ionoGrid) + continue; + if (regData.ionoPolySize <= 0) + continue; + + // Grid plus polynomial corrections + for (auto& [sat, satData] : regData.stecData) + { + if (satData.find(kfState.time) == satData.end()) + continue; + auto& stecData = satData[kfState.time]; + + stecData.poly.clear(); + + int ngrid = stecData.grid.size(); + if (ngrid <= 1) + continue; + + int npoly = (regData.ionoPolySize < (ngrid - 1)) ? regData.ionoPolySize : (ngrid - 1); + if (npoly == 2) + npoly = 1; + if (npoly == 5) + npoly = 4; + + VectorXd y = VectorXd::Zero(ngrid); + MatrixXd H = MatrixXd::Zero(ngrid, npoly); + + int nind = 0; + map gridMap; + for (auto& [ind, stec] : stecData.grid) + { + y(nind) = stec; + H(nind, 0) = 1; + if (npoly == 1) + continue; + + double dLat = regData.gridLatDeg[ind] - regData.gridLatDeg[0]; + double dLon = regData.gridLonDeg[ind] - regData.gridLonDeg[0]; + H(nind, 1) = dLat; + H(nind, 2) = dLon; + if (npoly < 4) + continue; + + H(nind, 3) = dLat * dLon; + if (npoly < 6) + continue; + + H(nind, 4) = dLat * dLat; + H(nind, 5) = dLon * dLon; + + gridMap[ind] = nind; + nind++; + } + + MatrixXd Q = (H.transpose() * H).inverse(); + VectorXd x = Q * H.transpose() * y; + VectorXd v = y - H * x; + + for (int i = 0; i < npoly; i++) + stecData.poly[i] = x[i]; + + for (auto& [ind, stec] : stecData.grid) + stec = v[gridMap[ind]]; + } + } } bool getCmpSSRIono( - Trace& trace, ///< Debug trace - GTime time, ///< GPS time - SSRAtm& ssrAtm, ///< SSR Atmospheric corrections - Vector3d& rRec, ///< receiver position - double& iono, ///< ionoapheric delay (in TECu) - double& var, ///< ionoapheric delay (in TECu^2) - SatSys Sat) ///< Satellite + Trace& trace, ///< Debug trace + GTime time, ///< GPS time + SSRAtm& ssrAtm, ///< SSR Atmospheric corrections + Vector3d& rRec, ///< receiver position + double& iono, ///< ionoapheric delay (in TECu) + double& var, ///< ionoapheric delay (in TECu^2) + SatSys Sat ///< Satellite +) { - GObs obs; - obs.Sat = Sat; - iono = 0; - var = 0; - - VectorPos pos = ecef2pos(rRec); - map numer; - map acmVar; - double denom=0; - for (auto& [regID,regData] : ssrAtm.atmosRegionsMap) - { - if (regData.stecData.find(Sat) == regData.stecData.end()) - continue; - auto& satData = regData.stecData[Sat]; - - GTime tAtm; - for (auto it = satData.begin(); it != satData.end();) - { - GTime tim = it->first; - double dt = (time-tim).to_double(); - if ( dt > acsConfig.ssrInOpts.local_stec_valid_time) - it = satData.erase(it); - else - { - if (tAtm == GTime::noTime() - || abs(dt) < abs((time-tAtm).to_double())) - tAtm = tim; - it++; - } - } - - if (tAtm == GTime::noTime()) - continue; - - auto& stecData = satData[tAtm]; - - double recLonDeg = pos.lonDeg(); - double midLonDeg = (regData.minLonDeg + regData.maxLonDeg) / 2; - if ((recLonDeg - midLonDeg) > 180) recLonDeg -= 360; - else if ((recLonDeg - midLonDeg) < -180) recLonDeg += 360; - - if (stecData.grid.size() <= 1) // No grid Map data - { - if (stecData.poly.size() <= 0) continue; - if (pos.latDeg() > regData.maxLatDeg) continue; - if (pos.latDeg() > regData.minLatDeg) continue; - if (recLonDeg > regData.maxLonDeg) continue; - if (recLonDeg < regData.minLonDeg) continue; - - var = stecData.sigma; - iono = stecData.poly[0]; - - double dLat = pos.latDeg() - regData.gridLatDeg[0]; - double dLon = recLonDeg - regData.gridLonDeg[0]; - - if (stecData.poly.size() > 2) - { - iono += stecData.poly[1] * dLat; - iono += stecData.poly[2] * dLon; - } - - if (stecData.poly.size() > 3) - iono += stecData.poly[3] * dLat * dLon; - - if (stecData.poly.size() > 5) - { - iono += stecData.poly[4] * dLat * dLat; - iono += stecData.poly[5] * dLon * dLon; - } - return true; - } - - if (regData.intLatDeg <= 0) continue; - if (regData.intLonDeg <= 0) continue; - - - if (stecData.poly.size() > 0) - { - for (auto& [ind, stec] : stecData.grid) - { - if (stec < -9000) - continue; - - double dLat = regData.gridLatDeg[ind] - regData.gridLatDeg[0]; - double dLon = regData.gridLonDeg[ind] - regData.gridLonDeg[0]; - - stec += stecData.poly[0]; - - if (stecData.poly.size() > 2) - { - stec += stecData.poly[1] * dLat; - stec += stecData.poly[2] * dLon; - } - if (stecData.poly.size() > 3) - stec += stecData.poly[3] * dLat * dLon; - - if (stecData.poly.size() > 5) - { - stec += stecData.poly[4] * dLat * dLat; - stec += stecData.poly[5] * dLon * dLon; - } - } - stecData.poly.clear(); - } - - for (auto& [ind, stec] : stecData.grid) - { - if (stec < -9000) - continue; - - double dLat = abs(pos.latDeg() - regData.gridLatDeg[ind]) / regData.intLatDeg; - double dLon = abs(recLonDeg - regData.gridLonDeg[ind]) / regData.intLonDeg; - - if (dLat >= 1) continue; - if (dLon >= 1) continue; - - // tracepdeex(2, std::cout,"Iono grid %s %2d, %2d: %.4f %.4f, %.4f\n", Sat.id().c_str(), regID, ind, dLat, dLon, stec); - double coef = (1-dLat)*(1-dLon); - - acmVar [64 * regID + ind] = SQR( coef * stecData.sigma); - numer [64 * regID + ind] = coef * stec; - denom += coef; - } - } - - if (numer.size() <= 0) - return false; - - for (auto [ind, val] : numer) - { - iono += val; - var += acmVar[ind]; - } - iono /= denom; - var /= SQR(denom); - - return true; + GObs obs; + obs.Sat = Sat; + iono = 0; + var = 0; + + VectorPos pos = ecef2pos(rRec); + map numer; + map acmVar; + double denom = 0; + for (auto& [regID, regData] : ssrAtm.atmosRegionsMap) + { + if (regData.stecData.find(Sat) == regData.stecData.end()) + continue; + auto& satData = regData.stecData[Sat]; + + GTime tAtm; + for (auto it = satData.begin(); it != satData.end();) + { + GTime tim = it->first; + double dt = (time - tim).to_double(); + if (dt > acsConfig.ssrInOpts.local_stec_valid_time) + it = satData.erase(it); + else + { + if (tAtm == GTime::noTime() || abs(dt) < abs((time - tAtm).to_double())) + tAtm = tim; + it++; + } + } + + if (tAtm == GTime::noTime()) + continue; + + auto& stecData = satData[tAtm]; + + double recLonDeg = pos.lonDeg(); + double midLonDeg = (regData.minLonDeg + regData.maxLonDeg) / 2; + if ((recLonDeg - midLonDeg) > 180) + recLonDeg -= 360; + else if ((recLonDeg - midLonDeg) < -180) + recLonDeg += 360; + + if (stecData.grid.size() <= 1) // No grid Map data + { + if (stecData.poly.size() <= 0) + continue; + if (pos.latDeg() > regData.maxLatDeg) + continue; + if (pos.latDeg() > regData.minLatDeg) + continue; + if (recLonDeg > regData.maxLonDeg) + continue; + if (recLonDeg < regData.minLonDeg) + continue; + + var = stecData.sigma; + iono = stecData.poly[0]; + + double dLat = pos.latDeg() - regData.gridLatDeg[0]; + double dLon = recLonDeg - regData.gridLonDeg[0]; + + if (stecData.poly.size() > 2) + { + iono += stecData.poly[1] * dLat; + iono += stecData.poly[2] * dLon; + } + + if (stecData.poly.size() > 3) + iono += stecData.poly[3] * dLat * dLon; + + if (stecData.poly.size() > 5) + { + iono += stecData.poly[4] * dLat * dLat; + iono += stecData.poly[5] * dLon * dLon; + } + return true; + } + + if (regData.intLatDeg <= 0) + continue; + if (regData.intLonDeg <= 0) + continue; + + if (stecData.poly.size() > 0) + { + for (auto& [ind, stec] : stecData.grid) + { + if (stec < -9000) + continue; + + double dLat = regData.gridLatDeg[ind] - regData.gridLatDeg[0]; + double dLon = regData.gridLonDeg[ind] - regData.gridLonDeg[0]; + + stec += stecData.poly[0]; + + if (stecData.poly.size() > 2) + { + stec += stecData.poly[1] * dLat; + stec += stecData.poly[2] * dLon; + } + if (stecData.poly.size() > 3) + stec += stecData.poly[3] * dLat * dLon; + + if (stecData.poly.size() > 5) + { + stec += stecData.poly[4] * dLat * dLat; + stec += stecData.poly[5] * dLon * dLon; + } + } + stecData.poly.clear(); + } + + for (auto& [ind, stec] : stecData.grid) + { + if (stec < -9000) + continue; + + double dLat = abs(pos.latDeg() - regData.gridLatDeg[ind]) / regData.intLatDeg; + double dLon = abs(recLonDeg - regData.gridLonDeg[ind]) / regData.intLonDeg; + + if (dLat >= 1) + continue; + if (dLon >= 1) + continue; + + // tracepdeex(2, std::cout,"Iono grid %s %2d, %2d: %.4f %.4f, %.4f\n", + // Sat.id().c_str(), regID, ind, dLat, dLon, stec); + double coef = (1 - dLat) * (1 - dLon); + + acmVar[64 * regID + ind] = SQR(coef * stecData.sigma); + numer[64 * regID + ind] = coef * stec; + denom += coef; + } + } + + if (numer.size() <= 0) + return false; + + for (auto [ind, val] : numer) + { + iono += val; + var += acmVar[ind]; + } + iono /= denom; + var /= SQR(denom); + + return true; } diff --git a/src/cpp/iono/ionoMeas.cpp b/src/cpp/iono/ionoMeas.cpp index 2133cd7a8..7312d1d9b 100644 --- a/src/cpp/iono/ionoMeas.cpp +++ b/src/cpp/iono/ionoMeas.cpp @@ -1,35 +1,38 @@ - // #pragma GCC optimize ("O0") -#include "observations.hpp" -#include "coordinates.hpp" -#include "ionoModel.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "ionModels.hpp" -#include "receiver.hpp" -#include "satStat.hpp" -#include "common.hpp" -#include "trace.hpp" -#include "enums.h" - -#define PHASE_BIAS_STD 0.05 -bool ionoConfigured = false; - -bool ippInRange( - GTime time, - VectorPos& ionPP) +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/ionModels.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/satStat.hpp" +#include "common/trace.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/coordinates.hpp" + +constexpr double PHASE_BIAS_STD = 0.05; +bool ionoConfigured = false; + +bool ippInRange(GTime time, VectorPos& ionPP) { - switch (acsConfig.ionModelOpts.model) - { - case E_IonoModel::MEAS_OUT: return true; - case E_IonoModel::SPHERICAL_HARMONICS: return ippCheckSphhar(time, ionPP); - case E_IonoModel::SPHERICAL_CAPS: return ippCheckSphcap(time, ionPP); - case E_IonoModel::BSPLINE: return ippCheckBsplin(time, ionPP); - case E_IonoModel::LOCAL: return ippCheckLocal (time, ionPP); - case E_IonoModel::NONE: return false; - } - return false; + switch (acsConfig.ionModelOpts.model) + { + case E_IonoModel::MEAS_OUT: + return true; + case E_IonoModel::SPHERICAL_HARMONICS: + return ippCheckSphhar(time, ionPP); + case E_IonoModel::SPHERICAL_CAPS: + return ippCheckSphcap(time, ionPP); + case E_IonoModel::BSPLINE: + return ippCheckBsplin(time, ionPP); + case E_IonoModel::LOCAL: + return ippCheckLocal(time, ionPP); + case E_IonoModel::NONE: + return false; + } + return false; } /*----------------------------------------------------------------------------------------------*/ @@ -55,302 +58,337 @@ bool ippInRange( /* - obs.STECsmth Smoothed Ionosphere measurement */ /* - obs.STECsmvr Variance of Smoothed Ionosphere measurement */ -void obsIonoData( - Trace& trace, - Receiver& rec) +void obsIonoData(Trace& trace, Receiver& rec) { - if (ionoConfigured == false) - ionoConfigured = configIonModel(trace); - - if (ionoConfigured == false) - return; - - if (rec.aprioriPos.isZero()) - return; - - tracepdeex(4, trace, "\n---------------------- Ionospheric delay measurments -----------------------------\n"); - tracepdeex(4, trace, "ION_MEAS sat tow RawGF meas RawGF std. GF_code_mea GF_phas_mea GF to TECu\n"); - - auto& recOpts = acsConfig.getRecOpts(rec.id); - - for (auto& obs : only(rec.obsList)) - if (obs.satNav_ptr) - if (obs.satStat_ptr) - { - SatNav& satNav = *obs.satNav_ptr; - SatStat& satStat = *obs.satStat_ptr; - - obs.stecVar = SQR(1e5); - - if ( satStat.el < recOpts.elevation_mask_deg * D2R - ||obs.exclude) - { - obs.ionExcludeElevation = 1; - continue; - } - - E_FType frq1; - E_FType frq2; - E_FType frq3; - if (!satFreqs(obs.Sat.sys, frq1, frq2, frq3)) - continue; - - S_LC lc = getLC(obs, obs.satStat_ptr->lc_new, frq1, frq2); - S_LC lc_pre = getLC(obs, obs.satStat_ptr->lc_pre, frq1, frq2); - - if (lc.valid == false) - { - obs.ionExcludeLC = 1; - continue; - } - - /* Setting STEC to Delay factor */ - obs.stecToDelay = STEC2DELAY * (SQR(satNav.lamMap[frq2]) - SQR(satNav.lamMap[frq1])); - - /* setting ionospheric piercing point data */ - VectorPos pos = ecef2pos(rec.aprioriPos); - - VectorPos posp; - - for (int j = 0; j < acsConfig.ionModelOpts.layer_heights.size(); j++) - { - obs.ippMap[j].slantFactor = ionppp(pos, satStat, RE_WGS84 / 1000, acsConfig.ionModelOpts.layer_heights[j] / 1000, posp); - - tracepdeex(5,trace,"IPP_verif %s %8.3f %8.3f %8.3f %8.3f ", obs.Sat.id().c_str(), - pos. latDeg(), - pos. lonDeg(), - posp.latDeg(), - posp.lonDeg()); - - if (ippInRange(obs.time, posp) == false) - { - obs.ionExcludeRange = 1; - break; - } - - obs.ippMap[j].latDeg = posp.latDeg(); - obs.ippMap[j].lonDeg = posp.lonDeg(); - tracepdeex(5,trace," %8.3f %8.3f\n", posp.latDeg(), posp.lonDeg()); - } - - if (obs.ionExclude) - continue; - - if (fabs((satStat.lastObsTime - obs.time).to_double()) > 300) - { - satStat.ambvar = 0; - } - satStat.lastObsTime = obs.time; - - double varL = obs.sigs.begin()->second.phasVar; - double varP = obs.sigs.begin()->second.codeVar; - - double amb = - (lc.GF_Phas_m + lc.GF_Code_m); - double oldSTEC = satStat.prevSTEC; - - if ( fabs(lc.GF_Phas_m - lc_pre.GF_Phas_m) > 0.05 /* Basic cycle slip detection */ - ||fabs(lc.GF_Phas_m - oldSTEC) > 0.05 - ||satStat.ambvar <= 0) - { - satStat.gf_amb = amb; - satStat.ambvar = varP; /* 1.0001*varP; */ - } - else - { - double SmtG = satStat.ambvar / (varP + satStat.ambvar); - satStat.gf_amb += SmtG * (amb - satStat.gf_amb); - satStat.ambvar = SmtG * (varP); - } - obs.stecType = 1; //todo aaron 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); - - obs.stecCodeCombo = obs.sigs[frq1].code._to_integral() * 100 - + obs.sigs[frq2].code._to_integral(); - - satStat.prevSTEC = lc.GF_Phas_m; - - GTow tow = obs.time; - tracepdeex(4,trace,"ION_MEAS %s %8.0f %10.4f %10.3e %10.4f %10.4f %10.4f\n", - obs.Sat.id().c_str(), - tow, - obs.stecVal, - obs.stecVar, - obs.sigs[frq2].P - obs.sigs[frq1].P, - obs.sigs[frq1].L * satNav.lamMap[frq1] - obs.sigs[frq2].L * satNav.lamMap[frq2], - obs.stecToDelay); - } + if (ionoConfigured == false) + ionoConfigured = configIonModel(trace); + + if (ionoConfigured == false) + return; + + if (rec.aprioriPos.isZero()) + return; + + tracepdeex( + 4, + trace, + "\n---------------------- Ionospheric delay measurments -----------------------------\n" + ); + tracepdeex( + 4, + trace, + "ION_MEAS sat tow RawGF meas RawGF std. GF_code_mea GF_phas_mea GF to TECu\n" + ); + + auto& recOpts = acsConfig.getRecOpts(rec.id); + + for (auto& obs : only(rec.obsList)) + if (obs.satNav_ptr) + if (obs.satStat_ptr) + { + SatNav& satNav = *obs.satNav_ptr; + SatStat& satStat = *obs.satStat_ptr; + + obs.stecVar = SQR(1e5); + + if (satStat.el < recOpts.elevation_mask_deg * D2R || obs.exclude) + { + obs.ionExcludeElevation = 1; + continue; + } + + E_FType frq1; + E_FType frq2; + E_FType frq3; + if (!satFreqs(obs.Sat.sys, frq1, frq2, frq3)) + continue; + + S_LC lc = getLC(obs, obs.satStat_ptr->lc_new, frq1, frq2); + S_LC lc_pre = getLC(obs, obs.satStat_ptr->lc_pre, frq1, frq2); + + if (lc.valid == false) + { + obs.ionExcludeLC = 1; + continue; + } + + /* Setting STEC to Delay factor */ + obs.stecToDelay = + STEC2DELAY * (SQR(satNav.lamMap[frq2]) - SQR(satNav.lamMap[frq1])); + + /* setting ionospheric piercing point data */ + VectorPos pos = ecef2pos(rec.aprioriPos); + + VectorPos posp; + + for (int j = 0; j < acsConfig.ionModelOpts.layer_heights.size(); j++) + { + obs.ippMap[j].slantFactor = ionppp( + pos, + satStat, + RE_WGS84 / 1000, + acsConfig.ionModelOpts.layer_heights[j] / 1000, + posp + ); + + tracepdeex( + 5, + trace, + "IPP_verif %s %8.3f %8.3f %8.3f %8.3f ", + obs.Sat.id().c_str(), + pos.latDeg(), + pos.lonDeg(), + posp.latDeg(), + posp.lonDeg() + ); + + if (ippInRange(obs.time, posp) == false) + { + obs.ionExcludeRange = 1; + break; + } + + obs.ippMap[j].latDeg = posp.latDeg(); + obs.ippMap[j].lonDeg = posp.lonDeg(); + tracepdeex(5, trace, " %8.3f %8.3f\n", posp.latDeg(), posp.lonDeg()); + } + + if (obs.ionExclude) + continue; + + if (fabs((satStat.lastObsTime - obs.time).to_double()) > 300) + { + satStat.ambvar = 0; + } + satStat.lastObsTime = obs.time; + + double varL = obs.sigs.begin()->second.phasVar; + double varP = obs.sigs.begin()->second.codeVar; + + double amb = -(lc.GF_Phas_m + lc.GF_Code_m); + double oldSTEC = satStat.prevSTEC; + + if (fabs(lc.GF_Phas_m - lc_pre.GF_Phas_m) > 0.05 /* Basic cycle slip detection */ + || fabs(lc.GF_Phas_m - oldSTEC) > 0.05 || satStat.ambvar <= 0) + { + satStat.gf_amb = amb; + satStat.ambvar = varP; /* 1.0001*varP; */ + } + else + { + double SmtG = satStat.ambvar / (varP + satStat.ambvar); + satStat.gf_amb += SmtG * (amb - satStat.gf_amb); + satStat.ambvar = SmtG * (varP); + } + obs.stecType = 1; // todo aaron 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); + + obs.stecCodeCombo = static_cast(obs.sigs[frq1].code) * 100 + + static_cast(obs.sigs[frq2].code); + + satStat.prevSTEC = lc.GF_Phas_m; + + GTow tow = obs.time; + tracepdeex( + 4, + trace, + "ION_MEAS %s %8.0f %10.4f %10.3e %10.4f %10.4f %10.4f\n", + obs.Sat.id().c_str(), + tow, + obs.stecVal, + obs.stecVar, + obs.sigs[frq2].P - obs.sigs[frq1].P, + obs.sigs[frq1].L * satNav.lamMap[frq1] - obs.sigs[frq2].L * satNav.lamMap[frq2], + obs.stecToDelay + ); + } } -void writeIonStec( - string filename, - KFState& kfState) +void writeIonStec(string filename, KFState& kfState) { - GWeek week = kfState.time; - GTow tow = kfState.time; - - std::ofstream stecfile(filename, std::ofstream::app); - if (!stecfile) - { - return; - } - - tracepdeex(0, stecfile, "#TYP_MEA,%4s,%10s,%4s,%3s,%10s,%10s,%1s,%1s,%13s,%13s,%13s,%4s,%13s,%13s,%13s", - "week", - "tow", - "site", - "sat", - "ion_meas", - "ion_var", - "s", - "l", - "Sat ECEF X", - "Sat ECEF Y", - "Sat ECEF Z", - "com", - "Rec ECEF X", - "Rec ECEF Y", - "Rec ECEF Z"); - - int nlayer = acsConfig.ionModelOpts.layer_heights.size(); - - for (int i = 0; i < nlayer; i++) - { - tracepdeex(0, stecfile,",%8s,%8s,%8s,%8s", - "height", - "ipplat", - "ipplon", - "slant_f"); - } - - tracepdeex(0, stecfile,"\n"); - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::IONO_STEC) - continue; - - double stecVal = 0; - double stecVar = 0; - - bool pass = kfState.getKFValue(key, stecVal, &stecVar); - - if (pass == false) - continue; - - GObs* obs_ptr = nullptr; - - if (key.rec_ptr) - { - auto& rec = *key.rec_ptr; - - for (auto& obs : only(rec.obsList)) - { - if (key.Sat != obs.Sat) - { - continue; - } - - //found the observation for this ionosphere - obs_ptr = &obs; - } - } - - tracepdeex(0 ,stecfile, "IONO_MEA,%4d,%10.3f,%4s,%3s,%10.4f,%10.4e", - week, - tow, - key.str.c_str(), - key.Sat.id().c_str(), - stecVal, - stecVar); - - if (obs_ptr) - { - auto& obs = *obs_ptr; - - tracepdeex(0, stecfile,",%1d,%1d,%13.3f,%13.3f,%13.3f,%4d", - obs.stecType, - nlayer, - obs.rSatCom[0], - obs.rSatCom[1], - obs.rSatCom[2], - obs.stecCodeCombo); - } - else - { - tracepdeex(0, stecfile,",%1s,%1s,%13s,%13s,%13s,%4s", - "", - "", - "", - "", - "", - ""); - } - - if (key.rec_ptr) - { - auto& rec = *key.rec_ptr; - - tracepdeex(0, stecfile,",%13.3f,%13.3f,%13.3f", - rec.aprioriPos[0], - rec.aprioriPos[1], - rec.aprioriPos[2]); - } - else - { - tracepdeex(0, stecfile,",%13s,%13s,%13s", - "", - "", - ""); - } - - if (obs_ptr) - for (int j = 0; j < nlayer; j++) - { - auto& obs = *obs_ptr; - - tracepdeex(0, stecfile, ",%8.0f,%8.3f,%8.3f,%8.3f", - acsConfig.ionModelOpts.layer_heights[j] / 1000, - obs.ippMap[j].latDeg, - obs.ippMap[j].lonDeg, - obs.ippMap[j].slantFactor); - } - - tracepdeex(0, stecfile, "\n"); - } + GWeek week = kfState.time; + GTow tow = kfState.time; + + std::ofstream stecfile(filename, std::ofstream::app); + if (!stecfile) + { + return; + } + + tracepdeex( + 0, + stecfile, + "#TYP_MEA,%4s,%10s,%4s,%3s,%10s,%10s,%1s,%1s,%13s,%13s,%13s,%4s,%13s,%13s,%13s", + "week", + "tow", + "site", + "sat", + "ion_meas", + "ion_var", + "s", + "l", + "Sat ECEF X", + "Sat ECEF Y", + "Sat ECEF Z", + "com", + "Rec ECEF X", + "Rec ECEF Y", + "Rec ECEF Z" + ); + + int nlayer = acsConfig.ionModelOpts.layer_heights.size(); + + for (int i = 0; i < nlayer; i++) + { + tracepdeex(0, stecfile, ",%8s,%8s,%8s,%8s", "height", "ipplat", "ipplon", "slant_f"); + } + + tracepdeex(0, stecfile, "\n"); + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::IONO_STEC) + continue; + + double stecVal = 0; + double stecVar = 0; + + E_Source pass = kfState.getKFValue(key, stecVal, &stecVar); + + if (pass == E_Source::NONE) + continue; + + GObs* obs_ptr = nullptr; + + if (key.rec_ptr) + { + auto& rec = *key.rec_ptr; + + for (auto& obs : only(rec.obsList)) + { + if (key.Sat != obs.Sat) + { + continue; + } + + // found the observation for this ionosphere + obs_ptr = &obs; + } + } + + tracepdeex( + 0, + stecfile, + "IONO_MEA,%4d,%10.3f,%4s,%3s,%10.4f,%10.4e", + week, + tow, + key.str.c_str(), + key.Sat.id().c_str(), + stecVal, + stecVar + ); + + if (obs_ptr) + { + auto& obs = *obs_ptr; + + tracepdeex( + 0, + stecfile, + ",%1d,%1d,%13.3f,%13.3f,%13.3f,%4d", + obs.stecType, + nlayer, + obs.rSatCom[0], + obs.rSatCom[1], + obs.rSatCom[2], + obs.stecCodeCombo + ); + } + else + { + tracepdeex(0, stecfile, ",%1s,%1s,%13s,%13s,%13s,%4s", "", "", "", "", "", ""); + } + + if (key.rec_ptr) + { + auto& rec = *key.rec_ptr; + + tracepdeex( + 0, + stecfile, + ",%13.3f,%13.3f,%13.3f", + rec.aprioriPos[0], + rec.aprioriPos[1], + rec.aprioriPos[2] + ); + } + else + { + tracepdeex(0, stecfile, ",%13s,%13s,%13s", "", "", ""); + } + + if (obs_ptr) + for (int j = 0; j < nlayer; j++) + { + auto& obs = *obs_ptr; + + tracepdeex( + 0, + stecfile, + ",%8.0f,%8.3f,%8.3f,%8.3f", + acsConfig.ionModelOpts.layer_heights[j] / 1000, + obs.ippMap[j].latDeg, + obs.ippMap[j].lonDeg, + obs.ippMap[j].slantFactor + ); + } + + tracepdeex(0, stecfile, "\n"); + } } void obsIonoDataFromFilter( - Trace& trace, ///< debug trace - ReceiverMap& receiverMap, ///< List of stations containing observations for this epoch - KFState& measKFstate) ///< Kalman filter object containing the ionosphere estimates + Trace& trace, ///< debug trace + ReceiverMap& receiverMap, ///< List of stations containing observations for this epoch + KFState& measKFstate ///< Kalman filter object containing the ionosphere estimates +) { - tracepdeex(3, trace,"\n%s %s\n", __FUNCTION__, measKFstate.time.to_string().c_str()); - - for (auto& [id, rec] : receiverMap) - for (auto& obs : only(rec.obsList)) - { - if (obs.ionExclude) - continue; - - KFKey kfKey; - kfKey.type = KF::IONO_STEC; - kfKey.str = obs.mount; - kfKey.Sat = obs.Sat; - - double stecVal = 0; - double stecVar = 0; - bool pass = measKFstate.getKFValue(kfKey, stecVal, &stecVar); - - if (pass == false) - { - obs.ionExclude = 1; - continue; - } - - tracepdeex(4, trace, " sTEC for %s %s found: %.4f -> %.4f\n", obs.Sat.id().c_str(), obs.mount.c_str(), obs.stecVal, stecVal); - obs.stecVal = stecVal; - obs.stecVar = stecVar; - obs.stecType = 3; - } + tracepdeex(3, trace, "\n%s %s\n", __FUNCTION__, measKFstate.time.to_string().c_str()); + + for (auto& [id, rec] : receiverMap) + for (auto& obs : only(rec.obsList)) + { + if (obs.ionExclude) + continue; + + KFKey kfKey; + kfKey.type = KF::IONO_STEC; + kfKey.str = obs.mount; + kfKey.Sat = obs.Sat; + + double stecVal = 0; + double stecVar = 0; + E_Source pass = measKFstate.getKFValue(kfKey, stecVal, &stecVar); + + if (pass == E_Source::NONE) + { + obs.ionExclude = 1; + continue; + } + + tracepdeex( + 4, + trace, + " STEC for %s %s found: %.4f -> %.4f\n", + obs.Sat.id().c_str(), + obs.mount.c_str(), + obs.stecVal, + stecVal + ); + obs.stecVal = stecVal; + obs.stecVar = stecVar; + obs.stecType = 3; + } } diff --git a/src/cpp/iono/ionoModel.cpp b/src/cpp/iono/ionoModel.cpp index 46c5f7c33..66ac3a38a 100644 --- a/src/cpp/iono/ionoModel.cpp +++ b/src/cpp/iono/ionoModel.cpp @@ -1,72 +1,76 @@ - // #pragma GCC optimize ("O0") -#include +#include "iono/ionoModel.hpp" #include +#include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/enums.h" +#include "common/receiver.hpp" -using std::string; using std::map; - -#include "ionoModel.hpp" -#include "testUtils.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "enums.h" - +using std::string; /* Global parameters */ -map ionRefRec; -map ionStateOutage; +map ionRefRec; +map ionStateOutage; -#define INIT_VAR_RDCB 100.0 -#define INIT_VAR_SDCB 100.0 -#define INIT_VAR_SCHP 100.0 +constexpr double INIT_VAR_RDCB = 100.0; +constexpr double INIT_VAR_SDCB = 100.0; +constexpr double INIT_VAR_SCHP = 100.0; -void ionosphereSsrUpdate( - Trace& trace, - KFState& kfState) +void ionosphereSsrUpdate(Trace& trace, KFState& kfState) { - switch (acsConfig.ionModelOpts.model) - { - case E_IonoModel::SPHERICAL_HARMONICS: return ionOutputSphcal(trace, kfState); - case E_IonoModel::LOCAL: return ionOutputLocal (trace, kfState); - case E_IonoModel::MEAS_OUT: return; - case E_IonoModel::NONE: return; - } - - tracepdeex (5,trace, "Unsupported system for SSR output\n"); + switch (acsConfig.ionModelOpts.model) + { + case E_IonoModel::SPHERICAL_HARMONICS: + return ionOutputSphcal(trace, kfState); + case E_IonoModel::LOCAL: + return ionOutputLocal(trace, kfState); + case E_IonoModel::MEAS_OUT: + return; + case E_IonoModel::NONE: + return; + } + + tracepdeex(5, trace, "Unsupported system for SSR output\n"); } -bool configIonModel( - Trace& trace) +bool configIonModel(Trace& trace) { - switch (acsConfig.ionModelOpts.model) - { - case E_IonoModel::MEAS_OUT: return true; - case E_IonoModel::SPHERICAL_HARMONICS: return configIonModelSphhar(trace); - case E_IonoModel::SPHERICAL_CAPS: return configIonModelSphcap(trace); - case E_IonoModel::BSPLINE: return configIonModelBsplin(trace); - case E_IonoModel::LOCAL: return configIonModelLocal_(trace); - default: return false; - } + switch (acsConfig.ionModelOpts.model) + { + case E_IonoModel::MEAS_OUT: + return true; + case E_IonoModel::SPHERICAL_HARMONICS: + return configIonModelSphhar(trace); + case E_IonoModel::SPHERICAL_CAPS: + return configIonModelSphcap(trace); + case E_IonoModel::BSPLINE: + return configIonModelBsplin(trace); + case E_IonoModel::LOCAL: + return configIonModelLocal_(trace); + default: + return false; + } } -double ionModelCoef( - Trace& trace, - int ind, - IonoObs& obs, - bool slant) +double ionModelCoef(Trace& trace, int ind, IonoObs& obs, bool slant) { - switch (acsConfig.ionModelOpts.model) - { - case E_IonoModel::SPHERICAL_HARMONICS: return ionCoefSphhar(trace, ind, obs, slant); - case E_IonoModel::SPHERICAL_CAPS: return ionCoefSphcap(trace, ind, obs, slant); - case E_IonoModel::BSPLINE: return ionCoefBsplin(trace, ind, obs, slant); - case E_IonoModel::LOCAL: return ionCoefLocal (trace, ind, obs); - default: return 0; - } + switch (acsConfig.ionModelOpts.model) + { + case E_IonoModel::SPHERICAL_HARMONICS: + return ionCoefSphhar(trace, ind, obs, slant); + case E_IonoModel::SPHERICAL_CAPS: + return ionCoefSphcap(trace, ind, obs, slant); + case E_IonoModel::BSPLINE: + return ionCoefBsplin(trace, ind, obs, slant); + case E_IonoModel::LOCAL: + return ionCoefLocal(trace, ind, obs); + default: + return 0; + } } /** Updating the ionosphere model parameters @@ -74,329 +78,378 @@ double ionModelCoef( * Ionosphere measurments from stations should be loaded using 'update_station_measr' */ void filterIonosphere( - Trace& trace, ///< Trace to output to - KFState& kfState, ///< Filter state - ReceiverMap& receiverMap, ///< List of pointers to stations to use - GTime time) ///< Time of this epoch + Trace& trace, ///< Trace to output to + KFState& kfState, ///< Filter state + ReceiverMap& receiverMap, ///< List of pointers to stations to use + GTime time ///< Time of this epoch +) { - if (!ionoConfigured) - ionoConfigured = configIonModel(trace); - - if (!ionoConfigured) - return; - - if (acsConfig.ionModelOpts.model == +E_IonoModel::NONE) - return; - - if (acsConfig.ionModelOpts.model == +E_IonoModel::MEAS_OUT) - return; - - tracepdeex(2, trace,"UPDATE IONO MODEL ... %s\n", time.to_string().c_str()); - //count valid measurements for each station - map> stationList; - map satelliteList; - map maxCountRec; - map satCount; - - for (auto& [id, rec] : receiverMap) - { - map satcnt; - for (auto& obs : only(rec.obsList)) - { - if (obs.ionExclude) - continue; - - if (acsConfig.use_for_iono_model[obs.Sat.sys] == false) - continue; - - satcnt[obs.Sat.sys]++; - satelliteList[obs.Sat]++; - } - - for (auto& [sys, nsat] : satcnt) - { - if (nsat < MIN_NSAT_REC) - continue; - - stationList[rec.id][sys] += nsat; - - if (rec.id == acsConfig.pivot_receiver) nsat = 999; - if (rec.id == ionRefRec[sys]) nsat = 9999; - - if (satCount[sys] < nsat) - { - satCount [sys] = nsat; - maxCountRec [sys] = rec.id; - } - } - } - - int nSatTot = 0; - int nRecTot = 0; - int nMeasTot = 0; - - for (auto& [rec, list] : stationList) - nRecTot += list.size(); - - for (auto& [sat, nRec] : satelliteList) - { - nSatTot++; - nMeasTot += nRec; - } - - int nStateTot = acsConfig.ionModelOpts.numBasis + nSatTot + nRecTot; - if (acsConfig.ionModelOpts.model == +E_IonoModel::LOCAL) - { - nStateTot = 0; - for (auto& [regId,regData] : nav.ssrAtm.atmosRegionsMap) - { - if (regData.ionoGrid) nStateTot += nSatTot * (regData.gridLatDeg.size() - 1); - else if (regData.ionoPolySize > 0) nStateTot += nSatTot * (regData.ionoPolySize - 1); - } - } - - if (nMeasTot < nStateTot) - { - tracepdeex(2, trace,"#IONO_MOD Not enough Measurements %5d < %4d; %3d, %3d\n", nMeasTot, nStateTot, nSatTot, nRecTot); - return; - } - - map reset_DCBs; - for (auto& [sys, nsat] : satCount) - { - reset_DCBs[sys] = false; - - if (nsat < MIN_NSAT_REC) - continue; - - if (maxCountRec[sys] != ionRefRec[sys]) - { - tracepdeex(2, trace,"#IONO_MOD WARNING change in reference station for %s: %s\n", sys._to_string(), maxCountRec[sys]); - reset_DCBs[sys] = true; - } - - ionRefRec[sys] = maxCountRec[sys]; - tracepdeex(4, trace,"#IONO_MOD REF STATION for %s: %s\n", sys._to_string(), maxCountRec[sys]); - } - - map mainObsCombo; - for (auto& [sys,pivt] : ionRefRec) - for (auto& obs : only(receiverMap[pivt].obsList)) - { - if (obs.Sat.sys != sys) continue; - - mainObsCombo[sys] = obs.stecCodeCombo; - break; - } - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::IONOSPHERIC) - continue; - - if (ionStateOutage[index]++ > 3) - kfState.removeState(key); - } - - //add measurements and create design matrix entries - KFMeasEntryList kfMeasEntryList; - - for (auto& [id, rec] : receiverMap) - for (auto& obs : only(rec.obsList)) - { - E_Sys sys = obs.Sat.sys; - - auto& recOpts = acsConfig.getRecOpts(id); - auto& satOpts = acsConfig.getSatOpts(obs.Sat); - - if (obs.ionExclude) { continue; } - if (obs.stecType <= 0) { continue; } - if (stationList[rec.id][sys] < MIN_NSAT_REC) { continue; } - if (obs.stecVar > SQR(recOpts.iono_sigma_limit)) { continue; } - - /************ Ionosphere Measurements ************/ - KFKey obsKey; - obsKey.Sat = obs.Sat; - obsKey.str = rec.id; - - KFMeasEntry meas(&kfState, obsKey); - meas.setValue(obs.stecVal); - meas.setNoise(obs.stecVar); - - /************ receiver DCB ************/ /* We may need to change this for multi-code solutions */ - SatSys sat0; - sat0.sys = sys; - sat0.prn = 0; - - KFKey recDCBKey; - recDCBKey.type = KF::CODE_BIAS; - recDCBKey.str = rec.id; - recDCBKey.Sat = sat0; - recDCBKey.num = obs.stecCodeCombo; - - if (reset_DCBs[sys]) - kfState.removeState(recDCBKey); - - InitialState init = initialStateFromConfig(recOpts.code_bias); - if (rec.id != ionRefRec[sys]) - 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.. - || mainObsCombo[sys] != obs.stecCodeCombo) - { - InitialState init = initialStateFromConfig(satOpts.code_bias); - - KFKey satDCBKey; - satDCBKey.type = KF::CODE_BIAS; - satDCBKey.Sat = obs.Sat; - satDCBKey.num = obs.stecCodeCombo; - - meas.addDsgnEntry(satDCBKey, 1, init); - } - - /************ Ionosphere basis ************/ - obs.ionoSat = obs.Sat; - for (int i = 0; i < acsConfig.ionModelOpts.numBasis; i++) - { - double coef = ionModelCoef(trace,i, obs, true); - - if (coef == 0) - continue; - - ionStateOutage[i] = 0; - - KFKey ionModelKey; - ionModelKey.type = KF::IONOSPHERIC; - ionModelKey.num = i; - - InitialState ionModelInit = initialStateFromConfig(acsConfig.ionModelOpts.ion); - - meas.addDsgnEntry(ionModelKey, coef, ionModelInit); - - tracepdeex(4, trace,"#IONO_MOD %s %4d %9.5f %10.5f %8.5f %8.5f %12.5e %9.5f %12.5e\n", - ((string)meas.obsKey).c_str(), - i, - obs.ippMap[0].latDeg, - obs.ippMap[0].lonDeg, - obs.ippMap[0].slantFactor, - obs.stecToDelay, - coef, - obs.stecVal, - obs.stecVar); - } - - kfMeasEntryList.push_back(meas); - } - - //add process noise to existing states as per their initialisations. - kfState.stateTransition(trace, time); - - //combine the measurement list into a single design matrix, measurement vector, and measurement noise vector - KFMeas kfMeas(kfState, kfMeasEntryList); - - //if there are uninitialised state values, estimate them using least squares - if (kfState.lsqRequired) - { - trace << "\n" << "-------INITIALISING IONO USING LEAST SQUARES--------" << "\n"; - - kfState.leastSquareInitStates(std::cout, kfMeas, true); - } - else - { - trace << "\n" << "------- DOING IONO KALMAN FILTER --------" << "\n"; - - kfState.filterKalman(trace, kfMeas, "/IONO", false); - } - - kfState.outputStates(trace, "/ION"); + if (!ionoConfigured) + ionoConfigured = configIonModel(trace); + + if (!ionoConfigured) + return; + + if (acsConfig.ionModelOpts.model == E_IonoModel::NONE) + return; + + if (acsConfig.ionModelOpts.model == E_IonoModel::MEAS_OUT) + return; + + tracepdeex(2, trace, "UPDATE IONO MODEL ... %s\n", time.to_string().c_str()); + // count valid measurements for each station + map> stationList; + map satelliteList; + map maxCountRec; + map satCount; + + for (auto& [id, rec] : receiverMap) + { + map satcnt; + for (auto& obs : only(rec.obsList)) + { + if (obs.ionExclude) + continue; + + if (acsConfig.use_for_iono_model[obs.Sat.sys] == false) + continue; + + satcnt[obs.Sat.sys]++; + satelliteList[obs.Sat]++; + } + + for (auto& [sys, nsat] : satcnt) + { + if (nsat < MIN_NSAT_REC) + continue; + + stationList[rec.id][sys] += nsat; + + if (rec.id == acsConfig.pivot_receiver) + nsat = 999; + if (rec.id == ionRefRec[sys]) + nsat = 9999; + + if (satCount[sys] < nsat) + { + satCount[sys] = nsat; + maxCountRec[sys] = rec.id; + } + } + } + + int nSatTot = 0; + int nRecTot = 0; + int nMeasTot = 0; + + for (auto& [rec, list] : stationList) + nRecTot += list.size(); + + for (auto& [sat, nRec] : satelliteList) + { + nSatTot++; + nMeasTot += nRec; + } + + int nStateTot = acsConfig.ionModelOpts.numBasis + nSatTot + nRecTot; + if (acsConfig.ionModelOpts.model == E_IonoModel::LOCAL) + { + nStateTot = 0; + for (auto& [regId, regData] : nav.ssrAtm.atmosRegionsMap) + { + if (regData.ionoGrid) + nStateTot += nSatTot * (regData.gridLatDeg.size() - 1); + else if (regData.ionoPolySize > 0) + nStateTot += nSatTot * (regData.ionoPolySize - 1); + } + } + + if (nMeasTot < nStateTot) + { + tracepdeex( + 2, + trace, + "#IONO_MOD Not enough Measurements %5d < %4d; %3d, %3d\n", + nMeasTot, + nStateTot, + nSatTot, + nRecTot + ); + return; + } + + map reset_DCBs; + for (auto& [sys, nsat] : satCount) + { + reset_DCBs[sys] = false; + + if (nsat < MIN_NSAT_REC) + continue; + + if (maxCountRec[sys] != ionRefRec[sys]) + { + tracepdeex( + 2, + trace, + "#IONO_MOD WARNING change in reference station for %s: %s\n", + enum_to_string(sys), + maxCountRec[sys] + ); + reset_DCBs[sys] = true; + } + + ionRefRec[sys] = maxCountRec[sys]; + tracepdeex( + 4, + trace, + "#IONO_MOD REF STATION for %s: %s\n", + enum_to_string(sys), + maxCountRec[sys] + ); + } + + map mainObsCombo; + for (auto& [sys, pivt] : ionRefRec) + for (auto& obs : only(receiverMap[pivt].obsList)) + { + if (obs.Sat.sys != sys) + continue; + + mainObsCombo[sys] = obs.stecCodeCombo; + break; + } + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::IONOSPHERIC) + continue; + + if (ionStateOutage[index]++ > 3) + kfState.removeState(key); + } + + // add measurements and create design matrix entries + KFMeasEntryList kfMeasEntryList; + + for (auto& [id, rec] : receiverMap) + for (auto& obs : only(rec.obsList)) + { + E_Sys sys = obs.Sat.sys; + + auto& recOpts = acsConfig.getRecOpts(id); + auto& satOpts = acsConfig.getSatOpts(obs.Sat); + + if (obs.ionExclude) + { + continue; + } + if (obs.stecType <= 0) + { + continue; + } + if (stationList[rec.id][sys] < MIN_NSAT_REC) + { + continue; + } + + /************ Ionosphere Measurements ************/ + KFKey obsKey; + obsKey.Sat = obs.Sat; + obsKey.str = rec.id; + + KFMeasEntry meas(&kfState, obsKey); + meas.setValue(obs.stecVal); + meas.setNoise(obs.stecVar); + + /************ receiver DCB ************/ /* We may need to change this for multi-code + solutions */ + SatSys sat0; + sat0.sys = sys; + sat0.prn = 0; + + KFKey recDCBKey; + recDCBKey.type = KF::CODE_BIAS; + recDCBKey.str = rec.id; + recDCBKey.Sat = sat0; + recDCBKey.num = obs.stecCodeCombo; + + if (reset_DCBs[sys]) + kfState.removeState(recDCBKey); + + InitialState init = initialStateFromConfig(recOpts.code_bias); + if (rec.id != ionRefRec[sys]) + 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.. + || mainObsCombo[sys] != obs.stecCodeCombo) + { + InitialState init = initialStateFromConfig(satOpts.code_bias); + + KFKey satDCBKey; + satDCBKey.type = KF::CODE_BIAS; + satDCBKey.Sat = obs.Sat; + satDCBKey.num = obs.stecCodeCombo; + + meas.addDsgnEntry(satDCBKey, 1, init); + } + + /************ Ionosphere basis ************/ + obs.ionoSat = obs.Sat; + for (int i = 0; i < acsConfig.ionModelOpts.numBasis; i++) + { + double coef = ionModelCoef(trace, i, obs, true); + + if (coef == 0) + continue; + + ionStateOutage[i] = 0; + + KFKey ionModelKey; + ionModelKey.type = KF::IONOSPHERIC; + ionModelKey.num = i; + + InitialState ionModelInit = initialStateFromConfig(acsConfig.ionModelOpts.ion); + + meas.addDsgnEntry(ionModelKey, coef, ionModelInit); + + tracepdeex( + 4, + trace, + "#IONO_MOD %s %4d %9.5f %10.5f %8.5f %8.5f %12.5e %9.5f %12.5e\n", + ((string)meas.obsKey).c_str(), + i, + obs.ippMap[0].latDeg, + obs.ippMap[0].lonDeg, + obs.ippMap[0].slantFactor, + obs.stecToDelay, + coef, + obs.stecVal, + obs.stecVar + ); + } + + kfMeasEntryList.push_back(meas); + } + + // add process noise to existing states as per their initialisations. + kfState.stateTransition(trace, time); + + // combine the measurement list into a single design matrix, measurement vector, and measurement + // noise vector + KFMeas kfMeas(kfState, kfMeasEntryList); + + string suffix = "/IONO"; + // if there are uninitialised state values, estimate them using least squares + if (kfState.lsqRequired) + { + trace << "\n" + << "-------INITIALISING IONO USING LEAST SQUARES--------" << "\n"; + + kfState.leastSquareInitStates(std::cout, kfMeas, suffix, true); + } + else + { + trace << "\n" + << "------- DOING IONO KALMAN FILTER --------" << "\n"; + + kfState.filterKalman(trace, kfMeas, suffix, false); + } + + kfState.outputStates(trace, suffix); } - double getSSRIono( - Trace& trace, ///< Debug trace - GTime time, ///< time of ionosphere correction - Vector3d& rRec, ///< receiver position - AzEl& azel, ///< satellite azimut/elevation - double& variance, ///< Ionosphere variance - SatSys& Sat) ///< Satellite + Trace& trace, ///< Debug trace + GTime time, ///< time of ionosphere correction + Vector3d& rRec, ///< receiver position + AzEl& azel, ///< satellite azimut/elevation + double& variance, ///< Ionosphere variance + SatSys& Sat ///< Satellite +) { - double ionoDelay = 0; + double ionoDelay = 0; - if (getCmpSSRIono(trace, time, nav.ssrAtm, rRec, ionoDelay, variance, Sat)) return ionoDelay; - if (getIGSSSRIono(trace, time, nav.ssrAtm, rRec, azel, ionoDelay, variance)) return ionoDelay; + if (getCmpSSRIono(trace, time, nav.ssrAtm, rRec, ionoDelay, variance, Sat)) + return ionoDelay; + if (getIGSSSRIono(trace, time, nav.ssrAtm, rRec, azel, ionoDelay, variance)) + return ionoDelay; - variance = -1; + variance = -1; - return 0; + return 0; } - -map galSigGroups = -{ - {E_ObsCode::L1C, 1}, - {E_ObsCode::L1X, 2}, - {E_ObsCode::L5Q, 1}, - {E_ObsCode::L5X, 2}, - {E_ObsCode::L7Q, 1}, - {E_ObsCode::L7X, 2}, - {E_ObsCode::L8Q, 1}, - {E_ObsCode::L8X, 2}, - {E_ObsCode::L6C, 1}, - {E_ObsCode::L6X, 2} +map galSigGroups = { + {E_ObsCode::L1C, 1}, + {E_ObsCode::L1X, 2}, + {E_ObsCode::L5Q, 1}, + {E_ObsCode::L5X, 2}, + {E_ObsCode::L7Q, 1}, + {E_ObsCode::L7X, 2}, + {E_ObsCode::L8Q, 1}, + {E_ObsCode::L8X, 2}, + {E_ObsCode::L6C, 1}, + {E_ObsCode::L6X, 2} }; bool galCodeMatch( - int keyNum, ///< key number for DCB state - E_ObsCode code) ///< signal code + int keyNum, ///< key number for DCB state + E_ObsCode code ///< signal code +) { - if (galSigGroups.find(code) == galSigGroups.end()) - return false; + if (galSigGroups.find(code) == galSigGroups.end()) + return false; - E_ObsCode code2 = E_ObsCode::_from_integral(keyNum/100); - if (galSigGroups.find(code2) == galSigGroups.end()) - return false; + E_ObsCode code2 = int_to_enum(keyNum / 100); + if (galSigGroups.find(code2) == galSigGroups.end()) + return false; - return galSigGroups[code] == galSigGroups[code2]; + return galSigGroups[code] == galSigGroups[code2]; } /** Estimate biases from Ionosphere modelling DCBs */ bool queryBiasDCB( - Trace& trace, ///< debug trace - KFState& kfState, ///< Kalman filter to take biases from - SatSys Sat, ///< GNSS Satellite - string Rec, ///< Receiver id - E_ObsCode code, ///< GNSS signal code - double& bias, ///< Output bias value - double& var) ///< Output bias variance + Trace& trace, ///< debug trace + KFState& kfState, ///< Kalman filter to take biases from + SatSys Sat, ///< GNSS Satellite + string Rec, ///< Receiver id + E_ObsCode code, ///< GNSS signal code + double& bias, ///< Output bias value + double& var ///< Output bias variance +) { - E_FType ftyp = code2Freq[Sat.sys][code]; - double lamb = nav.satNavMap[Sat].lamMap[ftyp]; - if (lamb == 0) - return false; - - double dcbVal; - double dcbVar; - - bool pass = false; - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != +KF::CODE_BIAS) continue; - if (key.Sat != Sat) continue; - if (key.str != Rec) continue; - if (Sat.sys == +E_Sys::GAL - && !galCodeMatch(key.num,code)) continue; - - pass = kfState.getKFValue(key, dcbVal, &dcbVar); - break; - } - - if (pass == false) - return false; - - double coef = TEC_CONSTANT * SQR(lamb / CLIGHT); - bias = coef * dcbVal; - var = SQR(coef) * dcbVar; - - return true; + E_FType ftyp = code2Freq[Sat.sys][code]; + double lamb = nav.satNavMap[Sat].lamMap[ftyp]; + if (lamb == 0) + return false; + + double dcbVal; + double dcbVar; + + E_Source pass = E_Source::NONE; + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::CODE_BIAS) + continue; + if (key.Sat != Sat) + continue; + if (key.str != Rec) + continue; + if (Sat.sys == E_Sys::GAL && !galCodeMatch(key.num, code)) + continue; + + pass = kfState.getKFValue(key, dcbVal, &dcbVar); + break; + } + + if (pass == E_Source::NONE) + return false; + + double coef = TEC_CONSTANT * SQR(lamb / CLIGHT); + bias = coef * dcbVal; + var = SQR(coef) * dcbVar; + + return true; } diff --git a/src/cpp/iono/ionoModel.hpp b/src/cpp/iono/ionoModel.hpp index 02476d57a..696c92e84 100644 --- a/src/cpp/iono/ionoModel.hpp +++ b/src/cpp/iono/ionoModel.hpp @@ -1,57 +1,39 @@ - #pragma once -#include "biases.hpp" -#include "gTime.hpp" - -#define STEC2DELAY 4.48397972589608 +#include "common/biases.hpp" +#include "common/gTime.hpp" +constexpr double STEC2DELAY = 4.48397972589608; -#define MIN_NSAT_REC 3 +constexpr int MIN_NSAT_REC = 3; struct ReceiverMap; extern bool ionoConfigured; -void obsIonoData( - Trace& trace, - Receiver& rec); +void obsIonoData(Trace& trace, Receiver& rec); -void obsIonoDataFromFilter( - Trace& trace, - ReceiverMap& receiverMap, - KFState& measKFstate); +void obsIonoDataFromFilter(Trace& trace, ReceiverMap& receiverMap, KFState& measKFstate); -void filterIonosphere( - Trace& trace, - KFState& kfState, - ReceiverMap& receiverMap, - GTime time); +void filterIonosphere(Trace& trace, KFState& kfState, ReceiverMap& receiverMap, GTime time); -void ionosphereSsrUpdate( - Trace& trace, - KFState& kfState); +void ionosphereSsrUpdate(Trace& trace, KFState& kfState); bool queryBiasDCB( - Trace& trace, - KFState& kfState, - SatSys Sat, - string Rec, - E_ObsCode code, - double& bias, - double& var); - -void ionexFileWrite( - Trace& trace, - string filename, - GTime time, - KFState& kfState); - -void writeIonStec( - string filename, - KFState& kFstate); - -bool configIonModel( Trace& trace); + Trace& trace, + KFState& kfState, + SatSys Sat, + string Rec, + E_ObsCode code, + double& bias, + double& var +); + +void ionexFileWrite(Trace& trace, string filename, GTime time, KFState& kfState); + +void writeIonStec(string filename, KFState& kFstate); + +bool configIonModel(Trace& trace); int configIonModelSphhar(Trace& trace); int configIonModelSphcap(Trace& trace); int configIonModelBsplin(Trace& trace); @@ -60,27 +42,44 @@ int configIonModelLocal_(Trace& trace); bool ippCheckSphhar(GTime time, VectorPos& Ion_pp); bool ippCheckSphcap(GTime time, VectorPos& Ion_pp); bool ippCheckBsplin(GTime time, VectorPos& Ion_pp); -bool ippCheckLocal (GTime time, VectorPos& Ion_pp); +bool ippCheckLocal(GTime time, VectorPos& Ion_pp); -double ionModelCoef (Trace& trace, int ind, IonoObs& obs, bool slant = true); +double ionModelCoef(Trace& trace, int ind, IonoObs& obs, bool slant = true); double ionCoefSphhar(Trace& trace, int ind, IonoObs& obs, bool slant = true); double ionCoefSphcap(Trace& trace, int ind, IonoObs& obs, bool slant = true); double ionCoefBsplin(Trace& trace, int ind, IonoObs& obs, bool slant = true); -double ionCoefLocal (Trace& trace, int ind, IonoObs& obs); +double ionCoefLocal(Trace& trace, int ind, IonoObs& obs); -double ionVtecSphhar(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState); -double ionVtecSphcap(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState); -double ionVtecBsplin(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState); +double +ionVtecSphhar(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState); +double +ionVtecSphcap(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState); +double +ionVtecBsplin(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState); -int checkSSRRegion (VectorPos& pos); +int checkSSRRegion(VectorPos& pos); void ionOutputSphcal(Trace& trace, KFState& kfState); -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); -bool getCmpSSRIono(Trace& trace, GTime time, SSRAtm& ssrAtm, Vector3d& rRec, double& iono, double& var, SatSys Sat); - -bool configAtmosRegions( - Trace& trace, - ReceiverMap& receiverMap); +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 + ); +bool getCmpSSRIono( + Trace& trace, + GTime time, + SSRAtm& ssrAtm, + Vector3d& rRec, + double& iono, + double& var, + SatSys Sat +); + +bool configAtmosRegions(Trace& trace, ReceiverMap& receiverMap); diff --git a/src/cpp/iono/ionoSpherical.cpp b/src/cpp/iono/ionoSpherical.cpp index 473eb0d44..617c13ae0 100644 --- a/src/cpp/iono/ionoSpherical.cpp +++ b/src/cpp/iono/ionoSpherical.cpp @@ -1,95 +1,101 @@ - -#include "acceleration.hpp" -#include "observations.hpp" -#include "coordinates.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "ionoModel.hpp" -#include "ionModels.hpp" -#include "planets.hpp" -#include "common.hpp" - +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/ionModels.hpp" +#include "common/observations.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/acceleration.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/planets.hpp" Legendre ionLeg; -double ionSPHLastColatitude = -100; +double ionSPHLastColatitude = -100; struct SphBasis { - int layer = 0; - int degree = 0; - int order = 0; - E_TrigType trigType; + int layer = 0; + int degree = 0; + int order = 0; + E_TrigType trigType; }; -map sphBasisMap; -map>>> sphBasisIndexMaps; - +map sphBasisMap; +map>>> sphBasisIndexMaps; /** configures the spherical harmonics model. Specifically it initializes: -shar_valid time validity of a rotation matrix (the rotation matrix will chase the sun position) -Sph_Basis_list List of ionosphere basis -time: I time of observations (to update the rotation matrix) -IPP: I Ionospheric piercing point to be updated -Since the spherical harmonic model has global validity, the check always return 1 +shar_valid time validity of a rotation matrix (the rotation matrix will chase the sun +position) Sph_Basis_list List of ionosphere basis time: I time of +observations (to update the rotation matrix) IPP: I Ionospheric piercing point to be +updated Since the spherical harmonic model has global validity, the check always return 1 ----------------------------------------------------- Author: Ken Harima @ RMIT 29 July 2020 -----------------------------------------------------*/ -int configIonModelSphhar( - Trace& trace) +int configIonModelSphhar(Trace& trace) { - if (acsConfig.ionModelOpts.function_degree == 0) acsConfig.ionModelOpts.function_degree = acsConfig.ionModelOpts.function_order; - else if (acsConfig.ionModelOpts.function_order > acsConfig.ionModelOpts.function_degree) acsConfig.ionModelOpts.function_order = acsConfig.ionModelOpts.function_degree; - - int Nmax = acsConfig.ionModelOpts.function_degree + 1; - int nlay = acsConfig.ionModelOpts.layer_heights.size(); - if (nlay == 0) - { - acsConfig.ionModelOpts.layer_heights.push_back(350); - nlay = 1; - } - - ionLeg.setNmax(Nmax); - - int ind = 0; - for (int layer = 0; layer < nlay; layer++) - for (int order = 0; order < acsConfig.ionModelOpts.function_order; order++) - for (int degree = order; degree < acsConfig.ionModelOpts.function_degree; degree++) - { - SphBasis basis; - basis.layer = layer; - basis.order = order; - basis.degree = degree; - -// if (1) - { - basis.trigType = E_TrigType::COS; - - sphBasisIndexMaps[layer][degree][order][basis.trigType] = ind; - sphBasisMap[ind] = basis; - - ind++; - } - - if (order > 0) - { - basis.trigType = E_TrigType::SIN; - - sphBasisIndexMaps[layer][degree][order][basis.trigType] = ind; - sphBasisMap[ind] = basis; - - ind++; - } - } - - acsConfig.ionModelOpts.numBasis = ind; - - tracepdeex(2,trace, "\nIONO_BASIS ind lay ord deg par"); - for (auto& [j,basis] : sphBasisMap) - tracepdeex(2,trace, "\nIONO_BASIS %3d %3d %3d %3d %s ", j, basis.layer, basis.order, basis.degree, basis.trigType._to_string()); - tracepdeex(2,trace, "\n"); - - return ind; + if (acsConfig.ionModelOpts.function_degree == 0) + acsConfig.ionModelOpts.function_degree = acsConfig.ionModelOpts.function_order; + else if (acsConfig.ionModelOpts.function_order > acsConfig.ionModelOpts.function_degree) + acsConfig.ionModelOpts.function_order = acsConfig.ionModelOpts.function_degree; + + int Nmax = acsConfig.ionModelOpts.function_degree + 1; + int nlay = acsConfig.ionModelOpts.layer_heights.size(); + if (nlay == 0) + { + acsConfig.ionModelOpts.layer_heights.push_back(350); + nlay = 1; + } + + ionLeg.setNmax(Nmax); + + int ind = 0; + for (int layer = 0; layer < nlay; layer++) + for (int order = 0; order < acsConfig.ionModelOpts.function_order; order++) + for (int degree = order; degree < acsConfig.ionModelOpts.function_degree; degree++) + { + SphBasis basis; + basis.layer = layer; + basis.order = order; + basis.degree = degree; + + // if (1) + { + basis.trigType = E_TrigType::COS; + + sphBasisIndexMaps[layer][degree][order][basis.trigType] = ind; + sphBasisMap[ind] = basis; + + ind++; + } + + if (order > 0) + { + basis.trigType = E_TrigType::SIN; + + sphBasisIndexMaps[layer][degree][order][basis.trigType] = ind; + sphBasisMap[ind] = basis; + + ind++; + } + } + + acsConfig.ionModelOpts.numBasis = ind; + + tracepdeex(2, trace, "\nIONO_BASIS ind lay ord deg par"); + for (auto& [j, basis] : sphBasisMap) + tracepdeex( + 2, + trace, + "\nIONO_BASIS %3d %3d %3d %3d %s ", + j, + basis.layer, + basis.order, + basis.degree, + enum_to_string(basis.trigType) + ); + tracepdeex(2, trace, "\n"); + + return ind; } /** rotates the Ionosphere piercing point @@ -99,357 +105,373 @@ Since the spherical harmonic model has global validity, the check always return ----------------------------------------------------- Author: Ken Harima @ RMIT 29 July 2020 -----------------------------------------------------*/ -bool ippCheckSphhar( - GTime time, - VectorPos& ionPP) +bool ippCheckSphhar(GTime time, VectorPos& ionPP) { - if (time == GTime::noTime()) - return false; + if (time == GTime::noTime()) + return false; - if (acsConfig.ionModelOpts.use_rotation_mtx) - { - static Matrix3d sphRotMatrix; /* Rotation matrix (to centre of map) */ - static GTime sphTime; + if (acsConfig.ionModelOpts.use_rotation_mtx) + { + static Matrix3d sphRotMatrix; /* Rotation matrix (to centre of map) */ + static GTime sphTime; - double sphValid = 10; + double sphValid = 10; - if ( sphTime == GTime::noTime() - || fabs((time - sphTime).to_double()) > sphValid ) - { - VectorEcef rSun; - planetPosEcef(time, E_ThirdBody::SUN, rSun); + if (sphTime == GTime::noTime() || fabs((time - sphTime).to_double()) > sphValid) + { + VectorEcef rSun; + planetPosEcef(time, E_ThirdBody::SUN, rSun); - VectorPos sunpos = ecef2pos(rSun); + VectorPos sunpos = ecef2pos(rSun); - double lat = sunpos.lat(); - double lon = sunpos.lon(); + double lat = sunpos.lat(); + double lon = sunpos.lon(); - sphRotMatrix = Eigen::AngleAxisd(lon, Vector3d::UnitZ()) * Eigen::AngleAxisd(-lat, Vector3d::UnitY()); + sphRotMatrix = Eigen::AngleAxisd(lon, Vector3d::UnitZ()) * + Eigen::AngleAxisd(-lat, Vector3d::UnitY()); - sphTime = time; - } + sphTime = time; + } - VectorPos pos; - pos.hgt() = ionPP.hgt(); - pos.lon() = ionPP.lon(); - pos.hgt() = acsConfig.ionModelOpts.layer_heights[0]; + VectorPos pos; + pos.hgt() = ionPP.hgt(); + pos.lon() = ionPP.lon(); + pos.hgt() = acsConfig.ionModelOpts.layer_heights[0]; - VectorEcef rpp = pos2ecef(pos); + VectorEcef rpp = pos2ecef(pos); - VectorEcef rrot = (Vector3d)(sphRotMatrix * rpp); - pos = ecef2pos(rrot); + VectorEcef rrot = (Vector3d)(sphRotMatrix * rpp); + pos = ecef2pos(rrot); - ionPP.lat() = pos.lat() + PI/2; /* colatitude for spherical harmonics */ - ionPP.lon() = pos.lon(); - } - else - { - double tow = GTow(time); + ionPP.lat() = pos.lat() + PI / 2; /* colatitude for spherical harmonics */ + ionPP.lon() = pos.lon(); + } + else + { + double tow = GTow(time); - ionPP.lat() = ionPP.lat() - PI/2; - ionPP.lon()+= (tow-50400)*PI/43200; - double day= floor(ionPP.lon()/(2*PI))*2*PI; + ionPP.lat() = ionPP.lat() - PI / 2; + ionPP.lon() += (tow - 50400) * PI / 43200; + double day = floor(ionPP.lon() / (2 * PI)) * 2 * PI; - ionPP.lon()-= day; - } - return true; + ionPP.lon() -= day; + } + return true; } /** Evaluates spherical harmonics basis functions - int ind I - obs I Ionosphere measurement struct - latIPP - Latitude of Ionosphere Piercing Point - lonIPP - Longitude of Ionosphere Piercing Point - angIPP - Angular gain for Ionosphere Piercing Point - int slant I 0: coefficient for VTEC, 1: coefficient for STEC + int ind I + obs I Ionosphere measurement struct + latIPP - Latitude of Ionosphere Piercing Point + lonIPP - Longitude of Ionosphere Piercing Point + angIPP - Angular gain for Ionosphere Piercing Point + int slant I 0: coefficient for VTEC, 1: coefficient for STEC ----------------------------------------------------------------------------*/ double ionCoefSphhar( - Trace& trace, - int ind, ///< Basis function number - IonoObs& obs, ///< Ionospheric observation metadata - bool slant) ///< apply slant factor, false: coefficient for VTEC, true: coefficient for STEC + Trace& trace, + int ind, ///< Basis function number + IonoObs& obs, ///< Ionospheric observation metadata + bool slant ///< apply slant factor, false: coefficient for VTEC, true: coefficient for STEC +) { - if (ind >= sphBasisMap.size()) - return 0; + if (ind >= sphBasisMap.size()) + return 0; - auto& basis = sphBasisMap[ind]; + auto& basis = sphBasisMap[ind]; - if (basis.order > acsConfig.ionModelOpts.function_order) - return 0; + if (basis.order > acsConfig.ionModelOpts.function_order) + return 0; - if (basis.degree > acsConfig.ionModelOpts.function_degree) - return 0; + if (basis.degree > acsConfig.ionModelOpts.function_degree) + return 0; - double colat = obs.ippMap[basis.layer].latDeg * D2R; + double colat = obs.ippMap[basis.layer].latDeg * D2R; - if (fabs(ionSPHLastColatitude - colat) > 0.01) - ionLeg.calculate(cos(colat)); + if (fabs(ionSPHLastColatitude - colat) > 0.01) + ionLeg.calculate(cos(colat)); - double coeff = pow(-1, basis.order) * ionLeg.Pnm(basis.degree, basis.order); + double coeff = pow(-1, basis.order) * ionLeg.Pnm(basis.degree, basis.order); - double angle = basis.order * obs.ippMap[basis.layer].lonDeg * D2R; + double angle = basis.order * obs.ippMap[basis.layer].lonDeg * D2R; - if (basis.trigType == +E_TrigType::SIN) coeff *= sin(angle); - else if (basis.trigType == +E_TrigType::COS) coeff *= cos(angle); + if (basis.trigType == E_TrigType::SIN) + coeff *= sin(angle); + else if (basis.trigType == E_TrigType::COS) + coeff *= cos(angle); - if (slant) - { - coeff *= obs.ippMap[basis.layer].slantFactor; - } + if (slant) + { + coeff *= obs.ippMap[basis.layer].slantFactor; + } - return coeff; + return coeff; } /** Estimate Ionosphere VTEC using Spherical Cap Harmonic models - gtime_t time I time of solutions (not useful for this one - Ion_pp I Ionosphere Piercing Point - layer I Layer number - vari O variance of VTEC + gtime_t time I time of solutions (not useful for this one + Ion_pp I Ionosphere Piercing Point + layer I Layer number + vari O variance of VTEC returns: VETC at piercing point ----------------------------------------------------------------------------*/ -double ionVtecSphhar( - Trace& trace, - GTime time, - VectorPos& ionPP, - int layer, - double& var, - KFState& kfState) +double +ionVtecSphhar(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState) { - VectorPos ionpp_cpy = ionPP; - ionpp_cpy[2] = acsConfig.ionModelOpts.layer_heights[layer]; + VectorPos ionpp_cpy = ionPP; + ionpp_cpy[2] = acsConfig.ionModelOpts.layer_heights[layer]; - ippCheckSphhar(time, ionpp_cpy); + ippCheckSphhar(time, ionpp_cpy); - var = 0; + var = 0; - IonoObs tmpobs; - tmpobs.ippMap[layer].latDeg = ionpp_cpy.latDeg(); - tmpobs.ippMap[layer].lonDeg = ionpp_cpy.lonDeg(); - tmpobs.ippMap[layer].slantFactor = 1; + IonoObs tmpobs; + tmpobs.ippMap[layer].latDeg = ionpp_cpy.latDeg(); + tmpobs.ippMap[layer].lonDeg = ionpp_cpy.lonDeg(); + tmpobs.ippMap[layer].slantFactor = 1; - double iono = 0; + double iono = 0; - for (int basisNum = 0; basisNum < acsConfig.ionModelOpts.numBasis; basisNum++) - { - auto& basis = sphBasisMap[basisNum]; + for (int basisNum = 0; basisNum < acsConfig.ionModelOpts.numBasis; basisNum++) + { + auto& basis = sphBasisMap[basisNum]; - if (basis.layer != layer) - continue; + if (basis.layer != layer) + continue; - double coef = ionCoefSphhar(trace, basisNum, tmpobs, false); + double coef = ionCoefSphhar(trace, basisNum, tmpobs, false); - KFKey key; - key.type = KF::IONOSPHERIC; - key.num = basisNum; + KFKey key; + key.type = KF::IONOSPHERIC; + key.num = basisNum; - double val = 0; - double kfvar = 0; - kfState.getKFValue(key, val, &kfvar); + double val = 0; + double kfvar = 0; + kfState.getKFValue(key, val, &kfvar); - iono += coef * val; - var += SQR( coef)* kfvar; - } + iono += coef * val; + var += SQR(coef) * kfvar; + } - return iono; + return iono; } -void ionOutputSphcal( - Trace& trace, - KFState& kfState) +void ionOutputSphcal(Trace& trace, KFState& kfState) { - SSRAtmGlobal atmGlob; - atmGlob.numberLayers = acsConfig.ionModelOpts.layer_heights.size(); - for (int j = 0; j < atmGlob.numberLayers; j++) - { - atmGlob.layers[j].height = acsConfig.ionModelOpts.layer_heights[j] / 1000; - atmGlob.layers[j].maxOrder = acsConfig.ionModelOpts.function_order; - atmGlob.layers[j].maxDegree = acsConfig.ionModelOpts.function_degree; - } - - tracepdeex (4, trace, "\n#IONO_MODL tow indx hght order degr part value variance"); - GTow tow = kfState.time; - map> dcbList; - for (auto [key, index] : kfState.kfIndexMap) - { - if (key.type == KF::IONOSPHERIC) - { - SphBasis& basis = sphBasisMap[key.num]; - auto& ionoRecord = atmGlob.layers[basis.layer].sphHarmonic[key.num]; - ionoRecord.layer = basis.layer; - ionoRecord.order = basis.order; - ionoRecord.degree = basis.degree; - ionoRecord.trigType = basis.trigType; - - kfState.getKFValue(key, ionoRecord.value, &ionoRecord.variance); - - tracepdeex (4, trace, "\nIONO_MODL %6d %3d %4.0f %2d %2d %s %10.4f %12.5e", - (int)tow, - key.num, - atmGlob.layers[basis.layer].height, - basis.order, - basis.degree, - basis.trigType._to_string(), - ionoRecord.value, - sqrt(ionoRecord.variance)); - } - - if (key.type == KF::CODE_BIAS) - { - if (key.num <= 100) - continue; - if (key.Sat.prn == 0) - continue; - - bool independentDCB = false; - for (auto& [set,dummy] : dcbList[key.Sat]) - { - int setdiff = key.num-set; - int diff1 = setdiff/100; - int diff2 = setdiff%100; - if (diff1 && diff2) - { - dcbList[key.Sat][key.num] = true; - independentDCB = true; - break; - } - } - - if (!independentDCB) - continue; - double bias = 0; - kfState.getKFValue(key, bias); - - auto& ssr = nav.satNavMap[key.Sat].receivedSSR; - auto itCod = ssr.ssrCodeBias_map.begin(); - if (itCod != ssr.ssrCodeBias_map.end()) - itCod->second.ionDCBOffset[key.num] = bias; - - auto itPhs = ssr.ssrPhasBias_map.begin(); - if (itPhs != ssr.ssrPhasBias_map.end()) - itPhs->second.ionDCBOffset[key.num] = bias; - } - - } - tracepdeex (4, trace, "\n"); - - atmGlob.time = kfState.time; - - nav.ssrAtm.atmosGlobalMap.clear(); - nav.ssrAtm.atmosGlobalMap[kfState.time] = atmGlob; + SSRAtmGlobal atmGlob; + atmGlob.numberLayers = acsConfig.ionModelOpts.layer_heights.size(); + for (int j = 0; j < atmGlob.numberLayers; j++) + { + atmGlob.layers[j].height = acsConfig.ionModelOpts.layer_heights[j] / 1000; + atmGlob.layers[j].maxOrder = acsConfig.ionModelOpts.function_order; + atmGlob.layers[j].maxDegree = acsConfig.ionModelOpts.function_degree; + } + + tracepdeex(4, trace, "\n#IONO_MODL tow indx hght order degr part value variance"); + GTow tow = kfState.time; + map> dcbList; + for (auto [key, index] : kfState.kfIndexMap) + { + if (key.type == KF::IONOSPHERIC) + { + SphBasis& basis = sphBasisMap[key.num]; + auto& ionoRecord = atmGlob.layers[basis.layer].sphHarmonic[key.num]; + ionoRecord.layer = basis.layer; + ionoRecord.order = basis.order; + ionoRecord.degree = basis.degree; + ionoRecord.trigType = basis.trigType; + + kfState.getKFValue(key, ionoRecord.value, &ionoRecord.variance); + + tracepdeex( + 4, + trace, + "\nIONO_MODL %6d %3d %4.0f %2d %2d %s %10.4f %12.5e", + (int)tow, + key.num, + atmGlob.layers[basis.layer].height, + basis.order, + basis.degree, + enum_to_string(basis.trigType), + ionoRecord.value, + sqrt(ionoRecord.variance) + ); + } + + if (key.type == KF::CODE_BIAS) + { + if (key.num <= 100) + continue; + if (key.Sat.prn == 0) + continue; + + bool independentDCB = false; + for (auto& [set, dummy] : dcbList[key.Sat]) + { + int setdiff = key.num - set; + int diff1 = setdiff / 100; + int diff2 = setdiff % 100; + if (diff1 && diff2) + { + dcbList[key.Sat][key.num] = true; + independentDCB = true; + break; + } + } + + if (!independentDCB) + continue; + double bias = 0; + kfState.getKFValue(key, bias); + + auto& ssr = nav.satNavMap[key.Sat].receivedSSR; + auto itCod = ssr.ssrCodeBias_map.begin(); + if (itCod != ssr.ssrCodeBias_map.end()) + itCod->second.ionDCBOffset[key.num] = bias; + + auto itPhs = ssr.ssrPhasBias_map.begin(); + if (itPhs != ssr.ssrPhasBias_map.end()) + itPhs->second.ionDCBOffset[key.num] = bias; + } + } + tracepdeex(4, trace, "\n"); + + atmGlob.time = kfState.time; + + nav.ssrAtm.atmosGlobalMap.clear(); + nav.ssrAtm.atmosGlobalMap[kfState.time] = atmGlob; } bool getEpcSsrIono( - Trace& trace, ///< Debug trace - GTime time, ///< time of ionosphere correction - SSRAtmGlobal& atmGlob, ///< SSR atmospheric correction - Vector3d& rRec, ///< receiver position - AzEl& azel, ///< Satellite azimuth and elevation - double& iono, ///< Ionosphere delay (in TECu) - double& var) ///< Ionosphere variance + Trace& trace, ///< Debug trace + GTime time, ///< time of ionosphere correction + SSRAtmGlobal& atmGlob, ///< SSR atmospheric correction + Vector3d& rRec, ///< receiver position + AzEl& azel, ///< Satellite azimuth and elevation + double& iono, ///< Ionosphere delay (in TECu) + double& var ///< Ionosphere variance +) { - var = 0; - iono = 0; + var = 0; + iono = 0; - if (fabs((time - atmGlob.time).to_double()) > acsConfig.ssrInOpts.global_vtec_valid_time) - return false; + if (fabs((time - atmGlob.time).to_double()) > acsConfig.ssrInOpts.global_vtec_valid_time) + return false; - if (sphBasisIndexMaps.size() < atmGlob.layers.size()) - { - sphBasisIndexMaps.clear(); - int maxdeg = 0; - int maxord = 0; - for (auto& [hind,atmLay]: atmGlob.layers) - { - if (maxdeg < atmLay.maxDegree) maxdeg = atmLay.maxDegree; - if (maxord < atmLay.maxOrder) maxord = atmLay.maxOrder; + if (sphBasisIndexMaps.size() < atmGlob.layers.size()) + { + sphBasisIndexMaps.clear(); + int maxdeg = 0; + int maxord = 0; + for (auto& [hind, atmLay] : atmGlob.layers) + { + if (maxdeg < atmLay.maxDegree) + maxdeg = atmLay.maxDegree; + if (maxord < atmLay.maxOrder) + maxord = atmLay.maxOrder; - acsConfig.ionModelOpts.layer_heights[hind] = atmLay.height; - } + acsConfig.ionModelOpts.layer_heights[hind] = atmLay.height; + } - acsConfig.ionModelOpts.function_degree = maxdeg; - acsConfig.ionModelOpts.function_order = maxord; + acsConfig.ionModelOpts.function_degree = maxdeg; + acsConfig.ionModelOpts.function_order = maxord; - configIonModelSphhar(trace); - } + configIonModelSphhar(trace); + } - VectorPos pos = ecef2pos(rRec); + VectorPos pos = ecef2pos(rRec); - for (auto& [layer, atmLay]: atmGlob.layers) - { - VectorPos posp; - double slantFactor = ionppp(pos, azel, RE_WGS84 / 1000, atmLay.height, posp); + for (auto& [layer, atmLay] : atmGlob.layers) + { + VectorPos posp; + double slantFactor = ionppp(pos, azel, RE_WGS84 / 1000, atmLay.height, posp); - if (ippCheckSphhar(time, posp) == false) - return false; + if (ippCheckSphhar(time, posp) == false) + return false; - GObs tmpobs; - tmpobs.ippMap[layer].latDeg = posp.latDeg(); - tmpobs.ippMap[layer].lonDeg = posp.lonDeg(); - tmpobs.ippMap[layer].slantFactor = slantFactor; + GObs tmpobs; + tmpobs.ippMap[layer].latDeg = posp.latDeg(); + tmpobs.ippMap[layer].lonDeg = posp.lonDeg(); + tmpobs.ippMap[layer].slantFactor = slantFactor; - for (auto& [ind, harmonic] : atmLay.sphHarmonic) - { - int reindex = sphBasisIndexMaps[harmonic.layer][harmonic.degree][harmonic.order][harmonic.trigType]; + for (auto& [ind, harmonic] : atmLay.sphHarmonic) + { + int reindex = sphBasisIndexMaps[harmonic.layer][harmonic.degree][harmonic.order] + [harmonic.trigType]; - double comp = ionCoefSphhar(trace, reindex, tmpobs, true); + double comp = ionCoefSphhar(trace, reindex, tmpobs, true); - iono += comp * harmonic.value; - var += SQR( comp) * harmonic.variance; - } - } + iono += comp * harmonic.value; + var += SQR(comp) * harmonic.variance; + } + } - var += atmGlob.vtecQuality; + var += atmGlob.vtecQuality; - return true; + return true; } - bool getIGSSSRIono( - Trace& trace, ///< Debug trace - GTime time, ///< time of ionosphere correction - SSRAtm& ssrAtm, ///< SSR atmospheric correction - Vector3d& rRec, ///< receiver position - AzEl& azel, ///< receiver position - double& iono, ///< Ionosphere delay (in TECu) - double& var) ///< Ionosphere variance + Trace& trace, ///< Debug trace + GTime time, ///< time of ionosphere correction + SSRAtm& ssrAtm, ///< SSR atmospheric correction + Vector3d& rRec, ///< receiver position + AzEl& azel, ///< receiver position + double& iono, ///< Ionosphere delay (in TECu) + double& var ///< Ionosphere variance +) { - var = 0; - iono = 0; - - auto it = ssrAtm.atmosGlobalMap.lower_bound(time); - if (it == ssrAtm.atmosGlobalMap.end()) - return false; - - auto& [t0, ssrAtm0] = *it; - - double iono0 = 0; - double var0 = 0; - bool pass0 = getEpcSsrIono(trace, t0, ssrAtm0, rRec, azel, iono0, var0); - - double iono1 = 0; - double var1 = 0; - bool pass1; - double a; - if (it == ssrAtm.atmosGlobalMap.begin()) - { - pass1 = false; - } - else - { - it--; - auto& [t1, ssrAtm1] = *it; - pass1 = getEpcSsrIono(trace, t1, ssrAtm1, rRec, azel, iono1, var1); - if (pass1) - { - a = (time - t0)/(t1 - t0); - } - } - - if (!pass0 && !pass1) { var = -1; iono=0; return false; } - if ( pass0 && !pass1) { var = var0; iono=iono0; return true; } - if (!pass0 && pass1) { var = var1; iono=iono1; return true; } - - var = var0 * SQR(1-a) + var1 * SQR(a); - iono = iono0 * (1-a) + iono1 * a; - return true; + var = 0; + iono = 0; + + auto it = ssrAtm.atmosGlobalMap.lower_bound(time); + if (it == ssrAtm.atmosGlobalMap.end()) + return false; + + auto& [t0, ssrAtm0] = *it; + + double iono0 = 0; + double var0 = 0; + bool pass0 = getEpcSsrIono(trace, t0, ssrAtm0, rRec, azel, iono0, var0); + + double iono1 = 0; + double var1 = 0; + bool pass1; + double a; + if (it == ssrAtm.atmosGlobalMap.begin()) + { + pass1 = false; + } + else + { + it--; + auto& [t1, ssrAtm1] = *it; + pass1 = getEpcSsrIono(trace, t1, ssrAtm1, rRec, azel, iono1, var1); + if (pass1) + { + a = (time - t0) / (t1 - t0); + } + } + + if (!pass0 && !pass1) + { + var = -1; + iono = 0; + return false; + } + if (pass0 && !pass1) + { + var = var0; + iono = iono0; + return true; + } + if (!pass0 && pass1) + { + var = var1; + iono = iono1; + return true; + } + + var = var0 * SQR(1 - a) + var1 * SQR(a); + iono = iono0 * (1 - a) + iono1 * a; + return true; } diff --git a/src/cpp/iono/ionoSphericalCaps.cpp b/src/cpp/iono/ionoSphericalCaps.cpp index 9034f47f3..61a02d668 100644 --- a/src/cpp/iono/ionoSphericalCaps.cpp +++ b/src/cpp/iono/ionoSphericalCaps.cpp @@ -1,27 +1,25 @@ +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/observations.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/coordinates.hpp" -#include "coordinates.hpp" -#include "ionoModel.hpp" -#include "observations.hpp" -#include "common.hpp" -#include "acsConfig.hpp" - -#define LEG_ITER_NUM 100 -#define LEG_EPSILON0 (1.e-7) // Legendre function accuracy -#define LEG_EPSILON1 (1.e-7) // Legendre function degree accuracy -#define SQR(x) ((x)*(x)) +constexpr int LEG_ITER_NUM = 100; +constexpr double LEG_EPSILON0 = (1.e-7); // Legendre function accuracy +constexpr double LEG_EPSILON1 = (1.e-7); // Legendre function degree accuracy struct ScpBasis { - int ind; /* layer number */ - int order; /* order of the legendre function */ - double degree; /* degree of the function */ - bool parity; /* longitude function: false=cosine, true=sine */ + int ind; /* layer number */ + int order; /* order of the legendre function */ + double degree; /* degree of the function */ + bool parity; /* longitude function: false=cosine, true=sine */ }; -map scpBasisMap; +map scpBasisMap; -double scapRotMat[9] = {}; -double scapMaxLatDeg = 90; +double scapRotMat[9] = {}; +double scapMaxLatDeg = 90; /*----------------------------------------------------- P=leg(m,n,x) Returns the legendre function @ x @@ -34,33 +32,34 @@ Author: German Olivares @ GA 17 January 2019 -----------------------------------------------------*/ double legendre_function(int m, double n, double x) { - double A, Kmn, P, Ptmp; - double eps = 10 * LEG_EPSILON0; - int j = 1; - - if (m == 0) - A = 1; - else - { - double p = pow(n / m, 2) - 1; - double e1 = -(1 + 1 / p) / (12 * m); - 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); - } - - P = A; - - while ( eps > LEG_EPSILON0 ) - { - Ptmp = P; - A = ((j + m - 1) * (j + m) - n * (n + 1)) / (j * (j + m)) * A; - P = P + A * pow(sin(x / 2), 2 * j); - eps = abs((Ptmp - P) / Ptmp); - j = j + 1; - } - - return (P); + double A, Kmn, P, Ptmp; + double eps = 10 * LEG_EPSILON0; + int j = 1; + + if (m == 0) + A = 1; + else + { + double p = pow(n / m, 2) - 1; + double e1 = -(1 + 1 / p) / (12 * m); + 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); + } + + P = A; + + while (eps > LEG_EPSILON0) + { + Ptmp = P; + A = ((j + m - 1) * (j + m) - n * (n + 1)) / (j * (j + m)) * A; + P = P + A * pow(sin(x / 2), 2 * j); + eps = abs((Ptmp - P) / Ptmp); + j = j + 1; + } + + return (P); } /*Returns the derivative of the legendre function @ x @@ -71,377 +70,381 @@ x: argument (rad) Truncation is set up by the resolution parameter eps0*/ double legendre_derivatv(int m, double n, double x) { - double A, Kmn, dP, P, Ptmp, dPtmp; - double eps = 10 * LEG_EPSILON0; - double p, e1, e2; - int i = 1; - - if ( m == 0 ) - { - Kmn = 1; - A = 1; - } - else - { - p = pow(n / m, 2) - 1; - e1 = -(1 + 1 / p) / (12 * m); - 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); - } - - P = A; - - while ( eps > LEG_EPSILON0 ) - { - Ptmp = P; - A = ((i + m - 1) * (i + m) - n * (n + 1)) / (i * (i + m)) * A; - P = P + A * pow(sin(x / 2), 2 * i); - eps = fabs((Ptmp - P) / Ptmp); - i = i + 1; - } - - if ( m == 0 || x == PI / 2 ) - dP = 0; - else - dP = m * cos(x) / sin(x) * P; - - // Compute intial A for dP - A = Kmn * pow(sin(x), m); - - eps = 10 * LEG_EPSILON0; - i = 1; - - while ( eps > LEG_EPSILON0 ) - { - dPtmp = dP; - A = ((i + m - 1) * (i + m) - n * (n + 1)) / (i * (i + m)) * A; - dP = dP + sin(x) / 2 * A * i * pow(sin(x / 2), 2 * (i - 1)); - eps = fabs((dPtmp - dP) / dPtmp); - i = i + 1; - } - - return (dP); + double A, Kmn, dP, P, Ptmp, dPtmp; + double eps = 10 * LEG_EPSILON0; + double p, e1, e2; + int i = 1; + + if (m == 0) + { + Kmn = 1; + A = 1; + } + else + { + p = pow(n / m, 2) - 1; + e1 = -(1 + 1 / p) / (12 * m); + 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); + } + + P = A; + + while (eps > LEG_EPSILON0) + { + Ptmp = P; + A = ((i + m - 1) * (i + m) - n * (n + 1)) / (i * (i + m)) * A; + P = P + A * pow(sin(x / 2), 2 * i); + eps = fabs((Ptmp - P) / Ptmp); + i = i + 1; + } + + if (m == 0 || x == PI / 2) + dP = 0; + else + dP = m * cos(x) / sin(x) * P; + + // Compute intial A for dP + A = Kmn * pow(sin(x), m); + + eps = 10 * LEG_EPSILON0; + i = 1; + + while (eps > LEG_EPSILON0) + { + dPtmp = dP; + A = ((i + m - 1) * (i + m) - n * (n + 1)) / (i * (i + m)) * A; + dP = dP + sin(x) / 2 * A * i * pow(sin(x / 2), 2 * (i - 1)); + eps = fabs((dPtmp - dP) / dPtmp); + i = i + 1; + } + + return (dP); } /* Returns the root of legendre functions in the interval (ntmp[0], ntmp[1]) */ double bisection(int m, double* ntmp, double x, int nu) { - double Pa, Pd, err; - double a = ntmp[0], b = ntmp[1]; - double d = (a + b) / 2; - int step = 0; - - /* Compute the Legendre function value between a and b */ - if ((nu - m) % 2 == 0) - { - // Even boundary conditions - Pa = legendre_derivatv(m, a, x); - Pd = legendre_derivatv(m, d, x); - } - else - { - /* Odd boundary conditions */ - Pa = legendre_function(m, a, x); - Pd = legendre_function(m, d, x); - } - - for (step = 0, err = fabs(Pd); step < LEG_ITER_NUM; step++) - { - if (err < LEG_EPSILON0) - break; - - if ( Pa * Pd < 0 ) - b = d; - else - a = d; - - d = (a + b) / 2; - - if ( (m - nu) % 2 == 0) - { - /* Even boundary conditions */ - Pa = legendre_derivatv(m, a, x); - Pd = legendre_derivatv(m, d, x); - } - else - { - /* Odd boundary conditions */ - Pa = legendre_function(m, a, x); - Pd = legendre_function(m, d, x); - } - - err = fabs(Pd); - } - - return (d); + double Pa, Pd, err; + double a = ntmp[0], b = ntmp[1]; + double d = (a + b) / 2; + int step = 0; + + /* Compute the Legendre function value between a and b */ + if ((nu - m) % 2 == 0) + { + // Even boundary conditions + Pa = legendre_derivatv(m, a, x); + Pd = legendre_derivatv(m, d, x); + } + else + { + /* Odd boundary conditions */ + Pa = legendre_function(m, a, x); + Pd = legendre_function(m, d, x); + } + + for (step = 0, err = fabs(Pd); step < LEG_ITER_NUM; step++) + { + if (err < LEG_EPSILON0) + break; + + if (Pa * Pd < 0) + b = d; + else + a = d; + + d = (a + b) / 2; + + if ((m - nu) % 2 == 0) + { + /* Even boundary conditions */ + Pa = legendre_derivatv(m, a, x); + Pd = legendre_derivatv(m, d, x); + } + else + { + /* Odd boundary conditions */ + Pa = legendre_function(m, a, x); + Pd = legendre_function(m, d, x); + } + + err = fabs(Pd); + } + + return (d); } /** transforms the Ionosphere Piercing Point and checks if it falls in area of coverage time: I time of observations (not used) IPP: I Ionospheric piercing point to be updated returns 1 if the IPP is within the area of coverage */ -bool ippCheckSphcap( - GTime time, - VectorPos& ionPP) +bool ippCheckSphcap(GTime time, VectorPos& ionPP) { - VectorPos pos = ionPP; - pos.hgt() = acsConfig.ionModelOpts.layer_heights[0]; + VectorPos pos = ionPP; + pos.hgt() = acsConfig.ionModelOpts.layer_heights[0]; - VectorEcef rpp = pos2ecef(pos); + VectorEcef rpp = pos2ecef(pos); - VectorEcef rrot; - matmul("NN", 3, 1, 3, 1, scapRotMat, rpp.data(), 0, rrot.data()); + VectorEcef rrot; + matmul("NN", 3, 1, 3, 1, scapRotMat, rpp.data(), 0, rrot.data()); - pos = ecef2pos(rrot); + pos = ecef2pos(rrot); - ionPP.lat() = PI / 2 - pos.lat(); /* colatitude for spherical harmonic caps */ + ionPP.lat() = PI / 2 - pos.lat(); /* colatitude for spherical harmonic caps */ - if (ionPP.latDeg() > scapMaxLatDeg) - return false; + if (ionPP.latDeg() > scapMaxLatDeg) + return false; - ionPP.lon() = pos.lon(); + ionPP.lon() = pos.lon(); - return true; + return true; } - /** Evaluates spherical cap harmonics basis functions - int ind I Basis function number - obs I Ionosphere measurement struct - latIPP - Latitude of Ionosphere Piercing Point - lonIPP - Longitude of Ionosphere Piercing Point - angIPP - Angular gain for Ionosphere Piercing Point - bool slant I false: output coefficient for Vtec, true: output coefficient for delay + int ind I Basis function number + obs I Ionosphere measurement struct + latIPP - Latitude of Ionosphere Piercing Point + lonIPP - Longitude of Ionosphere Piercing Point + angIPP - Angular gain for Ionosphere Piercing Point + bool slant I false: output coefficient for Vtec, true: output coefficient for delay ----------------------------------------------------------------------------*/ -double ionCoefSphcap( - Trace& trace, - int ind, - IonoObs& obs, - bool slant) +double ionCoefSphcap(Trace& trace, int ind, IonoObs& obs, bool slant) { - if (ind >= scpBasisMap.size()) - return 0; + if (ind >= scpBasisMap.size()) + return 0; - auto& basis = scpBasisMap[ind]; + auto& basis = scpBasisMap[ind]; - double legr = legendre_function(basis.order, basis.degree, obs.ippMap[basis.ind].latDeg * D2R); // Legendre function + double legr = legendre_function( + basis.order, + basis.degree, + obs.ippMap[basis.ind].latDeg * D2R + ); // Legendre function - double out; + double out; - if (basis.parity) out = legr * sin(basis.order * obs.ippMap[basis.ind].lonDeg * D2R); //todo aaron use enum - else out = legr * cos(basis.order * obs.ippMap[basis.ind].lonDeg * D2R); + if (basis.parity) + out = legr * sin(basis.order * obs.ippMap[basis.ind].lonDeg * D2R); // todo aaron use enum + else + out = legr * cos(basis.order * obs.ippMap[basis.ind].lonDeg * D2R); - if (slant) - { - out *= obs.ippMap[basis.ind].slantFactor * obs.stecToDelay; - } + if (slant) + { + out *= obs.ippMap[basis.ind].slantFactor * obs.stecToDelay; + } - return out; + return out; } /** Estimate Ionosphere VTEC using Spherical Cap Harmonic models - gtime_t time I time of solutions (not useful for this one - Ion_pp I Ionosphere Piercing Point - layer I Layer number - vari O variance of VTEC + gtime_t time I time of solutions (not useful for this one + Ion_pp I Ionosphere Piercing Point + layer I Layer number + vari O variance of VTEC returns: VETC at piercing point ----------------------------------------------------------------------------*/ -double ionVtecSphcap( - Trace& trace, - GTime time, - VectorPos& ionPP, - int layer, - double& var, - KFState& kfState) +double +ionVtecSphcap(Trace& trace, GTime time, VectorPos& ionPP, int layer, double& var, KFState& kfState) { - if (ippCheckSphcap(time, ionPP) == false) - { - var = 0; - return 0; - } + if (ippCheckSphcap(time, ionPP) == false) + { + var = 0; + return 0; + } - var = 0; - double iono = 0; - GObs tmpobs; - tmpobs.ippMap[layer].latDeg = ionPP.latDeg(); - tmpobs.ippMap[layer].lonDeg = ionPP.lonDeg(); - tmpobs.ippMap[layer].slantFactor = 1; + var = 0; + double iono = 0; + GObs tmpobs; + tmpobs.ippMap[layer].latDeg = ionPP.latDeg(); + tmpobs.ippMap[layer].lonDeg = ionPP.lonDeg(); + tmpobs.ippMap[layer].slantFactor = 1; - for (int ind = 0; ind < acsConfig.ionModelOpts.numBasis; ind++) - { - auto& basis = scpBasisMap[ind]; + for (int ind = 0; ind < acsConfig.ionModelOpts.numBasis; ind++) + { + auto& basis = scpBasisMap[ind]; - if (basis.ind != layer) - continue; + if (basis.ind != layer) + continue; - double coef = ionCoefSphcap(trace, ind, tmpobs, false); + double coef = ionCoefSphcap(trace, ind, tmpobs, false); - KFKey key; - key.type = KF::IONOSPHERIC; - key.num = ind; + KFKey key; + key.type = KF::IONOSPHERIC; + key.num = ind; - double val = 0; - double kfvar = 0; - kfState.getKFValue(key, val, &kfvar); + double val = 0; + double kfvar = 0; + kfState.getKFValue(key, val, &kfvar); - iono += coef * val; - var += SQR( coef) * kfvar; - } + iono += coef * val; + var += SQR(coef) * kfvar; + } - return iono; + return iono; } - /** Initializes Spherical caps Ionosphere model - The following configursation parameters are used - - acsConfig.ionoOpts.lat_center: latitude of map centre - - acsConfig.ionoOpts.lon_center: longitude of map centre - - acsConfig.ionoOpts.lat_width: latitude width of maps - - acsConfig.ionoOpts.lon_width: longitude width of maps - - acsConfig.ionoOpts.func_order: Legendre function order - - acsConfig.ionoOpts.layer_heights: Ionosphere layer Heights + The following configursation parameters are used + - acsConfig.ionoOpts.lat_center: latitude of map centre + - acsConfig.ionoOpts.lon_center: longitude of map centre + - acsConfig.ionoOpts.lat_width: latitude width of maps + - acsConfig.ionoOpts.lon_width: longitude width of maps + - acsConfig.ionoOpts.func_order: Legendre function order + - acsConfig.ionoOpts.layer_heights: Ionosphere layer Heights ----------------------------------------------------------------------------*/ -int configIonModelSphcap( - Trace& trace) +int configIonModelSphcap(Trace& trace) { - double latc = acsConfig.ionexGrid.lat_centre * D2R; - double lonc = acsConfig.ionexGrid.lon_centre * D2R; - double latw = acsConfig.ionexGrid.lat_width * D2R / 2; - double lonw = acsConfig.ionexGrid.lon_width * D2R / 2; - scapRotMat[0] = sin(latc) * cos(lonc); - scapRotMat[1] = sin(latc) * sin(lonc); - scapRotMat[2] = -cos(latc); - scapRotMat[3] = -sin(lonc); - scapRotMat[4] = cos(lonc); - scapRotMat[5] = 0; - scapRotMat[6] = cos(latc) * cos(lonc); - scapRotMat[7] = cos(latc) * sin(lonc); - scapRotMat[8] = sin(latc); - scapMaxLatDeg = acos(cos(latc) * cos(latc + latw) * (cos(lonw) - 1) + cos(latw)); - - if ( scapMaxLatDeg > 0.45 * 180 - && scapMaxLatDeg > 0.5 * 180) - { - scapMaxLatDeg = 90; - } - - int Kmax = acsConfig.ionModelOpts.function_order; - int nlay = acsConfig.ionModelOpts.layer_heights.size(); - int ind = 0; - - ScpBasis basis; - - for (int lay = 0; lay < nlay; lay++) - { - basis.ind = lay; - - for (int m = 0; m <= Kmax; m++) - { - basis.order = m; - double nodd[2] = {m + 0.05, m + 0.10}; - double neve[2] = {m + 0.05, m + 0.10}; - - for (int k = m; k <= Kmax; k++) - { - double nk = 0; - - if (scapMaxLatDeg == 90) - nk = k; - else if (k == 0) - nk = 0; - else if ((k - m) % 2) - { - while (1) - { - nodd[1] = nodd[0] + 0.5; - double p1 = legendre_function(m, nodd[0], scapMaxLatDeg); - double p2 = legendre_function(m, nodd[1], scapMaxLatDeg); - - if (fabs(p1) < LEG_EPSILON0) - { - nk = nodd[0]; - nodd[0] += 0.5; - } - - if (fabs(p2) < LEG_EPSILON0) - { - nk = nodd[1]; - nodd[0] = nodd[1]; - break; - } - - if ((p1 * p2) < 0) - { - nk = bisection(m, nodd, scapMaxLatDeg, k); - - if (fabs(nk - m) < LEG_EPSILON1) nk = 1.0 * m; - - nodd[0] = nk + 0.1; - } - else - nodd[0] = nodd[1]; - } - } - else - { - while (1) - { - neve[1] = neve[0] + 0.5; - double p1 = legendre_derivatv(m, neve[0], scapMaxLatDeg); - double p2 = legendre_derivatv(m, neve[1], scapMaxLatDeg); - - if (fabs(p1) < LEG_EPSILON0) - { - nk = neve[0]; - neve[0] += 0.5; - } - - if (fabs(p2) < LEG_EPSILON0) - { - nk = neve[1]; - neve[0] = neve[1]; - break; - } - - if ((p1 * p2) < 0) - { - nk = bisection(m, neve, scapMaxLatDeg, k); - - if (fabs(nk - m) < LEG_EPSILON1) nk = 1.0 * m; - - neve[0] = nk + 0.1; - } - else neve[0] = neve[1]; - } - } - - basis.degree = nk; - - { - basis.parity = false; - scpBasisMap[ind] = basis; - ind++; - } - - if (m > 0) - { - basis.parity = true; - scpBasisMap[ind] = basis; - ind++; - } - } - } - } - - acsConfig.ionModelOpts.numBasis = scpBasisMap.size(); - - tracepdeex(2,trace, "\nIONO_BASIS ind lay ord deg par"); - - for (auto& [j,basis] : scpBasisMap) - tracepdeex(2,trace, "\nIONO_BASIS %3d %3d %3d %8.4f %1d ", j, basis.ind, basis.order, basis.degree, basis.parity); - - tracepdeex(2,trace, "\n"); - - return ind; + double latc = acsConfig.ionexGrid.lat_centre * D2R; + double lonc = acsConfig.ionexGrid.lon_centre * D2R; + double latw = acsConfig.ionexGrid.lat_width * D2R / 2; + double lonw = acsConfig.ionexGrid.lon_width * D2R / 2; + scapRotMat[0] = sin(latc) * cos(lonc); + scapRotMat[1] = sin(latc) * sin(lonc); + scapRotMat[2] = -cos(latc); + scapRotMat[3] = -sin(lonc); + scapRotMat[4] = cos(lonc); + scapRotMat[5] = 0; + scapRotMat[6] = cos(latc) * cos(lonc); + scapRotMat[7] = cos(latc) * sin(lonc); + scapRotMat[8] = sin(latc); + scapMaxLatDeg = acos(cos(latc) * cos(latc + latw) * (cos(lonw) - 1) + cos(latw)); + + if (scapMaxLatDeg > 0.45 * 180 && scapMaxLatDeg > 0.5 * 180) + { + scapMaxLatDeg = 90; + } + + int Kmax = acsConfig.ionModelOpts.function_order; + int nlay = acsConfig.ionModelOpts.layer_heights.size(); + int ind = 0; + + ScpBasis basis; + + for (int lay = 0; lay < nlay; lay++) + { + basis.ind = lay; + + for (int m = 0; m <= Kmax; m++) + { + basis.order = m; + double nodd[2] = {m + 0.05, m + 0.10}; + double neve[2] = {m + 0.05, m + 0.10}; + + for (int k = m; k <= Kmax; k++) + { + double nk = 0; + + if (scapMaxLatDeg == 90) + nk = k; + else if (k == 0) + nk = 0; + else if ((k - m) % 2) + { + while (1) + { + nodd[1] = nodd[0] + 0.5; + double p1 = legendre_function(m, nodd[0], scapMaxLatDeg); + double p2 = legendre_function(m, nodd[1], scapMaxLatDeg); + + if (fabs(p1) < LEG_EPSILON0) + { + nk = nodd[0]; + nodd[0] += 0.5; + } + + if (fabs(p2) < LEG_EPSILON0) + { + nk = nodd[1]; + nodd[0] = nodd[1]; + break; + } + + if ((p1 * p2) < 0) + { + nk = bisection(m, nodd, scapMaxLatDeg, k); + + if (fabs(nk - m) < LEG_EPSILON1) + nk = 1.0 * m; + + nodd[0] = nk + 0.1; + } + else + nodd[0] = nodd[1]; + } + } + else + { + while (1) + { + neve[1] = neve[0] + 0.5; + double p1 = legendre_derivatv(m, neve[0], scapMaxLatDeg); + double p2 = legendre_derivatv(m, neve[1], scapMaxLatDeg); + + if (fabs(p1) < LEG_EPSILON0) + { + nk = neve[0]; + neve[0] += 0.5; + } + + if (fabs(p2) < LEG_EPSILON0) + { + nk = neve[1]; + neve[0] = neve[1]; + break; + } + + if ((p1 * p2) < 0) + { + nk = bisection(m, neve, scapMaxLatDeg, k); + + if (fabs(nk - m) < LEG_EPSILON1) + nk = 1.0 * m; + + neve[0] = nk + 0.1; + } + else + neve[0] = neve[1]; + } + } + + basis.degree = nk; + + { + basis.parity = false; + scpBasisMap[ind] = basis; + ind++; + } + + if (m > 0) + { + basis.parity = true; + scpBasisMap[ind] = basis; + ind++; + } + } + } + } + + acsConfig.ionModelOpts.numBasis = scpBasisMap.size(); + + tracepdeex(2, trace, "\nIONO_BASIS ind lay ord deg par"); + + for (auto& [j, basis] : scpBasisMap) + tracepdeex( + 2, + trace, + "\nIONO_BASIS %3d %3d %3d %8.4f %1d ", + j, + basis.ind, + basis.order, + basis.degree, + basis.parity + ); + + tracepdeex(2, trace, "\n"); + + return ind; } diff --git a/src/cpp/loading/boost_ma_type.h b/src/cpp/loading/boost_ma_type.h index d66aa5f83..e36553878 100644 --- a/src/cpp/loading/boost_ma_type.h +++ b/src/cpp/loading/boost_ma_type.h @@ -5,10 +5,9 @@ * @date 5/3/21 * */ - #pragma once - +#include typedef boost::multi_array MA3d; typedef boost::multi_array MA2d; typedef boost::multi_array MA1d; @@ -17,6 +16,6 @@ typedef boost::multi_array MA3f; typedef boost::multi_array MA2f; typedef boost::multi_array MA1f; -typedef boost::multi_array< std::complex, 3> MA3cf; -typedef boost::multi_array< std::complex, 2> MA2cf; -typedef boost::multi_array< std::complex, 1> MA1cf; +typedef boost::multi_array, 3> MA3cf; +typedef boost::multi_array, 2> MA2cf; +typedef boost::multi_array, 1> MA1cf; diff --git a/src/cpp/loading/input_otl.h b/src/cpp/loading/input_otl.h index d64816a90..2a607a6b7 100644 --- a/src/cpp/loading/input_otl.h +++ b/src/cpp/loading/input_otl.h @@ -4,25 +4,25 @@ * @date 5/3/21 * */ - #pragma once -#include "boost_ma_type.h" -struct otl_input { - std::string type; - std::string green; - std::string output_blq_file; - std::vector tide_file; - std::vector< std::vector> xyz_coords; - std::vector lon; - std::vector lat; - std::vector code; - std::vector< std::vector > dispZ_in; - std::vector< std::vector > dispEW_in; - std::vector< std::vector > dispNS_in; - std::vector< std::vector > dispZ_out; - std::vector< std::vector > dispEW_out; - std::vector< std::vector > dispNS_out; - std::vector< std::string > wave_names; - MA3cf out_disp; // nstation, nphase, naxis +#include "boost_ma_type.h" +struct otl_input +{ + std::string type; + std::string green; + std::string output_blq_file; + std::vector tide_file; + std::vector> xyz_coords; + std::vector lon; + std::vector lat; + std::vector code; + std::vector> dispZ_in; + std::vector> dispEW_in; + std::vector> dispNS_in; + std::vector> dispZ_out; + std::vector> dispEW_out; + std::vector> dispNS_out; + std::vector wave_names; + MA3cf out_disp; // nstation, nphase, naxis }; diff --git a/src/cpp/loading/interpolate_loading.cpp b/src/cpp/loading/interpolate_loading.cpp index 8fbece5f1..9e568e271 100644 --- a/src/cpp/loading/interpolate_loading.cpp +++ b/src/cpp/loading/interpolate_loading.cpp @@ -3,9 +3,10 @@ * * @brief Program to compute the ocean tide loading from a tide grid * - * With an input data containing the list of the tide file, the Green's function and the coordinate of the site of interest, - * this code will compute the loading (response to the solid earth to the load generated by the tide) displacement to the tide. - * It include the vertical, N-S and E-W load in amplitude and phase at the tidal frequencies. + * With an input data containing the list of the tide file, the Green's function and the coordinate + * of the site of interest, this code will compute the loading (response to the solid earth to the + * load generated by the tide) displacement to the tide. It include the vertical, N-S and E-W load + * in amplitude and phase at the tidal frequencies. * * @version 0.2 * @@ -13,40 +14,38 @@ * @date 26/02/2021 */ - -#include -#include - -#ifdef ENABLE_PARALLELISATION - #include "omp.h" -#endif - -#include -#include -#include -#include #include +#include #include +#include +#include +#include +#include +#include +#include "loading/input_otl.h" +#include "loading/load_functions.h" +#include "loading/loadgrid.h" +#include "loading/loading.h" +#include "loading/tide.h" +#include "loading/utils.h" +#include "omp.h" -#include "tide.h" -#include "loading.h" -#include "utils.h" -#include "input_otl.h" -#include "load_functions.h" -#include "loadgrid.h" +namespace po = boost::program_options; using namespace std; using namespace boost::timer; -namespace po = boost::program_options; + +#ifdef ENABLE_PARALLELISATION +#endif const int THREAD_COUNT = 8; -void program_options(int argc, char * argv[], otl_input & input) +void program_options(int argc, char* argv[], otl_input& input) { - po::options_description desc{"interpolate_loading "}; + po::options_description desc{"interpolate_loading "}; - // Do not set default values here, as this will overide the configuration file opitions!!! - desc.add_options() + // 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") @@ -59,196 +58,242 @@ void program_options(int argc, char * argv[], otl_input & input) ("output", po::value(), "Output BLQ file") ; - po::variables_map vm; - // This is to be able to parse negative numbers with boost. - po::store(po::command_line_parser(argc, argv).options(desc).style( - po::command_line_style::unix_style ^ po::command_line_style::allow_short - ).run(), vm); - po::notify(vm); - - - if (vm.count("help")) { - cout << "Usage: interpolate_loading [options]\n"; - cout << desc << "\n\n"; - cout << "Example: interpolate_loading --type o --grid //oceantide.nc --code 'ALIC 50137M0014' --location 133.8855 -23.6701 \n\n"; - cout << "Example with input file: interpolate_loading --type o --grid //oceantide.nc --input station.csv\n"; - cout << " Where station.csv contains station informations \n"; - cout << " format: station name, longitude, latitude\n\n"; - cout << " The file oceantide.nc contains the precalculated ocean tide loads\n"; - - exit(0); - } - - // Parser - if (vm.count("type")) - { - std::string type = vm["type"].as(); - if (type == "o") input.type = "Ocean"; - else if (type == "a") input.type = "Atmospheric"; - else throw std::runtime_error("the argument for option '--type' is invalid - only 'o' (ocean loading) or 'a' (atmospheric loading) is accepted"); - } - else - { - throw std::runtime_error("The required argument for option '--type' is missing"); - } - - std::string config_f; - std::string code_f; - std::vector location; - bool is_ecef = vm["xyz"].as(); - if (vm.count("config")) - { - config_f = vm["config"].as(); - } - if (vm.count("location")) { - location = vm["location"].as>(); - if (is_ecef) - { - if (location.size() == 3) - { - input.xyz_coords.push_back(location); - } else { - throw std::runtime_error("XYZ coordinate should have 3 values"); - } - } - else - { - input.lon.push_back(location[0]); - input.lat.push_back(location[1]); - } - if (vm.count("code")) { - input.code.push_back(vm["code"].as()); - } else { input.code.push_back("XXXX"); } - } - - input.tide_file.push_back(vm["grid"].as()); - // YAML::Node config = YAML::LoadFile(config_f ); - - // input.green = config["greenfunction"].as(); - - // YAML::Node tidefiles = config["tide"]; - // for (YAML::const_iterator it = tidefiles.begin() ; it != tidefiles.end(); ++it) { - // input.tide_file.push_back(it->as("")); - // }; - - vector row; - string word; - if (vm.count("input")) - { - string filename = vm["input"].as(); - std::ifstream infile(filename); - while(infile) { - string line; - if (!getline(infile, line)) break; - if (line.size()!= 0 and line.at(0) != '#') - { - istringstream data(line); - row.clear(); - while (getline(data, word, ',')) { - row.push_back(word); - } - - if (is_ecef) - { - if (row.size() == 4) - { - std::vector tmp; - input.code.push_back(row[0]); - tmp.push_back(stof(row[1])); - tmp.push_back(stof(row[2])); - tmp.push_back(stof(row[3])); - input.xyz_coords.push_back(tmp); - } else { - throw std::runtime_error("xyz coordinates in the csv file should have 4 values 'code, x, y, z'"); - } - } else { - if (row.size() == 3) - { - input.code.push_back(row[0]); - input.lon.push_back(stof(row[1])); - input.lat.push_back(stof(row[2])); - } - } - row.clear(); - data.clear(); - } - } - } - - if (vm.count("output")) { - input.output_blq_file = vm["output"].as(); - } else { - input.output_blq_file = "output.blq"; - } - - if (is_ecef) - { - for (int i=0; i/oceantide.nc --code 'ALIC " + "50137M0014' " + "--location 133.8855 -23.6701 \n\n"; + cout << "Example with input file: interpolate_loading --type o --grid " + "//oceantide.nc --input " + "station.csv\n"; + cout << " Where station.csv contains station informations \n"; + cout << " format: station name, longitude, latitude\n\n"; + cout << " The file oceantide.nc contains the precalculated ocean tide loads\n"; + + exit(0); + } + + // Parser + if (vm.count("type")) + { + std::string type = vm["type"].as(); + if (type == "o") + input.type = "Ocean"; + else if (type == "a") + input.type = "Atmospheric"; + else + throw std::runtime_error( + "the argument for option '--type' is invalid - only 'o' " + "(ocean loading) or 'a' (atmospheric loading) " + "is accepted" + ); + } + else + { + throw std::runtime_error("The required argument for option '--type' is missing"); + } + + std::string config_f; + std::string code_f; + std::vector location; + bool is_ecef = vm["xyz"].as(); + if (vm.count("config")) + { + config_f = vm["config"].as(); + } + if (vm.count("location")) + { + location = vm["location"].as>(); + if (is_ecef) + { + if (location.size() == 3) + { + input.xyz_coords.push_back(location); + } + else + { + throw std::runtime_error("XYZ coordinate should have 3 values"); + } + } + else + { + input.lon.push_back(location[0]); + input.lat.push_back(location[1]); + } + if (vm.count("code")) + { + input.code.push_back(vm["code"].as()); + } + else + { + input.code.push_back("XXXX"); + } + } + + input.tide_file.push_back(vm["grid"].as()); + // YAML::Node config = YAML::LoadFile(config_f ); + + // input.green = config["greenfunction"].as(); + + // YAML::Node tidefiles = config["tide"]; + // for (YAML::const_iterator it = tidefiles.begin() ; it != tidefiles.end(); ++it) { + // input.tide_file.push_back(it->as("")); + // }; + + vector row; + string word; + if (vm.count("input")) + { + string filename = vm["input"].as(); + std::ifstream infile(filename); + while (infile) + { + string line; + if (!getline(infile, line)) + break; + if (line.size() != 0 and line.at(0) != '#') + { + istringstream data(line); + row.clear(); + while (getline(data, word, ',')) + { + row.push_back(word); + } + + if (is_ecef) + { + if (row.size() == 4) + { + std::vector tmp; + input.code.push_back(row[0]); + tmp.push_back(stof(row[1])); + tmp.push_back(stof(row[2])); + tmp.push_back(stof(row[3])); + input.xyz_coords.push_back(tmp); + } + else + { + throw std::runtime_error( + "xyz coordinates in the csv file should have 4 values 'code, x, y, z'" + ); + } + } + else + { + if (row.size() == 3) + { + input.code.push_back(row[0]); + input.lon.push_back(stof(row[1])); + input.lat.push_back(stof(row[2])); + } + } + row.clear(); + data.clear(); + } + } + } + + if (vm.count("output")) + { + input.output_blq_file = vm["output"].as(); + } + else + { + input.output_blq_file = "output.blq"; + } + + if (is_ecef) + { + for (int i = 0; i < input.xyz_coords.size(); i++) + { + double tmp[3]; + double ecef[3]; + ecef[0] = input.xyz_coords[i][0]; + 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); + } + } }; - - -int main(int argc, char * argv[]) { - try { - otl_input input; - boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::info); - boost::log::add_console_log(std::cout, boost::log::keywords::format = "%Message%"); - - program_options(argc, argv, input); - - BOOST_LOG_TRIVIAL(info) << " ======== INPUT PARAMETERS ======= " << "\n"; - BOOST_LOG_TRIVIAL(info) << " - Will process the following station: " << "\n"; - for (int i = 0; i < input.code.size(); i++) - BOOST_LOG_TRIVIAL(info) << " * " << input.code[i] << " -- lon, lat -- " << input.lon[i] << ", " - << input.lat[i] << "\n"; - BOOST_LOG_TRIVIAL(info) << " - Green's function is : " << "\n" << " * " << input.green << "\n"; - BOOST_LOG_TRIVIAL(info) << " - Tide files is : " << "\n"; - for (int i = 0; i < input.tide_file.size(); i++) - BOOST_LOG_TRIVIAL(info) << " * " << input.tide_file[i] << "\n"; - BOOST_LOG_TRIVIAL(info) << " - Output file is: " << "\n" << " * " << input.output_blq_file << "\n"; - BOOST_LOG_TRIVIAL(info) << " ======== END ======= " << "\n"; - - - loadGrid tideinfo; - - tideinfo.set_name(input.tide_file[0]); - tideinfo.read(); - - - BOOST_LOG_TRIVIAL(debug) << "there is " << tideinfo.get_nwave() << " tides\n"; - input.wave_names = tideinfo.get_wave_names(); - - - - input.out_disp.resize(boost::extents[input.code.size()][tideinfo.get_nwave()][3]) ; - std::fill(input.out_disp.data(), input.out_disp.data() + input.out_disp.num_elements(), std::complex (0,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++ ) - input.out_disp[i_sta][i_wave][i_dir] = std::complex (tideinfo.interpolate(i_wave*6 + 2*i_dir, input.lon[i_sta], input.lat[i_sta]) , - tideinfo.interpolate(i_wave*6 + 2*i_dir +1, input.lon[i_sta], input.lat[i_sta]) - ); - - write_BLQ(&input, 0); - - } - catch (std::exception &e) - { - std::cerr << "\n\tERROR:\n\t\t " << e.what() << "\n"; - return -1; - } - return 0; +int main(int argc, char* argv[]) +{ + try + { + otl_input input; + boost::log::core::get()->set_filter( + boost::log::trivial::severity >= boost::log::trivial::info + ); + boost::log::add_console_log(std::cout, boost::log::keywords::format = "%Message%"); + + program_options(argc, argv, input); + + BOOST_LOG_TRIVIAL(info) << " ======== INPUT PARAMETERS ======= " << "\n"; + BOOST_LOG_TRIVIAL(info) << " - Will process the following station: " << "\n"; + for (int i = 0; i < input.code.size(); i++) + BOOST_LOG_TRIVIAL(info) << " * " << input.code[i] << " -- lon, lat -- " + << input.lon[i] << ", " << input.lat[i] << "\n"; + BOOST_LOG_TRIVIAL(info) << " - Green's function is : " << "\n" + << " * " << input.green << "\n"; + BOOST_LOG_TRIVIAL(info) << " - Tide files is : " << "\n"; + for (int i = 0; i < input.tide_file.size(); i++) + BOOST_LOG_TRIVIAL(info) << " * " << input.tide_file[i] << "\n"; + BOOST_LOG_TRIVIAL(info) << " - Output file is: " << "\n" + << " * " << input.output_blq_file << "\n"; + BOOST_LOG_TRIVIAL(info) << " ======== END ======= " << "\n"; + + loadGrid tideinfo; + + tideinfo.set_name(input.tide_file[0]); + tideinfo.read(); + + BOOST_LOG_TRIVIAL(debug) << "there is " << tideinfo.get_nwave() << " tides\n"; + input.wave_names = tideinfo.get_wave_names(); + + input.out_disp.resize(boost::extents[input.code.size()][tideinfo.get_nwave()][3]); + std::fill( + input.out_disp.data(), + input.out_disp.data() + input.out_disp.num_elements(), + std::complex(0, 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++) + input.out_disp[i_sta][i_wave][i_dir] = std::complex( + tideinfo.interpolate( + i_wave * 6 + 2 * i_dir, + input.lon[i_sta], + input.lat[i_sta] + ), + tideinfo.interpolate( + i_wave * 6 + 2 * i_dir + 1, + input.lon[i_sta], + input.lat[i_sta] + ) + ); + + write_BLQ(&input, 0); + } + catch (std::exception& e) + { + std::cerr << "\n\tERROR:\n\t\t " << e.what() << "\n"; + return -1; + } + return 0; } diff --git a/src/cpp/loading/load_functions.cpp b/src/cpp/loading/load_functions.cpp index 0a44b9cd1..d3bcf70f6 100644 --- a/src/cpp/loading/load_functions.cpp +++ b/src/cpp/loading/load_functions.cpp @@ -5,210 +5,217 @@ * */ -#include "load_functions.h" - +#include "loading/load_functions.h" #include -#include #include #include - -#include "loading.h" -#include "tide.h" -#include "input_otl.h" -#include "boost_ma_type.h" -#include "utils.h" +#include +#include "loading/boost_ma_type.h" +#include "loading/input_otl.h" +#include "loading/loading.h" +#include "loading/tide.h" +#include "loading/utils.h" /** Compute the loading of a single point */ void load_1_point( - tide* tide_info, ///< vector of classes containing the tide grids - otl_input* input, ///< class containing the coordinates information and also the loading vector - loading load, ///< class containing the Green's function - int idx) ///< index of the point in the list + tide* tide_info, ///< vector of classes containing the tide grids + otl_input* input, ///< class containing the coordinates information and also the loading vector + loading load, ///< class containing the Green's function + int idx ///< index of the point in the list +) { - MA2d greenZ; - MA2d greenNS; - MA2d greenEW; - - greenZ. resize(boost::extents[tide_info[0].get_nlat()][tide_info[0].get_nlon()]); - greenNS.resize(boost::extents[tide_info[0].get_nlat()][tide_info[0].get_nlon()]); - greenEW.resize(boost::extents[tide_info[0].get_nlat()][tide_info[0].get_nlon()]); - - auto greenZ_it = greenZ.origin(); - auto greenEW_it = greenEW.origin(); - auto greenNS_it = greenNS.origin(); - float lat0 = input->lat[idx]; - float lon0 = input->lon[idx]; - - for (float *lat_ptr = tide_info[0].get_lat_ptr(); lat_ptr != tide_info[0].get_lat_ptr_end(); lat_ptr++) - for (float *lon_ptr = tide_info[0].get_lon_ptr(); lon_ptr < tide_info[0].get_lon_ptr_end(); lon_ptr++) - { - double dist, azimuth; - calcDistanceBearing(&lat0, &lon0, lat_ptr, lon_ptr, &dist, &azimuth); - - *greenZ_it = load.interpolate_gz(dist); - *greenNS_it = load.interpolate_gh(dist) * cos(azimuth); - *greenEW_it = load.interpolate_gh(dist) * sin(azimuth); - - // *greenNS_it *= cos(azimuth); - // *greenEW_it *= sin(azimuth); - if (*greenZ_it != *greenZ_it) - { - std::cout << " nan detected for " << *lat_ptr << " " <<*lon_ptr << "\n"; - std::cout << dist << " " << azimuth << "\n"; - exit(0); - } - greenZ_it++; - greenNS_it++; - greenEW_it++; - } - - // Computing load - for (int it = 0 ; it < input->tide_file.size() ; it++ ) - { - auto tideim_it = tide_info[it].get_out_ptr(); - auto gz_it = greenZ.origin(); - auto gNS_it = greenNS.origin(); - auto gEW_it = greenEW.origin(); - for (auto tidere_it = tide_info[it].get_in_ptr(); - tidere_it != tide_info[it].get_in_ptr_end(); - tidere_it++, tideim_it++, gz_it++, gNS_it++, gEW_it++) - { - - input->dispEW_in[idx][it] += *gEW_it * *tidere_it; - input->dispEW_out[idx][it]+= *gEW_it * *tideim_it; - input->dispZ_in[idx][it] += *gz_it * *tidere_it; - input->dispZ_out[idx][it] += *gz_it * *tideim_it; - input->dispNS_in[idx][it] += *gNS_it * *tidere_it; - input->dispNS_out[idx][it]+= *gNS_it * *tideim_it; - - } - } + MA2d greenZ; + MA2d greenNS; + MA2d greenEW; + + greenZ.resize(boost::extents[tide_info[0].get_nlat()][tide_info[0].get_nlon()]); + greenNS.resize(boost::extents[tide_info[0].get_nlat()][tide_info[0].get_nlon()]); + greenEW.resize(boost::extents[tide_info[0].get_nlat()][tide_info[0].get_nlon()]); + + auto greenZ_it = greenZ.origin(); + auto greenEW_it = greenEW.origin(); + auto greenNS_it = greenNS.origin(); + float lat0 = input->lat[idx]; + float lon0 = input->lon[idx]; + + for (float* lat_ptr = tide_info[0].get_lat_ptr(); lat_ptr != tide_info[0].get_lat_ptr_end(); + lat_ptr++) + for (float* lon_ptr = tide_info[0].get_lon_ptr(); lon_ptr < tide_info[0].get_lon_ptr_end(); + lon_ptr++) + { + double dist, azimuth; + calcDistanceBearing(&lat0, &lon0, lat_ptr, lon_ptr, &dist, &azimuth); + + *greenZ_it = load.interpolate_gz(dist); + *greenNS_it = load.interpolate_gh(dist) * cos(azimuth); + *greenEW_it = load.interpolate_gh(dist) * sin(azimuth); + + // *greenNS_it *= cos(azimuth); + // *greenEW_it *= sin(azimuth); + if (*greenZ_it != *greenZ_it) + { + std::cout << " nan detected for " << *lat_ptr << " " << *lon_ptr << "\n"; + std::cout << dist << " " << azimuth << "\n"; + exit(0); + } + greenZ_it++; + greenNS_it++; + greenEW_it++; + } + + // Computing load + for (int it = 0; it < input->tide_file.size(); it++) + { + auto tideim_it = tide_info[it].get_out_ptr(); + auto gz_it = greenZ.origin(); + auto gNS_it = greenNS.origin(); + auto gEW_it = greenEW.origin(); + for (auto tidere_it = tide_info[it].get_in_ptr(); + tidere_it != tide_info[it].get_in_ptr_end(); + tidere_it++, tideim_it++, gz_it++, gNS_it++, gEW_it++) + { + input->dispEW_in[idx][it] += *gEW_it * *tidere_it; + input->dispEW_out[idx][it] += *gEW_it * *tideim_it; + input->dispZ_in[idx][it] += *gz_it * *tidere_it; + input->dispZ_out[idx][it] += *gz_it * *tideim_it; + input->dispNS_in[idx][it] += *gNS_it * *tidere_it; + input->dispNS_out[idx][it] += *gNS_it * *tideim_it; + } + } } -void write_BLQ(otl_input *input, int mode) +void write_BLQ(otl_input* input, int mode) { - std::ofstream out ; - out.open(input->output_blq_file); - - out << "$$ " << input->type << " loading displacement\n"; - out << "$$\n"; - out << "$$ OUTPUT OF interpolate_loading\n"; - out << "$$ Processed " << input->tide_file.size() << " tides\n"; - for (unsigned int i = 0; i < input->tide_file.size(); i++ ) - out << "$$ - " << input->tide_file[i] << "\n"; - out << "$$\n"; - out << "$$ COLUMN ORDER:"; - for (unsigned int i = 0; i < input->wave_names.size(); i++ ) - out << " " << std::setw(3) << input->wave_names[i]; - out << "\n"; - out << "$$\n"; - out << "$$ ROW ORDER:\n"; - out << "$$ AMPLITUDES (m)\n"; - out << "$$ RADIAL\n"; - out << "$$ TANGENTL EW\n"; - out << "$$ TANGENTL NS\n"; - out << "$$ PHASES (degrees)\n"; - out << "$$ RADIAL\n"; - out << "$$ TANGENTL EW\n"; - out << "$$ TANGENTL NS\n"; - out << "$$\n"; - out << "$$ CMC: NO (corr.tide centre of mass)\n"; - out << "$$\n"; - out << "$$ END HEADER\n"; - out << "$$\n"; - - for (unsigned int i = 0; i < input->code.size(); i++) - { - out << std::fixed << std::setprecision(4); - out << " " << input->code[i] << "\n"; - out << "$$ " << input->code[i] << " RADI TANG lon/lat: " << std::setw(9) << input->lon[i] << " " << std::setw(9) << input->lat[i] << "\n"; - - // write Amplitudes - for (int i_dir = 0; i_dir < 3; i_dir++) - { - for (int it = 0; it < input->wave_names.size(); it++) // Will need to write nwaves - out << " " << std::setprecision(7) << std::setw(10) << std::abs(input->out_disp[i][it][i_dir]) << " "; - out << "\n"; - } - - // Now write the phases - for (int i_dir = 0; i_dir < 3; i_dir++) - { - for (int it = 0; it < input->wave_names.size(); it++) // Will need to write nwaves - out << " " << std::setprecision(4) << std::setw(10) << std::arg(input->out_disp[i][it][i_dir]) * R2D << " "; - out << "\n"; - } - } - - out << "$$ END TABLE\n"; - out.close(); + std::ofstream out; + out.open(input->output_blq_file); + + out << "$$ " << input->type << " loading displacement\n"; + out << "$$\n"; + out << "$$ OUTPUT OF interpolate_loading\n"; + out << "$$ Processed " << input->tide_file.size() << " tides\n"; + for (unsigned int i = 0; i < input->tide_file.size(); i++) + out << "$$ - " << input->tide_file[i] << "\n"; + out << "$$\n"; + out << "$$ COLUMN ORDER:"; + for (unsigned int i = 0; i < input->wave_names.size(); i++) + out << " " << std::setw(3) << input->wave_names[i]; + out << "\n"; + out << "$$\n"; + out << "$$ ROW ORDER:\n"; + out << "$$ AMPLITUDES (m)\n"; + out << "$$ RADIAL\n"; + out << "$$ TANGENTL EW\n"; + out << "$$ TANGENTL NS\n"; + out << "$$ PHASES (degrees)\n"; + out << "$$ RADIAL\n"; + out << "$$ TANGENTL EW\n"; + out << "$$ TANGENTL NS\n"; + out << "$$\n"; + out << "$$ CMC: NO (corr.tide centre of mass)\n"; + out << "$$\n"; + out << "$$ END HEADER\n"; + out << "$$\n"; + + for (unsigned int i = 0; i < input->code.size(); i++) + { + out << std::fixed << std::setprecision(4); + out << " " << input->code[i] << "\n"; + out << "$$ " << input->code[i] << " RADI TANG lon/lat: " << std::setw(9) + << input->lon[i] << " " << std::setw(9) << input->lat[i] << "\n"; + + // write Amplitudes + for (int i_dir = 0; i_dir < 3; i_dir++) + { + for (int it = 0; it < input->wave_names.size(); it++) // Will need to write nwaves + out << " " << std::setprecision(7) << std::setw(10) + << std::abs(input->out_disp[i][it][i_dir]) << " "; + out << "\n"; + } + + // Now write the phases + for (int i_dir = 0; i_dir < 3; i_dir++) + { + for (int it = 0; it < input->wave_names.size(); it++) // Will need to write nwaves + out << " " << std::setprecision(4) << std::setw(10) + << std::arg(input->out_disp[i][it][i_dir]) * R2D << " "; + out << "\n"; + } + } + + out << "$$ END TABLE\n"; + out.close(); } -void write_BLQ(otl_input *input) +void write_BLQ(otl_input* input) { - std::ofstream out ; - out.open(input->output_blq_file); - - out << "$$ " << input->type << " loading displacement\n"; - out << "$$\n"; - out << "$$ OUTPUT OF make_otl_blq\n"; - out << "$$ Processed " << input->tide_file.size() << " tides\n"; - for (unsigned int i = 0; i < input->tide_file.size(); i++ ) - out << "$$ - " << input->tide_file[i] << "\n"; - out << "$$\n"; - out << "$$ Green function used is " << input->green << "\n"; - out << "$$\n"; - out << "$$ COLUMN ORDER:"; - for (unsigned int i = 0; i < input->wave_names.size(); i++ ) - out << " " << std::setw(3) << input->wave_names[i]; - out << "\n"; - out << "$$\n"; - out << "$$ ROW ORDER:\n"; - out << "$$ AMPLITUDES (m)\n"; - out << "$$ RADIAL\n"; - out << "$$ TANGENTL EW\n"; - out << "$$ TANGENTL NS\n"; - out << "$$ PHASES (degrees)\n"; - out << "$$ RADIAL\n"; - out << "$$ TANGENTL EW\n"; - out << "$$ TANGENTL NS\n"; - out << "$$\n"; - out << "$$ CMC: NO (corr.tide centre of mass)\n"; - out << "$$\n"; - out << "$$ END HEADER\n"; - out << "$$\n"; - - for (unsigned int i = 0; i < input->code.size(); i++) - { - out << std::fixed << std::setprecision(4); - out << " " << input->code[i] << "\n"; - out << "$$ " << input->code[i] << " RADI TANG lon/lat: " << std::setw(9) << input->lon[i] << " " << std::setw(9) << input->lat[i] << "\n"; - - // write Amplitudes - for (int it = 0; it < input->tide_file.size(); it++) - out << " " << std::setprecision(5) << std::setw(8) << sqrt(SQR(input->dispZ_out [i][it]) + SQR(input->dispZ_in [i][it])) << " "; - out << "\n"; - for (int it = 0; it < input->tide_file.size(); it++) - out << " " << std::setprecision(5) << std::setw(8) << sqrt(SQR(input->dispEW_out[i][it]) + SQR(input->dispEW_in[i][it])) << " "; - out << "\n"; - for (int it = 0; it < input->tide_file.size(); it++) - out << " " << std::setprecision(5) << std::setw(8) << sqrt(SQR(input->dispNS_out[i][it]) + SQR(input->dispNS_in[i][it])) << " "; - out << "\n"; - - for (int it = 0; it < input->tide_file.size(); it++) - out << " " << std::setprecision(1) << std::setw(8) << atan2(input->dispZ_out [i][it], input->dispZ_in [i][it]) * R2D << " "; - out << "\n"; - for (int it = 0; it < input->tide_file.size(); it++) - out << " " << std::setprecision(1) << std::setw(8) << atan2(input->dispEW_out[i][it], input->dispEW_in[i][it]) * R2D << " "; - out << "\n"; - for (int it = 0; it < input->tide_file.size(); it++) - out << " " << std::setprecision(1) << std::setw(8) << atan2(input->dispNS_out[i][it], input->dispNS_in[i][it]) * R2D << " "; - out << "\n"; - } - - out << "$$ END TABLE\n"; - out.close(); + std::ofstream out; + out.open(input->output_blq_file); + + out << "$$ " << input->type << " loading displacement\n"; + out << "$$\n"; + out << "$$ OUTPUT OF make_otl_blq\n"; + out << "$$ Processed " << input->tide_file.size() << " tides\n"; + for (unsigned int i = 0; i < input->tide_file.size(); i++) + out << "$$ - " << input->tide_file[i] << "\n"; + out << "$$\n"; + out << "$$ Green function used is " << input->green << "\n"; + out << "$$\n"; + out << "$$ COLUMN ORDER:"; + for (unsigned int i = 0; i < input->wave_names.size(); i++) + out << " " << std::setw(3) << input->wave_names[i]; + out << "\n"; + out << "$$\n"; + out << "$$ ROW ORDER:\n"; + out << "$$ AMPLITUDES (m)\n"; + out << "$$ RADIAL\n"; + out << "$$ TANGENTL EW\n"; + out << "$$ TANGENTL NS\n"; + out << "$$ PHASES (degrees)\n"; + out << "$$ RADIAL\n"; + out << "$$ TANGENTL EW\n"; + out << "$$ TANGENTL NS\n"; + out << "$$\n"; + out << "$$ CMC: NO (corr.tide centre of mass)\n"; + out << "$$\n"; + out << "$$ END HEADER\n"; + out << "$$\n"; + + for (unsigned int i = 0; i < input->code.size(); i++) + { + out << std::fixed << std::setprecision(4); + out << " " << input->code[i] << "\n"; + out << "$$ " << input->code[i] << " RADI TANG lon/lat: " << std::setw(9) + << input->lon[i] << " " << std::setw(9) << input->lat[i] << "\n"; + + // write Amplitudes + for (int it = 0; it < input->tide_file.size(); it++) + out << " " << std::setprecision(5) << std::setw(8) + << sqrt(SQR(input->dispZ_out[i][it]) + SQR(input->dispZ_in[i][it])) << " "; + out << "\n"; + for (int it = 0; it < input->tide_file.size(); it++) + out << " " << std::setprecision(5) << std::setw(8) + << sqrt(SQR(input->dispEW_out[i][it]) + SQR(input->dispEW_in[i][it])) << " "; + out << "\n"; + for (int it = 0; it < input->tide_file.size(); it++) + out << " " << std::setprecision(5) << std::setw(8) + << sqrt(SQR(input->dispNS_out[i][it]) + SQR(input->dispNS_in[i][it])) << " "; + out << "\n"; + + for (int it = 0; it < input->tide_file.size(); it++) + out << " " << std::setprecision(1) << std::setw(8) + << atan2(input->dispZ_out[i][it], input->dispZ_in[i][it]) * R2D << " "; + out << "\n"; + for (int it = 0; it < input->tide_file.size(); it++) + out << " " << std::setprecision(1) << std::setw(8) + << atan2(input->dispEW_out[i][it], input->dispEW_in[i][it]) * R2D << " "; + out << "\n"; + for (int it = 0; it < input->tide_file.size(); it++) + out << " " << std::setprecision(1) << std::setw(8) + << atan2(input->dispNS_out[i][it], input->dispNS_in[i][it]) * R2D << " "; + out << "\n"; + } + + out << "$$ END TABLE\n"; + out.close(); } - - diff --git a/src/cpp/loading/load_functions.h b/src/cpp/loading/load_functions.h index b80306385..59a2b850b 100644 --- a/src/cpp/loading/load_functions.h +++ b/src/cpp/loading/load_functions.h @@ -4,15 +4,11 @@ * @date 5/3/21 * */ - #pragma once -#include "tide.h" -#include "input_otl.h" -#include "loading.h" - -void load_1_point(tide *tide_info, otl_input *input, loading load, int idx); -void write_BLQ(otl_input *input); -void write_BLQ(otl_input *input, int code); - - +#include "loading/input_otl.h" +#include "loading/loading.h" +#include "loading/tide.h" +void load_1_point(tide* tide_info, otl_input* input, loading load, int idx); +void write_BLQ(otl_input* input); +void write_BLQ(otl_input* input, int code); diff --git a/src/cpp/loading/loadgrid.cpp b/src/cpp/loading/loadgrid.cpp index 8202ef3ff..99a18706b 100644 --- a/src/cpp/loading/loadgrid.cpp +++ b/src/cpp/loading/loadgrid.cpp @@ -5,101 +5,104 @@ * */ -#include "loadgrid.h" +#include "loading/loadgrid.h" +#include +#include +#include +#include #include #include -#include -#include -#include -#include - using namespace std; using namespace netCDF; using namespace netCDF::exceptions; using namespace boost::math::interpolators; -loadGrid::loadGrid(std::string name) : fileName(name ){ - return; -}; - -loadGrid::~loadGrid() +loadGrid::loadGrid(std::string name) : fileName(name) { -// amplitude.resize(boost::extents[0][0]); -// phase.resize(boost::extents[0][0]); -// return ; + return; }; - - -void loadGrid::set_name(std::string name) { - fileName = name; - return ; +loadGrid::~loadGrid() { + // amplitude.resize(boost::extents[0][0]); + // phase.resize(boost::extents[0][0]); + // return ; }; +void loadGrid::set_name(std::string name) +{ + fileName = name; + return; +}; -void loadGrid::read(){ - try{ - //std::cout << fileName << "\n"; - NcFile datafile(fileName, NcFile::read); - nWave = datafile.getDim("nwaves").getSize(); - //std::vector test; - //NcGroupAtt attr = datafile.getAtt("wave_name"); - NcGroupAtt attr = datafile.getAtt("wave_name"); - cout << attr.getAttLength() <<"\n"; - cout << attr.getType().getTypeClassName() << "\n"; - //std::string test; - //test = new char[5]; - std::string test; - //test.resize(2); - attr.getValues(test); - boost::to_upper(test); - - std::vector words; - boost::split(wave_names, test, boost::is_any_of(", "), boost::token_compress_on); - - NcVar lat_var = datafile.getVar("lat"); - nLat = lat_var.getDim(0).getSize() ; - - NcVar lon_var = datafile.getVar("lon"); - nLon = lon_var.getDim(0).getSize() ; - - lat.resize(boost::extents[nLat]); - lon.resize(boost::extents[nLon]); - - lat_var.getVar(lat.origin()); - lon_var.getVar(lon.origin()); - load.resize(boost::extents[nWave][nLat][nLon]); - NcVar amp_var = datafile.getVar("waves"); - amp_var.getVar(load.origin()); - - datafile.close(); - //cout << "1600,400 => " << amplitude[1600][400] << " n n " << amplitude[400][1600] << "\n"; - }catch(NcException &e) - { - throw e; - } - return ; +void loadGrid::read() +{ + try + { + // std::cout << fileName << "\n"; + NcFile datafile(fileName, NcFile::read); + nWave = datafile.getDim("nwaves").getSize(); + // std::vector test; + // NcGroupAtt attr = datafile.getAtt("wave_name"); + NcGroupAtt attr = datafile.getAtt("wave_name"); + cout << attr.getAttLength() << "\n"; + cout << attr.getType().getTypeClassName() << "\n"; + // std::string test; + // test = new char[5]; + std::string test; + // test.resize(2); + attr.getValues(test); + boost::to_upper(test); + + std::vector words; + boost::split(wave_names, test, boost::is_any_of(", "), boost::token_compress_on); + + NcVar lat_var = datafile.getVar("lat"); + nLat = lat_var.getDim(0).getSize(); + + NcVar lon_var = datafile.getVar("lon"); + nLon = lon_var.getDim(0).getSize(); + + lat.resize(boost::extents[nLat]); + lon.resize(boost::extents[nLon]); + + lat_var.getVar(lat.origin()); + lon_var.getVar(lon.origin()); + load.resize(boost::extents[nWave][nLat][nLon]); + NcVar amp_var = datafile.getVar("waves"); + amp_var.getVar(load.origin()); + + datafile.close(); + // cout << "1600,400 => " << amplitude[1600][400] << " n n " << amplitude[400][1600] << + // "\n"; + } + catch (NcException& e) + { + throw e; + } + return; }; float loadGrid::interpolate(int itide, float lon_, float lat_) { - MA1f lon_val; - lon_val.resize(boost::extents[nLat]); -// std::vector< cubic_b_spline > splines; -// splines.resize(nLat); - for (int iLat = 0; iLat < nLat; iLat ++ ) - { - cardinal_cubic_b_spline splines(load[itide][iLat].origin(),nLon, lon[0], static_cast(1.0)); - lon_val[iLat] = splines( lon_ ); -// cout << "for lat " << lat[iLat] << "\t" << lon_val[iLat] <<"\n"; - } - - cardinal_cubic_b_spline splines2(lon_val.origin(),nLat, lat[0], static_cast(1.0)); - return splines2(lat_); -// for (int iWave = 0; iWave < nWave ; iWave++) -// { -// lon_slice = -// } - return 0; + MA1f lon_val; + lon_val.resize(boost::extents[nLat]); + // std::vector< cubic_b_spline > splines; + // splines.resize(nLat); + for (int iLat = 0; iLat < nLat; iLat++) + { + cardinal_cubic_b_spline + splines(load[itide][iLat].origin(), nLon, lon[0], static_cast(1.0)); + lon_val[iLat] = splines(lon_); + // cout << "for lat " << lat[iLat] << "\t" << lon_val[iLat] <<"\n"; + } + + cardinal_cubic_b_spline + splines2(lon_val.origin(), nLat, lat[0], static_cast(1.0)); + return splines2(lat_); + // for (int iWave = 0; iWave < nWave ; iWave++) + // { + // lon_slice = + // } + return 0; } \ No newline at end of file diff --git a/src/cpp/loading/loadgrid.h b/src/cpp/loading/loadgrid.h index 8a660766b..71c4e4945 100644 --- a/src/cpp/loading/loadgrid.h +++ b/src/cpp/loading/loadgrid.h @@ -4,51 +4,47 @@ * @date 8/4/21 * */ - #pragma once -#include #include -#include "boost_ma_type.h" - +#include +#include "loading/boost_ma_type.h" /** * Implementation of the class to manage the tides. */ -class loadGrid { -public: - loadGrid(){}; - loadGrid(std::string name); - ~loadGrid(); - void set_name(std::string name); - void read(); - size_t get_nlon(){return nLon;}; - size_t get_nlat(){return nLat;}; - size_t get_nwave(){return static_cast (nWave/6) ;}; - - - float get_lat(size_t i){return lat[i];}; - float get_lon(size_t i){return lon[i];}; - - float * get_lat_ptr(){return lat.origin();}; - float * get_lon_ptr(){return lon.origin();}; - - float * get_lat_ptr_end(){return lat.origin()+lat.num_elements();}; - float * get_lon_ptr_end(){return lon.origin()+lon.num_elements();}; - - float interpolate(int, float, float); - - std::vector < std::string > get_wave_names(){return wave_names; }; -private: - std::string fileName; - std::vector wave_names; - size_t nLon; - size_t nLat; - size_t nWave; - MA1f lat; - MA1f lon; - MA3f load; - float fillNan; - - +class loadGrid +{ + public: + loadGrid() {}; + loadGrid(std::string name); + ~loadGrid(); + void set_name(std::string name); + void read(); + size_t get_nlon() { return nLon; }; + size_t get_nlat() { return nLat; }; + size_t get_nwave() { return static_cast(nWave / 6); }; + + float get_lat(size_t i) { return lat[i]; }; + float get_lon(size_t i) { return lon[i]; }; + + float* get_lat_ptr() { return lat.origin(); }; + float* get_lon_ptr() { return lon.origin(); }; + + float* get_lat_ptr_end() { return lat.origin() + lat.num_elements(); }; + float* get_lon_ptr_end() { return lon.origin() + lon.num_elements(); }; + + float interpolate(int, float, float); + + std::vector get_wave_names() { return wave_names; }; + + private: + std::string fileName; + std::vector wave_names; + size_t nLon; + size_t nLat; + size_t nWave; + MA1f lat; + MA1f lon; + MA3f load; + float fillNan; }; - diff --git a/src/cpp/loading/loading.cpp b/src/cpp/loading/loading.cpp index 188383023..b5728097c 100644 --- a/src/cpp/loading/loading.cpp +++ b/src/cpp/loading/loading.cpp @@ -5,91 +5,103 @@ * */ -#include "loading.h" +#include "loading/loading.h" +#include +#include #include -#include #include +#include #include -#include - -#include - -const double PI = std::atan(1.0)*4; - using namespace std; -double interpolate( vector &xData, vector &yData, double x, bool extrapolate ) +const double PI = std::atan(1.0) * 4; + +double interpolate(vector& xData, vector& yData, double x, bool extrapolate) { - int size = xData.size(); - int i = 0;// find left end of interval for interpolation + int size = xData.size(); + int i = 0; // find left end of interval for interpolation - if (x==0) return (double) 0.0; - if ( x >= xData[size - 2] ) // special case: beyond right end - { - i = size - 2; - } - else - { - while ( x > xData[i+1] ) i++; - } - double xL = xData[i], yL = yData[i], xR = xData[i+1], yR = yData[i+1]; // points on either side (unless beyond ends) - if ( !extrapolate ) // if beyond ends of array and not extrapolating - { - if ( x < xL ) yR = yL; - if ( x > xR ) yL = yR; - } - double dydx = ( yR - yL ) / ( xR - xL ); // gradient - double ret = yL + dydx * ( x - xL ); - return ret; // linear interpolation + if (x == 0) + return (double)0.0; + if (x >= xData[size - 2]) // special case: beyond right end + { + i = size - 2; + } + else + { + while (x > xData[i + 1]) + i++; + } + double xL = xData[i], yL = yData[i], xR = xData[i + 1], + yR = yData[i + 1]; // points on either side (unless beyond ends) + if (!extrapolate) // if beyond ends of array and not extrapolating + { + if (x < xL) + yR = yL; + if (x > xR) + yL = yR; + } + double dydx = (yR - yL) / (xR - xL); // gradient + double ret = yL + dydx * (x - xL); + return ret; // linear interpolation }; loading::loading() {}; -loading::loading(std::string fname): - fileName(fname) +loading::loading(std::string fname) : fileName(fname) { - return ; + return; }; -void loading::set_name(std::string name) {fileName=name; return;}; +void loading::set_name(std::string name) +{ + fileName = name; + return; +}; -void loading::read() { - n_green = 50; //@TODO read from the file. - dist.reserve(n_green); - Gz.reserve(n_green); - Gh.reserve(n_green); +void loading::read() +{ + n_green = 50; //@TODO read from the file. + dist.reserve(n_green); + Gz.reserve(n_green); + Gh.reserve(n_green); - ifstream infile(fileName); - if(infile.fail()) - { - BOOST_LOG_TRIVIAL(error) << fileName << " doesn't exist\n\t"; - exit(0); - } - string line; - int i=0; - while (getline(infile, line)) - { - if (line.size()!= 0 and line.at(0) != '#') - { - istringstream iss(line); - double d, gz, gzh, gh, ghz; - if (!(iss >> d >> gz >> gzh >> gh >> ghz)) { break; } // error - dist.push_back(d) ; - Gz.push_back(gz / (6371e3 * dist[i] * PI/180.0) * 1e-12) ; - Gh.push_back(gh / (6371e3 * dist[i] * PI/180.0) * 1e-12) ; - dist[i] *= PI/180.0; - i++; - } - } - BOOST_LOG_TRIVIAL(error) << fileName << " greens function has been loaded\n\t"; - return ; + ifstream infile(fileName); + if (infile.fail()) + { + BOOST_LOG_TRIVIAL(error) << fileName << " doesn't exist\n\t"; + exit(0); + } + string line; + int i = 0; + while (getline(infile, line)) + { + if (line.size() != 0 and line.at(0) != '#') + { + istringstream iss(line); + double d, gz, gzh, gh, ghz; + if (!(iss >> d >> gz >> gzh >> gh >> ghz)) + { + break; + } // error + dist.push_back(d); + Gz.push_back(gz / (6371e3 * dist[i] * PI / 180.0) * 1e-12); + Gh.push_back(gh / (6371e3 * dist[i] * PI / 180.0) * 1e-12); + dist[i] *= PI / 180.0; + i++; + } + } + BOOST_LOG_TRIVIAL(error) << fileName << " greens function has been loaded\n\t"; + return; } -double loading::interpolate_gz(double x) { - return interpolate( dist, Gz, x, false ); +double loading::interpolate_gz(double x) +{ + return interpolate(dist, Gz, x, false); } -double loading::interpolate_gh(double x) { - return interpolate( dist, Gh, x, false ); +double loading::interpolate_gh(double x) +{ + return interpolate(dist, Gh, x, false); } \ No newline at end of file diff --git a/src/cpp/loading/loading.h b/src/cpp/loading/loading.h index 228499ae2..858228758 100644 --- a/src/cpp/loading/loading.h +++ b/src/cpp/loading/loading.h @@ -4,29 +4,25 @@ * @date 26/2/21 * */ - #pragma once #include #include +class loading +{ + public: + loading(); + loading(std::string); + ~loading() {}; + void set_name(std::string name); + void read(); + double interpolate_gz(double); + double interpolate_gh(double); - -class loading { -public: - loading(); - loading(std::string); - ~loading(){}; - void set_name(std::string name); - void read(); - double interpolate_gz(double); - double interpolate_gh(double); - -private: - std::string fileName; - int n_green; - std::vector dist; - std::vector Gz; - std::vector Gh; - + private: + std::string fileName; + int n_green; + std::vector dist; + std::vector Gz; + std::vector Gh; }; - diff --git a/src/cpp/loading/make_otl_blq.cpp b/src/cpp/loading/make_otl_blq.cpp index 5ccc281a1..7ea1dd2e3 100644 --- a/src/cpp/loading/make_otl_blq.cpp +++ b/src/cpp/loading/make_otl_blq.cpp @@ -3,9 +3,10 @@ * * @brief Program to compute the ocean tide loading from a tide grid * - * With an input data containing the list of the tide file, the Green's function and the coordinate of the site of interest, - * this code will compute the loading (response to the solid earth to the load generated by the tide) displacement to the tide. - * It include the vertical, N-S and E-W load in amplitude and phase at the tidal frequencies. + * With an input data containing the list of the tide file, the Green's function and the coordinate + * of the site of interest, this code will compute the loading (response to the solid earth to the + * load generated by the tide) displacement to the tide. It include the vertical, N-S and E-W load + * in amplitude and phase at the tidal frequencies. * * @version 0.2 * @@ -13,51 +14,46 @@ * @date 26/02/2021 */ - -#include +#include +#include +#include +#include #include - -#ifdef ENABLE_PARALLELISATION - #include "omp.h" -#endif - +#include #include -#include -#include -#include -#include +#include "loading/input_otl.h" +#include "loading/load_functions.h" +#include "loading/loading.h" +#include "loading/tide.h" +#include "loading/utils.h" +#include "omp.h" -#include "tide.h" -#include "loading.h" -#include "utils.h" -#include "input_otl.h" -#include "load_functions.h" +namespace po = boost::program_options; using namespace std; using namespace boost::timer; -namespace po = boost::program_options; + +#ifdef ENABLE_PARALLELISATION +#endif const int THREAD_COUNT = 8; -void expand_path( - string& path) +void expand_path(string& path) { - char* home = std::getenv("HOME"); - if (path[0] == '~' - &&home) - { - path.erase(0, 1); - path.insert(0, home); - } + char* home = std::getenv("HOME"); + if (path[0] == '~' && home) + { + path.erase(0, 1); + path.insert(0, home); + } } - -void program_options(int argc, char * argv[], otl_input & input) +void program_options(int argc, char* argv[], otl_input& input) { - po::options_description desc{"make_otl_blq "}; + po::options_description desc{"make_otl_blq "}; - // Do not set default values here, as this will overide the configuration file opitions!!! - desc.add_options() + // 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") @@ -71,264 +67,296 @@ void program_options(int argc, char * argv[], otl_input & input) ; - po::variables_map vm; - // This is to be able to parse negative numbers with boost. - po::store(po::command_line_parser(argc, argv).options(desc).style( - po::command_line_style::unix_style ^ po::command_line_style::allow_short - ).run(), vm); - po::notify(vm); - - - if (vm.count("help")) { - cout << "Usage: make_otl_blq [options]\n"; - cout << desc << "\n\n"; - cout << "Example: make_otl_blq --type o --config otl.yaml --code 'ALIC 50137M0014' --location 133.8855 -23.6701 \n\n"; - cout << "Example with input file: make_otl_blq --type o --config otl.yaml --input station.csv\n"; - cout << " Where station.csv contains station informations \n"; - cout << " format: station name, longitude, latitude\n\n"; - - exit(0); - } - - // Parser - if (vm.count("type")) - { - std::string type = vm["type"].as(); - if (type == "o") input.type = "Ocean"; - else if (type == "a") input.type = "Atmospheric"; - else throw std::runtime_error("the argument for option '--type' is invalid - only 'o' (ocean loading) or 'a' (atmospheric loading) is accepted"); - } - else - { - throw std::runtime_error("The required argument for option '--type' is missing"); - } - - std::string config_f; - std::string code_f; - std::vector location; - bool is_ecef = vm["xyz"].as(); - if (vm.count("config")) - { - config_f = vm["config"].as(); - } - if (vm.count("location")) { - location = vm["location"].as>(); - if (is_ecef) - { - /// \todo Need to have a check if there is 3 values in the vector. - input.xyz_coords.push_back(location); - - } - else - { - input.lon.push_back(location[0]); - input.lat.push_back(location[1]); - } - - if (vm.count("code")) { - input.code.push_back(vm["code"].as()); - } else { input.code.push_back("XXXX"); } - } - // std::cout << input.xyz_coords[0][0] << input.xyz_coords[0][1] << input.xyz_coords[0][2] << "\n"; - // exit(0); - if (config_f.length() == 0) - { - struct noconfig : std::exception { - const char* what() const noexcept { return "No config file supplied"; } - } error; - throw error; - } - YAML::Node config = YAML::LoadFile(config_f ); - - if (config["greenfunction"]) - { - input.green = config["greenfunction"].as(); - if (input.green.length() == 0) - { - struct nogf : std::exception { - const char* what() const noexcept { return "No green function defined"; } - } error; - throw error; - } - } - else - { - struct nogfd : std::exception { - const char* what() const noexcept { return "No greenfunction config entry"; } - } error; - throw error; - } - expand_path(input.green); - - YAML::Node tidefiles = config["tide"]; - for (YAML::const_iterator it = tidefiles.begin() ; it != tidefiles.end(); ++it) { - std::string value = it->as(""); - expand_path(value) ; - input.tide_file.push_back( value ); - }; - - vector row; - string word; - if (vm.count("input")) - { - string filename = vm["input"].as(); - std::ifstream infile(filename); - while(infile) { - string line; - if (!getline(infile, line)) break; - if (line.size()!= 0 and line.at(0) != '#') - { - istringstream data(line); - row.clear(); - while (getline(data, word, ',')) { - row.push_back(word); - } - if (is_ecef) - { - if (row.size() == 4) - { - std::vector tmp; - input.code.push_back(row[0]); - tmp.push_back(stof(row[1])); - tmp.push_back(stof(row[2])); - tmp.push_back(stof(row[3])); - input.xyz_coords.push_back(tmp); - } - } else { - if (row.size() == 3) - { - input.code.push_back(row[0]); - input.lon.push_back(stof(row[1])); - input.lat.push_back(stof(row[2])); - } - } - - row.clear(); - data.clear(); - } - } - } - - if (vm.count("output")) { - input.output_blq_file = vm["output"].as(); - } else { - input.output_blq_file = "output.blq"; - } - - if (is_ecef) - { - for (int i=0; i(); + if (type == "o") + input.type = "Ocean"; + else if (type == "a") + input.type = "Atmospheric"; + else + throw std::runtime_error( + "the argument for option '--type' is invalid - only 'o' " + "(ocean loading) or 'a' (atmospheric loading) " + "is accepted" + ); + } + else + { + throw std::runtime_error("The required argument for option '--type' is missing"); + } + + std::string config_f; + std::string code_f; + std::vector location; + bool is_ecef = vm["xyz"].as(); + if (vm.count("config")) + { + config_f = vm["config"].as(); + } + if (vm.count("location")) + { + location = vm["location"].as>(); + if (is_ecef) + { + /// \todo Need to have a check if there is 3 values in the vector. + input.xyz_coords.push_back(location); + } + else + { + input.lon.push_back(location[0]); + input.lat.push_back(location[1]); + } + + if (vm.count("code")) + { + input.code.push_back(vm["code"].as()); + } + else + { + input.code.push_back("XXXX"); + } + } + // std::cout << input.xyz_coords[0][0] << input.xyz_coords[0][1] << input.xyz_coords[0][2] << + // "\n"; exit(0); + if (config_f.length() == 0) + { + struct noconfig : std::exception + { + const char* what() const noexcept { return "No config file supplied"; } + } error; + throw error; + } + YAML::Node config = YAML::LoadFile(config_f); + + if (config["greenfunction"]) + { + input.green = config["greenfunction"].as(); + if (input.green.length() == 0) + { + struct nogf : std::exception + { + const char* what() const noexcept { return "No green function defined"; } + } error; + throw error; + } + } + else + { + struct nogfd : std::exception + { + const char* what() const noexcept { return "No greenfunction config entry"; } + } error; + throw error; + } + expand_path(input.green); + + YAML::Node tidefiles = config["tide"]; + for (YAML::const_iterator it = tidefiles.begin(); it != tidefiles.end(); ++it) + { + std::string value = it->as(""); + expand_path(value); + input.tide_file.push_back(value); + }; + + vector row; + string word; + if (vm.count("input")) + { + string filename = vm["input"].as(); + std::ifstream infile(filename); + while (infile) + { + string line; + if (!getline(infile, line)) + break; + if (line.size() != 0 and line.at(0) != '#') + { + istringstream data(line); + row.clear(); + while (getline(data, word, ',')) + { + row.push_back(word); + } + if (is_ecef) + { + if (row.size() == 4) + { + std::vector tmp; + input.code.push_back(row[0]); + tmp.push_back(stof(row[1])); + tmp.push_back(stof(row[2])); + tmp.push_back(stof(row[3])); + input.xyz_coords.push_back(tmp); + } + } + else + { + if (row.size() == 3) + { + input.code.push_back(row[0]); + input.lon.push_back(stof(row[1])); + input.lat.push_back(stof(row[2])); + } + } + + row.clear(); + data.clear(); + } + } + } + + if (vm.count("output")) + { + input.output_blq_file = vm["output"].as(); + } + else + { + input.output_blq_file = "output.blq"; + } + + if (is_ecef) + { + for (int i = 0; i < input.xyz_coords.size(); i++) + { + double tmp[3]; + double ecef[3]; + ecef[0] = input.xyz_coords[i][0]; + 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); + } + } }; - - -int main(int argc, char * argv[]) { - try { - otl_input input; - boost::log::core::get()->set_filter (boost::log::trivial::severity >= boost::log::trivial::info); - boost::log::add_console_log(std::cout, boost::log::keywords::format = "%Message%"); - - program_options(argc, argv, input); - - BOOST_LOG_TRIVIAL(info) << " ======== INPUT PARAMETERS ======= " <<"\n"; - BOOST_LOG_TRIVIAL(info) << " - Will process the following station: " <<"\n"; - for (int i=0; i < input.code.size(); i++ ) - BOOST_LOG_TRIVIAL(info) << " * " << input.code[i] << " -- lon, lat -- " << input.lon[i] << ", " << input.lat[i] << "\n"; - BOOST_LOG_TRIVIAL(info) << " - Green's function is : " <<"\n" << " * " << input.green<<"\n"; - BOOST_LOG_TRIVIAL(info) << " - Tide files are : " <<"\n"; - for (int i=0; i < input.tide_file.size(); i++ ) - BOOST_LOG_TRIVIAL(info) << " * " << input.tide_file[i] << "\n"; - BOOST_LOG_TRIVIAL(info) << " - Output file is: " <<"\n" << " * " << input.output_blq_file<<"\n"; - BOOST_LOG_TRIVIAL(info) << " ======== END ======= " <<"\n"; - - - cpu_timer timer; - tide * tideinfo; - tideinfo = new tide [input.tide_file.size()]; - - - for ( int i =0 ; i < input.tide_file.size(); i++) - { - tideinfo[i].set_name(input.tide_file[i]); - tideinfo[i].read(); - - input.wave_names.push_back(tideinfo[i].get_wave_name()); - } - - - - loading load; - load.set_name(input.green); - load.read(); - BOOST_LOG_TRIVIAL(info) << "All file read \n\t" << timer.format() ; - - - - for (int i = 0; iset_filter( + boost::log::trivial::severity >= boost::log::trivial::info + ); + boost::log::add_console_log(std::cout, boost::log::keywords::format = "%Message%"); + + program_options(argc, argv, input); + + BOOST_LOG_TRIVIAL(info) << " ======== INPUT PARAMETERS ======= " << "\n"; + BOOST_LOG_TRIVIAL(info) << " - Will process the following station: " << "\n"; + for (int i = 0; i < input.code.size(); i++) + BOOST_LOG_TRIVIAL(info) << " * " << input.code[i] << " -- lon, lat -- " + << input.lon[i] << ", " << input.lat[i] << "\n"; + BOOST_LOG_TRIVIAL(info) << " - Green's function is : " << "\n" + << " * " << input.green << "\n"; + BOOST_LOG_TRIVIAL(info) << " - Tide files are : " << "\n"; + for (int i = 0; i < input.tide_file.size(); i++) + BOOST_LOG_TRIVIAL(info) << " * " << input.tide_file[i] << "\n"; + BOOST_LOG_TRIVIAL(info) << " - Output file is: " << "\n" + << " * " << input.output_blq_file << "\n"; + BOOST_LOG_TRIVIAL(info) << " ======== END ======= " << "\n"; + + cpu_timer timer; + tide* tideinfo; + tideinfo = new tide[input.tide_file.size()]; + + for (int i = 0; i < input.tide_file.size(); i++) + { + tideinfo[i].set_name(input.tide_file[i]); + tideinfo[i].read(); + + input.wave_names.push_back(tideinfo[i].get_wave_name()); + } + + loading load; + load.set_name(input.green); + load.read(); + BOOST_LOG_TRIVIAL(info) << "All file read \n\t" << timer.format(); + + for (int i = 0; i < input.tide_file.size(); i++) + { + tideinfo[i].fill_ReIm(); + } + BOOST_LOG_TRIVIAL(info) << "Computing tidal mass Done \n\t" << timer.format(); + + // reserve place + input.dispZ_in.resize(input.code.size()); + input.dispNS_in.resize(input.code.size()); + input.dispEW_in.resize(input.code.size()); + + input.dispZ_out.resize(input.code.size()); + input.dispNS_out.resize(input.code.size()); + input.dispEW_out.resize(input.code.size()); + + for (int i = 0; i < input.code.size(); i++) + { + input.dispZ_in[i].resize(input.tide_file.size()); + input.dispNS_in[i].resize(input.tide_file.size()); + input.dispEW_in[i].resize(input.tide_file.size()); + + input.dispZ_out[i].resize(input.tide_file.size()); + input.dispNS_out[i].resize(input.tide_file.size()); + input.dispEW_out[i].resize(input.tide_file.size()); + + for (int i2 = 0; i2 < input.tide_file.size(); i2++) + { + input.dispZ_in[i][i2] = 0.0; + input.dispNS_in[i][i2] = 0.0; + input.dispEW_in[i][i2] = 0.0; + + input.dispZ_out[i][i2] = 0.0; + input.dispNS_out[i][i2] = 0.0; + input.dispEW_out[i][i2] = 0.0; + } + } #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); - - BOOST_LOG_TRIVIAL(info) << "END " << timer.format() <<"\n"; - } - - catch (std::exception &e) - { - std::cerr << "\n\tERROR:\n\t\t " << e.what() << "\n"; - return -1; - } - return 0; + 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); + + BOOST_LOG_TRIVIAL(info) << "END " << timer.format() << "\n"; + } + + catch (std::exception& e) + { + std::cerr << "\n\tERROR:\n\t\t " << e.what() << "\n"; + return -1; + } + return 0; } diff --git a/src/cpp/loading/tide.cpp b/src/cpp/loading/tide.cpp index 9123a6717..dae09afbf 100644 --- a/src/cpp/loading/tide.cpp +++ b/src/cpp/loading/tide.cpp @@ -5,103 +5,118 @@ * */ -#include "tide.h" +#include "loading/tide.h" #include #include #include + using namespace std; using namespace netCDF; using namespace netCDF::exceptions; -tide::tide(std::string name) : fileName(name ){ - return; +tide::tide(std::string name) : fileName(name) +{ + return; }; tide::~tide() { - amplitude.resize(boost::extents[0][0]); - phase.resize(boost::extents[0][0]); - return ; + amplitude.resize(boost::extents[0][0]); + phase.resize(boost::extents[0][0]); + return; }; - - -void tide::set_name(std::string name) { - fileName = name; - return ; +void tide::set_name(std::string name) +{ + fileName = name; + return; }; -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 = reinterpret_cast const&>(*amplitude.shape()); - - in_phase.resize(ma_shape); - out_phase.resize(ma_shape); +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 = + reinterpret_cast const&>(*amplitude.shape()); - auto in_phase_ptr = in_phase.origin(); - auto out_phase_ptr = out_phase.origin(); - auto lat_ptr = lat.origin(); - auto lon_ptr = lon.origin(); - //cout << amplitude.num_elements() << " " << phase.num_elements() << " " << Re.num_elements() << "\n"; - for (auto amp_ptr = amplitude.origin(); - amp_ptr != amplitude.origin() + amplitude.num_elements() ; - ++amp_ptr, ++phase_ptr, ++in_phase_ptr, ++out_phase_ptr, ++lon_ptr) - { - 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); - //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); - } else { - *in_phase_ptr = 0.0; - *out_phase_ptr = 0.0 ; - } - if (lon_ptr == lon.origin()+lon.num_elements() ) { lat_ptr ++; lon_ptr = lon.origin(); }; - } + in_phase.resize(ma_shape); + out_phase.resize(ma_shape); + auto in_phase_ptr = in_phase.origin(); + auto out_phase_ptr = out_phase.origin(); + auto lat_ptr = lat.origin(); + auto lon_ptr = lon.origin(); + // cout << amplitude.num_elements() << " " << phase.num_elements() << " " << Re.num_elements() + // << "\n"; + for (auto amp_ptr = amplitude.origin(); + amp_ptr != amplitude.origin() + amplitude.num_elements(); + ++amp_ptr, ++phase_ptr, ++in_phase_ptr, ++out_phase_ptr, ++lon_ptr) + { + 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); + // 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); + } + else + { + *in_phase_ptr = 0.0; + *out_phase_ptr = 0.0; + } + if (lon_ptr == lon.origin() + lon.num_elements()) + { + lat_ptr++; + lon_ptr = lon.origin(); + }; + } -// cout << *std::max_element( Re.origin(), Re.origin() + Re.num_elements()) << " " << *std::min_element( Re.origin(), Re.origin() + Re.num_elements()) << "\n"; -// cout << *std::max_element( Im.origin(), Im.origin() + Im.num_elements()) << " " << *std::min_element( Im.origin(), Im.origin() + Im.num_elements()) << "\n"; + // cout << *std::max_element( Re.origin(), Re.origin() + Re.num_elements()) << " " << + // *std::min_element( Re.origin(), Re.origin() + Re.num_elements()) << "\n"; cout << + // *std::max_element( Im.origin(), Im.origin() + Im.num_elements()) << " " << + // *std::min_element( Im.origin(), Im.origin() + Im.num_elements()) << "\n"; - return ; + return; }; -void tide::read(){ - try{ - //std::cout << fileName << "\n"; - NcFile datafile(fileName, NcFile::read); - NcVar lat_var = datafile.getVar("lat"); - nLat = lat_var.getDim(0).getSize() ; - NcVar lon_var = datafile.getVar("lon"); - nLon = lon_var.getDim(0).getSize() ; - lat.resize(boost::extents[nLat]); - lon.resize(boost::extents[nLon]); - lat_var.getVar(lat.origin()); - lon_var.getVar(lon.origin()); - amplitude.resize(boost::extents[nLat][nLon]); - phase.resize(boost::extents[nLat][nLon]); - NcVar amp_var = datafile.getVar("amplitude"); - NcVar phase_var = datafile.getVar("phase"); - amp_var.getVar(&litude[0][0]); - phase_var.getVar(&phase[0][0]); - bool test; - phase_var.getFillModeParameters(test,fillNan); +void tide::read() +{ + try + { + // std::cout << fileName << "\n"; + NcFile datafile(fileName, NcFile::read); + NcVar lat_var = datafile.getVar("lat"); + nLat = lat_var.getDim(0).getSize(); + NcVar lon_var = datafile.getVar("lon"); + nLon = lon_var.getDim(0).getSize(); + lat.resize(boost::extents[nLat]); + lon.resize(boost::extents[nLon]); + lat_var.getVar(lat.origin()); + lon_var.getVar(lon.origin()); + amplitude.resize(boost::extents[nLat][nLon]); + phase.resize(boost::extents[nLat][nLon]); + NcVar amp_var = datafile.getVar("amplitude"); + NcVar phase_var = datafile.getVar("phase"); + amp_var.getVar(&litude[0][0]); + phase_var.getVar(&phase[0][0]); + bool test; + phase_var.getFillModeParameters(test, fillNan); - std::size_t pos1 = fileName.find_last_of("/"); - pos1++; - std::size_t pos2 = fileName.find_last_of("."); - wave_name = fileName.substr(pos1, pos2 - pos1); - boost::to_upper(wave_name); + std::size_t pos1 = fileName.find_last_of("/"); + pos1++; + std::size_t pos2 = fileName.find_last_of("."); + wave_name = fileName.substr(pos1, pos2 - pos1); + boost::to_upper(wave_name); - datafile.close(); - //cout << "1600,400 => " << amplitude[1600][400] << " n n " << amplitude[400][1600] << "\n"; - }catch(NcException &e) - { - throw e; - } - return ; + datafile.close(); + // cout << "1600,400 => " << amplitude[1600][400] << " n n " << amplitude[400][1600] << + // "\n"; + } + catch (NcException& e) + { + throw e; + } + return; }; diff --git a/src/cpp/loading/tide.h b/src/cpp/loading/tide.h index 464b8d58e..1dd28d6e9 100644 --- a/src/cpp/loading/tide.h +++ b/src/cpp/loading/tide.h @@ -4,57 +4,52 @@ * @date 26/2/21 * */ - #pragma once -#include #include -#include "boost_ma_type.h" - +#include +#include "loading/boost_ma_type.h" /** * Implementation of the class to manage the tides. */ -class tide { -public: - tide(){}; - tide(std::string name); - ~tide(); - void set_name(std::string name); - void read(); - size_t get_nlon(){return nLon;}; - size_t get_nlat(){return nLat;}; - void fill_ReIm(); - float get_lat(size_t i){return lat[i];}; - float get_lon(size_t i){return lon[i];}; - - float * get_lat_ptr(){return lat.origin();}; - float * get_lon_ptr(){return lon.origin();}; - - float * get_lat_ptr_end(){return lat.origin()+lat.num_elements();}; - float * get_lon_ptr_end(){return lon.origin()+lon.num_elements();}; - - double * get_in_ptr(){return in_phase.origin();}; - double * get_out_ptr(){return out_phase.origin();}; - - double * get_in_ptr_end(){return in_phase.origin()+in_phase.num_elements();}; - double * get_out_ptr_end(){return out_phase.origin()+out_phase.num_elements();}; - - std::string get_wave_name(){return wave_name;}; - -private: - std::string fileName; - size_t nLon; - size_t nLat; - MA1f lat; - MA1f lon; - MA2f amplitude; - MA2f phase; - MA2d in_phase; - MA2d out_phase; - float fillNan; - std::string wave_name; - - +class tide +{ + public: + tide() {}; + tide(std::string name); + ~tide(); + void set_name(std::string name); + void read(); + size_t get_nlon() { return nLon; }; + size_t get_nlat() { return nLat; }; + void fill_ReIm(); + float get_lat(size_t i) { return lat[i]; }; + float get_lon(size_t i) { return lon[i]; }; + + float* get_lat_ptr() { return lat.origin(); }; + float* get_lon_ptr() { return lon.origin(); }; + + float* get_lat_ptr_end() { return lat.origin() + lat.num_elements(); }; + float* get_lon_ptr_end() { return lon.origin() + lon.num_elements(); }; + + double* get_in_ptr() { return in_phase.origin(); }; + double* get_out_ptr() { return out_phase.origin(); }; + + double* get_in_ptr_end() { return in_phase.origin() + in_phase.num_elements(); }; + double* get_out_ptr_end() { return out_phase.origin() + out_phase.num_elements(); }; + + std::string get_wave_name() { return wave_name; }; + + private: + std::string fileName; + size_t nLon; + size_t nLat; + MA1f lat; + MA1f lon; + MA2f amplitude; + MA2f phase; + MA2d in_phase; + MA2d out_phase; + float fillNan; + std::string wave_name; }; - - diff --git a/src/cpp/loading/utils.cpp b/src/cpp/loading/utils.cpp index 526a19f23..d40092664 100644 --- a/src/cpp/loading/utils.cpp +++ b/src/cpp/loading/utils.cpp @@ -4,63 +4,67 @@ * @date 5/3/21 * */ -#include -#include "utils.h" +#include "loading/utils.h" +#include using namespace std; -#define RE_WGS84 6378137.0 /* earth semimajor axis (WGS84) (m) */ -#define FE_WGS84 (1.0/298.257223563) /* earth flattening (WGS84) */ - +constexpr double RE_WGS84 = 6378137.0; /* earth semimajor axis (WGS84) (m) */ +constexpr double FE_WGS84 = (1.0 / 298.257223563); /* earth flattening (WGS84) */ /* transform ecef to geodetic postion ------------------------------------------ -* Copied from RTKLIB -* transform ecef position to geodetic position -* args : double *r I ecef position {x,y,z} (m) -* double *pos O geodetic position {lat,lon,h} (rad,m) -* return : none -* notes : WGS84, ellipsoidal height -*-----------------------------------------------------------------------------*/ -void ecef2pos(const double *r, double* pos) + * Copied from RTKLIB + * transform ecef position to geodetic position + * args : double *r I ecef position {x,y,z} (m) + * double *pos O geodetic position {lat,lon,h} (rad,m) + * return : none + * notes : WGS84, ellipsoidal height + *-----------------------------------------------------------------------------*/ +void ecef2pos(const double* r, double* pos) { - double e2=FE_WGS84*(2.0-FE_WGS84); - double r2=r[0]*r[0] + r[1]*r[1]; - double z; - double zk; - double v=RE_WGS84; - double sinp; + double e2 = FE_WGS84 * (2.0 - FE_WGS84); + double r2 = r[0] * r[0] + r[1] * r[1]; + double z; + double zk; + double v = RE_WGS84; + double sinp; - for (z=r[2],zk=0.0;fabs(z-zk)>=1E-4;) - { - zk=z; - sinp=z/sqrt(r2+z*z); - v=RE_WGS84/sqrt(1.0-e2*sinp*sinp); - z=r[2]+v*e2*sinp; - } + for (z = r[2], zk = 0.0; fabs(z - zk) >= 1E-4;) + { + zk = z; + sinp = z / sqrt(r2 + z * z); + v = RE_WGS84 / sqrt(1.0 - e2 * sinp * sinp); + z = r[2] + v * e2 * sinp; + } - pos[0]=r2>1E-12?atan(z/sqrt(r2)):(r[2]>0.0?PI/2.0:-PI/2.0); - pos[1]=r2>1E-12?atan2(r[1],r[0]):0.0; - pos[2]=sqrt(r2+z*z)-v; + pos[0] = r2 > 1E-12 ? atan(z / sqrt(r2)) : (r[2] > 0.0 ? PI / 2.0 : -PI / 2.0); + pos[1] = r2 > 1E-12 ? atan2(r[1], r[0]) : 0.0; + pos[2] = sqrt(r2 + z * z) - v; } - -void calcDistanceBearing(float *lat1, float *lon1, float * lat2, float *lon2, double *dist, double *brng) +void calcDistanceBearing( + float* lat1, + float* lon1, + float* lat2, + float* lon2, + double* dist, + double* brng +) { - double lat1_r = (double) (*lat1 * PI/180.0); - double lat2_r = (double) (*lat2 * PI/180.0); - double lon1_r = (double) (*lon1 * PI/180.0); - double lon2_r = (double) (*lon2 * PI/180.0); - 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); + double lat1_r = (double)(*lat1 * PI / 180.0); + double lat2_r = (double)(*lat2 * PI / 180.0); + double lon1_r = (double)(*lon1 * PI / 180.0); + double lon2_r = (double)(*lon2 * PI / 180.0); + 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); - *brng = atan2(y, x); - *dist = 2 * atan2(sqrt(a), sqrt(1-a)); - if (*dist != *dist) *dist = M_PI; + *brng = atan2(y, x); + *dist = 2 * atan2(sqrt(a), sqrt(1 - a)); + if (*dist != *dist) + *dist = M_PI; } diff --git a/src/cpp/loading/utils.h b/src/cpp/loading/utils.h index 43ca44c73..291ca6e02 100644 --- a/src/cpp/loading/utils.h +++ b/src/cpp/loading/utils.h @@ -4,14 +4,25 @@ * @date 5/3/21 * */ - #pragma once -#define SQR(x) ((x)*(x)) -#define PI 3.141592653589793238462643383279502884197169399375105820974 -#define D2R (PI/180.0) /* deg to rad */ -#define R2D (180.0/PI) /* rad to deg */ - -void calcDistanceBearing(float *lat1, float *lon1, float * lat2, float *lon2, double *dist, double *brng); +// Template function replacing SQR macro +template +constexpr T SQR(const T& x) +{ + return x * x; +} -void ecef2pos(const double *r, double* pos); +// Mathematical constants +constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974; +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 + ); +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 fc7085f3e..b09fefc90 100644 --- a/src/cpp/orbprop/acceleration.cpp +++ b/src/cpp/orbprop/acceleration.cpp @@ -1,204 +1,211 @@ - -#include "acceleration.hpp" -#include "constants.hpp" -#include "common.hpp" - - +#include "orbprop/acceleration.hpp" +#include #include -#include #include -#include +#include +#include "common/common.hpp" +#include "common/constants.hpp" Legendre::Legendre() { - nmax = 0; - init(); + nmax = 0; + init(); } -Legendre::Legendre( - int nmax) - : nmax(nmax) +Legendre::Legendre(int nmax) : nmax(nmax) { - init(); + init(); } -void Legendre::setNmax( - int n) +void Legendre::setNmax(int n) { - nmax = n; - init(); + nmax = n; + init(); } void Legendre::init() { - anm = MatrixXd::Zero(nmax + 1, nmax + 1); - bnm = MatrixXd::Zero(nmax + 1, nmax + 1); - fnm = MatrixXd::Zero(nmax + 1, nmax + 1); - Pnm = MatrixXd::Zero(nmax + 1, nmax + 1); - dPnm = MatrixXd::Zero(nmax + 1, nmax + 1); - - for (int n = 0; n < nmax + 1; n++) - for (int m = 0; m <= n; m++) - { - anm(n, m) = sqrt(((double)(2 * n - 1) * (2 * n + 1)) / ((n - m) * (n + m))); - bnm(n, m) = sqrt(((double)(2 * n + 1) * (n + m - 1) * (n - m - 1)) / ((n - m) * (n + m) * (2 * n - 3))); - fnm(n, m) = sqrt(((double)(SQR(n) - SQR(m)) * (2 * n + 1)) / (2 * n - 1)); - } + anm = MatrixXd::Zero(nmax + 1, nmax + 1); + bnm = MatrixXd::Zero(nmax + 1, nmax + 1); + fnm = MatrixXd::Zero(nmax + 1, nmax + 1); + Pnm = MatrixXd::Zero(nmax + 1, nmax + 1); + dPnm = MatrixXd::Zero(nmax + 1, nmax + 1); + + for (int n = 0; n < nmax + 1; n++) + for (int m = 0; m <= n; m++) + { + anm(n, m) = sqrt(((double)(2 * n - 1) * (2 * n + 1)) / ((n - m) * (n + m))); + bnm(n, m) = sqrt( + ((double)(2 * n + 1) * (n + m - 1) * (n - m - 1)) / + ((n - m) * (n + m) * (2 * n - 3)) + ); + fnm(n, m) = sqrt(((double)(SQR(n) - SQR(m)) * (2 * n + 1)) / (2 * n - 1)); + } } void Legendre::calculate(double x) { - if (abs(x) >= 1) - throw std::runtime_error("Legendre polynomial computation arguments should be between -1 and 1."); - - double u = sqrt(1 - SQR(x)); - Pnm(0, 0) = 1; - Pnm(1, 0) = sqrt(3) * x; - Pnm(1, 1) = sqrt(3) * u; - - dPnm(1, 0) = 1 / u * (x * Pnm(1, 0) - sqrt(3) * Pnm(0, 0)); - dPnm(1, 1) = x / u * Pnm(1, 1); - - for (int n = 2; n < nmax + 1; n++) - { - for (int m = 0; m < n; m++) - { - Pnm (n, m) = anm(n, m) * x * Pnm(n - 1, m) - bnm(n, m) * Pnm(n - 2, m); - dPnm(n, m) = 1 / u * ( n * x * Pnm(n, m) - fnm(n, m) * Pnm(n - 1, m)); - } - Pnm (n, n) = u * sqrt((2.0 * n + 1) / (2 * n)) * Pnm(n - 1, n - 1); - dPnm(n, n) = n * x / u * Pnm(n, n); - } + if (abs(x) >= 1) + throw std::runtime_error( + "Legendre polynomial computation arguments should be between -1 and 1." + ); + + double u = sqrt(1 - SQR(x)); + Pnm(0, 0) = 1; + Pnm(1, 0) = sqrt(3) * x; + Pnm(1, 1) = sqrt(3) * u; + + dPnm(1, 0) = 1 / u * (x * Pnm(1, 0) - sqrt(3) * Pnm(0, 0)); + dPnm(1, 1) = x / u * Pnm(1, 1); + + for (int n = 2; n < nmax + 1; n++) + { + for (int m = 0; m < n; m++) + { + Pnm(n, m) = anm(n, m) * x * Pnm(n - 1, m) - bnm(n, m) * Pnm(n - 2, m); + dPnm(n, m) = 1 / u * (n * x * Pnm(n, m) - fnm(n, m) * Pnm(n - 1, m)); + } + Pnm(n, n) = u * sqrt((2.0 * n + 1) / (2 * n)) * Pnm(n - 1, n - 1); + dPnm(n, n) = n * x / u * Pnm(n, n); + } } /** Central force acceleration */ Vector3d accelCentralForce( - const Vector3d observer, ///< position of the oberserver - const double GM, ///< value of GM of the acting force - Matrix3d* dAdPos_ptr) ///< Optional pointer to differential matrix + const Vector3d observer, ///< position of the oberserver + const double GM, ///< value of GM of the acting force + Matrix3d* dAdPos_ptr ///< Optional pointer to differential matrix +) { - Vector3d acc = -1 * GM * observer.normalized() / observer.squaredNorm(); + Vector3d acc = -1 * GM * observer.normalized() / observer.squaredNorm(); - if (dAdPos_ptr) - { - *dAdPos_ptr += GM / pow(observer.norm(), 5) * (3 * observer * observer.transpose() - Matrix3d::Identity() * observer.squaredNorm()); - } + if (dAdPos_ptr) + { + *dAdPos_ptr += + GM / pow(observer.norm(), 5) * + (3 * observer * observer.transpose() - Matrix3d::Identity() * observer.squaredNorm()); + } - return acc; + return acc; } /** Source point acceleration from a planet acting on a satellite. */ Vector3d accelSourcePoint( - const Vector3d sat, ///< Vector posiiong of the satellite w.r.t to the orbiting body - const Vector3d planet, ///< Vector position of the planet w.r.t to the orbiting body - const double GM, ///< Constant GM of the acting body - Matrix3d* dAdPos_ptr) ///< Optional pointer to differential matrix + const Vector3d sat, ///< Vector posiiong of the satellite w.r.t to the orbiting body + const Vector3d planet, ///< Vector position of the planet w.r.t to the orbiting body + const double GM, ///< Constant GM of the acting body + Matrix3d* dAdPos_ptr ///< Optional pointer to differential matrix +) { - Vector3d relativePosition = planet - sat; - Vector3d acc_sat = accelCentralForce(relativePosition, GM, dAdPos_ptr); - Vector3d acc_e = accelCentralForce(planet, GM, dAdPos_ptr); + Vector3d relativePosition = planet - sat; + Vector3d acc_sat = accelCentralForce(relativePosition, GM, dAdPos_ptr); + Vector3d acc_e = accelCentralForce(planet, GM, dAdPos_ptr); - return acc_e - acc_sat; + return acc_e - acc_sat; } /** "The indirect J2 effect". * ref: GOCE standards GO-TN-HPF-GS-0111 */ Vector3d accelJ2( - const double C20, ///< Value of the C20 - const Matrix3d eci2ecf, ///< Rotation inertial to terestrila - Vector3d bodyPos, ///< Position of the planets - double GM) ///< Value of GM constant of the body in question + const double C20, ///< Value of the C20 + const Matrix3d eci2ecf, ///< Rotation inertial to terestrila + Vector3d bodyPos, ///< Position of the planets + double GM ///< Value of GM constant of the body in question +) { - Vector3d pos_ecef = eci2ecf * bodyPos; + Vector3d pos_ecef = eci2ecf * bodyPos; - double dist = pos_ecef.norm(); - double term = (GM / pow(dist, 3)) * SQR(RE_WGS84 / dist); + double dist = pos_ecef.norm(); + double term = (GM / pow(dist, 3)) * SQR(RE_WGS84 / dist); - Vector3d vec = Vector3d::Zero(); - vec.head(2) = term * (5 * SQR(pos_ecef.z()/dist) - 1) * pos_ecef.head(2); - vec.z() = term * (5 * SQR(pos_ecef.z()/dist) - 3) * pos_ecef.z(); + Vector3d vec = Vector3d::Zero(); + vec.head(2) = term * (5 * SQR(pos_ecef.z() / dist) - 1) * pos_ecef.head(2); + vec.z() = term * (5 * SQR(pos_ecef.z() / dist) - 3) * pos_ecef.z(); - Vector3d accJ2 = -1 * ( (3 * sqrt(5)) / 2) * C20 * vec; + Vector3d accJ2 = -1 * ((3 * sqrt(5)) / 2) * C20 * vec; - return accJ2; + return accJ2; } /** Compute the acceleration of due to a spherical harmonic field acting on the satellite - * @note This function does not contain the degree 0 acceleration need to be done via "accelCentralForce" + * @note This function does not contain the degree 0 acceleration need to be done via + * "accelCentralForce" */ 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, limit this to max found in file/struct - const double GM) ///< Value of GM constant of the body in question. + 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, + ///< limit this to max found in file/struct + const double GM ///< Value of GM constant of the body in question. +) { - double R = r.norm(); - double sin_lat = r.z() / R; // Is Cos colat too. - - double Rxy = sqrt(SQR(r.x()) + SQR(r.y())); - double cos_lon = r.x() / Rxy; - double sin_lon = r.y() / Rxy; - - VectorXd cosphi(maxDeg+1); - VectorXd sinphi(maxDeg+1); - - cosphi(0) = 1; - sinphi(0) = 0; - - cosphi(1) = cos_lon; - sinphi(1) = sin_lon; - - for (int i = 2; i <= maxDeg; i++) - { - cosphi(i) = cosphi(i-1) * cos_lon - sinphi(i-1) * sin_lon; - sinphi(i) = sinphi(i-1) * cos_lon + cosphi(i-1) * sin_lon; - } - - Legendre leg(maxDeg); - leg.calculate(sin_lat); - - double dVr = 0; - double dVtheta = 0; - double dVlambda = 0; - - double const_Radius = RE_GLO / R; - for (int i = 2; i <= maxDeg; i++) - { - const_Radius *= RE_GLO/R; /** (Re/R)**n : we start from deg 2 this formulation works. */ - double dVr_n = 0; - double dVtheta_n = 0; - double dVlambda_n = 0; - - for (int j = 0; j <= i; j++) - { - dVr_n += leg.Pnm(i,j) * (C(i,j) * cosphi(j) + S(i,j) * sinphi(j)); - dVtheta_n += leg.dPnm(i,j) * (C(i,j) * cosphi(j) + S(i,j) * sinphi(j)); // lat - dVlambda_n += j * leg.Pnm(i,j) * (S(i,j) * cosphi(j) - C(i,j) * sinphi(j)); // lon - } - dVr += -1*(i+1) * const_Radius * dVr_n; - dVtheta += const_Radius * dVtheta_n; - dVlambda += const_Radius * dVlambda_n; - } - - //step 2, scale by GM/r; GM/r2 for dVr. - - double constant = GM / R; - dVtheta *= constant; - dVlambda *= constant; - dVr *= GM / SQR(R); - - // step3, project in cartesian - - Vector3d acc; - acc.x() = dVr * r.x() / R + dVtheta * r.z() / SQR(R) * r.x() / Rxy - dVlambda * r.y() / SQR(Rxy); - acc.y() = dVr * r.y() / R + dVtheta * r.z() / SQR(R) * r.y() / Rxy + dVlambda * r.x() / SQR(Rxy); - acc.z() = dVr * r.z() / R - dVtheta * Rxy / SQR(R); - - return acc; + double R = r.norm(); + double sin_lat = r.z() / R; // Is Cos colat too. + + double Rxy = sqrt(SQR(r.x()) + SQR(r.y())); + double cos_lon = r.x() / Rxy; + double sin_lon = r.y() / Rxy; + + VectorXd cosphi(maxDeg + 1); + VectorXd sinphi(maxDeg + 1); + + cosphi(0) = 1; + sinphi(0) = 0; + + cosphi(1) = cos_lon; + sinphi(1) = sin_lon; + + for (int i = 2; i <= maxDeg; i++) + { + cosphi(i) = cosphi(i - 1) * cos_lon - sinphi(i - 1) * sin_lon; + sinphi(i) = sinphi(i - 1) * cos_lon + cosphi(i - 1) * sin_lon; + } + + Legendre leg(maxDeg); + leg.calculate(sin_lat); + + double dVr = 0; + double dVtheta = 0; + double dVlambda = 0; + + double const_Radius = RE_GLO / R; + for (int i = 2; i <= maxDeg; i++) + { + const_Radius *= RE_GLO / R; /** (Re/R)**n : we start from deg 2 this formulation works. */ + double dVr_n = 0; + double dVtheta_n = 0; + double dVlambda_n = 0; + + for (int j = 0; j <= i; j++) + { + dVr_n += leg.Pnm(i, j) * (C(i, j) * cosphi(j) + S(i, j) * sinphi(j)); + dVtheta_n += leg.dPnm(i, j) * (C(i, j) * cosphi(j) + S(i, j) * sinphi(j)); // lat + dVlambda_n += j * leg.Pnm(i, j) * (S(i, j) * cosphi(j) - C(i, j) * sinphi(j)); // lon + } + dVr += -1 * (i + 1) * const_Radius * dVr_n; + dVtheta += const_Radius * dVtheta_n; + dVlambda += const_Radius * dVlambda_n; + } + + // step 2, scale by GM/r; GM/r2 for dVr. + + double constant = GM / R; + dVtheta *= constant; + dVlambda *= constant; + dVr *= GM / SQR(R); + + // step3, project in cartesian + + Vector3d acc; + acc.x() = + dVr * r.x() / R + dVtheta * r.z() / SQR(R) * r.x() / Rxy - dVlambda * r.y() / SQR(Rxy); + acc.y() = + dVr * r.y() / R + dVtheta * r.z() / SQR(R) * r.y() / Rxy + dVlambda * r.x() / SQR(Rxy); + acc.z() = dVr * r.z() / R - dVtheta * Rxy / SQR(R); + + return acc; } - - diff --git a/src/cpp/orbprop/acceleration.hpp b/src/cpp/orbprop/acceleration.hpp index 9a54492a0..bc9e41a82 100644 --- a/src/cpp/orbprop/acceleration.hpp +++ b/src/cpp/orbprop/acceleration.hpp @@ -1,43 +1,37 @@ - #pragma once -#include "eigenIncluder.hpp" +#include "common/eigenIncluder.hpp" struct Legendre { - Legendre(); - - Legendre(int nmax); - - void setNmax(int n); - void init(); - void calculate(double X); - int nmax; - - MatrixXd Pnm; - MatrixXd dPnm; - - MatrixXd anm; - MatrixXd bnm; - MatrixXd fnm; + Legendre(); + + Legendre(int nmax); + + void setNmax(int n); + void init(); + void calculate(double X); + int nmax; + + MatrixXd Pnm; + MatrixXd dPnm; + + MatrixXd anm; + MatrixXd bnm; + MatrixXd fnm; }; Vector3d accelSourcePoint( - const Vector3d observer, - const Vector3d origin, - const double GM, - Matrix3d* dAdPos_ptr = nullptr); - -Vector3d accelCentralForce( - const Vector3d observer, - const double GM, - Matrix3d* dAdPos_ptr = nullptr); + const Vector3d observer, + const Vector3d origin, + const double GM, + Matrix3d* dAdPos_ptr = nullptr +); -Vector3d accelSPH(const Vector3d r, const MatrixXd C, const MatrixXd S, const int n, const double GM); +Vector3d +accelCentralForce(const Vector3d observer, const double GM, Matrix3d* dAdPos_ptr = nullptr); -Vector3d accelJ2( - const double C20, - const Matrix3d eci2ecf, - Vector3d bodyPos, - double GM); +Vector3d +accelSPH(const Vector3d r, const MatrixXd C, const MatrixXd S, const int n, const double GM); +Vector3d accelJ2(const double C20, const Matrix3d eci2ecf, Vector3d bodyPos, double GM); diff --git a/src/cpp/orbprop/aod.cpp b/src/cpp/orbprop/aod.cpp index fb5361385..b5a920904 100644 --- a/src/cpp/orbprop/aod.cpp +++ b/src/cpp/orbprop/aod.cpp @@ -1,110 +1,110 @@ - +#include "orbprop/aod.hpp" #include - #include -#include #include +#include #include -#include "aod.hpp" - Aod aod; -void Aod::read( - const string& filename, - int maxDeg) +void Aod::read(const string& filename, int maxDeg) { - std::ifstream infile(filename); - if (!infile) - { - BOOST_LOG_TRIVIAL(error) - << "Aod file open error " << filename; - - return; - } - - string line; - while (std::getline(infile, line)) - { - if (line.find("DATA SET") == std::string::npos) - { - continue; - } - int dataSetNumber; - int totalCoefficients; - int epoch[6]; - char type[4]; - int found = sscanf(line.c_str(), "DATA SET %d: %d COEFFICIENTS FOR %d-%d-%d %d:%d:%d OF TYPE %3s", - &dataSetNumber, - &totalCoefficients, - &epoch[0], - &epoch[1], - &epoch[2], - &epoch[3], - &epoch[4], - &epoch[5], - type); - - if (found != 9) - { - continue; - } - - if (string(type) == "glo") - { - AodData data_; - data_.Cnm = MatrixXd::Zero(maxDeg+1, maxDeg+1); - data_.Snm = MatrixXd::Zero(maxDeg+1, maxDeg+1); - - GEpoch epoch_f; - for (int i = 0 ; i <6; i++) - epoch_f[i] = (double)epoch[i]; - - GTime time = epoch_f; - for (int i = 0; i < totalCoefficients; i++) - { - int n; - int m; - double C; - double S; - std::getline(infile, line); - - int found = sscanf(line.c_str(), "%d %d %lf %lf", &n, &m, &C, &S); - - if (found != 4) - { - continue; - } - - if (n <= maxDeg) - { - data_.Cnm(n, m) = C; - data_.Snm(n, m) = S; - } - } - data[time] = data_; - } - } + std::ifstream infile(filename); + if (!infile) + { + BOOST_LOG_TRIVIAL(error) << "AOD file open error " << filename; + + return; + } + + string line; + while (std::getline(infile, line)) + { + if (line.find("DATA SET") == std::string::npos) + { + continue; + } + int dataSetNumber; + int totalCoefficients; + int epoch[6]; + char type[4]; + int found = sscanf( + line.c_str(), + "DATA SET %d: %d COEFFICIENTS FOR %d-%d-%d %d:%d:%d OF TYPE %3s", + &dataSetNumber, + &totalCoefficients, + &epoch[0], + &epoch[1], + &epoch[2], + &epoch[3], + &epoch[4], + &epoch[5], + type + ); + + if (found != 9) + { + continue; + } + + if (string(type) == "glo") + { + AodData data_; + data_.Cnm = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); + data_.Snm = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); + + GEpoch epoch_f; + for (int i = 0; i < 6; i++) + epoch_f[i] = (double)epoch[i]; + + GTime time = epoch_f; + for (int i = 0; i < totalCoefficients; i++) + { + int n; + int m; + double C; + double S; + std::getline(infile, line); + + int found = sscanf(line.c_str(), "%d %d %lf %lf", &n, &m, &C, &S); + + if (found != 4) + { + continue; + } + + if (n <= maxDeg) + { + data_.Cnm(n, m) = C; + data_.Snm(n, m) = S; + } + } + data[time] = data_; + } + } } -void Aod::interpolate( - const GTime time, - MatrixXd& Cnm, - MatrixXd& Snm) +void Aod::interpolate(const GTime time, MatrixXd& Cnm, MatrixXd& Snm) { - // Right now linear interpolation as it is recomanded by the technical note - auto it = data.lower_bound(time); - - if (it == data.end()) { it--; } - if (it == data.begin()) { it++; } - - auto it2 = it; - it2--; - - double t1 = (time - it2->first) .to_double(); - double t2 = (it->first - time) .to_double(); - double t = t1 + t2; - - Cnm = (it->second.Cnm * t1 + it2->second.Cnm * t2) / t; - Snm = (it->second.Snm * t1 + it2->second.Snm * t2) / t; + // Right now linear interpolation as it is recomanded by the technical note + auto it = data.lower_bound(time); + + if (it == data.end()) + { + it--; + } + if (it == data.begin()) + { + it++; + } + + auto it2 = it; + it2--; + + double t1 = (time - it2->first).to_double(); + double t2 = (it->first - time).to_double(); + double t = t1 + t2; + + Cnm = (it->second.Cnm * t1 + it2->second.Cnm * t2) / t; + Snm = (it->second.Snm * t1 + it2->second.Snm * t2) / t; } diff --git a/src/cpp/orbprop/aod.hpp b/src/cpp/orbprop/aod.hpp index f14282cb4..5cfaa2274 100644 --- a/src/cpp/orbprop/aod.hpp +++ b/src/cpp/orbprop/aod.hpp @@ -1,31 +1,24 @@ #pragma once -#include "eigenIncluder.hpp" -#include "gTime.hpp" - #include +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" using std::map; struct AodData { - MatrixXd Cnm; - MatrixXd Snm; + MatrixXd Cnm; + MatrixXd Snm; }; struct Aod { - void read( - const string& filename, - int maxDeg); - - void interpolate( - GTime time, - MatrixXd& Cnm, - MatrixXd& Snm); - - std::map data; + void read(const string& filename, int maxDeg); + + void interpolate(GTime time, MatrixXd& Cnm, MatrixXd& Snm); + + std::map data; }; extern Aod aod; - diff --git a/src/cpp/orbprop/boxwing.cpp b/src/cpp/orbprop/boxwing.cpp index a0960566f..c31aeefe7 100644 --- a/src/cpp/orbprop/boxwing.cpp +++ b/src/cpp/orbprop/boxwing.cpp @@ -1,125 +1,140 @@ - // #pragma GCC optimize ("O0") +#include "orbprop/boxwing.hpp" #include #include - -#include "common.hpp" -#include "eigenIncluder.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "boxwing.hpp" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" Vector3d getNormal( - const Vector3d &eD, - const Vector3d &eX, - const Vector3d &eY, - const Vector3d &eZ, - const SurfaceDetails &surf) + const Vector3d& eD, + const Vector3d& eX, + const Vector3d& eY, + const Vector3d& eZ, + const SurfaceDetails& surf +) { - Vector3d eN = surf.normal[0] * eX - + surf.normal[1] * eY - + surf.normal[2] * eZ; + Vector3d eN = surf.normal[0] * eX + surf.normal[1] * eY + surf.normal[2] * eZ; - if (surf.rotation_axis.empty()) - return eN; + if (surf.rotation_axis.empty()) + return eN; - double panelOrientation = surf.normal[0] - + surf.normal[1] - + surf.normal[2]; //will be +/- 1 + double panelOrientation = surf.normal[0] + surf.normal[1] + surf.normal[2]; // will be +/- 1 - Vector3d rotAxis = surf.rotation_axis[0] * eX - + surf.rotation_axis[1] * eY - + surf.rotation_axis[2] * eZ; + Vector3d rotAxis = + surf.rotation_axis[0] * eX + surf.rotation_axis[1] * eY + surf.rotation_axis[2] * eZ; - Vector3d eT = rotAxis.cross(eN); + Vector3d eT = rotAxis.cross(eN); - Vector3d neweN = eD.dot(eN) * eN - + eD.dot(eT) * eT; + Vector3d neweN = eD.dot(eN) * eN + eD.dot(eT) * eT; - eN = panelOrientation * neweN.normalized(); + eN = panelOrientation * neweN.normalized(); - return eN; + return eN; } Vector3d calculateAcceleration( - const Vector3d& direction, - const Vector3d& eN, - const double mass, - const double area, - const double alpha, - const double delta, - const double rho, - const double kappa, - const double s) + const Vector3d& direction, + const Vector3d& eN, + const double mass, + const double area, + const double alpha, + const double delta, + const double rho, + const double kappa, + const double s +) { - Vector3d acc = Vector3d::Zero(); + Vector3d acc = Vector3d::Zero(); - double S0 = 1367; // Solar flux constant + double S0 = 1367; // Solar flux constant - double costheta = direction.dot(eN); - if (costheta < 0) - { - costheta = 0; - } + double costheta = direction.dot(eN); + if (costheta < 0) + { + costheta = 0; + } - acc += -area / mass * S0 / CLIGHT * costheta * - ((alpha + delta) * direction - + (M_PI / 6 * s + 2.0 / 3.0 * (1 - s) * (delta + kappa * alpha)) * eN - + (4.0 / 3.0 * s + 2.0 * (1 - s)) * rho * costheta * eN - ); + acc += -area / mass * S0 / CLIGHT * costheta * + ((alpha + delta) * direction + + (PI / 6 * s + 2.0 / 3.0 * (1 - s) * (delta + kappa * alpha)) * eN + + (4.0 / 3.0 * s + 2.0 * (1 - s)) * rho * costheta * eN); - return acc; + return acc; } Vector3d applyBoxwingSrp( - const OrbitOptions& orbitOptions, - const Vector3d& eD, - const Vector3d& eX, - const Vector3d& eY, - const Vector3d& eZ) + const OrbitOptions& orbitOptions, + const Vector3d& eD, + const Vector3d& eX, + const Vector3d& eY, + const Vector3d& eZ +) { - Vector3d acceleration = Vector3d::Zero(); - for (auto& surf : orbitOptions.surface_details) - { - double alpha = surf.absorption_visible; - double delta = surf.diffusion_visible; - double rho = surf.reflection_visible; - double kappa = surf.thermal_reemission; - double s = surf.shape; - - Vector3d eN = getNormal(eD, eX, eY, eZ, surf); - - acceleration += calculateAcceleration(eD, eN, orbitOptions.mass, surf.area, alpha, delta, rho, kappa, s); - } - return acceleration; + Vector3d acceleration = Vector3d::Zero(); + for (auto& surf : orbitOptions.surface_details) + { + double alpha = surf.absorption_visible; + double delta = surf.diffusion_visible; + double rho = surf.reflection_visible; + double kappa = surf.thermal_reemission; + double s = surf.shape; + + Vector3d eN = getNormal(eD, eX, eY, eZ, surf); + + acceleration += calculateAcceleration( + eD, + eN, + orbitOptions.mass, + surf.area, + alpha, + delta, + rho, + kappa, + s + ); + } + return acceleration; } Vector3d applyBoxwingAlbedo( - const OrbitOptions& orbitOptions, - const double E_Vis, - const double E_IR, - const Vector3d& rsat, - const Vector3d& eD, - const Vector3d& eX, - const Vector3d& eY, - const Vector3d& eZ) + const OrbitOptions& orbitOptions, + const double E_Vis, + const double E_IR, + const Vector3d& rsat, + const Vector3d& eD, + const Vector3d& eX, + const Vector3d& eY, + const Vector3d& eZ +) { - Vector3d acceleration = Vector3d::Zero(); - Vector3d er = rsat.normalized(); - - for (auto& surf : orbitOptions.surface_details) - { - double alpha = surf.absorption_visible * E_Vis + surf.absorption_infrared * E_IR; - double delta = surf.diffusion_visible * E_Vis + surf.diffusion_infrared * E_IR; - double rho = surf.reflection_visible * E_Vis + surf.reflection_infrared * E_IR; - double kappa = surf.thermal_reemission; - double s = surf.shape; - - Vector3d eN = getNormal(eD, eX, eY, eZ, surf); - - acceleration += calculateAcceleration(er, eN, orbitOptions.mass, surf.area, alpha, delta, rho, kappa, s); - } - - return acceleration; + Vector3d acceleration = Vector3d::Zero(); + Vector3d er = rsat.normalized(); + + for (auto& surf : orbitOptions.surface_details) + { + double alpha = surf.absorption_visible * E_Vis + surf.absorption_infrared * E_IR; + double delta = surf.diffusion_visible * E_Vis + surf.diffusion_infrared * E_IR; + double rho = surf.reflection_visible * E_Vis + surf.reflection_infrared * E_IR; + double kappa = surf.thermal_reemission; + double s = surf.shape; + + Vector3d eN = getNormal(eD, eX, eY, eZ, surf); + + acceleration += calculateAcceleration( + er, + eN, + orbitOptions.mass, + surf.area, + alpha, + delta, + rho, + kappa, + s + ); + } + + return acceleration; } diff --git a/src/cpp/orbprop/boxwing.hpp b/src/cpp/orbprop/boxwing.hpp index 24f56637a..d1fc2c05c 100644 --- a/src/cpp/orbprop/boxwing.hpp +++ b/src/cpp/orbprop/boxwing.hpp @@ -1,24 +1,24 @@ - #pragma once -#include "eigenIncluder.hpp" +#include "common/eigenIncluder.hpp" struct OrbitOptions; -Vector3d applyBoxwingSrp( - const OrbitOptions& orbitOptions, - const Vector3d& eD, - const Vector3d& eX, - const Vector3d& eY, - const Vector3d& eZ); - +Vector3d applyBoxwingSrp( + const OrbitOptions& orbitOptions, + const Vector3d& eD, + const Vector3d& eX, + const Vector3d& eY, + const Vector3d& eZ +); -Vector3d applyBoxwingAlbedo( - const OrbitOptions& orbitOptions, - const double E_Vis, - const double E_IR, - const Vector3d& rsat, - const Vector3d& eD, - const Vector3d& eX, - const Vector3d& eY, - const Vector3d& eZ); +Vector3d applyBoxwingAlbedo( + const OrbitOptions& orbitOptions, + const double E_Vis, + const double E_IR, + const Vector3d& rsat, + const Vector3d& eD, + const Vector3d& eX, + const Vector3d& eY, + const Vector3d& eZ +); diff --git a/src/cpp/orbprop/centerMassCorrections.cpp b/src/cpp/orbprop/centerMassCorrections.cpp index c5e83e3dc..d3c44bb77 100644 --- a/src/cpp/orbprop/centerMassCorrections.cpp +++ b/src/cpp/orbprop/centerMassCorrections.cpp @@ -1,81 +1,78 @@ - +#include "orbprop/centerMassCorrections.hpp" #include - #include #include -#include "centerMassCorrections.hpp" - CenterMassCorrections cmc; -void CenterMassCorrections::read( - const string& filename) +void CenterMassCorrections::read(const string& filename) { - if (filename.empty()) - { - return; - } + if (filename.empty()) + { + return; + } - std::ifstream infile(filename); - if (!infile) - { - BOOST_LOG_TRIVIAL(error) - << "CMC file open error " << filename; + std::ifstream infile(filename); + if (!infile) + { + BOOST_LOG_TRIVIAL(error) << "CMC file open error " << filename; - return; - } + return; + } - string line; - std::getline(infile, line); + string line; + std::getline(infile, line); - while (std::getline(infile, line)) - { - std::istringstream iss(line); - string wavename; - string dummy; - double zIn; - double zOut; - double xIn; - double xOut; - double yIn; - double yOut; - iss >> wavename >> dummy >> zIn >> zOut >> xIn >> xOut >> yIn >> yOut; - data[wavename] << xIn, yIn, zIn, xOut, yOut, zOut; - } + while (std::getline(infile, line)) + { + std::istringstream iss(line); + string wavename; + string dummy; + double zIn; + double zOut; + double xIn; + double xOut; + double yIn; + double yOut; + iss >> wavename >> dummy >> zIn >> zOut >> xIn >> xOut >> yIn >> yOut; + data[wavename] << xIn, yIn, zIn, xOut, yOut, zOut; + } - for (auto& [wave, coeff] : data) - { - BOOST_LOG_TRIVIAL(debug) << wave << " " << coeff.transpose(); - } + for (auto& [wave, coeff] : data) + { + BOOST_LOG_TRIVIAL(debug) << wave << " " << coeff.transpose(); + } -// DoodsonNumbers["K1"] = Array6d (6); - doodsonNumbers["K1"] << 1, 1, 0, 0, 0, 0; // 165.555 - doodsonNumbers["K2"] << 2, 2, 0, 0, 0, 0; // 275.555 - doodsonNumbers["M2"] << 2, 0, 0, 0, 0, 0; // 255.555 - doodsonNumbers["Mf"] << 0, 2, 0, 0, 0, 0; // 75.555 - doodsonNumbers["Mm"] << 0, 1, -1, -1, 0, 0; // 64.455 - doodsonNumbers["N2"] << 2, -2, 0, 2, 0, 0; // 235.755 - doodsonNumbers["O1"] << 1, -1, 0, 0, 0, 0; // 145.555 - doodsonNumbers["P1"] << 1, 1, -2, 0, 0, 0; // 163.555 - doodsonNumbers["Q1"] << 1, -2, 0, 1, 0, 0; // 135.655 - doodsonNumbers["S2"] << 2, 2, -2, 0, 0, 0; // 273.555 - doodsonNumbers["Ssa"] << 0, 0, 2, 0, 0, 0; // 57.555 + // DoodsonNumbers["K1"] = Array6d (6); + doodsonNumbers["K1"] << 1, 1, 0, 0, 0, 0; // 165.555 + doodsonNumbers["K2"] << 2, 2, 0, 0, 0, 0; // 275.555 + doodsonNumbers["M2"] << 2, 0, 0, 0, 0, 0; // 255.555 + doodsonNumbers["Mf"] << 0, 2, 0, 0, 0, 0; // 75.555 + doodsonNumbers["Mm"] << 0, 1, -1, -1, 0, 0; // 64.455 + doodsonNumbers["N2"] << 2, -2, 0, 2, 0, 0; // 235.755 + doodsonNumbers["O1"] << 1, -1, 0, 0, 0, 0; // 145.555 + doodsonNumbers["P1"] << 1, 1, -2, 0, 0, 0; // 163.555 + doodsonNumbers["Q1"] << 1, -2, 0, 1, 0, 0; // 135.655 + doodsonNumbers["S2"] << 2, 2, -2, 0, 0, 0; // 273.555 + doodsonNumbers["Ssa"] << 0, 0, 2, 0, 0, 0; // 57.555 - initialized = true; + initialized = true; } -Vector3d CenterMassCorrections::estimate( - Array6d& dood) +Vector3d CenterMassCorrections::estimate(Array6d& dood) { - Vector3d cmcEstimate = Vector3d::Zero(); - for (auto& [wave, coeff] : data) - { - 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 - } - } - return cmcEstimate; + Vector3d cmcEstimate = Vector3d::Zero(); + for (auto& [wave, coeff] : data) + { + 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 + } + } + return cmcEstimate; } diff --git a/src/cpp/orbprop/centerMassCorrections.hpp b/src/cpp/orbprop/centerMassCorrections.hpp index dbf794287..0afcaf076 100644 --- a/src/cpp/orbprop/centerMassCorrections.hpp +++ b/src/cpp/orbprop/centerMassCorrections.hpp @@ -1,29 +1,23 @@ #pragma once -#include "eigenIncluder.hpp" - -#include #include +#include +#include "common/eigenIncluder.hpp" -using std::string; using std::map; +using std::string; - -struct CenterMassCorrections +struct CenterMassCorrections { - map data; - bool initialized = false; + map data; + bool initialized = false; - void read( - const string& filename); - - Vector3d estimate( - Array6d& dood); + void read(const string& filename); - //Need to add the fundamental args to the cmc estimate function. QnD - map doodsonNumbers; + Vector3d estimate(Array6d& dood); + // Need to add the fundamental args to the cmc estimate function. QnD + map doodsonNumbers; }; extern CenterMassCorrections cmc; - diff --git a/src/cpp/orbprop/coordinates.cpp b/src/cpp/orbprop/coordinates.cpp index f1baae30b..d457d53d5 100644 --- a/src/cpp/orbprop/coordinates.cpp +++ b/src/cpp/orbprop/coordinates.cpp @@ -1,299 +1,305 @@ - - // #pragma GCC optimize ("O0") - -#include "coordinates.hpp" -#include "constants.hpp" -#include "iers2010.hpp" -#include "attitude.hpp" -#include "planets.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "erp.hpp" -#include "sofa.h" - +#include "orbprop/coordinates.hpp" #include #include #include - #include #include #include #include - +#include "3rdparty/sofa/src/sofa.h" +#include "common/algebra.hpp" +#include "common/attitude.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/erp.hpp" +#include "orbprop/iers2010.hpp" +#include "orbprop/planets.hpp" array FrameSwapper::cacheArr; -FrameSwapper::FrameSwapper( - GTime time, - const ERPValues& erpv) -: time0 {time}, - erpv {erpv} +FrameSwapper::FrameSwapper(GTime time, const ERPValues& erpv) : time0{time}, erpv{erpv} { - for (auto& cache : cacheArr) - { - if ( time0 == cache.time0 - &&erpv == cache.erpv) - { - *this = cache; - return; - } - } - - eci2ecef(time, erpv, i2t_mat, &di2t_mat); - - if (cmc.initialized) - { - Array6d dood_arr = IERS2010::doodson(time, 0); //Will need to add erpval.ut1Utc later - - translation = cmc.estimate(dood_arr); - } + for (auto& cache : cacheArr) + { + if (time0 == cache.time0 && erpv == cache.erpv) + { + *this = cache; + return; + } + } + + eci2ecef(time, erpv, i2t_mat, &di2t_mat); + + if (cmc.initialized) + { + Array6d dood_arr = IERS2010::doodson(time, 0); // Will need to add erpval.ut1Utc later + + translation = cmc.estimate(dood_arr); + } } void eci2ecef( - GTime time, ///< Current time - const ERPValues& erpVal, ///< Structure containing the erp values - Matrix3d& U, ///< Matrix3d containing the rotation matrix - Matrix3d* dU_ptr) ///< Matrix3d containing the time derivative of the rotation matrix + GTime time, ///< Current time + const ERPValues& erpVal, ///< Structure containing the erp values + Matrix3d& U, ///< Matrix3d containing the rotation matrix + Matrix3d* dU_ptr ///< Matrix3d containing the time derivative of the rotation matrix +) { - double xp = erpVal.xp; - double yp = erpVal.yp; - double lod = erpVal.lod; - double ut1_utc = erpVal.ut1Utc; - double dx00 = 0.1725 * DMAS2R; - double dy00 = -0.2650 * DMAS2R; - IERS2010 iers; - - double xp_pm = 0; - double yp_pm = 0; - double ut1_pm = 0; - double lod_pm = 0; - double xp_o = 0; - double yp_o = 0; - double ut1_o = 0; - double lod_o = 0; - - iers.PMGravi (time, ut1_utc, xp_pm, yp_pm, ut1_pm, lod_pm); - FundamentalArgs fundArgs(time, ut1_utc); - - if (hfEop.initialized) - { - hfEop.compute(fundArgs, xp_o, yp_o, ut1_o, lod_o); - } - else - { - iers.PMUTOcean (time, ut1_utc, xp_o, yp_o, ut1_o); //, lod_pm); - } - - double xp_ = xp + (xp_pm + xp_o) * 1e-6 * AS2R; - double yp_ = yp + (yp_pm + yp_o) * 1e-6 * AS2R; - - ut1_utc += (ut1_pm + ut1_o) * 1e-6; - lod += lod_pm * 1e-6; - - MjDateUt1 mjDateUt1 (time, ut1_utc); - MjDateTT mjDateTT (time); - - double sp = Sofa::iauSp (mjDateTT); - double era = Sofa::iauEra (mjDateUt1); - - Matrix3d theta; - theta = Eigen::AngleAxisd(-era, Vector3d::UnitZ()); - - double X_iau = 0; - double Y_iau = 0; - double S_iau = 0; - Sofa::iauXys(mjDateTT, X_iau, Y_iau, S_iau); - X_iau += dx00; - Y_iau += dy00; - Matrix RC2I; - Matrix RPOM; - - iauC2ixys (X_iau, Y_iau, S_iau, (double(*)[3]) &RC2I(0,0)); - iauPom00 (xp_, yp_, sp, (double(*)[3]) &RPOM(0,0)); - - U = RPOM * theta * RC2I; - - if (dU_ptr) - { - Matrix3d matS = Matrix3d::Zero(); - matS (0, 1) = +1; - matS (1, 0) = -1; // Derivative of Earth rotation - - double omega = OMGE; /**@todo add length of day component*/ - Matrix3d matdTheta = omega * matS * theta; // matrix [1/s] - - *dU_ptr = RPOM * matdTheta * RC2I; - } + double xp = erpVal.xp; + double yp = erpVal.yp; + double lod = erpVal.lod; + double ut1_utc = erpVal.ut1Utc; + double dx00 = 0.1725 * DMAS2R; + double dy00 = -0.2650 * DMAS2R; + IERS2010 iers; + + double xp_pm = 0; + double yp_pm = 0; + double ut1_pm = 0; + double lod_pm = 0; + double xp_o = 0; + double yp_o = 0; + double ut1_o = 0; + double lod_o = 0; + + iers.PMGravi(time, ut1_utc, xp_pm, yp_pm, ut1_pm, lod_pm); + FundamentalArgs fundArgs(time, ut1_utc); + + if (hfEop.initialized) + { + hfEop.compute(fundArgs, xp_o, yp_o, ut1_o, lod_o); + } + else + { + iers.PMUTOcean(time, ut1_utc, xp_o, yp_o, ut1_o); //, lod_pm); + } + + double xp_ = xp + (xp_pm + xp_o) * 1e-6 * AS2R; + double yp_ = yp + (yp_pm + yp_o) * 1e-6 * AS2R; + + ut1_utc += (ut1_pm + ut1_o) * 1e-6; + lod += lod_pm * 1e-6; + + MjDateUt1 mjDateUt1(time, ut1_utc); + MjDateTT mjDateTT(time); + + double sp = Sofa::iauSp(mjDateTT); + double era = Sofa::iauEra(mjDateUt1); + + Matrix3d theta; + theta = Eigen::AngleAxisd(-era, Vector3d::UnitZ()); + + double X_iau = 0; + double Y_iau = 0; + double S_iau = 0; + Sofa::iauXys(mjDateTT, X_iau, Y_iau, S_iau); + X_iau += dx00; + Y_iau += dy00; + Matrix RC2I; + Matrix RPOM; + + iauC2ixys(X_iau, Y_iau, S_iau, (double (*)[3]) & RC2I(0, 0)); + iauPom00(xp_, yp_, sp, (double (*)[3]) & RPOM(0, 0)); + + U = RPOM * theta * RC2I; + + if (dU_ptr) + { + Matrix3d matS = Matrix3d::Zero(); + matS(0, 1) = +1; + matS(1, 0) = -1; // Derivative of Earth rotation + + double omega = OMGE; /**@todo add length of day component*/ + Matrix3d matdTheta = omega * matS * theta; // matrix [1/s] + + *dU_ptr = RPOM * matdTheta * RC2I; + } } /** Transform geodetic postion to ecef -*/ -VectorEcef pos2ecef( - const VectorPos& pos) ///< geodetic position {lat,lon,h} (rad,m) + */ +VectorEcef pos2ecef(const VectorPos& pos) ///< geodetic position {lat,lon,h} (rad,m) { - double sinp = sin(pos.lat()); - double cosp = cos(pos.lat()); - double sinl = sin(pos.lon()); - double cosl = cos(pos.lon()); - double e2 = FE_WGS84 * (2 - FE_WGS84); - double v = RE_WGS84 / sqrt(1 - e2 * SQR(sinp)); - - VectorEcef ecef; - ecef[0] = (v +pos.hgt()) * cosp * cosl; - ecef[1] = (v +pos.hgt()) * cosp * sinl; - ecef[2] = (v*(1-e2) +pos.hgt()) * sinp; - - return ecef; + double sinp = sin(pos.lat()); + double cosp = cos(pos.lat()); + double sinl = sin(pos.lon()); + double cosl = cos(pos.lon()); + double e2 = FE_WGS84 * (2 - FE_WGS84); + double v = RE_WGS84 / sqrt(1 - e2 * SQR(sinp)); + + VectorEcef ecef; + ecef[0] = (v + pos.hgt()) * cosp * cosl; + ecef[1] = (v + pos.hgt()) * cosp * sinl; + ecef[2] = (v * (1 - e2) + pos.hgt()) * sinp; + + return ecef; } /** transform ecef to geodetic postion -* args : double *r I ecef position {x,y,z} (m) -* notes : WGS84, ellipsoidal height*/ -VectorPos ecef2pos( - const VectorEcef& r) + * args : double *r I ecef position {x,y,z} (m) + * notes : WGS84, ellipsoidal height*/ +VectorPos ecef2pos(const VectorEcef& r) { - double e2 = FE_WGS84 * (2 - FE_WGS84); - double r2 = dot(r.data(),r.data(),2); - double v = RE_WGS84; - double z; - double zk; - double sinp; - - for (z = r[2], zk = 0; fabs(z-zk) >= 1E-4; ) - { - zk = z; - sinp = z / sqrt(r2 + SQR(z)); - v = RE_WGS84 / sqrt(1 - e2 * SQR(sinp)); - z = r[2] + v * e2 * sinp; - } - - VectorPos pos; - - pos.lat() = r2 > 1E-12 ? atan(z/sqrt(r2)) : (r[2] > 0 ? PI/2: -PI/2); - pos.lon() = r2 > 1E-12 ? atan2(r[1],r[0]) : 0; - pos.hgt() = sqrt(r2 + SQR(z)) - v; - - return pos; + double e2 = FE_WGS84 * (2 - FE_WGS84); + double r2 = dot(r.data(), r.data(), 2); + double v = RE_WGS84; + double z; + double zk; + double sinp; + + for (z = r[2], zk = 0; fabs(z - zk) >= 1E-4;) + { + zk = z; + sinp = z / sqrt(r2 + SQR(z)); + v = RE_WGS84 / sqrt(1 - e2 * SQR(sinp)); + z = r[2] + v * e2 * sinp; + } + + VectorPos pos; + + pos.lat() = r2 > 1E-12 ? atan(z / sqrt(r2)) : (r[2] > 0 ? PI / 2 : -PI / 2); + pos.lon() = r2 > 1E-12 ? atan2(r[1], r[0]) : 0; + pos.hgt() = sqrt(r2 + SQR(z)) - v; + + return pos; } /* ecef to local coordinate transfromation matrix -* args : double *pos I geodetic position {lat,lon} (rad) -* double *E O ecef to local coord transformation matrix (3x3) -* notes : matirix stored by column-major order (fortran convention)*/ + * args : double *pos I geodetic position {lat,lon} (rad) + * double *E O ecef to local coord transformation matrix (3x3) + * notes : matirix stored by column-major order (fortran convention)*/ void pos2enu( - const VectorPos& pos, - double* E) //todo aaron, convert to return Matrix3d, check orientation + const VectorPos& pos, + double* E +) // todo aaron, convert to return Matrix3d, check orientation { - double sinp = sin(pos.lat()); - double cosp = cos(pos.lat()); - double sinl = sin(pos.lon()); - double cosl = cos(pos.lon()); - - E[0] = -sinl; E[3] = +cosl; E[6] = 0; - E[1] = -sinp * cosl; E[4] = -sinp * sinl; E[7] = +cosp; - E[2] = +cosp * cosl; E[5] = +cosp * sinl; E[8] = +sinp; + double sinp = sin(pos.lat()); + double cosp = cos(pos.lat()); + double sinl = sin(pos.lon()); + double cosl = cos(pos.lon()); + + E[0] = -sinl; + E[3] = +cosl; + E[6] = 0; + E[1] = -sinp * cosl; + E[4] = -sinp * sinl; + E[7] = +cosp; + E[2] = +cosp * cosl; + E[5] = +cosp * sinl; + E[8] = +sinp; } /* transform ecef vector to local tangental coordinates -*/ + */ VectorEnu ecef2enu( - const VectorPos& pos, ///< geodetic position {lat,lon} (rad) - const VectorEcef& ecef) ///< vector in ecef coordinate {x,y,z} + const VectorPos& pos, ///< geodetic position {lat,lon} (rad) + const VectorEcef& ecef ///< vector in ecef coordinate {x,y,z} +) { - Matrix3d E; - pos2enu(pos, E.data()); + Matrix3d E; + pos2enu(pos, E.data()); - VectorEnu enu = (Vector3d) (E * ecef); + VectorEnu enu = (Vector3d)(E * ecef); - return enu; -// std::cout << "e\n" << e.transpose() << "\n"; + return enu; + // std::cout << "e\n" << e.transpose() << "\n"; } /** transform local tangental coordinate vector to ecef -*/ + */ VectorEcef enu2ecef( - const VectorPos& pos, ///< geodetic position {lat,lon} (rad) - const VectorEnu& enu) ///< vector in local tangental coordinate {e,n,u} + const VectorPos& pos, ///< geodetic position {lat,lon} (rad) + const VectorEnu& enu ///< vector in local tangental coordinate {e,n,u} +) { - Matrix3d E; - pos2enu(pos, E.data()); + Matrix3d E; + pos2enu(pos, E.data()); - VectorEcef ecef = (Vector3d)(E.transpose() * enu); + VectorEcef ecef = (Vector3d)(E.transpose() * enu); - return ecef; -// std::cout << "E\n" << E << "\n"; + return ecef; + // std::cout << "E\n" << E << "\n"; } /** transform vector in body frame to ecef -*/ + */ VectorEcef body2ecef( - const AttStatus& attStatus, ///< attitude (unit vectors of the axes of body frame) in ecef frame - const Vector3d& rBody) ///< vector in body frame + const AttStatus& + attStatus, ///< attitude (unit vectors of the axes of body frame) in ecef frame + const Vector3d& rBody ///< vector in body frame +) { - Matrix3d R; - R << attStatus.eXBody, attStatus.eYBody, attStatus.eZBody; + Matrix3d R; + R << attStatus.eXBody, attStatus.eYBody, attStatus.eZBody; - Vector3d ecef = R * rBody; + Vector3d ecef = R * rBody; - return ecef; + return ecef; } /** transform vector in ecef frame to body -*/ + */ Vector3d ecef2body( - AttStatus& attStatus, ///< attitude (unit vectors of the axes of body frame) in ecef frame - VectorEcef& ecef, ///< vector in ecef frame - MatrixXd* dEdQ_ptr) + AttStatus& attStatus, ///< attitude (unit vectors of the axes of body frame) in ecef frame + VectorEcef& ecef, ///< vector in ecef frame + MatrixXd* dEdQ_ptr +) { - Matrix3d R; - R << attStatus.eXBody, attStatus.eYBody, attStatus.eZBody; + Matrix3d R; + R << attStatus.eXBody, attStatus.eYBody, attStatus.eZBody; - Vector3d body = R.transpose() * ecef; + Vector3d body = R.transpose() * ecef; - if (dEdQ_ptr) - { - MatrixXd& dEdQ = *dEdQ_ptr; + if (dEdQ_ptr) + { + MatrixXd& dEdQ = *dEdQ_ptr; - dEdQ = MatrixXd(3,4); + dEdQ = MatrixXd(3, 4); - Quaterniond quat(R.transpose()); + Quaterniond quat(R.transpose()); - for (int i = 0; i < 4; i++) - { - Quaterniond qCopy = quat; - double delta = 0.01; + for (int i = 0; i < 4; i++) + { + Quaterniond qCopy = quat; + double delta = 0.01; - if (i == 0) qCopy.w() += delta; - if (i == 1) qCopy.x() += delta; - if (i == 2) qCopy.y() += delta; - if (i == 3) qCopy.z() += delta; + if (i == 0) + qCopy.w() += delta; + if (i == 1) + qCopy.x() += delta; + if (i == 2) + qCopy.y() += delta; + if (i == 3) + qCopy.z() += delta; - qCopy.normalize(); + qCopy.normalize(); - dEdQ.col(i) = ((qCopy * ecef) - body) / delta; - } + dEdQ.col(i) = ((qCopy * ecef) - body) / delta; + } -// std::cout << "\n" << dEdQ << "\n"; - } - return body; + // std::cout << "\n" << dEdQ << "\n"; + } + return body; } Matrix3d ecef2rac( - Vector3d& rSat, // Sat position (ECEF) - Vector3d& satVel) // Sat velocity (ECEF) + Vector3d& rSat, // Sat position (ECEF) + Vector3d& satVel +) // Sat velocity (ECEF) { - // Ref: RTCM c10403.3, equation (3.12-5), p188 (this rotation matrix performs RAC->ECEF, so ECEF->RAC is simply the transpose of this) - Vector3d ea = satVel.normalized(); - Vector3d rv = rSat.cross(satVel); Vector3d ec = rv.normalized(); - Vector3d er = ea.cross(ec); - - Matrix3d Rt; - Rt.row(0) = er; - Rt.row(1) = ea; - Rt.row(2) = ec; - - return Rt; + // Ref: RTCM c10403.3, equation (3.12-5), p188 (this rotation matrix performs RAC->ECEF, so + // ECEF->RAC is simply the transpose of this) + Vector3d ea = satVel.normalized(); + Vector3d rv = rSat.cross(satVel); + Vector3d ec = rv.normalized(); + Vector3d er = ea.cross(ec); + + Matrix3d Rt; + Rt.row(0) = er; + Rt.row(1) = ea; + Rt.row(2) = ec; + + return Rt; } - diff --git a/src/cpp/orbprop/coordinates.hpp b/src/cpp/orbprop/coordinates.hpp index fba30487f..f77337532 100644 --- a/src/cpp/orbprop/coordinates.hpp +++ b/src/cpp/orbprop/coordinates.hpp @@ -1,163 +1,146 @@ - #pragma once #include +#include "common/attitude.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/erp.hpp" +#include "common/gTime.hpp" +#include "orbprop/centerMassCorrections.hpp" +#include "orbprop/iers2010.hpp" using std::array; -#include "centerMassCorrections.hpp" -#include "eigenIncluder.hpp" -#include "constants.hpp" -#include "attitude.hpp" -#include "iers2010.hpp" -#include "gTime.hpp" -#include "erp.hpp" -#include "sofam.h" -#include "sofa.h" - - +// Undefine SOFA macros that conflict with Eigen variable names +///@todo find a better workaround for this +// clang-format off +#include "3rdparty/sofa/src/sofa.h" +#include "3rdparty/sofa/src/sofam.h" +#ifdef DC +#undef DC +#endif +// clang-format on struct Sofa { - static void iauXys (MjDateTT mjdTT, double& x, double& y, double& s) { iauXys00a (DJ00, mjdTT. to_j2000(), &x, &y, &s); } - static double iauGmst (MjDateUt1 mjdUt1, MjDateTT mjdTT) { return iauGmst06 (DJ00, mjdUt1. to_j2000(), DJ00, mjdTT. to_j2000()); } - static double iauGmst (MjDateUt1 mjdUt1) { return iauGmst06 (DJ00, mjdUt1. to_j2000(), DJ00, mjdUt1. to_j2000()); } - static double iauEra (MjDateUt1 mjdUt1) { return iauEra00 (DJ00, mjdUt1. to_j2000()); } - static double iauSp (MjDateTT mjdTT) { return iauSp00 (DJ00, mjdTT. to_j2000()); } - static double iauFal (MjDateTT mjdTT) { return iauFal03 (mjdTT.to_j2000() / 365.25 / 100); } - static double iauFalp (MjDateTT mjdTT) { return iauFalp03 (mjdTT.to_j2000() / 365.25 / 100); } - static double iauFaf (MjDateTT mjdTT) { return iauFaf03 (mjdTT.to_j2000() / 365.25 / 100); } - static double iauFad (MjDateTT mjdTT) { return iauFad03 (mjdTT.to_j2000() / 365.25 / 100); } - static double iauFaom (MjDateTT mjdTT) { return iauFaom03 (mjdTT.to_j2000() / 365.25 / 100); } - static int iauEpv (MjDateTT mjdTT, double pvh[2][3], double pvb[2][3]) { return iauEpv00 (DJ00, mjdTT. to_j2000(), pvh, pvb); } - static void iauMoon (MjDateTT mjdTT, double pv [2][3]) { return iauMoon98 (DJ00, mjdTT. to_j2000(), pv); } + static void iauXys(MjDateTT mjdTT, double& x, double& y, double& s) + { + iauXys00a(DJ00, mjdTT.to_j2000(), &x, &y, &s); + } + static double iauGmst(MjDateUt1 mjdUt1, MjDateTT mjdTT) + { + return iauGmst06(DJ00, mjdUt1.to_j2000(), DJ00, mjdTT.to_j2000()); + } + static double iauGmst(MjDateUt1 mjdUt1) + { + return iauGmst06(DJ00, mjdUt1.to_j2000(), DJ00, mjdUt1.to_j2000()); + } + static double iauEra(MjDateUt1 mjdUt1) { return iauEra00(DJ00, mjdUt1.to_j2000()); } + static double iauSp(MjDateTT mjdTT) { return iauSp00(DJ00, mjdTT.to_j2000()); } + static double iauFal(MjDateTT mjdTT) { return iauFal03(mjdTT.to_j2000() / 365.25 / 100); } + static double iauFalp(MjDateTT mjdTT) { return iauFalp03(mjdTT.to_j2000() / 365.25 / 100); } + static double iauFaf(MjDateTT mjdTT) { return iauFaf03(mjdTT.to_j2000() / 365.25 / 100); } + static double iauFad(MjDateTT mjdTT) { return iauFad03(mjdTT.to_j2000() / 365.25 / 100); } + static double iauFaom(MjDateTT mjdTT) { return iauFaom03(mjdTT.to_j2000() / 365.25 / 100); } + static int iauEpv(MjDateTT mjdTT, double pvh[2][3], double pvb[2][3]) + { + return iauEpv00(DJ00, mjdTT.to_j2000(), pvh, pvb); + } + static void iauMoon(MjDateTT mjdTT, double pv[2][3]) + { + return iauMoon98(DJ00, mjdTT.to_j2000(), pv); + } }; struct XFormData { - double xp_pm = 0; - double yp_pm = 0; - double ut1_pm = 0; - double lod_pm = 0; - double xp_o = 0; - double yp_o = 0; - double ut1_o = 0; - double sp = 0; - double era = 0; + double xp_pm = 0; + double yp_pm = 0; + double ut1_pm = 0; + double lod_pm = 0; + double xp_o = 0; + double yp_o = 0; + double ut1_o = 0; + double sp = 0; + double era = 0; }; -void eci2ecef( - GTime time, - const ERPValues& erpVal, - Matrix3d& U, - Matrix3d* dU_ptr = nullptr); +void eci2ecef(GTime time, const ERPValues& erpVal, Matrix3d& U, Matrix3d* dU_ptr = nullptr); + +void pos2enu(const VectorPos& pos, double* E); + +VectorEnu ecef2enu(const VectorPos& pos, const VectorEcef& r); -void pos2enu( - const VectorPos& pos, - double* E); +VectorEcef enu2ecef(const VectorPos& pos, const VectorEnu& e); -VectorEnu ecef2enu( - const VectorPos& pos, - const VectorEcef& r); +Matrix3d rotBasisMat(Vector3d& eX, Vector3d& eY, Vector3d& eZ); -VectorEcef enu2ecef( - const VectorPos& pos, - const VectorEnu& e); +VectorPos ecef2pos(const VectorEcef& r); -Matrix3d rotBasisMat( - Vector3d& eX, - Vector3d& eY, - Vector3d& eZ); +VectorEcef pos2ecef(const VectorPos& pos); +VectorEcef body2ecef(const AttStatus& attStatus, const Vector3d& rBody); -VectorPos ecef2pos( - const VectorEcef& r); +Vector3d ecef2body(AttStatus& attStatus, VectorEcef& ecef, MatrixXd* dEdQ_ptr = nullptr); -VectorEcef pos2ecef( - const VectorPos& pos); +struct FrameSwapper +{ + static array cacheArr; -VectorEcef body2ecef( - const AttStatus& attStatus, - const Vector3d& rBody); + GTime time0; + ERPValues erpv; -Vector3d ecef2body( - AttStatus& attStatus, - VectorEcef& ecef, - MatrixXd* dEdQ_ptr = nullptr); + Matrix3d i2t_mat; + Matrix3d di2t_mat; + Vector3d translation = Vector3d::Zero(); + void setCache(int cache) { cacheArr[cache] = *this; } + FrameSwapper() {} -struct FrameSwapper -{ - static array cacheArr; - - GTime time0; - ERPValues erpv; - - Matrix3d i2t_mat; - Matrix3d di2t_mat; - Vector3d translation = Vector3d::Zero(); - - void setCache( - int cache) - { - cacheArr[cache] = *this; - } - - FrameSwapper() - { - - } - - FrameSwapper( - GTime time, - const ERPValues& erpv); - - VectorEcef operator()( - const VectorEci rEci, - const VectorEci* vEci_ptr = nullptr, - VectorEcef* vEcef_ptr = nullptr) - { - if ( vEci_ptr - && vEcef_ptr) - { - auto& vEci = *vEci_ptr; - auto& vEcef = *vEcef_ptr; - - vEcef = i2t_mat * vEci - + di2t_mat * rEci; - } - - return (Vector3d) (i2t_mat * rEci + translation); - } - - VectorEci operator()( - const VectorEcef rEcef, - const VectorEcef* vEcef_ptr = nullptr, - VectorEci* vEci_ptr = nullptr) - { - if ( vEcef_ptr - && vEci_ptr) - { - auto& vEcef = *vEcef_ptr; - auto& vEci = *vEci_ptr; - - vEci = i2t_mat.transpose() * vEcef - + di2t_mat.transpose() * rEcef; - } - - return (Vector3d) (i2t_mat.transpose() * ((Vector3d)rEcef - translation)); - } - - VectorEci operator()( - const VectorEcef rEcef, - const GTime time) - { - VectorEci eci = operator()(rEcef); - - double dt = (time - time0).to_double(); - - eci += dt * di2t_mat.transpose() * rEcef; - - return eci; - } + FrameSwapper(GTime time, const ERPValues& erpv); + + VectorEcef operator()( + const VectorEci rEci, + const VectorEci* vEci_ptr = nullptr, + VectorEcef* vEcef_ptr = nullptr + ) + { + if (vEci_ptr && vEcef_ptr) + { + auto& vEci = *vEci_ptr; + auto& vEcef = *vEcef_ptr; + + vEcef = i2t_mat * vEci + di2t_mat * rEci; + } + + return (Vector3d)(i2t_mat * rEci + translation); + } + + VectorEci operator()( + const VectorEcef rEcef, + const VectorEcef* vEcef_ptr = nullptr, + VectorEci* vEci_ptr = nullptr + ) + { + if (vEcef_ptr && vEci_ptr) + { + auto& vEcef = *vEcef_ptr; + auto& vEci = *vEci_ptr; + + vEci = i2t_mat.transpose() * vEcef + di2t_mat.transpose() * rEcef; + } + + return (Vector3d)(i2t_mat.transpose() * ((Vector3d)rEcef - translation)); + } + + VectorEci operator()(const VectorEcef rEcef, const GTime time) + { + VectorEci eci = operator()(rEcef); + + double dt = (time - time0).to_double(); + + eci += dt * di2t_mat.transpose() * rEcef; + + return eci; + } }; diff --git a/src/cpp/orbprop/iers2010.cpp b/src/cpp/orbprop/iers2010.cpp index 2ea5d3441..d7b9ccedc 100644 --- a/src/cpp/orbprop/iers2010.cpp +++ b/src/cpp/orbprop/iers2010.cpp @@ -1,38 +1,38 @@ - // #pragma GCC optimize ("O0") +#include "orbprop/iers2010.hpp" +#include #include -#include #include -#include - -#include +#include +#include #include +#include +#include #include -#include - -#include "acceleration.hpp" -#include "coordinates.hpp" -#include "constants.hpp" -#include "iers2010.hpp" -#include "planets.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "sofa.h" +#include "3rdparty/sofa/src/sofa.h" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/gTime.hpp" +#include "orbprop/acceleration.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/planets.hpp" HfOceanEop hfEop; -const GTime time2010 = GEpoch{2010, E_Month::JAN, 1, 0, 0, 0}; +const GTime time2010 = GEpoch{2010, static_cast(E_Month::JAN), 1, 0, 0, 0}; void IERS2010::PMGravi( - GTime time, ///< Time - double ut1_utc, ///< Input ut1_utc value - double& x, ///< Polar motion in the x direction (micro arc seconds) - double& y, ///< Polar mothin in the y direction (micro arc seconds) - double& ut1, ///< ut1 variation (micro seconds) - double& lod) ///< lod variation (micro seconds) + GTime time, ///< Time + double ut1_utc, ///< Input ut1_utc value + double& x, ///< Polar motion in the x direction (micro arc seconds) + double& y, ///< Polar mothin in the y direction (micro arc seconds) + double& ut1, ///< ut1 variation (micro seconds) + double& lod ///< lod variation (micro seconds) +) { - Eigen::Array pmLibration; /** Tab5.1a IERS 2010, keeping only short periods */ + Eigen::Array pmLibration; /** Tab5.1a IERS 2010, keeping only short periods */ + // clang-format off pmLibration << 1, -1, 0, -2, 0, -1, -0.4, 0.3, -0.3, -0.4, @@ -45,8 +45,10 @@ void IERS2010::PMGravi( 1, 0, 0, 0, 0, 0, 14.3, -8.2, 8.2, 14.3, 1, 0, 0, 0, 0, -1, 1.9, -1.1, 1.1, 1.9, 1, 1, 0, 0, 0, 0, 0.8, -0.4, 0.4, 0.8; + // clang-format on - Eigen::Array utLibration; /** Tab5.1b IERS 2010 */ + Eigen::Array utLibration; /** Tab5.1b IERS 2010 */ + // clang-format off utLibration << 2, -2, 0, -2, 0, -2, 0.05, -0.03, -0.3, -0.6, 2, 0, 0, -2, -2, -2, 0.06, -0.03, -0.4, -0.7, @@ -59,39 +61,34 @@ void IERS2010::PMGravi( 2, 0, 0, -2, 2, -2, 0.76, -0.44, -5.5, -9.5, 2, 0, 0, 0, 0, 0, 0.21, -0.12, -1.5, -2.6, 2, 0, 0, 0, 0, -1, 0.06, -0.04, -0.4, -0.8; - - - FundamentalArgs fundArgs(time, ut1_utc); - - x = 0; - y = 0; - ut1 = 0; - lod = 0; - for (auto pnlib : pmLibration.rowwise()) - { - double arg = (pnlib.segment(0,6) * fundArgs).sum(); - - x += sin(arg) * pnlib(6) + cos(arg)* pnlib(7); - y += sin(arg) * pnlib(8) + cos(arg)* pnlib(9); - } - - for (auto utlib : utLibration.rowwise()) - { - double arg = (utlib.segment(0,6) * fundArgs).sum() ; - ut1 += sin(arg) * utlib(6) + cos(arg)* utlib(7); - lod += sin(arg) * utlib(8) + cos(arg)* utlib(9); - - } + // clang-format on + + FundamentalArgs fundArgs(time, ut1_utc); + + x = 0; + y = 0; + ut1 = 0; + lod = 0; + for (auto pnlib : pmLibration.rowwise()) + { + double arg = (pnlib.segment(0, 6) * fundArgs).sum(); + + x += sin(arg) * pnlib(6) + cos(arg) * pnlib(7); + y += sin(arg) * pnlib(8) + cos(arg) * pnlib(9); + } + + for (auto utlib : utLibration.rowwise()) + { + double arg = (utlib.segment(0, 6) * fundArgs).sum(); + ut1 += sin(arg) * utlib(6) + cos(arg) * utlib(7); + lod += sin(arg) * utlib(8) + cos(arg) * utlib(9); + } } -void IERS2010::PMUTOcean( - GTime time, - double ut1_utc, - double& x, - double& y, - double& ut1) +void IERS2010::PMUTOcean(GTime time, double ut1_utc, double& x, double& y, double& ut1) { - Eigen::Array data; + Eigen::Array data; + // clang-format off data << 1,-1, 0,-2,-2,-2, -0.05, 0.94, -0.94, -0.05, 0.396, -0.078, 1,-2, 0,-2, 0,-1, 0.06, 0.64, -0.64, 0.06, 0.195, -0.059, @@ -164,118 +161,129 @@ void IERS2010::PMUTOcean( 2, 1, 0, 0, 0, 0, -1.77, 1.79, 1.71, 1.04, -0.146, 0.037, 2, 1, 0, 0, 0,-1, -0.77, 0.78, 0.75, 0.45, -0.064, 0.017, 2, 0, 0, 2, 0, 2, -0.33, 0.62, 0.65, 0.19, -0.049, 0.018; - - x = 0; - y = 0; - ut1 = 0; - - FundamentalArgs fundArgs(time, ut1_utc); - for (auto pnlib : data.rowwise()) - { - double arg = (pnlib.segment(0, 6) * fundArgs).sum(); - x += sin(arg) * pnlib(6) + cos(arg) * pnlib(7); - y += sin(arg) * pnlib(8) + cos(arg) * pnlib(9); - ut1 += sin(arg) * pnlib(10) + cos(arg) * pnlib(11); - } + // clang-format on + + x = 0; + y = 0; + ut1 = 0; + + FundamentalArgs fundArgs(time, ut1_utc); + for (auto pnlib : data.rowwise()) + { + double arg = (pnlib.segment(0, 6) * fundArgs).sum(); + x += sin(arg) * pnlib(6) + cos(arg) * pnlib(7); + y += sin(arg) * pnlib(8) + cos(arg) * pnlib(9); + ut1 += sin(arg) * pnlib(10) + cos(arg) * pnlib(11); + } } - -FundamentalArgs::FundamentalArgs( - GTime time, - double ut1_utc) -: gmst {(*this)[0]}, - l {(*this)[1]}, - l_prime {(*this)[2]}, - f {(*this)[3]}, - d {(*this)[4]}, - omega {(*this)[5]} +FundamentalArgs::FundamentalArgs(GTime time, double ut1_utc) + : gmst{(*this)[0]}, + l{(*this)[1]}, + l_prime{(*this)[2]}, + f{(*this)[3]}, + d{(*this)[4]}, + omega{(*this)[5]} { - (*this)[0] = Sofa::iauGmst (MjDateUt1(time, ut1_utc), time) + PI; - (*this)[1] = Sofa::iauFal (time); - (*this)[2] = Sofa::iauFalp (time); - (*this)[3] = Sofa::iauFaf (time); - (*this)[4] = Sofa::iauFad (time); - (*this)[5] = Sofa::iauFaom (time); + (*this)[0] = Sofa::iauGmst(MjDateUt1(time, ut1_utc), time) + PI; + (*this)[1] = Sofa::iauFal(time); + (*this)[2] = Sofa::iauFalp(time); + (*this)[3] = Sofa::iauFaf(time); + (*this)[4] = Sofa::iauFad(time); + (*this)[5] = Sofa::iauFaom(time); } - -Array6d IERS2010::doodson( - GTime time, - double ut1_utc) +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(1) = fundArgs(3) + fundArgs(5); - Doodson(0) = fundArgs(0) - Doodson(1); - Doodson(2) = Doodson(1) - fundArgs(4); - Doodson(3) = Doodson(1) - fundArgs(1); - Doodson(5) = Doodson(1) - fundArgs(4) - fundArgs(2); - - return Doodson; + FundamentalArgs fundArgs(time, ut1_utc); + + Array6d Doodson; + Doodson(4) = + -1 * fundArgs(5); // todo aaron, 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); + Doodson(3) = Doodson(1) - fundArgs(1); + Doodson(5) = Doodson(1) - fundArgs(4) - fundArgs(2); + + return Doodson; } /** Implementation of the first part of the solidEarth tides (Eq 6.6 and 6.7 IERS2010) -*/ + */ void IERS2010::solidEarthTide1( - const Vector3d& ITRFSun, ///< Position of the Sun in ITRF / ECEF - const Vector3d& ITRFMoon, ///< Position of the Moon in ITRF/ ECEF - MatrixXd& Cnm, ///< Modification of the C coefficient - MatrixXd& Snm) ///< Modification of the S coefficient + const Vector3d& ITRFSun, ///< Position of the Sun in ITRF / ECEF + const Vector3d& ITRFMoon, ///< Position of the Moon in ITRF/ ECEF + MatrixXd& Cnm, ///< Modification of the C coefficient + MatrixXd& Snm ///< Modification of the S coefficient +) { - Matrix elasticLove; + Matrix elasticLove; + // clang-format off elasticLove << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.29525, 0.29470, 0.29801, 0, 0, 0.093, 0.093, 0.093, 0.094, 0, -0.00087, -0.00079, -0.00057, 0, 0; - - double GMe = GM_values[E_ThirdBody::EARTH]; - - for (auto body: {E_ThirdBody::SUN, E_ThirdBody::MOON}) - { - Vector3d position = Vector3d::Zero(); - double GM = 0; - if (body == E_ThirdBody::SUN) { position = ITRFSun; GM = GM_values[body]; } - else if (body == E_ThirdBody::MOON) { position = ITRFMoon; GM = GM_values[body]; } - - // BOOST_LOG_TRIVIAL(debug) << position << " " << GM ; - Legendre leg(3); - leg.calculate(position.z()/position.norm()); - double phi = atan2(position.y(), position.x()); - - // Do deg 2 and 3 - for (int ideg = 2; ideg <= 3; ideg++) - for (int iord = 0; iord <= ideg; iord++) - { - // BOOST_LOG_TRIVIAL(debug) << elasticLove(ideg,iord) << ideg << " " < tab65a; - Eigen::Array< double, 21, 8> tab65b; - Eigen::Array< double, 2, 7> tab65c; + Eigen::Array tab65a; + Eigen::Array tab65b; + Eigen::Array tab65c; + // clang-format off tab65a << 1.0e0, -3.0e0, 0.0e0, 2.0e0, 0.0e0, 0.0e0, -0.1e0, 0.0e0, 1.0e0, -3.0e0, 2.0e0, 0.0e0, 0.0e0, 0.0e0, -0.1e0, 0.0e0, @@ -352,196 +360,216 @@ void IERS2010::solidEarthTide2( tab65c << 2.0e0, -1.0e0, 0.0e0, 1.0e0, 0.0e0, 0.0e0, -0.3e0, 2.0e0, 0.0e0, 0.0e0, 0.0e0, 0.0e0, 0.0e0, -1.2e0; - - auto dood_arr = IERS2010::doodson(time, ut1_utc); - - /** - * Effect on C20 - */ - for (auto line : tab65b.rowwise()) - { - double thetaf = (line.segment(0, 6) * dood_arr).sum(); - Cnm(2, 0) += line(6) * 1e-12 * cos(thetaf) - line(7) * 1e-12 * sin(thetaf); - } - - /** - * Effect on C21/S21 - */ - for (auto line : tab65a.rowwise()) - { - double thetaf = (line.segment(0, 6) * dood_arr).sum(); - Cnm(2, 1) += line(6) * 1e-12 * sin(thetaf) + line(7) * 1e-12 * cos(thetaf); - Snm(2, 1) += line(6) * 1e-12 * cos(thetaf) - line(7) * 1e-12 * sin(thetaf); - } - - /** - * Effect on C22/S22 - */ - for (auto line : tab65c.rowwise()) - { - double thetaf = (line.segment(0, 6) * dood_arr).sum(); - Cnm(2, 2) += line(6) * 1e-12 * cos(thetaf); - Snm(2, 2) += -1 * line(6) * 1e-12 * sin(thetaf); - } + // clang-format on + + auto dood_arr = IERS2010::doodson(time, ut1_utc); + + /** + * Effect on C20 + */ + for (auto line : tab65b.rowwise()) + { + double thetaf = (line.segment(0, 6) * dood_arr).sum(); + Cnm(2, 0) += line(6) * 1e-12 * cos(thetaf) - line(7) * 1e-12 * sin(thetaf); + } + + /** + * Effect on C21/S21 + */ + for (auto line : tab65a.rowwise()) + { + double thetaf = (line.segment(0, 6) * dood_arr).sum(); + Cnm(2, 1) += line(6) * 1e-12 * sin(thetaf) + line(7) * 1e-12 * cos(thetaf); + Snm(2, 1) += line(6) * 1e-12 * cos(thetaf) - line(7) * 1e-12 * sin(thetaf); + } + + /** + * Effect on C22/S22 + */ + for (auto line : tab65c.rowwise()) + { + double thetaf = (line.segment(0, 6) * dood_arr).sum(); + Cnm(2, 2) += line(6) * 1e-12 * cos(thetaf); + Snm(2, 2) += -1 * line(6) * 1e-12 * sin(thetaf); + } } - void IERS2010::poleSolidEarthTide( - MjDateTT mjd, - const double xp, - const double yp, - MatrixXd& Cnm, - MatrixXd& Snm) + MjDateTT mjd, + const double xp, + const double yp, + MatrixXd& Cnm, + MatrixXd& Snm +) { - double xpv; - double ypv; - meanPole(mjd, xpv, ypv); - - double m1 = +(xp / AS2R - xpv / 1000); - double m2 = -(yp / AS2R - ypv / 1000); - Cnm(2, 1) += -1.333e-9 * (m1 + 0.0115 * m2); - Snm(2, 1) += -1.333e-9 * (m2 - 0.0115 * m1); + double xpv; + double ypv; + secularPole(mjd, xpv, ypv); + + double m1 = +(xp / AS2R - xpv / 1000); + double m2 = -(yp / AS2R - ypv / 1000); + Cnm(2, 1) += -1.333e-9 * (m1 + 0.0115 * m2); + Snm(2, 1) += -1.333e-9 * (m2 - 0.0115 * m1); } - void IERS2010::poleOceanTide( - MjDateTT mjd, - const double xp, - const double yp, - MatrixXd& Cnm, - MatrixXd& Snm) + MjDateTT mjd, + const double xp, + const double yp, + MatrixXd& Cnm, + MatrixXd& Snm +) { - double xpv; - double ypv; - meanPole(mjd, xpv, ypv); - - double m1 = +(xp / AS2R - xpv / 1000); - double m2 = -(yp / AS2R - ypv / 1000); - Cnm(2, 1) += -2.1778e-10 * (m1 - 0.01724 * m2); - Snm(2, 1) += -1.7232e-10 * (m2 - 0.03365 * m1); + double xpv; + double ypv; + secularPole(mjd, xpv, ypv); + + double m1 = +(xp / AS2R - xpv / 1000); + double m2 = -(yp / AS2R - ypv / 1000); + Cnm(2, 1) += -2.1778e-10 * (m1 - 0.01724 * m2); + Snm(2, 1) += -1.7232e-10 * (m2 - 0.03365 * m1); } - -void IERS2010::meanPole( - const MjDateTT& mjd, - double& xpv, - double& ypv) +void IERS2010::secularPole(const MjDateTT& mjd, double& xpv, double& ypv) { - double t = mjd.to_j2000() / 365.25; - xpv = 55.0 + 1.677 * t; - ypv = 320.5 + 3.460 * t; + double t = mjd.to_j2000() / 365.25; + xpv = 55.0 + 1.677 * t; + ypv = 320.5 + 3.460 * t; } - Vector3d IERS2010::relativity( - const Vector3d& posSat, - const Vector3d& velSat, - const Vector3d& posSun, - const Vector3d& velSun, - const Matrix3d& U, - const Matrix3d& dU) + const Vector3d& posSat, + const Vector3d& velSat, + const Vector3d& posSun, + const Vector3d& velSun, + const Matrix3d& U, + const Matrix3d& dU +) { - double GMe = GM_values[E_ThirdBody::EARTH]; - double GMs = GM_values[E_ThirdBody::SUN]; + double GMe = GM_values[E_ThirdBody::EARTH]; + double GMs = GM_values[E_ThirdBody::SUN]; - // required pos and vel of Earth wrt sun, we have sun wrt earth => so mult by -1 - Vector3d posEarth = -posSun; - Vector3d velEarth = -velSun; + // required pos and vel of Earth wrt sun, we have sun wrt earth => so mult by -1 + Vector3d posEarth = -posSun; + Vector3d velEarth = -velSun; - double beta = 1; - double gamma = 1; + double beta = 1; + double gamma = 1; - double rsat = posSat.norm(); - double rsun = posEarth.norm(); + double rsat = posSat.norm(); + double rsun = posEarth.norm(); - Matrix3d S = dU.transpose() * U; + Matrix3d S = dU.transpose() * U; - Vector3d anglevel; - anglevel(0) = S(2, 1); - anglevel(1) = S(0, 2); - anglevel(2) = S(1, 0); + Vector3d anglevel; + anglevel(0) = S(2, 1); + anglevel(1) = S(0, 2); + anglevel(2) = S(1, 0); - Vector3d J = anglevel * 8.0365e37 / 5.9736e24; + Vector3d J = anglevel * 8.0365e37 / 5.9736e24; - // 1st term (Schwarzchild) - Vector3d acc1 = GMe / (SQR(CLIGHT) * pow(rsat, 3)) * ((2 * (beta + gamma) * GMe / rsat - gamma * velSat.dot(velSat)) * posSat + 2 * (1 + gamma) * posSat.dot(velSat) * velSat); + // 1st term (Schwarzchild) + Vector3d acc1 = GMe / (SQR(CLIGHT) * pow(rsat, 3)) * + ((2 * (beta + gamma) * GMe / rsat - gamma * velSat.dot(velSat)) * posSat + + 2 * (1 + gamma) * posSat.dot(velSat) * velSat); - // 2nt term (Lense-Thirring) - Vector3d acc2 = (1 + gamma) * GMe / (SQR(CLIGHT) * pow(rsat, 3)) * ( 3 / SQR(rsat) * posSat.cross(velSat) * posSat.dot(J) + velSat.cross(J) ) ; + // 2nt term (Lense-Thirring) + Vector3d acc2 = (1 + gamma) * GMe / (SQR(CLIGHT) * pow(rsat, 3)) * + (3 / SQR(rsat) * posSat.cross(velSat) * posSat.dot(J) + velSat.cross(J)); - //3rd (de Sitter .aka. geodesic preciession) - Vector3d acc3 = (1 + 2 * gamma) * (velEarth.cross((-1 * GMs * posEarth) / (SQR(CLIGHT) * pow(rsun, 3)))).cross(velSat); + // 3rd (de Sitter .aka. geodesic preciession) + Vector3d acc3 = + (1 + 2 * gamma) * + (velEarth.cross((-1 * GMs * posEarth) / (SQR(CLIGHT) * pow(rsun, 3)))).cross(velSat); - return acc1 + acc2 + acc3; + return acc1 + acc2 + acc3; } - -void HfOceanEop::read( - const string& filename) +void HfOceanEop::read(const string& filename) { - std::ifstream file(filename); - - if (!file) - { - BOOST_LOG_TRIVIAL(error) - << "HF Ocean eop file open error " << filename; - - return; - } - - string line; - - while (std::getline(file, line)) - { - if (line[0] == '#') - { - continue; - } - - std::istringstream iss(line); - HfOceanEOPData data; - iss >> data.name; - for (int i = 0; i < 6; i++) - { - iss >> data.mFundamentalArgs[i]; - } - - iss >> data.doodson; - iss >> data.period; - iss >> data.xSin; - iss >> data.xCos; - iss >> data.ySin; - iss >> data.yCos; - iss >> data.ut1Sin; - iss >> data.ut1Cos; - iss >> data.lodSin; - iss >> data.lodCos; - - hfOceanDataVec.push_back(data); - } - - initialized = true; + std::ifstream file(filename); + + if (!file) + { + BOOST_LOG_TRIVIAL(error) << "HF Ocean eop file open error " << filename; + + return; + } + + string line; + + while (std::getline(file, line)) + { + if (line[0] == '#') + { + continue; + } + + std::istringstream iss(line); + HfOceanEOPData data; + iss >> data.name; + for (int i = 0; i < 6; i++) + { + iss >> data.mFundamentalArgs[i]; + } + + iss >> data.doodson; + iss >> data.period; + iss >> data.xSin; + iss >> data.xCos; + iss >> data.ySin; + iss >> data.yCos; + iss >> data.ut1Sin; + iss >> data.ut1Cos; + iss >> data.lodSin; + iss >> data.lodCos; + + hfOceanDataVec.push_back(data); + } + + initialized = true; } -void HfOceanEop::compute( - Array6d& fundamentalArgs, - double& x, - double& y, - double& ut1, - double& lod) +void HfOceanEop::compute(Array6d& fundamentalArgs, double& x, double& y, double& ut1, double& lod) { - x = 0; - y = 0; - ut1 = 0; - lod = 0; - - for (auto& hfdata : hfOceanDataVec) - { - double theta = (fundamentalArgs * hfdata.mFundamentalArgs).sum(); - - x += hfdata.xCos * cos(theta) + hfdata.xSin * sin(theta); - y += hfdata.yCos * cos(theta) + hfdata.ySin * sin(theta); - ut1 += hfdata.ut1Cos * cos(theta) + hfdata.ut1Sin * sin(theta); - lod += hfdata.lodCos * cos(theta) + hfdata.lodSin * sin(theta); - } -} + // Initialize output values + x = 0; + y = 0; + ut1 = 0; + lod = 0; + +// Parallelize the loop and avoid atomic by using local copies +#pragma omp parallel + { + // Thread-local accumulators + double thread_x = 0; + double thread_y = 0; + double thread_ut1 = 0; + double thread_lod = 0; + +// Perform computations in parallel +#pragma omp for + for (size_t i = 0; i < hfOceanDataVec.size(); ++i) + { + auto& hfdata = hfOceanDataVec[i]; + double theta = (fundamentalArgs * hfdata.mFundamentalArgs).sum(); + double cosTheta = cos(theta); + double sinTheta = sin(theta); + + // Accumulate results in thread-local variables + thread_x += hfdata.xCos * cosTheta + hfdata.xSin * sinTheta; + thread_y += hfdata.yCos * cosTheta + hfdata.ySin * sinTheta; + thread_ut1 += hfdata.ut1Cos * cosTheta + hfdata.ut1Sin * sinTheta; + thread_lod += hfdata.lodCos * cosTheta + hfdata.lodSin * sinTheta; + } + +// Combine the thread-local results into the global ones +#pragma omp critical + { + x += thread_x; + y += thread_y; + ut1 += thread_ut1; + lod += thread_lod; + } + } +} \ No newline at end of file diff --git a/src/cpp/orbprop/iers2010.hpp b/src/cpp/orbprop/iers2010.hpp index 1c65bcd35..d9f702e11 100644 --- a/src/cpp/orbprop/iers2010.hpp +++ b/src/cpp/orbprop/iers2010.hpp @@ -1,122 +1,89 @@ - #pragma once #include +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" using std::vector; -#include "eigenIncluder.hpp" -#include "gTime.hpp" - struct FundamentalArgs : Array6d { - double& gmst; - double& l; - double& l_prime; - double& f; - double& d; - double& omega; - - FundamentalArgs( - GTime time, - double ut1_utc); + double& gmst; + double& l; + double& l_prime; + double& f; + double& d; + double& omega; + + FundamentalArgs(GTime time, double ut1_utc); }; struct HfOceanEOPData { - //Array of 6 value for the fundamental args - string name; - string doodson; - double period; - Array6d mFundamentalArgs; - double xCos; - double xSin; - double yCos; - double ySin; - double ut1Cos; - double ut1Sin; - double lodCos; - double lodSin; + // Array of 6 value for the fundamental args + string name; + string doodson; + double period; + Array6d mFundamentalArgs; + double xCos; + double xSin; + double yCos; + double ySin; + double ut1Cos; + double ut1Sin; + double lodCos; + double lodSin; }; - struct HfOceanEop { - vector hfOceanDataVec; - string filename; - bool initialized = false; - - void read( - const string& filename); - - void compute( - Array6d& fundamentalArgs, - double& x, - double& y, - double& ut1, - double& lod); -}; + vector hfOceanDataVec; + string filename; + bool initialized = false; + + void read(const string& filename); -extern HfOceanEop hfEop; + void compute(Array6d& fundamentalArgs, double& x, double& y, double& ut1, double& lod); +}; +extern HfOceanEop hfEop; struct IERS2010 { - static void PMGravi( - GTime time, - double ut1_utc, - double& x, - double& y, - double& ut1, - double& lod); - - static void PMUTOcean( - GTime time, - double ut1_utc, - double& x, - double& y, - double& ut); - - static Array6d doodson( - GTime time, - double ut1_utc); - - static void solidEarthTide1( - const Vector3d& ITRFSun, - const Vector3d& ITRFMoon, - MatrixXd& Cnm, - MatrixXd& Snm); - - static void solidEarthTide2( - GTime time, - double ut1_utc, - MatrixXd& Cnm, - MatrixXd& Snm); - - static void poleSolidEarthTide( - MjDateTT mjdTT, - const double xp, - const double yp, - MatrixXd& Cnm, - MatrixXd& Snm); - - static void poleOceanTide( - MjDateTT mjdTT, - const double xp, - const double yp, - MatrixXd& Cnm, - MatrixXd& Snm); - - static Vector3d relativity( - const Vector3d& posSat, - const Vector3d& velSat, - const Vector3d& posSun, - const Vector3d& velSun, - const Matrix3d& U, - const Matrix3d& dU); - - static void meanPole( - const MjDateTT& mjd, - double& xpv, - double& ypv); + static void PMGravi(GTime time, double ut1_utc, double& x, double& y, double& ut1, double& lod); + + static void PMUTOcean(GTime time, double ut1_utc, double& x, double& y, double& ut); + + static Array6d doodson(GTime time, double ut1_utc); + + static void solidEarthTide1( + const Vector3d& ITRFSun, + const Vector3d& ITRFMoon, + MatrixXd& Cnm, + MatrixXd& Snm + ); + + static void solidEarthTide2(GTime time, double ut1_utc, MatrixXd& Cnm, MatrixXd& Snm); + + static void poleSolidEarthTide( + MjDateTT mjdTT, + const double xp, + const double yp, + MatrixXd& Cnm, + MatrixXd& Snm + ); + + static void + poleOceanTide(MjDateTT mjdTT, const double xp, const double yp, MatrixXd& Cnm, MatrixXd& Snm); + + static Vector3d relativity( + const Vector3d& posSat, + const Vector3d& velSat, + const Vector3d& posSun, + const Vector3d& velSun, + const Matrix3d& U, + const Matrix3d& dU + ); + + static void secularPole(const MjDateTT& mjd, double& xpv, double& ypv); }; diff --git a/src/cpp/orbprop/oceanPoleTide.cpp b/src/cpp/orbprop/oceanPoleTide.cpp index 956f9aabf..397ba399e 100644 --- a/src/cpp/orbprop/oceanPoleTide.cpp +++ b/src/cpp/orbprop/oceanPoleTide.cpp @@ -1,76 +1,66 @@ - +#include "orbprop/oceanPoleTide.hpp" #include - #include -#include #include - -#include "oceanPoleTide.hpp" +#include +#include "common/constants.hpp" OceanPoleTide oceanPoleTide; - -void OceanPoleTide::read( - const string& filename, - int maxDeg) +void OceanPoleTide::read(const string& filename, int maxDeg) { - if (filename.empty()) - { - return; - } + if (filename.empty()) + { + return; + } - std::ifstream infile(filename); - if (!infile) - { - BOOST_LOG_TRIVIAL(error) - << "Ocean pole tide file open error " << filename; + std::ifstream infile(filename); + if (!infile) + { + BOOST_LOG_TRIVIAL(error) << "Ocean pole tide file open error " << filename; - return; - } + return; + } - cnmp = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); - cnmm = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); - snmp = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); - snmm = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); + cnmp = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); + cnmm = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); + snmp = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); + snmm = MatrixXd::Zero(maxDeg + 1, maxDeg + 1); - string line; - std::getline(infile, line); - while (std::getline(infile, line)) - { - if (line[0] == '#') - { - continue; - } + string line; + std::getline(infile, line); + while (std::getline(infile, line)) + { + if (line[0] == '#') + { + continue; + } - std::istringstream iss(line); - int n; - int m; - double cnmp_; - double cnmm_; - double snmp_; - double snmm_; - iss >> n >> m >> cnmp_ >> cnmm_ >> snmp_ >> snmm_; - if (n <= maxDeg) - { - cnmp(n, m) = cnmp_; - cnmm(n, m) = cnmm_; - snmp(n, m) = snmp_; - snmm(n, m) = snmm_; - } - } - initialized = true; + std::istringstream iss(line); + int n; + int m; + double cnmp_; + double cnmm_; + double snmp_; + double snmm_; + iss >> n >> m >> cnmp_ >> cnmm_ >> snmp_ >> snmm_; + if (n <= maxDeg) + { + cnmp(n, m) = cnmp_; + cnmm(n, m) = cnmm_; + snmp(n, m) = snmp_; + snmm(n, m) = snmm_; + } + } + initialized = true; } -void OceanPoleTide::estimate( - double m1, - double m2, - MatrixXd& Cnm, - MatrixXd& Snm) +void OceanPoleTide::estimate(double m1, double m2, MatrixXd& Cnm, MatrixXd& Snm) { - double coeff1 = m1 * gamma2_r + m2 * gamma2_i; - double coeff2 = m2 * gamma2_r - m1 * gamma2_i; - coeff1 *= M_PI / 180 / 3600; - coeff2 *= M_PI / 180 / 3600; - Cnm += coeff1 * cnmp + coeff2 * cnmm; - Snm += coeff1 * snmp + coeff2 * snmm; + double coeff1 = m1 * gamma2_r + m2 * gamma2_i; + double coeff2 = m2 * gamma2_r - m1 * gamma2_i; + coeff1 *= PI / 180 / 3600; + coeff2 *= PI / 180 / 3600; + Cnm += coeff1 * cnmp + coeff2 * cnmm; + Snm += coeff1 * snmp + coeff2 * snmm; } diff --git a/src/cpp/orbprop/oceanPoleTide.hpp b/src/cpp/orbprop/oceanPoleTide.hpp index 15e3bee23..12ed32d5a 100644 --- a/src/cpp/orbprop/oceanPoleTide.hpp +++ b/src/cpp/orbprop/oceanPoleTide.hpp @@ -1,34 +1,24 @@ #pragma once #include +#include "common/eigenIncluder.hpp" using std::string; -#include "eigenIncluder.hpp" - - -struct OceanPoleTide +struct OceanPoleTide { - void read( - const string& filename, - int maxDeg); - - void estimate( - double m1, - double m2, - MatrixXd& Cnm, - MatrixXd& Snm); + void read(const string& filename, int maxDeg); + + void estimate(double m1, double m2, MatrixXd& Cnm, MatrixXd& Snm); - bool initialized = false; - MatrixXd cnmp; - MatrixXd cnmm; - MatrixXd snmp; - MatrixXd snmm; + bool initialized = false; + MatrixXd cnmp; + MatrixXd cnmm; + MatrixXd snmp; + MatrixXd snmm; - const double gamma2_i = 0.0036; - const double gamma2_r = 0.6870; + const double gamma2_i = 0.0036; + const double gamma2_r = 0.6870; }; extern OceanPoleTide oceanPoleTide; - - diff --git a/src/cpp/orbprop/orbitProp.cpp b/src/cpp/orbprop/orbitProp.cpp index 38fe9e4fe..3317d2e78 100644 --- a/src/cpp/orbprop/orbitProp.cpp +++ b/src/cpp/orbprop/orbitProp.cpp @@ -1,1102 +1,1529 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -/** - */ -Architecture Orbit_Integrator__() -{ - -} - +#include "orbprop/orbitProp.hpp" #include #include #include - -#ifdef ENABLE_PARALLELISATION - #include "omp.h" -#endif +#include "3rdparty/nrlmsise/nrlmsise-00.h" +#include "architectureDocs.hpp" +#include "common/attitude.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/mongo.hpp" +#include "common/navigation.hpp" +#include "common/sinex.hpp" +#include "common/ubxDecoder.hpp" +#include "omp.h" +#include "orbprop/acceleration.hpp" +#include "orbprop/aod.hpp" +#include "orbprop/boxwing.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/iers2010.hpp" +#include "orbprop/oceanPoleTide.hpp" +#include "orbprop/planets.hpp" +#include "orbprop/spaceWeather.hpp" +#include "orbprop/staticField.hpp" +#include "orbprop/tideCoeff.hpp" +#include "pea/inputsOutputs.hpp" using boost::algorithm::to_lower; using std::deque; using std::map; -#include "interactiveTerminal.hpp" -#include "inputsOutputs.hpp" -#include "oceanPoleTide.hpp" -#include "eigenIncluder.hpp" -#include "acceleration.hpp" -#include "coordinates.hpp" -#include "staticField.hpp" -#include "navigation.hpp" -#include "orbitProp.hpp" -#include "constants.hpp" -#include "tideCoeff.hpp" -#include "attitude.hpp" -#include "iers2010.hpp" -#include "boxwing.hpp" -#include "planets.hpp" -#include "mongo.hpp" -#include "sinex.hpp" -#include "enums.h" -#include "aod.hpp" - -#include "ubxDecoder.hpp" - -using bsoncxx::builder::stream::open_array; -using bsoncxx::builder::stream::close_array; -using bsoncxx::builder::stream::document; - -bool queryVectorElement( - vector& vec, - size_t index) -{ - if (index < vec.size()) { return vec[index]; } - else { return vec.back(); } -} +/** + */ +Architecture Orbit_Integrator__() {} +#ifdef ENABLE_PARALLELISATION +#endif -void OrbitIntegrator::computeCommon( - GTime time) +bool queryVectorElement(vector& vec, size_t index) { - ERPValues erpv = getErp(nav.erp, time); - - FrameSwapper frameSwapper(time, erpv); - eci2ecf = frameSwapper.i2t_mat; - deci2ecf = frameSwapper.di2t_mat; //todo aaron, just fs this instead of matrices? - - for (auto& body : E_ThirdBody::_values()) - { - jplEphPos(nav.jplEph_ptr, time, body, planetsPosMap[body], &planetsVelMap[body]); - } - - Array6d dood_arr = IERS2010::doodson(time, erpv.ut1Utc); - - if (acsConfig.propagationOptions.egm_field) - for (auto& once : {1}) - { - if (egm.initialised == false) - { - BOOST_LOG_TRIVIAL(error) << "Error: EGM data not initialised, check for valid egm file"; - - break; - } - - Cnm = egm.gfctC; - Snm = egm.gfctS; - if (acsConfig.propagationOptions.solid_earth_tide) - { - MatrixXd Cnm_solid = MatrixXd::Zero(5, 5); - MatrixXd Snm_solid = MatrixXd::Zero(5, 5); - - IERS2010::solidEarthTide1( - eci2ecf * planetsPosMap[E_ThirdBody::SUN], - eci2ecf * planetsPosMap[E_ThirdBody::MOON], - Cnm_solid, - Snm_solid); - - IERS2010::solidEarthTide2(time, erpv.ut1Utc, Cnm_solid, Snm_solid); - - Cnm.topLeftCorner(5, 5) += Cnm_solid; - Snm.topLeftCorner(5, 5) += Snm_solid; - } - - if (acsConfig.propagationOptions.pole_tide_ocean) - { - MatrixXd Cnm_poleTide = MatrixXd::Zero(Cnm.rows(), Cnm.cols()); - MatrixXd Snm_poleTide = MatrixXd::Zero(Snm.rows(), Snm.cols()); - - if (oceanPoleTide.initialized) - { - double xpv; - double ypv; - IERS2010::meanPole(time, xpv, ypv); - - double m1 = +(erpv.xp / AS2R - xpv / 1000); - double m2 = -(erpv.yp / AS2R - ypv / 1000); - - oceanPoleTide.estimate(m1, m2, Cnm_poleTide, Snm_poleTide); - } - else - { - IERS2010::poleOceanTide(time, erpv.xp, erpv.yp, Cnm_poleTide, Snm_poleTide); - } - - Cnm += Cnm_poleTide; - Snm += Snm_poleTide; - } - - if (acsConfig.propagationOptions.aod) - { - MatrixXd Cnm_aod; - MatrixXd Snm_aod; - - aod.interpolate(time, Cnm_aod, Snm_aod); - - Cnm += Cnm_aod; - Snm += Snm_aod; - } - - if (acsConfig.propagationOptions.pole_tide_solid) - { - IERS2010::poleSolidEarthTide(time, erpv.xp, erpv.yp, Cnm, Snm); - } - - if (acsConfig.propagationOptions.ocean_tide) - { - MatrixXd Cnm_ocean = MatrixXd::Zero(Cnm.rows(), Cnm.cols()); - MatrixXd Snm_ocean = MatrixXd::Zero(Snm.rows(), Snm.cols()); - - oceanTide.getSPH(dood_arr, Cnm_ocean, Snm_ocean); - - Cnm += Cnm_ocean; - Snm += Snm_ocean; - } - - if (acsConfig.propagationOptions.atm_tide) - { - MatrixXd Cnm_atm = MatrixXd::Zero(Cnm.rows(), Cnm.cols()); - MatrixXd Snm_atm = MatrixXd::Zero(Snm.rows(), Snm.cols()); - - atmosphericTide.getSPH(dood_arr, Cnm_atm, Snm_atm); - - Cnm += Cnm_atm; - Snm += Snm_atm; - } - } + if (index < vec.size()) + { + return vec[index]; + } + else + { + return vec.back(); + } } +void OrbitIntegrator::computeCommon(GTime time) +{ + ERPValues erpv = getErp(nav.erp, time); + + FrameSwapper frameSwapper(time, erpv); + eci2ecf = frameSwapper.i2t_mat; + deci2ecf = frameSwapper.di2t_mat; // todo aaron, just fs this instead of matrices? + + for (auto body : enum_values()) + { + jplEphPos(nav.jplEph_ptr, time, body, planetsPosMap[body], &planetsVelMap[body]); + } + + Array6d dood_arr = IERS2010::doodson(time, erpv.ut1Utc); + + if (acsConfig.propagationOptions.egm_field) + for (auto& once : {1}) + { + if (egm.initialised == false) + { + BOOST_LOG_TRIVIAL(error) << "EGM data not initialised, check for valid egm file"; + + break; + } + + Cnm = egm.gfctC; + Snm = egm.gfctS; + if (acsConfig.propagationOptions.solid_earth_tide) + { + MatrixXd Cnm_solid = MatrixXd::Zero(5, 5); + MatrixXd Snm_solid = MatrixXd::Zero(5, 5); + + IERS2010::solidEarthTide1( + eci2ecf * planetsPosMap[E_ThirdBody::SUN], + eci2ecf * planetsPosMap[E_ThirdBody::MOON], + Cnm_solid, + Snm_solid + ); + + IERS2010::solidEarthTide2(time, erpv.ut1Utc, Cnm_solid, Snm_solid); + + Cnm.topLeftCorner(5, 5) += Cnm_solid; + Snm.topLeftCorner(5, 5) += Snm_solid; + } + + if (acsConfig.propagationOptions.pole_tide_ocean) + { + MatrixXd Cnm_poleTide = MatrixXd::Zero(Cnm.rows(), Cnm.cols()); + MatrixXd Snm_poleTide = MatrixXd::Zero(Snm.rows(), Snm.cols()); + + if (oceanPoleTide.initialized) + { + double xpv; + double ypv; + IERS2010::secularPole(time, xpv, ypv); + + double m1 = +(erpv.xp / AS2R - xpv / 1000); + double m2 = -(erpv.yp / AS2R - ypv / 1000); + + oceanPoleTide.estimate(m1, m2, Cnm_poleTide, Snm_poleTide); + } + else + { + IERS2010::poleOceanTide(time, erpv.xp, erpv.yp, Cnm_poleTide, Snm_poleTide); + } + + Cnm += Cnm_poleTide; + Snm += Snm_poleTide; + } + + if (acsConfig.propagationOptions.aod) + { + MatrixXd Cnm_aod; + MatrixXd Snm_aod; + + aod.interpolate(time, Cnm_aod, Snm_aod); + + Cnm += Cnm_aod; + Snm += Snm_aod; + } + + if (acsConfig.propagationOptions.pole_tide_solid) + { + IERS2010::poleSolidEarthTide(time, erpv.xp, erpv.yp, Cnm, Snm); + } + + if (acsConfig.propagationOptions.ocean_tide) + { + MatrixXd Cnm_ocean = MatrixXd::Zero(Cnm.rows(), Cnm.cols()); + MatrixXd Snm_ocean = MatrixXd::Zero(Snm.rows(), Snm.cols()); + + oceanTide.getSPH(dood_arr, Cnm_ocean, Snm_ocean); + + Cnm += Cnm_ocean; + Snm += Snm_ocean; + } + + if (acsConfig.propagationOptions.atm_tide) + { + MatrixXd Cnm_atm = MatrixXd::Zero(Cnm.rows(), Cnm.cols()); + MatrixXd Snm_atm = MatrixXd::Zero(Snm.rows(), Snm.cols()); + + atmosphericTide.getSPH(dood_arr, Cnm_atm, Snm_atm); + + Cnm += Cnm_atm; + Snm += Snm_atm; + } + } +} void OrbitIntegrator::computeAcceleration( - const OrbitState& orbInit, - Vector3d& acc, - Matrix3d& dAdPos, - Matrix3d& dAdVel, - MatrixXd& dAdParam, - const GTime time) + const OrbitState& orbInit, + Vector3d& acc, + Matrix3d& dAdPos, + Matrix3d& dAdVel, + MatrixXd& dAdParam, + const GTime time +) { - auto trace = getTraceFile(nav.satNavMap[orbInit.Sat]); - - trace << "\n" << "Computing accelerations at " << time; - - Vector3d rSat = orbInit.pos; - Vector3d vSat = orbInit.vel; - - const double posOffset = 1e-3; - const double velOffset = 1e-6; - - const Vector3d satToSun = planetsPosMap[E_ThirdBody::SUN] - rSat; - const Vector3d ed = satToSun .normalized(); - const Vector3d er = rSat .normalized(); - const Vector3d ey = ed.cross(er) .normalized(); - const Vector3d eb = ed.cross(ey) .normalized(); - const Vector3d en = er.cross(vSat) .normalized(); - const Vector3d et = en.cross(er) .normalized(); - const Vector3d ep = ed.cross(Vector3d::UnitZ()) .normalized(); - const Vector3d eq = ed.cross(ep) .normalized(); - - if (acsConfig.propagationOptions.central_force) - { - Vector3d accCF = accelCentralForce(rSat, GM_values[E_ThirdBody::EARTH], &dAdPos); + auto trace = getTraceFile(nav.satNavMap[orbInit.Sat]); + auto& subState = *orbInit.subState_ptr; + auto keyTemplate = subState.kfIndexMap.begin()->first; + + trace << "\n" + << "Computing accelerations at " << time; + + Vector3d rSat = orbInit.pos; + Vector3d vSat = orbInit.vel; + + const double posOffset = 1e-3; + const double velOffset = 1e-6; + + const Vector3d satToSun = planetsPosMap[E_ThirdBody::SUN] - rSat; + const Vector3d ed = satToSun.normalized(); + const Vector3d er = rSat.normalized(); + const Vector3d ey = ed.cross(er).normalized(); + const Vector3d eb = ed.cross(ey).normalized(); + const Vector3d en = er.cross(vSat).normalized(); + const Vector3d et = en.cross(er).normalized(); + const Vector3d ep = ed.cross(Vector3d::UnitZ()).normalized(); + const Vector3d eq = ed.cross(ep).normalized(); + + if (acsConfig.propagationOptions.central_force) + { + Vector3d accCF = accelCentralForce(rSat, GM_values[E_ThirdBody::EARTH], &dAdPos); acc += accCF; - orbInit.componentsMap[E_Component::CENTRAL_FORCE] = accCF.norm(); - } + orbInit.componentsMap[E_Component::CENTRAL_FORCE] = accCF.norm(); + } - { - Vector3d accPlanets = Vector3d::Zero(); + { + Vector3d accPlanets = Vector3d::Zero(); - for (auto& planet : orbInit.planetary_perturbations) - { - if (planet == +E_ThirdBody::EARTH) - { - continue; - } + for (auto& planet : orbInit.planetary_perturbations) + { + if (planet == E_ThirdBody::EARTH) + { + continue; + } - auto& planetPos = planetsPosMap[planet]; + auto& planetPos = planetsPosMap[planet]; - Vector3d accPlanet = accelSourcePoint(rSat, planetPos, GM_values[planet], &dAdPos); + Vector3d accPlanet = accelSourcePoint(rSat, planetPos, GM_values[planet], &dAdPos); - accPlanets += accPlanet; - } + accPlanets += accPlanet; + } acc += accPlanets; - orbInit.componentsMap[E_Component::PLANETARY_PERTURBATION] = accPlanets.norm(); - } - - if (acsConfig.propagationOptions.egm_field) - { - Vector3d rsatE = eci2ecf * rSat; - Vector3d accSPH = accelSPH(rsatE, Cnm, Snm, acsConfig.propagationOptions.egm_degree, egm.earthGravityConstant); - - for (int i = 0; i < 3; i++) - { - Vector3d offset = Vector3d::Zero(); - offset(i) = posOffset; - - Vector3d posPerturbed = rsatE + offset; - Vector3d accPerturbed = accelSPH(posPerturbed, Cnm, Snm, acsConfig.propagationOptions.egm_degree, egm.earthGravityConstant); - - dAdPos.col(i) += eci2ecf.transpose() * (accPerturbed - accSPH) / posOffset; - } + orbInit.componentsMap[E_Component::PLANETARY_PERTURBATION] = accPlanets.norm(); + } + + if (acsConfig.propagationOptions.egm_field) + { + Vector3d rsatE = eci2ecf * rSat; + Vector3d accSPH = accelSPH( + rsatE, + Cnm, + Snm, + acsConfig.propagationOptions.egm_degree, + egm.earthGravityConstant + ); + + for (int i = 0; i < 3; i++) + { + Vector3d offset = Vector3d::Zero(); + offset(i) = posOffset; + + Vector3d posPerturbed = rsatE + offset; + Vector3d accPerturbed = accelSPH( + posPerturbed, + Cnm, + Snm, + acsConfig.propagationOptions.egm_degree, + egm.earthGravityConstant + ); + + dAdPos.col(i) += eci2ecf.transpose() * (accPerturbed - accSPH) / posOffset; + } acc += eci2ecf.transpose() * accSPH; - orbInit.componentsMap[E_Component::EGM] = accSPH.norm(); - } - - - - if ( acsConfig.propagationOptions.egm_field - &&acsConfig.propagationOptions.indirect_J2) - for (const auto body : {E_ThirdBody::SUN, E_ThirdBody::MOON}) - { - Vector3d accJ2 = accelJ2(Cnm(2,0), eci2ecf, planetsPosMap[body], GM_values[body]); - acc += eci2ecf.transpose() * accJ2; - - orbInit.componentsMap[E_Component::INDIRECT_J2] = accJ2.norm(); - } - - if (acsConfig.propagationOptions.general_relativity) - { - Vector3d accRel = IERS2010::relativity( rSat, - vSat, - planetsPosMap[E_ThirdBody::SUN], - planetsVelMap[E_ThirdBody::SUN], - eci2ecf, - deci2ecf); - - for (int i = 0; i < 3; i++) - { - Vector3d offset = Vector3d::Zero(); - Vector3d acc_rel_part = Vector3d::Zero(); - - offset(i) = posOffset; - acc_rel_part = IERS2010::relativity( rSat + offset, - vSat, - planetsPosMap[E_ThirdBody::SUN], - planetsVelMap[E_ThirdBody::SUN], - eci2ecf, - deci2ecf); - - dAdPos.col(i) += (acc_rel_part - accRel) / posOffset; - - offset(i) = velOffset; - acc_rel_part = IERS2010::relativity( rSat, - vSat + offset, - planetsPosMap[E_ThirdBody::SUN], - planetsVelMap[E_ThirdBody::SUN], - eci2ecf, - deci2ecf); - - dAdVel.col(i) += (acc_rel_part - accRel) / velOffset; - } + orbInit.componentsMap[E_Component::EGM] = accSPH.norm(); + } + + if (acsConfig.propagationOptions.egm_field && acsConfig.propagationOptions.indirect_J2) + for (const auto body : {E_ThirdBody::SUN, E_ThirdBody::MOON}) + { + Vector3d accJ2 = accelJ2(Cnm(2, 0), eci2ecf, planetsPosMap[body], GM_values[body]); + acc += eci2ecf.transpose() * accJ2; + + orbInit.componentsMap[E_Component::INDIRECT_J2] = accJ2.norm(); + } + + if (acsConfig.propagationOptions.general_relativity) + { + Vector3d accRel = IERS2010::relativity( + rSat, + vSat, + planetsPosMap[E_ThirdBody::SUN], + planetsVelMap[E_ThirdBody::SUN], + eci2ecf, + deci2ecf + ); + + for (int i = 0; i < 3; i++) + { + Vector3d offset = Vector3d::Zero(); + Vector3d acc_rel_part = Vector3d::Zero(); + + offset(i) = posOffset; + acc_rel_part = IERS2010::relativity( + rSat + offset, + vSat, + planetsPosMap[E_ThirdBody::SUN], + planetsVelMap[E_ThirdBody::SUN], + eci2ecf, + deci2ecf + ); + + dAdPos.col(i) += (acc_rel_part - accRel) / posOffset; + + offset(i) = velOffset; + acc_rel_part = IERS2010::relativity( + rSat, + vSat + offset, + planetsPosMap[E_ThirdBody::SUN], + planetsVelMap[E_ThirdBody::SUN], + eci2ecf, + deci2ecf + ); + + dAdVel.col(i) += (acc_rel_part - accRel) / velOffset; + } acc += accRel; - orbInit.componentsMap[E_Component::GENERAL_RELATIVITY] = accRel.norm(); - }; + orbInit.componentsMap[E_Component::GENERAL_RELATIVITY] = accRel.norm(); + }; - if (orbInit.antenna_thrust) - { - Vector3d accAnt = orbInit.power / (orbInit.mass * CLIGHT) * rSat.normalized(); + if (orbInit.antenna_thrust) + { + Vector3d accAnt = orbInit.power / (orbInit.mass * CLIGHT) * rSat.normalized(); - for (int i = 0; i < 3; i++) - { - Vector3d offset = Vector3d::Zero(); - offset(i) = posOffset; + for (int i = 0; i < 3; i++) + { + Vector3d offset = Vector3d::Zero(); + offset(i) = posOffset; - Vector3d acc_pert = orbInit.power / (orbInit.mass * CLIGHT) * (rSat + offset).normalized(); + Vector3d acc_pert = + orbInit.power / (orbInit.mass * CLIGHT) * (rSat + offset).normalized(); - dAdPos.col(i) += (acc_pert - accAnt) / posOffset; - } + dAdPos.col(i) += (acc_pert - accAnt) / posOffset; + } acc += accAnt; - orbInit.componentsMap[E_Component::ANTENNA_THRUST] = accAnt.norm(); - } - - switch (orbInit.solar_radiation_pressure) - { - case E_SRPModel::CANNONBALL: - { - double P0 = 4.56e-6; - double Cr = orbInit.srp_cr; - double A = orbInit.area; - double m = orbInit.mass; - - double eclipseFrac = sunVisibility(rSat, planetsPosMap[E_ThirdBody::SUN], planetsPosMap[E_ThirdBody::MOON]); - double scalar = P0 * Cr * A / m * SQR(AU) * eclipseFrac / satToSun.squaredNorm(); - double R = satToSun.norm(); - - dAdPos += scalar * (1 / pow(R, 3) * Matrix3d::Identity() - 3 * satToSun * (satToSun.transpose() / pow(R, 5))); - Vector3d accSrp = -1 * scalar * ed; - acc += accSrp; - - orbInit.componentsMap[E_Component::SRP_CANNONBALL] = accSrp.norm(); + orbInit.componentsMap[E_Component::ANTENNA_THRUST] = accAnt.norm(); + } + + switch (orbInit.solar_radiation_pressure) + { + case E_SRPModel::CANNONBALL: + { + double P0 = 4.56e-6; + double Cr = orbInit.estimateCr ? orbInit.srpCr : orbInit.srp_cr; + double A = orbInit.area; + double m = orbInit.mass; + + double eclipseFrac = sunVisibility( + rSat, + planetsPosMap[E_ThirdBody::SUN], + planetsPosMap[E_ThirdBody::MOON] + ); + double scalef = P0 * A / m * SQR(AU) / satToSun.squaredNorm(); + double R = satToSun.norm(); + + dAdPos += scalef * eclipseFrac * + (1 / pow(R, 3) * Matrix3d::Identity() - + 3 * satToSun * (satToSun.transpose() / pow(R, 5))); + Vector3d accSrp = -1 * scalef * eclipseFrac * ed; +#ifdef _ESTIMATE_CRCD + if (orbInit.estimateCr) + { + keyTemplate.num = 0; + keyTemplate.type = KF::CR; + int paramIndex = subState.kfIndexMap[keyTemplate] - 6; + dAdParam.col(paramIndex) = accSrp; + } +#endif + acc += accSrp * Cr; + orbInit.componentsMap[E_Component::SRP_CANNONBALL] = (accSrp * Cr).norm(); break; - } + } - case E_SRPModel::BOXWING: - { - SatPos satPos; - satPos.rSatCom = eci2ecf * rSat; - satPos.satVel = eci2ecf * vSat + deci2ecf * rSat; - satPos.posTime = timeInit; - satPos.Sat = orbInit.Sat; - satPos.satNav_ptr = &nav.satNavMap[orbInit.Sat]; + case E_SRPModel::BOXWING: + { + SatPos satPos; + satPos.rSatCom = eci2ecf * rSat; + satPos.satVel = eci2ecf * vSat + deci2ecf * rSat; + satPos.posTime = timeInit; + satPos.Sat = orbInit.Sat; + satPos.satNav_ptr = &nav.satNavMap[orbInit.Sat]; - updateSatAtts(satPos); + updateSatAtts(satPos); - Vector3d eX = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eXBody; - Vector3d eY = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eYBody; - Vector3d eZ = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eZBody; + Vector3d eX = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eXBody; + Vector3d eY = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eYBody; + Vector3d eZ = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eZBody; - Vector3d accSolarBoxwing = applyBoxwingSrp(orbInit, ed, eX, eY, eZ); + Vector3d accSolarBoxwing = applyBoxwingSrp(orbInit, ed, eX, eY, eZ); - double eclipseFrac = sunVisibility(rSat, planetsPosMap[E_ThirdBody::SUN], planetsPosMap[E_ThirdBody::MOON]); - accSolarBoxwing *= eclipseFrac; - acc += accSolarBoxwing; + double eclipseFrac = sunVisibility( + rSat, + planetsPosMap[E_ThirdBody::SUN], + planetsPosMap[E_ThirdBody::MOON] + ); + accSolarBoxwing *= eclipseFrac; + acc += accSolarBoxwing; orbInit.componentsMap[E_Component::SRP_BOXWING] = accSolarBoxwing.norm(); break; - } - } - - if (orbInit.albedo != +E_SRPModel::NONE) - { - double A = orbInit.area; - double m = orbInit.mass; - double E = 1367; - double cBall = 0.8 * E / CLIGHT; - double alpha = 0.3; - double Ae = M_PI * SQR(RE_WGS84); - Vector3d rSun = planetsPosMap[E_ThirdBody::SUN]; - - double factor = Ae / rSat.squaredNorm(); - double E_IR = (1 - alpha) / (4 * PI); - - double cos_ = rSat.dot(rSun) / (rSat.norm() * rSun.norm()); - double phi = acos(cos_); - double sin_ = sin(phi); - double E_Vis = 2 * alpha / (3 * SQR(PI)) * ((PI - phi) * cos_ + sin_); - - E_Vis *= factor; - E_IR *= factor; - - switch (orbInit.albedo) - { - case E_SRPModel::CANNONBALL: - { - Vector3d accAlbedo = A / m * (E_Vis + E_IR) * cBall * rSat.normalized(); - - orbInit.componentsMap[E_Component::ALBEDO] = accAlbedo.norm(); - - acc += accAlbedo; - - break; - } - case E_SRPModel::BOXWING: - { - SatPos satPos; - satPos.rSatCom = eci2ecf * rSat; - satPos.satVel = eci2ecf * vSat + deci2ecf * rSat; - satPos.posTime = timeInit; - satPos.Sat = orbInit.Sat; - satPos.satNav_ptr = &nav.satNavMap[orbInit.Sat]; - - updateSatAtts(satPos); - - Vector3d eX = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eXBody; - Vector3d eY = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eYBody; - Vector3d eZ = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eZBody; - - Vector3d accAlbedoBoxwing = applyBoxwingAlbedo(orbInit, E_Vis, E_IR, rSat*-1.0, ed, eX, eY, eZ); - - orbInit.componentsMap[E_Component::ALBEDO_BOXWING] = accAlbedoBoxwing.norm(); - - acc += accAlbedoBoxwing; - - break; - } - } - } - - if (0) - for (auto once : {1}) - { - auto it = UbxDecoder::acclDataMaps[orbInit.Sat.id()].lower_bound(time); - if (it == UbxDecoder::acclDataMaps[orbInit.Sat.id()].end()) - { - return; - } - - auto& [foundTime, accl] = *it; - - Vector3d accBody = ((accl - orbInit.acclBias).array() * orbInit.acclScale.array()).matrix(); - - VectorEcef accEcef = body2ecef(orbInit.attStatus, accBody); - - Vector3d accAccl = eci2ecf.transpose() * accEcef; - -// std::cout << "\n" << "vSat: " << vSat.normalized().transpose(); -// std::cout << "\n" << "R: " << er.transpose(); -// std::cout << "\n" << "T: " << et.transpose(); -// std::cout << "\n" << "N: " << en.transpose(); - - Vector3d afX = eci2ecf.transpose() * body2ecef(orbInit.attStatus, Vector3d::UnitX()); - Vector3d afY = eci2ecf.transpose() * body2ecef(orbInit.attStatus, Vector3d::UnitY()); - Vector3d afZ = eci2ecf.transpose() * body2ecef(orbInit.attStatus, Vector3d::UnitZ()); - -// std::cout << "\n" << "x: " << afX.transpose(); -// std::cout << "\n" << "y: " << afY.transpose(); -// std::cout << "\n" << "z: " << afZ.transpose(); - -// dAdParam.col(orbInit.numEmp + 0) = afX; -// dAdParam.col(orbInit.numEmp + 1) = afY; -// dAdParam.col(orbInit.numEmp + 2) = afZ; - - acc += accAccl; - } - - if (orbInit.empirical) - { - double scalef = SQR(AU) / satToSun.squaredNorm(); - double eclipseFrac = sunVisibility(rSat, planetsPosMap[E_ThirdBody::SUN], planetsPosMap[E_ThirdBody::MOON]); - - double scaling = 1e-9; - - vector axis = - { - Vector3d::Zero(), - ed * scaling * scalef, - ey * scaling * scalef, - eb * scaling * scalef, - er * scaling, - et * scaling, - en * scaling, - ep * scaling, - eq * scaling - }; - - Vector3d& rSun = planetsPosMap[E_ThirdBody::SUN]; - Vector3d z = (rSat .cross(vSat)) .normalized(); - Vector3d y = (z .cross(rSun)) .normalized(); - Vector3d x = (y .cross(z)) .normalized(); - - double du = atan2(rSat.dot(y), rSat.dot(x)); - - Vector3d accEmp = Vector3d::Zero(); - - for (int i = 0; i < orbInit.empInput.size(); i++) - { - auto& empdata = orbInit.empInput[i]; - double ecl = 1; - if (empdata.scaleEclipse) - { - ecl = eclipseFrac; - } - dAdParam.col(i) = axis[empdata.axisId] * ecl; - - if (empdata.type == +E_TrigType::COS) dAdParam.col(i) *= cos(empdata.deg * du); - else if (empdata.type == +E_TrigType::SIN) dAdParam.col(i) *= sin(empdata.deg * du); - - accEmp += empdata.value * dAdParam.col(i); - } - - orbInit.componentsMap[E_Component::EMPIRICAL] = accEmp.norm(); - - acc += accEmp; - } + } + default: + break; + } + + if (orbInit.albedo != E_SRPModel::NONE) + { + double A = orbInit.area; + double m = orbInit.mass; + double E = 1367; + double cBall = 0.8 * E / CLIGHT; + double alpha = 0.3; + double Ae = PI * SQR(RE_WGS84); + Vector3d rSun = planetsPosMap[E_ThirdBody::SUN]; + + double factor = Ae / rSat.squaredNorm(); + double E_IR = (1 - alpha) / (4 * PI); + + double cos_ = rSat.dot(rSun) / (rSat.norm() * rSun.norm()); + double phi = acos(cos_); + double sin_ = sin(phi); + double E_Vis = 2 * alpha / (3 * SQR(PI)) * ((PI - phi) * cos_ + sin_); + + E_Vis *= factor; + E_IR *= factor; + + switch (orbInit.albedo) + { + case E_SRPModel::CANNONBALL: + { + Vector3d accAlbedo = A / m * (E_Vis + E_IR) * cBall * rSat.normalized(); + + orbInit.componentsMap[E_Component::ALBEDO_CANNONBALL] = accAlbedo.norm(); + + acc += accAlbedo; + + break; + } + case E_SRPModel::BOXWING: + { + SatPos satPos; + satPos.rSatCom = eci2ecf * rSat; + satPos.satVel = eci2ecf * vSat + deci2ecf * rSat; + satPos.posTime = timeInit; + satPos.Sat = orbInit.Sat; + satPos.satNav_ptr = &nav.satNavMap[orbInit.Sat]; + + updateSatAtts(satPos); + + Vector3d eX = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eXBody; + Vector3d eY = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eYBody; + Vector3d eZ = eci2ecf.transpose() * satPos.satNav_ptr->attStatus.eZBody; + + Vector3d accAlbedoBoxwing = + applyBoxwingAlbedo(orbInit, E_Vis, E_IR, rSat * -1.0, ed, eX, eY, eZ); + + orbInit.componentsMap[E_Component::ALBEDO_BOXWING] = accAlbedoBoxwing.norm(); + + acc += accAlbedoBoxwing; + + break; + } + default: + break; + } + } + + if (orbInit.drag) + { + double Cd = orbInit.estimateCd ? orbInit.dragCd : orbInit.drag_cd; + double A = orbInit.area; + double m = orbInit.mass; + Vector3d OMEGA = {0.0, 0.0, OMGE}; + // Spaceweather + auto& swData = spaceWeatherData.SpaceWeather; + double ep[6]; // current epoch day + time2epoch(this->timeInit, ep, E_TimeSys::GPST); + int searchYear = ep[0]; + int searchMonth = ep[1]; + int searchDay = ep[2]; + auto it = std::find_if( + swData.begin(), + swData.end(), + [searchYear, searchMonth, searchDay](const SpaceWeatherData& data) + { return dateMatches(data, searchYear, searchMonth, searchDay); } + ); + nrlmsise_input input; + nrlmsise_output output; + nrlmsise_flags flags; + ap_array aph; + // simplification of Amirs code. if we have the day the prevouis and next days are just by + // incrementing the pointer. + if (it != swData.end()) + { + auto it_pd = (it != swData.begin()) ? std::prev(it) : swData.end(); // previous day + auto it_pd2 = + (it_pd != swData.end()) ? std::prev(it_pd) : swData.end(); // two days after + auto it_pd3 = + (it_pd2 != swData.end()) ? std::prev(it_pd2) : swData.end(); // three days after + + aph.a[0] = it->avg; + aph.a[1] = it->ap[0]; + aph.a[2] = it_pd->ap[7]; + aph.a[3] = it_pd->ap[6]; + aph.a[4] = it_pd->ap[5]; + double sum = it_pd->ap[4] + it_pd->ap[3] + it_pd->ap[2] + it_pd->ap[1] + it_pd->ap[0]; + sum = sum + it_pd2->ap[7] + it_pd2->ap[6] + it_pd2->ap[5]; + aph.a[5] = sum / 8; + sum = it_pd2->ap[4] + it_pd2->ap[3] + it_pd2->ap[2] + it_pd2->ap[1] + it_pd2->ap[0]; + sum = sum + it_pd3->ap[7] + it_pd3->ap[6] + it_pd3->ap[5]; + aph.a[6] = sum / 8; + VectorPos pos = ecef2pos(rSat); + input.alt = pos.hgt() / 1000; + input.g_lat = pos.latDeg(); + input.g_long = pos.lonDeg(); + double jd = ymdhms2jd(ep); + double gast = iauGst00a(jd, 0.0, jd, 0.0); + Vector3d& sun = planetsPosMap[E_ThirdBody::SUN]; + double lst = + PI + + atan2(sun[0] * rSat[1] - sun[1] * rSat[0], sun[0] * rSat[0] + sun[1] * rSat[1]); + lst = lst * 12.0 / PI; + input.lst = lst; + input.f107A = it->obs_ctr81; + input.f107 = it_pd->obs_f10_7; + input.ap_a = &aph; + input.ap = it->avg; + } + double yds[3]; + time2yds(this->timeInit, yds, E_TimeSys::GPST); + input.year = it->year; + input.doy = yds[1]; + input.sec = yds[2]; + + flags.switches[0] = 1; + for (int i = 1; i < 24; i++) + { + flags.switches[i] = 1; + } + flags.switches[9] = 1; + gtd7d(&input, &flags, &output); + double density = output.d[5]; //* 1e3;; + Vector3d v_rel = vSat - OMEGA.cross(rSat); + double v_abs = v_rel.norm(); + // The direction of the drag acceleration is always (anti-)parallel to the relative velocity + // vector as indicated by this unit vector + Vector3d ev = v_rel.normalized(); + Vector3d partial = -0.5 * A / m * density * pow(v_abs, 2) * ev; +#ifdef _ESTIMATE_CRCD + if (orbInit.estimateCd) + { + keyTemplate.num = 0; + keyTemplate.type = KF::CD; + int paramIndex = subState.kfIndexMap[keyTemplate] - 6; + dAdParam.col(paramIndex) = partial; + } +#endif + Matrix3d dAdVel_temp = + -0.5 * A / m * Cd * density * + ((v_rel * v_rel.transpose() / v_abs) + (v_abs * Matrix3d::Identity())); + dAdVel += dAdVel_temp; + Vector3d dDensdPos = Vector3d::Ones() * 1e-19; + Matrix3d xOmega = Matrix3d::Zero(); + xOmega(0, 0) = 0; + xOmega(0, 1) = -OMEGA.z(); + xOmega(0, 2) = OMEGA.y(); + xOmega(1, 0) = OMEGA.z(); + xOmega(1, 1) = 0; + xOmega(1, 2) = -OMEGA.x(); + xOmega(2, 2) = -OMEGA.y(); + xOmega(2, 2) = OMEGA.x(); + xOmega(2, 2) = 0; + dAdPos += (-0.5 * A / m * Cd * v_abs * v_rel * dDensdPos.transpose()) - + dAdVel_temp * xOmega.transpose(); + Vector3d accDrag = partial * Cd; + acc += accDrag; + orbInit.componentsMap[E_Component::DRAG] = accDrag.norm(); + } + if (0) + for (auto once : {1}) + { + auto it = UbxDecoder::acclDataMaps[orbInit.Sat.id()].lower_bound(time); + if (it == UbxDecoder::acclDataMaps[orbInit.Sat.id()].end()) + { + return; + } + + auto& [foundTime, accl] = *it; + + Vector3d accBody = + ((accl - orbInit.acclBias).array() * orbInit.acclScale.array()).matrix(); + + VectorEcef accEcef = body2ecef(orbInit.attStatus, accBody); + + Vector3d accAccl = eci2ecf.transpose() * accEcef; + + // std::cout << "\n" << "vSat: " << vSat.normalized().transpose(); + // std::cout << "\n" << "R: " << er.transpose(); + // std::cout << "\n" << "T: " << et.transpose(); + // std::cout << "\n" << "N: " << en.transpose(); + + Vector3d afX = eci2ecf.transpose() * body2ecef(orbInit.attStatus, Vector3d::UnitX()); + Vector3d afY = eci2ecf.transpose() * body2ecef(orbInit.attStatus, Vector3d::UnitY()); + Vector3d afZ = eci2ecf.transpose() * body2ecef(orbInit.attStatus, Vector3d::UnitZ()); + + // std::cout << "\n" << "x: " << afX.transpose(); + // std::cout << "\n" << "y: " << afY.transpose(); + // std::cout << "\n" << "z: " << afZ.transpose(); + + // dAdParam.col(orbInit.numEmp + 0) = afX; + // dAdParam.col(orbInit.numEmp + 1) = afY; + // dAdParam.col(orbInit.numEmp + 2) = afZ; + + acc += accAccl; + } + + if (orbInit.empirical) + { + double scalef = SQR(AU) / satToSun.squaredNorm(); + double eclipseFrac = + sunVisibility(rSat, planetsPosMap[E_ThirdBody::SUN], planetsPosMap[E_ThirdBody::MOON]); + + double scaling = 1e-9; + + vector axis = { + Vector3d::Zero(), + ed * scaling * scalef, + ey * scaling * scalef, + eb * scaling * scalef, + er * scaling, + et * scaling, + en * scaling, + ep * scaling, + eq * scaling + }; + + Vector3d& rSun = planetsPosMap[E_ThirdBody::SUN]; + Vector3d z = (rSat.cross(vSat)).normalized(); + Vector3d y = (z.cross(rSun)).normalized(); + Vector3d x = (y.cross(z)).normalized(); + + double du = atan2(rSat.dot(y), rSat.dot(x)); + + Vector3d accEmp = Vector3d::Zero(); + + for (int i = 0; i < orbInit.empInput.size(); i++) + { + auto& empdata = orbInit.empInput[i]; + int paramIndex = subState.kfIndexMap[empdata.key] - 6; + + double ecl = 1; + if (empdata.scaleEclipse) + { + ecl = eclipseFrac; + } + + dAdParam.col(paramIndex) = axis[static_cast(empdata.axisId)] * ecl; + if (empdata.type == E_TrigType::COS) + dAdParam.col(paramIndex) *= cos(empdata.deg * du); + else if (empdata.type == E_TrigType::SIN) + dAdParam.col(paramIndex) *= sin(empdata.deg * du); + + accEmp += empdata.value * dAdParam.col(paramIndex); + } + + orbInit.componentsMap[E_Component::EMPIRICAL] = accEmp.norm(); + + acc += accEmp; + } } void OrbitIntegrator::operator()( - const Orbits& orbInits, - Orbits& orbUpdates, - const double timeOffset) + const Orbits& orbInits, + Orbits& orbUpdates, + const double timeOffset +) { - computeCommon(timeInit + timeOffset); + computeCommon(timeInit + timeOffset); -# ifdef ENABLE_PARALLELISATION - Eigen::setNbThreads(1); -# pragma omp parallel for -# endif - for (int i = 0; i < orbInits.size(); i++) - { - Matrix6d A = Matrix6d::Zero(); - A.block<3,3>(0,3) = Matrix3d::Identity(); +#ifdef ENABLE_PARALLELISATION + Eigen::setNbThreads(1); +#pragma omp parallel for +#endif + for (int i = 0; i < orbInits.size(); i++) + { + Matrix6d A = Matrix6d::Zero(); + A.block<3, 3>(0, 3) = Matrix3d::Identity(); - auto& orbInit = orbInits [i]; - auto& orbUpdate = orbUpdates[i]; + auto& orbInit = orbInits[i]; + auto& orbUpdate = orbUpdates[i]; - if (orbInit.exclude) - { - continue; - } + // check if everything is good here - int numParam = orbInit.numEmp + orbInit.numParam; + auto& subState = *orbInit.subState_ptr; - Vector3d acc = Vector3d::Zero(); - Matrix3d dAdPos = Matrix3d::Zero(); - Matrix3d dAdVel = Matrix3d::Zero(); - MatrixXd dAdParam = MatrixXd::Zero(3, numParam); + if (orbInit.exclude) + { + continue; + } - computeAcceleration(orbInit, acc, dAdPos, dAdVel, dAdParam, timeInit + timeOffset); + int numParam = orbInit.subState_ptr->kfIndexMap.size() - 6; - A.block<3,3>(3,0) = dAdPos; - A.block<3,3>(3,3) = dAdVel; + Vector3d acc = Vector3d::Zero(); + Matrix3d dAdPos = Matrix3d::Zero(); + Matrix3d dAdVel = Matrix3d::Zero(); + MatrixXd dAdParam = MatrixXd::Zero(3, numParam); - orbUpdate.componentsMap = orbInit.componentsMap; - orbUpdate.pos = orbInit.vel; - orbUpdate.vel = acc; - orbUpdate.posVelSTM = A * orbInit.posVelSTM; + computeAcceleration(orbInit, acc, dAdPos, dAdVel, dAdParam, timeInit + timeOffset); - orbUpdate.posVelSTM.bottomRightCorner(3, numParam) += dAdParam; - } - Eigen::setNbThreads(0); -}; + A.block<3, 3>(3, 0) = dAdPos; + A.block<3, 3>(3, 3) = dAdVel; + orbUpdate.componentsMap = orbInit.componentsMap; + orbUpdate.pos = orbInit.vel; + orbUpdate.vel = acc; + orbUpdate.posVelSTM = A * orbInit.posVelSTM; + + orbUpdate.posVelSTM.bottomRightCorner(3, numParam) += dAdParam; + } + Eigen::setNbThreads(0); +}; void integrateOrbits( - OrbitIntegrator& orbitPropagator, - Orbits& orbits, - double integrationPeriod, - double dtRequested) + OrbitIntegrator& orbitPropagator, + Orbits& orbits, + double integrationPeriod, + double dtRequested +) { - if ( orbits.empty() - ||integrationPeriod == 0) - { - return; - } - - double dt = dtRequested; - int steps = round(integrationPeriod / dt); - double remainder = fmod (integrationPeriod, dtRequested); - - if (steps == 0) - { - steps = 1; - } - - if (remainder != 0) - { - double newDt = integrationPeriod / steps; - - BOOST_LOG_TRIVIAL(warning) << "Warning: Time step adjusted from " << dt << " to " << newDt; - - dt = newDt; - } - - for (int i = 0; i < steps; i++) - { - double initTime = i * dt; - - Orbits errors; - orbitPropagator.odeIntegrator.do_step(boost::ref(orbitPropagator), orbits, initTime, dt, errors); - - for (auto error : errors) - { - double errorMag = error.pos.norm(); - if (errorMag > 0.001) - { - BOOST_LOG_TRIVIAL(warning) << " Integrator error " << errorMag << " greater than 1mm for " << error.Sat << " " << error.str; - } - } - } - - for (auto& orbit : orbits) - { - auto& satNav = nav.satNavMap[orbit.Sat]; - auto& satPos0 = satNav.satPos0; - - auto satTrace = getTraceFile(satNav); - - satTrace << "\n" << "Propagated orbit from " << orbitPropagator.timeInit << " for " << integrationPeriod; - - for (auto& [component, value] : orbit.componentsMap) - { - tracepdeex(0, satTrace, "\n"); - tracepdeex(4, satTrace, "%s", orbitPropagator.timeInit.to_string().c_str()); - tracepdeex(0, satTrace, " %-25s %+14.4e", component._to_string(), value); - } - } + if (orbits.empty() || integrationPeriod == 0) + { + return; + } + + double dt = dtRequested; + int steps = round(integrationPeriod / dt); + double remainder = fmod(integrationPeriod, dtRequested); + + if (steps == 0) + { + steps = 1; + } + + if (remainder != 0) + { + double newDt = integrationPeriod / steps; + + BOOST_LOG_TRIVIAL(warning) << "Time step adjusted from " << dt << " to " << newDt; + + dt = newDt; + } + + for (int i = 0; i < steps; i++) + { + double initTime = i * dt; + + Orbits errors; + orbitPropagator.odeIntegrator + .do_step(boost::ref(orbitPropagator), orbits, initTime, dt, errors); + + for (auto error : errors) + { + double errorMag = error.pos.norm(); + if (errorMag > 0.001) + { + BOOST_LOG_TRIVIAL(warning) + << "Integrator error " << errorMag << " greater than 1mm for " << error.Sat + << " " << error.str; + } + } + } + + for (auto& orbit : orbits) + { + auto& satNav = nav.satNavMap[orbit.Sat]; + auto& satPos0 = satNav.satPos0; + + auto satTrace = getTraceFile(satNav); + + satTrace << "\n" + << "Propagated orbit from " << orbitPropagator.timeInit << " for " + << integrationPeriod; + + for (auto& [component, value] : orbit.componentsMap) + { + tracepdeex(0, satTrace, "\n"); + tracepdeex(4, satTrace, "%s", orbitPropagator.timeInit.to_string().c_str()); + tracepdeex(0, satTrace, " %-25s %+14.4e", enum_to_string(component), value); + } + } } /** Get the estimated elements for a single satellite's orbit -*/ -shared_ptr getOrbitFromState( - Trace& trace, - SatSys Sat, - string str, - const KFState& kfState) -{ - map kfKeyMap; - - int index = 0; - - //find all satellite orbit related states related to this sat/str pair - for (auto& [kfKey, unused] : kfState.kfIndexMap) - { - if ( kfKey.Sat != Sat - ||kfKey.str != str) - { - continue; - } - - if ( ( kfKey.type >= KF::BEGIN_ORBIT_STATES - && kfKey.type <= KF::END_ORBIT_STATES) - ||( kfKey.type >= KF::BEGIN_INERTIAL_STATES - && kfKey.type <= KF::END_INERTIAL_STATES)) - { - kfKeyMap[kfKey] = index; - index++; - } - } - - KFState subState; - kfState.getSubState(kfKeyMap, subState); - - return make_shared(subState); -} - -Orbits prepareOrbits( - Trace& trace, - const KFState& kfState) + */ +shared_ptr getOrbitFromState(Trace& trace, SatSys Sat, string str, const KFState& kfState) { - Orbits orbits; - - for (auto& [kfKey, index] : kfState.kfIndexMap) - { - if ( kfKey.type != KF::ORBIT - || kfKey.num != 0) - { - continue; - } - - orbits.emplace_back(OrbitState{.Sat = kfKey.Sat, .str = kfKey.str}); - } - - for (auto& orbit : orbits) - { - auto& Sat = orbit.Sat; - auto& satOpts = acsConfig.getSatOpts(Sat); - - orbit.subState_ptr = getOrbitFromState(trace, Sat, orbit.str, kfState); - - auto& subState = *orbit.subState_ptr; - - vector eclipse; - for (int i = 0; i < 3; i++) { eclipse.push_back(queryVectorElement(satOpts.empirical_dyb_eclipse, i)); } - for (int i = 0; i < 3; i++) { eclipse.push_back(queryVectorElement(satOpts.empirical_rtn_eclipse, i)); } - - for (auto& [subKey, index] : subState.kfIndexMap) - { - if (subKey.type == KF::ORBIT) - { - continue; - } - - E_TrigType trigType; - if (subKey.num == 0) trigType = E_TrigType::COS; - else trigType = E_TrigType::SIN; - - double stateValue = subState.x(index); - - switch (subKey.type) - { - case KF::GYRO_SCALE: { orbit.gyroScale (subKey.num) = stateValue; break;} - case KF::ACCL_SCALE: { orbit.acclScale (subKey.num) = stateValue; break;} - case KF::GYRO_BIAS: { orbit.gyroBias (subKey.num) = stateValue; break;} - case KF::ACCL_BIAS: { orbit.acclBias (subKey.num) = stateValue; break;} - - case KF::EMP_D_0: { orbit.empInput.push_back({eclipse[0], 0, E_EmpAxis::D, trigType, stateValue}); break;} - case KF::EMP_D_1: { orbit.empInput.push_back({eclipse[0], 1, E_EmpAxis::D, trigType, stateValue}); break;} - case KF::EMP_D_2: { orbit.empInput.push_back({eclipse[0], 2, E_EmpAxis::D, trigType, stateValue}); break;} - case KF::EMP_D_3: { orbit.empInput.push_back({eclipse[0], 3, E_EmpAxis::D, trigType, stateValue}); break;} - case KF::EMP_D_4: { orbit.empInput.push_back({eclipse[0], 4, E_EmpAxis::D, trigType, stateValue}); break;} - - case KF::EMP_Y_0: { orbit.empInput.push_back({eclipse[1], 0, E_EmpAxis::Y, trigType, stateValue}); break;} - case KF::EMP_Y_1: { orbit.empInput.push_back({eclipse[1], 1, E_EmpAxis::Y, trigType, stateValue}); break;} - case KF::EMP_Y_2: { orbit.empInput.push_back({eclipse[1], 2, E_EmpAxis::Y, trigType, stateValue}); break;} - case KF::EMP_Y_3: { orbit.empInput.push_back({eclipse[1], 3, E_EmpAxis::Y, trigType, stateValue}); break;} - case KF::EMP_Y_4: { orbit.empInput.push_back({eclipse[1], 4, E_EmpAxis::Y, trigType, stateValue}); break;} - - case KF::EMP_B_0: { orbit.empInput.push_back({eclipse[2], 0, E_EmpAxis::B, trigType, stateValue}); break;} - case KF::EMP_B_1: { orbit.empInput.push_back({eclipse[2], 1, E_EmpAxis::B, trigType, stateValue}); break;} - case KF::EMP_B_2: { orbit.empInput.push_back({eclipse[2], 2, E_EmpAxis::B, trigType, stateValue}); break;} - case KF::EMP_B_3: { orbit.empInput.push_back({eclipse[2], 3, E_EmpAxis::B, trigType, stateValue}); break;} - case KF::EMP_B_4: { orbit.empInput.push_back({eclipse[2], 4, E_EmpAxis::B, trigType, stateValue}); break;} - - case KF::EMP_R_0: { orbit.empInput.push_back({eclipse[3], 0, E_EmpAxis::R, trigType, stateValue}); break;} - case KF::EMP_R_1: { orbit.empInput.push_back({eclipse[3], 1, E_EmpAxis::R, trigType, stateValue}); break;} - case KF::EMP_R_2: { orbit.empInput.push_back({eclipse[3], 2, E_EmpAxis::R, trigType, stateValue}); break;} - case KF::EMP_R_3: { orbit.empInput.push_back({eclipse[3], 3, E_EmpAxis::R, trigType, stateValue}); break;} - case KF::EMP_R_4: { orbit.empInput.push_back({eclipse[3], 4, E_EmpAxis::R, trigType, stateValue}); break;} - - case KF::EMP_T_0: { orbit.empInput.push_back({eclipse[4], 0, E_EmpAxis::T, trigType, stateValue}); break;} - case KF::EMP_T_1: { orbit.empInput.push_back({eclipse[4], 1, E_EmpAxis::T, trigType, stateValue}); break;} - case KF::EMP_T_2: { orbit.empInput.push_back({eclipse[4], 2, E_EmpAxis::T, trigType, stateValue}); break;} - case KF::EMP_T_3: { orbit.empInput.push_back({eclipse[4], 3, E_EmpAxis::T, trigType, stateValue}); break;} - case KF::EMP_T_4: { orbit.empInput.push_back({eclipse[4], 4, E_EmpAxis::T, trigType, stateValue}); break;} - - case KF::EMP_N_0: { orbit.empInput.push_back({eclipse[5], 0, E_EmpAxis::N, trigType, stateValue}); break;} - case KF::EMP_N_1: { orbit.empInput.push_back({eclipse[5], 1, E_EmpAxis::N, trigType, stateValue}); break;} - case KF::EMP_N_2: { orbit.empInput.push_back({eclipse[5], 2, E_EmpAxis::N, trigType, stateValue}); break;} - case KF::EMP_N_3: { orbit.empInput.push_back({eclipse[5], 3, E_EmpAxis::N, trigType, stateValue}); break;} - case KF::EMP_N_4: { orbit.empInput.push_back({eclipse[5], 4, E_EmpAxis::N, trigType, stateValue}); break;} - - case KF::EMP_P_0: { orbit.empInput.push_back({false, 0, E_EmpAxis::P, trigType, stateValue}); break;} - case KF::EMP_P_1: { orbit.empInput.push_back({false, 1, E_EmpAxis::P, trigType, stateValue}); break;} - case KF::EMP_P_2: { orbit.empInput.push_back({false, 2, E_EmpAxis::P, trigType, stateValue}); break;} - case KF::EMP_P_3: { orbit.empInput.push_back({false, 3, E_EmpAxis::P, trigType, stateValue}); break;} - case KF::EMP_P_4: { orbit.empInput.push_back({false, 4, E_EmpAxis::P, trigType, stateValue}); break;} - - case KF::EMP_Q_0: { orbit.empInput.push_back({false, 0, E_EmpAxis::Q, trigType, stateValue}); break;} - case KF::EMP_Q_1: { orbit.empInput.push_back({false, 1, E_EmpAxis::Q, trigType, stateValue}); break;} - case KF::EMP_Q_2: { orbit.empInput.push_back({false, 2, E_EmpAxis::Q, trigType, stateValue}); break;} - case KF::EMP_Q_3: { orbit.empInput.push_back({false, 3, E_EmpAxis::Q, trigType, stateValue}); break;} - case KF::EMP_Q_4: { orbit.empInput.push_back({false, 4, E_EmpAxis::Q, trigType, stateValue}); break;} - - default: { break;} - } - } - - orbit.pos = subState.x.head (3); - orbit.vel = subState.x.segment (3, 3); - - if (orbit.pos.isZero()) - { - orbit.exclude = true; - continue; - } - - orbit.posVelSTM = MatrixXd::Identity(6, subState.kfIndexMap.size()); - - orbit.attStatus = nav.satNavMap[Sat].attStatus; - - orbit.numEmp = orbit.empInput.size(); - - orbit.OrbitOptions::operator=(satOpts); - - string svn = Sat.svn(); - - auto findPower_it = theSinex.mapsatpowers.find(svn); - if (findPower_it != theSinex.mapsatpowers.end()) - { - auto& [svn, satPowerMap] = *findPower_it; - auto& [time, firstPower] = *satPowerMap.begin(); - - orbit.power = firstPower.power; - } - - auto findMass_it = theSinex.mapsatmasses.find(svn); - if (findMass_it != theSinex.mapsatmasses.end()) - { - auto& [svn, satMassMap] = *findMass_it; - auto& [time, firstMass] = *satMassMap.begin(); - - orbit.mass = firstMass.mass; - } - } - - return orbits; + map kfKeyMap; + + int index = 0; + + // find all satellite orbit related states related to this sat/str pair + for (auto& [kfKey, unused] : kfState.kfIndexMap) + { + if (kfKey.Sat != Sat || kfKey.str != str) + { + continue; + } + if ((kfKey.type >= KF::BEGIN_ORBIT_STATES && kfKey.type <= KF::END_ORBIT_STATES) || + (kfKey.type >= KF::BEGIN_INERTIAL_STATES && kfKey.type <= KF::END_INERTIAL_STATES)) + { + kfKeyMap[kfKey] = index; + index++; + } + } + + KFState subState; + kfState.getSubState(kfKeyMap, subState); + + return make_shared(subState); } -/** Apply the prediction using the filter's state transition -*/ -void applyOrbits( - Trace& trace, - Orbits& orbits, - const KFState& kfState, - GTime time, - double tgap) +Orbits prepareOrbits(Trace& trace, const KFState& kfState) { - for (auto& orbit : orbits) - { - if (orbit.exclude) - { - continue; - } - - auto& subState = *orbit.subState_ptr; - - Vector6d inertialState; - inertialState << orbit.pos, orbit.vel; - - //Convert the absolute transition matrix to an identity matrix (already populated elsewhere) and stm-per-time matrix - MatrixXd transition = orbit.posVelSTM - MatrixXd::Identity(orbit.posVelSTM.rows(), orbit.posVelSTM.cols()); - transition /= tgap; - - KFKey keyI; - keyI.type = KF::ORBIT; - keyI.Sat = orbit.Sat; - keyI.str = orbit.str; - - //use the calculated transition matrix for the main filter, and a also a test filter (substate) - for (int i = 0; i < 6; i++) - for (auto& [keyJ, indexJ] : subState.kfIndexMap) - { - keyI.num = i; - - double transferIJ = transition(i, indexJ); - - kfState .setKFTransRate(keyI, keyJ, transferIJ); - subState.setKFTransRate(keyI, keyJ, transferIJ); - } - - //run a test run of the transition on the substate to see how much we miss by - subState.stateTransition(trace, time); - - //calculate the state error between the linearly transitioned filter state, and the orbit propagated states. - Vector6d deltaState = inertialState - - subState.x.head(6); - - //We should ensure that the state transition is smooth, without bulk adjustments or 'setting' the state - //add per-time adjusting state transitions to implement an addition of the shortfall delta calculated above - Vector6d deltaStatePerSec = deltaState / tgap; - - KFKey kfKey; - kfKey.type = KF::ORBIT; - kfKey.Sat = orbit.Sat; - kfKey.str = orbit.str; - - for (int i = 0; i < 6; i++) - { - kfKey.num = i; - - kfState.setKFTransRate(kfKey, KFState::oneKey, deltaStatePerSec(i)); - } - } -} + Orbits orbits; + + for (auto& [kfKey, index] : kfState.kfIndexMap) + { + if (kfKey.type != KF::ORBIT || kfKey.num != 0) + { + continue; + } + + orbits.emplace_back(OrbitState{.Sat = kfKey.Sat, .str = kfKey.str}); + } + + for (auto& orbit : orbits) + { + auto& Sat = orbit.Sat; + auto& satOpts = acsConfig.getSatOpts(Sat); + + orbit.subState_ptr = getOrbitFromState(trace, Sat, orbit.str, kfState); + + auto& subState = *orbit.subState_ptr; + + vector eclipse; + for (int i = 0; i < 3; i++) + { + eclipse.push_back(queryVectorElement(satOpts.empirical_dyb_eclipse, i)); + } + for (int i = 0; i < 3; i++) + { + eclipse.push_back(queryVectorElement(satOpts.empirical_rtn_eclipse, i)); + } + + for (auto& [subKey, index] : subState.kfIndexMap) + { + if (subKey.type == KF::ORBIT) + { + continue; + } + E_TrigType trigType; + if (subKey.num == 0) + trigType = E_TrigType::COS; + else + trigType = E_TrigType::SIN; + + double stateValue = subState.x(index); + + switch (subKey.type) + { + case KF::GYRO_SCALE: + { + orbit.gyroScale(subKey.num) = stateValue; + break; + } + case KF::ACCL_SCALE: + { + orbit.acclScale(subKey.num) = stateValue; + break; + } + case KF::GYRO_BIAS: + { + orbit.gyroBias(subKey.num) = stateValue; + break; + } + case KF::ACCL_BIAS: + { + orbit.acclBias(subKey.num) = stateValue; + break; + } + + case KF::EMP_D_0: + { + orbit.empInput.push_back( + {eclipse[0], 0, E_EmpAxis::D, E_TrigType::NONE, stateValue, subKey} + ); + break; + } + case KF::EMP_D_1: + { + orbit.empInput.push_back( + {eclipse[0], 1, E_EmpAxis::D, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_D_2: + { + orbit.empInput.push_back( + {eclipse[0], 2, E_EmpAxis::D, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_D_3: + { + orbit.empInput.push_back( + {eclipse[0], 3, E_EmpAxis::D, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_D_4: + { + orbit.empInput.push_back( + {eclipse[0], 4, E_EmpAxis::D, trigType, stateValue, subKey} + ); + break; + } + + case KF::EMP_Y_0: + { + orbit.empInput.push_back( + {eclipse[1], 0, E_EmpAxis::Y, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_Y_1: + { + orbit.empInput.push_back( + {eclipse[1], 1, E_EmpAxis::Y, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_Y_2: + { + orbit.empInput.push_back( + {eclipse[1], 2, E_EmpAxis::Y, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_Y_3: + { + orbit.empInput.push_back( + {eclipse[1], 3, E_EmpAxis::Y, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_Y_4: + { + orbit.empInput.push_back( + {eclipse[1], 4, E_EmpAxis::Y, trigType, stateValue, subKey} + ); + break; + } + + case KF::EMP_B_0: + { + orbit.empInput.push_back( + {eclipse[2], 0, E_EmpAxis::B, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_B_1: + { + orbit.empInput.push_back( + {eclipse[2], 1, E_EmpAxis::B, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_B_2: + { + orbit.empInput.push_back( + {eclipse[2], 2, E_EmpAxis::B, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_B_3: + { + orbit.empInput.push_back( + {eclipse[2], 3, E_EmpAxis::B, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_B_4: + { + orbit.empInput.push_back( + {eclipse[2], 4, E_EmpAxis::B, trigType, stateValue, subKey} + ); + break; + } + + case KF::EMP_R_0: + { + orbit.empInput.push_back( + {eclipse[3], 0, E_EmpAxis::R, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_R_1: + { + orbit.empInput.push_back( + {eclipse[3], 1, E_EmpAxis::R, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_R_2: + { + orbit.empInput.push_back( + {eclipse[3], 2, E_EmpAxis::R, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_R_3: + { + orbit.empInput.push_back( + {eclipse[3], 3, E_EmpAxis::R, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_R_4: + { + orbit.empInput.push_back( + {eclipse[3], 4, E_EmpAxis::R, trigType, stateValue, subKey} + ); + break; + } + + case KF::EMP_T_0: + { + orbit.empInput.push_back( + {eclipse[4], 0, E_EmpAxis::T, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_T_1: + { + orbit.empInput.push_back( + {eclipse[4], 1, E_EmpAxis::T, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_T_2: + { + orbit.empInput.push_back( + {eclipse[4], 2, E_EmpAxis::T, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_T_3: + { + orbit.empInput.push_back( + {eclipse[4], 3, E_EmpAxis::T, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_T_4: + { + orbit.empInput.push_back( + {eclipse[4], 4, E_EmpAxis::T, trigType, stateValue, subKey} + ); + break; + } + + case KF::EMP_N_0: + { + orbit.empInput.push_back( + {eclipse[5], 0, E_EmpAxis::N, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_N_1: + { + orbit.empInput.push_back( + {eclipse[5], 1, E_EmpAxis::N, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_N_2: + { + orbit.empInput.push_back( + {eclipse[5], 2, E_EmpAxis::N, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_N_3: + { + orbit.empInput.push_back( + {eclipse[5], 3, E_EmpAxis::N, trigType, stateValue, subKey} + ); + break; + } + case KF::EMP_N_4: + { + orbit.empInput.push_back( + {eclipse[5], 4, E_EmpAxis::N, trigType, stateValue, subKey} + ); + break; + } + + case KF::EMP_P_0: + { + 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} + ); + break; + } + case KF::EMP_P_2: + { + 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} + ); + break; + } + case KF::EMP_P_4: + { + 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} + ); + break; + } + case KF::EMP_Q_1: + { + 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} + ); + break; + } + case KF::EMP_Q_3: + { + 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} + ); + break; + } +#ifdef _ESTIMATE_CRCD + case KF::CR: + { + orbit.srpCr = stateValue; + orbit.estimateCr = true; + break; + } + case KF::CD: + { + orbit.dragCd = stateValue; + orbit.estimateCd = true; + break; + } +#endif + default: + { + break; + } + } + } -/** Use models to predict orbital motion and prepare state transition equations to implement those predictions in the filter -*/ -void predictOrbits( - Trace& trace, - const KFState& kfState, - GTime time) -{ - double tgap = (time - kfState.time).to_double(); + orbit.pos = subState.x.head(3); + orbit.vel = subState.x.segment(3, 3); - if (tgap == 0) - { - return; - } + if (orbit.pos.isZero()) + { + orbit.exclude = true; + continue; + } - Orbits orbits = prepareOrbits(trace, kfState); + orbit.posVelSTM = MatrixXd::Identity(6, subState.kfIndexMap.size()); - if (orbits.empty()) - { - return; - } + orbit.attStatus = nav.satNavMap[Sat].attStatus; - InteractiveTerminal::setMode(E_InteractiveMode::PropagatingOrbits); - BOOST_LOG_TRIVIAL(info) << " ------- PROPAGATING ORBITS --------" << "\n"; + orbit.numEmp = orbit.empInput.size(); - OrbitIntegrator integrator; - integrator.timeInit = kfState.time; + orbit.OrbitOptions::operator=(satOpts); - integrateOrbits(integrator, orbits, tgap, acsConfig.propagationOptions.integrator_time_step); + string svn = Sat.svn(); - applyOrbits(trace, orbits, kfState, time, tgap); + auto findPower_it = theSinex.mapsatpowers.find(svn); + if (findPower_it != theSinex.mapsatpowers.end()) + { + auto& [svn, satPowerMap] = *findPower_it; + auto& [time, firstPower] = *satPowerMap.begin(); - for (auto& orbit : orbits) - { - auto& satNav = nav.satNavMap[orbit.Sat]; - auto& satPos0 = satNav.satPos0; + orbit.power = firstPower.power; + } - satPos0.posTime = time; - satPos0.rSatEci0 = orbit.pos; - satPos0.vSatEci0 = orbit.vel; - } -}; + auto findMass_it = theSinex.mapsatmasses.find(svn); + if (findMass_it != theSinex.mapsatmasses.end()) + { + auto& [svn, satMassMap] = *findMass_it; + auto& [time, firstMass] = *satMassMap.begin(); -void addNilDesignStates( - const KalmanModel& model, - const KFState& kfState, - const KF& kfType, - int num, - const string& id) -{ - for (int i = 0; i < num; i++) - { - InitialState init = initialStateFromConfig(model, i); - - if (init.estimate) - { - KFKey key; - key.type = kfType; - key.num = i; - key.comment = init.comment; - - SatSys Sat = id.c_str(); - if (Sat.prn) key.Sat = id.c_str(); - else key.str = id; - - kfState.addKFState(key, init); - } - } + orbit.mass = firstMass.mass; + } + } + return orbits; } -void addEmpStates( - const EmpKalmans& satOpts, - const KFState& kfState, - const string& id) +/** Apply the prediction using the filter's state transition + */ +void applyOrbits(Trace& trace, Orbits& orbits, KFState& kfState, GTime time, double tgap) { - addNilDesignStates(satOpts.emp_d_0, kfState, KF::EMP_D_0, 1, id); - addNilDesignStates(satOpts.emp_d_1, kfState, KF::EMP_D_1, 2, id); - addNilDesignStates(satOpts.emp_d_2, kfState, KF::EMP_D_2, 2, id); - addNilDesignStates(satOpts.emp_d_3, kfState, KF::EMP_D_3, 2, id); - addNilDesignStates(satOpts.emp_d_4, kfState, KF::EMP_D_4, 2, id); - - addNilDesignStates(satOpts.emp_y_0, kfState, KF::EMP_Y_0, 1, id); - addNilDesignStates(satOpts.emp_y_1, kfState, KF::EMP_Y_1, 2, id); - addNilDesignStates(satOpts.emp_y_2, kfState, KF::EMP_Y_2, 2, id); - addNilDesignStates(satOpts.emp_y_3, kfState, KF::EMP_Y_3, 2, id); - addNilDesignStates(satOpts.emp_y_4, kfState, KF::EMP_Y_4, 2, id); - - addNilDesignStates(satOpts.emp_b_0, kfState, KF::EMP_B_0, 1, id); - addNilDesignStates(satOpts.emp_b_1, kfState, KF::EMP_B_1, 2, id); - addNilDesignStates(satOpts.emp_b_2, kfState, KF::EMP_B_2, 2, id); - addNilDesignStates(satOpts.emp_b_3, kfState, KF::EMP_B_3, 2, id); - addNilDesignStates(satOpts.emp_b_4, kfState, KF::EMP_B_4, 2, id); - - addNilDesignStates(satOpts.emp_r_0, kfState, KF::EMP_R_0, 1, id); - addNilDesignStates(satOpts.emp_r_1, kfState, KF::EMP_R_1, 2, id); - addNilDesignStates(satOpts.emp_r_2, kfState, KF::EMP_R_2, 2, id); - addNilDesignStates(satOpts.emp_r_3, kfState, KF::EMP_R_3, 2, id); - addNilDesignStates(satOpts.emp_r_4, kfState, KF::EMP_R_4, 2, id); - - addNilDesignStates(satOpts.emp_t_0, kfState, KF::EMP_T_0, 1, id); - addNilDesignStates(satOpts.emp_t_1, kfState, KF::EMP_T_1, 2, id); - addNilDesignStates(satOpts.emp_t_2, kfState, KF::EMP_T_2, 2, id); - addNilDesignStates(satOpts.emp_t_3, kfState, KF::EMP_T_3, 2, id); - addNilDesignStates(satOpts.emp_t_4, kfState, KF::EMP_T_4, 2, id); - - addNilDesignStates(satOpts.emp_n_0, kfState, KF::EMP_N_0, 1, id); - addNilDesignStates(satOpts.emp_n_1, kfState, KF::EMP_N_1, 2, id); - addNilDesignStates(satOpts.emp_n_2, kfState, KF::EMP_N_2, 2, id); - addNilDesignStates(satOpts.emp_n_3, kfState, KF::EMP_N_3, 2, id); - addNilDesignStates(satOpts.emp_n_4, kfState, KF::EMP_N_4, 2, id); - - addNilDesignStates(satOpts.emp_p_0, kfState, KF::EMP_P_0, 1, id); - addNilDesignStates(satOpts.emp_p_1, kfState, KF::EMP_P_1, 2, id); - addNilDesignStates(satOpts.emp_p_2, kfState, KF::EMP_P_2, 2, id); - addNilDesignStates(satOpts.emp_p_3, kfState, KF::EMP_P_3, 2, id); - addNilDesignStates(satOpts.emp_p_4, kfState, KF::EMP_P_4, 2, id); - - addNilDesignStates(satOpts.emp_q_0, kfState, KF::EMP_Q_0, 1, id); - addNilDesignStates(satOpts.emp_q_1, kfState, KF::EMP_Q_1, 2, id); - addNilDesignStates(satOpts.emp_q_2, kfState, KF::EMP_Q_2, 2, id); - addNilDesignStates(satOpts.emp_q_3, kfState, KF::EMP_Q_3, 2, id); - addNilDesignStates(satOpts.emp_q_4, kfState, KF::EMP_Q_4, 2, id); + for (auto& orbit : orbits) + { + if (orbit.exclude) + { + continue; + } + + auto& subState = *orbit.subState_ptr; + + Vector6d inertialState; + inertialState << orbit.pos, orbit.vel; + + // Convert the absolute transition matrix to an identity matrix (already populated + // elsewhere) and stm-per-time matrix + MatrixXd transition = + orbit.posVelSTM - MatrixXd::Identity(orbit.posVelSTM.rows(), orbit.posVelSTM.cols()); + transition /= tgap; + + KFKey keyI; + keyI.type = KF::ORBIT; + keyI.Sat = orbit.Sat; + keyI.str = orbit.str; + + // use the calculated transition matrix for the main filter, and a also a test filter + // (substate) + for (int i = 0; i < 6; i++) + for (auto& [keyJ, indexJ] : subState.kfIndexMap) + { + keyI.num = i; + + double transferIJ = transition(i, indexJ); + + kfState.setKFTransRate(keyI, keyJ, transferIJ); + subState.setKFTransRate(keyI, keyJ, transferIJ); + } + + // run a test run of the transition on the substate to see how much we miss by + subState.stateTransition(trace, time); + + // calculate the state error between the linearly transitioned filter state, and the orbit + // propagated states. + Vector6d deltaState = inertialState - subState.x.head(6); + + // We should ensure that the state transition is smooth, without bulk adjustments or + // 'setting' the state add per-time adjusting state transitions to implement an addition of + // the shortfall delta calculated above + Vector6d deltaStatePerSec = deltaState / tgap; + + KFKey kfKey; + kfKey.type = KF::ORBIT; + kfKey.Sat = orbit.Sat; + kfKey.str = orbit.str; + + for (int i = 0; i < 6; i++) + { + kfKey.num = i; + + kfState.setKFTransRate(kfKey, KFState::oneKey, deltaStatePerSec(i)); + } + } } -void outputOrbitConfig( - KFState& kfState, - string suffix) +/** Use models to predict orbital motion and prepare state transition equations to implement those + * predictions in the filter + */ +void predictOrbits(Trace& trace, KFState& kfState, GTime time) { - document satellites; - - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( key.type != KF::ORBIT - ||key.num != 0) - { - continue; - } + double tgap = (time - kfState.time).to_double(); - document satellite; + if (tgap == 0) + { + return; + } - for (KF type : - { - KF::ORBIT, + Orbits orbits = prepareOrbits(trace, kfState); - KF::EMP_D_0, KF::EMP_D_1, KF::EMP_D_2, KF::EMP_D_3, KF::EMP_D_4, - KF::EMP_Y_0, KF::EMP_Y_1, KF::EMP_Y_2, KF::EMP_Y_3, KF::EMP_Y_4, - KF::EMP_B_0, KF::EMP_B_1, KF::EMP_B_2, KF::EMP_B_3, KF::EMP_B_4, - KF::EMP_R_0, KF::EMP_R_1, KF::EMP_R_2, KF::EMP_R_3, KF::EMP_R_4, - KF::EMP_T_0, KF::EMP_T_1, KF::EMP_T_2, KF::EMP_T_3, KF::EMP_T_4, - KF::EMP_N_0, KF::EMP_N_1, KF::EMP_N_2, KF::EMP_N_3, KF::EMP_N_4, - KF::EMP_P_0, KF::EMP_P_1, KF::EMP_P_2, KF::EMP_P_3, KF::EMP_P_4, - KF::EMP_Q_0, KF::EMP_Q_1, KF::EMP_Q_2, KF::EMP_Q_3, KF::EMP_Q_4, - }) - { - int n = 2; - if (type == +KF::ORBIT) - n = 6; + if (orbits.empty()) + { + return; + } - vector aprioriVec (n); - vector sigmaVec (n); - deque estimatedVec(n); + BOOST_LOG_TRIVIAL(info) << " ------- PROPAGATING ORBITS --------" << "\n"; - bool anyFound = false; - for (int i = 0; i < n; i++) - { - KFKey kfKey = key; - kfKey.type = type; - kfKey.num = i; + OrbitIntegrator integrator; + integrator.timeInit = kfState.time; - double val = 0; - double var = 0; - bool found = kfState.getKFValue(kfKey, val, &var); + integrateOrbits(integrator, orbits, tgap, acsConfig.propagationOptions.integrator_time_step); - aprioriVec [i] = val; - sigmaVec [i] = sqrt(var); - estimatedVec[i] = found; - anyFound |= found; - } + applyOrbits(trace, orbits, kfState, time, tgap); - if (anyFound == false) - { - continue; - } + for (auto& orbit : orbits) + { + auto& satNav = nav.satNavMap[orbit.Sat]; + auto& satPos0 = satNav.satPos0; - document state; - - {auto arr = state << "apriori_val" << open_array; for (auto& val : aprioriVec) arr << val; arr << close_array;} - {auto arr = state << "sigma" << open_array; for (auto& val : sigmaVec) arr << val; arr << close_array;} - {auto arr = state << "estimated" << open_array; for (auto& val : estimatedVec) arr << val; arr << close_array;} - - string typeStr = type._to_string(); - to_lower(typeStr); - - satellite << typeStr << state; - } - - satellites << key.Sat.id() << satellite; - } - - document epoch_control; epoch_control << "start_epoch" << kfState.time.to_string(); - document processing_options; processing_options << "epoch_control" << epoch_control; - - document estimation_parameters; estimation_parameters << "satellites" << satellites; - - document json; json << "processing_options" << processing_options; - json << "estimation_parameters" << estimation_parameters; - - string filename = acsConfig.orbit_ics_filename + suffix; - - PTime logtime = kfState.time; - - boost::posix_time::ptime logptime = boost::posix_time::from_time_t((time_t)logtime.bigTime); - - if ((GTime)logtime == GTime::noTime()) - { - logptime = boost::posix_time::not_a_date_time; - } - - replaceTimes(filename, logptime); - - std::ofstream output(filename); - - if (!output) - { - return; - } + satPos0.posTime = time; + satPos0.rSatEci0 = orbit.pos; + satPos0.vSatEci0 = orbit.vel; + } +}; - output << bsoncxx::to_json(json) << "\n"; +void addNilDesignStates( + const KalmanModel& model, + KFState& kfState, + const KF& kfType, + int num, + const string& id +) +{ + for (int i = 0; i < num; i++) + { + InitialState init = initialStateFromConfig(model, i); + + if (init.estimate) + { + KFKey key; + key.type = kfType; + key.num = i; + key.comment = init.comment; + + SatSys Sat = id.c_str(); + if (Sat.prn) + key.Sat = id.c_str(); + else + key.str = id; + + kfState.addKFState(key, init); + } + } } +void addEmpStates(const EmpKalmans& satOpts, KFState& kfState, const string& id) +{ + addNilDesignStates(satOpts.emp_d_0, kfState, KF::EMP_D_0, 1, id); + addNilDesignStates(satOpts.emp_d_1, kfState, KF::EMP_D_1, 2, id); + addNilDesignStates(satOpts.emp_d_2, kfState, KF::EMP_D_2, 2, id); + addNilDesignStates(satOpts.emp_d_3, kfState, KF::EMP_D_3, 2, id); + addNilDesignStates(satOpts.emp_d_4, kfState, KF::EMP_D_4, 2, id); + + addNilDesignStates(satOpts.emp_y_0, kfState, KF::EMP_Y_0, 1, id); + addNilDesignStates(satOpts.emp_y_1, kfState, KF::EMP_Y_1, 2, id); + addNilDesignStates(satOpts.emp_y_2, kfState, KF::EMP_Y_2, 2, id); + addNilDesignStates(satOpts.emp_y_3, kfState, KF::EMP_Y_3, 2, id); + addNilDesignStates(satOpts.emp_y_4, kfState, KF::EMP_Y_4, 2, id); + + addNilDesignStates(satOpts.emp_b_0, kfState, KF::EMP_B_0, 1, id); + addNilDesignStates(satOpts.emp_b_1, kfState, KF::EMP_B_1, 2, id); + addNilDesignStates(satOpts.emp_b_2, kfState, KF::EMP_B_2, 2, id); + addNilDesignStates(satOpts.emp_b_3, kfState, KF::EMP_B_3, 2, id); + addNilDesignStates(satOpts.emp_b_4, kfState, KF::EMP_B_4, 2, id); + + addNilDesignStates(satOpts.emp_r_0, kfState, KF::EMP_R_0, 1, id); + addNilDesignStates(satOpts.emp_r_1, kfState, KF::EMP_R_1, 2, id); + addNilDesignStates(satOpts.emp_r_2, kfState, KF::EMP_R_2, 2, id); + addNilDesignStates(satOpts.emp_r_3, kfState, KF::EMP_R_3, 2, id); + addNilDesignStates(satOpts.emp_r_4, kfState, KF::EMP_R_4, 2, id); + + addNilDesignStates(satOpts.emp_t_0, kfState, KF::EMP_T_0, 1, id); + addNilDesignStates(satOpts.emp_t_1, kfState, KF::EMP_T_1, 2, id); + addNilDesignStates(satOpts.emp_t_2, kfState, KF::EMP_T_2, 2, id); + addNilDesignStates(satOpts.emp_t_3, kfState, KF::EMP_T_3, 2, id); + addNilDesignStates(satOpts.emp_t_4, kfState, KF::EMP_T_4, 2, id); + + addNilDesignStates(satOpts.emp_n_0, kfState, KF::EMP_N_0, 1, id); + addNilDesignStates(satOpts.emp_n_1, kfState, KF::EMP_N_1, 2, id); + addNilDesignStates(satOpts.emp_n_2, kfState, KF::EMP_N_2, 2, id); + addNilDesignStates(satOpts.emp_n_3, kfState, KF::EMP_N_3, 2, id); + addNilDesignStates(satOpts.emp_n_4, kfState, KF::EMP_N_4, 2, id); + + addNilDesignStates(satOpts.emp_p_0, kfState, KF::EMP_P_0, 1, id); + addNilDesignStates(satOpts.emp_p_1, kfState, KF::EMP_P_1, 2, id); + addNilDesignStates(satOpts.emp_p_2, kfState, KF::EMP_P_2, 2, id); + addNilDesignStates(satOpts.emp_p_3, kfState, KF::EMP_P_3, 2, id); + addNilDesignStates(satOpts.emp_p_4, kfState, KF::EMP_P_4, 2, id); + + addNilDesignStates(satOpts.emp_q_0, kfState, KF::EMP_Q_0, 1, id); + addNilDesignStates(satOpts.emp_q_1, kfState, KF::EMP_Q_1, 2, id); + addNilDesignStates(satOpts.emp_q_2, kfState, KF::EMP_Q_2, 2, id); + addNilDesignStates(satOpts.emp_q_3, kfState, KF::EMP_Q_3, 2, id); + addNilDesignStates(satOpts.emp_q_4, kfState, KF::EMP_Q_4, 2, id); +} +void outputOrbitConfig(KFState& kfState, bool isSmoothed) +{ + boost::json::object satellites; + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::ORBIT || key.num != 0) + { + continue; + } + + boost::json::object satellite; + + for (KF type : { + KF::ORBIT, + + KF::EMP_D_0, KF::EMP_D_1, KF::EMP_D_2, KF::EMP_D_3, KF::EMP_D_4, KF::EMP_Y_0, + KF::EMP_Y_1, KF::EMP_Y_2, KF::EMP_Y_3, KF::EMP_Y_4, KF::EMP_B_0, KF::EMP_B_1, + KF::EMP_B_2, KF::EMP_B_3, KF::EMP_B_4, KF::EMP_R_0, KF::EMP_R_1, KF::EMP_R_2, + KF::EMP_R_3, KF::EMP_R_4, KF::EMP_T_0, KF::EMP_T_1, KF::EMP_T_2, KF::EMP_T_3, + KF::EMP_T_4, KF::EMP_N_0, KF::EMP_N_1, KF::EMP_N_2, KF::EMP_N_3, KF::EMP_N_4, + KF::EMP_P_0, KF::EMP_P_1, KF::EMP_P_2, KF::EMP_P_3, KF::EMP_P_4, KF::EMP_Q_0, + KF::EMP_Q_1, KF::EMP_Q_2, KF::EMP_Q_3, KF::EMP_Q_4, + }) + { + int n = 2; + if (type == KF::ORBIT) + n = 6; + + vector aprioriVec(n); + vector sigmaVec(n); + deque estimatedVec(n); + + bool anyFound = false; + for (int i = 0; i < n; i++) + { + KFKey kfKey = key; + kfKey.type = type; + kfKey.num = i; + + double val = 0; + double var = 0; + E_Source foundSrc = kfState.getKFValue(kfKey, val, &var); + bool found = foundSrc != E_Source::NONE; + + aprioriVec[i] = val; + sigmaVec[i] = sqrt(var); + estimatedVec[i] = found; + anyFound |= found; + } + + if (anyFound == false) + { + continue; + } + + boost::json::object state; + boost::json::array apriori_arr; + for (auto& val : aprioriVec) + apriori_arr.push_back(val); + state["apriori_val"] = apriori_arr; + + boost::json::array sigma_arr; + for (auto& val : sigmaVec) + sigma_arr.push_back(val); + state["sigma"] = sigma_arr; + + boost::json::array estimated_arr; + for (auto& val : estimatedVec) + estimated_arr.push_back(val); + state["estimated"] = estimated_arr; + + string typeStr = enum_to_string(type); + to_lower(typeStr); + + satellite[typeStr] = state; + } + + satellites[key.Sat.id()] = satellite; + } + + boost::json::object epoch_control; + epoch_control["start_epoch"] = kfState.time.to_string(); + + boost::json::object processing_options; + processing_options["epoch_control"] = epoch_control; + + boost::json::object estimation_parameters; + estimation_parameters["satellites"] = satellites; + + boost::json::object json; + json["processing_options"] = processing_options; + json["estimation_parameters"] = estimation_parameters; + + string filename = acsConfig.orbit_ics_filename; + + if (isSmoothed) + { + filename += acsConfig.pppOpts.rts_smoothed_suffix; + } + + PTime logtime = kfState.time; + + boost::posix_time::ptime logptime = boost::posix_time::from_time_t((time_t)logtime.bigTime); + + if ((GTime)logtime == GTime::noTime()) + { + logptime = boost::posix_time::not_a_date_time; + } + replaceTimes(filename, logptime); + + std::ofstream output(filename); + + if (!output) + { + return; + } + + output << boost::json::serialize(json) << "\n"; +} diff --git a/src/cpp/orbprop/orbitProp.hpp b/src/cpp/orbprop/orbitProp.hpp index 9fb0591c8..7b7f85421 100644 --- a/src/cpp/orbprop/orbitProp.hpp +++ b/src/cpp/orbprop/orbitProp.hpp @@ -1,210 +1,192 @@ - #pragma once #include - #include -#include #include +#include +#include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/attitude.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/trace.hpp" -using std::vector; using std::map; - -#include "eigenIncluder.hpp" -#include "acsConfig.hpp" -#include "algebra.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" - +using std::shared_ptr; +using std::vector; using namespace boost::numeric::odeint; +// forward declarations +struct KFState; +struct KFKey; + struct EMP { - bool scaleEclipse = false; - int deg = 0; - E_EmpAxis axisId = E_EmpAxis::NONE; - E_TrigType type = E_TrigType::COS; - double value = 0; + bool scaleEclipse = false; + int deg = 0; + E_EmpAxis axisId = E_EmpAxis::NONE; + E_TrigType type = E_TrigType::COS; + double value = 0; + KFKey key; }; struct OrbitState : OrbitOptions { - SatSys Sat; - string str; - - bool exclude = false; - - shared_ptr subState_ptr; - - vector empInput; - - mutable map componentsMap; - - int numEmp = 0; - int numParam = 0; - Vector3d pos; - Vector3d vel; - MatrixXd posVelSTM; - - AttStatus attStatus; - Vector3d gyroBias = Vector3d::Zero(); - Vector3d acclBias = Vector3d::Zero(); - Vector3d gyroScale = Vector3d::Ones(); - Vector3d acclScale = Vector3d::Ones(); - double posVar = 0; - - OrbitState& operator+=(double rhs) - { - pos = (pos .array() + rhs).matrix(); - vel = (vel .array() + rhs).matrix(); - posVelSTM = (posVelSTM.array() + rhs).matrix(); - return *this; - } - - OrbitState& operator*=(double rhs) - { - pos *= rhs; - vel *= rhs; - posVelSTM *= rhs; - return *this; - } - - OrbitState operator+(double rhs) const - { - OrbitState newState = *this; - newState += rhs; - return newState; - } - - OrbitState operator+(const OrbitState& rhs) const - { - OrbitState newState = *this; - newState.pos += rhs.pos; - newState.vel += rhs.vel; - newState.posVelSTM += rhs.posVelSTM; - return newState; - } - - OrbitState operator*(double rhs) const - { - OrbitState newState = *this; - newState *= rhs; - return newState; - } + SatSys Sat; + string str; + + bool exclude = false; + + shared_ptr subState_ptr; + + vector empInput; + + mutable map componentsMap; + + int numEmp = 0; + int numParam = 0; + double srpCr = 0; + double dragCd = 0; + Vector3d pos; + Vector3d vel; + MatrixXd posVelSTM; + bool estimateCr = false; + bool estimateCd = false; + AttStatus attStatus; + Vector3d gyroBias = Vector3d::Zero(); + Vector3d acclBias = Vector3d::Zero(); + Vector3d gyroScale = Vector3d::Ones(); + Vector3d acclScale = Vector3d::Ones(); + double posVar = 0; + + OrbitState& operator+=(double rhs) + { + pos = (pos.array() + rhs).matrix(); + vel = (vel.array() + rhs).matrix(); + posVelSTM = (posVelSTM.array() + rhs).matrix(); + return *this; + } + + OrbitState& operator*=(double rhs) + { + pos *= rhs; + vel *= rhs; + posVelSTM *= rhs; + return *this; + } + + OrbitState operator+(double rhs) const + { + OrbitState newState = *this; + newState += rhs; + return newState; + } + + OrbitState operator+(const OrbitState& rhs) const + { + OrbitState newState = *this; + newState.pos += rhs.pos; + newState.vel += rhs.vel; + newState.posVelSTM += rhs.posVelSTM; + return newState; + } + + OrbitState operator*(double rhs) const + { + OrbitState newState = *this; + newState *= rhs; + return newState; + } }; typedef vector Orbits; -inline OrbitState operator*( - const double lhs, - const OrbitState& rhs) +inline OrbitState operator*(const double lhs, const OrbitState& rhs) { - return rhs * lhs; + return rhs * lhs; }; -inline Orbits operator+( - const Orbits& lhs, - const Orbits& rhs) +inline Orbits operator+(const Orbits& lhs, const Orbits& rhs) { - Orbits newState = lhs; - for (int i = 0; i < lhs.size(); i++) - { - newState[i] = lhs[i] + rhs[i]; - } - return newState; + Orbits newState = lhs; + for (int i = 0; i < lhs.size(); i++) + { + newState[i] = lhs[i] + rhs[i]; + } + return newState; } -inline Orbits operator*( - const Orbits& lhs, - const double rhs) +inline Orbits operator*(const Orbits& lhs, const double rhs) { - Orbits newState = lhs; - for (int i = 0; i < lhs.size(); i++) - { - newState[i] *= rhs; - } - return newState; + Orbits newState = lhs; + for (int i = 0; i < lhs.size(); i++) + { + newState[i] *= rhs; + } + return newState; } -inline Orbits operator*( - const double rhs, - const Orbits& lhs) +inline Orbits operator*(const double rhs, const Orbits& lhs) { - Orbits newState = lhs; - for (int i = 0; i < lhs.size(); i++) - { - newState[i] *= rhs; - } - return newState; + Orbits newState = lhs; + for (int i = 0; i < lhs.size(); i++) + { + newState[i] *= rhs; + } + return newState; } - struct OrbitIntegrator { - GTime timeInit; + GTime timeInit; - Matrix3d eci2ecf; - Matrix3d deci2ecf; + Matrix3d eci2ecf; + Matrix3d deci2ecf; - map planetsPosMap; - map planetsVelMap; + map planetsPosMap; + map planetsVelMap; - MatrixXd Cnm; - MatrixXd Snm; - runge_kutta_fehlberg78 odeIntegrator; + MatrixXd Cnm; + MatrixXd Snm; + runge_kutta_fehlberg78 odeIntegrator; - void operator()( - const Orbits& orbInit, - Orbits& orbUpdate, - const double mjdSec); + void operator()(const Orbits& orbInit, Orbits& orbUpdate, const double mjdSec); - void computeCommon( - const GTime time); + void computeCommon(const GTime time); - void computeAcceleration( - const OrbitState& orbInit, - Vector3d& acc, - Matrix3d& dAdPos, - Matrix3d& dAdVel, - MatrixXd& dAdParam, - const GTime time); + void computeAcceleration( + const OrbitState& orbInit, + Vector3d& acc, + Matrix3d& dAdPos, + Matrix3d& dAdVel, + MatrixXd& dAdParam, + const GTime time + ); }; -KFState getOrbitFromState( - Trace& trace, - string id, - const KFState& kfState); +KFState getOrbitFromState(Trace& trace, string id, const KFState& kfState); -void predictOrbits( - Trace& trace, - const KFState& kfState, - GTime time); +void predictOrbits(Trace& trace, KFState& kfState, GTime time); -Orbits prepareOrbits( - Trace& trace, - const KFState& kfState); +Orbits prepareOrbits(Trace& trace, const KFState& kfState); void integrateOrbits( - OrbitIntegrator& orbitPropagator, - Orbits& orbits, - double integrationPeriod, - double dt); + OrbitIntegrator& orbitPropagator, + Orbits& orbits, + double integrationPeriod, + double dt +); - -void addEmpStates( - const EmpKalmans& satOpts, - const KFState& kfState, - const string& id); +void addEmpStates(const EmpKalmans& satOpts, KFState& kfState, const string& id); void addNilDesignStates( - const KalmanModel& model, - const KFState& kfState, - const KF& kfType, - int num, - const string& id); - -void outputOrbitConfig( - KFState& kfState, - string suffix = ""); - + const KalmanModel& model, + KFState& kfState, + const KF& kfType, + int num, + const string& id +); + +void outputOrbitConfig(KFState& kfState, bool isSmoothed = false); \ No newline at end of file diff --git a/src/cpp/orbprop/planets.cpp b/src/cpp/orbprop/planets.cpp index 268790c10..a9b6fc8d5 100644 --- a/src/cpp/orbprop/planets.cpp +++ b/src/cpp/orbprop/planets.cpp @@ -1,95 +1,121 @@ - // #pragma GCC optimize ("O0") +#include "orbprop/planets.hpp" #include -#include +#include +#include "3rdparty/jpl/jpl_eph.hpp" +#include "3rdparty/sofa/src/sofa.h" +#include "common/constants.hpp" +#include "common/enums.h" +#include "common/erp.hpp" +#include "common/navigation.hpp" +#include "orbprop/coordinates.hpp" using std::lock_guard; -#include "coordinates.hpp" -#include "navigation.hpp" -#include "constants.hpp" -#include "jpl_eph.hpp" -#include "planets.hpp" -#include "enums.h" -#include "erp.hpp" -#include "sofa.h" - - std::mutex jplEphMutex; - bool jplEphPos( - struct jpl_eph_data* jplEph_ptr, ///< Pointer to jpl binary data - MjDateTT mjdTT, ///< Julian_TT - E_ThirdBody thirdBody, ///< Star to Calculate the Velocity and Position - Vector3d& pos, ///< Unit: m - Vector3d* vel_ptr) ///< vel (m/s) + struct jpl_eph_data* jplEph_ptr, ///< Pointer to jpl binary data + MjDateTT mjdTT, ///< Julian_TT + E_ThirdBody thirdBody, ///< Star to Calculate the Velocity and Position + Vector3d& pos, ///< Unit: m + Vector3d* vel_ptr ///< vel (m/s) +) { - if (jplEph_ptr == nullptr) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No JPL ephemeris file found."; - - return false; - } - - int result; - double r_p[6]; - { - lock_guard guard(jplEphMutex); - - result = jpl_pleph(jplEph_ptr, mjdTT.to_double() + JD2MJD, thirdBody, eEarth, r_p, !!vel_ptr); - } - - switch (result) - { - case 0: - pos(0) = r_p[0] * AU; - pos(1) = r_p[1] * AU; - pos(2) = r_p[2] * AU; - - if (vel_ptr) - { - auto& vel = *vel_ptr; - vel(0) = r_p[3] * AUPerDay; - vel(1) = r_p[4] * AUPerDay; - vel(2) = r_p[5] * AUPerDay; - } - return true; - case -1: std::cout << "JPL_EPH_OUTSIDE_RANGE" << "\n"; break; - case -2: std::cout << "JPL_EPH_READ_ERROR" << "\n"; break; - case -3: std::cout << "JPL_EPH_QUANTITY_NOT_IN_EPHEMERIS" << "\n"; break; - case -5: std::cout << "JPL_EPH_INVALID_INDEX" << "\n"; break; - case -6: std::cout << "JPL_EPH_FSEEK_ERROR" << "\n"; break; - default: std::cout << "Result is out of Known Situation" << "\n"; break; - } - - return false; + if (jplEph_ptr == nullptr) + { + BOOST_LOG_TRIVIAL(warning) << "No JPL ephemeris file found."; + + return false; + } + + int result; + double r_p[6]; + { + lock_guard guard(jplEphMutex); + + result = jpl_pleph( + jplEph_ptr, + mjdTT.to_double() + JD2MJD, + static_cast(thirdBody), + eEarth, + r_p, + !!vel_ptr + ); + } + + switch (result) + { + case 0: + pos(0) = r_p[0] * AU; + pos(1) = r_p[1] * AU; + pos(2) = r_p[2] * AU; + + if (vel_ptr) + { + auto& vel = *vel_ptr; + vel(0) = r_p[3] * AUPerDay; + vel(1) = r_p[4] * AUPerDay; + vel(2) = r_p[5] * AUPerDay; + } + return true; + case -1: + std::cout << "JPL_EPH_OUTSIDE_RANGE" << "\n"; + break; + case -2: + std::cout << "JPL_EPH_READ_ERROR" << "\n"; + break; + case -3: + std::cout << "JPL_EPH_QUANTITY_NOT_IN_EPHEMERIS" << "\n"; + break; + case -5: + std::cout << "JPL_EPH_INVALID_INDEX" << "\n"; + break; + case -6: + std::cout << "JPL_EPH_FSEEK_ERROR" << "\n"; + break; + default: + std::cout << "Result is out of Known Situation" << "\n"; + break; + } + + return false; } -bool planetPosEcef( - GTime time, - E_ThirdBody thirdBody, - VectorEcef& rBody, - ERPValues erpv) +bool planetPosEcef(GTime time, E_ThirdBody thirdBody, VectorEcef& rBody, ERPValues erpv) { - VectorEci rBodyEci; - - bool pass = jplEphPos(nav.jplEph_ptr, time, thirdBody, rBodyEci); - if (pass == false) - { - double pvh[2][3]; - double pvb[2][3]; - - if (thirdBody == +E_ThirdBody::SUN) { Sofa::iauEpv (time, pvh, pvb); for (int i = 0; i < 3; i++) { rBodyEci(i) = -pvh[0][i] * AU; } } - else if (thirdBody == +E_ThirdBody::MOON) { Sofa::iauMoon (time, pvh); for (int i = 0; i < 3; i++) { rBodyEci(i) = +pvh[0][i] * AU; } } - else - return false; - } - - FrameSwapper frameSwapper(time, erpv); - - rBody = frameSwapper(rBodyEci); - - return true; + VectorEci rBodyEci; + + bool pass = jplEphPos(nav.jplEph_ptr, time, thirdBody, rBodyEci); + if (pass == false) + { + double pvh[2][3]; + double pvb[2][3]; + + if (thirdBody == E_ThirdBody::SUN) + { + Sofa::iauEpv(time, pvh, pvb); + for (int i = 0; i < 3; i++) + { + rBodyEci(i) = -pvh[0][i] * AU; + } + } + else if (thirdBody == E_ThirdBody::MOON) + { + Sofa::iauMoon(time, pvh); + for (int i = 0; i < 3; i++) + { + rBodyEci(i) = +pvh[0][i] * AU; + } + } + else + return false; + } + + FrameSwapper frameSwapper(time, erpv); + + rBody = frameSwapper(rBodyEci); + + return true; } diff --git a/src/cpp/orbprop/planets.hpp b/src/cpp/orbprop/planets.hpp index d4aaaa0a2..91d76fe85 100644 --- a/src/cpp/orbprop/planets.hpp +++ b/src/cpp/orbprop/planets.hpp @@ -1,75 +1,65 @@ /* JPL planetary and lunar ephemerides */ - #pragma once -#include "eigenIncluder.hpp" -#include "constants.hpp" -#include "gTime.hpp" -#include "enums.h" -#include "erp.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/erp.hpp" +#include "common/gTime.hpp" -/** -* If nutations are wanted, set ntarg = 14. -* For librations, set ntarg = 15. set ncent= 0. -* For TT-TDB, set ntarg = 17. -* I've not actually seen an ntarg = 16 case yet.) -*/ -enum E_SolarSysStarType +/** + * If nutations are wanted, set ntarg = 14. + * For librations, set ntarg = 15. set ncent= 0. + * For TT-TDB, set ntarg = 17. + * I've not actually seen an ntarg = 16 case yet.) + */ +enum E_SolarSysStarType { - eMercury = 1, ///< Mercury - eVenus = 2, ///< Venus - eEarth = 3, ///< Earth - eMars = 4, ///< Mars - eJupiter = 5, ///< Jupiter - eSaturn = 6, ///< Saturn - eUranus = 7, ///< Uranus - eNeptune = 8, ///< Neptune - ePluto = 9, ///< Pluto - eMoon = 10, ///< Moon - eSun = 11, ///< Sun + eMercury = 1, ///< Mercury + eVenus = 2, ///< Venus + eEarth = 3, ///< Earth + eMars = 4, ///< Mars + eJupiter = 5, ///< Jupiter + eSaturn = 6, ///< Saturn + eUranus = 7, ///< Uranus + eNeptune = 8, ///< Neptune + ePluto = 9, ///< Pluto + eMoon = 10, ///< Moon + eSun = 11, ///< Sun - eSolarSysBarycenter = 12, ///< Solar System barycenter - eEarthMoonBaryCenter = 13, ///< Earth Moon barycenter - eJplNutation = 14, ///< nutations (longitude and obliq) - eJPLLnrLibration = 15, ///< Lunar Librations - eJPLLunarMantle = 16, ///< Lunar Mantle omega_x,omega_y,omega_z - eJPLTT_TDB = 17, ///< TT-TDB, if on eph. file + eSolarSysBarycenter = 12, ///< Solar System barycenter + eEarthMoonBaryCenter = 13, ///< Earth Moon barycenter + eJplNutation = 14, ///< nutations (longitude and obliq) + eJPLLnrLibration = 15, ///< Lunar Librations + eJPLLunarMantle = 16, ///< Lunar Mantle omega_x,omega_y,omega_z + eJPLTT_TDB = 17, ///< TT-TDB, if on eph. file - eSelfDefinedStarType = 99, + eSelfDefinedStarType = 99, }; - bool jplEphPos( - struct jpl_eph_data* jplEph_ptr, - MjDateTT mjdTT, - E_ThirdBody thirdBody, - Vector3d& pos, - Vector3d* vel_ptr = nullptr); + struct jpl_eph_data* jplEph_ptr, + MjDateTT mjdTT, + E_ThirdBody thirdBody, + Vector3d& pos, + Vector3d* vel_ptr = nullptr +); -bool planetPosEcef( - GTime time, - E_ThirdBody thirdBody, - VectorEcef& ecef, - ERPValues erpv = {}); +bool planetPosEcef(GTime time, E_ThirdBody thirdBody, VectorEcef& ecef, ERPValues erpv = {}); -//From DE440 -static map GM_values = -{ - {E_ThirdBody::MERCURY, 22031.868551e9 }, - {E_ThirdBody::VENUS, 324858.592000e9 }, - {E_ThirdBody::EARTH, 0.3986004415E+15 }, - {E_ThirdBody::MARS, 42828.375816e9 }, - {E_ThirdBody::JUPITER, 126712764.100000e9 }, - {E_ThirdBody::SATURN, 37940584.841800e9 }, - {E_ThirdBody::URANUS, 5794556.400000e9 }, - {E_ThirdBody::NEPTUNE, 6836527.100580e9 }, - {E_ThirdBody::PLUTO, 975.500000e9 }, - {E_ThirdBody::MOON, 4.9027989e12 }, - {E_ThirdBody::SUN, 1.32712440018e20 }, +// From DE440 +static map GM_values = { + {E_ThirdBody::MERCURY, 22031.868551e9}, + {E_ThirdBody::VENUS, 324858.592000e9}, + {E_ThirdBody::EARTH, 0.3986004415E+15}, + {E_ThirdBody::MARS, 42828.375816e9}, + {E_ThirdBody::JUPITER, 126712764.100000e9}, + {E_ThirdBody::SATURN, 37940584.841800e9}, + {E_ThirdBody::URANUS, 5794556.400000e9}, + {E_ThirdBody::NEPTUNE, 6836527.100580e9}, + {E_ThirdBody::PLUTO, 975.500000e9}, + {E_ThirdBody::MOON, 4.9027989e12}, + {E_ThirdBody::SUN, 1.32712440018e20}, }; -double sunVisibility( - Vector3d& rSat, - Vector3d& rSun, - Vector3d& rMoon); - +double sunVisibility(Vector3d& rSat, Vector3d& rSun, Vector3d& rMoon); diff --git a/src/cpp/orbprop/spaceWeather.cpp b/src/cpp/orbprop/spaceWeather.cpp new file mode 100644 index 000000000..56df35401 --- /dev/null +++ b/src/cpp/orbprop/spaceWeather.cpp @@ -0,0 +1,111 @@ +#include "spaceWeather.hpp" +#include +#include +#include + +SpaceWeather spaceWeatherData; + +/** Parser of each data line + * + */ +void ParseSpaceWeatherData( + const std::string& line, ///< Each line of the file + SpaceWeatherData& data ///< Space Weather Struct +) +{ + std::istringstream iss(line); + iss >> data.year >> data.month >> data.day >> data.bsrn >> data.nd; + for (int i = 0; i < 8; i++) + { + iss >> data.kp[i]; + } + iss >> data.sum; + for (int i = 0; i < 8; i++) + { + iss >> data.ap[i]; + } + iss >> data.avg >> data.cp >> data.c9 >> data.isn >> data.adj_f10_7 >> data.q >> + data.adj_ctr81 >> data.adj_lst81 >> data.obs_f10_7 >> data.obs_ctr81 >> data.obs_lst81; +} + +/** Read Space Weather file into the data struct + * + */ +void SpaceWeather::read(std::string filepath) ///< File path to Space Weather file +{ + std::ifstream file(filepath); + std::string line; + int numObservedPoints = 0; + int numDailyPredictedPoints = 0; + int numMonthlyPredictedPoints = 0; + std::vector dailyPredictedData; + std::vector monthlyPredictedData; + SpaceWeatherData data; + if (!file) + { + std::cout << "Failed to open Space Weather file." << std::endl; + return; + } + while (std::getline(file, line)) + { + if (line.find("NUM_OBSERVED_POINTS") != std::string::npos) + { + std::istringstream iss(line); + std::string ignore; + iss >> ignore >> numObservedPoints; + } + else if (line.find("NUM_DAILY_PREDICTED_POINTS") != std::string::npos) + { + std::istringstream iss(line); + std::string ignore; + iss >> ignore >> numDailyPredictedPoints; + } + else if (line.find("NUM_MONTHLY_PREDICTED_POINTS") != std::string::npos) + { + std::istringstream iss(line); + std::string ignore; + iss >> ignore >> numMonthlyPredictedPoints; + } + 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") + { + // Skip section endings + continue; + } + else if (line.empty()) + { + // Skip empty lines + continue; + } + else + { + ParseSpaceWeatherData(line, data); + if (SpaceWeather.size() < numObservedPoints) + { + SpaceWeather.push_back(data); + } + else if (dailyPredictedData.size() < numDailyPredictedPoints) + { + dailyPredictedData.push_back(data); + } + else if (monthlyPredictedData.size() < numMonthlyPredictedPoints) + { + monthlyPredictedData.push_back(data); + } + } + } + return; +} + +// Function to check if a SpaceWeatherData element matches a given date + +bool dateMatches(const SpaceWeatherData& data, int year, int month, int day) +{ + return (data.year == year && data.month == month && data.day == day); +} \ No newline at end of file diff --git a/src/cpp/orbprop/spaceWeather.hpp b/src/cpp/orbprop/spaceWeather.hpp new file mode 100644 index 000000000..4404ab74e --- /dev/null +++ b/src/cpp/orbprop/spaceWeather.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +/** Space Weather data struct + */ +struct SpaceWeatherData +{ + int year = 0; + int month = 0; + int day = 0; + int bsrn = 0; ///< Bartels Solar Rotation Number. A sequence of 27-day intervals counted + ///< continuously from 1832 Feb 8. + int nd = 0; ///< Number of Day within the Bartels 27-day cycle (01-27) + int kp[8] = {0}; + ; ///< Planetary 3-hour Range Index (Kp) (kp1 to kp8) + int sum = 0; ///< Sum of the 8 Kp indices for the day: Kp has values of 0o, 0+, 1-, 1o, 1+, 2-, + ///< 2o, 2+, ... , 8o, 8+, 9-, 9o, + ///< which are expressed in steps of one third unit. + ///< These values are multiplied by 10 and rounded to an + ///< integer value + int ap[8] = {0}; ///< Planetary Equivalent Amplitude (Ap) (ap1 to ap8) + int avg = 0; ///< Arithmetic average of the 8 Ap indices for the day + double cp = + 0; ///< Planetary Daily Character Figure: A qualitative estimate of overall level of + ///< magnetic activity for the day determined from the sum of the 8 Ap indices. + ///< Cp ranges, in steps of one-tenth, from 0 (quiet) to 2.5 + ///< (highly disturbed). + int c9 = + 0; ///< A conversion of the 0-to-2.5 range of the Cp index to one digit between 0 and 9 + int isn = 0; ///< International Sunspot Number. Records contain the Zurich number through 1980 + ///< Dec 31 and the International Brussels number thereafter. + double adj_f10_7 = + 0.0; ///< 10.7-cm Solar Radio Flux (F10.7) adjusted to 1 AU: Measured at Ottawa at 1700 UT + ///< daily from 1947 Feb 14 until 1991 May 31 and measured at + ///< Penticton at 2000 UT from 1991 Jun 01 + ///< on. Expressed in units of 10-22 W/m2/Hz. + int q = 0; ///< Flux Qualifier: (0: flux required no adjustment; + ///< 1: flux required adjustment for burst in progress at time of measurement; + ///< 2: a flux approximated by either interpolation or extrapolation; + ///< 3: no observation; + ///< 4: CelesTrak interpolation of missing data) + double adj_ctr81 = 0.0; ///< Centered 81-day arithmetic average of F10.7 (adjusted) + double adj_lst81 = 0.0; ///< Last 81-day arithmetic average of F10.7 (adjusted) + double obs_f10_7 = 0.0; ///< Observed (unadjusted) value of F10.7 + double obs_ctr81 = 0.0; ///< Centered 81-day arithmetic average of F10.7 (observed) + double obs_lst81 = 0.0; ///< Last 81-day arithmetic average of F10.7 (observed) +}; + +struct SpaceWeather +{ + void read(std::string filepath); + std::vector SpaceWeather; +}; + +extern SpaceWeather spaceWeatherData; +bool dateMatches(const SpaceWeatherData& data, int year, int month, int day); \ No newline at end of file diff --git a/src/cpp/orbprop/staticField.cpp b/src/cpp/orbprop/staticField.cpp index e0231ca79..6ef1634b5 100644 --- a/src/cpp/orbprop/staticField.cpp +++ b/src/cpp/orbprop/staticField.cpp @@ -1,119 +1,125 @@ - - -#include +#include "orbprop/staticField.hpp" +#include +#include +#include #include +#include #include +#include "common/gTime.hpp" using std::vector; -#include -#include -#include - -#include "staticField.hpp" -#include "gTime.hpp" - -//todo aaron global +// todo aaron global StaticField egm; /** Read the static gravity field -* @todo include the time variable component of the gravity field. -*/ -void StaticField::read( - const string& filename, - int degMax) + * @todo include the time variable component of the gravity field. + */ +void StaticField::read(const string& filename, int degMax) { - if (filename.empty()) - { - return; - } + if (filename.empty()) + { + return; + } - std::ifstream infile(filename); - if (!infile) - { - BOOST_LOG_TRIVIAL(error) - << "EGM file open error " << filename; + std::ifstream infile(filename); + if (!infile) + { + BOOST_LOG_TRIVIAL(error) << "EGM file open error " << filename; - return; - } + return; + } - this->filename = filename; + this->filename = filename; - bool header = true; - gfctC = MatrixXd::Zero(degMax + 1, degMax + 1); - gfctS = MatrixXd::Zero(degMax + 1, degMax + 1); + bool header = true; + gfctC = MatrixXd::Zero(degMax + 1, degMax + 1); + gfctS = MatrixXd::Zero(degMax + 1, degMax + 1); - string line; - int maxDegreeRead = 0; - while (std::getline(infile, line)) - { - std::istringstream iss(line); - if (header) - { - if (line.compare(0, 11, "end_of_head") == 0) - { - header = false; - } - else - { - if (line.length() != 0) - { - vector split; - boost::algorithm::split(split, line, boost::algorithm::is_any_of(" "), boost::token_compress_on); - if (split[0].compare("modelname") == 0) modelName = split[1]; - else if (split[0].compare("earth_gravity_constant") == 0) earthGravityConstant = stod( split[1]); - else if (split[0].compare("radius") == 0) earthRadius = stod( split[1]); - else if (split[0].compare("max_degree") == 0) maxDegree = stoi( split[1]); - else if (split[0].compare("tide_system") == 0) - { - if (split[1].compare("tide_free") == 0) isTideFree = true; - else isTideFree = false; - } - } - } - } - else - { - int n; - int m; - double C; - double S; - double sigC; - double sigs; - string key; - iss >> key >> n >> m >> C >> S >> sigC >> sigs ; - if (n <= degMax) - { - maxDegreeRead = std::max(maxDegreeRead, n); - if (key.compare(0, 3, "gfc") == 0) - { - gfctC(n, m) = C; - if (m > 0) gfctS(n, m) = S; - } - } - else - { - break; - } - } - } - if (maxDegreeRead < degMax) - { - BOOST_LOG_TRIVIAL(warning) << "Maximum degree requested is higher than the model " << degMax << ">" << maxDegreeRead; - } - this->toTideFree(); - initialised = true; + string line; + int maxDegreeRead = 0; + while (std::getline(infile, line)) + { + std::istringstream iss(line); + if (header) + { + if (line.compare(0, 11, "end_of_head") == 0) + { + header = false; + } + else + { + if (line.length() != 0) + { + vector split; + boost::algorithm::split( + split, + line, + boost::algorithm::is_any_of(" "), + boost::token_compress_on + ); + if (split[0].compare("modelname") == 0) + modelName = split[1]; + else if (split[0].compare("earth_gravity_constant") == 0) + earthGravityConstant = stod(split[1]); + else if (split[0].compare("radius") == 0) + earthRadius = stod(split[1]); + else if (split[0].compare("max_degree") == 0) + maxDegree = stoi(split[1]); + else if (split[0].compare("tide_system") == 0) + { + if (split[1].compare("tide_free") == 0) + isTideFree = true; + else + isTideFree = false; + } + } + } + } + else + { + int n; + int m; + double C; + double S; + double sigC; + double sigs; + string key; + iss >> key >> n >> m >> C >> S >> sigC >> sigs; + if (n <= degMax) + { + maxDegreeRead = std::max(maxDegreeRead, n); + if (key.compare(0, 3, "gfc") == 0) + { + gfctC(n, m) = C; + if (m > 0) + gfctS(n, m) = S; + } + } + else + { + break; + } + } + } + if (maxDegreeRead < degMax) + { + BOOST_LOG_TRIVIAL(warning) << "Maximum degree requested is higher than the model " << degMax + << ">" << maxDegreeRead; + } + this->toTideFree(); + initialised = true; } void StaticField::toTideFree() { - double correction = -4.1736e-9; + double correction = -4.1736e-9; - if (isTideFree == false) - { - gfctC(2, 0) -= correction; - BOOST_LOG_TRIVIAL(info) << "Earth gravity field converted to tide free system (compatible with orbit propagation)"; - isTideFree = true; - } + if (isTideFree == false) + { + gfctC(2, 0) -= correction; + BOOST_LOG_TRIVIAL(info) << "Earth gravity field converted to tide free system (compatible " + "with orbit propagation)"; + isTideFree = true; + } } - diff --git a/src/cpp/orbprop/staticField.hpp b/src/cpp/orbprop/staticField.hpp index c4500c836..834443584 100644 --- a/src/cpp/orbprop/staticField.hpp +++ b/src/cpp/orbprop/staticField.hpp @@ -1,34 +1,30 @@ - #pragma once #include +#include "common/eigenIncluder.hpp" using std::string; -#include "eigenIncluder.hpp" - /** Structure for variable and function related to the static gravity field * @todo time variable static gravity field */ -struct StaticField +struct StaticField { - void read( - const string& filename, - int degMax); + void read(const string& filename, int degMax); + + void toTideFree(); - void toTideFree(); - - bool initialised = false; - string filename; - MatrixXd gfctC; - MatrixXd gfctS; - int degMax; - string modelName; - double earthGravityConstant; - double earthRadius; - int maxDegree; - bool isTideFree; - string norm; + bool initialised = false; + string filename; + MatrixXd gfctC; + MatrixXd gfctS; + int degMax; + string modelName; + double earthGravityConstant; + double earthRadius; + int maxDegree; + bool isTideFree; + string norm; }; -extern StaticField egm; +extern StaticField egm; diff --git a/src/cpp/orbprop/tideCoeff.cpp b/src/cpp/orbprop/tideCoeff.cpp index e3905dc39..97192928f 100644 --- a/src/cpp/orbprop/tideCoeff.cpp +++ b/src/cpp/orbprop/tideCoeff.cpp @@ -1,154 +1,144 @@ - -#include +#include "orbprop/tideCoeff.hpp" +#include +#include +#include #include +#include #include #include +#include "3rdparty/sofa/src/sofa.h" +#include "common/constants.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/iers2010.hpp" using std::string; using std::vector; -#include -#include -#include - -#include "coordinates.hpp" -#include "tideCoeff.hpp" -#include "constants.hpp" -#include "iers2010.hpp" -#include "sofa.h" - Tide oceanTide; Tide atmosphericTide; -void Tide::read( - const string& filename, - int degMax) +void Tide::read(const string& filename, int degMax) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " Reading tide"; - - if (filename.empty()) - { - return; - } - - std::ifstream infile(filename); - if (!infile) - { - BOOST_LOG_TRIVIAL(error) - << "Tide file open error " << filename; - - return; - } - - string line; - // Skip the 4 lines of header - for (int i = 0; i < 4; i++) - std::getline(infile, line); - - while (std::getline(infile, line)) - { - std::istringstream iss(line); - - string wave_; - string doodson_; - int n_; - int m_; - double cnmp_; - double snmp_; - double cnmm_; - double snmm_; - iss >> doodson_ >> wave_ >> n_ >> m_ >> cnmp_ >> snmp_ >> cnmm_ >> snmm_; - if (n_ <= degMax) - { - bool isnew = true; - for (auto& wave : tidalWaves) - { - if (wave.waveName == wave_) - { - wave.CnmP(n_, m_) = cnmp_; - wave.SnmP(n_, m_) = snmp_; - wave.CnmM(n_, m_) = cnmm_; - wave.SnmM(n_, m_) = snmm_; - isnew = false; - } - } - - if (isnew) - { - tidalWaves.push_back(TidalWave(wave_, doodson_, degMax)); - tidalWaves.back().CnmP(n_, m_) = cnmp_; - tidalWaves.back().CnmM(n_, m_) = cnmm_; - tidalWaves.back().SnmP(n_, m_) = snmp_; - tidalWaves.back().SnmM(n_, m_) = snmm_; - } - } - } - - for (auto& wave : tidalWaves) - { - wave.C1 = wave.CnmP + wave.CnmM; - wave.C2 = wave.SnmP + wave.SnmM; - wave.S1 = wave.SnmP - wave.SnmM; - wave.S2 = wave.CnmP - wave.CnmM; - } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " Reading tide"; + + if (filename.empty()) + { + return; + } + + std::ifstream infile(filename); + if (!infile) + { + BOOST_LOG_TRIVIAL(error) << "Tide file open error " << filename; + + return; + } + + string line; + // Skip the 4 lines of header + for (int i = 0; i < 4; i++) + std::getline(infile, line); + + while (std::getline(infile, line)) + { + std::istringstream iss(line); + + string wave_; + string doodson_; + int n_; + int m_; + double cnmp_; + double snmp_; + double cnmm_; + double snmm_; + iss >> doodson_ >> wave_ >> n_ >> m_ >> cnmp_ >> snmp_ >> cnmm_ >> snmm_; + if (n_ <= degMax) + { + bool isnew = true; + for (auto& wave : tidalWaves) + { + if (wave.waveName == wave_) + { + wave.CnmP(n_, m_) = cnmp_; + wave.SnmP(n_, m_) = snmp_; + wave.CnmM(n_, m_) = cnmm_; + wave.SnmM(n_, m_) = snmm_; + isnew = false; + } + } + + if (isnew) + { + tidalWaves.push_back(TidalWave(wave_, doodson_, degMax)); + tidalWaves.back().CnmP(n_, m_) = cnmp_; + tidalWaves.back().CnmM(n_, m_) = cnmm_; + tidalWaves.back().SnmP(n_, m_) = snmp_; + tidalWaves.back().SnmM(n_, m_) = snmm_; + } + } + } + + for (auto& wave : tidalWaves) + { + wave.C1 = wave.CnmP + wave.CnmM; + wave.C2 = wave.SnmP + wave.SnmM; + wave.S1 = wave.SnmP - wave.SnmM; + wave.S2 = wave.CnmP - wave.CnmM; + } } -TidalWave::TidalWave( - string name, - string darw, - int degmax) -: waveName(name) +TidalWave::TidalWave(string name, string darw, int degmax) : waveName(name) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " Creating new wave " << waveName << " degmax " << degmax; - - size_t dot = darw.find("."); - - doodson = Array6d::Zero(); - if (darw.length() >= 7) doodson(0) = std::stoi(darw.substr(dot - 3, 1)); - if (darw.length() >= 6) doodson(1) = std::stoi(darw.substr(dot - 2, 1)); - if (darw.length() >= 5) doodson(2) = std::stoi(darw.substr(dot - 1, 1)); - doodson(3) = std::stoi(darw.substr(dot + 1, 1)); - doodson(4) = std::stoi(darw.substr(dot + 2, 1)); - doodson(5) = std::stoi(darw.substr(dot + 3, 1)); - - for (int i = 1; i < 6; i++) doodson(i) -= 5; - - CnmM = MatrixXd::Zero(degmax + 1, degmax + 1); - CnmP = MatrixXd::Zero(degmax + 1, degmax + 1); - SnmM = MatrixXd::Zero(degmax + 1, degmax + 1); - SnmP = MatrixXd::Zero(degmax + 1, degmax + 1); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " Creating new wave " << waveName << " degmax " + << degmax; + + size_t dot = darw.find("."); + + doodson = Array6d::Zero(); + if (darw.length() >= 7) + doodson(0) = std::stoi(darw.substr(dot - 3, 1)); + if (darw.length() >= 6) + doodson(1) = std::stoi(darw.substr(dot - 2, 1)); + if (darw.length() >= 5) + doodson(2) = std::stoi(darw.substr(dot - 1, 1)); + doodson(3) = std::stoi(darw.substr(dot + 1, 1)); + doodson(4) = std::stoi(darw.substr(dot + 2, 1)); + doodson(5) = std::stoi(darw.substr(dot + 3, 1)); + + for (int i = 1; i < 6; i++) + doodson(i) -= 5; + + CnmM = MatrixXd::Zero(degmax + 1, degmax + 1); + CnmP = MatrixXd::Zero(degmax + 1, degmax + 1); + SnmM = MatrixXd::Zero(degmax + 1, degmax + 1); + SnmP = MatrixXd::Zero(degmax + 1, degmax + 1); } /** Generates the Beta angles from the given a modified julian day. */ -void Tide::setBeta( - GTime time, - double ut1_utc) +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(1) = fundArgs.f + fundArgs.omega; - beta(2) = beta(1) - fundArgs.d; - beta(3) = beta(1) - fundArgs.l; - beta(4) = -1 * fundArgs.omega; - beta(5) = beta(1) - fundArgs.d - fundArgs.l_prime; + FundamentalArgs fundArgs(time, ut1_utc); + + beta(0) = fundArgs.gmst - fundArgs.f - fundArgs.omega; // todo aaron, swap with doodson? + beta(1) = fundArgs.f + fundArgs.omega; + beta(2) = beta(1) - fundArgs.d; + beta(3) = beta(1) - fundArgs.l; + beta(4) = -1 * fundArgs.omega; + beta(5) = beta(1) - fundArgs.d - fundArgs.l_prime; } -void Tide::getSPH( - Array6d& dood, - MatrixXd& Cnm, - MatrixXd& Snm) +void Tide::getSPH(Array6d& dood, MatrixXd& Cnm, MatrixXd& Snm) { - for (auto& wave : tidalWaves) - { - double thetaf = (dood * wave.doodson).sum(); - double cosThetaf = cos(thetaf); - double sinThetaf = sin(thetaf); - - Cnm += (wave.C1 * cosThetaf + wave.C2 * sinThetaf) * 1e-11; - Snm += (wave.S1 * cosThetaf - wave.S2 * sinThetaf) * 1e-11; - } - Snm.col(0).setZero(); - Snm.row(0).setZero(); + for (auto& wave : tidalWaves) + { + double thetaf = (dood * wave.doodson).sum(); + double cosThetaf = cos(thetaf); + double sinThetaf = sin(thetaf); + + Cnm += (wave.C1 * cosThetaf + wave.C2 * sinThetaf) * 1e-11; + Snm += (wave.S1 * cosThetaf - wave.S2 * sinThetaf) * 1e-11; + } + Snm.col(0).setZero(); + Snm.row(0).setZero(); } diff --git a/src/cpp/orbprop/tideCoeff.hpp b/src/cpp/orbprop/tideCoeff.hpp index ba4790c47..7501cae35 100644 --- a/src/cpp/orbprop/tideCoeff.hpp +++ b/src/cpp/orbprop/tideCoeff.hpp @@ -1,57 +1,45 @@ - #pragma once #include #include +#include "common/eigenIncluder.hpp" using std::string; using std::vector; -#include "eigenIncluder.hpp" - +struct GTime; struct TidalWave { - TidalWave(); - - TidalWave( - string name, - string doodson, - int degmax - ); - - string waveName; - MatrixXd CnmP; - MatrixXd CnmM; - MatrixXd SnmP; - MatrixXd SnmM; - MatrixXd C1; - MatrixXd C2; - MatrixXd S1; - MatrixXd S2; - ArrayXd coeff; - Array6d doodson; + TidalWave(); + + TidalWave(string name, string doodson, int degmax); + + string waveName; + MatrixXd CnmP; + MatrixXd CnmM; + MatrixXd SnmP; + MatrixXd SnmM; + MatrixXd C1; + MatrixXd C2; + MatrixXd S1; + MatrixXd S2; + ArrayXd coeff; + Array6d doodson; }; struct Tide { - string filename; - int degMax; + string filename; + int degMax; - vector tidalWaves; - Vector6d beta; + vector tidalWaves; + Vector6d beta; - void read( - const string& filename, - int degMax); + void read(const string& filename, int degMax); - void setBeta( - GTime time, - double ut1_utc = 0); + void setBeta(GTime time, double ut1_utc = 0); - void getSPH( - Array6d& beta, - MatrixXd& Cnm, - MatrixXd& Snm); + void getSPH(Array6d& beta, MatrixXd& Cnm, MatrixXd& Snm); }; extern Tide oceanTide; diff --git a/src/cpp/other_ssr/otherSSR.hpp b/src/cpp/other_ssr/otherSSR.hpp index b68a1ea6c..1c7409f15 100644 --- a/src/cpp/other_ssr/otherSSR.hpp +++ b/src/cpp/other_ssr/otherSSR.hpp @@ -1,15 +1,14 @@ -/* prototype functions for compact SSR and IGS SSR messages */ - #pragma once -#include #include +#include +#include "common/enums.h" +#include "common/gTime.hpp" -using std::vector; using std::map; +using std::vector; -#include "gTime.hpp" -#include "enums.h" +/* prototype functions for compact SSR and IGS SSR messages */ struct SSRUra; struct SSRAtm; @@ -17,142 +16,89 @@ struct SSROut; struct SSRPhasBias; struct SSRCodeBias; -const int ssrUdi[16] = -{ - 1, 2, 5, 10, 15, 30, 60, 120, 240, 300, 600, 900, 1800, 3600, 7200, 10800 -}; +const int ssrUdi[16] = {1, 2, 5, 10, 15, 30, 60, 120, 240, 300, 600, 900, 1800, 3600, 7200, 10800}; extern map> cmpSSRIndex2Code; extern map> igsSSRIndex2Code; extern map> igsSSRCode2Index; -int decodecompactSSR( - vector& data, - GTime now); - -E_ReturnType decodeigsSSR( - vector& data, - GTime now); - -vector encodecompactMSK( - map& orbClkMap, - map& codBiaMap, - map& phsBiaMap, - SSRAtm ssrAtm, - int updateIntIndex); - -vector encodecompactORB( - map& orbClkMap, - int updateIntIndex, - bool last); - -vector encodecompactCLK( - map& orbClkMap, - int updateIntIndex, - bool last); - -vector encodecompactURA( - map& orbClkMap, - int updateIntIndex, - bool last); - -vector encodecompactCMB( - map& orbClkMap, - int updateIntIndex, - bool last); - -vector encodecompactCOD( - map& codBiaMap, - int updateIntIndex, - bool last); - -vector encodecompactPHS( - map& phsBiaMap, - int updateIntIndex, - bool last); - -vector encodecompactBIA( - map& codBiaMap, - map& phsBiaMap, - int updateIntIndex, - bool last); - -vector encodecompactTEC( - SSRMeta& ssrMeta, - int regId, - SSRAtmRegion& ssrAtmReg, - int updateIntIndex, - bool last); - -vector encodecompactGRD( - SSRMeta& ssrMeta, - int regId, - SSRAtmRegion& ssrAtmReg, - int updateIntIndex, - bool last); - -vector encodecompactATM( - SSRMeta& ssrMeta, - int regId, - SSRAtmRegion& ssrAtmReg, - int updateIntIndex, - bool last); - -vector encodecompactSRV( - SSRAtm& ssrAtm); - - - -unsigned short IGS_SSR_subtype( - IgsSSRSubtype type, - E_Sys sys); - -IgsSSRSubtype IGS_SSR_group( - IgsSSRSubtype subType, - E_Sys& sys); - -vector encodeIGS_ORB( - map& orbClkMap, - E_Sys sys, - bool last); - -vector encodeIGS_CLK( - map& orbClkMap, - E_Sys sys, - bool last); - -vector encodeIGS_CMB( - map& orbClkMap, - E_Sys sys, - bool last); - -vector encodeIGS_HRC( - map& orbClkMap, - E_Sys sys, - bool last); - -vector encodeIGS_COD( - map& codBiaMap, - E_Sys sys, - bool last); - -vector encodeIGS_PHS( - map& phsBiaMap, - E_Sys sys, - bool last); - -vector encodeIGS_URA( - map& uraMap, - E_Sys sys, - bool last); - -vector encodeIGS_ATM( - SSRAtm& ssrAtm, - bool last); - -double checkPhaseDisc( - SatSys Sat, - E_ObsCode code, - double bias, - int disc, - int regn); +int decodecompactSSR(vector& data, GTime now); + +E_ReturnType decodeigsSSR(vector& data, GTime now); + +vector encodecompactMSK( + map& orbClkMap, + map& codBiaMap, + map& phsBiaMap, + SSRAtm ssrAtm, + int updateIntIndex +); + +vector encodecompactORB(map& orbClkMap, int updateIntIndex, bool last); + +vector encodecompactCLK(map& orbClkMap, int updateIntIndex, bool last); + +vector encodecompactURA(map& orbClkMap, int updateIntIndex, bool last); + +vector encodecompactCMB(map& orbClkMap, int updateIntIndex, bool last); + +vector +encodecompactCOD(map& codBiaMap, int updateIntIndex, bool last); + +vector +encodecompactPHS(map& phsBiaMap, int updateIntIndex, bool last); + +vector encodecompactBIA( + map& codBiaMap, + map& phsBiaMap, + int updateIntIndex, + bool last +); + +vector encodecompactTEC( + SSRMeta& ssrMeta, + int regId, + SSRAtmRegion& ssrAtmReg, + int updateIntIndex, + bool last +); + +vector encodecompactGRD( + SSRMeta& ssrMeta, + int regId, + SSRAtmRegion& ssrAtmReg, + int updateIntIndex, + bool last +); + +vector encodecompactATM( + SSRMeta& ssrMeta, + int regId, + SSRAtmRegion& ssrAtmReg, + int updateIntIndex, + bool last +); + +vector encodecompactSRV(SSRAtm& ssrAtm); + +unsigned short IGS_SSR_subtype(IgsSSRSubtype type, E_Sys sys); + +IgsSSRSubtype IGS_SSR_group(IgsSSRSubtype subType, E_Sys& sys); + +vector encodeIGS_ORB(map& orbClkMap, E_Sys sys, bool last); + +vector encodeIGS_CLK(map& orbClkMap, E_Sys sys, bool last); + +vector encodeIGS_CMB(map& orbClkMap, E_Sys sys, bool last); + +vector encodeIGS_HRC(map& orbClkMap, E_Sys sys, bool last); + +vector encodeIGS_COD(map& codBiaMap, E_Sys sys, bool last); + +vector encodeIGS_PHS(map& phsBiaMap, E_Sys sys, bool last); + +vector encodeIGS_URA(map& uraMap, E_Sys sys, bool last); + +vector encodeIGS_ATM(SSRAtm& ssrAtm, bool last); + +double checkPhaseDisc(SatSys Sat, E_ObsCode code, double bias, int disc, int regn); diff --git a/src/cpp/other_ssr/prototypeCmpSSRDecode.cpp b/src/cpp/other_ssr/prototypeCmpSSRDecode.cpp index bf8de5690..ae43ae934 100644 --- a/src/cpp/other_ssr/prototypeCmpSSRDecode.cpp +++ b/src/cpp/other_ssr/prototypeCmpSSRDecode.cpp @@ -1,1632 +1,1853 @@ +#include "common/rtcmDecoder.hpp" +#include "iono/ionoModel.hpp" +#include "other_ssr/otherSSR.hpp" -#include "rtcmDecoder.hpp" -#include "ionoModel.hpp" -#include "otherSSR.hpp" - -#define CMPSSRTRCLVL 2 +constexpr int CMPSSRTRCLVL = 2; struct PhaseDiscControl { - double bias = 0; - int disc = -1; - int intlevel = 0; + double bias = 0; + int disc = -1; + int intlevel = 0; }; -map>> compactSsrPhaseDisc; +map>> compactSsrPhaseDisc; -map> compactSsrSatelliteIndex; -map>> compactSsrSignalIndex; -map> compactSsrIonoIndex; +map> compactSsrSatelliteIndex; +map>> compactSsrSignalIndex; +map> compactSsrIonoIndex; -map> compactSsrStorage; -SSRAtm compactSsrAtmStorage; +map> compactSsrStorage; +SSRAtm compactSsrAtmStorage; -vector compactSsrServiceMessage; +vector compactSsrServiceMessage; bool ssrAtmUpdated = false; int lastServiceMessage = -1; -map> compactSSRIndex2Code -{ - { E_Sys::GPS, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1P}, - { 2, E_ObsCode::L1W}, - { 3, E_ObsCode::L1S}, - { 4, E_ObsCode::L1L}, - { 5, E_ObsCode::L1X}, - { 6, E_ObsCode::L2S}, - { 7, E_ObsCode::L2L}, - { 8, E_ObsCode::L2X}, - { 9, E_ObsCode::L2P}, - {10, E_ObsCode::L2W}, - {11, E_ObsCode::L5I}, - {12, E_ObsCode::L5Q}, - {13, E_ObsCode::L5X} - } - }, - { E_Sys::GLO, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1P}, - { 2, E_ObsCode::L2C}, - { 3, E_ObsCode::L2P}, - { 4, E_ObsCode::L4A}, - { 5, E_ObsCode::L4B}, - { 6, E_ObsCode::L4X}, - { 7, E_ObsCode::L6A}, - { 8, E_ObsCode::L6B}, - { 9, E_ObsCode::L6X}, - {10, E_ObsCode::L3I}, - {11, E_ObsCode::L3Q}, - {12, E_ObsCode::L3X} - } - }, - { E_Sys::GAL, - { - { 0, E_ObsCode::L1B}, - { 1, E_ObsCode::L1C}, - { 2, E_ObsCode::L1X}, - { 3, E_ObsCode::L5I}, - { 4, E_ObsCode::L5Q}, - { 5, E_ObsCode::L5X}, - { 6, E_ObsCode::L7I}, - { 7, E_ObsCode::L7Q}, - { 8, E_ObsCode::L7X}, - { 9, E_ObsCode::L8I}, - {10, E_ObsCode::L8Q}, - {11, E_ObsCode::L8X}, - {12, E_ObsCode::L6B}, - {13, E_ObsCode::L6C}, - {14, E_ObsCode::L6X} - } - }, - { E_Sys::QZS, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1S}, - { 2, E_ObsCode::L1L}, - { 3, E_ObsCode::L1X}, - { 4, E_ObsCode::L2S}, - { 5, E_ObsCode::L2L}, - { 6, E_ObsCode::L2X}, - { 7, E_ObsCode::L5I}, - { 8, E_ObsCode::L5Q}, - { 9, E_ObsCode::L5X}, - {10, E_ObsCode::L6S}, - {11, E_ObsCode::L6E}, - {12, E_ObsCode::L6X} - } - }, - { E_Sys::BDS, - { - { 0, E_ObsCode::L2I}, - { 1, E_ObsCode::L2Q}, - { 2, E_ObsCode::L2X}, - { 3, E_ObsCode::L6I}, - { 4, E_ObsCode::L6Q}, - { 5, E_ObsCode::L6X}, - { 6, E_ObsCode::L7I}, - { 7, E_ObsCode::L7Q}, - { 8, E_ObsCode::L7X} - } - } +map> compactSSRIndex2Code{ + {E_Sys::GPS, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1P}, + {2, E_ObsCode::L1W}, + {3, E_ObsCode::L1S}, + {4, E_ObsCode::L1L}, + {5, E_ObsCode::L1X}, + {6, E_ObsCode::L2S}, + {7, E_ObsCode::L2L}, + {8, E_ObsCode::L2X}, + {9, E_ObsCode::L2P}, + {10, E_ObsCode::L2W}, + {11, E_ObsCode::L5I}, + {12, E_ObsCode::L5Q}, + {13, E_ObsCode::L5X}}}, + {E_Sys::GLO, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1P}, + {2, E_ObsCode::L2C}, + {3, E_ObsCode::L2P}, + {4, E_ObsCode::L4A}, + {5, E_ObsCode::L4B}, + {6, E_ObsCode::L4X}, + {7, E_ObsCode::L6A}, + {8, E_ObsCode::L6B}, + {9, E_ObsCode::L6X}, + {10, E_ObsCode::L3I}, + {11, E_ObsCode::L3Q}, + {12, E_ObsCode::L3X}}}, + {E_Sys::GAL, + {{0, E_ObsCode::L1B}, + {1, E_ObsCode::L1C}, + {2, E_ObsCode::L1X}, + {3, E_ObsCode::L5I}, + {4, E_ObsCode::L5Q}, + {5, E_ObsCode::L5X}, + {6, E_ObsCode::L7I}, + {7, E_ObsCode::L7Q}, + {8, E_ObsCode::L7X}, + {9, E_ObsCode::L8I}, + {10, E_ObsCode::L8Q}, + {11, E_ObsCode::L8X}, + {12, E_ObsCode::L6B}, + {13, E_ObsCode::L6C}, + {14, E_ObsCode::L6X}}}, + {E_Sys::QZS, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1S}, + {2, E_ObsCode::L1L}, + {3, E_ObsCode::L1X}, + {4, E_ObsCode::L2S}, + {5, E_ObsCode::L2L}, + {6, E_ObsCode::L2X}, + {7, E_ObsCode::L5I}, + {8, E_ObsCode::L5Q}, + {9, E_ObsCode::L5X}, + {10, E_ObsCode::L6S}, + {11, E_ObsCode::L6E}, + {12, E_ObsCode::L6X}}}, + {E_Sys::BDS, + {{0, E_ObsCode::L2I}, + {1, E_ObsCode::L2Q}, + {2, E_ObsCode::L2X}, + {3, E_ObsCode::L6I}, + {4, E_ObsCode::L6Q}, + {5, E_ObsCode::L6X}, + {6, E_ObsCode::L7I}, + {7, E_ObsCode::L7Q}, + {8, E_ObsCode::L7X}}} }; -int compactSSRIod = -1; +int compactSSRIod = -1; GTime compactSsrMaskTime; GTime compactSsrLastTime; -double decodeCmpSsrField( - vector& data, - int& i, - int bitLen, - double scale, - double offset) +double +decodeCmpSsrField(vector& data, int& i, int bitLen, double scale, double offset) { - int tmp = getbitsInc(data, i,bitLen); + int tmp = getbitsInc(data, i, bitLen); - if (tmp == -pow(2,bitLen-1)) - return SSR_UNAVAILABLE; + if (tmp == -pow(2, bitLen - 1)) + return SSR_UNAVAILABLE; - return scale * tmp + offset; + return scale * tmp + offset; } -double checkDisc( - SatSys Sat, - E_ObsCode code, - double bias, - int disc, - int regionID = -1) +double checkDisc(SatSys Sat, E_ObsCode code, double bias, int disc, int regionID = -1) { - bool newBias = false; - - if (compactSsrPhaseDisc.find(regionID) == compactSsrPhaseDisc.end()) newBias = true; - else if (compactSsrPhaseDisc[regionID].find(Sat) == compactSsrPhaseDisc[regionID].end()) newBias = true; - else if (compactSsrPhaseDisc[regionID][Sat].find(code) == compactSsrPhaseDisc[regionID][Sat].end()) newBias = true; - - auto& storedDisc = compactSsrPhaseDisc[regionID][Sat][code]; - - if (newBias) - { - storedDisc.bias = bias; - storedDisc.disc = disc; - storedDisc.intlevel = 0; - return bias; - } - - double lam = genericWavelength[code2Freq[Sat.sys][code]]; - - if (storedDisc.disc != disc) - { - int dlam = (int) round((bias - storedDisc.bias) / lam); - storedDisc.intlevel += dlam; - storedDisc.disc = disc; - } - - storedDisc.bias = bias; - - return bias + lam * storedDisc.intlevel; + bool newBias = false; + + if (compactSsrPhaseDisc.find(regionID) == compactSsrPhaseDisc.end()) + newBias = true; + else if (compactSsrPhaseDisc[regionID].find(Sat) == compactSsrPhaseDisc[regionID].end()) + newBias = true; + else if (compactSsrPhaseDisc[regionID][Sat].find(code) == + compactSsrPhaseDisc[regionID][Sat].end()) + newBias = true; + + auto& storedDisc = compactSsrPhaseDisc[regionID][Sat][code]; + + if (newBias) + { + storedDisc.bias = bias; + storedDisc.disc = disc; + storedDisc.intlevel = 0; + return bias; + } + + double lam = genericWavelength[code2Freq[Sat.sys][code]]; + + if (storedDisc.disc != disc) + { + int dlam = (int)round((bias - storedDisc.bias) / lam); + storedDisc.intlevel += dlam; + storedDisc.disc = disc; + } + + storedDisc.bias = bias; + + return bias + lam * storedDisc.intlevel; } -void copySSRBlock ( - SatSys Sat, - SSROut ssrBlock) +void copySSRBlock(SatSys Sat, SSROut ssrBlock) { - auto& ssr = nav.satNavMap[Sat].receivedSSR; - - if (ssrBlock.ephUpdated) - { - GTime tEph = ssrBlock.ssrEph.ssrMeta.receivedTime; - if (ssr.ssrEph_map.find(tEph) == ssr.ssrEph_map.end()) - { - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n#CMP_SSR ORBITS %s %s %4d %2d %10.4f %10.4f %10.4f", - Sat.id().c_str(), - tEph.to_string().c_str(), - ssrBlock.ssrEph.iode, - ssrBlock.ssrEph.iod, - ssrBlock.ssrEph.deph[0], - ssrBlock.ssrEph.deph[1], - ssrBlock.ssrEph.deph[2]); - - if ( ssrBlock.ssrEph.deph[0] == SSR_UNAVAILABLE - || ssrBlock.ssrEph.deph[1] == SSR_UNAVAILABLE - || ssrBlock.ssrEph.deph[2] == SSR_UNAVAILABLE) - { - ssr.ssrEph_map.clear(); - } - else - ssr.ssrEph_map[tEph] = ssrBlock.ssrEph; - - ssrBlock.ephUpdated = false; - } - } - - if (ssrBlock.clkUpdated) - { - GTime tClk = ssrBlock.ssrClk.ssrMeta.receivedTime; - if (ssr.ssrClk_map.find(tClk) == ssr.ssrClk_map.end()) - { - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n#CMP_SSR CLOCKS %s %s %2d %10.4f", - Sat.id().c_str(), - tClk.to_string().c_str(), - ssrBlock.ssrClk.iod, - ssrBlock.ssrClk.dclk[0]); - - if (fabs(ssrBlock.ssrClk.dclk[0]) > 9000) - ssr.ssrClk_map.clear(); - else - ssr.ssrClk_map[tClk] = ssrBlock.ssrClk; - - ssrBlock.clkUpdated = false; - } - } - if (ssrBlock.codeUpdated) - { - BiasEntry entry; - string id = Sat.id() + ":" + Sat.sysChar(); - entry.measType = CODE; - entry.Sat = Sat; - entry.tini = ssrBlock.ssrCodeBias.t0 - 0.5*ssrBlock.ssrCodeBias.udi; - entry.tfin = entry.tini + acsConfig.ssrInOpts.code_bias_valid_time; - entry.source = "ssr"; - - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n#CMP_SSR CODBIA %s %s: ", Sat.id().c_str(),entry.tini.to_string().c_str()); - - for (auto& [code,biasSSR] : ssrBlock.ssrCodeBias.obsCodeBiasMap) - { - entry.cod1 = code; - entry.cod2 = E_ObsCode::NONE; - entry.bias = -biasSSR.bias; - entry.var = 0; - entry.slop = 0; - entry.slpv = 0; - - pushBiasEntry(id, entry); - tracepdeex(CMPSSRTRCLVL+1,std::cout, "%s %9.4f; ", code._to_string(), biasSSR.bias); - } - - if ( ssr.ssrCodeBias_map.find(entry.tini) == ssr.ssrCodeBias_map.end()) - { - ssr.ssrCodeBias_map[entry.tini] = ssrBlock.ssrCodeBias; - } - ssrBlock.codeUpdated = false; - } - - if (ssrBlock.phaseUpdated) - { - BiasEntry entry; - string id = Sat.id() + ":" + Sat.sysChar(); - entry.measType = PHAS; - entry.Sat = Sat; - entry.tini = ssrBlock.ssrPhasBias.t0 - 0.5*ssrBlock.ssrPhasBias.udi; - entry.tfin = entry.tini + acsConfig.ssrInOpts.phase_bias_valid_time; - entry.source = "ssr"; - - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n#CMP_SSR PHSBIA %s %s: ", Sat.id().c_str(),entry.tini.to_string().c_str()); - - for(auto& [code,biasSSR] : ssrBlock.ssrPhasBias.obsCodeBiasMap) - { - entry.cod1 = code; - entry.cod2 = E_ObsCode::NONE; - entry.bias = -biasSSR.bias; - entry.var = 0; - entry.slop = 0; - entry.slpv = 0; - - tracepdeex(CMPSSRTRCLVL+1,std::cout, "%s %9.4f; ", code._to_string(), biasSSR.bias); - pushBiasEntry(id, entry); - } - - if ( ssr.ssrPhasBias_map.find(entry.tini) == ssr.ssrPhasBias_map.end()) - { - ssr.ssrPhasBias_map[entry.tini] = ssrBlock.ssrPhasBias; - } - ssrBlock.phaseUpdated = false; - } - - if (ssrBlock.uraUpdated) - if ( ssr.ssrUra_map.find(ssrBlock.ssrUra.t0) == ssr.ssrUra_map.end() ) - { - ssr.ssrUra_map[ssrBlock.ssrUra.t0] = ssrBlock.ssrUra; - - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n#CMP_SSR URA %s %s %.4f", - Sat.id().c_str(), - ssrBlock.ssrUra.t0.to_string().c_str(), - uraSsr[ssrBlock.ssrUra.ura] / 1000); - - ssrBlock.uraUpdated = false; - } + auto& ssr = nav.satNavMap[Sat].receivedSSR; + + if (ssrBlock.ephUpdated) + { + GTime tEph = ssrBlock.ssrEph.ssrMeta.receivedTime; + if (ssr.ssrEph_map.find(tEph) == ssr.ssrEph_map.end()) + { + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "\n#CMP_SSR ORBITS %s %s %4d %2d %10.4f %10.4f %10.4f", + Sat.id().c_str(), + tEph.to_string().c_str(), + ssrBlock.ssrEph.iode, + ssrBlock.ssrEph.iod, + ssrBlock.ssrEph.deph[0], + ssrBlock.ssrEph.deph[1], + ssrBlock.ssrEph.deph[2] + ); + + if (ssrBlock.ssrEph.deph[0] == SSR_UNAVAILABLE || + ssrBlock.ssrEph.deph[1] == SSR_UNAVAILABLE || + ssrBlock.ssrEph.deph[2] == SSR_UNAVAILABLE) + { + ssr.ssrEph_map.clear(); + } + else + ssr.ssrEph_map[tEph] = ssrBlock.ssrEph; + + ssrBlock.ephUpdated = false; + } + } + + if (ssrBlock.clkUpdated) + { + GTime tClk = ssrBlock.ssrClk.ssrMeta.receivedTime; + if (ssr.ssrClk_map.find(tClk) == ssr.ssrClk_map.end()) + { + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "\n#CMP_SSR CLOCKS %s %s %2d %10.4f", + Sat.id().c_str(), + tClk.to_string().c_str(), + ssrBlock.ssrClk.iod, + ssrBlock.ssrClk.dclk[0] + ); + + if (fabs(ssrBlock.ssrClk.dclk[0]) > 9000) + ssr.ssrClk_map.clear(); + else + ssr.ssrClk_map[tClk] = ssrBlock.ssrClk; + + ssrBlock.clkUpdated = false; + } + } + if (ssrBlock.codeUpdated) + { + BiasEntry entry; + string id = Sat.id() + ":" + Sat.sysChar(); + entry.measType = CODE; + entry.Sat = Sat; + entry.tini = ssrBlock.ssrCodeBias.t0 - 0.5 * ssrBlock.ssrCodeBias.udi; + entry.tfin = entry.tini + acsConfig.ssrInOpts.code_bias_valid_time; + entry.source = "ssr"; + + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "\n#CMP_SSR CODBIA %s %s: ", + Sat.id().c_str(), + entry.tini.to_string().c_str() + ); + + for (auto& [code, biasSSR] : ssrBlock.ssrCodeBias.obsCodeBiasMap) + { + entry.cod1 = code; + entry.cod2 = E_ObsCode::NONE; + entry.bias = -biasSSR.bias; + entry.var = 0; + entry.slop = 0; + entry.slpv = 0; + + updateRefTime(entry); + pushBiasEntry(id, entry); + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "%s %9.4f; ", + enum_to_string(code), + biasSSR.bias + ); + } + + if (ssr.ssrCodeBias_map.find(entry.tini) == ssr.ssrCodeBias_map.end()) + { + ssr.ssrCodeBias_map[entry.tini] = ssrBlock.ssrCodeBias; + } + ssrBlock.codeUpdated = false; + } + + if (ssrBlock.phaseUpdated) + { + BiasEntry entry; + string id = Sat.id() + ":" + Sat.sysChar(); + entry.measType = PHAS; + entry.Sat = Sat; + entry.tini = ssrBlock.ssrPhasBias.t0 - 0.5 * ssrBlock.ssrPhasBias.udi; + entry.tfin = entry.tini + acsConfig.ssrInOpts.phase_bias_valid_time; + entry.source = "ssr"; + + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "\n#CMP_SSR PHSBIA %s %s: ", + Sat.id().c_str(), + entry.tini.to_string().c_str() + ); + + for (auto& [code, biasSSR] : ssrBlock.ssrPhasBias.obsCodeBiasMap) + { + entry.cod1 = code; + entry.cod2 = E_ObsCode::NONE; + entry.bias = -biasSSR.bias; + entry.var = 0; + entry.slop = 0; + entry.slpv = 0; + + updateRefTime(entry); + pushBiasEntry(id, entry); + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "%s %9.4f; ", + enum_to_string(code), + biasSSR.bias + ); + } + + if (ssr.ssrPhasBias_map.find(entry.tini) == ssr.ssrPhasBias_map.end()) + { + ssr.ssrPhasBias_map[entry.tini] = ssrBlock.ssrPhasBias; + } + ssrBlock.phaseUpdated = false; + } + + if (ssrBlock.uraUpdated) + if (ssr.ssrUra_map.find(ssrBlock.ssrUra.t0) == ssr.ssrUra_map.end()) + { + ssr.ssrUra_map[ssrBlock.ssrUra.t0] = ssrBlock.ssrUra; + + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "\n#CMP_SSR URA %s %s %.4f", + Sat.id().c_str(), + ssrBlock.ssrUra.t0.to_string().c_str(), + uraSsr[ssrBlock.ssrUra.ura] / 1000 + ); + + ssrBlock.uraUpdated = false; + } } -void copySSRCorrections( - int regID) +void copySSRCorrections(int regID) { - if (regID == -1 - || acsConfig.ssrOpts.region_id == regID) - for (auto& [Sat, ssrBlock] : compactSsrStorage[regID]) - copySSRBlock (Sat, ssrBlock); - - if (ssrAtmUpdated) - { - GTime tAtm = compactSsrAtmStorage.ssrMeta.receivedTime; - for (auto& [regInd,regData] : compactSsrAtmStorage.atmosRegionsMap) - { - auto& navAtm = nav.ssrAtm.atmosRegionsMap[regInd]; - - if (regData.regionDefIOD > 0 - && navAtm.regionDefIOD != regData.regionDefIOD) - { - navAtm.regionDefIOD = regData.regionDefIOD; - navAtm.minLatDeg = regData.minLatDeg; - navAtm.maxLatDeg = regData.maxLatDeg; - navAtm.intLatDeg = regData.intLatDeg; - navAtm.minLonDeg = regData.minLonDeg; - navAtm.maxLonDeg = regData.maxLonDeg; - navAtm.intLonDeg = regData.intLonDeg; - navAtm.gridType = regData.gridType; - navAtm.ionoGrid = regData.ionoGrid; - navAtm.tropGrid = regData.tropGrid; - navAtm.tropPolySize = regData.tropPolySize; - navAtm.ionoPolySize = regData.ionoPolySize; - navAtm.gridLatDeg.clear(); - navAtm.gridLonDeg.clear(); - - for (auto& [ind, latDeg] : regData.gridLatDeg) - { - navAtm.gridLatDeg[ind] = latDeg; - navAtm.gridLonDeg[ind] = regData.gridLonDeg[ind]; - } - } - - if (regData.tropData.find(tAtm) != regData.tropData.end()) - { - navAtm.tropData[tAtm] = regData.tropData[tAtm]; - if (regInd == acsConfig.ssrOpts.region_id) - { - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n#CMP_SSR TRP %s %.4f\n Poly:", tAtm.to_string().c_str(), regData.tropData[tAtm].sigma); - for (auto& [ind,val] : regData.tropData[tAtm].polyDry) tracepdeex(CMPSSRTRCLVL+1,std::cout, " %.4f", val); - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n Grid:"); - for (auto& [ind,val] : regData.tropData[tAtm].gridWet) tracepdeex(CMPSSRTRCLVL+1,std::cout, " %.4f", val); - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n"); - } - } - - for (auto& [Sat, stecMap] : regData.stecData) - if (stecMap.find(tAtm) != stecMap.end()) - { - navAtm.stecData[Sat][tAtm] = stecMap[tAtm]; - if (regInd == acsConfig.ssrOpts.region_id) - { - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n#CMP_SSR ION %s %s %.4f\n Poly:",Sat.id().c_str(), tAtm.to_string().c_str(), stecMap[tAtm].sigma); - for (auto& [ind,val] : stecMap[tAtm].poly) tracepdeex(CMPSSRTRCLVL+1,std::cout, " %.4f", val); - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n Grid:"); - for (auto& [ind,val] : stecMap[tAtm].grid) tracepdeex(CMPSSRTRCLVL+1,std::cout, " %.4f", val); - tracepdeex(CMPSSRTRCLVL+1,std::cout, "\n"); - } - - } - } - ssrAtmUpdated = false; - } - - compactSsrStorage.clear(); + if (regID == -1 || acsConfig.ssrOpts.region_id == regID) + for (auto& [Sat, ssrBlock] : compactSsrStorage[regID]) + copySSRBlock(Sat, ssrBlock); + + if (ssrAtmUpdated) + { + GTime tAtm = compactSsrAtmStorage.ssrMeta.receivedTime; + for (auto& [regInd, regData] : compactSsrAtmStorage.atmosRegionsMap) + { + auto& navAtm = nav.ssrAtm.atmosRegionsMap[regInd]; + + if (regData.regionDefIOD > 0 && navAtm.regionDefIOD != regData.regionDefIOD) + { + navAtm.regionDefIOD = regData.regionDefIOD; + navAtm.minLatDeg = regData.minLatDeg; + navAtm.maxLatDeg = regData.maxLatDeg; + navAtm.intLatDeg = regData.intLatDeg; + navAtm.minLonDeg = regData.minLonDeg; + navAtm.maxLonDeg = regData.maxLonDeg; + navAtm.intLonDeg = regData.intLonDeg; + navAtm.gridType = regData.gridType; + navAtm.ionoGrid = regData.ionoGrid; + navAtm.tropGrid = regData.tropGrid; + navAtm.tropPolySize = regData.tropPolySize; + navAtm.ionoPolySize = regData.ionoPolySize; + navAtm.gridLatDeg.clear(); + navAtm.gridLonDeg.clear(); + + for (auto& [ind, latDeg] : regData.gridLatDeg) + { + navAtm.gridLatDeg[ind] = latDeg; + navAtm.gridLonDeg[ind] = regData.gridLonDeg[ind]; + } + } + + if (regData.tropData.find(tAtm) != regData.tropData.end()) + { + navAtm.tropData[tAtm] = regData.tropData[tAtm]; + if (regInd == acsConfig.ssrOpts.region_id) + { + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "\n#CMP_SSR TRP %s %.4f\n Poly:", + tAtm.to_string().c_str(), + regData.tropData[tAtm].sigma + ); + for (auto& [ind, val] : regData.tropData[tAtm].polyDry) + tracepdeex(CMPSSRTRCLVL + 1, std::cout, " %.4f", val); + tracepdeex(CMPSSRTRCLVL + 1, std::cout, "\n Grid:"); + for (auto& [ind, val] : regData.tropData[tAtm].gridWet) + tracepdeex(CMPSSRTRCLVL + 1, std::cout, " %.4f", val); + tracepdeex(CMPSSRTRCLVL + 1, std::cout, "\n"); + } + } + + for (auto& [Sat, stecMap] : regData.stecData) + if (stecMap.find(tAtm) != stecMap.end()) + { + navAtm.stecData[Sat][tAtm] = stecMap[tAtm]; + if (regInd == acsConfig.ssrOpts.region_id) + { + tracepdeex( + CMPSSRTRCLVL + 1, + std::cout, + "\n#CMP_SSR ION %s %s %.4f\n Poly:", + Sat.id().c_str(), + tAtm.to_string().c_str(), + stecMap[tAtm].sigma + ); + for (auto& [ind, val] : stecMap[tAtm].poly) + tracepdeex(CMPSSRTRCLVL + 1, std::cout, " %.4f", val); + tracepdeex(CMPSSRTRCLVL + 1, std::cout, "\n Grid:"); + for (auto& [ind, val] : stecMap[tAtm].grid) + tracepdeex(CMPSSRTRCLVL + 1, std::cout, " %.4f", val); + tracepdeex(CMPSSRTRCLVL + 1, std::cout, "\n"); + } + } + } + ssrAtmUpdated = false; + } + + compactSsrStorage.clear(); } -GTime toh2time( - GTime close, - double toh) +GTime toh2time(GTime close, double toh) { - double tow = GTow(close); - double dtoh = toh - (tow - 3600.0 * floor(tow/3600)); + double tow = GTow(close); + double dtoh = toh - (tow - 3600.0 * floor(tow / 3600)); - if (dtoh < -1800) dtoh += 3600; - else if (dtoh > 1800) dtoh -= 3600; + if (dtoh < -1800) + dtoh += 3600; + else if (dtoh > 1800) + dtoh -= 3600; - return close + dtoh; + return close + dtoh; } -void decodeGridInfo( - vector& data) +void decodeGridInfo(vector& data) { - int i=4; - int servIOD = getbituInc(data,i,3); - int areaIOD = getbituInc(data,i,4); - int numNet = getbituInc(data,i,6)+1; - - for (int n = 0; n < numNet; n++) - { - int netw_ID = getbituInc(data,i,5); - bool part = getbituInc(data,i,1); - int partID = netw_ID; - if (part) - partID += 32*getbituInc(data,i,4); - int gridType = getbituInc(data,i,2); - - SSRAtmRegion& atmRegion = nav.ssrAtm.atmosRegionsMap[partID]; - - if (atmRegion.regionDefIOD != servIOD) - { - atmRegion.gridLatDeg .clear(); - atmRegion.gridLonDeg .clear(); - atmRegion.tropData .clear(); - atmRegion.stecData .clear(); - - atmRegion.regionDefIOD = servIOD; - } - double radscale = PI * P2_15; - switch (gridType) - { - case 0: - { - double thisLatDeg = getbitsInc(data,i,15) * radscale * R2D; - double thisLonDeg = getbitsInc(data,i,16) * radscale * R2D; - - atmRegion.gridLatDeg[0]= thisLatDeg; - atmRegion.gridLonDeg[0]= thisLonDeg; - int ngrid = getbituInc(data,i, 6); - - atmRegion.minLatDeg = thisLatDeg - 0.1; - atmRegion.minLonDeg = thisLonDeg - 0.1; - atmRegion.maxLatDeg = thisLatDeg + 0.1; - atmRegion.maxLonDeg = thisLonDeg + 0.1; - - for (int grd = 0; grd < ngrid; grd++) - { - thisLatDeg += getbitsInc(data,i,10)*0.01 * R2D; - thisLatDeg += getbitsInc(data,i,11)*0.01 * R2D; - - if (atmRegion.minLatDeg > (thisLatDeg)) atmRegion.minLatDeg = thisLatDeg; - if (atmRegion.maxLatDeg < (thisLatDeg)) atmRegion.maxLatDeg = thisLatDeg; - if (atmRegion.minLonDeg > (thisLonDeg)) atmRegion.minLonDeg = thisLonDeg; - if (atmRegion.maxLonDeg < (thisLonDeg)) atmRegion.maxLonDeg = thisLonDeg; - - atmRegion.gridLatDeg[grd] = thisLatDeg < -180 ? (thisLatDeg + 360) : (thisLatDeg > 180 ? (thisLatDeg - 360) : thisLatDeg); - atmRegion.gridLonDeg[grd] = thisLonDeg < -180 ? (thisLonDeg + 360) : (thisLonDeg > 180 ? (thisLonDeg - 360) : thisLonDeg); - } - double dLat = atmRegion.maxLatDeg - atmRegion.minLatDeg; - double dLon = atmRegion.maxLonDeg - atmRegion.minLonDeg; - - double nLat = sqrt(dLat * ngrid / dLon); - double nLon = ngrid / nLat; - - atmRegion.intLatDeg = dLat / nLat; - atmRegion.intLonDeg = dLon / nLon; - - break; - } - case 1: - { - atmRegion.gridLatDeg[0] = getbitsInc(data,i,15) * radscale * R2D; - atmRegion.gridLonDeg[0] = getbitsInc(data,i,16) * radscale * R2D; - int ngridLat = getbituInc(data,i, 6); - int ngridLon = getbituInc(data,i, 6); - - - atmRegion.intLatDeg = getbituInc(data,i, 9) * 0.01 * R2D; - atmRegion.intLonDeg = getbituInc(data,i,10) * 0.01 * R2D; - atmRegion.minLatDeg = atmRegion.gridLatDeg[0] - ngridLat * atmRegion.intLatDeg; - atmRegion.maxLatDeg = atmRegion.gridLatDeg[0]; - atmRegion.minLonDeg = atmRegion.gridLonDeg[0]; - atmRegion.maxLatDeg = atmRegion.gridLonDeg[0] + ngridLon * atmRegion.intLonDeg; - - for (int nlat = 0, grd = 0; nlat < ngridLat; nlat++) - for (int nlon = 0; nlon < ngridLon; nlon++) - { - atmRegion.gridLatDeg[grd] = atmRegion.gridLatDeg[0] - nlat * atmRegion.intLatDeg; - atmRegion.gridLonDeg[grd] = atmRegion.gridLonDeg[0] + nlon * atmRegion.intLonDeg; - grd++; - } - break; - } - case 2: - { - atmRegion.gridLatDeg[0] = getbitsInc(data,i,15) * radscale * R2D; - atmRegion.gridLonDeg[0] = getbitsInc(data,i,16) * radscale * R2D; - int ngridLat = getbituInc(data,i, 6); - int ngridLon = getbituInc(data,i, 6); - - atmRegion.intLatDeg = getbituInc(data,i, 9) * 0.01 * R2D; - atmRegion.intLonDeg = getbituInc(data,i,10) * 0.01 * R2D; - atmRegion.minLatDeg = atmRegion.gridLatDeg[0]; - atmRegion.maxLatDeg = atmRegion.gridLatDeg[0] + ngridLat * atmRegion.intLatDeg; - atmRegion.minLonDeg = atmRegion.gridLonDeg[0]; - atmRegion.maxLatDeg = atmRegion.gridLonDeg[0] + ngridLon * atmRegion.intLonDeg; - - for (int nlon = 0, grd = 0; nlon < ngridLon; nlon++) - for (int nlat = 0; nlat < ngridLat; nlat++) - { - atmRegion.gridLatDeg[grd] = atmRegion.gridLatDeg[0] + nlat * atmRegion.intLatDeg; - atmRegion.gridLonDeg[grd] = atmRegion.gridLonDeg[0] + nlon * atmRegion.intLonDeg; - grd++; - } - break; - } - } - - } + int i = 4; + int servIOD = getbituInc(data, i, 3); + int areaIOD = getbituInc(data, i, 4); + int numNet = getbituInc(data, i, 6) + 1; + + for (int n = 0; n < numNet; n++) + { + int netw_ID = getbituInc(data, i, 5); + bool part = getbituInc(data, i, 1); + int partID = netw_ID; + if (part) + partID += 32 * getbituInc(data, i, 4); + int gridType = getbituInc(data, i, 2); + + SSRAtmRegion& atmRegion = nav.ssrAtm.atmosRegionsMap[partID]; + + if (atmRegion.regionDefIOD != servIOD) + { + atmRegion.gridLatDeg.clear(); + atmRegion.gridLonDeg.clear(); + atmRegion.tropData.clear(); + atmRegion.stecData.clear(); + + atmRegion.regionDefIOD = servIOD; + } + double radscale = PI * P2_15; + switch (gridType) + { + case 0: + { + double thisLatDeg = getbitsInc(data, i, 15) * radscale * R2D; + double thisLonDeg = getbitsInc(data, i, 16) * radscale * R2D; + + atmRegion.gridLatDeg[0] = thisLatDeg; + atmRegion.gridLonDeg[0] = thisLonDeg; + int ngrid = getbituInc(data, i, 6); + + atmRegion.minLatDeg = thisLatDeg - 0.1; + atmRegion.minLonDeg = thisLonDeg - 0.1; + atmRegion.maxLatDeg = thisLatDeg + 0.1; + atmRegion.maxLonDeg = thisLonDeg + 0.1; + + for (int grd = 0; grd < ngrid; grd++) + { + thisLatDeg += getbitsInc(data, i, 10) * 0.01 * R2D; + thisLatDeg += getbitsInc(data, i, 11) * 0.01 * R2D; + + if (atmRegion.minLatDeg > (thisLatDeg)) + atmRegion.minLatDeg = thisLatDeg; + if (atmRegion.maxLatDeg < (thisLatDeg)) + atmRegion.maxLatDeg = thisLatDeg; + if (atmRegion.minLonDeg > (thisLonDeg)) + atmRegion.minLonDeg = thisLonDeg; + if (atmRegion.maxLonDeg < (thisLonDeg)) + atmRegion.maxLonDeg = thisLonDeg; + + atmRegion.gridLatDeg[grd] = + thisLatDeg < -180 ? (thisLatDeg + 360) + : (thisLatDeg > 180 ? (thisLatDeg - 360) : thisLatDeg); + atmRegion.gridLonDeg[grd] = + thisLonDeg < -180 ? (thisLonDeg + 360) + : (thisLonDeg > 180 ? (thisLonDeg - 360) : thisLonDeg); + } + double dLat = atmRegion.maxLatDeg - atmRegion.minLatDeg; + double dLon = atmRegion.maxLonDeg - atmRegion.minLonDeg; + + double nLat = sqrt(dLat * ngrid / dLon); + double nLon = ngrid / nLat; + + atmRegion.intLatDeg = dLat / nLat; + atmRegion.intLonDeg = dLon / nLon; + + break; + } + case 1: + { + atmRegion.gridLatDeg[0] = getbitsInc(data, i, 15) * radscale * R2D; + atmRegion.gridLonDeg[0] = getbitsInc(data, i, 16) * radscale * R2D; + int ngridLat = getbituInc(data, i, 6); + int ngridLon = getbituInc(data, i, 6); + + atmRegion.intLatDeg = getbituInc(data, i, 9) * 0.01 * R2D; + atmRegion.intLonDeg = getbituInc(data, i, 10) * 0.01 * R2D; + atmRegion.minLatDeg = atmRegion.gridLatDeg[0] - ngridLat * atmRegion.intLatDeg; + atmRegion.maxLatDeg = atmRegion.gridLatDeg[0]; + atmRegion.minLonDeg = atmRegion.gridLonDeg[0]; + atmRegion.maxLatDeg = atmRegion.gridLonDeg[0] + ngridLon * atmRegion.intLonDeg; + + for (int nlat = 0, grd = 0; nlat < ngridLat; nlat++) + for (int nlon = 0; nlon < ngridLon; nlon++) + { + atmRegion.gridLatDeg[grd] = + atmRegion.gridLatDeg[0] - nlat * atmRegion.intLatDeg; + atmRegion.gridLonDeg[grd] = + atmRegion.gridLonDeg[0] + nlon * atmRegion.intLonDeg; + grd++; + } + break; + } + case 2: + { + atmRegion.gridLatDeg[0] = getbitsInc(data, i, 15) * radscale * R2D; + atmRegion.gridLonDeg[0] = getbitsInc(data, i, 16) * radscale * R2D; + int ngridLat = getbituInc(data, i, 6); + int ngridLon = getbituInc(data, i, 6); + + atmRegion.intLatDeg = getbituInc(data, i, 9) * 0.01 * R2D; + atmRegion.intLonDeg = getbituInc(data, i, 10) * 0.01 * R2D; + atmRegion.minLatDeg = atmRegion.gridLatDeg[0]; + atmRegion.maxLatDeg = atmRegion.gridLatDeg[0] + ngridLat * atmRegion.intLatDeg; + atmRegion.minLonDeg = atmRegion.gridLonDeg[0]; + atmRegion.maxLatDeg = atmRegion.gridLonDeg[0] + ngridLon * atmRegion.intLonDeg; + + for (int nlon = 0, grd = 0; nlon < ngridLon; nlon++) + for (int nlat = 0; nlat < ngridLat; nlat++) + { + atmRegion.gridLatDeg[grd] = + atmRegion.gridLatDeg[0] + nlat * atmRegion.intLatDeg; + atmRegion.gridLonDeg[grd] = + atmRegion.gridLonDeg[0] + nlon * atmRegion.intLonDeg; + grd++; + } + break; + } + } + } } -void processServiceData( - vector& data) +void processServiceData(vector& data) { - int srvMessageType = getbitu(data,0,4); - switch (srvMessageType) - { - case 3: decodeGridInfo(data); break; - default: tracepdeex(4, std::cout,"Unsupported compact SSR service message\n"); - } + int srvMessageType = getbitu(data, 0, 4); + switch (srvMessageType) + { + case 3: + decodeGridInfo(data); + break; + default: + tracepdeex(4, std::cout, "Unsupported compact SSR service message\n"); + } } - -int decodeSSR_header( - vector& data, - SSRMeta& ssrMeta, - bool mask) +int decodeSSR_header(vector& data, SSRMeta& ssrMeta, bool mask) { - int i = 16; - if (mask) - ssrMeta.epochTime1s = getbituInc(data,i,20); - else - { - ssrMeta.epochTime1s = getbituInc(data,i,12); - ssrMeta.receivedTime = toh2time(compactSsrMaskTime, ssrMeta.epochTime1s); - } - ssrMeta.updateIntIndex = getbituInc(data,i, 4); - ssrMeta.multipleMessage = getbituInc(data,i, 1); - int ssrIod = getbituInc(data,i, 4); - - if (mask) - compactSSRIod = ssrIod; - else if (ssrIod != compactSSRIod) - return E_ReturnType::BAD_LENGTH; - - return i; + int i = 16; + if (mask) + ssrMeta.epochTime1s = getbituInc(data, i, 20); + else + { + ssrMeta.epochTime1s = getbituInc(data, i, 12); + ssrMeta.receivedTime = toh2time(compactSsrMaskTime, ssrMeta.epochTime1s); + } + ssrMeta.updateIntIndex = getbituInc(data, i, 4); + ssrMeta.multipleMessage = getbituInc(data, i, 1); + int ssrIod = getbituInc(data, i, 4); + + if (mask) + compactSSRIod = ssrIod; + else if (ssrIod != compactSSRIod) + return E_ReturnType::BAD_LENGTH; + + return i; } /** * Compact SSR message type 1: satellite/code mask */ -int decodeSSR_mask( - vector& data, - GTime now) +int decodeSSR_mask(vector& data, GTime now) { - compactSsrStorage .clear(); - compactSsrSatelliteIndex.clear(); - compactSsrSignalIndex .clear(); - - for (auto& [regId, regData] : compactSsrAtmStorage.atmosRegionsMap) - { - regData.tropData.clear(); - regData.stecData.clear(); - } - - int bitLen = data.size() * 8; - - - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, true); - - compactSsrLastTime = GTime(GTow(ssrMeta.epochTime1s), now); - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - compactSsrMaskTime = compactSsrLastTime; - - int nsys = getbituInc(data,i,4); - - for (int s = 0, satind = 0, cellind = 0; s < nsys; s++) - { - if ((i + 61) > bitLen) - { - compactSSRIod = -1; - return E_ReturnType::BAD_LENGTH; - } - - int sysID = getbituInc(data,i,4); - E_Sys sys; - switch (sysID) - { - case 0: sys = E_Sys::GPS; break; - case 1: sys = E_Sys::GLO; break; - case 2: sys = E_Sys::GAL; break; - case 3: sys = E_Sys::BDS; break; - case 4: sys = E_Sys::QZS; break; - case 5: sys = E_Sys::SBS; break; - default: - tracepdeex(2, std::cout,"Warning unsupported GNSS for compact SSR\n"); - compactSSRIod = -1; - return 0; - } - - int nsat = 0; - map sysSatIndex; - for (int n=1; n<41; n++) - { - SatSys Sat(sys,n); - if (getbituInc(data,i,1)==1) - { - compactSsrSatelliteIndex[-1][satind++] = Sat; - sysSatIndex[nsat++] = Sat; - } - } - - int ncod = 0; - map sysCodeIndex; - for (int n=0; n<16; n++) - { - if (getbituInc(data,i,1) == 1 - && compactSSRIndex2Code[sys].find(n) != compactSSRIndex2Code[sys].end()) - { - sysCodeIndex[ncod++] = compactSSRIndex2Code[sys][n]; - } - } - - bool cellField = false; - if (getbituInc(data,i,1) == 1) - { - if (nsat*ncod >128) - { - tracepdeex(2, std::cout,"Warning cell size for compact SSR execeds maximum size\n"); - compactSSRIod = -1; - return 0; - } - cellField = true; - if ((i+nsat*ncod) > bitLen) - return E_ReturnType::BAD_LENGTH; - } - - for (auto& [sind,Sat] : sysSatIndex) - for (auto& [cind,code] : sysCodeIndex) - { - if ( cellField - && getbituInc(data,i,1)==0) - { - continue; - } - - compactSsrSignalIndex[-1][cellind].first = Sat; - compactSsrSignalIndex[-1][cellind].second = code; - cellind++; - } - } - - tracepdeex(CMPSSRTRCLVL, std::cout, "\n#CMPSSR_MSK %s iod: %2d nsat: %2d nsig: %2d ", - compactSsrMaskTime.to_string().c_str(), - compactSSRIod, - compactSsrSatelliteIndex[-1].size(), - compactSsrSignalIndex[-1].size()); - - return i; + compactSsrStorage.clear(); + compactSsrSatelliteIndex.clear(); + compactSsrSignalIndex.clear(); + + for (auto& [regId, regData] : compactSsrAtmStorage.atmosRegionsMap) + { + regData.tropData.clear(); + regData.stecData.clear(); + } + + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, true); + + compactSsrLastTime = GTime(GTow(ssrMeta.epochTime1s), now); + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + compactSsrMaskTime = compactSsrLastTime; + + int nsys = getbituInc(data, i, 4); + + for (int s = 0, satind = 0, cellind = 0; s < nsys; s++) + { + if ((i + 61) > bitLen) + { + compactSSRIod = -1; + return E_ReturnType::BAD_LENGTH; + } + + int sysID = getbituInc(data, i, 4); + E_Sys sys; + switch (sysID) + { + case 0: + sys = E_Sys::GPS; + break; + case 1: + sys = E_Sys::GLO; + break; + case 2: + sys = E_Sys::GAL; + break; + case 3: + sys = E_Sys::BDS; + break; + case 4: + sys = E_Sys::QZS; + break; + case 5: + sys = E_Sys::SBS; + break; + default: + tracepdeex(2, std::cout, "Warning unsupported GNSS for compact SSR\n"); + compactSSRIod = -1; + return 0; + } + + int nsat = 0; + map sysSatIndex; + for (int n = 1; n < 41; n++) + { + SatSys Sat(sys, n); + if (getbituInc(data, i, 1) == 1) + { + compactSsrSatelliteIndex[-1][satind++] = Sat; + sysSatIndex[nsat++] = Sat; + } + } + + int ncod = 0; + map sysCodeIndex; + for (int n = 0; n < 16; n++) + { + if (getbituInc(data, i, 1) == 1 && + compactSSRIndex2Code[sys].find(n) != compactSSRIndex2Code[sys].end()) + { + sysCodeIndex[ncod++] = compactSSRIndex2Code[sys][n]; + } + } + + bool cellField = false; + if (getbituInc(data, i, 1) == 1) + { + if (nsat * ncod > 128) + { + tracepdeex( + 2, + std::cout, + "Warning cell size for compact SSR execeds maximum size\n" + ); + compactSSRIod = -1; + return 0; + } + cellField = true; + if ((i + nsat * ncod) > bitLen) + return E_ReturnType::BAD_LENGTH; + } + + for (auto& [sind, Sat] : sysSatIndex) + for (auto& [cind, code] : sysCodeIndex) + { + if (cellField && getbituInc(data, i, 1) == 0) + { + continue; + } + + compactSsrSignalIndex[-1][cellind].first = Sat; + compactSsrSignalIndex[-1][cellind].second = code; + cellind++; + } + } + + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_MSK %s iod: %2d nsat: %2d nsig: %2d ", + compactSsrMaskTime.to_string().c_str(), + compactSSRIod, + compactSsrSatelliteIndex[-1].size(), + compactSsrSignalIndex[-1].size() + ); + + return i; } /** * Compact SSR message type 2: global satellite orbits */ -int decodeSSR_orbit( - vector& data, - GTime now) +int decodeSSR_orbit(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - int bitLen = data.size()*8; - - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i < 0) - { - tracepdeex(4, std::cout,"Orbit message inconsistent with mask IOD\n"); - return 0; - } - - SSREph ssrEph; - ssrEph.ssrMeta = ssrMeta; - ssrEph.t0 = ssrMeta.receivedTime; - ssrEph.udi = ssrUdi[ssrMeta.updateIntIndex]; - if (ssrEph.udi > 1) - ssrEph.t0 += 0.5*ssrEph.udi; - - ssrEph.iod = compactSSRIod; - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - { - int ni = (Sat.sys == +E_Sys::GAL)?10:8; - if ((i+ni+41) > bitLen) - return E_ReturnType::BAD_LENGTH; - - ssrEph.iode = getbituInc(data, i, ni); - ssrEph.deph[0] = decodeCmpSsrField (data,i,15,0.0016,0); - ssrEph.deph[1] = decodeCmpSsrField (data,i,13,0.0064,0); - ssrEph.deph[2] = decodeCmpSsrField (data,i,13,0.0064,0); - - compactSsrStorage[-1][Sat].ssrEph = ssrEph; - compactSsrStorage[-1][Sat].ephUpdated = true; - } - - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_ORB %s iod: %2d nsat: %2d udi: %2d", ssrMeta.receivedTime.to_string().c_str(), compactSSRIod, compactSsrSatelliteIndex[-1].size(), ssrEph.udi); - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-1); - - return i; + if (compactSSRIod < 0) + return 0; + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "Orbit message inconsistent with mask IOD\n"); + return 0; + } + + SSREph ssrEph; + ssrEph.ssrMeta = ssrMeta; + ssrEph.t0 = ssrMeta.receivedTime; + ssrEph.udi = ssrUdi[ssrMeta.updateIntIndex]; + if (ssrEph.udi > 1) + ssrEph.t0 += 0.5 * ssrEph.udi; + + ssrEph.iod = compactSSRIod; + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + { + int ni = (Sat.sys == E_Sys::GAL) ? 10 : 8; + if ((i + ni + 41) > bitLen) + return E_ReturnType::BAD_LENGTH; + + ssrEph.iode = getbituInc(data, i, ni); + ssrEph.deph[0] = decodeCmpSsrField(data, i, 15, 0.0016, 0); + ssrEph.deph[1] = decodeCmpSsrField(data, i, 13, 0.0064, 0); + ssrEph.deph[2] = decodeCmpSsrField(data, i, 13, 0.0064, 0); + + compactSsrStorage[-1][Sat].ssrEph = ssrEph; + compactSsrStorage[-1][Sat].ephUpdated = true; + } + + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_ORB %s iod: %2d nsat: %2d udi: %2d", + ssrMeta.receivedTime.to_string().c_str(), + compactSSRIod, + compactSsrSatelliteIndex[-1].size(), + ssrEph.udi + ); + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-1); + + return i; } /** * Compact SSR message type 3: global satellite clock offsets */ -int decodeSSR_clock( - vector& data, - GTime now) +int decodeSSR_clock(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - int bitLen = data.size()*8; - - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; //todo aaron, this is all copy-paste in every function - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i<0) - { - tracepdeex(4, std::cout,"Clock message inconsistent with mask IOD\n"); - return 0; - } - - SSRClk ssrClk; - ssrClk.ssrMeta = ssrMeta; - ssrClk.t0 = ssrMeta.receivedTime; - ssrClk.udi = ssrUdi[ssrMeta.updateIntIndex]; - if (ssrClk.udi > 1) - ssrClk.t0 += 0.5*ssrClk.udi; - ssrClk.iod = compactSSRIod; - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - { - if ((i+15) > bitLen) - return E_ReturnType::BAD_LENGTH; - - ssrClk.dclk[0] = decodeCmpSsrField (data,i,15,0.0016,0); - - compactSsrStorage[-1][Sat].ssrClk = ssrClk; - compactSsrStorage[-1][Sat].clkUpdated = true; - } - - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_CLK %s iod: %2d nsat: %2d udi: %2d", ssrMeta.receivedTime.to_string().c_str(), compactSSRIod, compactSsrSatelliteIndex[-1].size(), ssrClk.udi); - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-1); - - return i; + if (compactSSRIod < 0) + return 0; + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = + ssrMeta.receivedTime; // todo aaron, this is all copy-paste in every function + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "Clock message inconsistent with mask IOD\n"); + return 0; + } + + SSRClk ssrClk; + ssrClk.ssrMeta = ssrMeta; + ssrClk.t0 = ssrMeta.receivedTime; + ssrClk.udi = ssrUdi[ssrMeta.updateIntIndex]; + if (ssrClk.udi > 1) + ssrClk.t0 += 0.5 * ssrClk.udi; + ssrClk.iod = compactSSRIod; + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + { + if ((i + 15) > bitLen) + return E_ReturnType::BAD_LENGTH; + + ssrClk.dclk[0] = decodeCmpSsrField(data, i, 15, 0.0016, 0); + + compactSsrStorage[-1][Sat].ssrClk = ssrClk; + compactSsrStorage[-1][Sat].clkUpdated = true; + } + + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_CLK %s iod: %2d nsat: %2d udi: %2d", + ssrMeta.receivedTime.to_string().c_str(), + compactSSRIod, + compactSsrSatelliteIndex[-1].size(), + ssrClk.udi + ); + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-1); + + return i; } /** * Compact SSR message type 11: regional satellite orbits and clock offsets */ -int decodeSSR_combined( - vector& data, - GTime now) +int decodeSSR_combined(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - - int bitLen = data.size() * 8; - - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i<0) - { - tracepdeex(4, std::cout,"Orbit/Clock message inconsistent with mask IOD\n"); - return 0; - } - - if ((i+3) > bitLen) - return E_ReturnType::BAD_LENGTH; - - bool orbitAvailable = getbituInc(data,i,1); - bool clockAvailable = getbituInc(data,i,1); - int regionID = -1; - if (getbituInc(data,i,1)) - { - if ((i+5+compactSsrSatelliteIndex[-1].size()) > bitLen) - return E_ReturnType::BAD_LENGTH; - - regionID = getbituInc(data,i,5); - int n=0; - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - if (getbituInc(data,i,1)==1) - compactSsrSatelliteIndex[regionID][n++] = Sat; - } - - SSREph ssrEph; - ssrEph.ssrMeta = ssrMeta; - ssrEph.t0 = ssrMeta.receivedTime; - ssrEph.udi = ssrUdi[ssrMeta.updateIntIndex]; - if (ssrEph.udi > 1) - ssrEph.t0 += 0.5*ssrEph.udi; - ssrEph.iod = compactSSRIod; - - SSRClk ssrClk; - ssrClk.ssrMeta = ssrMeta; - ssrClk.t0 = ssrEph.t0; - ssrClk.udi = ssrEph.udi; - ssrClk.iod = compactSSRIod; - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[regionID]) - { - int ni = (Sat.sys == +E_Sys::GAL)?10:8; - - if (orbitAvailable) - { - if ((i+ni+41)> bitLen) - return E_ReturnType::BAD_LENGTH; - - ssrEph.iode = getbituInc(data, i, ni); - ssrEph.deph[0] = decodeCmpSsrField (data,i,15,0.0016,0); - ssrEph.deph[1] = decodeCmpSsrField (data,i,13,0.0064,0); - ssrEph.deph[2] = decodeCmpSsrField (data,i,13,0.0064,0); - - compactSsrStorage[regionID][Sat].ssrEph = ssrEph; - compactSsrStorage[regionID][Sat].ephUpdated = true; - } - if (clockAvailable) - { - if ((i+15) > bitLen) - return E_ReturnType::BAD_LENGTH; - - ssrClk.dclk[0] = decodeCmpSsrField (data,i,15,0.0016,0); - - compactSsrStorage[regionID][Sat].ssrClk = ssrClk; - compactSsrStorage[regionID][Sat].clkUpdated = true; - } - } - - if (regionID == -1) - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_CMB %s %s %s global , nsat:%d", ssrMeta.receivedTime.to_string().c_str(), orbitAvailable?"orb":" ", clockAvailable?"clk":" ", compactSsrSatelliteIndex[regionID].size()); - else - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_CMB %s %s %s region %2d, nsat:%d",ssrMeta.receivedTime.to_string().c_str(), orbitAvailable?"orb":" ", clockAvailable?"clk":" ", regionID, compactSsrSatelliteIndex[regionID].size()); - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(regionID); - - return i; + if (compactSSRIod < 0) + return 0; + + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "Orbit/Clock message inconsistent with mask IOD\n"); + return 0; + } + + if ((i + 3) > bitLen) + return E_ReturnType::BAD_LENGTH; + + bool orbitAvailable = getbituInc(data, i, 1); + bool clockAvailable = getbituInc(data, i, 1); + int regionID = -1; + if (getbituInc(data, i, 1)) + { + if ((i + 5 + compactSsrSatelliteIndex[-1].size()) > bitLen) + return E_ReturnType::BAD_LENGTH; + + regionID = getbituInc(data, i, 5); + int n = 0; + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + if (getbituInc(data, i, 1) == 1) + compactSsrSatelliteIndex[regionID][n++] = Sat; + } + + SSREph ssrEph; + ssrEph.ssrMeta = ssrMeta; + ssrEph.t0 = ssrMeta.receivedTime; + ssrEph.udi = ssrUdi[ssrMeta.updateIntIndex]; + if (ssrEph.udi > 1) + ssrEph.t0 += 0.5 * ssrEph.udi; + ssrEph.iod = compactSSRIod; + + SSRClk ssrClk; + ssrClk.ssrMeta = ssrMeta; + ssrClk.t0 = ssrEph.t0; + ssrClk.udi = ssrEph.udi; + ssrClk.iod = compactSSRIod; + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[regionID]) + { + int ni = (Sat.sys == E_Sys::GAL) ? 10 : 8; + + if (orbitAvailable) + { + if ((i + ni + 41) > bitLen) + return E_ReturnType::BAD_LENGTH; + + ssrEph.iode = getbituInc(data, i, ni); + ssrEph.deph[0] = decodeCmpSsrField(data, i, 15, 0.0016, 0); + ssrEph.deph[1] = decodeCmpSsrField(data, i, 13, 0.0064, 0); + ssrEph.deph[2] = decodeCmpSsrField(data, i, 13, 0.0064, 0); + + compactSsrStorage[regionID][Sat].ssrEph = ssrEph; + compactSsrStorage[regionID][Sat].ephUpdated = true; + } + if (clockAvailable) + { + if ((i + 15) > bitLen) + return E_ReturnType::BAD_LENGTH; + + ssrClk.dclk[0] = decodeCmpSsrField(data, i, 15, 0.0016, 0); + + compactSsrStorage[regionID][Sat].ssrClk = ssrClk; + compactSsrStorage[regionID][Sat].clkUpdated = true; + } + } + + if (regionID == -1) + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_CMB %s %s %s global , nsat:%d", + ssrMeta.receivedTime.to_string().c_str(), + orbitAvailable ? "orb" : " ", + clockAvailable ? "clk" : " ", + compactSsrSatelliteIndex[regionID].size() + ); + else + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_CMB %s %s %s region %2d, nsat:%d", + ssrMeta.receivedTime.to_string().c_str(), + orbitAvailable ? "orb" : " ", + clockAvailable ? "clk" : " ", + regionID, + compactSsrSatelliteIndex[regionID].size() + ); + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(regionID); + + return i; } /** * Compact SSR message type 4: global satellite code biases -*/ -int decodeSSR_code_bias( - vector& data, - GTime now) + */ +int decodeSSR_code_bias(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - int bitLen = data.size()*8; - - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i<0) - { - tracepdeex(4, std::cout,"Code bias message inconsistent with mask IOD\n"); - return 0; - } - - SSRCodeBias ssrCodeBias; - ssrCodeBias.ssrMeta = ssrMeta; - ssrCodeBias.t0 = ssrMeta.receivedTime; - ssrCodeBias.udi = ssrUdi[ssrMeta.updateIntIndex]; - if (ssrCodeBias.udi > 1) - ssrCodeBias.t0 += 0.5*ssrCodeBias.udi; - ssrCodeBias.iod = compactSSRIod; - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - compactSsrStorage[-1][Sat].ssrCodeBias = ssrCodeBias; - - for (auto& [indx, SatnCode] : compactSsrSignalIndex[-1]) - { - SatSys Sat = SatnCode.first; - E_ObsCode code = SatnCode.second; - - if ((i+11) > bitLen) - return E_ReturnType::BAD_LENGTH; - - auto& biasEntry = compactSsrStorage[-1][Sat].ssrCodeBias.obsCodeBiasMap[code]; - biasEntry.bias = decodeCmpSsrField (data,i,11,0.02,0); - } - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_COD %s iod: %2d udi: %2d nsig:%d", ssrMeta.receivedTime.to_string().c_str(), compactSSRIod, ssrCodeBias.udi, compactSsrSignalIndex[-1].size()); - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - compactSsrStorage[-1][Sat].codeUpdated = true; - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-1); - - return i; + if (compactSSRIod < 0) + return 0; + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "Code bias message inconsistent with mask IOD\n"); + return 0; + } + + SSRCodeBias ssrCodeBias; + ssrCodeBias.ssrMeta = ssrMeta; + ssrCodeBias.t0 = ssrMeta.receivedTime; + ssrCodeBias.udi = ssrUdi[ssrMeta.updateIntIndex]; + if (ssrCodeBias.udi > 1) + ssrCodeBias.t0 += 0.5 * ssrCodeBias.udi; + ssrCodeBias.iod = compactSSRIod; + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + compactSsrStorage[-1][Sat].ssrCodeBias = ssrCodeBias; + + for (auto& [indx, SatnCode] : compactSsrSignalIndex[-1]) + { + SatSys Sat = SatnCode.first; + E_ObsCode code = SatnCode.second; + + if ((i + 11) > bitLen) + return E_ReturnType::BAD_LENGTH; + + auto& biasEntry = compactSsrStorage[-1][Sat].ssrCodeBias.obsCodeBiasMap[code]; + biasEntry.bias = decodeCmpSsrField(data, i, 11, 0.02, 0); + } + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_COD %s iod: %2d udi: %2d nsig:%d", + ssrMeta.receivedTime.to_string().c_str(), + compactSSRIod, + ssrCodeBias.udi, + compactSsrSignalIndex[-1].size() + ); + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + compactSsrStorage[-1][Sat].codeUpdated = true; + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-1); + + return i; } /** * Compact SSR message type 5: global satellite phase biases -*/ -int decodeSSR_phas_bias( - vector& data, - GTime now) + */ +int decodeSSR_phas_bias(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - int bitLen = data.size()*8; - - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i<0) - { - tracepdeex(4, std::cout,"Phase bias message inconsistent with mask IOD\n"); - return 0; - } - - SSRPhasBias ssrPhasBias; - ssrPhasBias.ssrMeta = ssrMeta; - ssrPhasBias.t0 = ssrMeta.receivedTime; - ssrPhasBias.udi = ssrUdi[ssrMeta.updateIntIndex]; - if (ssrPhasBias.udi > 1) - ssrPhasBias.t0 += 0.5*ssrPhasBias.udi; - ssrPhasBias.iod = compactSSRIod; - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - compactSsrStorage[-1][Sat].ssrPhasBias = ssrPhasBias; - - - for (auto& [indx, SatnCode] : compactSsrSignalIndex[-1]) - { - SatSys Sat = SatnCode.first; - E_ObsCode code = SatnCode.second; - - if ((i+17) > bitLen) - return E_ReturnType::BAD_LENGTH; - - double bias = decodeCmpSsrField (data,i,15,0.001,0); - int disc = getbituInc(data, i, 2); - - auto& biasEntry = compactSsrStorage[-1][Sat].ssrPhasBias.obsCodeBiasMap[code]; - biasEntry.bias = checkDisc(Sat, code, bias, disc, -1); - - auto& ssrPhaseChs = compactSsrStorage[-1][Sat].ssrPhasBias.ssrPhaseChs[code]; - ssrPhaseChs.signalDisconCnt = disc; - } - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - compactSsrStorage[-1][Sat].phaseUpdated = true; - - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_PHS %s iod: %2d nsig:%d udi: %2d", ssrMeta.receivedTime.to_string().c_str(), compactSSRIod, compactSsrSignalIndex[-1].size(), ssrPhasBias.udi); - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-1); - - return i; + if (compactSSRIod < 0) + return 0; + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "Phase bias message inconsistent with mask IOD\n"); + return 0; + } + + SSRPhasBias ssrPhasBias; + ssrPhasBias.ssrMeta = ssrMeta; + ssrPhasBias.t0 = ssrMeta.receivedTime; + ssrPhasBias.udi = ssrUdi[ssrMeta.updateIntIndex]; + if (ssrPhasBias.udi > 1) + ssrPhasBias.t0 += 0.5 * ssrPhasBias.udi; + ssrPhasBias.iod = compactSSRIod; + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + compactSsrStorage[-1][Sat].ssrPhasBias = ssrPhasBias; + + for (auto& [indx, SatnCode] : compactSsrSignalIndex[-1]) + { + SatSys Sat = SatnCode.first; + E_ObsCode code = SatnCode.second; + + if ((i + 17) > bitLen) + return E_ReturnType::BAD_LENGTH; + + double bias = decodeCmpSsrField(data, i, 15, 0.001, 0); + int disc = getbituInc(data, i, 2); + + auto& biasEntry = compactSsrStorage[-1][Sat].ssrPhasBias.obsCodeBiasMap[code]; + biasEntry.bias = checkDisc(Sat, code, bias, disc, -1); + + auto& ssrPhaseChs = compactSsrStorage[-1][Sat].ssrPhasBias.ssrPhaseChs[code]; + ssrPhaseChs.signalDisconCnt = disc; + } + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + compactSsrStorage[-1][Sat].phaseUpdated = true; + + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_PHS %s iod: %2d nsig:%d udi: %2d", + ssrMeta.receivedTime.to_string().c_str(), + compactSSRIod, + compactSsrSignalIndex[-1].size(), + ssrPhasBias.udi + ); + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-1); + + return i; } /** * Combined code and phase bias (with potential regional biases) -*/ -int decodeSSR_comb_bias( - vector& data, - GTime now) + */ +int decodeSSR_comb_bias(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - int bitLen = data.size()*8; - - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i<0) - { - tracepdeex(4, std::cout,"Combined bias message inconsistent with mask IOD\n"); - return 0; - } - - if ((i+3) > bitLen) - return E_ReturnType::BAD_LENGTH; - - bool code_Available = getbituInc(data,i,1); - bool phaseAvailable = getbituInc(data,i,1); - int regionID = -1; - if (getbituInc(data,i,1)==1) - { - if ((i+5 + compactSsrSignalIndex[-1].size()) > bitLen) - return E_ReturnType::BAD_LENGTH; - - regionID = getbituInc(data,i,5); - int n=0; - map regSats; - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - if (getbituInc(data,i,1)==1) - { - compactSsrSatelliteIndex[regionID][n]=Sat; - regSats[Sat] = n++; - } - n=0; - for (auto& [indx, SatnCode] : compactSsrSignalIndex[-1]) - if (regSats.find(SatnCode.first) != regSats.end()) - { - compactSsrSignalIndex[regionID][n++] = SatnCode; - } - } - - SSRCodeBias ssrCodeBias; - ssrCodeBias.ssrMeta = ssrMeta; - ssrCodeBias.t0 = ssrMeta.receivedTime; - ssrCodeBias.udi = ssrUdi[ssrMeta.updateIntIndex]; - if (ssrCodeBias.udi > 1) - ssrCodeBias.t0 += 0.5*ssrCodeBias.udi; - ssrCodeBias.iod = compactSSRIod; - - SSRPhasBias ssrPhasBias; - ssrPhasBias.ssrMeta = ssrMeta; - ssrPhasBias.t0 = ssrCodeBias.t0; - ssrPhasBias.udi = ssrCodeBias.udi; - ssrPhasBias.iod = compactSSRIod; - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[regionID]) - { - if (code_Available) compactSsrStorage[regionID][Sat].ssrCodeBias = ssrCodeBias; - if (phaseAvailable) compactSsrStorage[regionID][Sat].ssrPhasBias = ssrPhasBias; - } - - int nCode = 0; - int nPhase = 0; - - for (auto& [indx, SatnCode] : compactSsrSignalIndex[regionID]) - { - SatSys Sat = SatnCode.first; - E_ObsCode code = SatnCode.second; - - - if (code_Available) - { - if ((i+11) > bitLen) - return E_ReturnType::BAD_LENGTH; - - auto& biasEntry = compactSsrStorage[regionID][Sat].ssrCodeBias.obsCodeBiasMap[code]; - biasEntry.bias = decodeCmpSsrField (data,i,11,0.02,0); - if (regionID>=0) - biasEntry.bias += compactSsrStorage[-1][Sat].ssrCodeBias.obsCodeBiasMap[code].bias; - nCode++; - } - - if (phaseAvailable) - { - if ((i+17)> bitLen) - return E_ReturnType::BAD_LENGTH; - - double bias = decodeCmpSsrField (data,i,15,0.001,0); - int disc = getbituInc(data, i, 2); - - auto& biasEntry = compactSsrStorage[regionID][Sat].ssrPhasBias.obsCodeBiasMap[code]; - biasEntry.bias = checkDisc(Sat, code, bias, disc, -1); - if (regionID>=0) - biasEntry.bias += compactSsrStorage[-1][Sat].ssrPhasBias.obsCodeBiasMap[code].bias; - - auto& ssrPhaseChs = compactSsrStorage[regionID][Sat].ssrPhasBias.ssrPhaseChs[code]; - ssrPhaseChs.signalDisconCnt = disc; - - nPhase++; - } - } - - if (regionID == -1) - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_BIA %s %s %s global , nsig:%d", ssrMeta.receivedTime.to_string().c_str(), code_Available?"cod":" ", phaseAvailable?"phs":" ", compactSsrSignalIndex[regionID].size()); - else - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_BIA %s %s %s region %2d, nsig:%d",ssrMeta.receivedTime.to_string().c_str(), code_Available?"cod":" ", phaseAvailable?"phs":" ", regionID, compactSsrSignalIndex[regionID].size()); - - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[regionID]) - { - if (nCode > 0) compactSsrStorage[regionID][Sat].codeUpdated = true; - if (nPhase> 0) compactSsrStorage[regionID][Sat].phaseUpdated = true; - } - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(regionID); - - return i; + if (compactSSRIod < 0) + return 0; + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "Combined bias message inconsistent with mask IOD\n"); + return 0; + } + + if ((i + 3) > bitLen) + return E_ReturnType::BAD_LENGTH; + + bool code_Available = getbituInc(data, i, 1); + bool phaseAvailable = getbituInc(data, i, 1); + int regionID = -1; + if (getbituInc(data, i, 1) == 1) + { + if ((i + 5 + compactSsrSignalIndex[-1].size()) > bitLen) + return E_ReturnType::BAD_LENGTH; + + regionID = getbituInc(data, i, 5); + int n = 0; + map regSats; + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + if (getbituInc(data, i, 1) == 1) + { + compactSsrSatelliteIndex[regionID][n] = Sat; + regSats[Sat] = n++; + } + n = 0; + for (auto& [indx, SatnCode] : compactSsrSignalIndex[-1]) + if (regSats.find(SatnCode.first) != regSats.end()) + { + compactSsrSignalIndex[regionID][n++] = SatnCode; + } + } + + SSRCodeBias ssrCodeBias; + ssrCodeBias.ssrMeta = ssrMeta; + ssrCodeBias.t0 = ssrMeta.receivedTime; + ssrCodeBias.udi = ssrUdi[ssrMeta.updateIntIndex]; + if (ssrCodeBias.udi > 1) + ssrCodeBias.t0 += 0.5 * ssrCodeBias.udi; + ssrCodeBias.iod = compactSSRIod; + + SSRPhasBias ssrPhasBias; + ssrPhasBias.ssrMeta = ssrMeta; + ssrPhasBias.t0 = ssrCodeBias.t0; + ssrPhasBias.udi = ssrCodeBias.udi; + ssrPhasBias.iod = compactSSRIod; + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[regionID]) + { + if (code_Available) + compactSsrStorage[regionID][Sat].ssrCodeBias = ssrCodeBias; + if (phaseAvailable) + compactSsrStorage[regionID][Sat].ssrPhasBias = ssrPhasBias; + } + + int nCode = 0; + int nPhase = 0; + + for (auto& [indx, SatnCode] : compactSsrSignalIndex[regionID]) + { + SatSys Sat = SatnCode.first; + E_ObsCode code = SatnCode.second; + + if (code_Available) + { + if ((i + 11) > bitLen) + return E_ReturnType::BAD_LENGTH; + + auto& biasEntry = compactSsrStorage[regionID][Sat].ssrCodeBias.obsCodeBiasMap[code]; + biasEntry.bias = decodeCmpSsrField(data, i, 11, 0.02, 0); + if (regionID >= 0) + biasEntry.bias += compactSsrStorage[-1][Sat].ssrCodeBias.obsCodeBiasMap[code].bias; + nCode++; + } + + if (phaseAvailable) + { + if ((i + 17) > bitLen) + return E_ReturnType::BAD_LENGTH; + + double bias = decodeCmpSsrField(data, i, 15, 0.001, 0); + int disc = getbituInc(data, i, 2); + + auto& biasEntry = compactSsrStorage[regionID][Sat].ssrPhasBias.obsCodeBiasMap[code]; + biasEntry.bias = checkDisc(Sat, code, bias, disc, -1); + if (regionID >= 0) + biasEntry.bias += compactSsrStorage[-1][Sat].ssrPhasBias.obsCodeBiasMap[code].bias; + + auto& ssrPhaseChs = compactSsrStorage[regionID][Sat].ssrPhasBias.ssrPhaseChs[code]; + ssrPhaseChs.signalDisconCnt = disc; + + nPhase++; + } + } + + if (regionID == -1) + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_BIA %s %s %s global , nsig:%d", + ssrMeta.receivedTime.to_string().c_str(), + code_Available ? "cod" : " ", + phaseAvailable ? "phs" : " ", + compactSsrSignalIndex[regionID].size() + ); + else + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_BIA %s %s %s region %2d, nsig:%d", + ssrMeta.receivedTime.to_string().c_str(), + code_Available ? "cod" : " ", + phaseAvailable ? "phs" : " ", + regionID, + compactSsrSignalIndex[regionID].size() + ); + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[regionID]) + { + if (nCode > 0) + compactSsrStorage[regionID][Sat].codeUpdated = true; + if (nPhase > 0) + compactSsrStorage[regionID][Sat].phaseUpdated = true; + } + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(regionID); + + return i; } /** * User Range Accuracy -*/ -int decodeSSR_URA( - vector& data, - GTime now) + */ +int decodeSSR_URA(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - - int bitLen = data.size()*8; - - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i<0) - { - tracepdeex(4, std::cout,"URA message inconsistent with mask IOD\n"); - return 0; - } - - SSRUra ssrUra; - ssrUra.t0 = ssrMeta.receivedTime; - ssrUra.udi = ssrUdi[ssrMeta.updateIntIndex]; - if (ssrUra.udi > 1) - ssrUra.t0 += 0.5*ssrUra.udi; - ssrUra.iod = compactSSRIod; - - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - { - if ((i+6) > bitLen) - return E_ReturnType::BAD_LENGTH; - - ssrUra.ura = getbituInc(data, i, 6); - - compactSsrStorage[-1][Sat].ssrUra = ssrUra; - compactSsrStorage[-1][Sat].uraUpdated = true; - } - - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_URA %s iod: %2d nsat: %2d udi: %2d", ssrMeta.receivedTime.to_string().c_str(), compactSSRIod, compactSsrSatelliteIndex[-1].size(), ssrUra.udi); - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-1); - - return i; + if (compactSSRIod < 0) + return 0; + + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "URA message inconsistent with mask IOD\n"); + return 0; + } + + SSRUra ssrUra; + ssrUra.t0 = ssrMeta.receivedTime; + ssrUra.udi = ssrUdi[ssrMeta.updateIntIndex]; + if (ssrUra.udi > 1) + ssrUra.t0 += 0.5 * ssrUra.udi; + ssrUra.iod = compactSSRIod; + + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + { + if ((i + 6) > bitLen) + return E_ReturnType::BAD_LENGTH; + + ssrUra.ura = getbituInc(data, i, 6); + + compactSsrStorage[-1][Sat].ssrUra = ssrUra; + compactSsrStorage[-1][Sat].uraUpdated = true; + } + + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_URA %s iod: %2d nsat: %2d udi: %2d", + ssrMeta.receivedTime.to_string().c_str(), + compactSSRIod, + compactSsrSatelliteIndex[-1].size(), + ssrUra.udi + ); + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-1); + + return i; } /** * Compact (polynomial based) salnt TEC maps -*/ -int decodeSSR_slant_TEC( - vector& data, - GTime now) + */ +int decodeSSR_slant_TEC(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; + if (compactSSRIod < 0) + return 0; - int bitLen = data.size() * 8; + int bitLen = data.size() * 8; - SSRMeta ssrMeta; - int i = decodeSSR_header(data, ssrMeta, false); + SSRMeta ssrMeta; + int i = decodeSSR_header(data, ssrMeta, false); - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; - if (i<0) - { - tracepdeex(4, std::cout,"STEC message inconsistent with mask IOD\n"); - return 0; - } + if (i < 0) + { + tracepdeex(4, std::cout, "STEC message inconsistent with mask IOD\n"); + return 0; + } - if ((i+7) > bitLen) - return E_ReturnType::BAD_LENGTH; + if ((i + 7) > bitLen) + return E_ReturnType::BAD_LENGTH; - compactSsrAtmStorage.ssrMeta = ssrMeta; + compactSsrAtmStorage.ssrMeta = ssrMeta; - int STECType = getbituInc(data,i,2); - int regionID = getbituInc(data,i,5); - int n=0; - compactSsrIonoIndex[regionID].clear(); - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - if (getbituInc(data,i,1)==1) - compactSsrIonoIndex[regionID][n++] = Sat; + int STECType = getbituInc(data, i, 2); + int regionID = getbituInc(data, i, 5); + int n = 0; + compactSsrIonoIndex[regionID].clear(); + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + if (getbituInc(data, i, 1) == 1) + compactSsrIonoIndex[regionID][n++] = Sat; - SSRAtmRegion& atmRegion = compactSsrAtmStorage.atmosRegionsMap[regionID]; + SSRAtmRegion& atmRegion = compactSsrAtmStorage.atmosRegionsMap[regionID]; - GTime tAtm = ssrMeta.receivedTime; + GTime tAtm = ssrMeta.receivedTime; - for (auto& [indx, Sat] : compactSsrIonoIndex[regionID]) - { - if ((i+20) > bitLen) - return E_ReturnType::BAD_LENGTH; + for (auto& [indx, Sat] : compactSsrIonoIndex[regionID]) + { + if ((i + 20) > bitLen) + return E_ReturnType::BAD_LENGTH; - atmRegion.stecData[Sat][tAtm].sigma = uraSsr[getbituInc(data, i, 6)]/1000; - atmRegion.stecData[Sat][tAtm].iod = compactSSRIod; + atmRegion.stecData[Sat][tAtm].sigma = uraSsr[getbituInc(data, i, 6)] / 1000; + atmRegion.stecData[Sat][tAtm].iod = compactSSRIod; + atmRegion.stecData[Sat][tAtm].poly[0] = decodeCmpSsrField(data, i, 14, 0.05, 0); - atmRegion.stecData[Sat][tAtm].poly[0] = decodeCmpSsrField (data,i,14,0.05,0); + if (STECType == 0) + continue; - if (STECType==0) - continue; + if ((i + 24) > bitLen) + return E_ReturnType::BAD_LENGTH; - if ((i+24) > bitLen) - return E_ReturnType::BAD_LENGTH; + atmRegion.stecData[Sat][tAtm].poly[1] = decodeCmpSsrField(data, i, 12, 0.02, 0); + atmRegion.stecData[Sat][tAtm].poly[2] = decodeCmpSsrField(data, i, 12, 0.02, 0); - atmRegion.stecData[Sat][tAtm].poly[1] = decodeCmpSsrField (data,i,12,0.02,0); - atmRegion.stecData[Sat][tAtm].poly[2] = decodeCmpSsrField (data,i,12,0.02,0); + if (STECType == 1) + continue; - if (STECType == 1) - continue; + if ((i + 10) > bitLen) + return E_ReturnType::BAD_LENGTH; - if ((i+10) > bitLen) - return E_ReturnType::BAD_LENGTH; + atmRegion.stecData[Sat][tAtm].poly[3] = decodeCmpSsrField(data, i, 10, 0.02, 0); - atmRegion.stecData[Sat][tAtm].poly[3] = decodeCmpSsrField (data,i,10,0.02,0); + if (STECType == 2) + continue; - if (STECType == 2) - continue; + if ((i + 16) > bitLen) + return E_ReturnType::BAD_LENGTH; - if ((i+16) > bitLen) - return E_ReturnType::BAD_LENGTH; + atmRegion.stecData[Sat][tAtm].poly[4] = decodeCmpSsrField(data, i, 8, 0.005, 0); + atmRegion.stecData[Sat][tAtm].poly[5] = decodeCmpSsrField(data, i, 8, 0.005, 0); + } - atmRegion.stecData[Sat][tAtm].poly[4] = decodeCmpSsrField (data,i, 8,0.005,0); - atmRegion.stecData[Sat][tAtm].poly[5] = decodeCmpSsrField (data,i, 8,0.005,0); - } + ssrAtmUpdated = true; + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_TEC %s %d region %2d, nsat: %d", + tAtm.to_string().c_str(), + STECType, + regionID, + compactSsrIonoIndex[regionID].size() + ); - ssrAtmUpdated = true; - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_TEC %s %d region %2d, nsat: %d",tAtm.to_string().c_str(), STECType, regionID, compactSsrIonoIndex[regionID].size()); + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-2); - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-2); - - return i; + return i; } /** * Grid based Ionosphere and troposphere corrections -*/ -int decodeSSR_grid_ATM( - vector& data, - GTime now) + */ +int decodeSSR_grid_ATM(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - - int bitLen = data.size() * 8; - - SSRMeta ssrMeta; - - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; - - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i<0) - { - tracepdeex(4, std::cout,"Grid message inconsistent with mask IOD\n"); - return 0; - } - - if ((i+20) > bitLen) - return E_ReturnType::BAD_LENGTH; - - compactSsrAtmStorage.ssrMeta = ssrMeta; - - int tropType = getbituInc(data,i,2); - int STECtype = getbituInc(data,i,1); - int regionID = getbituInc(data,i,5); - GTime tAtm = ssrMeta.receivedTime; - - int n=0; - compactSsrIonoIndex[regionID].clear(); - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - if (getbituInc(data,i,1)==1) - compactSsrIonoIndex[regionID][n++] = Sat; - - SSRAtmRegion& atmRegion = compactSsrAtmStorage.atmosRegionsMap[regionID]; - - if (tropType > 0) - atmRegion.tropData[tAtm].sigma = uraSsr[getbituInc(data,i,6)]/1000; - - int numGrid = getbituInc(data,i,6); - int tmp; - for (int grd=0; grd < numGrid; grd++) - { - if (tropType==1) - { - if ((i+17) > bitLen) - return E_ReturnType::BAD_LENGTH; - - atmRegion.tropData[tAtm].gridDry[grd] = decodeCmpSsrField (data,i, 9,0.004,2.3); - atmRegion.tropData[tAtm].gridWet[grd] = decodeCmpSsrField (data,i, 8,0.004,0.252); - } - - int ni = STECtype==0?7:16; - for (auto& [indx, Sat] : compactSsrIonoIndex[regionID]) - { - if ((i+ni) > bitLen) - return E_ReturnType::BAD_LENGTH; - - - atmRegion.stecData[Sat][tAtm].grid[grd] = decodeCmpSsrField (data,i,ni,0.02,0); - } - } - - ssrAtmUpdated = true; - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_GRD %s %d %d region %2d, nsig:%d", tAtm.to_string().c_str(), tropType, STECtype, regionID, compactSsrIonoIndex[regionID].size()); - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-2); - - return i; + if (compactSSRIod < 0) + return 0; + + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; + + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "Grid message inconsistent with mask IOD\n"); + return 0; + } + + if ((i + 20) > bitLen) + return E_ReturnType::BAD_LENGTH; + + compactSsrAtmStorage.ssrMeta = ssrMeta; + + int tropType = getbituInc(data, i, 2); + int STECtype = getbituInc(data, i, 1); + int regionID = getbituInc(data, i, 5); + GTime tAtm = ssrMeta.receivedTime; + + int n = 0; + compactSsrIonoIndex[regionID].clear(); + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + if (getbituInc(data, i, 1) == 1) + compactSsrIonoIndex[regionID][n++] = Sat; + + SSRAtmRegion& atmRegion = compactSsrAtmStorage.atmosRegionsMap[regionID]; + + if (tropType > 0) + atmRegion.tropData[tAtm].sigma = uraSsr[getbituInc(data, i, 6)] / 1000; + + int numGrid = getbituInc(data, i, 6); + int tmp; + for (int grd = 0; grd < numGrid; grd++) + { + if (tropType == 1) + { + if ((i + 17) > bitLen) + return E_ReturnType::BAD_LENGTH; + + atmRegion.tropData[tAtm].gridDry[grd] = decodeCmpSsrField(data, i, 9, 0.004, 2.3); + atmRegion.tropData[tAtm].gridWet[grd] = decodeCmpSsrField(data, i, 8, 0.004, 0.252); + } + + int ni = STECtype == 0 ? 7 : 16; + for (auto& [indx, Sat] : compactSsrIonoIndex[regionID]) + { + if ((i + ni) > bitLen) + return E_ReturnType::BAD_LENGTH; + + atmRegion.stecData[Sat][tAtm].grid[grd] = decodeCmpSsrField(data, i, ni, 0.02, 0); + } + } + + ssrAtmUpdated = true; + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_GRD %s %d %d region %2d, nsig:%d", + tAtm.to_string().c_str(), + tropType, + STECtype, + regionID, + compactSsrIonoIndex[regionID].size() + ); + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-2); + + return i; } /** * Compact (polynomial based) atmospheric corrections -*/ -int decodeSSR_comp_ATM( - vector& data, - GTime now) + */ +int decodeSSR_comp_ATM(vector& data, GTime now) { - if (compactSSRIod < 0) - return 0; - - int bitLen = data.size()*8; - - SSRMeta ssrMeta; - - int i = decodeSSR_header(data, ssrMeta, false); - - if (ssrMeta.receivedTime > compactSsrLastTime) - compactSsrLastTime = ssrMeta.receivedTime; - if (now < compactSsrLastTime) - return E_ReturnType::WAIT; - - if (i < 0) - { - tracepdeex(4, std::cout,"Compact atmosphere message inconsistent with mask IOD\n"); - return 1; - } - - if ((i+21) > bitLen) - return E_ReturnType::BAD_LENGTH; - - compactSsrAtmStorage.ssrMeta = ssrMeta; - - int tropType = getbituInc(data,i,2); - int stecType = getbituInc(data,i,2); - int regionID = getbituInc(data,i,5); - int numbGrid = getbituInc(data,i,6); - - SSRAtmRegion& atmRegion = compactSsrAtmStorage.atmosRegionsMap[regionID]; - GTime tAtm = ssrMeta.receivedTime; - - if (tropType > 0) - { - atmRegion.tropData[tAtm].sigma = uraSsr[getbituInc(data,i,6)]/1000; - tracepdeex(CMPSSRTRCLVL+2,std::cout, "\n Trop (%.3f)", atmRegion.tropData[tAtm].sigma); - - if (tropType&1) - { - if ((i+11) > bitLen) - return E_ReturnType::BAD_LENGTH; - - int dry_Type = getbituInc(data,i,2); - atmRegion.tropData[tAtm].polyDry[0] = decodeCmpSsrField (data,i, 9,0.004,2.3); - tracepdeex(CMPSSRTRCLVL+2,std::cout, " dry: %.3f", atmRegion.tropData[tAtm].polyDry[0]); - - if (dry_Type>0) - { - if ((i+14) > bitLen) - return E_ReturnType::BAD_LENGTH; - - atmRegion.tropData[tAtm].polyDry[1] = decodeCmpSsrField (data,i, 7,0.002,0); - atmRegion.tropData[tAtm].polyDry[2] = decodeCmpSsrField (data,i, 7,0.002,0); - - tracepdeex(CMPSSRTRCLVL+2,std::cout, " %.3f %.3f", atmRegion.tropData[tAtm].polyDry[1], atmRegion.tropData[tAtm].polyDry[2]); - } - if (dry_Type>1) - { - if ((i+14) > bitLen) - return E_ReturnType::BAD_LENGTH; - - atmRegion.tropData[tAtm].polyDry[3] = decodeCmpSsrField (data,i, 7,0.001,0); - tracepdeex(CMPSSRTRCLVL+2,std::cout, " %.3f", atmRegion.tropData[tAtm].polyDry[3]); - } - } - if (tropType>1) - { - if ((i+5) > bitLen) - return E_ReturnType::BAD_LENGTH; - - int wet_Type = getbituInc(data,i,1); - int nw = wet_Type==0?6:8; - - tracepdeex(CMPSSRTRCLVL+2,std::cout, " wet: "); - - double wet_offset = getbituInc(data,i,4) * 0.02; - for (int grd=0; grd bitLen) - return E_ReturnType::BAD_LENGTH; - - atmRegion.tropData[tAtm].gridWet[grd] = decodeCmpSsrField (data,i,nw,0.004,wet_offset); - tracepdeex(CMPSSRTRCLVL+2,std::cout, " %.3f", atmRegion.tropData[tAtm].gridWet[grd]); - } - } - ssrAtmUpdated = true; - } - - if (stecType == 0) - { - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_ATM %s %s region %2d, nsig:%d",ssrMeta.receivedTime.to_string().c_str(), (tropType>0)?"trp":" ", regionID, compactSsrSignalIndex[regionID].size()); - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-2); - return i; - } - - int n=0; - compactSsrIonoIndex[regionID].clear(); - for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) - if (getbituInc(data,i,1)==1) - compactSsrIonoIndex[regionID][n++] = Sat; - - tracepdeex(CMPSSRTRCLVL+2,std::cout, "\n Ionosphere: "); - - for (auto& [indx, Sat] : compactSsrIonoIndex[regionID]) - { - tracepdeex(CMPSSRTRCLVL+2,std::cout, "\n %s (%.3f)", Sat.id().c_str(), atmRegion.tropData[tAtm].sigma); - atmRegion.stecData[Sat][tAtm].sigma = uraSsr[getbituInc(data,i,6)]/1000; - if (stecType & 1) - { - if ((i+16) > bitLen) - return E_ReturnType::BAD_LENGTH; - - int polyType = getbituInc(data,i, 2); - atmRegion.stecData[Sat][tAtm].poly[0] = decodeCmpSsrField (data,i,14,0.05,0); - tracepdeex(CMPSSRTRCLVL+2,std::cout, " poly: %.3f", atmRegion.stecData[Sat][tAtm].poly[0]); - - if (polyType>0) - { - if ((i+24) > bitLen) - return E_ReturnType::BAD_LENGTH; - - atmRegion.stecData[Sat][tAtm].poly[1] = decodeCmpSsrField (data,i,12,0.02,0); - atmRegion.stecData[Sat][tAtm].poly[2] = decodeCmpSsrField (data,i,12,0.02,0); - - tracepdeex(CMPSSRTRCLVL+2,std::cout, " %.3f %.3f", atmRegion.stecData[Sat][tAtm].poly[1], atmRegion.stecData[Sat][tAtm].poly[2]); - } - - if (polyType>1) - { - if ((i+10) > bitLen) - return E_ReturnType::BAD_LENGTH; - - atmRegion.stecData[Sat][tAtm].poly[3] = decodeCmpSsrField (data,i,10,0.02,0); - - tracepdeex(CMPSSRTRCLVL+2,std::cout, " %.3f", atmRegion.stecData[Sat][tAtm].poly[3]); - } - - if (polyType==3) - { - if ((i+16) > bitLen) - return E_ReturnType::BAD_LENGTH; - - atmRegion.stecData[Sat][tAtm].poly[4] = decodeCmpSsrField (data,i, 8,0.005,0); - atmRegion.stecData[Sat][tAtm].poly[5] = decodeCmpSsrField (data,i, 8,0.005,0); - tracepdeex(CMPSSRTRCLVL+2,std::cout, " %.3f %.3f", atmRegion.stecData[Sat][tAtm].poly[4], atmRegion.stecData[Sat][tAtm].poly[5]); - } - } - - if (stecType > 1) - { - int gridType = getbituInc(data,i, 2); - int ni = 4; - double scale = 0; - switch (gridType) - { - case 0: ni=4; scale=0.04; break; - case 1: ni=4; scale=0.12; break; - case 2: ni=5; scale=0.16; break; - case 3: ni=7; scale=0.24; break; - } - - tracepdeex(CMPSSRTRCLVL+2,std::cout, " grid: %d", gridType); - - for (int grd=0; grd bitLen) - return -1; - atmRegion.stecData[Sat][tAtm].grid[grd] = decodeCmpSsrField (data,i,ni,scale,0); - } - } - } - - ssrAtmUpdated = true; - tracepdeex(CMPSSRTRCLVL, std::cout,"\n#CMPSSR_ATM %s %s ion region %2d, nsig:%d",ssrMeta.receivedTime.to_string().c_str(), (tropType>0)?"trp":" ", regionID, compactSsrSignalIndex[regionID].size()); - - if (ssrMeta.multipleMessage == 0) - copySSRCorrections(-2); - - return i; + if (compactSSRIod < 0) + return 0; + + int bitLen = data.size() * 8; + + SSRMeta ssrMeta; + + int i = decodeSSR_header(data, ssrMeta, false); + + if (ssrMeta.receivedTime > compactSsrLastTime) + compactSsrLastTime = ssrMeta.receivedTime; + if (now < compactSsrLastTime) + return E_ReturnType::WAIT; + + if (i < 0) + { + tracepdeex(4, std::cout, "Compact atmosphere message inconsistent with mask IOD\n"); + return 1; + } + + if ((i + 21) > bitLen) + return E_ReturnType::BAD_LENGTH; + + compactSsrAtmStorage.ssrMeta = ssrMeta; + + int tropType = getbituInc(data, i, 2); + int stecType = getbituInc(data, i, 2); + int regionID = getbituInc(data, i, 5); + int numbGrid = getbituInc(data, i, 6); + + SSRAtmRegion& atmRegion = compactSsrAtmStorage.atmosRegionsMap[regionID]; + GTime tAtm = ssrMeta.receivedTime; + + if (tropType > 0) + { + atmRegion.tropData[tAtm].sigma = uraSsr[getbituInc(data, i, 6)] / 1000; + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + "\n Trop (%.3f)", + atmRegion.tropData[tAtm].sigma + ); + + if (tropType & 1) + { + if ((i + 11) > bitLen) + return E_ReturnType::BAD_LENGTH; + + int dry_Type = getbituInc(data, i, 2); + atmRegion.tropData[tAtm].polyDry[0] = decodeCmpSsrField(data, i, 9, 0.004, 2.3); + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + " dry: %.3f", + atmRegion.tropData[tAtm].polyDry[0] + ); + + if (dry_Type > 0) + { + if ((i + 14) > bitLen) + return E_ReturnType::BAD_LENGTH; + + atmRegion.tropData[tAtm].polyDry[1] = decodeCmpSsrField(data, i, 7, 0.002, 0); + atmRegion.tropData[tAtm].polyDry[2] = decodeCmpSsrField(data, i, 7, 0.002, 0); + + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + " %.3f %.3f", + atmRegion.tropData[tAtm].polyDry[1], + atmRegion.tropData[tAtm].polyDry[2] + ); + } + if (dry_Type > 1) + { + if ((i + 14) > bitLen) + return E_ReturnType::BAD_LENGTH; + + atmRegion.tropData[tAtm].polyDry[3] = decodeCmpSsrField(data, i, 7, 0.001, 0); + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + " %.3f", + atmRegion.tropData[tAtm].polyDry[3] + ); + } + } + if (tropType > 1) + { + if ((i + 5) > bitLen) + return E_ReturnType::BAD_LENGTH; + + int wet_Type = getbituInc(data, i, 1); + int nw = wet_Type == 0 ? 6 : 8; + + tracepdeex(CMPSSRTRCLVL + 2, std::cout, " wet: "); + + double wet_offset = getbituInc(data, i, 4) * 0.02; + for (int grd = 0; grd < numbGrid; grd++) + { + if ((i + nw) > bitLen) + return E_ReturnType::BAD_LENGTH; + + atmRegion.tropData[tAtm].gridWet[grd] = + decodeCmpSsrField(data, i, nw, 0.004, wet_offset); + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + " %.3f", + atmRegion.tropData[tAtm].gridWet[grd] + ); + } + } + ssrAtmUpdated = true; + } + + if (stecType == 0) + { + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_ATM %s %s region %2d, nsig:%d", + ssrMeta.receivedTime.to_string().c_str(), + (tropType > 0) ? "trp" : " ", + regionID, + compactSsrSignalIndex[regionID].size() + ); + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-2); + return i; + } + + int n = 0; + compactSsrIonoIndex[regionID].clear(); + for (auto& [indx, Sat] : compactSsrSatelliteIndex[-1]) + if (getbituInc(data, i, 1) == 1) + compactSsrIonoIndex[regionID][n++] = Sat; + + tracepdeex(CMPSSRTRCLVL + 2, std::cout, "\n Ionosphere: "); + + for (auto& [indx, Sat] : compactSsrIonoIndex[regionID]) + { + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + "\n %s (%.3f)", + Sat.id().c_str(), + atmRegion.tropData[tAtm].sigma + ); + atmRegion.stecData[Sat][tAtm].sigma = uraSsr[getbituInc(data, i, 6)] / 1000; + if (stecType & 1) + { + if ((i + 16) > bitLen) + return E_ReturnType::BAD_LENGTH; + + int polyType = getbituInc(data, i, 2); + atmRegion.stecData[Sat][tAtm].poly[0] = decodeCmpSsrField(data, i, 14, 0.05, 0); + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + " poly: %.3f", + atmRegion.stecData[Sat][tAtm].poly[0] + ); + + if (polyType > 0) + { + if ((i + 24) > bitLen) + return E_ReturnType::BAD_LENGTH; + + atmRegion.stecData[Sat][tAtm].poly[1] = decodeCmpSsrField(data, i, 12, 0.02, 0); + atmRegion.stecData[Sat][tAtm].poly[2] = decodeCmpSsrField(data, i, 12, 0.02, 0); + + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + " %.3f %.3f", + atmRegion.stecData[Sat][tAtm].poly[1], + atmRegion.stecData[Sat][tAtm].poly[2] + ); + } + + if (polyType > 1) + { + if ((i + 10) > bitLen) + return E_ReturnType::BAD_LENGTH; + + atmRegion.stecData[Sat][tAtm].poly[3] = decodeCmpSsrField(data, i, 10, 0.02, 0); + + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + " %.3f", + atmRegion.stecData[Sat][tAtm].poly[3] + ); + } + + if (polyType == 3) + { + if ((i + 16) > bitLen) + return E_ReturnType::BAD_LENGTH; + + atmRegion.stecData[Sat][tAtm].poly[4] = decodeCmpSsrField(data, i, 8, 0.005, 0); + atmRegion.stecData[Sat][tAtm].poly[5] = decodeCmpSsrField(data, i, 8, 0.005, 0); + tracepdeex( + CMPSSRTRCLVL + 2, + std::cout, + " %.3f %.3f", + atmRegion.stecData[Sat][tAtm].poly[4], + atmRegion.stecData[Sat][tAtm].poly[5] + ); + } + } + + if (stecType > 1) + { + int gridType = getbituInc(data, i, 2); + int ni = 4; + double scale = 0; + switch (gridType) + { + case 0: + ni = 4; + scale = 0.04; + break; + case 1: + ni = 4; + scale = 0.12; + break; + case 2: + ni = 5; + scale = 0.16; + break; + case 3: + ni = 7; + scale = 0.24; + break; + } + + tracepdeex(CMPSSRTRCLVL + 2, std::cout, " grid: %d", gridType); + + for (int grd = 0; grd < numbGrid; grd++) + { + if ((i + ni) > bitLen) + return -1; + atmRegion.stecData[Sat][tAtm].grid[grd] = decodeCmpSsrField(data, i, ni, scale, 0); + } + } + } + + ssrAtmUpdated = true; + tracepdeex( + CMPSSRTRCLVL, + std::cout, + "\n#CMPSSR_ATM %s %s ion region %2d, nsig:%d", + ssrMeta.receivedTime.to_string().c_str(), + (tropType > 0) ? "trp" : " ", + regionID, + compactSsrSignalIndex[regionID].size() + ); + + if (ssrMeta.multipleMessage == 0) + copySSRCorrections(-2); + + return i; } -int decodeSSR_service( - vector& data) +int decodeSSR_service(vector& data) { - int bitLen = data.size()*8; - - int i=16; - bool multimessage = getbituInc(data,i,1); - int dataPacks = getbituInc(data,i,2)+1; - int messageCount = getbituInc(data,i,3); - - if ((i+40*dataPacks) > bitLen) - return E_ReturnType::BAD_LENGTH; - - if ( lastServiceMessage >=0 - && lastServiceMessage != (messageCount-1)) - { - tracepdeex(2,std::cout,"Missing packets in compact SSR service messages %d -> %d\n", - lastServiceMessage,messageCount ); - - compactSsrServiceMessage.clear(); - lastServiceMessage = -1; - } - - if ( lastServiceMessage < 0 - && messageCount != 0) - { - tracepdeex(2,std::cout,"Missing packets in compact SSR service messages %d -> %d\n", - lastServiceMessage,messageCount); - return i; - } - - lastServiceMessage = messageCount; - - for (int i = 0; i < 40 * dataPacks; i++) - { - unsigned char newbyte = getbituInc(data,i,8); - compactSsrServiceMessage.push_back(newbyte); - } - - if (multimessage) - processServiceData(compactSsrServiceMessage); - - return i; + int bitLen = data.size() * 8; + + int i = 16; + bool multimessage = getbituInc(data, i, 1); + int dataPacks = getbituInc(data, i, 2) + 1; + int messageCount = getbituInc(data, i, 3); + + if ((i + 40 * dataPacks) > bitLen) + return E_ReturnType::BAD_LENGTH; + + if (lastServiceMessage >= 0 && lastServiceMessage != (messageCount - 1)) + { + tracepdeex( + 2, + std::cout, + "Missing packets in compact SSR service messages %d -> %d\n", + lastServiceMessage, + messageCount + ); + + compactSsrServiceMessage.clear(); + lastServiceMessage = -1; + } + + if (lastServiceMessage < 0 && messageCount != 0) + { + tracepdeex( + 2, + std::cout, + "Missing packets in compact SSR service messages %d -> %d\n", + lastServiceMessage, + messageCount + ); + return i; + } + + lastServiceMessage = messageCount; + + for (int i = 0; i < 40 * dataPacks; i++) + { + unsigned char newbyte = getbituInc(data, i, 8); + compactSsrServiceMessage.push_back(newbyte); + } + + if (multimessage) + processServiceData(compactSsrServiceMessage); + + return i; } -int decodecompactSSR( - vector& data, - GTime now) +int decodecompactSSR(vector& data, GTime now) { - if ( compactSsrLastTime != GTime::noTime() - && now < compactSsrLastTime) - { - return E_ReturnType::WAIT; - } - - if (data.size() < 7) - return E_ReturnType::BAD_LENGTH; - - int stype = getbitu(data, 12, 4); - - CompactSSRSubtype subtype = CompactSSRSubtype::_from_integral(stype); - - switch (subtype) - { - case CompactSSRSubtype::MSK: return decodeSSR_mask (data, now); - case CompactSSRSubtype::ORB: return decodeSSR_orbit (data, now); - case CompactSSRSubtype::CLK: return decodeSSR_clock (data, now); - case CompactSSRSubtype::COD: return decodeSSR_code_bias (data, now); - case CompactSSRSubtype::PHS: return decodeSSR_phas_bias (data, now); - case CompactSSRSubtype::BIA: return decodeSSR_comb_bias (data, now); - case CompactSSRSubtype::URA: return decodeSSR_URA (data, now); - case CompactSSRSubtype::TEC: return decodeSSR_slant_TEC (data, now); - case CompactSSRSubtype::GRD: return decodeSSR_grid_ATM (data, now); - case CompactSSRSubtype::SRV: return decodeSSR_service (data); - case CompactSSRSubtype::CMB: return decodeSSR_combined (data, now); - case CompactSSRSubtype::ATM: return decodeSSR_comp_ATM (data, now); - - default: tracepdeex(2, std::cout,"Submessage type %d not supported\n", stype); - - return E_ReturnType::OK; - } + if (compactSsrLastTime != GTime::noTime() && now < compactSsrLastTime) + { + return E_ReturnType::WAIT; + } + + if (data.size() < 7) + return E_ReturnType::BAD_LENGTH; + + int stype = getbitu(data, 12, 4); + + CompactSSRSubtype subtype = int_to_enum(stype); + + switch (subtype) + { + case CompactSSRSubtype::MSK: + return decodeSSR_mask(data, now); + case CompactSSRSubtype::ORB: + return decodeSSR_orbit(data, now); + case CompactSSRSubtype::CLK: + return decodeSSR_clock(data, now); + case CompactSSRSubtype::COD: + return decodeSSR_code_bias(data, now); + case CompactSSRSubtype::PHS: + return decodeSSR_phas_bias(data, now); + case CompactSSRSubtype::BIA: + return decodeSSR_comb_bias(data, now); + case CompactSSRSubtype::URA: + return decodeSSR_URA(data, now); + case CompactSSRSubtype::TEC: + return decodeSSR_slant_TEC(data, now); + case CompactSSRSubtype::GRD: + return decodeSSR_grid_ATM(data, now); + case CompactSSRSubtype::SRV: + return decodeSSR_service(data); + case CompactSSRSubtype::CMB: + return decodeSSR_combined(data, now); + case CompactSSRSubtype::ATM: + return decodeSSR_comp_ATM(data, now); + + default: + tracepdeex(2, std::cout, "Submessage type %d not supported\n", stype); + + return E_ReturnType::OK; + } } diff --git a/src/cpp/other_ssr/prototypeCmpSSREncode.cpp b/src/cpp/other_ssr/prototypeCmpSSREncode.cpp index 185421645..4c82b6eba 100644 --- a/src/cpp/other_ssr/prototypeCmpSSREncode.cpp +++ b/src/cpp/other_ssr/prototypeCmpSSREncode.cpp @@ -1,1509 +1,1630 @@ #include +#include "common/acsConfig.hpp" +#include "common/constants.hpp" +#include "common/rtcmEncoder.hpp" +#include "common/satSys.hpp" +#include "common/ssr.hpp" +#include "other_ssr/otherSSR.hpp" using std::tuple; -#include "rtcmEncoder.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "otherSSR.hpp" -#include "satSys.hpp" -#include "ssr.hpp" +int currentIOD = -1; +map currentSatMap; +map> currentBiasMap; +map> currentCellMap; +map currentSatIodes; -int currentIOD = -1; -map currentSatMap; -map> currentBiasMap; -map> currentCellMap; -map currentSatIodes; +map> phaseBiasOffset; +map> phaseDiscCountr; -map> phaseBiasOffset; -map> phaseDiscCountr; +map> cmpSSRServiceMessages; -map> cmpSSRServiceMessages; - -map compactSSRSysMap = -{ - {E_Sys::GPS, 0}, - {E_Sys::GLO, 1}, - {E_Sys::GAL, 2}, - {E_Sys::BDS, 3}, - {E_Sys::QZS, 4}, - {E_Sys::SBS, 5} +map compactSSRSysMap = { + {E_Sys::GPS, 0}, + {E_Sys::GLO, 1}, + {E_Sys::GAL, 2}, + {E_Sys::BDS, 3}, + {E_Sys::QZS, 4}, + {E_Sys::SBS, 5} }; -map> cmpSSRIndex2Code = -{ - { E_Sys::GPS, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1P}, - { 2, E_ObsCode::L1W}, - { 3, E_ObsCode::L1S}, - { 4, E_ObsCode::L1L}, - { 5, E_ObsCode::L1X}, - { 6, E_ObsCode::L2S}, - { 7, E_ObsCode::L2L}, - { 8, E_ObsCode::L2X}, - { 9, E_ObsCode::L2P}, - {10, E_ObsCode::L2W}, - {11, E_ObsCode::L5I}, - {12, E_ObsCode::L5Q}, - {13, E_ObsCode::L5X} - } - }, - { E_Sys::GLO, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1P}, - { 2, E_ObsCode::L2C}, - { 3, E_ObsCode::L2P}, - { 4, E_ObsCode::L4A}, - { 5, E_ObsCode::L4B}, - { 6, E_ObsCode::L4X}, - { 7, E_ObsCode::L6A}, - { 8, E_ObsCode::L6B}, - { 9, E_ObsCode::L6X}, - {10, E_ObsCode::L3I}, - {11, E_ObsCode::L3Q}, - {12, E_ObsCode::L3X} - } - }, - { E_Sys::GAL, - { - { 0, E_ObsCode::L1B}, - { 1, E_ObsCode::L1C}, - { 2, E_ObsCode::L1X}, - { 3, E_ObsCode::L5I}, - { 4, E_ObsCode::L5Q}, - { 5, E_ObsCode::L5X}, - { 6, E_ObsCode::L7I}, - { 7, E_ObsCode::L7Q}, - { 8, E_ObsCode::L7X}, - { 9, E_ObsCode::L8I}, - {10, E_ObsCode::L8Q}, - {11, E_ObsCode::L8X} - } - }, - { E_Sys::QZS, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1S}, - { 2, E_ObsCode::L1L}, - { 3, E_ObsCode::L1X}, - { 4, E_ObsCode::L2S}, - { 5, E_ObsCode::L2L}, - { 6, E_ObsCode::L2X}, - { 7, E_ObsCode::L5I}, - { 8, E_ObsCode::L5Q}, - { 9, E_ObsCode::L5X} - } - }, - { E_Sys::BDS, - { - { 0, E_ObsCode::L2I}, - { 1, E_ObsCode::L2Q}, - { 2, E_ObsCode::L2X}, - { 3, E_ObsCode::L6I}, - { 4, E_ObsCode::L6Q}, - { 5, E_ObsCode::L6X}, - { 6, E_ObsCode::L7I}, - { 7, E_ObsCode::L7Q}, - { 8, E_ObsCode::L7X} - } - } +map> cmpSSRIndex2Code = { + {E_Sys::GPS, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1P}, + {2, E_ObsCode::L1W}, + {3, E_ObsCode::L1S}, + {4, E_ObsCode::L1L}, + {5, E_ObsCode::L1X}, + {6, E_ObsCode::L2S}, + {7, E_ObsCode::L2L}, + {8, E_ObsCode::L2X}, + {9, E_ObsCode::L2P}, + {10, E_ObsCode::L2W}, + {11, E_ObsCode::L5I}, + {12, E_ObsCode::L5Q}, + {13, E_ObsCode::L5X}}}, + {E_Sys::GLO, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1P}, + {2, E_ObsCode::L2C}, + {3, E_ObsCode::L2P}, + {4, E_ObsCode::L4A}, + {5, E_ObsCode::L4B}, + {6, E_ObsCode::L4X}, + {7, E_ObsCode::L6A}, + {8, E_ObsCode::L6B}, + {9, E_ObsCode::L6X}, + {10, E_ObsCode::L3I}, + {11, E_ObsCode::L3Q}, + {12, E_ObsCode::L3X}}}, + {E_Sys::GAL, + {{0, E_ObsCode::L1B}, + {1, E_ObsCode::L1C}, + {2, E_ObsCode::L1X}, + {3, E_ObsCode::L5I}, + {4, E_ObsCode::L5Q}, + {5, E_ObsCode::L5X}, + {6, E_ObsCode::L7I}, + {7, E_ObsCode::L7Q}, + {8, E_ObsCode::L7X}, + {9, E_ObsCode::L8I}, + {10, E_ObsCode::L8Q}, + {11, E_ObsCode::L8X}}}, + {E_Sys::QZS, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1S}, + {2, E_ObsCode::L1L}, + {3, E_ObsCode::L1X}, + {4, E_ObsCode::L2S}, + {5, E_ObsCode::L2L}, + {6, E_ObsCode::L2X}, + {7, E_ObsCode::L5I}, + {8, E_ObsCode::L5Q}, + {9, E_ObsCode::L5X}}}, + {E_Sys::BDS, + {{0, E_ObsCode::L2I}, + {1, E_ObsCode::L2Q}, + {2, E_ObsCode::L2X}, + {3, E_ObsCode::L6I}, + {4, E_ObsCode::L6Q}, + {5, E_ObsCode::L6X}, + {6, E_ObsCode::L7I}, + {7, E_ObsCode::L7Q}, + {8, E_ObsCode::L7X}}} }; -vector encodecompactMSK( - map& orbClkMap, - map& codeBiasMap, - map& phaseBiasMap, - SSRAtm ssrAtm, - int updateIntIndex) +vector encodecompactMSK( + map& orbClkMap, + map& codeBiasMap, + map& phaseBiasMap, + SSRAtm ssrAtm, + int updateIntIndex +) { - vector buffer; - - currentSatMap .clear(); - currentBiasMap .clear(); - currentCellMap .clear(); - currentSatIodes .clear(); - - int epochTime1s = -1; - - map sysNsat; - map availSat; - - /* get available sat from OrbClkMaps */ - for (auto& [Sat, data] : orbClkMap) - if (availSat.find(Sat) == availSat.end()) - { - availSat[Sat] = sysNsat[Sat.sys]; - sysNsat[Sat.sys]++; - - currentSatIodes[Sat] = data.ssrEph.iode; - - if (epochTime1s < 0) - { - epochTime1s = data.ssrEph.ssrMeta.epochTime1s; - } - } - - map sysNbias; - map> availBias; - - /* get available sat and biases from CodBiaMaps */ - for (auto& [Sat, data] : codeBiasMap) - for (auto& [code, bias] : data.obsCodeBiasMap) - { - if (availSat.find(Sat) == availSat.end()) - { - availSat[Sat] = sysNsat[Sat.sys]; - sysNsat[Sat.sys]++; - - if (epochTime1s<0) - { - epochTime1s = data.ssrMeta.epochTime1s; - } - } - - if (availBias[Sat.sys].find(code) == availBias[Sat.sys].end()) - { - availBias[Sat.sys][code] = sysNbias[Sat.sys]; - sysNbias[Sat.sys]++; - } - } - - for (auto& [Sat,data] : phaseBiasMap) - for (auto& [code,bia] : data.obsCodeBiasMap) - { - if (availSat.find(Sat) == availSat.end()) - { - availSat[Sat] = sysNsat[Sat.sys]++; - if (epochTime1s<0) - { - epochTime1s = data.ssrMeta.epochTime1s; - } - } - - if (availBias[Sat.sys].find(code) == availBias[Sat.sys].end()) - availBias[Sat.sys][code] = sysNbias[Sat.sys]++; - } - - for (auto& [regID, regData] : ssrAtm.atmosRegionsMap) - for (auto& [sat, stecData] : regData.stecData) - { - if (availSat.find(sat) == availSat.end()) - { - availSat[sat] = sysNsat[sat.sys]++; - if (epochTime1s<0) - { - epochTime1s = ssrAtm.ssrMeta.epochTime1s; - } - } - } - - int nSat_all = 0; - int nSig_all = 0; - int bitLen = 49; - - for (auto& [Sat, ind] : availSat) - ind = -1; - - for (auto& [sys, nsat] : sysNsat) - { - sysNbias[sys] = 0; - nsat = 0; - bitLen += 61; - for (auto& [code,ind] : availBias[sys]) - ind=-1; - - for (auto& [ind,code] : cmpSSRIndex2Code[sys]) - if (availBias[sys].find(code) != availBias[sys].end()) - { - availBias[sys][code] = sysNbias[sys]; - currentBiasMap[sys][sysNbias[sys]++] = code; - } - - for (int s = 1; s < 41; s++) - { - SatSys Sat; - Sat.sys = sys; - Sat.prn = s; - if (availSat.find(Sat) != availSat.end()) - { - currentSatMap[nSat_all++] = Sat; - availSat[Sat] = nsat++; - } - else - continue; - - if (acsConfig.ssrOpts.cmpssr_cell_mask) - { - bitLen += sysNbias[sys]; - for (auto& [ind,code] : currentBiasMap[sys]) - { - bool noCodes = ( codeBiasMap .find(Sat) == codeBiasMap .end() - || codeBiasMap[Sat].obsCodeBiasMap .find(code) == codeBiasMap[Sat].obsCodeBiasMap .end()); - - bool noPhase = ( phaseBiasMap .find(Sat) == phaseBiasMap .end() - || phaseBiasMap[Sat].obsCodeBiasMap .find(code) == phaseBiasMap[Sat].obsCodeBiasMap .end()); - - if (noCodes && noPhase) - continue; - - currentCellMap[nSig_all] = {Sat, code}; - nSig_all++; - } - } - } - } - - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - if (++currentIOD > 15) - currentIOD = 0; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 1); - i = setbituInc(buf,i,20, epochTime1s); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, 0); - i = setbituInc(buf,i,4, currentIOD); - i = setbituInc(buf,i,4, sysNsat.size()); - - for (auto& [sys,nbia] : sysNsat) - { - i = setbituInc(buf,i,4, compactSSRSysMap[sys]); - int isys = i; - - for (auto& [sat,ind] : availSat) - if (sat.sys == sys) - setbituInc(buf,isys+sat.prn,1,1); - isys += 40; - - for (auto& [ind,code] : cmpSSRIndex2Code[sys]) - if (availBias[sys].find(code) != availBias[sys].end()) - setbituInc(buf,isys+ind,1,1); - isys += 16; - - i=isys; - if (!acsConfig.ssrOpts.cmpssr_cell_mask) - i = setbituInc(buf,i,1,0); - else - { - i = setbituInc(buf,i,1,1); - isys = i; - - for (auto& [sat,satInd] : availSat) - { - if (satInd < 0) continue; - if (sat.sys != sys) continue; - - for (auto& [code, codInd] : availBias[sys]) - { - if (codInd < 0) - continue; - - int ind = satInd * nbia + codInd; - setbituInc(buf,isys+ind,1,1); - } - } - i = isys + nbia * sysNsat[sys]; - } - } - - return buffer; + vector buffer; + + currentSatMap.clear(); + currentBiasMap.clear(); + currentCellMap.clear(); + currentSatIodes.clear(); + + int epochTime1s = -1; + + map sysNsat; + map availSat; + + /* get available sat from OrbClkMaps */ + for (auto& [Sat, data] : orbClkMap) + if (availSat.find(Sat) == availSat.end()) + { + availSat[Sat] = sysNsat[Sat.sys]; + sysNsat[Sat.sys]++; + + currentSatIodes[Sat] = data.ssrEph.iode; + + if (epochTime1s < 0) + { + epochTime1s = data.ssrEph.ssrMeta.epochTime1s; + } + } + + map sysNbias; + map> availBias; + + /* get available sat and biases from CodBiaMaps */ + for (auto& [Sat, data] : codeBiasMap) + for (auto& [code, bias] : data.obsCodeBiasMap) + { + if (availSat.find(Sat) == availSat.end()) + { + availSat[Sat] = sysNsat[Sat.sys]; + sysNsat[Sat.sys]++; + + if (epochTime1s < 0) + { + epochTime1s = data.ssrMeta.epochTime1s; + } + } + + if (availBias[Sat.sys].find(code) == availBias[Sat.sys].end()) + { + availBias[Sat.sys][code] = sysNbias[Sat.sys]; + sysNbias[Sat.sys]++; + } + } + + for (auto& [Sat, data] : phaseBiasMap) + for (auto& [code, bia] : data.obsCodeBiasMap) + { + if (availSat.find(Sat) == availSat.end()) + { + availSat[Sat] = sysNsat[Sat.sys]++; + if (epochTime1s < 0) + { + epochTime1s = data.ssrMeta.epochTime1s; + } + } + + if (availBias[Sat.sys].find(code) == availBias[Sat.sys].end()) + availBias[Sat.sys][code] = sysNbias[Sat.sys]++; + } + + for (auto& [regID, regData] : ssrAtm.atmosRegionsMap) + for (auto& [sat, stecData] : regData.stecData) + { + if (availSat.find(sat) == availSat.end()) + { + availSat[sat] = sysNsat[sat.sys]++; + if (epochTime1s < 0) + { + epochTime1s = ssrAtm.ssrMeta.epochTime1s; + } + } + } + + int nSat_all = 0; + int nSig_all = 0; + int bitLen = 49; + + for (auto& [Sat, ind] : availSat) + ind = -1; + + for (auto& [sys, nsat] : sysNsat) + { + sysNbias[sys] = 0; + nsat = 0; + bitLen += 61; + for (auto& [code, ind] : availBias[sys]) + ind = -1; + + for (auto& [ind, code] : cmpSSRIndex2Code[sys]) + if (availBias[sys].find(code) != availBias[sys].end()) + { + availBias[sys][code] = sysNbias[sys]; + currentBiasMap[sys][sysNbias[sys]++] = code; + } + + for (int s = 1; s < 41; s++) + { + SatSys Sat; + Sat.sys = sys; + Sat.prn = s; + if (availSat.find(Sat) != availSat.end()) + { + currentSatMap[nSat_all++] = Sat; + availSat[Sat] = nsat++; + } + else + continue; + + if (acsConfig.ssrOpts.cmpssr_cell_mask) + { + bitLen += sysNbias[sys]; + for (auto& [ind, code] : currentBiasMap[sys]) + { + bool noCodes = + (codeBiasMap.find(Sat) == codeBiasMap.end() || + codeBiasMap[Sat].obsCodeBiasMap.find(code) == + codeBiasMap[Sat].obsCodeBiasMap.end()); + + bool noPhase = + (phaseBiasMap.find(Sat) == phaseBiasMap.end() || + phaseBiasMap[Sat].obsCodeBiasMap.find(code) == + phaseBiasMap[Sat].obsCodeBiasMap.end()); + + if (noCodes && noPhase) + continue; + + currentCellMap[nSig_all] = {Sat, code}; + nSig_all++; + } + } + } + } + + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + if (++currentIOD > 15) + currentIOD = 0; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 1); + i = setbituInc(buf, i, 20, epochTime1s); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, 0); + i = setbituInc(buf, i, 4, currentIOD); + i = setbituInc(buf, i, 4, sysNsat.size()); + + for (auto& [sys, nbia] : sysNsat) + { + i = setbituInc(buf, i, 4, compactSSRSysMap[sys]); + int isys = i; + + for (auto& [sat, ind] : availSat) + if (sat.sys == sys) + setbituInc(buf, isys + sat.prn, 1, 1); + isys += 40; + + for (auto& [ind, code] : cmpSSRIndex2Code[sys]) + if (availBias[sys].find(code) != availBias[sys].end()) + setbituInc(buf, isys + ind, 1, 1); + isys += 16; + + i = isys; + if (!acsConfig.ssrOpts.cmpssr_cell_mask) + i = setbituInc(buf, i, 1, 0); + else + { + i = setbituInc(buf, i, 1, 1); + isys = i; + + for (auto& [sat, satInd] : availSat) + { + if (satInd < 0) + continue; + if (sat.sys != sys) + continue; + + for (auto& [code, codInd] : availBias[sys]) + { + if (codInd < 0) + continue; + + int ind = satInd * nbia + codInd; + setbituInc(buf, isys + ind, 1, 1); + } + } + i = isys + nbia * sysNsat[sys]; + } + } + + return buffer; } -vector encodecompactORB( - map& orbClkMap, - int updateIntIndex, - bool last) +vector encodecompactORB(map& orbClkMap, int updateIntIndex, bool last) { - vector buffer; - - int bitLen = 37; - for (auto& [ind, sat] : currentSatMap) - { - bitLen +=49; - if (sat.sys == +E_Sys::GAL) - bitLen +=2; - } - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - auto& [Sat, ssrOut1] = *orbClkMap.begin(); - auto& ssrEph = ssrOut1.ssrEph; - auto& ssrMeta = ssrEph.ssrMeta; - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 2); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - - for (auto& [ind,sat] : currentSatMap) - { - int iode = 0; - int dRad = -16384; - int dAlg = - 4096; - int dCrs = - 4096; - - if (orbClkMap.find(sat) != orbClkMap.end()) - { - iode = orbClkMap[sat].ssrEph.iode; - if (iode == currentSatIodes[sat]) - { - dRad = (int)round(orbClkMap[sat].ssrEph.deph[0]*625 ); - dAlg = (int)round(orbClkMap[sat].ssrEph.deph[1]*156.25); - dCrs = (int)round(orbClkMap[sat].ssrEph.deph[2]*156.25); - - if (abs(dRad) > 16383 - || abs(dRad) > 4095 - || abs(dRad) > 4095) - { - dRad = -16384; - dAlg = - 4096; - dCrs = - 4096; - } - } - } - - int ns = 8; - if (sat.sys == +E_Sys::GAL) - ns=10; - - i = setbituInc(buf,i,ns, iode); - i = setbitsInc(buf,i,15, dRad); - i = setbitsInc(buf,i,13, dAlg); - i = setbitsInc(buf,i,13, dCrs); - } - - return buffer; + vector buffer; + + int bitLen = 37; + for (auto& [ind, sat] : currentSatMap) + { + bitLen += 49; + if (sat.sys == E_Sys::GAL) + bitLen += 2; + } + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + auto& [Sat, ssrOut1] = *orbClkMap.begin(); + auto& ssrEph = ssrOut1.ssrEph; + auto& ssrMeta = ssrEph.ssrMeta; + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 2); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + + for (auto& [ind, sat] : currentSatMap) + { + int iode = 0; + int dRad = -16384; + int dAlg = -4096; + int dCrs = -4096; + + if (orbClkMap.find(sat) != orbClkMap.end()) + { + iode = orbClkMap[sat].ssrEph.iode; + if (iode == currentSatIodes[sat]) + { + dRad = (int)round(orbClkMap[sat].ssrEph.deph[0] * 625); + dAlg = (int)round(orbClkMap[sat].ssrEph.deph[1] * 156.25); + dCrs = (int)round(orbClkMap[sat].ssrEph.deph[2] * 156.25); + + if (abs(dRad) > 16383 || abs(dRad) > 4095 || abs(dRad) > 4095) + { + dRad = -16384; + dAlg = -4096; + dCrs = -4096; + } + } + } + + int ns = 8; + if (sat.sys == E_Sys::GAL) + ns = 10; + + i = setbituInc(buf, i, ns, iode); + i = setbitsInc(buf, i, 15, dRad); + i = setbitsInc(buf, i, 13, dAlg); + i = setbitsInc(buf, i, 13, dCrs); + } + + return buffer; } -vector encodecompactCLK( - map& orbClkMap, - int updateIntIndex, - bool last) +vector encodecompactCLK(map& orbClkMap, int updateIntIndex, bool last) { - vector buffer; - - int bitLen = 37 + 15*currentSatMap.size(); - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - auto& [Sat, ssrOut1] = *orbClkMap.begin(); - auto& ssrEph = ssrOut1.ssrEph; - auto& ssrMeta = ssrEph.ssrMeta; - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 3); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - - for (auto& [ind,sat] : currentSatMap) - { - int dClk = -16384; - - if (orbClkMap.find(sat) != orbClkMap.end() - && orbClkMap[sat].ssrEph.iode == currentSatIodes[sat]) - { - dClk = (int)round(orbClkMap[sat].ssrClk.dclk[0]*625 ); - if (abs(dClk) > 16383) - dClk = -16384; - } - - i = setbitsInc(buf,i,15, dClk); - } - return buffer; + vector buffer; + + int bitLen = 37 + 15 * currentSatMap.size(); + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + auto& [Sat, ssrOut1] = *orbClkMap.begin(); + auto& ssrEph = ssrOut1.ssrEph; + auto& ssrMeta = ssrEph.ssrMeta; + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 3); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + + for (auto& [ind, sat] : currentSatMap) + { + int dClk = -16384; + + if (orbClkMap.find(sat) != orbClkMap.end() && + orbClkMap[sat].ssrEph.iode == currentSatIodes[sat]) + { + dClk = (int)round(orbClkMap[sat].ssrClk.dclk[0] * 625); + if (abs(dClk) > 16383) + dClk = -16384; + } + + i = setbitsInc(buf, i, 15, dClk); + } + return buffer; } -vector encodecompactURA( - map& orbClkMap, - int updateIntIndex, - bool last) +vector encodecompactURA(map& orbClkMap, int updateIntIndex, bool last) { - vector buffer; - - /* not implemented yet... */ - /* - - int bitLen = 37 + 6*currentSatMap.size(); - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - auto& [Sat, ssrOut1] = *orbClkMap.begin(); - auto& ssrEph = ssrOut1.ssrEph; - auto& ssrMeta = ssrEph.ssrMeta; - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 7); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - - for (auto& [ind,sat] : currentSatMap) - { - double ura = xxxx; - int uraClass = uraToClassValue(ura); - i = setbituInc(buf,i, 6, uraClass); - } - */ - return buffer; + vector buffer; + + /* not implemented yet... */ + /* + + int bitLen = 37 + 6*currentSatMap.size(); + int byteLen = ceil(bitLen/8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + auto& [Sat, ssrOut1] = *orbClkMap.begin(); + auto& ssrEph = ssrOut1.ssrEph; + auto& ssrMeta = ssrEph.ssrMeta; + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last?0:1; + + int i = 0; + i = setbituInc(buf,i,12, 4073); + i = setbituInc(buf,i,4, 7); + i = setbituInc(buf,i,12, timeOfHour); + i = setbituInc(buf,i,4, updateIntIndex); + i = setbituInc(buf,i,1, multipleMessage); + i = setbituInc(buf,i,4, currentIOD); + + for (auto& [ind,sat] : currentSatMap) + { + double ura = xxxx; + int uraClass = uraToClassValue(ura); + i = setbituInc(buf,i, 6, uraClass); + } + */ + return buffer; } /** - Encode hybrid orbit and clock message - warning: Only global mode supported - warning: Requires both orbit and clocks use subtype 2 and 3 for orbit-only or clock-only messages + Encode hybrid orbit and clock message + warning: Only global mode supported + warning: Requires both orbit and clocks use subtype 2 and 3 for orbit-only or clock-only + messages */ -vector encodecompactCMB( - map& orbClkMap, - int updateIntIndex, - bool last) +vector encodecompactCMB(map& orbClkMap, int updateIntIndex, bool last) { - vector buffer; - - int bitLen = 40; - for (auto& [ind,sat] : currentSatMap) - { - bitLen +=64; - if (sat.sys == +E_Sys::GAL) - bitLen +=2; - } - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - auto& [Sat, ssrOut1] = *orbClkMap.begin(); - auto& ssrEph = ssrOut1.ssrEph; - auto& ssrMeta = ssrEph.ssrMeta; - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 11); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - i = setbituInc(buf,i,1, 1); // Orbit corrections required - i = setbituInc(buf,i,1, 1); // Clock corrections required - i = setbituInc(buf,i,1, 0); // Regional corrections not supported - - for (auto& [ind,sat] : currentSatMap) - { - int iode = 0; - int dRad = -16384; - int dAlg = - 4096; - int dCrs = - 4096; - int dClk = -16384; - - if (orbClkMap.find(sat) != orbClkMap.end()) - { - iode = orbClkMap[sat].ssrEph.iode; - if (iode == currentSatIodes[sat]) - { - dRad = (int)round(orbClkMap[sat].ssrEph.deph[0]*625 ); - dAlg = (int)round(orbClkMap[sat].ssrEph.deph[1]*156.25); - dCrs = (int)round(orbClkMap[sat].ssrEph.deph[2]*156.25); - dClk = (int)round(orbClkMap[sat].ssrClk.dclk[0]*625 ); - - if (abs(dRad) > 16383 - || abs(dRad) > 4095 - || abs(dRad) > 4095 - || abs(dClk) > 16383) - { - dRad = -16384; - dAlg = - 4096; - dCrs = - 4096; - dClk = -16384; - } - } - } - - int ns = 8; - if (sat.sys == +E_Sys::GAL) - ns=10; - - i = setbituInc(buf,i,ns, iode); - i = setbitsInc(buf,i,15, dRad); - i = setbitsInc(buf,i,13, dAlg); - i = setbitsInc(buf,i,13, dCrs); - i = setbitsInc(buf,i,15, dClk); - } - - return buffer; + vector buffer; + + int bitLen = 40; + for (auto& [ind, sat] : currentSatMap) + { + bitLen += 64; + if (sat.sys == E_Sys::GAL) + bitLen += 2; + } + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + auto& [Sat, ssrOut1] = *orbClkMap.begin(); + auto& ssrEph = ssrOut1.ssrEph; + auto& ssrMeta = ssrEph.ssrMeta; + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 11); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + i = setbituInc(buf, i, 1, 1); // Orbit corrections required + i = setbituInc(buf, i, 1, 1); // Clock corrections required + i = setbituInc(buf, i, 1, 0); // Regional corrections not supported + + for (auto& [ind, sat] : currentSatMap) + { + int iode = 0; + int dRad = -16384; + int dAlg = -4096; + int dCrs = -4096; + int dClk = -16384; + + if (orbClkMap.find(sat) != orbClkMap.end()) + { + iode = orbClkMap[sat].ssrEph.iode; + if (iode == currentSatIodes[sat]) + { + dRad = (int)round(orbClkMap[sat].ssrEph.deph[0] * 625); + dAlg = (int)round(orbClkMap[sat].ssrEph.deph[1] * 156.25); + dCrs = (int)round(orbClkMap[sat].ssrEph.deph[2] * 156.25); + dClk = (int)round(orbClkMap[sat].ssrClk.dclk[0] * 625); + + if (abs(dRad) > 16383 || abs(dRad) > 4095 || abs(dRad) > 4095 || abs(dClk) > 16383) + { + dRad = -16384; + dAlg = -4096; + dCrs = -4096; + dClk = -16384; + } + } + } + + int ns = 8; + if (sat.sys == E_Sys::GAL) + ns = 10; + + i = setbituInc(buf, i, ns, iode); + i = setbitsInc(buf, i, 15, dRad); + i = setbitsInc(buf, i, 13, dAlg); + i = setbitsInc(buf, i, 13, dCrs); + i = setbitsInc(buf, i, 15, dClk); + } + + return buffer; } -vector encodecompactCOD( - map& codeBiasMap, - int updateIntIndex, - bool last) +vector +encodecompactCOD(map& codeBiasMap, int updateIntIndex, bool last) { - vector buffer; - - int bitLen = 37; - if (acsConfig.ssrOpts.cmpssr_cell_mask) - bitLen += 11*currentCellMap.size(); - else - for (auto& [ind,sat] : currentSatMap) - { - bitLen += 11*currentBiasMap[sat.sys].size(); - } - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - auto& [Sat, ssrCod] = *codeBiasMap.begin(); - auto& ssrMeta = ssrCod.ssrMeta; - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 4); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - - if (acsConfig.ssrOpts.cmpssr_cell_mask) - { - for (auto& [ind, sig] : currentCellMap) - { - auto& [sat, code] = sig; - - int cBias = -1024; - if (codeBiasMap.find(sat) != codeBiasMap.end() - && codeBiasMap[sat].obsCodeBiasMap.find(code) != codeBiasMap[sat].obsCodeBiasMap.end()) - cBias = (int)round(codeBiasMap[sat].obsCodeBiasMap[code].bias * 50); - - if (abs(cBias)>1023) - cBias = -1024; - - i = setbitsInc(buf,i,11, cBias); - } - return buffer; - } - - for (auto& [ind,sat] : currentSatMap) - { - for (auto& [ind2, code] : currentBiasMap[sat.sys]) - { - int cBias = -1024; - if (codeBiasMap.find(sat) != codeBiasMap.end() - && codeBiasMap[sat].obsCodeBiasMap.find(code) != codeBiasMap[sat].obsCodeBiasMap.end()) - cBias = (int)round(codeBiasMap[sat].obsCodeBiasMap[code].bias * 50); - - if (abs(cBias)>1023) - cBias = -1024; - - i = setbitsInc(buf,i,11, cBias); - } - } - - return buffer; + vector buffer; + + int bitLen = 37; + if (acsConfig.ssrOpts.cmpssr_cell_mask) + bitLen += 11 * currentCellMap.size(); + else + for (auto& [ind, sat] : currentSatMap) + { + bitLen += 11 * currentBiasMap[sat.sys].size(); + } + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + auto& [Sat, ssrCod] = *codeBiasMap.begin(); + auto& ssrMeta = ssrCod.ssrMeta; + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 4); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + + if (acsConfig.ssrOpts.cmpssr_cell_mask) + { + for (auto& [ind, sig] : currentCellMap) + { + auto& [sat, code] = sig; + + int cBias = -1024; + if (codeBiasMap.find(sat) != codeBiasMap.end() && + codeBiasMap[sat].obsCodeBiasMap.find(code) != codeBiasMap[sat].obsCodeBiasMap.end()) + cBias = (int)round(codeBiasMap[sat].obsCodeBiasMap[code].bias * 50); + + if (abs(cBias) > 1023) + cBias = -1024; + + i = setbitsInc(buf, i, 11, cBias); + } + return buffer; + } + + for (auto& [ind, sat] : currentSatMap) + { + for (auto& [ind2, code] : currentBiasMap[sat.sys]) + { + int cBias = -1024; + if (codeBiasMap.find(sat) != codeBiasMap.end() && + codeBiasMap[sat].obsCodeBiasMap.find(code) != codeBiasMap[sat].obsCodeBiasMap.end()) + cBias = (int)round(codeBiasMap[sat].obsCodeBiasMap[code].bias * 50); + + if (abs(cBias) > 1023) + cBias = -1024; + + i = setbitsInc(buf, i, 11, cBias); + } + } + + return buffer; } -int phaseBiasDiscChk( - map biasMap, - SatSys sat, - E_ObsCode code) +int phaseBiasDiscChk(map biasMap, SatSys sat, E_ObsCode code) { - double bias = 0; - bool found = false; - E_FType frq = code2Freq[sat.sys][code]; - - if (biasMap.find(code) != biasMap.end()) - { - bias = biasMap[code].bias; - found = true; - } - else - { - E_ObsCode hax = freq2CodeHax(sat.sys,frq); - if (biasMap.find(hax) != biasMap.end()) - { - bias = biasMap[hax].bias; - found = true; - } - } - - if (!found) - return -16384; - - int pBias = (int)round(1000*(bias-phaseBiasOffset[sat][code])); - - if (abs(pBias)>16383) - { - double lamb = genericWavelength[frq]; - phaseBiasOffset[sat][code] = lamb*round(bias/lamb); - pBias = (int)round(1000*(bias-phaseBiasOffset[sat][code])); - if(++phaseDiscCountr[sat][code] > 3) - phaseDiscCountr[sat][code]=0; - } - - return pBias; + double bias = 0; + bool found = false; + E_FType frq = code2Freq[sat.sys][code]; + + if (biasMap.find(code) != biasMap.end()) + { + bias = biasMap[code].bias; + found = true; + } + else + { + E_ObsCode hax = freq2CodeHax(sat.sys, frq); + if (biasMap.find(hax) != biasMap.end()) + { + bias = biasMap[hax].bias; + found = true; + } + } + + if (!found) + return -16384; + + int pBias = (int)round(1000 * (bias - phaseBiasOffset[sat][code])); + + if (abs(pBias) > 16383) + { + double lamb = genericWavelength[frq]; + phaseBiasOffset[sat][code] = lamb * round(bias / lamb); + pBias = (int)round(1000 * (bias - phaseBiasOffset[sat][code])); + if (++phaseDiscCountr[sat][code] > 3) + phaseDiscCountr[sat][code] = 0; + } + + return pBias; } -vector encodecompactPHS( - map& phaseBiasMap, - int updateIntIndex, - bool last) +vector +encodecompactPHS(map& phaseBiasMap, int updateIntIndex, bool last) { - vector buffer; - - int bitLen = 37; - if (acsConfig.ssrOpts.cmpssr_cell_mask) - bitLen += 17*currentCellMap.size(); - else - for (auto& [ind,sat] : currentSatMap) - { - bitLen += 17*currentBiasMap[sat.sys].size(); - } - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - auto& [Sat, ssrPhs] = *phaseBiasMap.begin(); - auto& ssrMeta = ssrPhs.ssrMeta; - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 5); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - - if (acsConfig.ssrOpts.cmpssr_cell_mask) - { - for (auto& [ind, sig] : currentCellMap) - { - auto& [sat, code] = sig; - - int pBias = -16384; - if (phaseBiasMap.find(sat) != phaseBiasMap.end()) - pBias = phaseBiasDiscChk(phaseBiasMap[sat].obsCodeBiasMap, sat, code); - - i = setbitsInc(buf,i,15, pBias); - i = setbitsInc(buf,i,2, phaseDiscCountr[sat][code]); - } - return buffer; - } - - for (auto& [ind,sat] : currentSatMap) - for (auto& [ind2, code] : currentBiasMap[sat.sys]) - { - int pBias = -16384; - if (phaseBiasMap.find(sat) != phaseBiasMap.end()) - pBias = phaseBiasDiscChk(phaseBiasMap[sat].obsCodeBiasMap, sat, code); - - i = setbitsInc(buf,i,15, pBias); - i = setbitsInc(buf,i,2, phaseDiscCountr[sat][code]); - } - - return buffer; + vector buffer; + + int bitLen = 37; + if (acsConfig.ssrOpts.cmpssr_cell_mask) + bitLen += 17 * currentCellMap.size(); + else + for (auto& [ind, sat] : currentSatMap) + { + bitLen += 17 * currentBiasMap[sat.sys].size(); + } + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + auto& [Sat, ssrPhs] = *phaseBiasMap.begin(); + auto& ssrMeta = ssrPhs.ssrMeta; + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 5); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + + if (acsConfig.ssrOpts.cmpssr_cell_mask) + { + for (auto& [ind, sig] : currentCellMap) + { + auto& [sat, code] = sig; + + int pBias = -16384; + if (phaseBiasMap.find(sat) != phaseBiasMap.end()) + pBias = phaseBiasDiscChk(phaseBiasMap[sat].obsCodeBiasMap, sat, code); + + i = setbitsInc(buf, i, 15, pBias); + i = setbitsInc(buf, i, 2, phaseDiscCountr[sat][code]); + } + return buffer; + } + + for (auto& [ind, sat] : currentSatMap) + for (auto& [ind2, code] : currentBiasMap[sat.sys]) + { + int pBias = -16384; + if (phaseBiasMap.find(sat) != phaseBiasMap.end()) + pBias = phaseBiasDiscChk(phaseBiasMap[sat].obsCodeBiasMap, sat, code); + + i = setbitsInc(buf, i, 15, pBias); + i = setbitsInc(buf, i, 2, phaseDiscCountr[sat][code]); + } + + return buffer; } /** - Encode hybrid code and phase bias message - warning 1: Currently only global mode supported + Encode hybrid code and phase bias message + warning 1: Currently only global mode supported */ -vector encodecompactBIA( - map& codeBiasMap, - map& phaseBiasMap, - int updateIntIndex, - bool last) +vector encodecompactBIA( + map& codeBiasMap, + map& phaseBiasMap, + int updateIntIndex, + bool last +) { - vector buffer; - - int bitLen = 40; - - int inclCodBia = codeBiasMap.empty()?0:1; // add config mask here? - int inclPhsBia = phaseBiasMap.empty()?0:1; // add config mask here? - - int nbit = 11*inclCodBia + 17*inclPhsBia; - if (nbit == 0) - return buffer; - - if (acsConfig.ssrOpts.cmpssr_cell_mask) - bitLen += nbit*currentCellMap.size(); - - for (auto& [ind, sat] : currentSatMap) - { - bitLen += nbit*currentBiasMap[sat.sys].size(); - } - - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - int timeOfHour=0; - if (inclCodBia>0) - { - auto& [Sat, ssrCod] = *codeBiasMap.begin(); - auto& ssrMeta = ssrCod.ssrMeta; - timeOfHour = ssrMeta.epochTime1s % 3600; - } - else if (inclPhsBia>0) - { - auto& [Sat, ssrPhs] = *phaseBiasMap.begin(); - auto& ssrMeta = ssrPhs.ssrMeta; - timeOfHour = ssrMeta.epochTime1s % 3600; - } - - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 6); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - - i = setbituInc(buf,i,1, inclCodBia); - i = setbituInc(buf,i,1, inclPhsBia); - i = setbituInc(buf,i,1, 0); // Todo: Regional bias correction not supported at the moment - - if (acsConfig.ssrOpts.cmpssr_cell_mask) - { - for (auto& [ind,sig] : currentCellMap) - { - auto& [sat, code] = sig; - - if (inclCodBia>0) - { - int cBias = -1024; - if (codeBiasMap.find(sat) != codeBiasMap.end() - && codeBiasMap[sat].obsCodeBiasMap.find(code) != codeBiasMap[sat].obsCodeBiasMap.end()) - cBias = (int)round(codeBiasMap[sat].obsCodeBiasMap[code].bias * 50); - - if (abs(cBias)>1023) - cBias = -1024; - - i = setbitsInc(buf,i,11, cBias); - } - - if (inclPhsBia>0) - { - int pBias = -16384; - if (phaseBiasMap.find(sat) != phaseBiasMap.end()) - pBias = phaseBiasDiscChk(phaseBiasMap[sat].obsCodeBiasMap, sat, code); - i = setbitsInc(buf,i,15, pBias); - i = setbitsInc(buf,i,2, phaseDiscCountr[sat][code]); - } - } - return buffer; - } - - for (auto& [ind,sat] : currentSatMap) - { - for (auto& [ind2, code] : currentBiasMap[sat.sys]) - { - if (inclCodBia>0) - { - int cBias = -1024; - if (codeBiasMap.find(sat) != codeBiasMap.end() - && codeBiasMap[sat].obsCodeBiasMap.find(code) != codeBiasMap[sat].obsCodeBiasMap.end()) - cBias = (int)round(codeBiasMap[sat].obsCodeBiasMap[code].bias * 50); - - if (abs(cBias)>1023) - cBias = -1024; - - i = setbitsInc(buf,i,11, cBias); - } - - if (inclPhsBia>0) - { - int pBias = -16384; - if (phaseBiasMap.find(sat) != phaseBiasMap.end()) - pBias = phaseBiasDiscChk(phaseBiasMap[sat].obsCodeBiasMap, sat, code); - - i = setbitsInc(buf,i,15, pBias); - i = setbitsInc(buf,i,2, phaseDiscCountr[sat][code]); - } - } - } - - return buffer; + vector buffer; + + int bitLen = 40; + + int inclCodBia = codeBiasMap.empty() ? 0 : 1; // add config mask here? + int inclPhsBia = phaseBiasMap.empty() ? 0 : 1; // add config mask here? + + int nbit = 11 * inclCodBia + 17 * inclPhsBia; + if (nbit == 0) + return buffer; + + if (acsConfig.ssrOpts.cmpssr_cell_mask) + bitLen += nbit * currentCellMap.size(); + + for (auto& [ind, sat] : currentSatMap) + { + bitLen += nbit * currentBiasMap[sat.sys].size(); + } + + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + int timeOfHour = 0; + if (inclCodBia > 0) + { + auto& [Sat, ssrCod] = *codeBiasMap.begin(); + auto& ssrMeta = ssrCod.ssrMeta; + timeOfHour = ssrMeta.epochTime1s % 3600; + } + else if (inclPhsBia > 0) + { + auto& [Sat, ssrPhs] = *phaseBiasMap.begin(); + auto& ssrMeta = ssrPhs.ssrMeta; + timeOfHour = ssrMeta.epochTime1s % 3600; + } + + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 6); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + + i = setbituInc(buf, i, 1, inclCodBia); + i = setbituInc(buf, i, 1, inclPhsBia); + i = setbituInc(buf, i, 1, 0); // Todo: Regional bias correction not supported at the moment + + if (acsConfig.ssrOpts.cmpssr_cell_mask) + { + for (auto& [ind, sig] : currentCellMap) + { + auto& [sat, code] = sig; + + if (inclCodBia > 0) + { + int cBias = -1024; + if (codeBiasMap.find(sat) != codeBiasMap.end() && + codeBiasMap[sat].obsCodeBiasMap.find(code) != + codeBiasMap[sat].obsCodeBiasMap.end()) + cBias = (int)round(codeBiasMap[sat].obsCodeBiasMap[code].bias * 50); + + if (abs(cBias) > 1023) + cBias = -1024; + + i = setbitsInc(buf, i, 11, cBias); + } + + if (inclPhsBia > 0) + { + int pBias = -16384; + if (phaseBiasMap.find(sat) != phaseBiasMap.end()) + pBias = phaseBiasDiscChk(phaseBiasMap[sat].obsCodeBiasMap, sat, code); + i = setbitsInc(buf, i, 15, pBias); + i = setbitsInc(buf, i, 2, phaseDiscCountr[sat][code]); + } + } + return buffer; + } + + for (auto& [ind, sat] : currentSatMap) + { + for (auto& [ind2, code] : currentBiasMap[sat.sys]) + { + if (inclCodBia > 0) + { + int cBias = -1024; + if (codeBiasMap.find(sat) != codeBiasMap.end() && + codeBiasMap[sat].obsCodeBiasMap.find(code) != + codeBiasMap[sat].obsCodeBiasMap.end()) + cBias = (int)round(codeBiasMap[sat].obsCodeBiasMap[code].bias * 50); + + if (abs(cBias) > 1023) + cBias = -1024; + + i = setbitsInc(buf, i, 11, cBias); + } + + if (inclPhsBia > 0) + { + int pBias = -16384; + if (phaseBiasMap.find(sat) != phaseBiasMap.end()) + pBias = phaseBiasDiscChk(phaseBiasMap[sat].obsCodeBiasMap, sat, code); + + i = setbitsInc(buf, i, 15, pBias); + i = setbitsInc(buf, i, 2, phaseDiscCountr[sat][code]); + } + } + } + + return buffer; } -map> stecPolyCommonMode; -vector encodecompactTEC( - SSRMeta& ssrMeta, - int regId, - SSRAtmRegion& ssrAtmReg, - int updateIntIndex, - bool last) +map> stecPolyCommonMode; +vector encodecompactTEC( + SSRMeta& ssrMeta, + int regId, + SSRAtmRegion& ssrAtmReg, + int updateIntIndex, + bool last + ) { - vector buffer; - - if (ssrAtmReg.ionoPolySize<=0) - return buffer; - - int nBitTec = 0; - int polyType = -1; - switch(ssrAtmReg.ionoPolySize) - { - case 1: polyType = 0; nBitTec = 20; break; - case 3: polyType = 1; nBitTec = 44; break; - case 4: polyType = 2; nBitTec = 54; break; - case 6: polyType = 3; nBitTec = 70; break; - } - - if (nBitTec ==0) - return buffer; - - int bitLen = 44 + currentSatMap.size(); - - map regSat; - for (auto& [ind,sat] : currentSatMap) - if (ssrAtmReg.stecData.find(sat) != ssrAtmReg.stecData.end()) - { - regSat[ind]=sat; - bitLen+=nBitTec; - } - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 3); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - i = setbituInc(buf,i,2, polyType); - i = setbituInc(buf,i,5, regId); - - int isys =i; - for (auto& [ind,sat] : regSat) - setbituInc(buf,isys+ind,1,1); - i = isys + currentSatMap.size(); - - // Remove common mode - for (int iPoly = 0; iPoly 127) {inRange = false; break;} - C20_tmp = (int)round( (stecData.poly[5]-stecPolyCommonMode[regId][5]) * 200); if (abs(C20_tmp) > 127) {inRange = false; break;} - case 2: C11_tmp = (int)round( (stecData.poly[3]-stecPolyCommonMode[regId][3]) * 50); if (abs(C11_tmp) > 511) {inRange = false; break;} - case 1: C01_tmp = (int)round( (stecData.poly[1]-stecPolyCommonMode[regId][1]) * 50); if (abs(C01_tmp) > 2047) {inRange = false; break;} - C10_tmp = (int)round( (stecData.poly[2]-stecPolyCommonMode[regId][2]) * 50); if (abs(C10_tmp) > 2047) {inRange = false; break;} - case 0: C00_tmp = (int)round( (stecData.poly[0]-stecPolyCommonMode[regId][0]) * 20); if (abs(C00_tmp) > 8191) {inRange = false; break;} - } - - if (inRange) - { - C00 = C00_tmp; - C10 = C10_tmp; - C01 = C01_tmp; - C11 = C11_tmp; - C20 = C20_tmp; - C02 = C02_tmp; - } - - i = setbitsInc(buf,i, 14, C00); - if (polyType==0) - continue; - i = setbitsInc(buf,i, 12, C01); - i = setbitsInc(buf,i, 12, C10); - if (polyType==1) - continue; - i = setbitsInc(buf,i, 10, C11); - if (polyType==2) - continue; - i = setbitsInc(buf,i, 8, C02); - i = setbitsInc(buf,i, 8, C20); - } - - return buffer; + vector buffer; + + if (ssrAtmReg.ionoPolySize <= 0) + return buffer; + + int nBitTec = 0; + int polyType = -1; + switch (ssrAtmReg.ionoPolySize) + { + case 1: + polyType = 0; + nBitTec = 20; + break; + case 3: + polyType = 1; + nBitTec = 44; + break; + case 4: + polyType = 2; + nBitTec = 54; + break; + case 6: + polyType = 3; + nBitTec = 70; + break; + } + + if (nBitTec == 0) + return buffer; + + int bitLen = 44 + currentSatMap.size(); + + map regSat; + for (auto& [ind, sat] : currentSatMap) + if (ssrAtmReg.stecData.find(sat) != ssrAtmReg.stecData.end()) + { + regSat[ind] = sat; + bitLen += nBitTec; + } + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 3); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + i = setbituInc(buf, i, 2, polyType); + i = setbituInc(buf, i, 5, regId); + + int isys = i; + for (auto& [ind, sat] : regSat) + setbituInc(buf, isys + ind, 1, 1); + i = isys + currentSatMap.size(); + + // Remove common mode + for (int iPoly = 0; iPoly < ssrAtmReg.ionoPolySize; iPoly++) + { + double valPoly = 0; + double nsamp = 0; + for (auto& [ind, sat] : regSat) + { + auto& [time, stecData] = *ssrAtmReg.stecData[sat].begin(); + double val = stecData.poly[iPoly]; + if (val < -9990) + continue; + valPoly += val; + nsamp += 1.0; + } + if (nsamp == 0) + continue; + + valPoly /= nsamp; + + if (stecPolyCommonMode[regId].find(iPoly) == stecPolyCommonMode[regId].end()) + stecPolyCommonMode[regId][iPoly] = valPoly; + + if (stecPolyCommonMode[regId].find(iPoly) == stecPolyCommonMode[regId].end()) + stecPolyCommonMode[regId][iPoly] += 0.01 * (valPoly - stecPolyCommonMode[regId][iPoly]); + } + + for (auto& [ind, sat] : regSat) + { + auto& [time, stecData] = *ssrAtmReg.stecData[sat].begin(); + + int uraClass = uraToClassValue(stecData.sigma); + i = setbituInc(buf, i, 6, uraClass); + + bool inRange = true; + int C00 = -8192, C00_tmp = 0; + int C01 = -2048, C01_tmp = 0; + int C10 = -2048, C10_tmp = 0; + int C11 = -512, C11_tmp = 0; + int C02 = -128, C02_tmp = 0; + int C20 = -128, C20_tmp = 0; + + switch (polyType) + { + case 3: + C02_tmp = (int)round((stecData.poly[4] - stecPolyCommonMode[regId][4]) * 200); + if (abs(C02_tmp) > 127) + { + inRange = false; + break; + } + C20_tmp = (int)round((stecData.poly[5] - stecPolyCommonMode[regId][5]) * 200); + if (abs(C20_tmp) > 127) + { + inRange = false; + break; + } + case 2: + C11_tmp = (int)round((stecData.poly[3] - stecPolyCommonMode[regId][3]) * 50); + if (abs(C11_tmp) > 511) + { + inRange = false; + break; + } + case 1: + C01_tmp = (int)round((stecData.poly[1] - stecPolyCommonMode[regId][1]) * 50); + if (abs(C01_tmp) > 2047) + { + inRange = false; + break; + } + C10_tmp = (int)round((stecData.poly[2] - stecPolyCommonMode[regId][2]) * 50); + if (abs(C10_tmp) > 2047) + { + inRange = false; + break; + } + case 0: + C00_tmp = (int)round((stecData.poly[0] - stecPolyCommonMode[regId][0]) * 20); + if (abs(C00_tmp) > 8191) + { + inRange = false; + break; + } + } + + if (inRange) + { + C00 = C00_tmp; + C10 = C10_tmp; + C01 = C01_tmp; + C11 = C11_tmp; + C20 = C20_tmp; + C02 = C02_tmp; + } + + i = setbitsInc(buf, i, 14, C00); + if (polyType == 0) + continue; + i = setbitsInc(buf, i, 12, C01); + i = setbitsInc(buf, i, 12, C10); + if (polyType == 1) + continue; + i = setbitsInc(buf, i, 10, C11); + if (polyType == 2) + continue; + i = setbitsInc(buf, i, 8, C02); + i = setbitsInc(buf, i, 8, C20); + } + + return buffer; } -vector encodecompactGRD( - SSRMeta& ssrMeta, - int regId, - SSRAtmRegion& ssrAtmReg, - int updateIntIndex, - bool last) +vector encodecompactGRD( + SSRMeta& ssrMeta, + int regId, + SSRAtmRegion& ssrAtmReg, + int updateIntIndex, + bool last +) { - vector buffer; - - if (ssrAtmReg.ionoGrid - && ssrAtmReg.tropGrid) - return buffer; - - int bitLen = 57 + currentSatMap.size(); - - int nGrid = ssrAtmReg.gridLatDeg.size(); - - int tropType = ssrAtmReg.tropGrid ? 1 : 0; - int nBitTrop = ssrAtmReg.tropGrid ? 17 : 0; - double offsetDry = 2.3; - double offsetWet = 0.252; - bitLen+= nGrid*nBitTrop; - - int ionoType = acsConfig.ssrOpts.cmpssr_stec_format < 4 ? 0 : 1; - int nBitIono = acsConfig.ssrOpts.cmpssr_stec_format < 4 ? 7 : 16; - map regSat; - for (auto& [ind, sat] : currentSatMap) - if (ssrAtmReg.stecData.find(sat) != ssrAtmReg.stecData.end()) - { - regSat[ind] = sat; - bitLen += nGrid * nBitIono; - } - - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 9); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - i = setbituInc(buf,i,2, tropType); - i = setbituInc(buf,i,1, ionoType); - i = setbituInc(buf,i,5, regId); - - int isys =i; - for (auto& [ind,sat] : regSat) - setbituInc(buf,isys+ind,1,1); - i = isys + currentSatMap.size(); - - auto& [tim1, tropData] = *ssrAtmReg.tropData.begin(); - int uraClass = uraToClassValue(tropData.sigma); - i = setbituInc(buf,i, 6, uraClass); - i = setbituInc(buf,i, 6, nGrid); - - for (auto& [iGrid, latDeg] : ssrAtmReg.gridLatDeg) - { - if (tropType==1) - { - int dTDry = (int)round((tropData.gridDry[iGrid]-offsetDry)*250); - if (abs(dTDry)>255) - dTDry=-256; - int dTWet = (int)round((tropData.gridWet[iGrid]-offsetWet)*250); - if (abs(dTWet)>127) - dTWet=-128; - i = setbituInc(buf,i, 9, dTDry); - i = setbituInc(buf,i, 8, dTWet); - } - - int stecMax = 32767; - if (acsConfig.ssrOpts.cmpssr_stec_format>0) - stecMax = 63; - - for (auto& [iSat,sat] : regSat) - { - auto& [tim2, stecData] = *ssrAtmReg.stecData[sat].begin(); - int dSTEC = (int)round(stecData.grid[iGrid]*25); - if (abs(dSTEC)>stecMax) - dSTEC=-stecMax-1; - i = setbituInc(buf,i,nBitIono, dSTEC); - } - } - return buffer; + vector buffer; + + if (ssrAtmReg.ionoGrid && ssrAtmReg.tropGrid) + return buffer; + + int bitLen = 57 + currentSatMap.size(); + + int nGrid = ssrAtmReg.gridLatDeg.size(); + + int tropType = ssrAtmReg.tropGrid ? 1 : 0; + int nBitTrop = ssrAtmReg.tropGrid ? 17 : 0; + double offsetDry = 2.3; + double offsetWet = 0.252; + bitLen += nGrid * nBitTrop; + + int ionoType = acsConfig.ssrOpts.cmpssr_stec_format < 4 ? 0 : 1; + int nBitIono = acsConfig.ssrOpts.cmpssr_stec_format < 4 ? 7 : 16; + map regSat; + for (auto& [ind, sat] : currentSatMap) + if (ssrAtmReg.stecData.find(sat) != ssrAtmReg.stecData.end()) + { + regSat[ind] = sat; + bitLen += nGrid * nBitIono; + } + + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 9); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + i = setbituInc(buf, i, 2, tropType); + i = setbituInc(buf, i, 1, ionoType); + i = setbituInc(buf, i, 5, regId); + + int isys = i; + for (auto& [ind, sat] : regSat) + setbituInc(buf, isys + ind, 1, 1); + i = isys + currentSatMap.size(); + + auto& [tim1, tropData] = *ssrAtmReg.tropData.begin(); + int uraClass = uraToClassValue(tropData.sigma); + i = setbituInc(buf, i, 6, uraClass); + i = setbituInc(buf, i, 6, nGrid); + + for (auto& [iGrid, latDeg] : ssrAtmReg.gridLatDeg) + { + if (tropType == 1) + { + int dTDry = (int)round((tropData.gridDry[iGrid] - offsetDry) * 250); + if (abs(dTDry) > 255) + dTDry = -256; + int dTWet = (int)round((tropData.gridWet[iGrid] - offsetWet) * 250); + if (abs(dTWet) > 127) + dTWet = -128; + i = setbituInc(buf, i, 9, dTDry); + i = setbituInc(buf, i, 8, dTWet); + } + + int stecMax = 32767; + if (acsConfig.ssrOpts.cmpssr_stec_format > 0) + stecMax = 63; + + for (auto& [iSat, sat] : regSat) + { + auto& [tim2, stecData] = *ssrAtmReg.stecData[sat].begin(); + int dSTEC = (int)round(stecData.grid[iGrid] * 25); + if (abs(dSTEC) > stecMax) + dSTEC = -stecMax - 1; + i = setbituInc(buf, i, nBitIono, dSTEC); + } + } + return buffer; } -vector encodecompactATM( - SSRMeta& ssrMeta, - int regId, - SSRAtmRegion& ssrAtmReg, - int updateIntIndex, - bool last) +vector encodecompactATM( + SSRMeta& ssrMeta, + int regId, + SSRAtmRegion& ssrAtmReg, + int updateIntIndex, + bool last +) { - vector buffer; - - - int tropType = 0; - int nBitTrpGrid = 0; - if (ssrAtmReg.tropGrid) - { - tropType++; - nBitTrpGrid = acsConfig.ssrOpts.cmpssr_trop_format==0?6:8; - } - int nBitTrpPoly = 0; - int tropPoly = -1; - switch (ssrAtmReg.tropPolySize) - { - case 1: tropPoly =0; nBitTrpPoly = 9; tropType+=2; break; - case 3: tropPoly =1; nBitTrpPoly = 23; tropType+=2; break; - case 4: tropPoly =2; nBitTrpPoly = 30; tropType+=2; break; - } - - int ionoType = 0; - int nBitIonGrid = 0; - double ionGridLSB = 0; - if (ssrAtmReg.ionoGrid) - { - ionoType++; - switch (acsConfig.ssrOpts.cmpssr_stec_format) - { - case 0: nBitIonGrid = 4; ionGridLSB = 0.04; break; - case 1: nBitIonGrid = 4; ionGridLSB = 0.12; break; - case 2: nBitIonGrid = 5; ionGridLSB = 0.16; break; - case 3: nBitIonGrid = 7; ionGridLSB = 0.24; break; - } - } - int nBitIonPoly = 0; - int ionoPoly = -1; - switch (ssrAtmReg.ionoPolySize) - { - case 1: ionoPoly =0; nBitIonPoly = 14; ionoType+=2; break; - case 3: ionoPoly =1; nBitIonPoly = 38; ionoType+=2; break; - case 4: ionoPoly =2; nBitIonPoly = 48; ionoType+=2; break; - case 6: ionoPoly =3; nBitIonPoly = 64; ionoType+=2; break; - } - - - if (tropType == 0 - && ionoType == 0) - return buffer; - - int nGrid = ssrAtmReg.gridLatDeg.size(); - - int bitLen = 52; - if (tropType > 0) - { - bitLen += 6; - if (nBitTrpPoly>0) - bitLen += 2 + nBitTrpPoly; - if (nBitTrpGrid>0) - bitLen += 1 + nGrid*nBitTrpGrid; - } - - map regSat; - if (ionoType>0) - { - bitLen += currentSatMap.size(); - - for (auto& [ind,sat] : currentSatMap) - if (ssrAtmReg.stecData.find(sat) != ssrAtmReg.stecData.end()) - { - regSat[ind]=sat; - bitLen+= 6; - if (nBitIonPoly>0) - bitLen+= 2 + nBitIonPoly; - if (nBitIonGrid>0) - bitLen+= 2 + nGrid*nBitIonGrid; - } - } - int byteLen = ceil(bitLen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - - int timeOfHour = ssrMeta.epochTime1s % 3600; - int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 12); - i = setbituInc(buf,i,12, timeOfHour); - i = setbituInc(buf,i,4, updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, currentIOD); - i = setbituInc(buf,i,2, tropType); - i = setbituInc(buf,i,2, ionoType); - i = setbituInc(buf,i,5, regId); - i = setbituInc(buf,i,6, nGrid); - - if (tropType > 0) - { - auto& [tim1, tropData] = *ssrAtmReg.tropData.begin(); - int uraClass = uraToClassValue(tropData.sigma); - i = setbituInc(buf,i, 6, uraClass); - - if (nBitTrpPoly>0) - { - bool inRange = true; - int T00 = -256, T00_tmp = 0; - int T01 = - 64, T01_tmp = 0; - int T10 = - 64, T10_tmp = 0; - int T11 = - 64, T11_tmp = 0; - - switch(tropPoly) - { - case 2: T11_tmp = (int)round( tropData.polyDry[3] *1000); if (abs(T11_tmp) > 63 ) {inRange = false; break;} - case 1: T01_tmp = (int)round( tropData.polyDry[1] * 500); if (abs(T01_tmp) > 63 ) {inRange = false; break;} - T10_tmp = (int)round( tropData.polyDry[2] * 500); if (abs(T10_tmp) > 63 ) {inRange = false; break;} - case 0: T00_tmp = (int)round( tropData.polyDry[0] * 250); if (abs(T00_tmp) > 255) {inRange = false; break;} - } - - if (inRange) - { - T00 = T00_tmp; - T10 = T10_tmp; - T01 = T01_tmp; - T11 = T11_tmp; - } - - i = setbitsInc(buf,i, 9, T00); - if (tropPoly>0) - { - i = setbitsInc(buf,i, 12, T01); - i = setbitsInc(buf,i, 12, T10); - } - if (tropPoly>1) - i = setbitsInc(buf,i, 10, T11); - } - - if (nBitTrpGrid > 0) - { - double acumGrid = 0; - int nOkGrid = 0; - for ( auto& [ind, zwd] : tropData.gridWet) - if (zwd>-9999) - { - acumGrid += zwd; - nOkGrid++; - } - - int intZWDOffset = (int)round(acumGrid * 50 / nOkGrid); - double offsetWet = 0.02 * intZWDOffset; - int tropGridFormat = acsConfig.ssrOpts.cmpssr_trop_format; - - i = setbituInc(buf,i, 1, tropGridFormat); - i = setbituInc(buf,i, 4, intZWDOffset); - - for ( auto& [ind, lat] : ssrAtmReg.gridLatDeg) - { - int maxZWD = pow(2, nBitTrpGrid - 1) - 1; - int preZWD = -maxZWD - 1; - - if (tropData.gridWet.find(ind) != tropData.gridWet.end()) - preZWD = (int)round((tropData.gridWet[ind] - offsetWet) * 250); - - if (abs(preZWD)>maxZWD) - preZWD = -maxZWD - 1; - - i = setbitsInc(buf,i, nBitTrpGrid, preZWD); - } - } - } - - if (ionoType) - { - for (auto& [j, sat] : regSat) - setbituInc(buf, i+j, 1, 1); - - i += currentSatMap.size(); - - for (auto& [j,sat] : regSat) - { - auto& [time, stecData] = *ssrAtmReg.stecData[sat].begin(); - - int uraClass = uraToClassValue(stecData.sigma); - i = setbituInc(buf,i, 6, uraClass); - - if (nBitIonPoly>0) - { - i = setbituInc(buf,i, 2, ionoPoly); - - bool inRange = true; - int C00 = -8192, C00_tmp = 0; - int C01 = -2048, C01_tmp = 0; - int C10 = -2048, C10_tmp = 0; - int C11 = - 512, C11_tmp = 0; - int C02 = - 128, C02_tmp = 0; - int C20 = - 128, C20_tmp = 0; - - switch(ionoPoly) - { - case 3: C02_tmp = (int)round( stecData.poly[4] * 200); if (abs(C02_tmp) > 127) {inRange = false; break;} - C20_tmp = (int)round( stecData.poly[5] * 200); if (abs(C20_tmp) > 127) {inRange = false; break;} - case 2: C11_tmp = (int)round( stecData.poly[3] * 50); if (abs(C11_tmp) > 511) {inRange = false; break;} - case 1: C01_tmp = (int)round( stecData.poly[1] * 50); if (abs(C01_tmp) > 2047) {inRange = false; break;} - C10_tmp = (int)round( stecData.poly[2] * 50); if (abs(C10_tmp) > 2047) {inRange = false; break;} - case 0: C00_tmp = (int)round( stecData.poly[0] * 20); if (abs(C00_tmp) > 8191) {inRange = false; break;} - } - - if (inRange) - { - C00 = C00_tmp; - C10 = C10_tmp; - C01 = C01_tmp; - C11 = C11_tmp; - C20 = C20_tmp; - C02 = C02_tmp; - } - - i = setbitsInc(buf,i, 14, C00); - if (ionoPoly==0) - continue; - i = setbitsInc(buf,i, 12, C01); - i = setbitsInc(buf,i, 12, C10); - if (ionoPoly==1) - continue; - i = setbitsInc(buf,i, 10, C11); - if (ionoPoly==2) - continue; - i = setbitsInc(buf,i, 8, C02); - i = setbitsInc(buf,i, 8, C20); - } - - if (nBitIonGrid>0) - for (auto& [ind, lat] : ssrAtmReg.gridLatDeg) - { - int maxSTEC = pow(2,nBitIonGrid-1)-1; - int preSTEC = -maxSTEC - 1; - - if (stecData.grid.find(ind) != stecData.grid.end()) - preSTEC = (int)round(stecData.grid[ind] / ionGridLSB); - - if (abs(preSTEC) > maxSTEC) - preSTEC = -maxSTEC - 1; - - i = setbitsInc(buf,i, nBitIonGrid, preSTEC); - } - } - } - return buffer; + vector buffer; + + int tropType = 0; + int nBitTrpGrid = 0; + if (ssrAtmReg.tropGrid) + { + tropType++; + nBitTrpGrid = acsConfig.ssrOpts.cmpssr_trop_format == 0 ? 6 : 8; + } + int nBitTrpPoly = 0; + int tropPoly = -1; + switch (ssrAtmReg.tropPolySize) + { + case 1: + tropPoly = 0; + nBitTrpPoly = 9; + tropType += 2; + break; + case 3: + tropPoly = 1; + nBitTrpPoly = 23; + tropType += 2; + break; + case 4: + tropPoly = 2; + nBitTrpPoly = 30; + tropType += 2; + break; + } + + int ionoType = 0; + int nBitIonGrid = 0; + double ionGridLSB = 0; + if (ssrAtmReg.ionoGrid) + { + ionoType++; + switch (acsConfig.ssrOpts.cmpssr_stec_format) + { + case 0: + nBitIonGrid = 4; + ionGridLSB = 0.04; + break; + case 1: + nBitIonGrid = 4; + ionGridLSB = 0.12; + break; + case 2: + nBitIonGrid = 5; + ionGridLSB = 0.16; + break; + case 3: + nBitIonGrid = 7; + ionGridLSB = 0.24; + break; + } + } + int nBitIonPoly = 0; + int ionoPoly = -1; + switch (ssrAtmReg.ionoPolySize) + { + case 1: + ionoPoly = 0; + nBitIonPoly = 14; + ionoType += 2; + break; + case 3: + ionoPoly = 1; + nBitIonPoly = 38; + ionoType += 2; + break; + case 4: + ionoPoly = 2; + nBitIonPoly = 48; + ionoType += 2; + break; + case 6: + ionoPoly = 3; + nBitIonPoly = 64; + ionoType += 2; + break; + } + + if (tropType == 0 && ionoType == 0) + return buffer; + + int nGrid = ssrAtmReg.gridLatDeg.size(); + + int bitLen = 52; + if (tropType > 0) + { + bitLen += 6; + if (nBitTrpPoly > 0) + bitLen += 2 + nBitTrpPoly; + if (nBitTrpGrid > 0) + bitLen += 1 + nGrid * nBitTrpGrid; + } + + map regSat; + if (ionoType > 0) + { + bitLen += currentSatMap.size(); + + for (auto& [ind, sat] : currentSatMap) + if (ssrAtmReg.stecData.find(sat) != ssrAtmReg.stecData.end()) + { + regSat[ind] = sat; + bitLen += 6; + if (nBitIonPoly > 0) + bitLen += 2 + nBitIonPoly; + if (nBitIonGrid > 0) + bitLen += 2 + nGrid * nBitIonGrid; + } + } + int byteLen = ceil(bitLen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + int timeOfHour = ssrMeta.epochTime1s % 3600; + int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 12); + i = setbituInc(buf, i, 12, timeOfHour); + i = setbituInc(buf, i, 4, updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, currentIOD); + i = setbituInc(buf, i, 2, tropType); + i = setbituInc(buf, i, 2, ionoType); + i = setbituInc(buf, i, 5, regId); + i = setbituInc(buf, i, 6, nGrid); + + if (tropType > 0) + { + auto& [tim1, tropData] = *ssrAtmReg.tropData.begin(); + int uraClass = uraToClassValue(tropData.sigma); + i = setbituInc(buf, i, 6, uraClass); + + if (nBitTrpPoly > 0) + { + bool inRange = true; + int T00 = -256, T00_tmp = 0; + int T01 = -64, T01_tmp = 0; + int T10 = -64, T10_tmp = 0; + int T11 = -64, T11_tmp = 0; + + switch (tropPoly) + { + case 2: + T11_tmp = (int)round(tropData.polyDry[3] * 1000); + if (abs(T11_tmp) > 63) + { + inRange = false; + break; + } + case 1: + T01_tmp = (int)round(tropData.polyDry[1] * 500); + if (abs(T01_tmp) > 63) + { + inRange = false; + break; + } + T10_tmp = (int)round(tropData.polyDry[2] * 500); + if (abs(T10_tmp) > 63) + { + inRange = false; + break; + } + case 0: + T00_tmp = (int)round(tropData.polyDry[0] * 250); + if (abs(T00_tmp) > 255) + { + inRange = false; + break; + } + } + + if (inRange) + { + T00 = T00_tmp; + T10 = T10_tmp; + T01 = T01_tmp; + T11 = T11_tmp; + } + + i = setbitsInc(buf, i, 9, T00); + if (tropPoly > 0) + { + i = setbitsInc(buf, i, 12, T01); + i = setbitsInc(buf, i, 12, T10); + } + if (tropPoly > 1) + i = setbitsInc(buf, i, 10, T11); + } + + if (nBitTrpGrid > 0) + { + double acumGrid = 0; + int nOkGrid = 0; + for (auto& [ind, zwd] : tropData.gridWet) + if (zwd > -9999) + { + acumGrid += zwd; + nOkGrid++; + } + + int intZWDOffset = (int)round(acumGrid * 50 / nOkGrid); + double offsetWet = 0.02 * intZWDOffset; + int tropGridFormat = acsConfig.ssrOpts.cmpssr_trop_format; + + i = setbituInc(buf, i, 1, tropGridFormat); + i = setbituInc(buf, i, 4, intZWDOffset); + + for (auto& [ind, lat] : ssrAtmReg.gridLatDeg) + { + int maxZWD = pow(2, nBitTrpGrid - 1) - 1; + int preZWD = -maxZWD - 1; + + if (tropData.gridWet.find(ind) != tropData.gridWet.end()) + preZWD = (int)round((tropData.gridWet[ind] - offsetWet) * 250); + + if (abs(preZWD) > maxZWD) + preZWD = -maxZWD - 1; + + i = setbitsInc(buf, i, nBitTrpGrid, preZWD); + } + } + } + + if (ionoType) + { + for (auto& [j, sat] : regSat) + setbituInc(buf, i + j, 1, 1); + + i += currentSatMap.size(); + + for (auto& [j, sat] : regSat) + { + auto& [time, stecData] = *ssrAtmReg.stecData[sat].begin(); + + int uraClass = uraToClassValue(stecData.sigma); + i = setbituInc(buf, i, 6, uraClass); + + if (nBitIonPoly > 0) + { + i = setbituInc(buf, i, 2, ionoPoly); + + bool inRange = true; + int C00 = -8192, C00_tmp = 0; + int C01 = -2048, C01_tmp = 0; + int C10 = -2048, C10_tmp = 0; + int C11 = -512, C11_tmp = 0; + int C02 = -128, C02_tmp = 0; + int C20 = -128, C20_tmp = 0; + + switch (ionoPoly) + { + case 3: + C02_tmp = (int)round(stecData.poly[4] * 200); + if (abs(C02_tmp) > 127) + { + inRange = false; + break; + } + C20_tmp = (int)round(stecData.poly[5] * 200); + if (abs(C20_tmp) > 127) + { + inRange = false; + break; + } + case 2: + C11_tmp = (int)round(stecData.poly[3] * 50); + if (abs(C11_tmp) > 511) + { + inRange = false; + break; + } + case 1: + C01_tmp = (int)round(stecData.poly[1] * 50); + if (abs(C01_tmp) > 2047) + { + inRange = false; + break; + } + C10_tmp = (int)round(stecData.poly[2] * 50); + if (abs(C10_tmp) > 2047) + { + inRange = false; + break; + } + case 0: + C00_tmp = (int)round(stecData.poly[0] * 20); + if (abs(C00_tmp) > 8191) + { + inRange = false; + break; + } + } + + if (inRange) + { + C00 = C00_tmp; + C10 = C10_tmp; + C01 = C01_tmp; + C11 = C11_tmp; + C20 = C20_tmp; + C02 = C02_tmp; + } + + i = setbitsInc(buf, i, 14, C00); + if (ionoPoly == 0) + continue; + i = setbitsInc(buf, i, 12, C01); + i = setbitsInc(buf, i, 12, C10); + if (ionoPoly == 1) + continue; + i = setbitsInc(buf, i, 10, C11); + if (ionoPoly == 2) + continue; + i = setbitsInc(buf, i, 8, C02); + i = setbitsInc(buf, i, 8, C20); + } + + if (nBitIonGrid > 0) + for (auto& [ind, lat] : ssrAtmReg.gridLatDeg) + { + int maxSTEC = pow(2, nBitIonGrid - 1) - 1; + int preSTEC = -maxSTEC - 1; + + if (stecData.grid.find(ind) != stecData.grid.end()) + preSTEC = (int)round(stecData.grid[ind] / ionGridLSB); + + if (abs(preSTEC) > maxSTEC) + preSTEC = -maxSTEC - 1; + + i = setbitsInc(buf, i, nBitIonGrid, preSTEC); + } + } + } + return buffer; } -vector encodeGridInfo( - SSRAtm& ssrAtm) +vector encodeGridInfo(SSRAtm& ssrAtm) { - vector buffer; - - int bitlen = 17; - for (auto& [regID,regData] : ssrAtm.atmosRegionsMap) - { - bitlen+=39; - switch (regData.gridType) - { - case 0: bitlen+= 6 + 21 * regData.gridLatDeg.size(); break; - case 1: bitlen+= 31; break; - case 2: bitlen+= 31; break; - default: - std::cout << "Unknown gridtype for region: " << regID << "\n"; - regData.gridType = -1; - continue; - } - } - - int byteLen = ceil(bitlen/8.0); - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - int i=0; - i = setbituInc(buf,i, 4, 3); - int regIOD = 0; - auto& [regID,regData] = *ssrAtm.atmosRegionsMap.begin(); - - i = setbituInc(buf,i, 3, regIOD); - i = setbituInc(buf,i, 4, 0); - i = setbituInc(buf,i, 6, ssrAtm.atmosRegionsMap.size()-1); - - for (auto& [regID,regData] : ssrAtm.atmosRegionsMap) - { - if (regData.gridType<0) - continue; - i = setbituInc(buf,i, 5, regID); - i = setbituInc(buf,i, 1, 0); /** Warning: network/region parts are not enabled */ - int tmp = 0; - switch (regData.gridType) - { - case 0: - { - 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); - - for (auto& [ind, lat] : regData.gridLatDeg) - { - double dLat = regData.gridLatDeg[ind] - thisLatDeg; - tmp =(int) round(dLat * 100 * D2R); i = setbitsInc(buf,i,10, tmp); - thisLatDeg += 0.01 * tmp * R2D; - - double dLon = regData.gridLonDeg[ind] - thisLonDeg; - tmp =(int) round(dLon * 100 * D2R); i = setbitsInc(buf,i,11, tmp); - thisLonDeg += 0.01 * tmp * R2D; - } - break; - } - case 1: - case 2: - { - if ( regData.intLatDeg <= 0 - || regData.intLonDeg <= 0) - { - continue; - } - - int nGridLat = ceil((regData.maxLatDeg - regData.minLatDeg) / regData.intLatDeg); - int nGridLon = ceil((regData.maxLonDeg - regData.minLonDeg) / regData.intLonDeg); - tmp =(int) round(regData.gridLatDeg[0]*D2R*16284/PI); i = setbitsInc(buf,i,15, tmp); - tmp =(int) round(regData.gridLonDeg[0]*D2R*16284/PI); i = setbitsInc(buf,i,16, tmp); - i = setbituInc(buf,i, 6, nGridLat); - i = setbituInc(buf,i, 6, nGridLon); - tmp =(int) round(regData.intLatDeg*D2R*100); i = setbituInc(buf,i, 9, tmp); - tmp =(int) round(regData.intLonDeg*D2R*100); i = setbituInc(buf,i,10, tmp); - break; - } - default: - { - vector dummy; - return dummy; - } - } - } - - return buffer; + vector buffer; + + int bitlen = 17; + for (auto& [regID, regData] : ssrAtm.atmosRegionsMap) + { + bitlen += 39; + switch (regData.gridType) + { + case 0: + bitlen += 6 + 21 * regData.gridLatDeg.size(); + break; + case 1: + bitlen += 31; + break; + case 2: + bitlen += 31; + break; + default: + std::cout << "Unknown gridtype for region: " << regID << "\n"; + regData.gridType = -1; + continue; + } + } + + int byteLen = ceil(bitlen / 8.0); + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + int i = 0; + i = setbituInc(buf, i, 4, 3); + int regIOD = 0; + auto& [regID, regData] = *ssrAtm.atmosRegionsMap.begin(); + + i = setbituInc(buf, i, 3, regIOD); + i = setbituInc(buf, i, 4, 0); + i = setbituInc(buf, i, 6, ssrAtm.atmosRegionsMap.size() - 1); + + for (auto& [regID, regData] : ssrAtm.atmosRegionsMap) + { + if (regData.gridType < 0) + continue; + i = setbituInc(buf, i, 5, regID); + i = setbituInc(buf, i, 1, 0); /** Warning: network/region parts are not enabled */ + int tmp = 0; + switch (regData.gridType) + { + case 0: + { + 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); + + for (auto& [ind, lat] : regData.gridLatDeg) + { + double dLat = regData.gridLatDeg[ind] - thisLatDeg; + tmp = (int)round(dLat * 100 * D2R); + i = setbitsInc(buf, i, 10, tmp); + thisLatDeg += 0.01 * tmp * R2D; + + double dLon = regData.gridLonDeg[ind] - thisLonDeg; + tmp = (int)round(dLon * 100 * D2R); + i = setbitsInc(buf, i, 11, tmp); + thisLonDeg += 0.01 * tmp * R2D; + } + break; + } + case 1: + case 2: + { + if (regData.intLatDeg <= 0 || regData.intLonDeg <= 0) + { + continue; + } + + int nGridLat = ceil((regData.maxLatDeg - regData.minLatDeg) / regData.intLatDeg); + int nGridLon = ceil((regData.maxLonDeg - regData.minLonDeg) / regData.intLonDeg); + tmp = (int)round(regData.gridLatDeg[0] * D2R * 16284 / PI); + i = setbitsInc(buf, i, 15, tmp); + tmp = (int)round(regData.gridLonDeg[0] * D2R * 16284 / PI); + i = setbitsInc(buf, i, 16, tmp); + i = setbituInc(buf, i, 6, nGridLat); + i = setbituInc(buf, i, 6, nGridLon); + tmp = (int)round(regData.intLatDeg * D2R * 100); + i = setbituInc(buf, i, 9, tmp); + tmp = (int)round(regData.intLonDeg * D2R * 100); + i = setbituInc(buf, i, 10, tmp); + break; + } + default: + { + vector dummy; + return dummy; + } + } + } + + return buffer; } -vector encodecompactSRV( - SSRAtm& ssrAtm) +vector encodecompactSRV(SSRAtm& ssrAtm) { - /* Send archived messages */ - if (cmpSSRServiceMessages.empty() == false) - { - auto [ind, message] = *cmpSSRServiceMessages.begin(); + /* Send archived messages */ + if (cmpSSRServiceMessages.empty() == false) + { + auto [ind, message] = *cmpSSRServiceMessages.begin(); - vector buffer; + vector buffer; - buffer.resize(message.size()); + buffer.resize(message.size()); - copy(message.begin(), message.end(), buffer.begin()); + copy(message.begin(), message.end(), buffer.begin()); - cmpSSRServiceMessages.erase(ind); + cmpSSRServiceMessages.erase(ind); - return buffer; - } + return buffer; + } - /* service packets to be implemented here */ - for (auto& [regID,regData] : ssrAtm.atmosRegionsMap) - { - vector buffer = encodeGridInfo(ssrAtm); + /* service packets to be implemented here */ + for (auto& [regID, regData] : ssrAtm.atmosRegionsMap) + { + vector buffer = encodeGridInfo(ssrAtm); - int buffSize = buffer.size(); - int ibuff=0; - int indMess=0; - while (ibuff < buffSize) - { - int nBlocks = ceil(0.2*(buffSize-ibuff)); + int buffSize = buffer.size(); + int ibuff = 0; + int indMess = 0; + while (ibuff < buffSize) + { + int nBlocks = ceil(0.2 * (buffSize - ibuff)); - if (nBlocks > 4) - nBlocks = 4; + if (nBlocks > 4) + nBlocks = 4; - int bitLen = 22 + 40*nBlocks; - int byteLen = ceil(bitLen / 8.0); + int bitLen = 22 + 40 * nBlocks; + int byteLen = ceil(bitLen / 8.0); - vector message; - message.resize(byteLen); + vector message; + message.resize(byteLen); - unsigned char* buf = message.data(); + unsigned char* buf = message.data(); - int multipleMessage = 1; + int multipleMessage = 1; - if ((buffSize - ibuff) <= 20) - multipleMessage = 0; + if ((buffSize - ibuff) <= 20) + multipleMessage = 0; - int i=0; - i = setbituInc(buf,i,12, 4073); - i = setbituInc(buf,i,4, 10); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,3, indMess); - i = setbituInc(buf,i,2, nBlocks); + int i = 0; + i = setbituInc(buf, i, 12, 4073); + i = setbituInc(buf, i, 4, 10); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 3, indMess); + i = setbituInc(buf, i, 2, nBlocks); - for (int ibyte = 0; ibyte<(5*nBlocks) && ibuff quickout; + vector quickout; - quickout.resize(message.size()); + quickout.resize(message.size()); - copy(message.begin(), message.end(), quickout.begin()); + copy(message.begin(), message.end(), quickout.begin()); - cmpSSRServiceMessages.erase(ind); + cmpSSRServiceMessages.erase(ind); - return quickout; - } + return quickout; + } - vector dummy; - return dummy; + vector dummy; + return dummy; } diff --git a/src/cpp/other_ssr/prototypeIgsSSRDecode.cpp b/src/cpp/other_ssr/prototypeIgsSSRDecode.cpp index 1edecc6c6..aa8a82dfd 100644 --- a/src/cpp/other_ssr/prototypeIgsSSRDecode.cpp +++ b/src/cpp/other_ssr/prototypeIgsSSRDecode.cpp @@ -1,755 +1,757 @@ +#define IGSSSRTRCLVL 4 #include +#include "common/biases.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/rtcmDecoder.hpp" +#include "common/ssr.hpp" +#include "other_ssr/otherSSR.hpp" using std::pair; -#include "rtcmDecoder.hpp" -#include "navigation.hpp" -#include "otherSSR.hpp" -#include "biases.hpp" -#include "gTime.hpp" -#include "enums.h" -#include "ssr.hpp" - -#define IGSSSRTRCLVL 4 - struct SSRHeader { - GTime time; - int updateInterval = 1; - int iod = -1; - int numSats; - int dispBiasConsis; - int mwConsis; - int numLayers; - double vtecQuality; - - SSRMeta ssrMeta; + GTime time; + int updateInterval = 1; + int iod = -1; + int numSats; + int dispBiasConsis; + int mwConsis; + int numLayers; + double vtecQuality; + + SSRMeta ssrMeta; }; -map> IGSSSRIndex2Code = -{ - { E_Sys::GPS, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1P}, - { 2, E_ObsCode::L1W}, - { 3, E_ObsCode::L1S}, - { 4, E_ObsCode::L1L}, - { 5, E_ObsCode::L2C}, - { 6, E_ObsCode::L2D}, - { 7, E_ObsCode::L2S}, - { 8, E_ObsCode::L2L}, - {10, E_ObsCode::L2P}, - {11, E_ObsCode::L2W}, - {14, E_ObsCode::L5I}, - {15, E_ObsCode::L5Q} - } - }, - { E_Sys::GLO, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1P}, - { 2, E_ObsCode::L2C}, - { 3, E_ObsCode::L2P}, - { 4, E_ObsCode::L4A}, - { 5, E_ObsCode::L4B}, - { 6, E_ObsCode::L6A}, - { 7, E_ObsCode::L6B}, - { 8, E_ObsCode::L3I}, - { 9, E_ObsCode::L3Q} - } - }, - { E_Sys::GAL, - { - { 0, E_ObsCode::L1A}, - { 1, E_ObsCode::L1B}, - { 2, E_ObsCode::L1C}, - { 5, E_ObsCode::L5I}, - { 6, E_ObsCode::L5Q}, - { 8, E_ObsCode::L7I}, - { 9, E_ObsCode::L7Q}, - {14, E_ObsCode::L6A}, - {15, E_ObsCode::L6B}, - {16, E_ObsCode::L6C} - } - }, - { E_Sys::QZS, - { - { 0, E_ObsCode::L1C}, - { 1, E_ObsCode::L1S}, - { 2, E_ObsCode::L1L}, - { 3, E_ObsCode::L2S}, - { 4, E_ObsCode::L2L}, - { 6, E_ObsCode::L5I}, - { 7, E_ObsCode::L5Q}, - { 9, E_ObsCode::L6S}, - {10, E_ObsCode::L6L}, - {17, E_ObsCode::L6E} - } - }, - { E_Sys::BDS, - { - { 0, E_ObsCode::L2I}, - { 1, E_ObsCode::L2Q}, - { 3, E_ObsCode::L6I}, - { 4, E_ObsCode::L6Q}, - { 6, E_ObsCode::L7I}, - { 7, E_ObsCode::L7Q}, - { 9, E_ObsCode::L1D}, - {10, E_ObsCode::L1P}, - {12, E_ObsCode::L5D}, - {13, E_ObsCode::L5P}, - {15, E_ObsCode::L1A}, - {18, E_ObsCode::L6A} - } - } +map> IGSSSRIndex2Code = { + {E_Sys::GPS, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1P}, + {2, E_ObsCode::L1W}, + {3, E_ObsCode::L1S}, + {4, E_ObsCode::L1L}, + {5, E_ObsCode::L2C}, + {6, E_ObsCode::L2D}, + {7, E_ObsCode::L2S}, + {8, E_ObsCode::L2L}, + {10, E_ObsCode::L2P}, + {11, E_ObsCode::L2W}, + {14, E_ObsCode::L5I}, + {15, E_ObsCode::L5Q}}}, + {E_Sys::GLO, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1P}, + {2, E_ObsCode::L2C}, + {3, E_ObsCode::L2P}, + {4, E_ObsCode::L4A}, + {5, E_ObsCode::L4B}, + {6, E_ObsCode::L6A}, + {7, E_ObsCode::L6B}, + {8, E_ObsCode::L3I}, + {9, E_ObsCode::L3Q}}}, + {E_Sys::GAL, + {{0, E_ObsCode::L1A}, + {1, E_ObsCode::L1B}, + {2, E_ObsCode::L1C}, + {5, E_ObsCode::L5I}, + {6, E_ObsCode::L5Q}, + {8, E_ObsCode::L7I}, + {9, E_ObsCode::L7Q}, + {14, E_ObsCode::L6A}, + {15, E_ObsCode::L6B}, + {16, E_ObsCode::L6C}}}, + {E_Sys::QZS, + {{0, E_ObsCode::L1C}, + {1, E_ObsCode::L1S}, + {2, E_ObsCode::L1L}, + {3, E_ObsCode::L2S}, + {4, E_ObsCode::L2L}, + {6, E_ObsCode::L5I}, + {7, E_ObsCode::L5Q}, + {9, E_ObsCode::L6S}, + {10, E_ObsCode::L6L}, + {17, E_ObsCode::L6E}}}, + {E_Sys::BDS, + {{0, E_ObsCode::L2I}, + {1, E_ObsCode::L2Q}, + {3, E_ObsCode::L6I}, + {4, E_ObsCode::L6Q}, + {6, E_ObsCode::L7I}, + {7, E_ObsCode::L7Q}, + {9, E_ObsCode::L1D}, + {10, E_ObsCode::L1P}, + {12, E_ObsCode::L5D}, + {13, E_ObsCode::L5P}, + {15, E_ObsCode::L1A}, + {18, E_ObsCode::L6A}}} }; -map> igsSSRCode2Index = -{ - { E_Sys::GPS, - { - {E_ObsCode::L1C, 0}, - {E_ObsCode::L1P, 1}, - {E_ObsCode::L1W, 2}, - {E_ObsCode::L1S, 3}, - {E_ObsCode::L1L, 4}, - {E_ObsCode::L2C, 5}, - {E_ObsCode::L2D, 6}, - {E_ObsCode::L2S, 7}, - {E_ObsCode::L2L, 8}, - {E_ObsCode::L2P,10}, - {E_ObsCode::L2W, 11}, - {E_ObsCode::L5I, 14}, - {E_ObsCode::L5Q, 15} - } - }, - { E_Sys::GLO, - { - { E_ObsCode::L1C, 0}, - { E_ObsCode::L1P, 1}, - { E_ObsCode::L2C, 2}, - { E_ObsCode::L2P, 3}, - { E_ObsCode::L4A, 4}, - { E_ObsCode::L4B, 5}, - { E_ObsCode::L6A, 6}, - { E_ObsCode::L6B, 7}, - { E_ObsCode::L3I, 8}, - { E_ObsCode::L3Q, 9} - } - }, - { E_Sys::GAL, - { - {E_ObsCode::L1A, 0}, - {E_ObsCode::L1B, 1}, - {E_ObsCode::L1C, 2}, - {E_ObsCode::L5I, 5}, - {E_ObsCode::L5Q, 6}, - {E_ObsCode::L7I, 8}, - {E_ObsCode::L7Q, 9}, - {E_ObsCode::L6A, 14}, - {E_ObsCode::L6B, 15}, - {E_ObsCode::L6C, 16} - } - }, - { E_Sys::QZS, - { - {E_ObsCode::L1C, 0}, - {E_ObsCode::L1S, 1}, - {E_ObsCode::L1L, 2}, - {E_ObsCode::L2S, 3}, - {E_ObsCode::L2L, 4}, - {E_ObsCode::L5I, 6}, - {E_ObsCode::L5Q, 7}, - {E_ObsCode::L6S, 9}, - {E_ObsCode::L6L, 10}, - {E_ObsCode::L6E, 17} - } - }, - { E_Sys::BDS, - { - {E_ObsCode::L2I, 0}, - {E_ObsCode::L2Q, 1}, - {E_ObsCode::L6I, 3}, - {E_ObsCode::L6Q, 4}, - {E_ObsCode::L7I, 6}, - {E_ObsCode::L7Q, 7}, - {E_ObsCode::L1D, 9}, - {E_ObsCode::L1P, 10}, - {E_ObsCode::L5D, 12}, - {E_ObsCode::L5P, 13}, - {E_ObsCode::L1A, 15}, - {E_ObsCode::L6A, 18} - } - } +map> igsSSRCode2Index = { + {E_Sys::GPS, + {{E_ObsCode::L1C, 0}, + {E_ObsCode::L1P, 1}, + {E_ObsCode::L1W, 2}, + {E_ObsCode::L1S, 3}, + {E_ObsCode::L1L, 4}, + {E_ObsCode::L2C, 5}, + {E_ObsCode::L2D, 6}, + {E_ObsCode::L2S, 7}, + {E_ObsCode::L2L, 8}, + {E_ObsCode::L2P, 10}, + {E_ObsCode::L2W, 11}, + {E_ObsCode::L5I, 14}, + {E_ObsCode::L5Q, 15}}}, + {E_Sys::GLO, + {{E_ObsCode::L1C, 0}, + {E_ObsCode::L1P, 1}, + {E_ObsCode::L2C, 2}, + {E_ObsCode::L2P, 3}, + {E_ObsCode::L4A, 4}, + {E_ObsCode::L4B, 5}, + {E_ObsCode::L6A, 6}, + {E_ObsCode::L6B, 7}, + {E_ObsCode::L3I, 8}, + {E_ObsCode::L3Q, 9}}}, + {E_Sys::GAL, + {{E_ObsCode::L1A, 0}, + {E_ObsCode::L1B, 1}, + {E_ObsCode::L1C, 2}, + {E_ObsCode::L5I, 5}, + {E_ObsCode::L5Q, 6}, + {E_ObsCode::L7I, 8}, + {E_ObsCode::L7Q, 9}, + {E_ObsCode::L6A, 14}, + {E_ObsCode::L6B, 15}, + {E_ObsCode::L6C, 16}}}, + {E_Sys::QZS, + {{E_ObsCode::L1C, 0}, + {E_ObsCode::L1S, 1}, + {E_ObsCode::L1L, 2}, + {E_ObsCode::L2S, 3}, + {E_ObsCode::L2L, 4}, + {E_ObsCode::L5I, 6}, + {E_ObsCode::L5Q, 7}, + {E_ObsCode::L6S, 9}, + {E_ObsCode::L6L, 10}, + {E_ObsCode::L6E, 17}}}, + {E_Sys::BDS, + {{E_ObsCode::L2I, 0}, + {E_ObsCode::L2Q, 1}, + {E_ObsCode::L6I, 3}, + {E_ObsCode::L6Q, 4}, + {E_ObsCode::L7I, 6}, + {E_ObsCode::L7Q, 7}, + {E_ObsCode::L1D, 9}, + {E_ObsCode::L1P, 10}, + {E_ObsCode::L5D, 12}, + {E_ObsCode::L5P, 13}, + {E_ObsCode::L1A, 15}, + {E_ObsCode::L6A, 18}}} }; -map igsSSRStorage; +map igsSSRStorage; GTime igsSSRlastTime; void updateNavSSR() { - for (auto& [Sat, ssrBlock] : igsSSRStorage) - { - auto& ssr = nav.satNavMap[Sat].receivedSSR; - - if (ssrBlock.ephUpdated) - if (ssr.ssrEph_map.find(ssrBlock.ssrEph.t0) == ssr.ssrEph_map.end()) - { - ssr.ssrEph_map[ssrBlock.ssrEph.t0] = ssrBlock.ssrEph; - tracepdeex(IGSSSRTRCLVL,std::cout, "\n#IGS_SSR ORBITS %s %s %4d %10.4f %10.4f %10.4f %d ", Sat.id().c_str(),ssrBlock.ssrEph.t0.to_string().c_str(), ssrBlock.ssrEph.iode,ssrBlock.ssrEph.deph[0],ssrBlock.ssrEph.deph[1],ssrBlock.ssrEph.deph[2], ssrBlock.ssrEph.iod); - } - - if (ssrBlock.clkUpdated) - if ( ssr.ssrClk_map.find(ssrBlock.ssrClk.t0) == ssr.ssrClk_map.end()) - { - ssr.ssrClk_map[ssrBlock.ssrClk.t0] = ssrBlock.ssrClk; - tracepdeex(IGSSSRTRCLVL,std::cout, "\n#IGS_SSR CLOCKS %s %s %10.4f %10.4f %10.4f %d ", Sat.id().c_str(),ssrBlock.ssrClk.t0.to_string().c_str(), ssrBlock.ssrClk.dclk[0], ssrBlock.ssrClk.dclk[1],ssrBlock.ssrClk.dclk[2], ssrBlock.ssrClk.iod); - } - - if (ssrBlock.hrclkUpdated) - if (ssr.ssrHRClk_map.find(ssrBlock.ssrHRClk.t0) == ssr.ssrHRClk_map.end()) - { - ssr.ssrHRClk_map[ssrBlock.ssrHRClk.t0] = ssrBlock.ssrHRClk; - } - - if (ssrBlock.codeUpdated) - { - BiasEntry entry; - string id = Sat.id() + ":" + Sat.sysChar(); - entry.measType = CODE; - entry.Sat = Sat; - entry.tini = ssrBlock.ssrCodeBias.t0 - ssrBlock.ssrCodeBias.udi / 2.0; - entry.tfin = entry.tini + acsConfig.ssrInOpts.code_bias_valid_time; - entry.source = "ssr"; - - tracepdeex(IGSSSRTRCLVL,std::cout, "\n#IGS_SSR CODBIA %s %s: ", - Sat.id().c_str(),ssrBlock.ssrCodeBias.t0.to_string().c_str()); - - for (auto& [code,biasSSR] : ssrBlock.ssrCodeBias.obsCodeBiasMap) - { - entry.cod1 = code; - entry.cod2 = E_ObsCode::NONE; - entry.bias = -biasSSR.bias; - entry.var = 0; - entry.slop = 0; - entry.slpv = 0; - - pushBiasEntry(id, entry); - tracepdeex(IGSSSRTRCLVL,std::cout, "%s %9.4f; ", code._to_string(), biasSSR.bias); - } - - if (ssr.ssrCodeBias_map.find(ssrBlock.ssrCodeBias.t0) == ssr.ssrCodeBias_map.end()) - { - ssr.ssrCodeBias_map[ssrBlock.ssrCodeBias.t0] = ssrBlock.ssrCodeBias; - } - } - - if (ssrBlock.phaseUpdated) - { - BiasEntry entry; - string id = Sat.id() + ":" + string(1, Sat.sysChar()); - entry.measType = PHAS; - entry.Sat = Sat; - entry.tini = ssrBlock.ssrPhasBias.t0 - ssrBlock.ssrPhasBias.udi/2.0; - entry.tfin = entry.tini + acsConfig.ssrInOpts.code_bias_valid_time; - entry.source = "ssr"; - - tracepdeex(IGSSSRTRCLVL,std::cout, "\n#IGS_SSR PHSBIA %s %s: ", Sat.id().c_str(),ssrBlock.ssrPhasBias.t0.to_string().c_str()); - - for (auto& [code,biasSSR] : ssrBlock.ssrPhasBias.obsCodeBiasMap) - { - entry.cod1 = code; - entry.cod2 = E_ObsCode::NONE; - entry.bias = -biasSSR.bias; - entry.var = 0; - entry.slop = 0; - entry.slpv = 0; - - pushBiasEntry(id, entry); - tracepdeex(IGSSSRTRCLVL,std::cout, "%s %9.4f; ", code._to_string(), biasSSR.bias); - } - - if (ssr.ssrPhasBias_map.find(ssrBlock.ssrPhasBias.t0) == ssr.ssrPhasBias_map.end()) - { - ssr.ssrPhasBias_map[ssrBlock.ssrPhasBias.t0] = ssrBlock.ssrPhasBias; - } - } - - if (ssrBlock.uraUpdated) - if (ssr.ssrUra_map.find(ssrBlock.ssrUra.t0) == ssr.ssrUra_map.end()) - { - ssr.ssrUra_map[ssrBlock.ssrUra.t0] = ssrBlock.ssrUra; - } - } - - igsSSRStorage.clear(); + for (auto& [Sat, ssrBlock] : igsSSRStorage) + { + auto& ssr = nav.satNavMap[Sat].receivedSSR; + + if (ssrBlock.ephUpdated) + if (ssr.ssrEph_map.find(ssrBlock.ssrEph.t0) == ssr.ssrEph_map.end()) + { + ssr.ssrEph_map[ssrBlock.ssrEph.t0] = ssrBlock.ssrEph; + tracepdeex( + IGSSSRTRCLVL, + std::cout, + "\n#IGS_SSR ORBITS %s %s %4d %10.4f %10.4f %10.4f %d ", + Sat.id().c_str(), + ssrBlock.ssrEph.t0.to_string().c_str(), + ssrBlock.ssrEph.iode, + ssrBlock.ssrEph.deph[0], + ssrBlock.ssrEph.deph[1], + ssrBlock.ssrEph.deph[2], + ssrBlock.ssrEph.iod + ); + } + + if (ssrBlock.clkUpdated) + if (ssr.ssrClk_map.find(ssrBlock.ssrClk.t0) == ssr.ssrClk_map.end()) + { + ssr.ssrClk_map[ssrBlock.ssrClk.t0] = ssrBlock.ssrClk; + tracepdeex( + IGSSSRTRCLVL, + std::cout, + "\n#IGS_SSR CLOCKS %s %s %10.4f %10.4f %10.4f %d ", + Sat.id().c_str(), + ssrBlock.ssrClk.t0.to_string().c_str(), + ssrBlock.ssrClk.dclk[0], + ssrBlock.ssrClk.dclk[1], + ssrBlock.ssrClk.dclk[2], + ssrBlock.ssrClk.iod + ); + } + + if (ssrBlock.hrclkUpdated) + if (ssr.ssrHRClk_map.find(ssrBlock.ssrHRClk.t0) == ssr.ssrHRClk_map.end()) + { + ssr.ssrHRClk_map[ssrBlock.ssrHRClk.t0] = ssrBlock.ssrHRClk; + } + + if (ssrBlock.codeUpdated) + { + BiasEntry entry; + string id = Sat.id() + ":" + Sat.sysChar(); + entry.measType = CODE; + entry.Sat = Sat; + entry.tini = ssrBlock.ssrCodeBias.t0 - ssrBlock.ssrCodeBias.udi / 2.0; + entry.tfin = entry.tini + acsConfig.ssrInOpts.code_bias_valid_time; + entry.source = "ssr"; + + tracepdeex( + IGSSSRTRCLVL, + std::cout, + "\n#IGS_SSR CODBIA %s %s: ", + Sat.id().c_str(), + ssrBlock.ssrCodeBias.t0.to_string().c_str() + ); + + for (auto& [code, biasSSR] : ssrBlock.ssrCodeBias.obsCodeBiasMap) + { + entry.cod1 = code; + entry.cod2 = E_ObsCode::NONE; + entry.bias = -biasSSR.bias; + entry.var = 0; + entry.slop = 0; + entry.slpv = 0; + + updateRefTime(entry); + pushBiasEntry(id, entry); + tracepdeex( + IGSSSRTRCLVL, + std::cout, + "%s %9.4f; ", + enum_to_string(code), + biasSSR.bias + ); + } + + if (ssr.ssrCodeBias_map.find(ssrBlock.ssrCodeBias.t0) == ssr.ssrCodeBias_map.end()) + { + ssr.ssrCodeBias_map[ssrBlock.ssrCodeBias.t0] = ssrBlock.ssrCodeBias; + } + } + + if (ssrBlock.phaseUpdated) + { + BiasEntry entry; + string id = Sat.id() + ":" + string(1, Sat.sysChar()); + entry.measType = PHAS; + entry.Sat = Sat; + entry.tini = ssrBlock.ssrPhasBias.t0 - ssrBlock.ssrPhasBias.udi / 2.0; + entry.tfin = entry.tini + acsConfig.ssrInOpts.code_bias_valid_time; + entry.source = "ssr"; + + tracepdeex( + IGSSSRTRCLVL, + std::cout, + "\n#IGS_SSR PHSBIA %s %s: ", + Sat.id().c_str(), + ssrBlock.ssrPhasBias.t0.to_string().c_str() + ); + + for (auto& [code, biasSSR] : ssrBlock.ssrPhasBias.obsCodeBiasMap) + { + entry.cod1 = code; + entry.cod2 = E_ObsCode::NONE; + entry.bias = -biasSSR.bias; + entry.var = 0; + entry.slop = 0; + entry.slpv = 0; + + updateRefTime(entry); + pushBiasEntry(id, entry); + tracepdeex( + IGSSSRTRCLVL, + std::cout, + "%s %9.4f; ", + enum_to_string(code), + biasSSR.bias + ); + } + + if (ssr.ssrPhasBias_map.find(ssrBlock.ssrPhasBias.t0) == ssr.ssrPhasBias_map.end()) + { + ssr.ssrPhasBias_map[ssrBlock.ssrPhasBias.t0] = ssrBlock.ssrPhasBias; + } + } + + if (ssrBlock.uraUpdated) + if (ssr.ssrUra_map.find(ssrBlock.ssrUra.t0) == ssr.ssrUra_map.end()) + { + ssr.ssrUra_map[ssrBlock.ssrUra.t0] = ssrBlock.ssrUra; + } + } + + igsSSRStorage.clear(); } -int decodeigsSSR_header( - vector& data, - GTime now, - int opt, - SSRHeader& ssrHead) +int decodeigsSSR_header(vector& data, GTime now, int opt, SSRHeader& ssrHead) { - SSRMeta& ssrMeta = ssrHead.ssrMeta; - - int i = 23; - ssrMeta.epochTime1s = getbituInc(data, i, 20); - ssrMeta.updateIntIndex = getbituInc(data, i, 4); - - ssrHead.updateInterval = ssrUdi[ssrMeta.updateIntIndex]; - ssrMeta.multipleMessage = getbituInc(data, i, 1); - ssrHead.iod = getbituInc(data, i, 4); - ssrMeta.provider = getbituInc(data, i, 16); - ssrMeta.solution = getbituInc(data, i, 4); - - if (opt == 1) - ssrMeta.referenceDatum = getbituInc(data, i, 1); - - if (opt == 2) - { - ssrHead.dispBiasConsis = getbituInc(data, i, 1); - ssrHead.mwConsis = getbituInc(data, i, 1); - } - - if (opt == 3) - { - ssrHead.vtecQuality = getbituInc(data, i, 9)*0.05; - ssrHead.numLayers = getbituInc(data, i, 2)+1; - } - else - ssrHead.numSats = getbituInc(data, i, 6); - - double tow = ssrMeta.epochTime1s; - ssrMeta.receivedTime = GTime(GTow(tow), now); - - if (ssrHead.updateInterval > 1) - tow+= 0.5 * ssrHead.updateInterval; - - ssrHead.time = GTime(GTow(tow), now); - return i; + SSRMeta& ssrMeta = ssrHead.ssrMeta; + + int i = 23; + ssrMeta.epochTime1s = getbituInc(data, i, 20); + ssrMeta.updateIntIndex = getbituInc(data, i, 4); + + ssrHead.updateInterval = ssrUdi[ssrMeta.updateIntIndex]; + ssrMeta.multipleMessage = getbituInc(data, i, 1); + ssrHead.iod = getbituInc(data, i, 4); + ssrMeta.provider = getbituInc(data, i, 16); + ssrMeta.solution = getbituInc(data, i, 4); + + if (opt == 1) + ssrMeta.referenceDatum = getbituInc(data, i, 1); + + if (opt == 2) + { + ssrHead.dispBiasConsis = getbituInc(data, i, 1); + ssrHead.mwConsis = getbituInc(data, i, 1); + } + + if (opt == 3) + { + ssrHead.vtecQuality = getbituInc(data, i, 9) * 0.05; + ssrHead.numLayers = getbituInc(data, i, 2) + 1; + } + else + ssrHead.numSats = getbituInc(data, i, 6); + + double tow = ssrMeta.epochTime1s; + ssrMeta.receivedTime = GTime(GTow(tow), now); + + if (ssrHead.updateInterval > 1) + tow += 0.5 * ssrHead.updateInterval; + + ssrHead.time = GTime(GTow(tow), now); + return i; } /* orbit */ -void decodeigsSSR_type1( - vector& data, - GTime now, - E_Sys sys) +void decodeigsSSR_type1(vector& data, GTime now, E_Sys sys) { - SSRHeader ssrHead; - int i = decodeigsSSR_header(data, now, 1, ssrHead); - - if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) - igsSSRlastTime = ssrHead.ssrMeta.receivedTime; - if (now < igsSSRlastTime) - return; - - if (i==0) - return; - - for (int sat = 0; sat < ssrHead.numSats && i + 135 <= data.size() * 8; sat++) - { - SSREph ssrEph; - int satId = getbituInc(data, i, 6); - ssrEph.iode = getbituInc(data, i, 8); - ssrEph.deph[0] = getbitsInc(data, i, 22) * 0.1e-3; // Position, radial, along track, cross track. - ssrEph.deph[1] = getbitsInc(data, i, 20) * 0.4e-3; - ssrEph.deph[2] = getbitsInc(data, i, 20) * 0.4e-3; - ssrEph.ddeph[0] = getbitsInc(data, i, 21) * 0.001e-3; // Velocity - ssrEph.ddeph[1] = getbitsInc(data, i, 19) * 0.004e-3; - ssrEph.ddeph[2] = getbitsInc(data, i, 19) * 0.004e-3; - - ssrEph.ssrMeta = ssrHead.ssrMeta; - ssrEph.t0 = ssrHead.time; - ssrEph.udi = ssrHead.updateInterval; - ssrEph.iod = ssrHead.iod; - - SatSys Sat(sys, satId); - igsSSRStorage[Sat].ssrEph = ssrEph; - igsSSRStorage[Sat].ephUpdated = true; - } - - if (ssrHead.ssrMeta.multipleMessage == 0) - updateNavSSR(); - - return; + SSRHeader ssrHead; + int i = decodeigsSSR_header(data, now, 1, ssrHead); + + if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) + igsSSRlastTime = ssrHead.ssrMeta.receivedTime; + if (now < igsSSRlastTime) + return; + + if (i == 0) + return; + + for (int sat = 0; sat < ssrHead.numSats && i + 135 <= data.size() * 8; sat++) + { + SSREph ssrEph; + int satId = getbituInc(data, i, 6); + ssrEph.iode = getbituInc(data, i, 8); + ssrEph.deph[0] = + getbitsInc(data, i, 22) * 0.1e-3; // Position, radial, along track, cross track. + ssrEph.deph[1] = getbitsInc(data, i, 20) * 0.4e-3; + ssrEph.deph[2] = getbitsInc(data, i, 20) * 0.4e-3; + ssrEph.ddeph[0] = getbitsInc(data, i, 21) * 0.001e-3; // Velocity + ssrEph.ddeph[1] = getbitsInc(data, i, 19) * 0.004e-3; + ssrEph.ddeph[2] = getbitsInc(data, i, 19) * 0.004e-3; + + ssrEph.ssrMeta = ssrHead.ssrMeta; + ssrEph.t0 = ssrHead.time; + ssrEph.udi = ssrHead.updateInterval; + ssrEph.iod = ssrHead.iod; + + SatSys Sat(sys, satId); + igsSSRStorage[Sat].ssrEph = ssrEph; + igsSSRStorage[Sat].ephUpdated = true; + } + + if (ssrHead.ssrMeta.multipleMessage == 0) + updateNavSSR(); + + return; } /* clock */ -void decodeigsSSR_type2( - vector& data, - GTime now, - E_Sys sys) +void decodeigsSSR_type2(vector& data, GTime now, E_Sys sys) { - SSRHeader ssrHead; - int i = decodeigsSSR_header(data, now, 0, ssrHead); - - if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) - igsSSRlastTime = ssrHead.ssrMeta.receivedTime; - if (now < igsSSRlastTime) - return; - - for (int sat = 0; sat < ssrHead.numSats && i + 76 <= data.size() * 8; sat++) - { - SSRClk ssrClk; - int satId = getbituInc(data, i, 6); - ssrClk.dclk[0] = getbitsInc(data, i, 22) * 0.1e-3; - ssrClk.dclk[1] = getbitsInc(data, i, 21) * 0.001e-3; - ssrClk.dclk[2] = getbitsInc(data, i, 27) * 0.00002e-3; - - ssrClk.ssrMeta = ssrHead.ssrMeta; - ssrClk.t0 = ssrHead.time; - ssrClk.udi = ssrHead.updateInterval; - ssrClk.iod = ssrHead.iod; - - SatSys Sat(sys, satId); - igsSSRStorage[Sat].ssrClk = ssrClk; - igsSSRStorage[Sat].clkUpdated = true; - } - - if (ssrHead.ssrMeta.multipleMessage == 0) - updateNavSSR(); - - return; + SSRHeader ssrHead; + int i = decodeigsSSR_header(data, now, 0, ssrHead); + + if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) + igsSSRlastTime = ssrHead.ssrMeta.receivedTime; + if (now < igsSSRlastTime) + return; + + for (int sat = 0; sat < ssrHead.numSats && i + 76 <= data.size() * 8; sat++) + { + SSRClk ssrClk; + int satId = getbituInc(data, i, 6); + ssrClk.dclk[0] = getbitsInc(data, i, 22) * 0.1e-3; + ssrClk.dclk[1] = getbitsInc(data, i, 21) * 0.001e-3; + ssrClk.dclk[2] = getbitsInc(data, i, 27) * 0.00002e-3; + + ssrClk.ssrMeta = ssrHead.ssrMeta; + ssrClk.t0 = ssrHead.time; + ssrClk.udi = ssrHead.updateInterval; + ssrClk.iod = ssrHead.iod; + + SatSys Sat(sys, satId); + igsSSRStorage[Sat].ssrClk = ssrClk; + igsSSRStorage[Sat].clkUpdated = true; + } + + if (ssrHead.ssrMeta.multipleMessage == 0) + updateNavSSR(); + + return; } /* combined */ -void decodeigsSSR_type3( - vector& data, - GTime now, - E_Sys sys) +void decodeigsSSR_type3(vector& data, GTime now, E_Sys sys) { - SSRHeader ssrHead; - int i = decodeigsSSR_header(data, now, 1, ssrHead); - - if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) - igsSSRlastTime = ssrHead.ssrMeta.receivedTime; - if (now < igsSSRlastTime) - return; - - for (int sat = 0; sat < ssrHead.numSats && i + 205 <= data.size() * 8; sat++) - { - SSREph ssrEph; - SSRClk ssrClk; - - int satId = getbituInc(data, i, 6); - ssrEph.iode = getbituInc(data, i, 8); - ssrEph.deph[0] = getbitsInc(data, i, 22) * 0.1e-3; // Position, radial, along track, cross track. - ssrEph.deph[1] = getbitsInc(data, i, 20) * 0.4e-3; - ssrEph.deph[2] = getbitsInc(data, i, 20) * 0.4e-3; - ssrEph.ddeph[0] = getbitsInc(data, i, 21) * 0.001e-3; // Velocity - ssrEph.ddeph[1] = getbitsInc(data, i, 19) * 0.004e-3; - ssrEph.ddeph[2] = getbitsInc(data, i, 19) * 0.004e-3; - ssrClk.dclk[0] = getbitsInc(data, i, 22) * 0.1e-3; - ssrClk.dclk[1] = getbitsInc(data, i, 21) * 0.001e-3; - ssrClk.dclk[2] = getbitsInc(data, i, 27) * 0.00002e-3; - - ssrEph.ssrMeta = ssrHead.ssrMeta; - ssrEph.t0 = ssrHead.time; - ssrEph.udi = ssrHead.updateInterval; - ssrEph.iod = ssrHead.iod; - - ssrClk.ssrMeta = ssrHead.ssrMeta; - ssrClk.t0 = ssrHead.time; - ssrClk.udi = ssrHead.updateInterval; - ssrClk.iod = ssrHead.iod; - - SatSys Sat(sys, satId); - igsSSRStorage[Sat].ssrEph = ssrEph; - igsSSRStorage[Sat].ephUpdated = true; - igsSSRStorage[Sat].ssrClk = ssrClk; - igsSSRStorage[Sat].clkUpdated = true; - } - - if (ssrHead.ssrMeta.multipleMessage == 0) - updateNavSSR(); - - return; + SSRHeader ssrHead; + int i = decodeigsSSR_header(data, now, 1, ssrHead); + + if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) + igsSSRlastTime = ssrHead.ssrMeta.receivedTime; + if (now < igsSSRlastTime) + return; + + for (int sat = 0; sat < ssrHead.numSats && i + 205 <= data.size() * 8; sat++) + { + SSREph ssrEph; + SSRClk ssrClk; + + int satId = getbituInc(data, i, 6); + ssrEph.iode = getbituInc(data, i, 8); + ssrEph.deph[0] = + getbitsInc(data, i, 22) * 0.1e-3; // Position, radial, along track, cross track. + ssrEph.deph[1] = getbitsInc(data, i, 20) * 0.4e-3; + ssrEph.deph[2] = getbitsInc(data, i, 20) * 0.4e-3; + ssrEph.ddeph[0] = getbitsInc(data, i, 21) * 0.001e-3; // Velocity + ssrEph.ddeph[1] = getbitsInc(data, i, 19) * 0.004e-3; + ssrEph.ddeph[2] = getbitsInc(data, i, 19) * 0.004e-3; + ssrClk.dclk[0] = getbitsInc(data, i, 22) * 0.1e-3; + ssrClk.dclk[1] = getbitsInc(data, i, 21) * 0.001e-3; + ssrClk.dclk[2] = getbitsInc(data, i, 27) * 0.00002e-3; + + ssrEph.ssrMeta = ssrHead.ssrMeta; + ssrEph.t0 = ssrHead.time; + ssrEph.udi = ssrHead.updateInterval; + ssrEph.iod = ssrHead.iod; + + ssrClk.ssrMeta = ssrHead.ssrMeta; + ssrClk.t0 = ssrHead.time; + ssrClk.udi = ssrHead.updateInterval; + ssrClk.iod = ssrHead.iod; + + SatSys Sat(sys, satId); + igsSSRStorage[Sat].ssrEph = ssrEph; + igsSSRStorage[Sat].ephUpdated = true; + igsSSRStorage[Sat].ssrClk = ssrClk; + igsSSRStorage[Sat].clkUpdated = true; + } + + if (ssrHead.ssrMeta.multipleMessage == 0) + updateNavSSR(); + + return; } /* HR clocks */ -void decodeigsSSR_type4( - vector& data, - GTime now, - E_Sys sys) +void decodeigsSSR_type4(vector& data, GTime now, E_Sys sys) { - SSRHeader ssrHead; - int i = decodeigsSSR_header(data, now, 0, ssrHead); - - if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) - igsSSRlastTime = ssrHead.ssrMeta.receivedTime; - if (now < igsSSRlastTime) - return; - - for (int sat = 0; sat < ssrHead.numSats && i + 28 <= data.size() * 8; sat++) - { - SSRHRClk ssrHRClk; - int satId = getbituInc(data, i, 6); - ssrHRClk.hrclk = getbitsInc(data, i, 22) * 0.1e-3; - - ssrHRClk.ssrMeta = ssrHead.ssrMeta; - ssrHRClk.t0 = ssrHead.time; - ssrHRClk.udi = ssrHead.updateInterval; - ssrHRClk.iod = ssrHead.iod; - - SatSys Sat(sys, satId); - igsSSRStorage[Sat].ssrHRClk = ssrHRClk; - igsSSRStorage[Sat].hrclkUpdated = true; - } - - if (ssrHead.ssrMeta.multipleMessage == 0) - updateNavSSR(); - - return; + SSRHeader ssrHead; + int i = decodeigsSSR_header(data, now, 0, ssrHead); + + if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) + igsSSRlastTime = ssrHead.ssrMeta.receivedTime; + if (now < igsSSRlastTime) + return; + + for (int sat = 0; sat < ssrHead.numSats && i + 28 <= data.size() * 8; sat++) + { + SSRHRClk ssrHRClk; + int satId = getbituInc(data, i, 6); + ssrHRClk.hrclk = getbitsInc(data, i, 22) * 0.1e-3; + + ssrHRClk.ssrMeta = ssrHead.ssrMeta; + ssrHRClk.t0 = ssrHead.time; + ssrHRClk.udi = ssrHead.updateInterval; + ssrHRClk.iod = ssrHead.iod; + + SatSys Sat(sys, satId); + igsSSRStorage[Sat].ssrHRClk = ssrHRClk; + igsSSRStorage[Sat].hrclkUpdated = true; + } + + if (ssrHead.ssrMeta.multipleMessage == 0) + updateNavSSR(); + + return; } /* Code Bias */ -void decodeigsSSR_type5( - vector& data, - GTime now, - E_Sys sys) +void decodeigsSSR_type5(vector& data, GTime now, E_Sys sys) { - SSRHeader ssrHead; - int i = decodeigsSSR_header(data, now, 0, ssrHead); - - if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) - igsSSRlastTime = ssrHead.ssrMeta.receivedTime; - if (now < igsSSRlastTime) - return; - - for (int sat = 0; sat < ssrHead.numSats && i + 11 <= data.size() * 8; sat++) - { - int satId = getbituInc(data, i, 6); - int nbias = getbituInc(data, i, 5); - - SSRCodeBias ssrBiasCode; - ssrBiasCode.ssrMeta = ssrHead.ssrMeta; - ssrBiasCode.t0 = ssrHead.time; - ssrBiasCode.udi = ssrHead.updateInterval; - ssrBiasCode.iod = ssrHead.iod; - - for (int k = 0; k < nbias && i + 19 <= data.size() * 8; k++) - { - int rtcm_code = getbituInc(data, i, 5); - double bias = getbitsInc(data, i, 14) * 0.01; - if (IGSSSRIndex2Code[sys].find(rtcm_code) == IGSSSRIndex2Code[sys].end()) - continue; - E_ObsCode code = IGSSSRIndex2Code[sys][rtcm_code]; - ssrBiasCode.obsCodeBiasMap[code].bias = bias; - } - - SatSys Sat(sys, satId); - igsSSRStorage[Sat].ssrCodeBias = ssrBiasCode; - igsSSRStorage[Sat].codeUpdated = true; - } - - if (ssrHead.ssrMeta.multipleMessage == 0) - updateNavSSR(); - - return; + SSRHeader ssrHead; + int i = decodeigsSSR_header(data, now, 0, ssrHead); + + if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) + igsSSRlastTime = ssrHead.ssrMeta.receivedTime; + if (now < igsSSRlastTime) + return; + + for (int sat = 0; sat < ssrHead.numSats && i + 11 <= data.size() * 8; sat++) + { + int satId = getbituInc(data, i, 6); + int nbias = getbituInc(data, i, 5); + + SSRCodeBias ssrBiasCode; + ssrBiasCode.ssrMeta = ssrHead.ssrMeta; + ssrBiasCode.t0 = ssrHead.time; + ssrBiasCode.udi = ssrHead.updateInterval; + ssrBiasCode.iod = ssrHead.iod; + + for (int k = 0; k < nbias && i + 19 <= data.size() * 8; k++) + { + int rtcm_code = getbituInc(data, i, 5); + double bias = getbitsInc(data, i, 14) * 0.01; + if (IGSSSRIndex2Code[sys].find(rtcm_code) == IGSSSRIndex2Code[sys].end()) + continue; + E_ObsCode code = IGSSSRIndex2Code[sys][rtcm_code]; + ssrBiasCode.obsCodeBiasMap[code].bias = bias; + } + + SatSys Sat(sys, satId); + igsSSRStorage[Sat].ssrCodeBias = ssrBiasCode; + igsSSRStorage[Sat].codeUpdated = true; + } + + if (ssrHead.ssrMeta.multipleMessage == 0) + updateNavSSR(); + + return; } /* Phase Bias */ -void decodeigsSSR_type6( - vector& data, - GTime now, - E_Sys sys) +void decodeigsSSR_type6(vector& data, GTime now, E_Sys sys) { - SSRHeader ssrHead; - int i = decodeigsSSR_header(data, now, 2, ssrHead); - - if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) - igsSSRlastTime = ssrHead.ssrMeta.receivedTime; - if (now < igsSSRlastTime) - return; - - for (int sat = 0; sat < ssrHead.numSats && i + 28 <= data.size() * 8; sat++) - { - int satId = getbituInc(data, i, 6); - SSRPhase ssrPhase; - ssrPhase.dispBiasConistInd = ssrHead.dispBiasConsis; - ssrPhase.MWConistInd = ssrHead.mwConsis; - - int nbias = getbituInc(data, i, 5); - ssrPhase.yawAngle = getbituInc(data, i, 9)/256.0 *PI; - ssrPhase.yawRate = getbitsInc(data, i, 8)/8192.0 *PI; - - SSRPhasBias ssrBiasPhas; - ssrBiasPhas.ssrMeta = ssrHead.ssrMeta; - ssrBiasPhas.t0 = ssrHead.time; - ssrBiasPhas.udi = ssrHead.updateInterval; - ssrBiasPhas.iod = ssrHead.iod; - ssrBiasPhas.ssrPhase = ssrPhase; - - for (int k = 0; k < nbias && i + 32 <= data.size() * 8; k++) - { - int rtcm_code = getbituInc(data, i, 5); - - SSRPhaseCh ssrPhaseCh; - ssrPhaseCh.signalIntInd = getbituInc(data, i, 1); - ssrPhaseCh.signalWLIntInd = getbituInc(data, i, 2); - ssrPhaseCh.signalDisconCnt = getbituInc(data, i, 4); - double phaseBias = getbitsInc(data, i, 20) * 0.0001; - - if (IGSSSRIndex2Code[sys].find(rtcm_code) == IGSSSRIndex2Code[sys].end()) - continue; - - E_ObsCode code = IGSSSRIndex2Code[sys][rtcm_code]; - - ssrBiasPhas.obsCodeBiasMap[code].bias = phaseBias; - ssrBiasPhas.ssrPhaseChs [code] = ssrPhaseCh; - } - - SatSys Sat(sys, satId); - igsSSRStorage[Sat].ssrPhasBias = ssrBiasPhas; - igsSSRStorage[Sat].phaseUpdated = true; - } - - if (ssrHead.ssrMeta.multipleMessage == 0) - updateNavSSR(); - - return; + SSRHeader ssrHead; + int i = decodeigsSSR_header(data, now, 2, ssrHead); + + if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) + igsSSRlastTime = ssrHead.ssrMeta.receivedTime; + if (now < igsSSRlastTime) + return; + + for (int sat = 0; sat < ssrHead.numSats && i + 28 <= data.size() * 8; sat++) + { + int satId = getbituInc(data, i, 6); + SSRPhase ssrPhase; + ssrPhase.dispBiasConistInd = ssrHead.dispBiasConsis; + ssrPhase.MWConistInd = ssrHead.mwConsis; + + int nbias = getbituInc(data, i, 5); + ssrPhase.yawAngle = getbituInc(data, i, 9) / 256.0 * PI; + ssrPhase.yawRate = getbitsInc(data, i, 8) / 8192.0 * PI; + + SSRPhasBias ssrBiasPhas; + ssrBiasPhas.ssrMeta = ssrHead.ssrMeta; + ssrBiasPhas.t0 = ssrHead.time; + ssrBiasPhas.udi = ssrHead.updateInterval; + ssrBiasPhas.iod = ssrHead.iod; + ssrBiasPhas.ssrPhase = ssrPhase; + + for (int k = 0; k < nbias && i + 32 <= data.size() * 8; k++) + { + int rtcm_code = getbituInc(data, i, 5); + + SSRPhaseCh ssrPhaseCh; + ssrPhaseCh.signalIntInd = getbituInc(data, i, 1); + ssrPhaseCh.signalWLIntInd = getbituInc(data, i, 2); + ssrPhaseCh.signalDisconCnt = getbituInc(data, i, 4); + double phaseBias = getbitsInc(data, i, 20) * 0.0001; + + if (IGSSSRIndex2Code[sys].find(rtcm_code) == IGSSSRIndex2Code[sys].end()) + continue; + + E_ObsCode code = IGSSSRIndex2Code[sys][rtcm_code]; + + ssrBiasPhas.obsCodeBiasMap[code].bias = phaseBias; + ssrBiasPhas.ssrPhaseChs[code] = ssrPhaseCh; + } + + SatSys Sat(sys, satId); + igsSSRStorage[Sat].ssrPhasBias = ssrBiasPhas; + igsSSRStorage[Sat].phaseUpdated = true; + } + + if (ssrHead.ssrMeta.multipleMessage == 0) + updateNavSSR(); + + return; } /* URA message */ -void decodeigsSSR_type7( - vector& data, - GTime now, - E_Sys sys) +void decodeigsSSR_type7(vector& data, GTime now, E_Sys sys) { - SSRHeader ssrHead; - int i = decodeigsSSR_header(data, now, 0, ssrHead); - - if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) - igsSSRlastTime = ssrHead.ssrMeta.receivedTime; - if (now < igsSSRlastTime) - return; - - for (int sat = 0; sat < ssrHead.numSats && i + 12 <= data.size() * 8; sat++) - { - SSRUra ssrUra; - int satId = getbituInc(data, i, 6); - int uraInd = getbitsInc(data, i, 6); - - ssrUra.t0 = ssrHead.time; - ssrUra.udi = ssrHead.updateInterval; - ssrUra.iod = ssrHead.iod; - ssrUra.ura = uraInd; - - SatSys Sat(sys, satId); - igsSSRStorage[Sat].ssrUra = ssrUra; - igsSSRStorage[Sat].uraUpdated = true; - } - - if (ssrHead.ssrMeta.multipleMessage == 0) - updateNavSSR(); - - return; + SSRHeader ssrHead; + int i = decodeigsSSR_header(data, now, 0, ssrHead); + + if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) + igsSSRlastTime = ssrHead.ssrMeta.receivedTime; + if (now < igsSSRlastTime) + return; + + for (int sat = 0; sat < ssrHead.numSats && i + 12 <= data.size() * 8; sat++) + { + SSRUra ssrUra; + int satId = getbituInc(data, i, 6); + int uraInd = getbitsInc(data, i, 6); + + ssrUra.t0 = ssrHead.time; + ssrUra.udi = ssrHead.updateInterval; + ssrUra.iod = ssrHead.iod; + ssrUra.ura = uraInd; + + SatSys Sat(sys, satId); + igsSSRStorage[Sat].ssrUra = ssrUra; + igsSSRStorage[Sat].uraUpdated = true; + } + + if (ssrHead.ssrMeta.multipleMessage == 0) + updateNavSSR(); + + return; } /* Iono VTEC */ -void decodeigsSSR_type8( - vector& data, - GTime now) +void decodeigsSSR_type8(vector& data, GTime now) { - SSRHeader ssrHead; - - int i = decodeigsSSR_header(data, now, 3, ssrHead); - - if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) - igsSSRlastTime = ssrHead.ssrMeta.receivedTime; - if (now < igsSSRlastTime) - return; - - SSRAtmGlobal ssrAtmGlobal; - ssrAtmGlobal.numberLayers = ssrHead.numLayers; //todo aaron, can these be the same thing? - ssrAtmGlobal.vtecQuality = ssrHead.vtecQuality; - ssrAtmGlobal.time = ssrHead.time; - - for (int layerNum = 0; layerNum < ssrHead.numLayers && i + 16 <= data.size() * 8; layerNum++) - { - auto& layer = ssrAtmGlobal.layers[layerNum]; - - layer.height = getbituInc(data, i, 8) * 10; - layer.maxDegree = getbituInc(data, i, 4) + 1; - layer.maxOrder = getbituInc(data, i, 4) + 1; - - 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? - { - layer.sphHarmonic[nind].layer = layerNum; - - auto& sphComp = layer.sphHarmonic[nind]; - sphComp.order = ord; - sphComp.degree = deg; - sphComp.trigType = E_TrigType::SIN; - sphComp.value = getbitsInc(data, i, 16) * 0.005; - - if ((i+16)>(data.size()*8)) - return; - - nind++; - } - for (int ord = 1; ord < layer.maxOrder; ord++) - for (int deg = ord; deg < layer.maxDegree && i + 16 <= data.size() * 8; deg++) - { - layer.sphHarmonic[nind].layer = layerNum; - - auto& sphComp = layer.sphHarmonic[nind]; - sphComp.order = ord; - sphComp.degree = deg; - sphComp.trigType = E_TrigType::COS; - sphComp.value = getbitsInc(data, i, 16) * 0.005; - - if ((i+16)>(data.size()*8)) - return; - - nind++; - } - } - - if (ssrHead.ssrMeta.multipleMessage == 0) - updateNavSSR(); - - nav.ssrAtm.atmosGlobalMap[ssrAtmGlobal.time] = ssrAtmGlobal; - - return; + SSRHeader ssrHead; + + int i = decodeigsSSR_header(data, now, 3, ssrHead); + + if (ssrHead.ssrMeta.receivedTime > igsSSRlastTime) + igsSSRlastTime = ssrHead.ssrMeta.receivedTime; + if (now < igsSSRlastTime) + return; + + SSRAtmGlobal ssrAtmGlobal; + ssrAtmGlobal.numberLayers = ssrHead.numLayers; // todo aaron, can these be the same thing? + ssrAtmGlobal.vtecQuality = ssrHead.vtecQuality; + ssrAtmGlobal.time = ssrHead.time; + + for (int layerNum = 0; layerNum < ssrHead.numLayers && i + 16 <= data.size() * 8; layerNum++) + { + auto& layer = ssrAtmGlobal.layers[layerNum]; + + layer.height = getbituInc(data, i, 8) * 10; + layer.maxDegree = getbituInc(data, i, 4) + 1; + layer.maxOrder = getbituInc(data, i, 4) + 1; + + 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? + { + layer.sphHarmonic[nind].layer = layerNum; + + auto& sphComp = layer.sphHarmonic[nind]; + sphComp.order = ord; + sphComp.degree = deg; + sphComp.trigType = E_TrigType::SIN; + sphComp.value = getbitsInc(data, i, 16) * 0.005; + + if ((i + 16) > (data.size() * 8)) + return; + + nind++; + } + for (int ord = 1; ord < layer.maxOrder; ord++) + for (int deg = ord; deg < layer.maxDegree && i + 16 <= data.size() * 8; deg++) + { + layer.sphHarmonic[nind].layer = layerNum; + + auto& sphComp = layer.sphHarmonic[nind]; + sphComp.order = ord; + sphComp.degree = deg; + sphComp.trigType = E_TrigType::COS; + sphComp.value = getbitsInc(data, i, 16) * 0.005; + + if ((i + 16) > (data.size() * 8)) + return; + + nind++; + } + } + + if (ssrHead.ssrMeta.multipleMessage == 0) + updateNavSSR(); + + nav.ssrAtm.atmosGlobalMap[ssrAtmGlobal.time] = ssrAtmGlobal; + + return; } -E_ReturnType decodeigsSSR( - vector& data, - GTime now) +E_ReturnType decodeigsSSR(vector& data, GTime now) { - if (data.size() < 7) - return E_ReturnType::BAD_LENGTH; - - if (now < igsSSRlastTime) - return E_ReturnType::WAIT; - - int stype = getbitu(data,15,8); - IgsSSRSubtype subType = IgsSSRSubtype::_from_integral(stype); - - E_Sys sys; - IgsSSRSubtype group = IGS_SSR_group(subType, sys); - - switch (group) - { - case IgsSSRSubtype::GROUP_ORB: decodeigsSSR_type1(data, now, sys); break; - case IgsSSRSubtype::GROUP_CLK: decodeigsSSR_type2(data, now, sys); break; - case IgsSSRSubtype::GROUP_CMB: decodeigsSSR_type3(data, now, sys); break; - case IgsSSRSubtype::GROUP_HRC: decodeigsSSR_type4(data, now, sys); break; - case IgsSSRSubtype::GROUP_COD: decodeigsSSR_type5(data, now, sys); break; - case IgsSSRSubtype::GROUP_PHS: decodeigsSSR_type6(data, now, sys); break; - case IgsSSRSubtype::GROUP_URA: decodeigsSSR_type7(data, now, sys); break; - case IgsSSRSubtype::GROUP_ION: decodeigsSSR_type8(data, now); break; - } - - if (now < igsSSRlastTime) - return E_ReturnType::WAIT; - - return E_ReturnType::OK; + if (data.size() < 7) + return E_ReturnType::BAD_LENGTH; + + if (now < igsSSRlastTime) + return E_ReturnType::WAIT; + + int stype = getbitu(data, 15, 8); + IgsSSRSubtype subType = int_to_enum(stype); + + E_Sys sys; + IgsSSRSubtype group = IGS_SSR_group(subType, sys); + + switch (group) + { + case IgsSSRSubtype::GROUP_ORB: + decodeigsSSR_type1(data, now, sys); + break; + case IgsSSRSubtype::GROUP_CLK: + decodeigsSSR_type2(data, now, sys); + break; + case IgsSSRSubtype::GROUP_CMB: + decodeigsSSR_type3(data, now, sys); + break; + case IgsSSRSubtype::GROUP_HRC: + decodeigsSSR_type4(data, now, sys); + break; + case IgsSSRSubtype::GROUP_COD: + decodeigsSSR_type5(data, now, sys); + break; + case IgsSSRSubtype::GROUP_PHS: + decodeigsSSR_type6(data, now, sys); + break; + case IgsSSRSubtype::GROUP_URA: + decodeigsSSR_type7(data, now, sys); + break; + case IgsSSRSubtype::GROUP_ION: + decodeigsSSR_type8(data, now); + break; + } + + if (now < igsSSRlastTime) + return E_ReturnType::WAIT; + + return E_ReturnType::OK; } diff --git a/src/cpp/other_ssr/prototypeIgsSSREncode.cpp b/src/cpp/other_ssr/prototypeIgsSSREncode.cpp index 342652451..d1e832c98 100644 --- a/src/cpp/other_ssr/prototypeIgsSSREncode.cpp +++ b/src/cpp/other_ssr/prototypeIgsSSREncode.cpp @@ -1,670 +1,700 @@ +#include "common/constants.hpp" +#include "common/navigation.hpp" +#include "common/rtcmEncoder.hpp" +#include "common/satSys.hpp" +#include "common/ssr.hpp" +#include "other_ssr/otherSSR.hpp" -#include "rtcmEncoder.hpp" -#include "navigation.hpp" -#include "constants.hpp" -#include "otherSSR.hpp" -#include "satSys.hpp" -#include "ssr.hpp" +map last_clock; -map last_clock; - -unsigned short IGS_SSR_subtype( - IgsSSRSubtype type, - E_Sys sys) +unsigned short IGS_SSR_subtype(IgsSSRSubtype type, E_Sys sys) { - if (type == +IgsSSRSubtype::GROUP_ION) return IgsSSRSubtype::IONVTEC; - - switch (sys) - { - case E_Sys::GPS: return type + IgsSSRSubtype::GPS_OFFSET; - case E_Sys::GLO: return type + IgsSSRSubtype::GLO_OFFSET; - case E_Sys::GAL: return type + IgsSSRSubtype::GAL_OFFSET; - case E_Sys::QZS: return type + IgsSSRSubtype::QZS_OFFSET; - case E_Sys::BDS: return type + IgsSSRSubtype::BDS_OFFSET; - case E_Sys::SBS: return type + IgsSSRSubtype::SBS_OFFSET; - } - return IgsSSRSubtype::NONE; + // Direct enum comparison (BETTER_ENUM unary '+' removed) + if (type == IgsSSRSubtype::GROUP_ION) + return static_cast(IgsSSRSubtype::IONVTEC); + + switch (sys) + { + case E_Sys::GPS: + return static_cast( + static_cast(type) + static_cast(IgsSSRSubtype::GPS_OFFSET) + ); + case E_Sys::GLO: + return static_cast( + static_cast(type) + static_cast(IgsSSRSubtype::GLO_OFFSET) + ); + case E_Sys::GAL: + return static_cast( + static_cast(type) + static_cast(IgsSSRSubtype::GAL_OFFSET) + ); + case E_Sys::QZS: + return static_cast( + static_cast(type) + static_cast(IgsSSRSubtype::QZS_OFFSET) + ); + case E_Sys::BDS: + return static_cast( + static_cast(type) + static_cast(IgsSSRSubtype::BDS_OFFSET) + ); + case E_Sys::SBS: + return static_cast( + static_cast(type) + static_cast(IgsSSRSubtype::SBS_OFFSET) + ); + } + return static_cast(IgsSSRSubtype::NONE); } -IgsSSRSubtype IGS_SSR_group( - IgsSSRSubtype subType, - E_Sys& sys) +IgsSSRSubtype IGS_SSR_group(IgsSSRSubtype subType, E_Sys& sys) { - sys = E_Sys::NONE; - if (subType == +IgsSSRSubtype::IONVTEC) - { - return IgsSSRSubtype::GROUP_ION; - } - - int iSubType = subType; - - switch (iSubType / 20) - { - case 1: sys = E_Sys::GPS; return IgsSSRSubtype::_from_integral(iSubType % 20); - case 2: sys = E_Sys::GLO; return IgsSSRSubtype::_from_integral(iSubType % 20); - case 3: sys = E_Sys::GAL; return IgsSSRSubtype::_from_integral(iSubType % 20); - case 4: sys = E_Sys::QZS; return IgsSSRSubtype::_from_integral(iSubType % 20); - case 5: sys = E_Sys::BDS; return IgsSSRSubtype::_from_integral(iSubType % 20); - case 6: sys = E_Sys::SBS; return IgsSSRSubtype::_from_integral(iSubType % 20); - } - return IgsSSRSubtype::NONE; + sys = E_Sys::NONE; + if (subType == IgsSSRSubtype::IONVTEC) + { + return IgsSSRSubtype::GROUP_ION; + } + + int iSubType = static_cast(subType); + + switch (iSubType / 20) + { + case 1: + sys = E_Sys::GPS; + return int_to_enum(iSubType % 20); + case 2: + sys = E_Sys::GLO; + return int_to_enum(iSubType % 20); + case 3: + sys = E_Sys::GAL; + return int_to_enum(iSubType % 20); + case 4: + sys = E_Sys::QZS; + return int_to_enum(iSubType % 20); + case 5: + sys = E_Sys::BDS; + return int_to_enum(iSubType % 20); + case 6: + sys = E_Sys::SBS; + return int_to_enum(iSubType % 20); + } + return IgsSSRSubtype::NONE; } -vector encodeIGS_ORB( - map& orbClkMap, - E_Sys sys, - bool last) +vector encodeIGS_ORB(map& orbClkMap, E_Sys sys, bool last) { - tracepdeex(2,std::cout,"\n Encoding IGS SSR ORB for %s", sys._to_string()); - - int numSat = orbClkMap.size(); - if (numSat == 0) - { - return vector(); - } - - auto& [Sat, ssrOut1] = *orbClkMap.begin(); - auto& ssrEph = ssrOut1.ssrEph; - auto& ssrMeta = ssrEph.ssrMeta; - - int bitLen = 78 + numSat * 135; - int byteLen = ceil(bitLen/8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_ORB, sys); - unsigned int multipleMessage = last?0:1; - -// tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); - - int i = 0; - i = setbituInc(buf,i,12, 4076); - i = setbituInc(buf,i,3, 1); - i = setbituInc(buf,i,8, submessCode); - i = setbituInc(buf,i,20, ssrMeta.epochTime1s); - i = setbituInc(buf,i,4, ssrMeta.updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, ssrEph.iod); - i = setbituInc(buf,i,16, ssrMeta.provider); - i = setbituInc(buf,i,4, ssrMeta.solution); - i = setbituInc(buf,i,1, ssrMeta.referenceDatum); - i = setbituInc(buf,i,6, numSat); - - for (auto& [Sat, ssrOut] : orbClkMap) - { - auto& ssrEph = ssrOut.ssrEph; - - i = setbituInc(buf,i, 6, Sat.prn); - i = setbituInc(buf,i, 8, ssrEph.iode); - - int d; - d = (int)round(ssrEph.deph[0] / 0.1e-3); i = setbitsInc(buf,i,22,d); - d = (int)round(ssrEph.deph[1] / 0.4e-3); i = setbitsInc(buf,i,20,d); - d = (int)round(ssrEph.deph[2] / 0.4e-3); i = setbitsInc(buf,i,20,d); - d = (int)round(ssrEph.ddeph[0] / 0.001e-3); i = setbitsInc(buf,i,21,d); - d = (int)round(ssrEph.ddeph[1] / 0.004e-3); i = setbitsInc(buf,i,19,d); - d = (int)round(ssrEph.ddeph[2] / 0.004e-3); i = setbitsInc(buf,i,19,d); - } - int bitl = byteLen*8-i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding orbit.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - return buffer; + tracepdeex(2, std::cout, "\n Encoding IGS SSR ORB for %s", enum_to_string(sys)); + + int numSat = orbClkMap.size(); + if (numSat == 0) + { + return vector(); + } + + auto& [Sat, ssrOut1] = *orbClkMap.begin(); + auto& ssrEph = ssrOut1.ssrEph; + auto& ssrMeta = ssrEph.ssrMeta; + + int bitLen = 78 + numSat * 135; + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_ORB, sys); + unsigned int multipleMessage = last ? 0 : 1; + + // tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); + + int i = 0; + i = setbituInc(buf, i, 12, 4076); + i = setbituInc(buf, i, 3, 1); + i = setbituInc(buf, i, 8, submessCode); + i = setbituInc(buf, i, 20, ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, ssrEph.iod); + i = setbituInc(buf, i, 16, ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrMeta.solution); + i = setbituInc(buf, i, 1, ssrMeta.referenceDatum); + i = setbituInc(buf, i, 6, numSat); + + for (auto& [Sat, ssrOut] : orbClkMap) + { + auto& ssrEph = ssrOut.ssrEph; + + i = setbituInc(buf, i, 6, Sat.prn); + i = setbituInc(buf, i, 8, ssrEph.iode); + + int d; + d = (int)round(ssrEph.deph[0] / 0.1e-3); + i = setbitsInc(buf, i, 22, d); + d = (int)round(ssrEph.deph[1] / 0.4e-3); + i = setbitsInc(buf, i, 20, d); + d = (int)round(ssrEph.deph[2] / 0.4e-3); + i = setbitsInc(buf, i, 20, d); + d = (int)round(ssrEph.ddeph[0] / 0.001e-3); + i = setbitsInc(buf, i, 21, d); + d = (int)round(ssrEph.ddeph[1] / 0.004e-3); + i = setbitsInc(buf, i, 19, d); + d = (int)round(ssrEph.ddeph[2] / 0.004e-3); + i = setbitsInc(buf, i, 19, d); + } + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding orbit.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + return buffer; } -vector encodeIGS_CLK( - map& orbClkMap, - E_Sys sys, - bool last) +vector encodeIGS_CLK(map& orbClkMap, E_Sys sys, bool last) { - tracepdeex(2,std::cout,"\n Encoding IGS SSR CLK for %s", sys._to_string()); - - int numSat = orbClkMap.size(); - if (numSat == 0) - { - return vector(); - } - - auto& [Sat, ssrOut1] = *orbClkMap.begin(); - auto& ssrEph = ssrOut1.ssrEph; - auto& ssrMeta = ssrEph.ssrMeta; - - int bitLen = 78 + numSat * 76; - int byteLen = ceil(bitLen/8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_CLK, sys); - unsigned int multipleMessage = last?0:1; - -// tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); - - int i = 0; - i = setbituInc(buf,i,12, 4076); - i = setbituInc(buf,i,3, 1); - i = setbituInc(buf,i,8, submessCode); - i = setbituInc(buf,i,20, ssrMeta.epochTime1s); - i = setbituInc(buf,i,4, ssrMeta.updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, ssrEph.iod); - i = setbituInc(buf,i,16, ssrMeta.provider); - i = setbituInc(buf,i,4, ssrMeta.solution); - i = setbituInc(buf,i,6, numSat); - - for (auto& [Sat, ssrOut] : orbClkMap) - { - auto& ssrClk = ssrOut.ssrClk; - - i = setbituInc(buf,i, 6, Sat.prn); - - int d; - d = (int)round(ssrClk.dclk[0] / 0.1e-3); i = setbitsInc(buf,i,22,d); - d = (int)round(ssrClk.dclk[1] / 0.001e-3); i = setbitsInc(buf,i,21,d); - d = (int)round(ssrClk.dclk[2] / 0.00002e-3); i = setbitsInc(buf,i,27,d); - - last_clock[Sat] = ssrClk; - } - int bitl = byteLen*8-i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding clock.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - return buffer; + tracepdeex(2, std::cout, "\n Encoding IGS SSR CLK for %s", enum_to_string(sys)); + + int numSat = orbClkMap.size(); + if (numSat == 0) + { + return vector(); + } + + auto& [Sat, ssrOut1] = *orbClkMap.begin(); + auto& ssrEph = ssrOut1.ssrEph; + auto& ssrMeta = ssrEph.ssrMeta; + + int bitLen = 78 + numSat * 76; + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_CLK, sys); + unsigned int multipleMessage = last ? 0 : 1; + + // tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); + + int i = 0; + i = setbituInc(buf, i, 12, 4076); + i = setbituInc(buf, i, 3, 1); + i = setbituInc(buf, i, 8, submessCode); + i = setbituInc(buf, i, 20, ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, ssrEph.iod); + i = setbituInc(buf, i, 16, ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrMeta.solution); + i = setbituInc(buf, i, 6, numSat); + + for (auto& [Sat, ssrOut] : orbClkMap) + { + auto& ssrClk = ssrOut.ssrClk; + + i = setbituInc(buf, i, 6, Sat.prn); + + int d; + d = (int)round(ssrClk.dclk[0] / 0.1e-3); + i = setbitsInc(buf, i, 22, d); + d = (int)round(ssrClk.dclk[1] / 0.001e-3); + i = setbitsInc(buf, i, 21, d); + d = (int)round(ssrClk.dclk[2] / 0.00002e-3); + i = setbitsInc(buf, i, 27, d); + + last_clock[Sat] = ssrClk; + } + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding clock.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + return buffer; } -vector encodeIGS_CMB( - map& orbClkMap, - E_Sys sys, - bool last) +vector encodeIGS_CMB(map& orbClkMap, E_Sys sys, bool last) { - tracepdeex(2,std::cout,"\n Encoding IGS SSR CMB for %s", sys._to_string()); - - int numSat = orbClkMap.size(); - if (numSat == 0) - { - return vector(); - } - - auto& [Sat, ssrOut1] = *orbClkMap.begin(); - auto& ssrEph = ssrOut1.ssrEph; - auto& ssrMeta = ssrEph.ssrMeta; - - int bitLen = 79 + numSat * 205; - int byteLen = ceil(bitLen/8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - -// tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); - - unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_CMB, sys); - unsigned int multipleMessage = last?0:1; - - int i = 0; - i = setbituInc(buf,i,12, 4076); - i = setbituInc(buf,i,3, 1); - i = setbituInc(buf,i,8, submessCode); - i = setbituInc(buf,i,20, ssrMeta.epochTime1s); - i = setbituInc(buf,i,4, ssrMeta.updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, ssrEph.iod); - i = setbituInc(buf,i,16, ssrMeta.provider); - i = setbituInc(buf,i,4, ssrMeta.solution); - i = setbituInc(buf,i,1, ssrMeta.referenceDatum); - i = setbituInc(buf,i,6, numSat); - - for (auto& [Sat, ssrOut] : orbClkMap) - { - auto& ssrEph = ssrOut.ssrEph; - auto& ssrClk = ssrOut.ssrClk; - - i = setbituInc(buf,i, 6, Sat.prn); - i = setbituInc(buf,i, 8, ssrEph.iode); - - int d; - d = (int)round(ssrEph.deph[0] / 0.1e-3); i = setbitsInc(buf,i,22,d); - d = (int)round(ssrEph.deph[1] / 0.4e-3); i = setbitsInc(buf,i,20,d); - d = (int)round(ssrEph.deph[2] / 0.4e-3); i = setbitsInc(buf,i,20,d); - d = (int)round(ssrEph.ddeph[0] / 0.001e-3); i = setbitsInc(buf,i,21,d); - d = (int)round(ssrEph.ddeph[1] / 0.004e-3); i = setbitsInc(buf,i,19,d); - d = (int)round(ssrEph.ddeph[2] / 0.004e-3); i = setbitsInc(buf,i,19,d); - - d = (int)round(ssrClk.dclk[0] / 0.1e-3); i = setbitsInc(buf,i,22,d); - d = (int)round(ssrClk.dclk[1] / 0.001e-3); i = setbitsInc(buf,i,21,d); - d = (int)round(ssrClk.dclk[2] / 0.00002e-3); i = setbitsInc(buf,i,27,d); - - last_clock[Sat] = ssrClk; - } - int bitl = byteLen*8-i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding combined.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - return buffer; + tracepdeex(2, std::cout, "\n Encoding IGS SSR CMB for %s", enum_to_string(sys)); + + int numSat = orbClkMap.size(); + if (numSat == 0) + { + return vector(); + } + + auto& [Sat, ssrOut1] = *orbClkMap.begin(); + auto& ssrEph = ssrOut1.ssrEph; + auto& ssrMeta = ssrEph.ssrMeta; + + int bitLen = 79 + numSat * 205; + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + // tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); + + unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_CMB, sys); + unsigned int multipleMessage = last ? 0 : 1; + + int i = 0; + i = setbituInc(buf, i, 12, 4076); + i = setbituInc(buf, i, 3, 1); + i = setbituInc(buf, i, 8, submessCode); + i = setbituInc(buf, i, 20, ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, ssrEph.iod); + i = setbituInc(buf, i, 16, ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrMeta.solution); + i = setbituInc(buf, i, 1, ssrMeta.referenceDatum); + i = setbituInc(buf, i, 6, numSat); + + for (auto& [Sat, ssrOut] : orbClkMap) + { + auto& ssrEph = ssrOut.ssrEph; + auto& ssrClk = ssrOut.ssrClk; + + i = setbituInc(buf, i, 6, Sat.prn); + i = setbituInc(buf, i, 8, ssrEph.iode); + + int d; + d = (int)round(ssrEph.deph[0] / 0.1e-3); + i = setbitsInc(buf, i, 22, d); + d = (int)round(ssrEph.deph[1] / 0.4e-3); + i = setbitsInc(buf, i, 20, d); + d = (int)round(ssrEph.deph[2] / 0.4e-3); + i = setbitsInc(buf, i, 20, d); + d = (int)round(ssrEph.ddeph[0] / 0.001e-3); + i = setbitsInc(buf, i, 21, d); + d = (int)round(ssrEph.ddeph[1] / 0.004e-3); + i = setbitsInc(buf, i, 19, d); + d = (int)round(ssrEph.ddeph[2] / 0.004e-3); + i = setbitsInc(buf, i, 19, d); + + d = (int)round(ssrClk.dclk[0] / 0.1e-3); + i = setbitsInc(buf, i, 22, d); + d = (int)round(ssrClk.dclk[1] / 0.001e-3); + i = setbitsInc(buf, i, 21, d); + d = (int)round(ssrClk.dclk[2] / 0.00002e-3); + i = setbitsInc(buf, i, 27, d); + + last_clock[Sat] = ssrClk; + } + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding combined.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + return buffer; } -vector encodeIGS_HRC( - map& orbClkMap, - E_Sys sys, - bool last) +vector encodeIGS_HRC(map& orbClkMap, E_Sys sys, bool last) { - tracepdeex(2,std::cout,"\n Encoding IGS SSR HRC for %s", sys._to_string()); - - map hrClocks; - for (auto& [Sat, ssrOut] : orbClkMap) - { - auto& ssrClk = ssrOut.ssrClk; - - if (last_clock.find(Sat) == last_clock.end()) continue; - - if (last_clock[Sat].iod != ssrOut.ssrClk.iod) continue; - - double dt = (ssrClk.t0 - last_clock[Sat].t0).to_double(); - if (fabs(dt) > ssrUdi[ssrClk.ssrMeta.updateIntIndex]) continue; - - hrClocks[Sat] = ssrClk.dclk[0] - -(last_clock[Sat].dclk[0] - + last_clock[Sat].dclk[1]*dt - + last_clock[Sat].dclk[2]*dt*dt); - } - - int numSat = hrClocks.size(); - if (numSat == 0) - return vector(); - - auto& [Sat, ssrOut1] = *orbClkMap.begin(); - auto& ssrClk = ssrOut1.ssrClk; - auto& ssrMeta = ssrClk.ssrMeta; - - int bitLen = 78 + numSat * 28; - int byteLen = ceil(bitLen/8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_HRC, sys); - unsigned int multipleMessage = last?0:1; - -// tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); - - int i = 0; - i = setbituInc(buf,i,12, 4076); - i = setbituInc(buf,i,3, 1); - i = setbituInc(buf,i,8, submessCode); - i = setbituInc(buf,i,20, ssrMeta.epochTime1s); - i = setbituInc(buf,i,4, ssrMeta.updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, ssrClk.iod); - i = setbituInc(buf,i,16, ssrMeta.provider); - i = setbituInc(buf,i,4, ssrMeta.solution); - i = setbituInc(buf,i,6, numSat); - - for (auto& [Sat, hrClk] : hrClocks) - { - i = setbituInc(buf,i, 6, Sat.prn); - - int d = (int)round(hrClk / 0.1e-3); - i = setbitsInc(buf,i,22, d); - } - int bitl = byteLen*8-i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding HR clock.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - return buffer; + tracepdeex(2, std::cout, "\n Encoding IGS SSR HRC for %s", enum_to_string(sys)); + + map hrClocks; + for (auto& [Sat, ssrOut] : orbClkMap) + { + auto& ssrClk = ssrOut.ssrClk; + + if (last_clock.find(Sat) == last_clock.end()) + continue; + + if (last_clock[Sat].iod != ssrOut.ssrClk.iod) + continue; + + double dt = (ssrClk.t0 - last_clock[Sat].t0).to_double(); + if (fabs(dt) > ssrUdi[ssrClk.ssrMeta.updateIntIndex]) + continue; + + hrClocks[Sat] = ssrClk.dclk[0] - (last_clock[Sat].dclk[0] + last_clock[Sat].dclk[1] * dt + + last_clock[Sat].dclk[2] * dt * dt); + } + + int numSat = hrClocks.size(); + if (numSat == 0) + return vector(); + + auto& [Sat, ssrOut1] = *orbClkMap.begin(); + auto& ssrClk = ssrOut1.ssrClk; + auto& ssrMeta = ssrClk.ssrMeta; + + int bitLen = 78 + numSat * 28; + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_HRC, sys); + unsigned int multipleMessage = last ? 0 : 1; + + // tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); + + int i = 0; + i = setbituInc(buf, i, 12, 4076); + i = setbituInc(buf, i, 3, 1); + i = setbituInc(buf, i, 8, submessCode); + i = setbituInc(buf, i, 20, ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, ssrClk.iod); + i = setbituInc(buf, i, 16, ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrMeta.solution); + i = setbituInc(buf, i, 6, numSat); + + for (auto& [Sat, hrClk] : hrClocks) + { + i = setbituInc(buf, i, 6, Sat.prn); + + int d = (int)round(hrClk / 0.1e-3); + i = setbitsInc(buf, i, 22, d); + } + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding HR clock.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + return buffer; } - -vector encodeIGS_COD( - map& codBiasMap, - E_Sys sys, - bool last) +vector encodeIGS_COD(map& codBiasMap, E_Sys sys, bool last) { - tracepdeex(2,std::cout,"\n Encoding IGS SSR COD for %s", sys._to_string()); - - if (igsSSRCode2Index.find(sys) == igsSSRCode2Index.end()) - return vector(); - - int numSat = codBiasMap.size(); - - if (numSat <= 0) - return vector(); - - int totalNbias = 0; - for (auto& [Sat,ssrCodeBias] : codBiasMap) - for (auto it = ssrCodeBias.obsCodeBiasMap.begin(); it != ssrCodeBias.obsCodeBiasMap.end();) - { - auto obsCode = it->first; - if (igsSSRCode2Index[sys].find(obsCode) == igsSSRCode2Index[sys].end()) - { - it = ssrCodeBias.obsCodeBiasMap.erase(it); - } - else - { - it++; - totalNbias++; - } - } - - if (totalNbias == 0) - return vector(); - - // Write the header information. - auto s_it = codBiasMap.begin(); - auto& [Sat, ssrCodeBias] = *s_it; - SSRMeta& ssrMeta = ssrCodeBias.ssrMeta; - - int bitLen = 78+numSat*11+totalNbias*19; - int byteLen = ceil(bitLen/8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_COD, sys); - unsigned int multipleMessage = last?0:1; - -// tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); - - int i = 0; - i = setbituInc(buf,i,12, 4076); - i = setbituInc(buf,i,3, 1); - i = setbituInc(buf,i,8, submessCode); - i = setbituInc(buf,i,20, ssrMeta.epochTime1s); - i = setbituInc(buf,i,4, ssrMeta.updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, ssrCodeBias.iod); - i = setbituInc(buf,i,16, ssrMeta.provider); - i = setbituInc(buf,i,4, ssrMeta.solution); - i = setbituInc(buf,i,6, numSat); - - for (auto& [sat, ssrCodeBias] : codBiasMap) - { - int nbias= ssrCodeBias.obsCodeBiasMap.size(); - - i = setbituInc(buf,i,6, sat.prn); - i = setbituInc(buf,i,5, nbias); - - for (auto& [obsCode, entry] : ssrCodeBias.obsCodeBiasMap) - { - int rtcm_code = igsSSRCode2Index[sys][obsCode]; - int bia = (int)round(entry.bias / 0.01); - - i = setbituInc(buf,i,5, rtcm_code); - i = setbitsInc(buf,i,14,bia); - } - } - - int bitl = byteLen*8-i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Phase.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - - i = setbituInc(buf,i,bitl,0); - - return buffer; + tracepdeex(2, std::cout, "\n Encoding IGS SSR COD for %s", enum_to_string(sys)); + + if (igsSSRCode2Index.find(sys) == igsSSRCode2Index.end()) + return vector(); + + int numSat = codBiasMap.size(); + + if (numSat <= 0) + return vector(); + + int totalNbias = 0; + for (auto& [Sat, ssrCodeBias] : codBiasMap) + for (auto it = ssrCodeBias.obsCodeBiasMap.begin(); it != ssrCodeBias.obsCodeBiasMap.end();) + { + auto obsCode = it->first; + if (igsSSRCode2Index[sys].find(obsCode) == igsSSRCode2Index[sys].end()) + { + it = ssrCodeBias.obsCodeBiasMap.erase(it); + } + else + { + it++; + totalNbias++; + } + } + + if (totalNbias == 0) + return vector(); + + // Write the header information. + auto s_it = codBiasMap.begin(); + auto& [Sat, ssrCodeBias] = *s_it; + SSRMeta& ssrMeta = ssrCodeBias.ssrMeta; + + int bitLen = 78 + numSat * 11 + totalNbias * 19; + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_COD, sys); + unsigned int multipleMessage = last ? 0 : 1; + + // tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); + + int i = 0; + i = setbituInc(buf, i, 12, 4076); + i = setbituInc(buf, i, 3, 1); + i = setbituInc(buf, i, 8, submessCode); + i = setbituInc(buf, i, 20, ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, ssrCodeBias.iod); + i = setbituInc(buf, i, 16, ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrMeta.solution); + i = setbituInc(buf, i, 6, numSat); + + for (auto& [sat, ssrCodeBias] : codBiasMap) + { + int nbias = ssrCodeBias.obsCodeBiasMap.size(); + + i = setbituInc(buf, i, 6, sat.prn); + i = setbituInc(buf, i, 5, nbias); + + for (auto& [obsCode, entry] : ssrCodeBias.obsCodeBiasMap) + { + int rtcm_code = igsSSRCode2Index[sys][obsCode]; + int bia = (int)round(entry.bias / 0.01); + + i = setbituInc(buf, i, 5, rtcm_code); + i = setbitsInc(buf, i, 14, bia); + } + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Phase.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + + i = setbituInc(buf, i, bitl, 0); + + return buffer; } -vector encodeIGS_PHS( - map& ssrPBMap, - E_Sys sys, - bool last) +vector encodeIGS_PHS(map& ssrPBMap, E_Sys sys, bool last) { - tracepdeex(2,std::cout,"\n Encoding IGS SSR PHS for %s", sys._to_string()); - - if (igsSSRCode2Index.find(sys) == igsSSRCode2Index.end()) - return vector(); - - int numSat = ssrPBMap.size(); - - if (numSat <= 0) - return vector(); - - int totalNbias = 0; - for (auto& [Sat,ssrPhasBias] : ssrPBMap) - for (auto it = ssrPhasBias.obsCodeBiasMap.begin(); it != ssrPhasBias.obsCodeBiasMap.end();) - { - auto obsCode = it->first; - if (igsSSRCode2Index[sys].find(obsCode) == igsSSRCode2Index[sys].end()) - { - it = ssrPhasBias.obsCodeBiasMap.erase(it); - } - else - { - it++; - totalNbias++; - } - } - - if (totalNbias == 0) - return vector(); - - // Write the header information. - auto s_it = ssrPBMap.begin(); - auto& [Sat, ssrPhasBias] = *s_it; - SSRMeta& ssrMeta = ssrPhasBias.ssrMeta; - - int bitLen = 80+numSat*28+totalNbias*32; - int byteLen = ceil(bitLen/8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_PHS, sys); - unsigned int multipleMessage = last?0:1; - -// tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); - - int i = 0; - i = setbituInc(buf,i,12, 4076); - i = setbituInc(buf,i,3, 1); - i = setbituInc(buf,i,8, submessCode); - i = setbituInc(buf,i,20, ssrMeta.epochTime1s); - i = setbituInc(buf,i,4, ssrMeta.updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, ssrPhasBias.iod); - i = setbituInc(buf,i,16, ssrMeta.provider); - i = setbituInc(buf,i,4, ssrMeta.solution); - i = setbituInc(buf,i,1, ssrPhasBias.ssrPhase.dispBiasConistInd); - i = setbituInc(buf,i,1, ssrPhasBias.ssrPhase.MWConistInd); - i = setbituInc(buf,i,6, numSat); - - for (auto& [sat, ssrPhasBias] : ssrPBMap) - { - SSRPhase ssrPhase = ssrPhasBias.ssrPhase; - int nbias = ssrPhasBias.obsCodeBiasMap.size(); - int yaw = (int)round(ssrPhase.yawAngle* 256/PI); - int rate = (int)round(ssrPhase.yawRate *8192/PI); - - i = setbituInc(buf,i,6, sat.prn); - i = setbituInc(buf,i,5, nbias); - i = setbituInc(buf,i,9, yaw); - i = setbitsInc(buf,i,8, rate); - - for (auto& [obsCode, entry] : ssrPhasBias.obsCodeBiasMap) - { - int rtcm_code = igsSSRCode2Index[sys][obsCode]; - int bias = (int)round(entry.bias / 0.0001); - SSRPhaseCh ssrPhaseCh = ssrPhasBias.ssrPhaseChs[obsCode]; - - i = setbituInc(buf,i,5, rtcm_code); - i = setbituInc(buf,i,1, ssrPhaseCh.signalIntInd); - i = setbituInc(buf,i,2, ssrPhaseCh.signalWLIntInd); - i = setbituInc(buf,i,4, ssrPhaseCh.signalDisconCnt); - i = setbitsInc(buf,i,20, bias); - } - } - - int bitl = byteLen*8-i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Phase.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - - i = setbituInc(buf,i,bitl,0); - - return buffer; + tracepdeex(2, std::cout, "\n Encoding IGS SSR PHS for %s", enum_to_string(sys)); + + if (igsSSRCode2Index.find(sys) == igsSSRCode2Index.end()) + return vector(); + + int numSat = ssrPBMap.size(); + + if (numSat <= 0) + return vector(); + + int totalNbias = 0; + for (auto& [Sat, ssrPhasBias] : ssrPBMap) + for (auto it = ssrPhasBias.obsCodeBiasMap.begin(); it != ssrPhasBias.obsCodeBiasMap.end();) + { + auto obsCode = it->first; + if (igsSSRCode2Index[sys].find(obsCode) == igsSSRCode2Index[sys].end()) + { + it = ssrPhasBias.obsCodeBiasMap.erase(it); + } + else + { + it++; + totalNbias++; + } + } + + if (totalNbias == 0) + return vector(); + + // Write the header information. + auto s_it = ssrPBMap.begin(); + auto& [Sat, ssrPhasBias] = *s_it; + SSRMeta& ssrMeta = ssrPhasBias.ssrMeta; + + int bitLen = 80 + numSat * 28 + totalNbias * 32; + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_PHS, sys); + unsigned int multipleMessage = last ? 0 : 1; + + // tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); + + int i = 0; + i = setbituInc(buf, i, 12, 4076); + i = setbituInc(buf, i, 3, 1); + i = setbituInc(buf, i, 8, submessCode); + i = setbituInc(buf, i, 20, ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, ssrPhasBias.iod); + i = setbituInc(buf, i, 16, ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrMeta.solution); + i = setbituInc(buf, i, 1, ssrPhasBias.ssrPhase.dispBiasConistInd); + i = setbituInc(buf, i, 1, ssrPhasBias.ssrPhase.MWConistInd); + i = setbituInc(buf, i, 6, numSat); + + for (auto& [sat, ssrPhasBias] : ssrPBMap) + { + SSRPhase ssrPhase = ssrPhasBias.ssrPhase; + int nbias = ssrPhasBias.obsCodeBiasMap.size(); + int yaw = (int)round(ssrPhase.yawAngle * 256 / PI); + int rate = (int)round(ssrPhase.yawRate * 8192 / PI); + + i = setbituInc(buf, i, 6, sat.prn); + i = setbituInc(buf, i, 5, nbias); + i = setbituInc(buf, i, 9, yaw); + i = setbitsInc(buf, i, 8, rate); + + for (auto& [obsCode, entry] : ssrPhasBias.obsCodeBiasMap) + { + int rtcm_code = igsSSRCode2Index[sys][obsCode]; + int bias = (int)round(entry.bias / 0.0001); + SSRPhaseCh ssrPhaseCh = ssrPhasBias.ssrPhaseChs[obsCode]; + + i = setbituInc(buf, i, 5, rtcm_code); + i = setbituInc(buf, i, 1, ssrPhaseCh.signalIntInd); + i = setbituInc(buf, i, 2, ssrPhaseCh.signalWLIntInd); + i = setbituInc(buf, i, 4, ssrPhaseCh.signalDisconCnt); + i = setbitsInc(buf, i, 20, bias); + } + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Phase.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + + i = setbituInc(buf, i, bitl, 0); + + return buffer; } -vector encodeIGS_URA( - map& uraMap, - E_Sys sys, - bool last) +vector encodeIGS_URA(map& uraMap, E_Sys sys, bool last) { - tracepdeex(2,std::cout,"\n Encoding IGS SSR URA for %s", sys._to_string()); - - int numSat = uraMap.size(); - if (numSat == 0) - { - return vector(); - } - - auto& [Sat, ssrUra] = *uraMap.begin(); - auto& ssrMeta = ssrUra.ssrMeta; - - int bitLen = 78 + numSat * 12; - int byteLen = ceil(bitLen/8.0); - vector buffer(byteLen); - unsigned char* buf = buffer.data(); - - unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_URA, sys); - unsigned int multipleMessage = last?0:1; - -// tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); - - int i = 0; - i = setbituInc(buf,i,12, 4076); - i = setbituInc(buf,i,3, 1); - i = setbituInc(buf,i,8, submessCode); - i = setbituInc(buf,i,20, ssrMeta.epochTime1s); - i = setbituInc(buf,i,4, ssrMeta.updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, ssrUra.iod); - i = setbituInc(buf,i,16, ssrMeta.provider); - i = setbituInc(buf,i,4, ssrMeta.solution); - i = setbituInc(buf,i,6, numSat); - - for (auto& [Sat, satUra] : uraMap) - { - i = setbituInc(buf,i, 6, Sat.prn); - - int uraClass = uraToClassValue(satUra.ura); - i = setbituInc(buf,i, 6, uraClass); - } - int bitl = byteLen*8-i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding URA.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - i = setbituInc(buf, i, bitl, 0); - - return buffer; + tracepdeex(2, std::cout, "\n Encoding IGS SSR URA for %s", enum_to_string(sys)); + + int numSat = uraMap.size(); + if (numSat == 0) + { + return vector(); + } + + auto& [Sat, ssrUra] = *uraMap.begin(); + auto& ssrMeta = ssrUra.ssrMeta; + + int bitLen = 78 + numSat * 12; + int byteLen = ceil(bitLen / 8.0); + vector buffer(byteLen); + unsigned char* buf = buffer.data(); + + unsigned int submessCode = IGS_SSR_subtype(IgsSSRSubtype::GROUP_URA, sys); + unsigned int multipleMessage = last ? 0 : 1; + + // tracepdeex(2,std::cout,", %d", ssrMeta.epochTime1s); + + int i = 0; + i = setbituInc(buf, i, 12, 4076); + i = setbituInc(buf, i, 3, 1); + i = setbituInc(buf, i, 8, submessCode); + i = setbituInc(buf, i, 20, ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, ssrUra.iod); + i = setbituInc(buf, i, 16, ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrMeta.solution); + i = setbituInc(buf, i, 6, numSat); + + for (auto& [Sat, satUra] : uraMap) + { + i = setbituInc(buf, i, 6, Sat.prn); + + int uraClass = uraToClassValue(satUra.ura); + i = setbituInc(buf, i, 6, uraClass); + } + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding URA.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + i = setbituInc(buf, i, bitl, 0); + + return buffer; } - -vector encodeIGS_ATM( - SSRAtm& ssrAtm, - bool last) +vector encodeIGS_ATM(SSRAtm& ssrAtm, bool last) { - tracepdeex(2,std::cout,"\n Encoding IGS SSR ATM"); - - vector buffer; - - auto it = nav.ssrAtm.atmosGlobalMap.begin(); - if (it == nav.ssrAtm.atmosGlobalMap.end()) - return buffer; - - auto& [time, ssrGlobAtm] = *it; - - if (ssrGlobAtm.numberLayers <= 0) - return buffer; - - if ( ssrGlobAtm.layers.size() > 4 - || ssrGlobAtm.layers.size() != ssrGlobAtm.numberLayers) - { - return buffer; - } - int bitLen = 83+ssrGlobAtm.numberLayers*16; - - map>>> basisMaps; // basis coefficients indexed by layer, degree, order and parity - - for (auto& [ilay, vteclay] : ssrGlobAtm.layers) - 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? - } - - int byteLen = ceil(bitLen / 8.0); - - buffer.resize(byteLen); - unsigned char* buf = buffer.data(); - - int multipleMessage = last ? 0 : 1; - int VTECQuality = (int)round(ssrGlobAtm.vtecQuality / 0.05); - if (VTECQuality > 511) - VTECQuality = 511; - -// tracepdeex(2,std::cout,", %d", ssrAtm.ssrMeta.epochTime1s); - - int i = 0; - i = setbituInc(buf,i,12, 4076); - i = setbituInc(buf,i,3, 1); - i = setbituInc(buf,i,8, 201); - i = setbituInc(buf,i,20, ssrAtm.ssrMeta.epochTime1s); - i = setbituInc(buf,i,4, ssrAtm.ssrMeta.updateIntIndex); - i = setbituInc(buf,i,1, multipleMessage); - i = setbituInc(buf,i,4, ssrGlobAtm.iod); - i = setbituInc(buf,i,16, ssrAtm.ssrMeta.provider); - i = setbituInc(buf,i,4, ssrAtm.ssrMeta.solution); - i = setbituInc(buf,i,9, VTECQuality); - i = setbituInc(buf,i,2, ssrGlobAtm.layers.size()-1); - - for (auto& [ilay,vteclay] : ssrGlobAtm.layers) - { - int height = (int)round(vteclay.height/10); - int ndegre = vteclay.maxDegree; - int norder = vteclay.maxOrder; - - i = setbituInc(buf,i,20,height); - i = setbituInc(buf,i,4, ndegre-1); - i = setbituInc(buf,i,4, norder-1); - - for (int m = 0; m < norder; m++) - for (int n = m; n < ndegre; n++) - { - int coefC = (int)round(basisMaps[ilay][n][m][E_TrigType::SIN]/0.005); - i = setbitsInc(buf,i,16, coefC); - } - - for (int m = 1; m < norder; m++) - for (int n = m; n < ndegre; n++) - { - int coefS = (int)round(basisMaps[ilay][n][m][E_TrigType::COS]/0.005); - i = setbitsInc(buf,i,16, coefS); - } - } - - int bitl = byteLen*8-i; - if (bitl > 7 ) - { - BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Ionosphere.\n"; - BOOST_LOG_TRIVIAL(error) << "Error: bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen << "\n"; - } - - i = setbituInc(buf,i,bitl,0); - - return buffer; + tracepdeex(2, std::cout, "\n Encoding IGS SSR ATM"); + + vector buffer; + + auto it = nav.ssrAtm.atmosGlobalMap.begin(); + if (it == nav.ssrAtm.atmosGlobalMap.end()) + return buffer; + + auto& [time, ssrGlobAtm] = *it; + + if (ssrGlobAtm.numberLayers <= 0) + return buffer; + + if (ssrGlobAtm.layers.size() > 4 || ssrGlobAtm.layers.size() != ssrGlobAtm.numberLayers) + { + return buffer; + } + int bitLen = 83 + ssrGlobAtm.numberLayers * 16; + + map>>> + basisMaps; // basis coefficients indexed by layer, degree, order and parity + + for (auto& [ilay, vteclay] : ssrGlobAtm.layers) + 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? + } + + int byteLen = ceil(bitLen / 8.0); + + buffer.resize(byteLen); + unsigned char* buf = buffer.data(); + + int multipleMessage = last ? 0 : 1; + int VTECQuality = (int)round(ssrGlobAtm.vtecQuality / 0.05); + if (VTECQuality > 511) + VTECQuality = 511; + + // tracepdeex(2,std::cout,", %d", ssrAtm.ssrMeta.epochTime1s); + + int i = 0; + i = setbituInc(buf, i, 12, 4076); + i = setbituInc(buf, i, 3, 1); + i = setbituInc(buf, i, 8, 201); + i = setbituInc(buf, i, 20, ssrAtm.ssrMeta.epochTime1s); + i = setbituInc(buf, i, 4, ssrAtm.ssrMeta.updateIntIndex); + i = setbituInc(buf, i, 1, multipleMessage); + i = setbituInc(buf, i, 4, ssrGlobAtm.iod); + i = setbituInc(buf, i, 16, ssrAtm.ssrMeta.provider); + i = setbituInc(buf, i, 4, ssrAtm.ssrMeta.solution); + i = setbituInc(buf, i, 9, VTECQuality); + i = setbituInc(buf, i, 2, ssrGlobAtm.layers.size() - 1); + + for (auto& [ilay, vteclay] : ssrGlobAtm.layers) + { + int height = (int)round(vteclay.height / 10); + int ndegre = vteclay.maxDegree; + int norder = vteclay.maxOrder; + + i = setbituInc(buf, i, 20, height); + i = setbituInc(buf, i, 4, ndegre - 1); + i = setbituInc(buf, i, 4, norder - 1); + + for (int m = 0; m < norder; m++) + for (int n = m; n < ndegre; n++) + { + int coefC = (int)round(basisMaps[ilay][n][m][E_TrigType::SIN] / 0.005); + i = setbitsInc(buf, i, 16, coefC); + } + + for (int m = 1; m < norder; m++) + for (int n = m; n < ndegre; n++) + { + int coefS = (int)round(basisMaps[ilay][n][m][E_TrigType::COS] / 0.005); + i = setbitsInc(buf, i, 16, coefS); + } + } + + int bitl = byteLen * 8 - i; + if (bitl > 7) + { + BOOST_LOG_TRIVIAL(error) << "Error encoding SSR Ionosphere.\n"; + BOOST_LOG_TRIVIAL(error) << "bitl : " << bitl << ", i : " << i << ", byteLen : " << byteLen + << "\n"; + } + + i = setbituInc(buf, i, bitl, 0); + + return buffer; } diff --git a/src/cpp/pea/inputs.cpp b/src/cpp/pea/inputs.cpp index 678a0290e..ec4f48d47 100644 --- a/src/cpp/pea/inputs.cpp +++ b/src/cpp/pea/inputs.cpp @@ -1,657 +1,772 @@ - // #pragma GCC optimize ("O0") +#include "3rdparty/jpl/jpl_eph.hpp" #include "architectureDocs.hpp" - +#include "common/acsConfig.hpp" +#include "common/antenna.hpp" +#include "common/biases.hpp" +#include "common/ephPrecise.hpp" +#include "common/ionModels.hpp" +#include "common/navigation.hpp" +#include "common/sinex.hpp" +#include "common/sinexParser.hpp" +#include "common/streamCustom.hpp" +#include "common/streamFile.hpp" +#include "common/streamNtrip.hpp" +#include "common/streamRinex.hpp" +#include "common/streamRtcm.hpp" +#include "common/streamSerial.hpp" +#include "common/streamSlr.hpp" +#include "common/streamSp3.hpp" +#include "common/streamUbx.hpp" +#include "common/tides.hpp" +#include "iono/geomagField.hpp" +#include "orbprop/aod.hpp" +#include "orbprop/centerMassCorrections.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/oceanPoleTide.hpp" +#include "orbprop/spaceWeather.hpp" +#include "orbprop/staticField.hpp" +#include "orbprop/tideCoeff.hpp" +#include "pea/inputsOutputs.hpp" +#include "sbas/sisnet.hpp" +#include "trop/tropModels.hpp" Input Input_Files__() { - DOCS_REFERENCE(UBX__); - DOCS_REFERENCE(YAML__); - DOCS_REFERENCE(IGS_Files__); + DOCS_REFERENCE(UBX__); + DOCS_REFERENCE(YAML__); + DOCS_REFERENCE(IGS_Files__); } - -#include "centerMassCorrections.hpp" -#include "inputsOutputs.hpp" -#include "oceanPoleTide.hpp" -#include "streamCustom.hpp" -#include "streamSerial.hpp" -#include "sinexParser.hpp" -#include "streamRinex.hpp" -#include "streamNtrip.hpp" -#include "coordinates.hpp" -#include "staticField.hpp" -#include "geomagField.hpp" -#include "navigation.hpp" -#include "streamFile.hpp" -#include "streamRtcm.hpp" -#include "tropModels.hpp" -#include "ephPrecise.hpp" -#include "acsConfig.hpp" -#include "streamUbx.hpp" -#include "streamSlr.hpp" -#include "streamSp3.hpp" -#include "tideCoeff.hpp" -#include "ionModels.hpp" -#include "antenna.hpp" -#include "jpl_eph.hpp" -#include "biases.hpp" -#include "sisnet.hpp" -#include "sinex.hpp" -#include "tides.hpp" -#include "aod.hpp" - - /** Check that filename is valid and the file exists -*/ + */ bool checkValidFile( - const string& path, ///< Filename to check - const string& description) ///< Description for error messages + const string& path, ///< Filename to check + const string& description ///< Description for error messages +) { - if ( !path.empty() - &&!std::filesystem::exists(path)) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Missing " << description << " file " - << path; - - return false; - } - return true; + if (!path.empty() && !std::filesystem::exists(path)) + { + BOOST_LOG_TRIVIAL(error) << "Missing " << description << " file " << path; + + return false; + } + return true; } -bool checkValidFiles( - vector& paths, - const string& description) +bool checkValidFiles(vector& paths, const string& description) { - bool pass = true; - for (auto& path : paths) - { - pass &= checkValidFile(path, description); - } - return pass; + bool pass = true; + for (auto& path : paths) + { + pass &= checkValidFile(path, description); + } + return pass; } - -bool fileChanged( - string filename) +bool fileChanged(string filename) { - bool valid = checkValidFile(filename); - if (valid == false) - { - return false; - } - - std::filesystem::file_time_type modifyTime; - try - { - modifyTime = std::filesystem::last_write_time(filename); - } - catch (...) - { - //failed despite just checking it exists.. sometimes happens apparently - return false; - } - - auto it = acsConfig.configModifyTimeMap.find(filename); - if (it == acsConfig.configModifyTimeMap.end()) - { - //the first time this file has been read, - //update then return true - acsConfig.configModifyTimeMap[filename] = modifyTime; - - return true; - } - - auto& [dummy, readTime] = *it; - if (readTime != modifyTime) - { - //has a different modification time, update then return true - readTime = modifyTime; - - return true; - } - - //has been read with this time before - return false; + bool valid = checkValidFile(filename); + if (valid == false) + { + return false; + } + + std::filesystem::file_time_type modifyTime; + try + { + modifyTime = std::filesystem::last_write_time(filename); + } + catch (...) + { + // failed despite just checking it exists.. sometimes happens apparently + return false; + } + + auto it = acsConfig.configModifyTimeMap.find(filename); + if (it == acsConfig.configModifyTimeMap.end()) + { + // the first time this file has been read, + // update then return true + acsConfig.configModifyTimeMap[filename] = modifyTime; + + return true; + } + + auto& [dummy, readTime] = *it; + if (readTime != modifyTime) + { + // has a different modification time, update then return true + readTime = modifyTime; + + return true; + } + + // has been read with this time before + return false; } -void removeInvalidFiles( - vector& files) +void removeInvalidFiles(vector& files) { - for (auto it = files.begin(); it != files.end(); ) - { - auto& filename = *it; - bool valid = checkValidFile(filename); - if (valid == false) - { - it = files.erase(it); - } - else - { - it++; - } - } + for (auto it = files.begin(); it != files.end();) + { + auto& filename = *it; + bool valid = checkValidFile(filename); + if (valid == false) + { + it = files.erase(it); + } + else + { + it++; + } + } } - - -/** Create a station object from an input -*/ -void addStationData( - string stationId, ///< Id of station to add data for - vector& inputNames, ///< Filename to create station from - string inputFormat, ///< Type of data in file - string dataType) ///< Type of data +/** Create a receiver object from an input + */ +void addReceiverData( + string stationId, ///< Id of receiver to add data for + vector& inputNames, ///< Filename to create receiver from + string inputFormat, ///< Type of data in file + string dataType ///< Type of data +) { - for (auto& inputName : inputNames) - { - if (streamDOAMap.find(inputName) != streamDOAMap.end()) - { - //this stream was already added, dont re-add - continue; - } - - - string mountpoint; - auto lastSlashPos = inputName.find_last_of('/'); - if (lastSlashPos == string::npos) { mountpoint = inputName; } - else { mountpoint = inputName.substr(lastSlashPos + 1); }; - - string id = stationId; - - if (id == "") - { - id = mountpoint.substr(0,4); - } - - boost::algorithm::to_upper(id); - - auto& recOpts = acsConfig.getRecOpts(id); - - if ( recOpts.exclude - || recOpts.kill) - { - continue; - } - - string protocol; - string subInputName; - auto protocolPos = inputName.find("://"); - if (protocolPos == string::npos) { protocol = "file"; subInputName = inputName; } - else { protocol = inputName.substr(0, protocolPos); subInputName = inputName.substr(protocolPos + 3); } - - std::unique_ptr stream_ptr; - std::unique_ptr parser_ptr; - - if ( protocol == "file" - ||protocol == "serial") - { - if (checkValidFile(subInputName, dataType) == false) - { - continue; - } - } - - if (protocol == "file") { stream_ptr = make_unique (subInputName); } - else if (protocol == "serial") { stream_ptr = make_unique (subInputName); } - else if (protocol == "http") { stream_ptr = make_unique (inputName); } - else if (protocol == "https") { stream_ptr = make_unique (inputName); } - else if (protocol == "ntrip") { stream_ptr = make_unique (inputName); } - else if (protocol == "sisnet") { stream_ptr = make_unique (inputName); } - else - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Invalid protocol " << protocol; - } - - if (inputFormat == "RINEX") { parser_ptr = make_unique (); static_cast (parser_ptr.get())->rnxRec.id = id; } - else if (inputFormat == "UBX") { parser_ptr = make_unique (); static_cast (parser_ptr.get())->recId = id; } - else if (inputFormat == "CUSTOM") { parser_ptr = make_unique (); static_cast (parser_ptr.get())->recId = id; } - else if (inputFormat == "RTCM") { parser_ptr = make_unique (); static_cast (parser_ptr.get())->rtcmMountpoint = mountpoint; if (id == "QZSL6") { static_cast (parser_ptr.get())->qzssL6 = true;}} - else if (inputFormat == "SP3") { parser_ptr = make_unique (); } - else if (inputFormat == "SINEX") { parser_ptr = make_unique (); } - else if (inputFormat == "SLR") { parser_ptr = make_unique (); } - else if (inputFormat == "DS2DC") { parser_ptr = make_unique (); } - else - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Invalid inputFormat " << inputFormat; - } - - shared_ptr streamParser_ptr; - - if (dataType == "OBS") streamParser_ptr = make_shared (std::move(stream_ptr), std::move(parser_ptr)); - else if (dataType == "PSEUDO") streamParser_ptr = make_shared (std::move(stream_ptr), std::move(parser_ptr), true); - else streamParser_ptr = make_shared(std::move(stream_ptr), std::move(parser_ptr)); - - if (dataType == "OBS") - { - auto& rec = receiverMap[id]; - - initialiseStation(id, rec); - } - - streamParser_ptr->stream.sourceString = inputName; - - streamParserMultimap.insert({id, std::move(streamParser_ptr)}); - - streamDOAMap[inputName] = false; - } + for (auto& inputName : inputNames) + { + if (streamDOAMap.find(inputName) != streamDOAMap.end()) + { + // this stream was already added, dont re-add + continue; + } + + string mountpoint; + auto lastSlashPos = inputName.find_last_of('/'); + if (lastSlashPos == string::npos) + { + mountpoint = inputName; + } + else + { + mountpoint = inputName.substr(lastSlashPos + 1); + }; + + string id = stationId; + + if (id == "") + { + id = mountpoint.substr(0, 4); + } + + boost::algorithm::to_upper(id); + + auto& recOpts = acsConfig.getRecOpts(id); + + if (recOpts.exclude || recOpts.kill) + { + continue; + } + + string protocol; + string subInputName; + auto protocolPos = inputName.find("://"); + if (protocolPos == string::npos) + { + protocol = "file"; + subInputName = inputName; + } + else + { + protocol = inputName.substr(0, protocolPos); + subInputName = inputName.substr(protocolPos + 3); + } + + std::unique_ptr stream_ptr; + std::unique_ptr parser_ptr; + + if (protocol == "file" || protocol == "serial") + { + if (acsConfig.allow_missing_inputs == false && + checkValidFile(subInputName, dataType) == false) + { + continue; + } + } + + if (protocol == "file") + { + stream_ptr = make_unique(subInputName); + } + else if (protocol == "serial") + { + stream_ptr = make_unique(subInputName); + } + else if (protocol == "http") + { + stream_ptr = make_unique(inputName); + } + else if (protocol == "https") + { + stream_ptr = make_unique(inputName); + } + else if (protocol == "ntrip") + { + stream_ptr = make_unique(inputName); + } + else if (protocol == "sisnet") + { + stream_ptr = make_unique(inputName); + } + else + { + BOOST_LOG_TRIVIAL(warning) << "Invalid protocol " << protocol; + } + + if (inputFormat == "RINEX") + { + parser_ptr = make_unique(); + static_cast(parser_ptr.get())->rnxRec.id = id; + } + else if (inputFormat == "UBX") + { + parser_ptr = make_unique(); + static_cast(parser_ptr.get())->recId = id; + } + else if (inputFormat == "CUSTOM") + { + parser_ptr = make_unique(); + static_cast(parser_ptr.get())->recId = id; + } + else if (inputFormat == "RTCM") + { + parser_ptr = make_unique(); + static_cast(parser_ptr.get())->rtcmMountpoint = mountpoint; + if (id == "QZSL6") + { + static_cast(parser_ptr.get())->qzssL6 = true; + } + } + else if (inputFormat == "SP3") + { + parser_ptr = make_unique(); + } + else if (inputFormat == "SINEX") + { + parser_ptr = make_unique(); + } + else if (inputFormat == "SLR") + { + parser_ptr = make_unique(); + } + else if (inputFormat == "DS2DC") + { + parser_ptr = make_unique(); + } + else + { + BOOST_LOG_TRIVIAL(warning) << "Invalid inputFormat " << inputFormat; + } + + shared_ptr streamParser_ptr; + + if (dataType == "OBS") + streamParser_ptr = make_shared(std::move(stream_ptr), std::move(parser_ptr)); + else if (dataType == "PSEUDO") + streamParser_ptr = + make_shared(std::move(stream_ptr), std::move(parser_ptr), 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; + + streamParserMultimap.insert({id, std::move(streamParser_ptr)}); + + streamDOAMap[inputName] = false; + } } void reloadInputFiles() { - DOCS_REFERENCE(Input_Files__); - - removeInvalidFiles(acsConfig.atx_files); - for (auto& atxfile : acsConfig.atx_files) - { - if (fileChanged(atxfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading ATX file " << atxfile; - - readantexf(atxfile, nav); - } - - removeInvalidFiles(acsConfig.sp3_files); - for (auto& sp3file : acsConfig.sp3_files) - { - if (fileChanged(sp3file) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading SP3 file " << sp3file; - - readSp3ToNav(sp3file, nav, 0); - } - - removeInvalidFiles(acsConfig.obx_files); - for (auto& obxfile : acsConfig.obx_files) - { - if (fileChanged(obxfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading OBX file " << obxfile; - - readOrbex(obxfile, nav); - } - - removeInvalidFiles(acsConfig.nav_files); - for (auto& navfile : acsConfig.nav_files) - { - if (fileChanged(navfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading NAV file " << navfile; - - auto rinexStream = make_unique(make_unique(navfile), make_unique()); - - rinexStream->parse(); - } - - removeInvalidFiles(acsConfig.erp_files); - for (auto& erpfile : acsConfig.erp_files) - { - if (fileChanged(erpfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading ERP file " << erpfile; - - readErp(erpfile, nav.erp); - } - - removeInvalidFiles(acsConfig.clk_files); - for (auto& clkfile : acsConfig.clk_files) - { - if (fileChanged(clkfile) == false) - { - continue; - } - - /* CLK file - RINEX 3 */ - BOOST_LOG_TRIVIAL(info) - << "Loading CLK file " << clkfile; - - auto rinexStream = make_unique(make_unique(clkfile), make_unique()); - - rinexStream->parse(); - } - - removeInvalidFiles(acsConfig.dcb_files); - for (auto& dcbfile : acsConfig.dcb_files) - { - if (fileChanged(dcbfile) == false) - { - continue; - } - - /* DCB file */ - BOOST_LOG_TRIVIAL(info) - << "Loading DCB file " << dcbfile; - - readdcb(dcbfile); - } - - removeInvalidFiles(acsConfig.bsx_files); - for (auto& bsxfile : acsConfig.bsx_files) - { - if (fileChanged(bsxfile) == false) - { - continue; - } - - /* BSX file*/ - BOOST_LOG_TRIVIAL(info) - << "Loading BSX file " << bsxfile; - - readBiasSinex(bsxfile); - } - - removeInvalidFiles(acsConfig.ion_files); - for (auto& ionfile : acsConfig.ion_files) - { - if (fileChanged(ionfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading ION file " << ionfile; - - readTec(ionfile, &nav); - } - - removeInvalidFiles(acsConfig.igrf_files); - for (auto& igrffile : acsConfig.igrf_files) - { - if (fileChanged(igrffile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading IGRF file " << igrffile; - - readIGRF(igrffile); - } - - removeInvalidFiles(acsConfig.snx_files); - for (auto& snxfile : acsConfig.snx_files) - { - if (fileChanged(snxfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading SNX file " << snxfile; - - bool pass = readSinex(snxfile); - if (pass == false) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Unable to load SINEX file " << snxfile; - - continue; - } - } - - //dont remove invalid files for pseudo filters, they may exist only intermittently - // removeInvalidFiles(acsConfig.pseudo_filter_files); - for (auto& pseudo_filter_file : acsConfig.pseudo_filter_files) - { - if (fileChanged(pseudo_filter_file) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading Pseudo filter file " << pseudo_filter_file; - - readPseudosFromFile(pseudo_filter_file); - } - - removeInvalidFiles(acsConfig.orography_files); - for (auto& orographyfile : acsConfig.orography_files) - { - if (fileChanged(orographyfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading ORO from " << orographyfile; - - readorog(orographyfile); - } - - removeInvalidFiles(acsConfig.vmf_files); - for (auto& vmffile : acsConfig.vmf_files) - { - if (fileChanged(vmffile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading VMF file " << vmffile; - - readvmf3(vmffile); - } - - removeInvalidFiles(acsConfig.gpt2grid_files); - for (auto& gpt2gridfile : acsConfig.gpt2grid_files) - { - if (fileChanged(gpt2gridfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading GPT file " << gpt2gridfile; - - readgrid(gpt2gridfile); - } - - removeInvalidFiles(acsConfig.ocean_pole_tide_loading_files); - for (auto& optfile : acsConfig.ocean_pole_tide_loading_files) - { - if (fileChanged(optfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading Ocean Pole Tide file " << optfile; - - readOceanPoleCoeff(optfile); - } - - removeInvalidFiles(acsConfig.sid_files); // satellite ID (sp3c code) data - for (auto& sidfile : acsConfig.sid_files) - { - if (fileChanged(sidfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading Sat ID file " << sidfile; - - readSatId(sidfile); - } - - removeInvalidFiles(acsConfig.crd_files); // SLR observation data - for (auto& crdfile : acsConfig.crd_files) - { - if (fileChanged(crdfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading CRD file " << crdfile; - - readCrd(crdfile); - } - - if (acsConfig.output_slr_obs) - { - slrObsFiles = outputSortedSlrObs(); // CRD files need to be parsed before sorted .slr_obs files are exported - } - - removeInvalidFiles(acsConfig.com_files); // centre-of-mass data - for (auto& comfile : acsConfig.com_files) - { - if (fileChanged(comfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading CoM file " << comfile; - - readCom(comfile); - } - - removeInvalidFiles(acsConfig.egm_files); - for (auto& egmfile : acsConfig.egm_files) - { - if (fileChanged(egmfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading EGM file " << egmfile; - - egm.read(egmfile, acsConfig.propagationOptions.egm_degree); - } - - removeInvalidFiles(acsConfig.ocean_tide_potential_files); - for (auto& tidefile : acsConfig.ocean_tide_potential_files) - { - if (fileChanged(tidefile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading Tide file " << tidefile; - - oceanTide.read(tidefile, acsConfig.propagationOptions.egm_degree); - } - - removeInvalidFiles(acsConfig.atmos_tide_potential_files); - for (auto& tidefile : acsConfig.atmos_tide_potential_files) - { - if (fileChanged(tidefile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading Tide file " << tidefile; - - atmosphericTide.read(tidefile, acsConfig.propagationOptions.egm_degree); - } - - removeInvalidFiles(acsConfig.cmc_files); - for (auto& cmcfile : acsConfig.cmc_files) - { - if (fileChanged(cmcfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) << "Loading CMC file " << cmcfile; - - cmc.read(cmcfile); - } - - removeInvalidFiles(acsConfig.hfeop_files); - for (auto& hfeopfile : acsConfig.hfeop_files) - { - if (fileChanged(hfeopfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) << "Loading HFEOP file " << hfeopfile; - - hfEop.read(hfeopfile); - } - - removeInvalidFiles(acsConfig.atmos_oceean_dealiasing_files); - for (auto& aod1b_file : acsConfig.atmos_oceean_dealiasing_files) - { - if (fileChanged(aod1b_file) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) << "Loading AOD file " << aod1b_file; - - aod.read(aod1b_file, acsConfig.propagationOptions.egm_degree); - } - - removeInvalidFiles(acsConfig.ocean_pole_tide_potential_files); - for (auto& poleocean_file : acsConfig.ocean_pole_tide_potential_files) - { - if (fileChanged(poleocean_file) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) << "Loading Pole Ocean Tide file " << poleocean_file; - - oceanPoleTide.read(poleocean_file, acsConfig.propagationOptions.egm_degree); - } - - removeInvalidFiles(acsConfig.planetary_ephemeris_files); - for (auto& jplfile : acsConfig.planetary_ephemeris_files) - { - if (fileChanged(jplfile) == false) - { - continue; - } - - BOOST_LOG_TRIVIAL(info) - << "Loading planetary ephemeris file " << jplfile; - - nav.jplEph_ptr = (struct jpl_eph_data*) jpl_init_ephemeris(jplfile.c_str(), nullptr, nullptr); // a Pointer to The jpl_eph_data Structure - - if (jpl_init_error_code()) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: planetary ephemeris file had error code " << jpl_init_error_code(); - } - } - - for (auto& [id, slrinputs] : slrObsFiles) { addStationData(id, slrinputs, "SLR", "OBS"); } - for (auto& [id, ubxinputs] : acsConfig.ubx_inputs) { addStationData(id, ubxinputs, "UBX", "OBS"); } - for (auto& [id, custominputs] : acsConfig.custom_inputs) { addStationData(id, custominputs, "CUSTOM", "OBS"); } - for (auto& [id, rnxinputs] : acsConfig.rnx_inputs) { addStationData(id, rnxinputs, "RINEX", "OBS"); } - for (auto& [id, rtcminputs] : acsConfig.obs_rtcm_inputs) { addStationData(id, rtcminputs, "RTCM", "OBS"); } - for (auto& [id, pseudosp3inputs] : acsConfig.pseudo_sp3_inputs) { addStationData(id, pseudosp3inputs, "SP3", "PSEUDO"); } - for (auto& [id, pseudosnxinputs] : acsConfig.pseudo_snx_inputs) { addStationData(id, pseudosnxinputs, "SINEX", "PSEUDO"); } - { addStationData("Nav", acsConfig.nav_rtcm_inputs, "RTCM", "NAV"); } - { addStationData("QZSL6", acsConfig.qzs_rtcm_inputs, "RTCM", "NAV"); } - { addStationData("sisnet",acsConfig.sisnet_inputs, "DS2DC", "NAV"); } + DOCS_REFERENCE(Input_Files__); + + removeInvalidFiles(acsConfig.atx_files); + for (auto& atxfile : acsConfig.atx_files) + { + if (fileChanged(atxfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading ATX file " << atxfile; + + readantexf(atxfile, nav); + } + + removeInvalidFiles(acsConfig.sp3_files); + for (auto& sp3file : acsConfig.sp3_files) + { + if (fileChanged(sp3file) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading SP3 file " << sp3file; + + readSp3ToNav(sp3file, nav, 0); + } + + removeInvalidFiles(acsConfig.ems_files); + for (auto& emsfile : acsConfig.ems_files) + { + if (fileChanged(emsfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading EMS file " << emsfile; + + readEMSdata(emsfile); + } + + removeInvalidFiles(acsConfig.obx_files); + for (auto& obxfile : acsConfig.obx_files) + { + if (fileChanged(obxfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading OBX file " << obxfile; + + readOrbex(obxfile, nav); + } + + removeInvalidFiles(acsConfig.nav_files); + for (auto& navfile : acsConfig.nav_files) + { + if (fileChanged(navfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading NAV file " << navfile; + + auto rinexStream = + make_unique(make_unique(navfile), make_unique()); + + rinexStream->parse(); + } + + removeInvalidFiles(acsConfig.erp_files); + for (auto& erpfile : acsConfig.erp_files) + { + if (fileChanged(erpfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading ERP file " << erpfile; + + readErp(erpfile, nav.erp); + } + + removeInvalidFiles(acsConfig.clk_files); + for (auto& clkfile : acsConfig.clk_files) + { + if (fileChanged(clkfile) == false) + { + continue; + } + + /* CLK file - RINEX 3 */ + BOOST_LOG_TRIVIAL(info) << "Loading CLK file " << clkfile; + + auto rinexStream = + make_unique(make_unique(clkfile), make_unique()); + + rinexStream->parse(); + } + + removeInvalidFiles(acsConfig.dcb_files); + for (auto& dcbfile : acsConfig.dcb_files) + { + if (fileChanged(dcbfile) == false) + { + continue; + } + + /* DCB file */ + BOOST_LOG_TRIVIAL(info) << "Loading DCB file " << dcbfile; + + readdcb(dcbfile); + } + + removeInvalidFiles(acsConfig.bsx_files); + for (auto& bsxfile : acsConfig.bsx_files) + { + if (fileChanged(bsxfile) == false) + { + continue; + } + + /* BSX file*/ + BOOST_LOG_TRIVIAL(info) << "Loading BSX file " << bsxfile; + + readBiasSinex(bsxfile); + } + + removeInvalidFiles(acsConfig.ion_files); + for (auto& ionfile : acsConfig.ion_files) + { + if (fileChanged(ionfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading ION file " << ionfile; + + readTec(ionfile, &nav); + } + + removeInvalidFiles(acsConfig.igrf_files); + for (auto& igrffile : acsConfig.igrf_files) + { + if (fileChanged(igrffile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading IGRF file " << igrffile; + + readIGRF(igrffile); + } + + removeInvalidFiles(acsConfig.snx_files); + for (auto& snxfile : acsConfig.snx_files) + { + if (fileChanged(snxfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading SNX file " << snxfile; + + bool pass = readSinex(snxfile); + if (pass == false) + { + BOOST_LOG_TRIVIAL(error) << "Unable to load SINEX file " << snxfile; + + continue; + } + } + + // dont remove invalid files for pseudo filters, they may exist only intermittently + // removeInvalidFiles(acsConfig.pseudo_filter_files); + for (auto& pseudo_filter_file : acsConfig.pseudo_filter_files) + { + if (fileChanged(pseudo_filter_file) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading Pseudo filter file " << pseudo_filter_file; + + readPseudosFromFile(pseudo_filter_file); + } + + removeInvalidFiles(acsConfig.orography_files); + for (auto& orographyfile : acsConfig.orography_files) + { + if (fileChanged(orographyfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading ORO from " << orographyfile; + + readorog(orographyfile); + } + + removeInvalidFiles(acsConfig.vmf_files); + for (auto& vmffile : acsConfig.vmf_files) + { + if (fileChanged(vmffile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading VMF file " << vmffile; + + readvmf3(vmffile); + } + + removeInvalidFiles(acsConfig.gpt2grid_files); + for (auto& gpt2gridfile : acsConfig.gpt2grid_files) + { + if (fileChanged(gpt2gridfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading GPT file " << gpt2gridfile; + + readgrid(gpt2gridfile); + } + + removeInvalidFiles(acsConfig.ocean_pole_tide_loading_files); + for (auto& optfile : acsConfig.ocean_pole_tide_loading_files) + { + if (fileChanged(optfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading Ocean Pole Tide file " << optfile; + + readOceanPoleCoeff(optfile); + } + + removeInvalidFiles(acsConfig.ocean_tide_loading_blq_files); + for (auto& otlfile : acsConfig.ocean_tide_loading_blq_files) + { + if (fileChanged(otlfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading Ocean Tide Loading BLQ file " << otlfile; + + readBlq(otlfile, E_LoadingType::OCEAN); + } + + removeInvalidFiles(acsConfig.atmos_tide_loading_blq_files); + for (auto& atlfile : acsConfig.atmos_tide_loading_blq_files) + { + if (fileChanged(atlfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading Atmos Tide Loading BLQ file " << atlfile; + + readBlq(atlfile, E_LoadingType::ATMOSPHERIC); + } + + removeInvalidFiles(acsConfig.sid_files); // satellite ID (sp3c code) data + for (auto& sidfile : acsConfig.sid_files) + { + if (fileChanged(sidfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading Sat ID file " << sidfile; + + readSatId(sidfile); + } + + removeInvalidFiles(acsConfig.crd_files); // SLR observation data + for (auto& crdfile : acsConfig.crd_files) + { + if (fileChanged(crdfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading CRD file " << crdfile; + + readCrd(crdfile); + } + + if (acsConfig.output_slr_obs) + { + slrObsFiles = outputSortedSlrObs(); // CRD files need to be parsed before sorted .slr_obs + // files are exported + } + + removeInvalidFiles(acsConfig.com_files); // centre-of-mass data + for (auto& comfile : acsConfig.com_files) + { + if (fileChanged(comfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading CoM file " << comfile; + + readCom(comfile); + } + + removeInvalidFiles(acsConfig.egm_files); + for (auto& egmfile : acsConfig.egm_files) + { + if (fileChanged(egmfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading EGM file " << egmfile; + + egm.read(egmfile, acsConfig.propagationOptions.egm_degree); + } + + removeInvalidFiles(acsConfig.space_weather_files); + for (auto& swfile : acsConfig.space_weather_files) + { + if (fileChanged(swfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading SW file " << swfile; + + spaceWeatherData.read(swfile); + } + + removeInvalidFiles(acsConfig.ocean_tide_potential_files); + for (auto& tidefile : acsConfig.ocean_tide_potential_files) + { + if (fileChanged(tidefile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading Tide file " << tidefile; + + oceanTide.read(tidefile, acsConfig.propagationOptions.egm_degree); + } + + removeInvalidFiles(acsConfig.atmos_tide_potential_files); + for (auto& tidefile : acsConfig.atmos_tide_potential_files) + { + if (fileChanged(tidefile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading Tide file " << tidefile; + + atmosphericTide.read(tidefile, acsConfig.propagationOptions.egm_degree); + } + + removeInvalidFiles(acsConfig.cmc_files); + for (auto& cmcfile : acsConfig.cmc_files) + { + if (fileChanged(cmcfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading CMC file " << cmcfile; + + cmc.read(cmcfile); + } + + removeInvalidFiles(acsConfig.hfeop_files); + for (auto& hfeopfile : acsConfig.hfeop_files) + { + if (fileChanged(hfeopfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading HFEOP file " << hfeopfile; + + hfEop.read(hfeopfile); + } + + removeInvalidFiles(acsConfig.atmos_ocean_dealiasing_files); + for (auto& aod1b_file : acsConfig.atmos_ocean_dealiasing_files) + { + if (fileChanged(aod1b_file) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading AOD file " << aod1b_file; + + aod.read(aod1b_file, acsConfig.propagationOptions.egm_degree); + } + + removeInvalidFiles(acsConfig.ocean_pole_tide_potential_files); + for (auto& poleocean_file : acsConfig.ocean_pole_tide_potential_files) + { + if (fileChanged(poleocean_file) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading Pole Ocean Tide file " << poleocean_file; + + oceanPoleTide.read(poleocean_file, acsConfig.propagationOptions.egm_degree); + } + + removeInvalidFiles(acsConfig.planetary_ephemeris_files); + for (auto& jplfile : acsConfig.planetary_ephemeris_files) + { + if (fileChanged(jplfile) == false) + { + continue; + } + + BOOST_LOG_TRIVIAL(info) << "Loading planetary ephemeris file " << jplfile; + + nav.jplEph_ptr = (struct jpl_eph_data*)jpl_init_ephemeris( + jplfile.c_str(), + nullptr, + nullptr + ); // a Pointer to The jpl_eph_data Structure + + if (jpl_init_error_code()) + { + BOOST_LOG_TRIVIAL(warning) + << "Planetary ephemeris file had error code " << jpl_init_error_code(); + } + } + + for (auto& [id, slrinputs] : slrObsFiles) + { + addReceiverData(id, slrinputs, "SLR", "OBS"); + } + for (auto& [id, ubxinputs] : acsConfig.ubx_inputs) + { + addReceiverData(id, ubxinputs, "UBX", "OBS"); + } + for (auto& [id, custominputs] : acsConfig.custom_inputs) + { + addReceiverData(id, custominputs, "CUSTOM", "OBS"); + } + for (auto& [id, rnxinputs] : acsConfig.rnx_inputs) + { + addReceiverData(id, rnxinputs, "RINEX", "OBS"); + } + for (auto& [id, rtcminputs] : acsConfig.obs_rtcm_inputs) + { + addReceiverData(id, rtcminputs, "RTCM", "OBS"); + } + for (auto& [id, pseudosp3inputs] : acsConfig.pseudo_sp3_inputs) + { + addReceiverData(id, pseudosp3inputs, "SP3", "PSEUDO"); + } + for (auto& [id, pseudosnxinputs] : acsConfig.pseudo_snx_inputs) + { + addReceiverData(id, pseudosnxinputs, "SINEX", "PSEUDO"); + } + { + addReceiverData("Nav", acsConfig.nav_rtcm_inputs, "RTCM", "NAV"); + } + { + addReceiverData("QZSL6", acsConfig.qzs_rtcm_inputs, "RTCM", "NAV"); + } + { + addReceiverData("sisnet", acsConfig.sisnet_inputs, "DS2DC", "NAV"); + } } diff --git a/src/cpp/pea/inputsOutputs.hpp b/src/cpp/pea/inputsOutputs.hpp index 7d4db7850..d85014b72 100644 --- a/src/cpp/pea/inputsOutputs.hpp +++ b/src/cpp/pea/inputsOutputs.hpp @@ -1,16 +1,13 @@ - #pragma once #include - #include #include +#include "common/trace.hpp" using std::string; using std::vector; -#include "trace.hpp" - struct ReceiverMap; struct Network; struct KFState; @@ -18,38 +15,27 @@ struct GTime; void reloadInputFiles(); -bool checkValidFile( - const string& path, - const string& description = ""); +bool checkValidFile(const string& path, const string& description = ""); -bool checkValidFiles( - vector& paths, - const string& description = ""); +bool checkValidFiles(vector& paths, const string& description = ""); -void replaceTimes( - string& str, - boost::posix_time::ptime time); +void replaceTimes(string& str, boost::posix_time::ptime time); -void createDirectories( - boost::posix_time::ptime time); +void createDirectories(boost::posix_time::ptime time); void perEpochPostProcessingAndOutputs( - Trace& pppTrace, - Network& pppNet, - Network& ionNet, - ReceiverMap& receiverMap, - KFState& kfState, - KFState& ionState, - const GTime& time, - bool emptyEpoch); - -void createTracefiles( - ReceiverMap& receiverMap, - Network& pppNet, - Network& ionNet); - -void outputPredictedStates( - Trace& trace, - KFState& kfState); + Trace& pppTrace, + GTime time, + Network& ionNet, + ReceiverMap& receiverMap, + KFState& kfState, + bool emptyEpoch, + bool inRts = false, + bool firstEpoch = false +); + +void createTracefiles(ReceiverMap& receiverMap, Network& pppNet, Network& ionNet); + +void outputPredictedStates(Trace& trace, KFState& kfState); void configureUploadingStreams(); diff --git a/src/cpp/pea/main.cpp b/src/cpp/pea/main.cpp index d306cc21b..7cd4c5f90 100644 --- a/src/cpp/pea/main.cpp +++ b/src/cpp/pea/main.cpp @@ -1,1202 +1,1261 @@ - // #pragma GCC optimize ("O0") +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "architectureDocs.hpp" +#include "common/algebraTrace.hpp" +#include "common/api.hpp" +#include "common/debug.hpp" +#include "common/fileLog.hpp" +#include "common/gTime.hpp" +#include "common/mongoRead.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/ntripBroadcast.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/rinexClkWrite.hpp" +#include "common/rinexNavWrite.hpp" +#include "common/rinexObsWrite.hpp" +#include "common/rtsSmoothing.hpp" +#include "common/sinex.hpp" +#include "common/streamNtrip.hpp" +#include "common/streamObs.hpp" +#include "common/summary.hpp" +#include "common/tcpSocket.hpp" +#include "common/testUtils.hpp" +#include "inertial/posProp.hpp" +#include "iono/ionoModel.hpp" +#include "omp.h" +#include "orbprop/coordinates.hpp" +#include "orbprop/orbitProp.hpp" +#include "pea/inputsOutputs.hpp" +#include "pea/minimumConstraints.hpp" +#include "pea/peaCommitStrings.hpp" +#include "pea/preprocessor.hpp" + +using namespace std::literals::chrono_literals; +using std::make_shared; +using std::make_unique; +using std::string; +using std::chrono::system_clock; +using std::chrono::time_point; +using std::this_thread::sleep_for; /** Retrieve data from streams and process together once per epoch. * - * In order to be capable of real-time and post-processed execution, the pea treats all observation inputs as streams. - * These streams are created with a stream source type, and a parser, with the separation of these strategies allowing for far more reusable code, - * and immediate extension of inputs from simply files, to HTTP, TCP, Serial, Pipes etc. + * In order to be capable of real-time and post-processed execution, the pea treats all observation + * inputs as streams. These streams are created with a stream source type, and a parser, with the + * separation of these strategies allowing for far more reusable code, and immediate extension of + * inputs from simply files, to HTTP, TCP, Serial, Pipes etc. * * The fundamental tick of the pea is the `epoch_interval`, which drives all major processing steps. - * Once an initial epoch has been designated the pea requests data for the next epoch from each stream. - * The streams either respond with the data, or with a flag indicating the data may be available later, or will never be available. + * Once an initial epoch has been designated the pea requests data for the next epoch from each + * stream. The streams either respond with the data, or with a flag indicating the data may be + * available later, or will never be available. * - * Various configuration parameters define the exact flow through the synchronisation code, however once all data is available, or no more is expected, - * or a timeout has occurred, the epoch is considered synchronised and processing is performed. + * Various configuration parameters define the exact flow through the synchronisation code, however + * once all data is available, or no more is expected, or a timeout has occurred, the epoch is + * considered synchronised and processing is performed. * - * Unlike observation streams, which provide data up until the requested epoch time, other streams such as navigation streams parse all data available to them, - * and output any values to global maps, which may be later accessed as required by other components. + * Unlike observation streams, which provide data up until the requested epoch time, other streams + * such as navigation streams parse all data available to them, and output any values to global + * maps, which may be later accessed as required by other components. * - * The synchronisation process parses all streams sequentially, without multithreading, so map collisions are not expected. + * The synchronisation process parses all streams sequentially, without multithreading, so map + * collisions are not expected. */ -Architecture Streams_And_Synchronisation__() -{ - -} - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +Architecture Streams_And_Synchronisation__() {} #ifdef ENABLE_PARALLELISATION - #include "omp.h" #endif -using namespace std::literals::chrono_literals; -using std::this_thread::sleep_for; -using std::chrono::system_clock; -using std::chrono::time_point; -using std::make_unique; -using std::make_shared; -using std::string; - -#include -#include -#include +bool doDocs = false; +Navigation nav = {}; +int epoch = 0; +GTime tsync = GTime::noTime(); +map satIdMap; -#include "interactiveTerminal.hpp" -#include "minimumConstraints.hpp" -#include "peaCommitStrings.hpp" -#include "ntripBroadcast.hpp" -#include "inputsOutputs.hpp" -#include "rinexNavWrite.hpp" -#include "rinexObsWrite.hpp" -#include "rinexClkWrite.hpp" -#include "algebraTrace.hpp" -#include "rtsSmoothing.hpp" -#include "observations.hpp" -#include "preprocessor.hpp" -#include "streamNtrip.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "mongoWrite.hpp" -#include "streamObs.hpp" -#include "tcpSocket.hpp" -#include "mongoRead.hpp" -#include "testUtils.hpp" -#include "orbitProp.hpp" -#include "ionoModel.hpp" -#include "receiver.hpp" -#include "posProp.hpp" -#include "summary.hpp" -#include "fileLog.hpp" -#include "sinex.hpp" -#include "gTime.hpp" -#include "debug.hpp" -#include "api.hpp" - -bool doDocs = false; -Navigation nav = {}; -int epoch = 0; -GTime tsync = GTime::noTime(); -map satIdMap; - -void avoidCollisions( - ReceiverMap& receiverMap) +void avoidCollisions(ReceiverMap& receiverMap) { - for (auto& [id, rec] : receiverMap) - { - auto trace = getTraceFile(rec); - } - - for (auto& [id, rec] : receiverMap) - { - //create sinex estimate maps - theSinex.estimatesMap [id]; - theSinex.solEpochMap [id]; - - auto& recOpts = acsConfig.getRecOpts(id); - - if (recOpts.sat_id.empty() == false) - { - auto& satNav = nav.satNavMap[recOpts.sat_id.c_str()]; - } - } + for (auto& [id, rec] : receiverMap) + { + auto trace = getTraceFile(rec); + } + + for (auto& [id, rec] : receiverMap) + { + // create sinex estimate maps + theSinex.estimatesMap[id]; + theSinex.solEpochMap[id]; + + auto& recOpts = acsConfig.getRecOpts(id); + + if (recOpts.sat_id.empty() == false) + { + auto& satNav = nav.satNavMap[recOpts.sat_id.c_str()]; + } + } } /** Perform operations for each station -* This function occurs in parallel with other stations - ensure that any operations on global maps do not create new entries, as that will destroy the map for other processes. -* Variables within the rec object are ok to use, but be aware that pointers from the within the receiver often point to global variables. -* Prepare global maps by accessing the desired elements before calling this function. -*/ -void mainOncePerEpochPerStation( - Receiver& rec, - Network& net, - bool& emptyEpoch, - KFState& remoteState) + * This function occurs in parallel with other stations - ensure that any operations on global maps + * do not create new entries, as that will destroy the map for other processes. Variables within the + * rec object are ok to use, but be aware that pointers from the within the receiver often point to + * global variables. Prepare global maps by accessing the desired elements before calling this + * function. + */ +void mainOncePerEpochPerStation(Receiver& rec, Network& net, bool& emptyEpoch, KFState& remoteState) { - if (rec.isPseudoRec) - { - return; - } - - auto trace = getTraceFile(rec); - - if (rec.ready == false) - { - trace << "\n" << "Receiver " << rec.id << " has no data for this epoch"; - BOOST_LOG_TRIVIAL(info) << "Receiver " << rec.id << " has no data for this epoch"; - return; - } - - sinexPerEpochPerStation(trace, tsync, rec); - - preprocessor(trace, rec, true); - - //recalculate variances now that elevations are known due to satellite postions calculation above - obsVariances(rec.obsList); - - if (acsConfig.process_spp) - { - spp(trace, rec.obsList, rec.sol, rec.id, &net.kfState, &remoteState); - } - - if ( rec.ready == false - || rec.invalid) - { - return; - } - - auto missingWarnInvalidate = [&]( - string thing, - bool failureBool, - bool requiredConfig) -> bool - { - if (failureBool == false) - { - return false; - } - - if (requiredConfig) - { - trace << "\n" << "Warning: Receiver " << rec.id << " rejected due to lack of " << thing; - BOOST_LOG_TRIVIAL(warning) << "Warning: Receiver " << rec.id << " rejected due to lack of " << thing; - - rec.invalid = true; - - return true; - } - - BOOST_LOG_TRIVIAL(warning) << "Warning: " << thing << " not found for " << rec.id; - - return false; - }; - - bool sppUsed; - selectAprioriSource(trace, rec, tsync, sppUsed, net.kfState, &remoteState); - - if (missingWarnInvalidate("Apriori position1", sppUsed, acsConfig.require_apriori_positions)) return; - if (missingWarnInvalidate("Apriori position2", rec.failureAprioriPos, acsConfig.require_apriori_positions)) return; - if (missingWarnInvalidate("Antenna details", rec.antennaId.empty(), acsConfig.require_antenna_details)) return; - if (missingWarnInvalidate("Site eccentricity", rec.failureEccentricity, acsConfig.require_site_eccentricity)) return; - if (missingWarnInvalidate("Sinex information", rec.failureSinex, acsConfig.require_sinex_data)) return; - - emptyEpoch = false; - - BOOST_LOG_TRIVIAL(trace) - << "Read " << rec.obsList.size() - << " observations for station " << rec.id; - - for (auto& obs : only(rec.obsList)) - { - if (obs.exclude) - { - continue; - } - - if (obs.satStat_ptr == nullptr) - { - continue; - } - - auto& satStat = *obs.satStat_ptr; - auto& recOpts = acsConfig.getRecOpts(rec.id, {obs.Sat.sysName()}); - - if (satStat.el < recOpts.elevation_mask_deg * D2R) - { - obs.excludeElevation = true; - } - } - - //calculate statistics - { - if ((GTime) rec.firstEpoch == GTime::noTime()) { rec.firstEpoch = rec.obsList.front()->time; } - rec.lastEpoch = rec.obsList.front()->time; - rec.epochCount++; - rec.obsCount += rec.obsList.size(); - - for (auto& obs : only(rec.obsList)) - for (auto& [ft, sigList] : obs.sigsLists) - for (auto& sig : sigList) - { - rec.codeCount[sig.code]++; - } - - for (auto& obs : only(rec.obsList)) - { - rec.satCount[obs.Sat]++; - } - } - - if (acsConfig.process_ionosphere) - { - obsIonoData(trace, rec); - } - - auto& recOpts = acsConfig.getRecOpts(rec.id); - - rec.antBoresight = recOpts.antenna_boresight; - rec.antAzimuth = recOpts.antenna_azimuth; - - recAtt(rec, tsync, recOpts.attitudeModel.sources); - - testEclipse(rec.obsList); - - if (acsConfig.output_rinex_obs) - { - writeRinexObs(rec.id, rec.snx, tsync, rec.obsList, acsConfig.rinex_obs_version); - } + if (rec.isPseudoRec) + { + return; + } + + auto trace = getTraceFile(rec); + + if (rec.ready == false) + { + trace << "\n" + << "Receiver " << rec.id << " has no data for this epoch"; + BOOST_LOG_TRIVIAL(info) << "Receiver " << rec.id << " has no data for this epoch"; + return; + } + + sinexPerEpochPerStation(trace, tsync, rec); + + preprocessor(trace, rec, true, &net.kfState, &remoteState); + + if (acsConfig.process_preprocessor == false) + { + // Update observation variances if not calculated in preprocessor + obsVariances(rec.obsList); + } + + if (acsConfig.process_spp) + { + spp(trace, rec, &net.kfState, &remoteState); + } + + if (rec.ready == false || rec.invalid) + { + return; + } + + auto missingWarnInvalidate = [&](string thing, bool failureBool, bool requiredConfig) -> bool + { + if (failureBool == false) + { + return false; + } + + if (requiredConfig) + { + trace << "\n" + << "Warning: Receiver " << rec.id << " rejected due to lack of " << thing; + BOOST_LOG_TRIVIAL(warning) + << "Receiver " << rec.id << " rejected due to lack of " << thing; + + rec.invalid = true; + + return true; + } + + BOOST_LOG_TRIVIAL(warning) << thing << " not found for " << rec.id; + + return false; + }; + + bool sppUsed; + selectAprioriSource(trace, rec, tsync, sppUsed, net.kfState, &remoteState); + + if (missingWarnInvalidate("Apriori position1", sppUsed, acsConfig.require_apriori_positions)) + return; + if (missingWarnInvalidate( + "Apriori position2", + rec.failureAprioriPos, + acsConfig.require_apriori_positions + )) + return; + if (missingWarnInvalidate( + "Antenna details", + rec.antennaId.empty(), + acsConfig.require_antenna_details + )) + return; + if (missingWarnInvalidate( + "Site eccentricity", + rec.failureEccentricity, + acsConfig.require_site_eccentricity + )) + return; + if (missingWarnInvalidate("Sinex information", rec.failureSinex, acsConfig.require_sinex_data)) + return; + + emptyEpoch = false; + + BOOST_LOG_TRIVIAL(trace) << "Read " << rec.obsList.size() << " observations for station " + << rec.id; + + // calculate statistics + { + if ((GTime)rec.firstEpoch == GTime::noTime()) + { + rec.firstEpoch = rec.obsList.front()->time; + } + rec.lastEpoch = rec.obsList.front()->time; + rec.epochCount++; + rec.obsCount += rec.obsList.size(); + + for (auto& obs : only(rec.obsList)) + for (auto& [ft, sigList] : obs.sigsLists) + for (auto& sig : sigList) + { + rec.codeCount[sig.code]++; + } + + for (auto& obs : only(rec.obsList)) + { + rec.satCount[obs.Sat]++; + } + } + + if (acsConfig.process_ionosphere) + { + obsIonoData(trace, rec); + } + + auto& recOpts = acsConfig.getRecOpts(rec.id); + + rec.antBoresight = recOpts.antenna_boresight; + rec.antAzimuth = recOpts.antenna_azimuth; + + recAtt(rec, tsync, recOpts.attitudeModel.sources); + testEclipse(rec.obsList); + + if (acsConfig.output_rinex_obs) + { + writeRinexObs(rec.id, rec, tsync, rec.obsList, acsConfig.rinex_obs_version); + } } void mainOncePerEpochPerSatellite( - Trace& trace, - GTime time, - SatSys Sat, - KFState& kfState, - KFState& remoteKF) + Trace& trace, + GTime time, + SatSys Sat, + KFState& kfState, + KFState& remoteKF +) { - auto& satNav = nav.satNavMap[Sat]; - auto& satOpts = acsConfig.getSatOpts(Sat); - - if (satOpts.exclude) - { - return; - } - - //get svn and block type if possible - if (Sat.svn().empty()) - { - auto it = nav.svnMap[Sat].lower_bound(time); - if (it == nav.svnMap[Sat].end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: SVN not found for " << Sat.id(); - - Sat.setSvn("UNKNOWN"); - } - else - { - Sat.setSvn(it->second); - } - - //reinitialise the options with the updated values - satOpts._initialised = false; - } - - if (Sat.blockType().empty()) - { - auto it = nav.blocktypeMap.find(Sat.svn()); - if (it == nav.blocktypeMap.end()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Block type not found for " << Sat.id() << ", attitude modelling etc may be affected, check sinex file"; - - Sat.setBlockType("UNKNOWN"); - } - else - { - Sat.setBlockType(it->second); - } - - //reinitialise the options with the updated values - satOpts._initialised = false; //todo aaron, this is insufficient since the opts are inherited from the other initialised ones per file which are not reset - } - - satOpts = acsConfig.getSatOpts(Sat); - - satNav.antBoresight = satOpts.antenna_boresight; - satNav.antAzimuth = satOpts.antenna_azimuth; - - selectAprioriSource(Sat, time, kfState, &remoteKF); + auto& satNav = nav.satNavMap[Sat]; + auto& satOpts = acsConfig.getSatOpts(Sat); + + if (satOpts.exclude) + { + return; + } + + // get svn and block type if possible + if (Sat.svn().empty()) + { + auto it = nav.svnMap[Sat].lower_bound(time); + if (it == nav.svnMap[Sat].end()) + { + BOOST_LOG_TRIVIAL(warning) << "SVN not found for " << Sat.id(); + + Sat.setSvn("UNKNOWN"); + } + else + { + Sat.setSvn(it->second); + } + + // reinitialise the options with the updated values + satOpts._initialised = false; + } + + if (Sat.blockType().empty()) + { + auto it = nav.blocktypeMap.find(Sat.svn()); + if (it == nav.blocktypeMap.end()) + { + BOOST_LOG_TRIVIAL(warning) + << "Block type not found for " << Sat.id() + << ", attitude modelling etc may be affected, check sinex file"; + + Sat.setBlockType("UNKNOWN"); + } + else + { + Sat.setBlockType(it->second); + } + + // reinitialise the options with the updated values + satOpts._initialised = + false; // todo aaron, this is insufficient since the opts are inherited from the other + // initialised ones per file which are not reset + } + + satOpts = acsConfig.getSatOpts(Sat); + + satNav.antBoresight = satOpts.antenna_boresight; + satNav.antAzimuth = satOpts.antenna_azimuth; + + selectAprioriSource(Sat, time, kfState, &remoteKF); } -void cullData( - GTime time) +void cullData(GTime time) { - cullOldEphs (time); - cullOldSSRs (time); - cullOldBiases (time); + cullOldEphs(time); + cullOldSSRs(time); + cullOldBiases(time); - mongoCull(time); + mongoCull(time); } -void mainOncePerEpoch( - Network& pppNet, - Network& ionNet, - ReceiverMap& receiverMap, - GTime time) +void mainOncePerEpoch(Network& pppNet, Network& ionNet, ReceiverMap& receiverMap, GTime time) { - avoidCollisions(receiverMap); - - //reload any new or modified files - reloadInputFiles(); - - addDefaultBias(); - - createTracefiles(receiverMap, pppNet, ionNet); - - auto pppTrace = getTraceFile(pppNet); - - //initialise mongo if not already done - mongoooo(); - - //integrate accelerations and early prepare satPos0 with propagated values - predictOrbits (pppTrace, pppNet.kfState, time); - // predictInertials(pppTrace, pppNet.kfState, time); - - InteractiveTerminal::setMode(E_InteractiveMode::Preprocessing); - BOOST_LOG_TRIVIAL(info) << " ------- PREPROCESSING STATIONS --------" << "\n"; - - KFState remoteState; - if (acsConfig.mongoOpts.use_predictions) - { - mongoReadFilter(remoteState, time, acsConfig.mongoOpts.used_predictions); - - BOOST_LOG_TRIVIAL(info) << "Remote state was updated at " << remoteState.time.to_string(); - - remoteState.outputStates(pppTrace, "/REMOTE"); - - mongoStates(pppNet.kfState, - { - .suffix = "/REMOTE", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); + avoidCollisions(receiverMap); - pppNet.kfState.alternate_ptr = &remoteState; + // reload any new or modified files + reloadInputFiles(); - nav.erp.filterValues = getErpFromFilter(pppNet.kfState); - } + addDefaultBias(); - const int DT = 1; - for (int dt : {DT, 0}) - { - ERPValues erpv = getErp(nav.erp, time + dt); + createTracefiles(receiverMap, pppNet, ionNet); - FrameSwapper frameSwapper(time + dt, erpv); + auto pppTrace = getTraceFile(pppNet); - frameSwapper.setCache(dt); - } + // initialise mongo if not already done + mongoooo(); - //try to get svns & block types of all used satellites - for (auto& [Sat, satNav] : nav.satNavMap) - { - if (acsConfig.process_sys[Sat.sys] == false) - continue; + // integrate accelerations and early prepare satPos0 with propagated values + predictOrbits(pppTrace, pppNet.kfState, time); + // predictInertials(pppTrace, pppNet.kfState, time); - mainOncePerEpochPerSatellite(pppTrace, time, Sat, pppNet.kfState, remoteState); - } + BOOST_LOG_TRIVIAL(info) << " ------- PREPROCESSING STATIONS --------" << "\n"; - //do per-station pre processing - bool emptyEpoch = true; -# ifdef ENABLE_PARALLELISATION - Eigen::setNbThreads(1); -# pragma omp parallel for -# endif - for (int i = 0; i < receiverMap.size(); i++) - { - auto rec_ptr_iterator = receiverMap.begin(); - std::advance(rec_ptr_iterator, i); + KFState remoteState; + if (acsConfig.mongoOpts.use_predictions != E_Mongo::NONE) + { + mongoReadFilter(remoteState, time, acsConfig.mongoOpts.used_predictions); - auto& [id, rec] = *rec_ptr_iterator; - mainOncePerEpochPerStation(rec, pppNet, emptyEpoch, remoteState); - } - Eigen::setNbThreads(0); + BOOST_LOG_TRIVIAL(info) << "Remote state was updated at " << remoteState.time.to_string(); + remoteState.outputStates(pppTrace, "/REMOTE"); - if (emptyEpoch) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Epoch " << epoch << " has no observations"; - } + mongoStates( + pppNet.kfState, + {.suffix = "/REMOTE", + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); - if (acsConfig.process_ppp) - { - ppp(pppTrace, receiverMap, pppNet.kfState, remoteState); - } + pppNet.kfState.alternate_ptr = &remoteState; - KFState* kfState_ptr; + nav.erp.filterValues = getErpFromFilter(pppNet.kfState); + } - KFState tempKfState; + const int DT = 1; + for (int dt : {DT, 0}) + { + ERPValues erpv = getErp(nav.erp, time + dt); - if (acsConfig.ambrOpts.fix_and_hold) { kfState_ptr = &pppNet.kfState; } - else { tempKfState = pppNet.kfState; kfState_ptr = &tempKfState; } + FrameSwapper frameSwapper(time + dt, erpv); - auto& kfState = *kfState_ptr; + frameSwapper.setCache(dt); + } - perEpochPostProcessingAndOutputs(pppTrace, pppNet, ionNet, receiverMap, kfState, ionNet.kfState, time, emptyEpoch); + // try to get svns & block types of all used satellites + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (acsConfig.process_sys[Sat.sys] == false) + continue; - if (acsConfig.delete_old_ephemerides) - { - cullData(time); - } + mainOncePerEpochPerSatellite(pppTrace, time, Sat, pppNet.kfState, remoteState); + } - if (acsConfig.check_plumbing) - { - plumber(); - } - - callbacksOncePerEpoch(); + // do per-station pre processing + bool emptyEpoch = true; +#ifdef ENABLE_PARALLELISATION + Eigen::setNbThreads(1); +#pragma omp parallel for +#endif + for (int i = 0; i < receiverMap.size(); i++) + { + auto rec_ptr_iterator = receiverMap.begin(); + std::advance(rec_ptr_iterator, i); + + auto& [id, rec] = *rec_ptr_iterator; + mainOncePerEpochPerStation(rec, pppNet, emptyEpoch, remoteState); + } + Eigen::setNbThreads(0); + + if (emptyEpoch) + { + BOOST_LOG_TRIVIAL(warning) << "Epoch " << epoch << " has no observations"; + } + + if (acsConfig.process_ppp) + { + ppp(pppTrace, receiverMap, pppNet.kfState, remoteState); + + for (auto& [Sat, satNav] : nav.satNavMap) + { + // prevent the memoised orbital positions being used in per epoch post processing and + // outputs + satNav.satPos0.posTime = GTime::noTime(); + } + } + + perEpochPostProcessingAndOutputs( + pppTrace, + time, + ionNet, + receiverMap, + pppNet.kfState, + emptyEpoch + ); + + if (acsConfig.delete_old_ephemerides) + { + cullData(time); + } + + if (acsConfig.check_plumbing) + { + plumber(); + } + + callbacksOncePerEpoch(); } /** Perform any post-final epoch calculations and outputs, then begin reverse smoothing and tidy up */ -void mainPostProcessing( - Network& pppNet, - Network& ionNet, - ReceiverMap& receiverMap) +void mainPostProcessing(Network& pppNet, Network& ionNet, ReceiverMap& receiverMap) { - BOOST_LOG_TRIVIAL(info) - << "Post processing... "; - - auto pppTrace = getTraceFile(pppNet); - - if ( acsConfig.process_ppp - && acsConfig.ambrOpts.mode != +E_ARmode::OFF - && acsConfig.ambrOpts.once_per_epoch == false - && acsConfig.ambrOpts.fix_and_hold) - { - BOOST_LOG_TRIVIAL(info) - << "\n" - << "---------------PERFORMING AMBIGUITY RESOLUTION ON NETWORK WITH FIX AND HOLD ------------- " << "\n"; - - fixAndHoldAmbiguities(pppTrace, pppNet.kfState); - - mongoStates(pppNet.kfState, - { - .suffix = "/AR", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - if ( acsConfig.process_ppp - && acsConfig.process_minimum_constraints - && acsConfig.minconOpts.once_per_epoch == false) - { - InteractiveTerminal::setMode(E_InteractiveMode::MinimumConstraints); - BOOST_LOG_TRIVIAL(info) << " ------- PERFORMING MIN-CONSTRAINTS --------" << "\n"; - - for (auto& [id, rec] : receiverMap) - { - rec.minconApriori = rec.aprioriPos; - } - - { - ERPValues erpv = getErp(nav.erp, pppNet.kfState.time); - - FrameSwapper frameSwapper(pppNet.kfState.time, erpv); - - for (auto& [Sat, satNav] : nav.satNavMap) - { - SatPos satPos; - - satPos.Sat = Sat; - satPos.satNav_ptr = &satNav; - - bool pass = satpos(nullStream, pppNet.kfState.time, pppNet.kfState.time, satPos, {E_Source::PRECISE, E_Source::BROADCAST}, E_OffsetType::COM, nav); - if (pass == false) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No sat pos found for " << satPos.Sat.id() << "."; - continue; - } - - satNav.aprioriPos = frameSwapper(satPos.rSatCom); - } - } - - MinconStatistics minconStatistics; - - - InteractiveTerminal minconTrace("MinimumConstraints", pppTrace); - - mincon(minconTrace, pppNet.kfState, &minconStatistics); - - pppNet.kfState.outputStates(minconTrace, "/CONSTRAINED"); - - mongoStates(pppNet.kfState, - { - .suffix = "/CONSTRAINED", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - - outputMinconStatistics(minconTrace, minconStatistics); - } - - if (acsConfig.output_sinex) - { - sinexPostProcessing(tsync, receiverMap, pppNet.kfState); - } - - outputPredictedStates(pppTrace, pppNet.kfState); - - if (acsConfig.process_rts) - { - while (spitQueueRunning) - { - sleep_for(std::chrono::milliseconds(acsConfig.sleep_milliseconds)); - } - - for (auto& [Sat, satNav] : nav.satNavMap) - { - //prevent the memoised orbital positions being used in rts - satNav.satPos0.posTime = GTime::noTime(); - } - - if (acsConfig.process_ppp) - { - rtsSmoothing(pppNet.kfState, receiverMap, true); - } - - if (acsConfig.process_ionosphere) - { - rtsSmoothing(ionNet.kfState, receiverMap, true); - } - } - - outputSummaries(pppTrace, receiverMap); - - outputStatistics(pppTrace, pppNet.kfState.statisticsMapSum, pppNet.kfState.statisticsMapSum); + BOOST_LOG_TRIVIAL(info) << "Post processing... "; + + auto pppTrace = getTraceFile(pppNet); + + if (acsConfig.process_ppp && acsConfig.ambrOpts.mode != E_ARmode::OFF && + acsConfig.ambrOpts.once_per_epoch == false && acsConfig.ambrOpts.fix_and_hold) + { + BOOST_LOG_TRIVIAL(info) << "\n" + << "---------------PERFORMING AMBIGUITY RESOLUTION ON NETWORK WITH " + "FIX AND HOLD ------------- " + << "\n"; + + fixAndHoldAmbiguities(pppTrace, pppNet.kfState); + + mongoStates( + pppNet.kfState, + {.suffix = "/AR", + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + } + + if (acsConfig.process_ppp && acsConfig.process_minimum_constraints && + acsConfig.minconOpts.once_per_epoch == false) + { + BOOST_LOG_TRIVIAL(info) << " ------- PERFORMING MIN-CONSTRAINTS --------" << "\n"; + + for (auto& [id, rec] : receiverMap) + { + rec.minconApriori = rec.aprioriPos; + } + + { + ERPValues erpv = getErp(nav.erp, pppNet.kfState.time); + + FrameSwapper frameSwapper(pppNet.kfState.time, erpv); + + for (auto& [Sat, satNav] : nav.satNavMap) + { + SatPos satPos; + + satPos.Sat = Sat; + satPos.satNav_ptr = &satNav; + + bool pass = satpos( + nullStream, + pppNet.kfState.time, + pppNet.kfState.time, + satPos, + {E_Source::PRECISE, E_Source::BROADCAST}, + E_OffsetType::COM, + nav + ); + if (pass == false) + { + BOOST_LOG_TRIVIAL(warning) << "No sat pos found for " << satPos.Sat.id() << "."; + continue; + } + + satNav.aprioriPos = frameSwapper(satPos.rSatCom); + } + } + + MinconStatistics minconStatistics; + + mincon(pppTrace, pppNet.kfState, &minconStatistics); + + pppNet.kfState.outputStates(pppTrace, "/CONSTRAINED"); + + mongoStates( + pppNet.kfState, + {.suffix = "/CONSTRAINED", + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + + outputMinconStatistics(pppTrace, minconStatistics); + } + + if (acsConfig.output_sinex) + { + sinexPostProcessing(tsync, receiverMap, pppNet.kfState); + } + + outputPredictedStates(pppTrace, pppNet.kfState); + + if (acsConfig.process_rts) + { + while (spitQueueRunning) + { + sleep_for(std::chrono::milliseconds(acsConfig.sleep_milliseconds)); + } + + for (auto& [Sat, satNav] : nav.satNavMap) + { + // prevent the memoised orbital positions being used in rts + satNav.satPos0.posTime = GTime::noTime(); + } + + if (acsConfig.process_ppp) + { + rtsSmoothing(pppNet.kfState, receiverMap, true); + } + + if (acsConfig.process_ionosphere) + { + rtsSmoothing(ionNet.kfState, receiverMap, true); + } + } + + outputSummaries(pppTrace, receiverMap); + + outputStatistics(pppTrace, pppNet.kfState.statisticsMapSum, pppNet.kfState.statisticsMapSum); } -void tryExitGracefully( - int signum) +void tryExitGracefully(int signum) { - static bool closeRequest = false; + static bool closeRequest = false; - if (closeRequest) - { - //second time this is called, dont worry too much about gracefullness - interactiveTerminaldestructor.~InteractiveTerminalDestructor(); + if (closeRequest) + { + // second time this is called, dont worry too much about gracefullness + abort(); + } - abort(); - } + BOOST_LOG_TRIVIAL(info) << "SIGINT detected - tidying up and exiting..."; - BOOST_LOG_TRIVIAL(info) << "SIGINT detected - tidying up and exiting..."; + closeRequest = true; - closeRequest = true; - - acsConfig.max_epochs = 1; + acsConfig.max_epochs = 1; } - -int main( - int argc, - char** argv) +int main(int argc, char** argv) { - traceLevel = 5; - - // Register the sink in the logging core - boost::log::core::get()->add_sink(boost::make_shared>()); - boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::info); - acsSeverity = boost::log::trivial::info; - - BOOST_LOG_TRIVIAL(info) - << "PEA starting... (" << ginanBranchName() << " " << ginanCommitVersion() << " from " << ginanCommitDate() << ")" << "\n"; - - GTime peaStartTime = timeGet(); - auto peaStartTimeChrono = system_clock::now(); - - exitOnErrors(); - - // Define the function to be called when ctrl-c (SIGINT) is sent to process - signal(SIGINT, tryExitGracefully); - - bool pass = configure(argc, argv); - if (pass == false) - { - BOOST_LOG_TRIVIAL(error) << "Error: Incorrect configuration"; - BOOST_LOG_TRIVIAL(info) << "PEA finished"; - TcpSocket::ioService.stop(); - return EXIT_FAILURE; - } - - if (acsConfig.output_log) - { - addFileLog(); - } - - BOOST_LOG_TRIVIAL(info) << "Compilation details:"; - BOOST_LOG_TRIVIAL(info) << "===================="; - BOOST_LOG_TRIVIAL(info) << "Ginan branch: " << ginanBranchName(); - BOOST_LOG_TRIVIAL(info) << "Ginan version: " << ginanCommitVersion(); - BOOST_LOG_TRIVIAL(info) << "Commit date: " << ginanCommitDate(); - BOOST_LOG_TRIVIAL(info) << "Operating system: " << ginanOsName(); - BOOST_LOG_TRIVIAL(info) << "Compiler version: " << ginanCompilerVersion(); - BOOST_LOG_TRIVIAL(info) << "Boost version: " << ginanBoostVersion(); - BOOST_LOG_TRIVIAL(info) << "Eigen version: " << ginanEigenVersion(); - BOOST_LOG_TRIVIAL(info) << "Mongocxx version: " << ginanMongoVersion(); - BOOST_LOG_TRIVIAL(info) << "\n"; - - - - BOOST_LOG_TRIVIAL(info) << "Runtime details:"; - BOOST_LOG_TRIVIAL(info) << "================"; - BOOST_LOG_TRIVIAL(info) << "Logging at trace level" << traceLevel; - BOOST_LOG_TRIVIAL(info) << "Threading with max " << Eigen::nbThreads() << " eigen threads"; + traceLevel = 5; + + // Register the sink in the logging core + boost::log::core::get()->add_sink(boost::make_shared>()); + boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::info); + acsSeverity = boost::log::trivial::info; + + BOOST_LOG_TRIVIAL(info) << "PEA starting... (" << ginanBranchName() << " " + << ginanCommitVersion() << " from " << ginanCommitDate() << ")" << "\n"; + + GTime peaStartTime = timeGet(); + auto peaStartTimeChrono = system_clock::now(); + + exitOnErrors(); + + // Define the function to be called when ctrl-c (SIGINT) is sent to process + signal(SIGINT, tryExitGracefully); + + bool pass = configure(argc, argv); + if (pass == false) + { + BOOST_LOG_TRIVIAL(error) << "Incorrect configuration"; + BOOST_LOG_TRIVIAL(info) << "PEA finished"; + TcpSocket::ioContext.stop(); + return EXIT_FAILURE; + } + + if (acsConfig.output_log) + { + addFileLog(acsConfig.log_json); + } + + BOOST_LOG_TRIVIAL(info) << "Compilation details:"; + BOOST_LOG_TRIVIAL(info) << "===================="; + BOOST_LOG_TRIVIAL(info) << "Ginan branch: " << ginanBranchName(); + BOOST_LOG_TRIVIAL(info) << "Ginan version: " << ginanCommitVersion(); + BOOST_LOG_TRIVIAL(info) << "Commit date: " << ginanCommitDate(); + BOOST_LOG_TRIVIAL(info) << "Operating system: " << ginanOsName(); + BOOST_LOG_TRIVIAL(info) << "Compiler version: " << ginanCompilerVersion(); + BOOST_LOG_TRIVIAL(info) << "Boost version: " << ginanBoostVersion(); + BOOST_LOG_TRIVIAL(info) << "Eigen version: " << ginanEigenVersion(); + BOOST_LOG_TRIVIAL(info) << "Mongocxx version: " << ginanMongoVersion(); + BOOST_LOG_TRIVIAL(info) << "\n"; + + BOOST_LOG_TRIVIAL(info) << "Runtime details:"; + BOOST_LOG_TRIVIAL(info) << "================"; + BOOST_LOG_TRIVIAL(info) << "Logging at trace level" << traceLevel; + BOOST_LOG_TRIVIAL(info) << "Threading with max " << Eigen::nbThreads() << " eigen threads"; #ifdef ENABLE_PARALLELISATION - BOOST_LOG_TRIVIAL(info) << "Threading with max " << omp_get_max_threads() << " omp threads"; + BOOST_LOG_TRIVIAL(info) << "Threading with max " << omp_get_max_threads() << " omp threads"; #endif - BOOST_LOG_TRIVIAL(info) << "\n"; - BOOST_LOG_TRIVIAL(info) << "\n"; - - //prepare the satNavMap so that it at least has entries for everything - for (auto [sys, max] : { tuple{E_Sys::GPS, NSATGPS}, - tuple{E_Sys::GLO, NSATGLO}, - tuple{E_Sys::GAL, NSATGAL}, - tuple{E_Sys::QZS, NSATQZS}, - tuple{E_Sys::LEO, NSATLEO}, - tuple{E_Sys::BDS, NSATBDS}, - tuple{E_Sys::SBS, NSATSBS}}) - for (int prn = 1; prn <= max; prn++) - { - SatSys Sat(sys, prn); - - if (acsConfig.process_sys[Sat.sys] == false) - continue; - - auto& satOpts = acsConfig.getSatOpts(Sat); - - if (satOpts.exclude) - continue; - - nav.satNavMap[Sat].id = Sat.id(); - } - - Network pppNet; - { - pppNet.kfState.FilterOptions::operator=(acsConfig.pppOpts); - pppNet.kfState.id = "Net"; - pppNet.kfState.output_residuals = acsConfig.output_residuals; - pppNet.kfState.outputMongoMeasurements = acsConfig.mongoOpts.output_measurements; - - pppNet.kfState.measRejectCallbacks .push_back(deweightMeas); - pppNet.kfState.measRejectCallbacks .push_back(incrementPhaseSignalError); - pppNet.kfState.measRejectCallbacks .push_back(incrementReceiverError); - pppNet.kfState.measRejectCallbacks .push_back(pseudoMeasTest); - - pppNet.kfState.stateRejectCallbacks .push_back(orbitGlitchReaction); //this goes before reject by state - pppNet.kfState.stateRejectCallbacks .push_back(rejectByState); - } - - Network ionNet; - if (acsConfig.process_ionosphere) - { - ionNet.kfState.FilterOptions::operator=(acsConfig.ionModelOpts); - ionNet.kfState.id = "ION"; - ionNet.kfState.output_residuals = acsConfig.output_residuals; - ionNet.kfState.outputMongoMeasurements = acsConfig.mongoOpts.output_measurements; - ionNet.kfState.rts_basename = "IONEX_RTS"; - - ionNet.kfState.measRejectCallbacks .push_back(deweightMeas); - - ionNet.kfState.stateRejectCallbacks .push_back(rejectByState); - } - - //initialise mongo - mongoooo(); - - if (acsConfig.rts_only) - { - pppNet.kfState.rts_basename = acsConfig.pppOpts.rts_filename; - - rtsSmoothing(pppNet.kfState, receiverMap); - - exit(0); - } - - initialiseBias(); - - boost::posix_time::ptime logptime = currentLogptime(); - createDirectories(logptime); - - reloadInputFiles(); - - 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); - - tsync = startTime; - } - - createTracefiles(receiverMap, pppNet, ionNet); - - for (auto yaml : acsConfig.yamls) - { - YAML::Emitter emitter; - emitter << YAML::DoubleQuoted << YAML::Flow << YAML::BeginSeq << yaml; - - string config(emitter.c_str() + 1); - - mongoOutputConfig(config); - } - - configureUploadingStreams(); - - configAtmosRegions(std::cout, receiverMap); - - if (acsConfig.mincon_only) - { - minconOnly(std::cout, receiverMap); - } - - doDebugs(); - - BOOST_LOG_TRIVIAL(info) - << "\n"; - BOOST_LOG_TRIVIAL(info) - << "Starting to process epochs..."; - - //============================================================================ - // MAIN PROCESSING LOOP // - //============================================================================ - - GTime lastEpochStartTime; - GTime lastEpochStopTime; + BOOST_LOG_TRIVIAL(info) << "\n"; + BOOST_LOG_TRIVIAL(info) << "\n"; + + // prepare the satNavMap so that it at least has entries for everything + for (auto [sys, max] : + {tuple{E_Sys::GPS, NSATGPS}, + tuple{E_Sys::GLO, NSATGLO}, + tuple{E_Sys::GAL, NSATGAL}, + tuple{E_Sys::QZS, NSATQZS}, + tuple{E_Sys::LEO, NSATLEO}, + tuple{E_Sys::BDS, NSATBDS}, + tuple{E_Sys::SBS, NSATSBS}}) + for (int prn = 1; prn <= max; prn++) + { + SatSys Sat(sys, prn); + + if (acsConfig.process_sys[Sat.sys] == false) + continue; + + auto& satOpts = acsConfig.getSatOpts(Sat); + + if (satOpts.exclude) + continue; + + nav.satNavMap[Sat].id = Sat.id(); + } + + nav.leaps = acsConfig.leap_seconds; + + Network pppNet; + { + pppNet.kfState.FilterOptions::operator=(acsConfig.pppOpts); + pppNet.kfState.id = "Net"; + pppNet.kfState.output_residuals = acsConfig.output_residuals; + pppNet.kfState.outputMongoMeasurements = + (acsConfig.mongoOpts.output_measurements != E_Mongo::NONE); - auto atLastEpoch = [&](bool processed = false) -> bool - { - // Check number of epochs - if ( acsConfig.max_epochs > 0 - && epoch >= acsConfig.max_epochs) - { - BOOST_LOG_TRIVIAL(info) - << "\n" - << "Exiting at epoch " << epoch - << " as epoch count " << acsConfig.max_epochs - << " has been reached"; - - return true; - } + pppNet.kfState.measRejectCallbacks.push_back(incrementPhaseSignalError); + pppNet.kfState.measRejectCallbacks.push_back(incrementSatelliteErrors); + pppNet.kfState.measRejectCallbacks.push_back(incrementReceiverErrors); + pppNet.kfState.measRejectCallbacks.push_back(pseudoMeasTest); + pppNet.kfState.measRejectCallbacks.push_back(deweightMeas); - if (tsync == GTime::noTime()) - { - return false; - } + pppNet.kfState.stateRejectCallbacks.push_back(incrementStateErrors); + pppNet.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); + pppNet.kfState.stateRejectCallbacks.push_back(rejectAllMeasByState); + } - 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); + Network ionNet; + if (acsConfig.process_ionosphere) + { + ionNet.kfState.FilterOptions::operator=(acsConfig.ionModelOpts); + ionNet.kfState.id = "ION"; + ionNet.kfState.output_residuals = acsConfig.output_residuals; + ionNet.kfState.outputMongoMeasurements = + (acsConfig.mongoOpts.output_measurements != E_Mongo::NONE); + ionNet.kfState.rts_basename = "IONEX_RTS"; - GWeek week = tsync; - GTow tow = tsync; + ionNet.kfState.measRejectCallbacks.push_back(deweightMeas); + + pppNet.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); + ionNet.kfState.stateRejectCallbacks.push_back(rejectAllMeasByState); + } - if (processed) - { - BOOST_LOG_TRIVIAL(info) - << "Processed epoch #" << epoch - << " - " << "GPS time: " << week << " " << std::setw(6) << tow << " - " << boostTime - << " (took " << (lastEpochStopTime - lastEpochStartTime) << ")"; - } + // initialise mongo + mongoooo(); + + if (acsConfig.rts_only) + { + replaceString(acsConfig.pppOpts.rts_filename, "", pppNet.id); + + pppNet.kfState.rts_basename = acsConfig.pppOpts.rts_filename; + + rtsSmoothing(pppNet.kfState, receiverMap); + + exit(0); + } + + initialiseBias(); + + boost::posix_time::ptime logptime = currentLogptime(); + createDirectories(logptime); + + reloadInputFiles(); + + 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; + tsync = startGTime.floorTime(acsConfig.epoch_interval); + + if (tsync != startGTime) + { + BOOST_LOG_TRIVIAL(warning) + << "Start epoch " << startGTime << " is not aligned to the epoch interval " + << acsConfig.epoch_interval << ", rounding down to " << tsync; + } + + acsConfig.start_epoch = boost::posix_time::from_time_t((time_t)((PTime)tsync).bigTime); + } - // Check end epoch - if ( acsConfig.end_epoch.is_not_a_date_time() == false - && boostTime >= acsConfig.end_epoch) - { - BOOST_LOG_TRIVIAL(info) - << "Exiting at epoch " << epoch << " (" << boostTime - << ") as end epoch " << acsConfig.end_epoch - << " has been reached"; - - return true; - } - - return false; - }; - - // Read the observations for each station and do stuff - - bool nextEpoch = true; - bool complete = false; // When all input files are empty the processing is deemed complete - run until then, or until something else breaks the loop - int loopEpochs = 0; // A count of how many loops of epoch_interval this loop used up (usually one, but may be more if skipping epochs) - 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 - while (complete == false) - { - if (nextEpoch) - { - nextEpoch = false; - epoch++; - - BOOST_LOG_TRIVIAL(info) << "\n" - << "Starting epoch #" << epoch; - - nominalLoopStartTime += std::chrono::milliseconds((int)(acsConfig.wait_next_epoch * 1000)); - - if (tsync != GTime::noTime()) - { - //dont obliterate the freshly configured tsync before the first epoch - if (epoch != 1) - { - tsync.bigTime += acsConfig.epoch_interval; - } - - if (fabs(tsync.bigTime - round(tsync.bigTime)) < acsConfig.epoch_tolerance) - { - tsync.bigTime = round(tsync.bigTime); - } - } - - InteractiveTerminal::clearModes( - (string)" Processing epoch " + std::to_string(epoch) + " " + tsync.to_string(), - (string)" Last Epoch took " + std::to_string((lastEpochStopTime - lastEpochStartTime).to_double()) + "s"); - InteractiveTerminal::setMode(E_InteractiveMode::Syncing); - - for (auto& [id, rec] : receiverMap) - { - rec.ready = false; - - auto trace = getTraceFile(rec); - - trace << "\n" << "------=============== Epoch " << epoch << " =============-----------" << "\n"; - trace << "\n" << "------=============== Time " << tsync << " =============-----------" << "\n"; - } - - { - auto pppTrace = getTraceFile(pppNet); - - pppTrace << "\n" << "------=============== Epoch " << epoch << " =============-----------" << "\n"; - pppTrace << "\n" << "------=============== Time " << tsync << " =============-----------" << "\n"; - } - } - - // Calculate the time at which we will stop waiting for data to come in for this epoch - auto breakTime = nominalLoopStartTime - + std::chrono::milliseconds((int)(acsConfig.max_rec_latency * 1000)); - - if (loopEpochs) - { - BOOST_LOG_TRIVIAL(info) << "\n" - << "Starting epoch #" << epoch; - } - - if (system_clock::now() > breakTime) - { - BOOST_LOG_TRIVIAL(warning) << "\n" - << "Warning: Excessive time elapsed, skipping epoch " << epoch - << ". Configuration 'wait_next_epoch' is " << acsConfig.wait_next_epoch; - - nextEpoch = true; - - if (atLastEpoch()) - { - break; - } - - continue; - } - - //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; - while ( repeat - &&system_clock::now() < breakTime) - { - repeat = false; - - //load any changes from the config - bool newConfig = acsConfig.parse(); - - //make any changes to streams. - if (newConfig) - { - configureUploadingStreams(); - } - - //remove any dead streams - for (auto iter = streamParserMultimap.begin(); iter != streamParserMultimap.end(); ) - { - auto& [id, streamParser_ptr] = *iter; - auto& stream = streamParser_ptr->stream; - - auto& recOpts = acsConfig.getRecOpts(id); - - if (recOpts.kill) - { - BOOST_LOG_TRIVIAL(info) - << "Removing " << stream.sourceString << " due to kill config" << "\n"; - - for (auto& [key, index] : pppNet.kfState.kfIndexMap) - { - if (key.str == id) - { - pppNet.kfState.removeState(key); - - auto nodeHandler = pppNet.kfState.kfIndexMap.extract(key); - nodeHandler.key().rec_ptr = nullptr; - pppNet.kfState.kfIndexMap.insert(std::move(nodeHandler)); - } - } - - receiverMap.erase(id); - - iter = streamParserMultimap.erase(iter); - - continue; - } - - try - { - auto& obsStream = dynamic_cast(*streamParser_ptr); - - if (obsStream.hasObs()) - { - iter++; - continue; - } - } - catch(...){} - - if (stream.isDead()) - { - BOOST_LOG_TRIVIAL(info) - << "No more data available on " << stream.sourceString << "\n"; - - //record as dead and erase - streamDOAMap[stream.sourceString] = true; - - receiverMap[id].obsList.clear(); - - iter = streamParserMultimap.erase(iter); - - continue; - } - - iter++; - } - - if (streamParserMultimap.empty()) - { - static bool once = true; - if (once) - { - once = false; - - BOOST_LOG_TRIVIAL(info) - << "\n"; - BOOST_LOG_TRIVIAL(info) - << "Inputs finished at epoch #" << epoch; - } - - if (acsConfig.require_obs) - complete = true; - - break; - } - - //parse all non-observation streams - for (auto& [id, streamParser_ptr] : streamParserMultimap) - try - { - auto& obsStream = dynamic_cast(*streamParser_ptr); - } - catch (std::bad_cast& e) - { - streamParser_ptr->parse(); - } - - - - for (auto& [id, streamParser_ptr] : streamParserMultimap) - { - ObsStream* obsStream_ptr; - - try - { - obsStream_ptr = &dynamic_cast(*streamParser_ptr); - } - catch (std::bad_cast& e) - { - continue; - } - - auto& obsStream = *obsStream_ptr; - - auto& recOpts = acsConfig.getRecOpts(id); - - if (recOpts.exclude) - { - continue; - } - - auto& rec = receiverMap[id]; - - auto trace = getTraceFile(rec); - - if (obsStream.isPseudoRec) - { - rec.isPseudoRec = true; - } - - //try to get some data (again) - if (rec.ready) - { - continue; - } - - 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); - - switch (obsStream.obsWaitCode) - { - case E_ObsWaitCode::EARLY_DATA: preprocessor(trace, rec); break; - case E_ObsWaitCode::OK: moreData = false; preprocessor(trace, rec); break; - case E_ObsWaitCode::NO_DATA_WAIT: moreData = false; break; - case E_ObsWaitCode::NO_DATA_EVER: moreData = false; break; - } - } - - if (rec.obsList.empty()) - { - //failed to get observations - if (obsStream.obsWaitCode == +E_ObsWaitCode::NO_DATA_WAIT) - { - // try again later - repeat = true; - sleep_for(std::chrono::milliseconds(acsConfig.sleep_milliseconds)); - } - - continue; - } - - if (tsync == GTime::noTime()) - { - tsync = rec.obsList.front()->time.floorTime(acsConfig.epoch_interval); - - acsConfig.start_epoch = boost::posix_time::from_time_t((time_t)((PTime)tsync).bigTime); - - if (tsync + acsConfig.epoch_tolerance < rec.obsList.front()->time) - { - repeat = true; - continue; - } - } - - dataAvailableMap[rec.id] = true; - rec.ready = true; - - auto now = system_clock::now(); - - if (now >= nominalLoopStartTime) - { - auto nominalLatency = now - nominalLoopStartTime; - - BOOST_LOG_TRIVIAL(debug) - << 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; - - BOOST_LOG_TRIVIAL(debug) - << 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; } - } - } - } - - if (complete) - { - break; - } - - if (tsync == GTime::noTime()) - { - if (acsConfig.require_obs) - continue; - - tsync = timeGet(); - } - - BOOST_LOG_TRIVIAL(info) - << "Synced " << dataAvailableMap.size() << " receivers..."; - - lastEpochStartTime = timeGet(); - if ( acsConfig.require_obs == false - ||dataAvailableMap.empty() == false) - { - mainOncePerEpoch(pppNet, ionNet, receiverMap, tsync); - } - lastEpochStopTime = timeGet(); - - - if (atLastEpoch(true)) - { - break; - } - - nextEpoch = true; - } - - // Disconnect the downloading clients and stop the io_service for clean shutdown. - for (auto& [id, ntripStream] : only(streamParserMultimap)) - { - ntripStream.disconnect(); - } - - ntripBroadcaster.stopBroadcast(); - TcpSocket::ioService.stop(); - - GTime peaInterTime = timeGet(); - BOOST_LOG_TRIVIAL(info) - << "\n" - << "PEA started processing at : " << peaStartTime << "\n" - << "and finished processing at : " << peaInterTime << "\n" - << "Total processing duration : " << (peaInterTime - peaStartTime) << "\n" << "\n"; - - BOOST_LOG_TRIVIAL(info) - << "\n" - << "Finalising streams and post processing..."; - - mainPostProcessing(pppNet, ionNet, receiverMap); - - GTime peaStopTime = timeGet(); - BOOST_LOG_TRIVIAL(info) - << "\n" - << "PEA started processing at : " << peaStartTime << "\n" - << "and finished processing at : " << peaStopTime << "\n" - << "Total processing duration : " << (peaStopTime - peaStartTime) << "\n" << "\n"; - - InteractiveTerminal::clearModes( - (string)" Processing complete at epoch " + std::to_string(epoch) + " " + tsync.to_string(), - (string)" Processing took " + std::to_string((peaStopTime - peaStartTime).to_double()) + "s"); - InteractiveTerminal::setMode(E_InteractiveMode::Complete); - - BOOST_LOG_TRIVIAL(info) << "PEA finished"; - - while (InteractiveTerminal::enabled) - { - sleep_for(std::chrono::seconds(10)); - } - - return EXIT_SUCCESS; + createTracefiles(receiverMap, pppNet, ionNet); + + for (auto yaml : acsConfig.yamls) + { + YAML::Emitter emitter; + emitter << YAML::DoubleQuoted << YAML::Flow << YAML::BeginSeq << yaml; + + string config(emitter.c_str() + 1); + + mongoOutputConfig(config); + } + + configureUploadingStreams(); + + configAtmosRegions(std::cout, receiverMap); + + if (acsConfig.mincon_only) + { + minconOnly(std::cout, receiverMap); + } + + doDebugs(); + + BOOST_LOG_TRIVIAL(info) << "\n"; + BOOST_LOG_TRIVIAL(info) << "Starting to process epochs..."; + + //============================================================================ + // MAIN PROCESSING LOOP // + //============================================================================ + + GTime lastEpochStartTime; + GTime lastEpochStopTime; + + auto atLastEpoch = [&](bool processed = false) -> bool + { + // Check number of epochs + if (acsConfig.max_epochs > 0 && epoch >= acsConfig.max_epochs) + { + BOOST_LOG_TRIVIAL(info) << "\n" + << "Exiting at epoch " << epoch << " as epoch count " + << acsConfig.max_epochs << " has been reached"; + + return true; + } + + if (tsync == GTime::noTime()) + { + 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) << ")"; + } + + // Check end epoch + if (acsConfig.end_epoch.is_not_a_date_time() == false && boostTime >= acsConfig.end_epoch) + { + BOOST_LOG_TRIVIAL(info) + << "Exiting at epoch " << epoch << " (" << boostTime << ") as end epoch " + << acsConfig.end_epoch << " has been reached"; + + return true; + } + + return false; + }; + + // Read the observations for each station and do stuff + + bool waitMessage = false; + bool nextEpoch = true; + bool complete = false; // When all input files are empty the processing is deemed complete - + // run until then, or until something else breaks the loop + int loopEpochs = 0; // A count of how many loops of epoch_interval this loop used up (usually + // one, but may be more if skipping epochs) + 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 + while (complete == false) + { + if (nextEpoch) + { + nextEpoch = false; + epoch++; + + BOOST_LOG_TRIVIAL(info) << "Starting epoch #" << epoch; + + nominalLoopStartTime += + std::chrono::milliseconds((int)(acsConfig.wait_next_epoch * 1000)); + + if (tsync != GTime::noTime()) + { + // dont obliterate the freshly configured tsync before the first epoch + if (epoch != 1) + { + tsync.bigTime += acsConfig.epoch_interval; + } + + if (fabs(tsync.bigTime - round(tsync.bigTime)) < acsConfig.epoch_tolerance) + { + tsync.bigTime = round(tsync.bigTime); + } + } + + for (auto& [id, rec] : receiverMap) + { + rec.ready = false; + + auto trace = getTraceFile(rec); + + trace << "\n"; + trace << "\n" + << "------=============== Epoch " << epoch << " =============-----------" + << "\n"; + trace << "\n" + << "------=============== Time " << tsync << " =============-----------" + << "\n"; + } + + { + auto pppTrace = getTraceFile(pppNet); + + pppTrace << "\n"; + pppTrace << "\n" + << "------=============== Epoch " << epoch << " =============-----------" + << "\n"; + pppTrace << "\n" + << "------=============== Time " << tsync << " =============-----------" + << "\n"; + } + } + + // Calculate the time at which we will stop waiting for data to come in for this epoch + auto breakTime = nominalLoopStartTime + + std::chrono::milliseconds((int)(acsConfig.max_rec_latency * 1000)); + + if (loopEpochs) + { + BOOST_LOG_TRIVIAL(info) << "Starting epoch #" << epoch; + + waitMessage = false; + } + + if (system_clock::now() > breakTime) + { + BOOST_LOG_TRIVIAL(warning) + << "Excessive time elapsed, skipping epoch " << epoch + << ". Configuration 'wait_next_epoch' is " << acsConfig.wait_next_epoch; + + nextEpoch = true; + + if (atLastEpoch()) + { + break; + } + + continue; + } + + // 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; + while (repeat && system_clock::now() < breakTime) + { + repeat = false; + + // load any changes from the config + bool newConfig = acsConfig.parse(); + + // make any changes to streams. + if (newConfig) + { + configureUploadingStreams(); + } + + // remove any dead streams + for (auto iter = streamParserMultimap.begin(); iter != streamParserMultimap.end();) + { + auto& [id, streamParser_ptr] = *iter; + auto& stream = streamParser_ptr->stream; + + auto& recOpts = acsConfig.getRecOpts(id); + + if (recOpts.kill) + { + BOOST_LOG_TRIVIAL(info) + << "Removing " << stream.sourceString << " due to kill config" << "\n"; + + for (auto& [key, index] : pppNet.kfState.kfIndexMap) + { + if (key.str == id) + { + pppNet.kfState.removeState(key); + + auto nodeHandler = pppNet.kfState.kfIndexMap.extract(key); + nodeHandler.key().rec_ptr = nullptr; + pppNet.kfState.kfIndexMap.insert(std::move(nodeHandler)); + } + } + + receiverMap.erase(id); + + iter = streamParserMultimap.erase(iter); + + continue; + } + + try + { + auto& obsStream = dynamic_cast(*streamParser_ptr); + + if (obsStream.hasObs()) + { + iter++; + continue; + } + } + catch (...) + { + } + + if (stream.isAvailable() && stream.isDead()) + { + BOOST_LOG_TRIVIAL(info) + << "No more data available on " << stream.sourceString << "\n"; + + // record as dead and erase + streamDOAMap[stream.sourceString] = true; + + iter = streamParserMultimap.erase(iter); + + continue; + } + + iter++; + } + + if (streamParserMultimap.empty()) + { + static bool once = true; + if (once) + { + once = false; + + BOOST_LOG_TRIVIAL(info) << "\n"; + BOOST_LOG_TRIVIAL(info) << "Inputs finished at epoch #" << epoch; + } + + if (acsConfig.require_obs) + complete = true; + + break; + } + + // parse all non-observation streams + for (auto& [id, streamParser_ptr] : streamParserMultimap) + try + { + auto& obsStream = dynamic_cast(*streamParser_ptr); + } + catch (std::bad_cast& e) + { + streamParser_ptr->parse(); + } + + for (auto& [id, streamParser_ptr] : streamParserMultimap) + { + ObsStream* obsStream_ptr; + + try + { + obsStream_ptr = &dynamic_cast(*streamParser_ptr); + } + catch (std::bad_cast& e) + { + continue; + } + + auto& obsStream = *obsStream_ptr; + + if (obsStream.stream.isAvailable() == false) + { + continue; + } + + auto& recOpts = acsConfig.getRecOpts(id); + + if (recOpts.exclude) + { + continue; + } + + auto& rec = receiverMap[id]; + + auto trace = getTraceFile(rec); + + if (obsStream.isPseudoRec) + { + rec.isPseudoRec = true; + } + + // try to get some data (again) + if (rec.ready) + { + continue; + } + + 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); + + switch (obsStream.obsAgeCode) + { + 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; + } + } + + if (rec.obsList.empty()) + { + // failed to get observations + if (obsStream.obsAgeCode == E_ObsAgeCode::NO_OBS) + { + // try again later + repeat = true; + sleep_for(std::chrono::milliseconds(acsConfig.sleep_milliseconds)); + } + + continue; + } + + if (tsync == GTime::noTime()) + { + tsync = rec.obsList.front()->time.floorTime(acsConfig.epoch_interval); + + acsConfig.start_epoch = + boost::posix_time::from_time_t((time_t)((PTime)tsync).bigTime); + + if (tsync + acsConfig.epoch_tolerance < rec.obsList.front()->time) + { + repeat = true; + continue; + } + } + + dataAvailableMap[rec.id] = true; + rec.ready = true; + rec.source = obsStream.stream.sourceString; + + extractTrackedSignals(rec, obsStream.parser, &rec.obsList); + + auto now = system_clock::now(); + + 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) + { + breakTime = alternateBreakTime; + } + if (alternateStartTime < nominalLoopStartTime) + { + nominalLoopStartTime = alternateStartTime; + } + } + } + } + + if (complete) + { + break; + } + + if (acsConfig.allow_missing_inputs && dataAvailableMap.empty()) + { + if (waitMessage == false) + { + waitMessage = true; + + BOOST_LOG_TRIVIAL(info) << "No more data available, waiting for further inputs..."; + } + + continue; + } + + if (tsync == GTime::noTime()) + { + if (acsConfig.require_obs) + continue; + + tsync = timeGet().floorTime(acsConfig.epoch_interval); + + acsConfig.start_epoch = boost::posix_time::from_time_t((time_t)((PTime)tsync).bigTime); + } + + BOOST_LOG_TRIVIAL(info) << "Synced " << dataAvailableMap.size() << " receivers..."; + + lastEpochStartTime = timeGet(); + if (acsConfig.require_obs == false || dataAvailableMap.empty() == false) + { + mainOncePerEpoch(pppNet, ionNet, receiverMap, tsync); + } + lastEpochStopTime = timeGet(); + + if (atLastEpoch(true)) + { + break; + } + + nextEpoch = true; + } + + // Disconnect the downloading clients and stop the io_service for clean shutdown. + for (auto& [id, ntripStream] : only(streamParserMultimap)) + { + ntripStream.disconnect(); + } + + ntripBroadcaster.stopBroadcast(); + TcpSocket::ioContext.stop(); + + GTime peaInterTime = timeGet(); + BOOST_LOG_TRIVIAL(info) << "\n" + << "PEA started processing at : " << peaStartTime << "\n" + << "and finished processing at : " << peaInterTime << "\n" + << "Total processing duration : " << (peaInterTime - peaStartTime) + << "\n" + << "\n"; + + BOOST_LOG_TRIVIAL(info) << "\n" + << "Finalising streams and post processing..."; + + mainPostProcessing(pppNet, ionNet, receiverMap); + + GTime peaStopTime = timeGet(); + BOOST_LOG_TRIVIAL(info) << "\n" + << "PEA started processing at : " << peaStartTime << "\n" + << "and finished processing at : " << peaStopTime << "\n" + << "Total processing duration : " << (peaStopTime - peaStartTime) + << "\n" + << "\n"; + + BOOST_LOG_TRIVIAL(info) << "PEA finished"; + + return EXIT_SUCCESS; } diff --git a/src/cpp/pea/minimumConstraints.cpp b/src/cpp/pea/minimumConstraints.cpp index 408d324c8..38349175e 100644 --- a/src/cpp/pea/minimumConstraints.cpp +++ b/src/cpp/pea/minimumConstraints.cpp @@ -1,941 +1,1117 @@ - // #pragma GCC optimize ("O0") +#include "pea/minimumConstraints.hpp" #include #include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/algebraTrace.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/sinex.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" using std::vector; -#include "minimumConstraints.hpp" -#include "eigenIncluder.hpp" -#include "algebraTrace.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "mongoWrite.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "sinex.hpp" -#include "trace.hpp" -#include "enums.h" - -void minSiteData( - Trace& trace, - KFState& kfStateStations, - string suffix, - map& usedMap) +void minSiteData(Trace& trace, KFState& kfStateStations, string suffix, map& usedMap) { - Block block(trace, (string)"SITE/DATA" + suffix); - - tracepdeex(0, trace, "#\t%4s\t%9s\t%9s\t%8s\t%8s\t%8s\t%8s\t%8s\t%8s\t%8s\t%s\n", - "Site", - "Lat", - "Lon", - "Height", - "resN(mm)", - "resE(mm)", - "resH(mm)", - "sig_N", - "sig_E", - "sig_H", - "Constraint"); - - for (auto& [key, index] : kfStateStations.kfIndexMap) - { - if (key.type != KF::REC_POS) continue; - if (key.rec_ptr == nullptr) continue; - if (key.num != 0) continue; - - Receiver& rec = *key.rec_ptr; - - //get all of the position elements for this station - map kfKeyMap; - for (int i = 0; i < 3; i++) - { - KFKey kfKey = key; - kfKey.num = i; - kfKeyMap[kfKey] = i; - } - - MatrixXd filterVar; - Vector3d filterPos = kfStateStations.getSubState(kfKeyMap, &filterVar); - - string constraint; - if (usedMap[index]) - { - constraint = "!"; - } - - Vector3d aprioriPos = rec.minconApriori; - - VectorPos pos = ecef2pos(filterPos); - - Vector3d deltaR = filterPos - aprioriPos; - - Matrix3d E; - pos2enu(pos, E.data()); - Matrix3d enuCovariance = E * filterVar * E.transpose(); - Vector3d enuResidual = E * deltaR; - - tracepdeex(0, trace, "@\t%4s\t%9.4f\t%9.4f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%s\n", - rec.id.c_str(), - pos.latDeg(), - pos.lonDeg(), - pos.hgt(), - enuResidual(1) * 1000, - enuResidual(0) * 1000, - enuResidual(2) * 1000, - sqrt(enuCovariance(1,1)) * 1e3, - sqrt(enuCovariance(0,0)) * 1e3, - sqrt(enuCovariance(2,2)) * 1e3, - constraint.c_str()); - } + Block block(trace, (string) "SITE/DATA" + suffix); + + tracepdeex( + 0, + trace, + "#\t%4s\t%9s\t%9s\t%8s\t%8s\t%8s\t%8s\t%8s\t%8s\t%8s\t%s\n", + "Site", + "Lat", + "Lon", + "Height", + "resN(mm)", + "resE(mm)", + "resH(mm)", + "sig_N", + "sig_E", + "sig_H", + "Constraint" + ); + + for (auto& [key, index] : kfStateStations.kfIndexMap) + { + if (key.type != KF::REC_POS) + continue; + if (key.rec_ptr == nullptr) + continue; + if (key.num != 0) + continue; + + Receiver& rec = *key.rec_ptr; + + // get all of the position elements for this station + map kfKeyMap; + for (int i = 0; i < 3; i++) + { + KFKey kfKey = key; + kfKey.num = i; + kfKeyMap[kfKey] = i; + } + + MatrixXd filterVar; + Vector3d filterPos = kfStateStations.getSubState(kfKeyMap, &filterVar); + + string constraint; + if (usedMap[index]) + { + constraint = "!"; + } + + Vector3d aprioriPos = rec.minconApriori; + + VectorPos pos = ecef2pos(filterPos); + + Vector3d deltaR = filterPos - aprioriPos; + + Matrix3d E; + pos2enu(pos, E.data()); + Matrix3d enuCovariance = E * filterVar * E.transpose(); + Vector3d enuResidual = E * deltaR; + + tracepdeex( + 0, + trace, + "@\t%4s\t%9.4f\t%9.4f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%8.3f\t%s\n", + rec.id.c_str(), + pos.latDeg(), + pos.lonDeg(), + pos.hgt(), + enuResidual(1) * 1000, + enuResidual(0) * 1000, + enuResidual(2) * 1000, + sqrt(enuCovariance(1, 1)) * 1e3, + sqrt(enuCovariance(0, 0)) * 1e3, + sqrt(enuCovariance(2, 2)) * 1e3, + constraint.c_str() + ); + } } void minOrbitData( - Trace& trace, - KFState& kfStateStations, - string suffix, - map& usedMap, - FrameSwapper& frameSwapper) + Trace& trace, + KFState& kfStateStations, + string suffix, + map& usedMap, + FrameSwapper& frameSwapper +) { - Block block(trace, (string)"ORBIT/DATA" + suffix); - - tracepdeex(0, trace, "#\t%4s\t%9s\t%9s\t%12s\t%10s\t%10s\t%10s\t%10s\t%10s\t%10s\t%s\n", - "Sat", - "Lat", - "Lon", - "Height", - "resR(mm)", - "resT(mm)", - "resN(mm)", - "sig_R", - "sig_Y", - "sig_N", - "Constraint"); - - for (auto& [key, index] : kfStateStations.kfIndexMap) - { - if (key.type != KF::ORBIT) continue; - if (key.num != 0) continue; - - //get all of the position elements for this station - map kfKeyMap; - for (int i = 0; i < 6; i++) - { - KFKey kfKey = key; - kfKey.num = i; - kfKeyMap[kfKey] = i; - } - - MatrixXd filterVar; - VectorXd filterState = kfStateStations.getSubState(kfKeyMap, &filterVar); - - Vector3d filterPos = filterState.head(3); - Vector3d filterVel = filterState.tail(3); - - string constraint; - if (usedMap[index]) - { - constraint = "!"; - } - - auto& satNav = nav.satNavMap[key.Sat]; - - Vector3d aprioriPos = satNav.aprioriPos; - - VectorPos pos = ecef2pos(frameSwapper((VectorEci)filterPos)); - - Vector3d deltaR = filterPos - aprioriPos; - - Matrix3d E = ecef2rac(filterPos, filterVel); - - Matrix3d rtnCovariance = E * filterVar.topLeftCorner(3,3) * E.transpose(); - Vector3d rtnResidual = E * deltaR; - - tracepdeex(0, trace, "@\t%4s\t%9.4f\t%9.4f\t%12.3f\t%10.3f\t%10.3f\t%10.3f\t%10.3f\t%10.3f\t%10.3f\t%s\n", - key.Sat.id().c_str(), - pos.latDeg(), - pos.lonDeg(), - pos.hgt(), - rtnResidual(0) * 1000, - rtnResidual(1) * 1000, - rtnResidual(2) * 1000, - sqrt(rtnCovariance(0,0)) * 1e3, - sqrt(rtnCovariance(1,1)) * 1e3, - sqrt(rtnCovariance(2,2)) * 1e3, - constraint.c_str()); - } + Block block(trace, (string) "ORBIT/DATA" + suffix); + + tracepdeex( + 0, + trace, + "#\t%4s\t%9s\t%9s\t%12s\t%10s\t%10s\t%10s\t%10s\t%10s\t%10s\t%s\n", + "Sat", + "Lat", + "Lon", + "Height", + "resR(mm)", + "resT(mm)", + "resN(mm)", + "sig_R", + "sig_Y", + "sig_N", + "Constraint" + ); + + for (auto& [key, index] : kfStateStations.kfIndexMap) + { + if (key.type != KF::ORBIT) + continue; + if (key.num != 0) + continue; + + // get all of the position elements for this station + map kfKeyMap; + for (int i = 0; i < 6; i++) + { + KFKey kfKey = key; + kfKey.num = i; + kfKeyMap[kfKey] = i; + } + + MatrixXd filterVar; + VectorXd filterState = kfStateStations.getSubState(kfKeyMap, &filterVar); + + Vector3d filterPos = filterState.head(3); + Vector3d filterVel = filterState.tail(3); + + string constraint; + if (usedMap[index]) + { + constraint = "!"; + } + + auto& satNav = nav.satNavMap[key.Sat]; + + Vector3d aprioriPos = satNav.aprioriPos; + + VectorPos pos = ecef2pos(frameSwapper((VectorEci)filterPos)); + + Vector3d deltaR = filterPos - aprioriPos; + + Matrix3d E = ecef2rac(filterPos, filterVel); + + Matrix3d rtnCovariance = E * filterVar.topLeftCorner(3, 3) * E.transpose(); + Vector3d rtnResidual = E * deltaR; + + tracepdeex( + 0, + trace, + "@\t%4s\t%9.4f\t%9.4f\t%12.3f\t%10.3f\t%10.3f\t%10.3f\t%10.3f\t%10.3f\t%10.3f\t%s\n", + key.Sat.id().c_str(), + pos.latDeg(), + pos.lonDeg(), + pos.hgt(), + rtnResidual(0) * 1000, + rtnResidual(1) * 1000, + rtnResidual(2) * 1000, + sqrt(rtnCovariance(0, 0)) * 1e3, + sqrt(rtnCovariance(1, 1)) * 1e3, + sqrt(rtnCovariance(2, 2)) * 1e3, + constraint.c_str() + ); + } } void zeroAndPush(int index, MatrixXd& R, KFState& kfStateTrans, KFMeasEntryList& measList) { - R.row(index).setZero(); - R.col(index).setZero(); + R.row(index).setZero(); + R.col(index).setZero(); - // Add null measurement and continue, it's needed for inverse later - KFMeasEntry meas(&kfStateTrans); - measList.push_back(meas); + // Add null measurement and continue, it's needed for inverse later + KFMeasEntry meas(&kfStateTrans); + measList.push_back(meas); } void mincon( - Trace& trace, - KFState& kfStateStations, - MinconStatistics* minconStatistics_ptr0, - MinconStatistics* minconStatistics_ptr1, - bool commentSinex, - KFState* kfStateTransform_ptr) + Trace& trace, + KFState& kfStateStations, + MinconStatistics* minconStatistics_ptr0, + MinconStatistics* minconStatistics_ptr1, + bool commentSinex, + KFState* kfStateTransform_ptr, + bool estimateTransform, + bool outputPrePost +) { - // Reference: Estimating regional deformation from a combination of space and terrestrial geodetic data - Appendix E - // Perform LSQ/Kalman filter to determine transformation state - - if (acsConfig.output_mincon) - { - std::cout << "\n" << "Writing backup point for minimum constraints to " << acsConfig.mincon_filename; - - spitFilterToFile(kfStateStations, E_SerialObject::FILTER_PLUS, acsConfig.mincon_filename); - } - - if (acsConfig.mincon_only) - { - long int startPos = -1; - E_SerialObject type = getFilterTypeFromFile(startPos, acsConfig.mincon_filename); - } - - //Determine transformation state - KFState kfStateTrans; - - kfStateTrans.FilterOptions::operator=(acsConfig.minconOpts); - kfStateTrans.id = "MINIMUM"; - kfStateTrans.output_residuals = acsConfig.output_residuals; - kfStateTrans.outputMongoMeasurements = acsConfig.mongoOpts.output_measurements; - - kfStateTrans.measRejectCallbacks.push_back(deweightStationMeas); - - KFMeasEntryList measList; - KFMeasEntryList measListCulled; - - InitialState xlateInit = initialStateFromConfig(acsConfig.minconOpts.translation); - InitialState rtateInit = initialStateFromConfig(acsConfig.minconOpts.rotation); - InitialState scaleInit = initialStateFromConfig(acsConfig.minconOpts.scale); - InitialState delayInit = initialStateFromConfig(acsConfig.minconOpts.delay); - - MatrixXd R = kfStateStations.P; - - vector indices; - map usedMap; - - bool hasStations = false; - bool hasSatellites = false; - - - ERPValues erpv = getErp(nav.erp, kfStateStations.time); - - FrameSwapper frameSwapper(kfStateStations.time, erpv); - - VectorEcef xUnitEcef = (Vector3d) Vector3d::UnitX(); - VectorEcef yUnitEcef = (Vector3d) Vector3d::UnitY(); - VectorEcef zUnitEcef = (Vector3d) Vector3d::UnitZ(); - - VectorEci xUnitEci = frameSwapper(xUnitEcef); - VectorEci yUnitEci = frameSwapper(yUnitEcef); - VectorEci zUnitEci = frameSwapper(zUnitEcef); - - for (auto& [key, index] : kfStateStations.kfIndexMap) - { - - if (key.type != KF::REC_POS && key.type != KF::ORBIT) - { - zeroAndPush(index, R, kfStateTrans, measList); - continue; - } - - if ( key.type == KF::ORBIT - &&( key.num >= 3 - ||acsConfig.minconOpts.constrain_orbits == false)) - { - zeroAndPush(index, R, kfStateTrans, measList); - continue; - } - - Vector3d aprioriPos = Vector3d::Zero(); - Matrix3d aprioriVar = Matrix3d::Zero(); - Matrix3d filterVar = Matrix3d::Zero(); - string str; - - if (key.type == +KF::REC_POS) - { - if (key.rec_ptr == nullptr) - { - BOOST_LOG_TRIVIAL(error) - << "Error: rec_ptr is null during mincon"; - - continue; - } - - auto& rec = *key.rec_ptr; - auto& recOpts = acsConfig.getRecOpts(rec.id); - - aprioriPos = rec.minconApriori; - aprioriVar = rec.aprioriVar * SQR(recOpts.mincon_scale_apriori_sigma); - filterVar = kfStateStations.P.block(index, index, 3, 3) * SQR(recOpts.mincon_scale_filter_sigma); - str = rec.id; - - hasStations = true; - } - else if (key.type == +KF::ORBIT) - { - auto& satNav = nav.satNavMap[key.Sat]; - - if (satNav.aprioriPos.isZero()) - { - zeroAndPush(index, R, kfStateTrans, measList); - continue; - } - - auto& satOpts = acsConfig.getSatOpts(key.Sat); - - aprioriPos = satNav.aprioriPos; - aprioriVar = 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(); - - hasSatellites = true; - } - - bool used = true; - - MatrixXd noise = aprioriVar + filterVar; - - if ( aprioriVar(0,0) <= 0 - &&acsConfig.minconOpts.transform_unweighted == false) - { - zeroAndPush(index, R, kfStateTrans, measList); - continue; - } - - if ( aprioriVar(0,0) <= 0 - ||aprioriPos.isZero()) - { - R.row(index).setZero(); - R.col(index).setZero(); - - used = false; - } - - if (key.num != 0) - { - //deal with all position dimensions at same time when (num == 0) - continue; - } - - BOOST_LOG_TRIVIAL(debug) << "\n" << str << "Noises" << "\n" << aprioriVar << "\n" << filterVar; - - //get all of the position elements for this thing - Vector3d filterPos = Vector3d::Zero(); - Vector3d filterVel = Vector3d::Zero(); - - for (int i = 0; i < 3; i++) - { - KFKey kfKey = key; - kfKey.num = i; - kfStateStations.getKFValue(kfKey, filterPos(i)); - } - - if (key.type == +KF::ORBIT) - for (int i = 0; i < 3; i++) - { - KFKey kfKey = key; - kfKey.num = i + 3; - kfStateStations.getKFValue(kfKey, filterVel(i)); - } - - Vector3d dRdX; - Vector3d dRdY; - Vector3d dRdZ; - Vector3d dRdT; - Vector3d dRdThetaX; - Vector3d dRdThetaY; - Vector3d dRdThetaZ; - - if (key.type == +KF::REC_POS) - { - dRdX = xUnitEcef; - dRdY = yUnitEcef; - dRdZ = zUnitEcef; - dRdT = filterVel; - dRdThetaX = aprioriPos.cross(xUnitEcef); - dRdThetaY = aprioriPos.cross(yUnitEcef); - dRdThetaZ = aprioriPos.cross(zUnitEcef); - } - else if (key.type == +KF::ORBIT) - { - dRdX = xUnitEci; - dRdY = yUnitEci; - dRdZ = zUnitEci; - dRdT = filterVel; - dRdThetaX = aprioriPos.cross(xUnitEci); - dRdThetaY = aprioriPos.cross(yUnitEci); - dRdThetaZ = aprioriPos.cross(zUnitEci); - } - - Vector3d deltaR = filterPos - aprioriPos; - - if (acsConfig.minconOpts.full_vcv == false) - { - R.middleRows(index, 3).setZero(); - R.middleCols(index, 3).setZero(); - } - - if ( acsConfig.minconOpts.full_vcv == false - && used) - { - R.block(index, index, 3, 3) = noise; - } - - for (short xyz = 0; xyz < 3; xyz++) - { - KFKey obsKey; - obsKey.str = str; - obsKey.num = xyz; - obsKey.comment = "MINCON"; - - KFMeasEntry meas(&kfStateTrans, obsKey); - - if (xlateInit.estimate) - { - meas.addDsgnEntry({KF::XFORM_XLATE, {}, "", 0, "M" }, dRdX(xyz), xlateInit); - meas.addDsgnEntry({KF::XFORM_XLATE, {}, "", 1, "M" }, dRdY(xyz), xlateInit); - meas.addDsgnEntry({KF::XFORM_XLATE, {}, "", 2, "M" }, dRdZ(xyz), xlateInit); - } - - if (rtateInit.estimate) - { - meas.addDsgnEntry({KF::XFORM_RTATE, {}, "", 0, "MAS" }, dRdThetaX(xyz) * MAS2R, rtateInit); - meas.addDsgnEntry({KF::XFORM_RTATE, {}, "", 1, "MAS" }, dRdThetaY(xyz) * MAS2R, rtateInit); - meas.addDsgnEntry({KF::XFORM_RTATE, {}, "", 2, "MAS" }, dRdThetaZ(xyz) * MAS2R, rtateInit); - } - - if (scaleInit.estimate) - { - meas.addDsgnEntry({KF::XFORM_SCALE, {}, "", 0, "PPB" }, aprioriPos(xyz) * 1e-9, scaleInit); - } - - if (delayInit.estimate) - { - meas.addDsgnEntry({KF::XFORM_DELAY, {}, "", 0, "S" }, dRdT(xyz), delayInit); - } - - double innov = deltaR(xyz); - - meas.setValue(innov); - - - //Add null measurement and continue, its needed for inverse later - - measList.push_back(meas); - - if (used) - { - int xIndex = index + xyz; - indices.push_back(xIndex); - usedMap[xIndex] = true; - meas.metaDataMap["used_ptr"] = &usedMap[xIndex]; - meas.metaDataMap["otherIndex"] = (void*) (measList.size() - 1); //need an index into the big measurement matrix for deweighting to get applied in 2 places - - measListCulled.push_back(meas); - } - } - } - - //use a state transition to initialise elements - kfStateTrans.stateTransition(trace, kfStateStations.time); - -// std::cout << "\n" << "R" << "\n" << R << "\n"; - - MatrixXd RR = R(indices, indices); - - KFMeas combinedMeas (kfStateTrans, measList, GTime::noTime(), &R); - KFMeas combinedMeasCulled (kfStateTrans, measListCulled, GTime::noTime(), &RR); - - for (auto& metaDataMap : combinedMeasCulled.metaDataMaps) - { - metaDataMap["otherNoiseMatrix_ptr"] = &combinedMeas.R; - } - - if (kfStateTrans.lsqRequired) - { - trace << "\n" << "------- LEAST SQUARES FOR MINIMUM CONSTRAINTS TRANSFORMATION --------" << "\n"; - kfStateTrans.leastSquareInitStates(trace, combinedMeasCulled, false, &kfStateTrans.dx); - kfStateTrans.dx = VectorXd::Zero(kfStateTrans.x.rows()); - kfStateTrans.outputStates(trace, "/MINCON_TRANSFORM_LSQ"); - } - - trace << "\n" << "------- FILTERING FOR MINIMUM CONSTRAINTS TRANSFORMATION --------" << "\n"; - - kfStateTrans.filterKalman(trace, combinedMeasCulled, "/MINCON_TRANSFORM"); - - kfStateTrans.outputStates(trace, "/MINCON_TRANSFORM"); - - if (kfStateTransform_ptr) - { - *kfStateTransform_ptr = kfStateTrans; - } - - mongoStates(kfStateTrans, - { - .suffix = "/MINCON_TRANSFORM", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - - KFState oldStateStations = kfStateStations; - - //Do kalman filter on original state using pseudomeasurements - MatrixXd K; - VectorXd v; - MatrixXd H; - - auto& P = kfStateStations.P; - - switch (acsConfig.minconOpts.application_mode) - { - case E_Mincon::PSEUDO_OBS: - { - //rename for some semblance of resemblance to the 'bible' - auto& T = combinedMeas.H; - auto& Theta = kfStateTrans.x; - - //create a subset matrix for positions only - vector posIndices; - for (int i = 0; i < T.rows(); i++) - { - if (T.row(i).any()) - { - posIndices.push_back(i); - } - } - - //calculate position deltas and their variances according to transform - VectorXd tTheta = T * Theta; //will correspond to (but not be) pseudoobservations - MatrixXd Omega = T * kfStateTrans.P * T.transpose(); //will be pseudoobservations variances - - //just get the subset that are positions - tTheta = tTheta(posIndices ).eval(); - Omega = Omega (posIndices, posIndices ).eval(); - - - //standard kalman filter stuff using the ordinary states - //filter using pseudoobs for each position state - - //design matrix is subset of identity matrix - H = MatrixXd::Identity(kfStateStations.x.rows(), kfStateStations.x.rows()); - H = H(posIndices, all).eval(); - - - //calculate kalman gain - K = P * H.transpose() * (H * P * H.transpose() + Omega).inverse(); - - //do some algebra to compute the required v values such that a subset of kalman state adjustments are the desired dx calculated previously - //ie, find v - - // H. K. v = tTheta - // Kt. Ht. H. K. v = Kt. Ht. tTheta //normal equations - - // solve A.x = b, where: - // x = v - auto A = K.transpose() * H.transpose() * H * K; - auto b = K.transpose() * H.transpose() * tTheta; - - v = A.ldlt().solve(b); - - if (0) - { - VectorXd errors = tTheta - H * K * v; - std::cout << "\n" << "tTheta:" << "\n" << tTheta << "\n"; - std::cout << "\n" << "H:" << "\n" << H << "\n"; - std::cout << "\n" << "errors:" << "\n" << errors << "\n"; - } - break; - } - case E_Mincon::WEIGHT_MATRIX: - case E_Mincon::VARIANCE_INVERSE: - case E_Mincon::COVARIANCE_INVERSE: - { - int numXform = kfStateTrans.x.rows() - 1; - int numStates = combinedMeas.R.rows(); - - v = kfStateTrans.x.bottomRows(numXform); - - //generalised inverse (Ref:E.3) - MatrixXd T = combinedMeas.H.bottomRightCorner(numStates, numXform); - MatrixXd W = MatrixXd::Zero(numStates, numStates); - // std::cout << "\n" << "R" << "\n" << combinedMeasCulled.R<< "\n"; - - switch (acsConfig.minconOpts.application_mode) - { - case E_Mincon::WEIGHT_MATRIX: - { - for (int i = 0; i < numStates; i++) - { - double val = combinedMeas.R(i,i); - if (val) - { - W(i,i) = 1 / val; - } - } - break; - } - case E_Mincon::VARIANCE_INVERSE: - { - for (auto& [kfKey, index] : kfStateStations.kfIndexMap) - { - if (kfStateStations.P(index, index)) - { - W(index, index) = 1 / kfStateStations.P(index, index); - } - } - break; - } - case E_Mincon::COVARIANCE_INVERSE: - { - vector validP; - for (auto& [kfKey, index] : kfStateStations.kfIndexMap) - { - if (kfStateStations.P(index, index)) - { - validP.push_back(index); - } - } - - MatrixXd inverse = kfStateStations.P(validP, validP).inverse(); - - int row = 0; - for (auto& i : validP) - { - int col = 0; - for (auto& j : validP) - { - W(i, j) = inverse(row, col); - col++; - } - row++; - } - - break; - } - } - - - - W = ((W + W.transpose()) / 2).eval(); - - // std::cout << "\n" << "P" << "\n" << kfStateStations.P << "\n"; -// std::cout << "\n" << "W_" << "\n" << W << "\n"; - // -// std::cout << "\n" << "T" << "\n" << T << "\n"; - - MatrixXd TW = T.transpose() * W; - - MatrixXd TWT = TW * T; - -// std::cout << "\n" << "TWT" << "\n" << TWT << "\n"; - - auto QQ = TWT.triangularView().transpose(); - LDLT solver; - solver.compute(QQ); - if (solver.info() != Eigen::ComputationInfo::Success) - { - std::cout << "Mincon borked." << "\n"; - return; - } - - H = MatrixXd::Zero(numXform, numStates); - H.rightCols(numStates) = solver.solve(TW); - if (solver.info() != Eigen::ComputationInfo::Success) - { - std::cout << "Mincon borked!" << "\n"; - return; - } - -// std::cout << "\n" << "TWT" << "\n" << TWT << "\n"; -// std::cout << "\n" << "TW" << "\n" << TW << "\n"; -// std::cout << "\n" << "TDash" << "\n" << H << "\n"; - - //calculate kalman gain - K = P * H.transpose() * (H * P * H.transpose()/* + Omega*/).inverse(); - - break; - } - } - - //standard kalman filter things again, but do manually since already have K calculated - { - KFState& kfState = kfStateStations; - - //use a state transition to ensure output logs are complete - kfState.stateTransition(std::cout, kfState.time); - - trace << "\n" << " -------DOING KALMAN FILTER WITH PSEUDO ELEMENTS FOR MINIMUM CONSTRAINTS --------" << "\n"; - - if (kfState.rts_basename.empty() == false) - { - spitFilterToFile(kfState, E_SerialObject::FILTER_MINUS, kfState.rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - } - - kfState.dx = (K * v ).eval(); - kfState.x = (kfState.x - kfState.dx ).eval(); - kfState.P = (kfState.P - K * H * P ).eval(); - - if (isPositiveSemiDefinite(P) == false) - { - std::cout << "\n" << "WARNING, NOT PSD"; - } - - if (kfState.rts_basename.empty() == false) - { - kfState.metaDataMap["SKIP_PREV_RTS"] = "TRUE"; - spitFilterToFile(kfState.metaDataMap, E_SerialObject::METADATA, kfState.rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - - spitFilterToFile(kfState, E_SerialObject::FILTER_PLUS, kfState.rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - } - } - - if (hasStations) - { - minSiteData (trace, oldStateStations, " Pre Constraint", usedMap); - minSiteData (trace, kfStateStations, " Post Constraint", usedMap); - } - - if (hasSatellites) - { - minOrbitData(trace, oldStateStations, " Pre Constraint", usedMap, frameSwapper); - minOrbitData(trace, kfStateStations, " Post Constraint", usedMap, frameSwapper); - } - - for (auto type : {KF::REC_POS, KF::ORBIT}) - { - if (type == KF::REC_POS && hasStations == false) continue; - if (type == KF::ORBIT && hasSatellites == false) continue; - - for (auto before : {true, false}) - for (auto& [key, index] : kfStateStations.kfIndexMap) - { - if (key.type != type) { continue; } - if (key.num != 0) { continue; } - - Vector3d filterPos; - for (int i = 0; i < 3; i++) - { - if (before) filterPos(i) = oldStateStations .x(index + i); - else filterPos(i) = kfStateStations .x(index + i); - } - - Vector3d filterVel; - if (type == KF::ORBIT) - for (int i = 0; i < 3; i++) - { - if (before) filterVel(i) = oldStateStations .x(index + i + 3); - else filterVel(i) = kfStateStations .x(index + i + 3); - } - - string aggregatedUsed = "Agg-Used"; - string aggregatedAll = "Agg-All"; - string str; - Vector3d aprioriPos = Vector3d::Zero(); - if (type == KF::REC_POS) { auto& rec = *key.rec_ptr; aprioriPos = rec.minconApriori; str = key.str; } - else if (type == KF::ORBIT) { auto& satNav = nav.satNavMap[key.Sat]; aprioriPos = satNav.aprioriPos; str = key.Sat.id(); } - - Vector3d deltaR = filterPos - aprioriPos; - - Matrix3d E = Matrix3d::Zero(); - - if (type == KF::REC_POS) { VectorPos pos = ecef2pos(filterPos); pos2enu(pos, E.data()); } - else if (type == KF::ORBIT) { E = ecef2rac(filterPos, filterVel); } - - Vector3d frameResidual = E * deltaR; - - - for (auto& rmsStatisticsMap_ptr : {minconStatistics_ptr0, minconStatistics_ptr1}) - for (auto& entry : {aggregatedUsed, aggregatedAll, str}) - { - if (rmsStatisticsMap_ptr == nullptr) - { - continue; - } - - if ( entry == aggregatedUsed - &&usedMap[index] == false) - { - continue; - } - - auto& rmsStatisticsMap = *rmsStatisticsMap_ptr; - - for (int i = 0; i < 4; i++) - { - string component = "3D"; - if (type == KF::REC_POS) - switch (i) - { - case 0: component = "E"; break; - case 1: component = "N"; break; - case 2: component = "U"; break; - } - else if (type == KF::ORBIT) - switch (i) - { - case 0: component = "R"; break; - case 1: component = "T"; break; - case 2: component = "N"; break; - } - - if (i < 3) rmsStatisticsMap[entry][component][before].sum += SQR(frameResidual(i)); - else rmsStatisticsMap[entry][component][before].sum += frameResidual.squaredNorm(); - - rmsStatisticsMap[entry][component][before].count++; - } - } - } - } - - if (commentSinex) - { - for (auto& [key, index] : kfStateStations.kfIndexMap) - { - if ( key.num == 0 - && key.type == KF::REC_POS) - { - sinexAddComment((string)" Minimum Constraints Stations: " + key.str + (usedMap[index] ? " used" : " unused")); - } - } - - for (auto& [key, index] : kfStateTrans.kfIndexMap) - { - if (key.type == +KF::ONE) - continue; - - char line[128]; - snprintf(line, sizeof(line), " Minimum Constraints Transform: %12s:%c %+9f %6s +- %8f", - KF::_from_integral(key.type)._to_string(), - 'X' + key.num, - kfStateTrans.x(index), - key.comment.c_str(), - sqrt(kfStateTrans.P(index,index))); - - sinexAddComment(line); - } - } + // Reference: Estimating regional deformation from a combination of space and terrestrial + // geodetic data - Appendix E Perform LSQ/Kalman filter to determine transformation state + + if (acsConfig.output_mincon) + { + std::cout << "\n" + << "Writing backup point for minimum constraints to " + << acsConfig.mincon_filename; + + spitFilterToFile(kfStateStations, E_SerialObject::FILTER_PLUS, acsConfig.mincon_filename); + } + + if (acsConfig.mincon_only) + { + long int startPos = -1; + E_SerialObject type = getFilterTypeFromFile(startPos, acsConfig.mincon_filename); + } + + // Determine transformation state + KFState kfStateTrans; + + if (kfStateTransform_ptr) + { + kfStateTrans = *kfStateTransform_ptr; + } + + if (kfStateTrans.id != "MINIMUM") + { + kfStateTrans.FilterOptions::operator=(acsConfig.minconOpts); + kfStateTrans.id = "MINIMUM"; + kfStateTrans.output_residuals = acsConfig.output_residuals; + kfStateTrans.outputMongoMeasurements = + (acsConfig.mongoOpts.output_measurements != E_Mongo::NONE); + + kfStateTrans.measRejectCallbacks.push_back(deweightStationMeas); + } + + KFMeasEntryList measList; + KFMeasEntryList measListCulled; + + InitialState xlateInit = initialStateFromConfig(acsConfig.minconOpts.translation); + InitialState rtateInit = initialStateFromConfig(acsConfig.minconOpts.rotation); + InitialState scaleInit = initialStateFromConfig(acsConfig.minconOpts.scale); + InitialState delayInit = initialStateFromConfig(acsConfig.minconOpts.delay); + InitialState xlateRateInit = initialStateFromConfig(acsConfig.minconOpts.translation_rate); + InitialState rtateRateInit = initialStateFromConfig(acsConfig.minconOpts.rotation_rate); + InitialState scaleRateInit = initialStateFromConfig(acsConfig.minconOpts.scale_rate); + InitialState delayRateInit = initialStateFromConfig(acsConfig.minconOpts.delay_rate); + + MatrixXd R = kfStateStations.P; + + vector indices; + map usedMap; + + bool hasStations = false; + bool hasSatellites = false; + + ERPValues erpv = getErp(nav.erp, kfStateStations.time); + + FrameSwapper frameSwapper(kfStateStations.time, erpv); + + VectorEcef xUnitEcef = (Vector3d)Vector3d::UnitX(); + VectorEcef yUnitEcef = (Vector3d)Vector3d::UnitY(); + VectorEcef zUnitEcef = (Vector3d)Vector3d::UnitZ(); + + VectorEci xUnitEci = frameSwapper(xUnitEcef); + VectorEci yUnitEci = frameSwapper(yUnitEcef); + VectorEci zUnitEci = frameSwapper(zUnitEcef); + + for (auto& [key, index] : kfStateStations.kfIndexMap) + { + if (key.type != KF::REC_POS && key.type != KF::ORBIT) + { + zeroAndPush(index, R, kfStateTrans, measList); + continue; + } + + if (key.type == KF::ORBIT && + (key.num >= 3 || acsConfig.minconOpts.constrain_orbits == false)) + { + zeroAndPush(index, R, kfStateTrans, measList); + continue; + } + + Vector3d aprioriPos = Vector3d::Zero(); + Matrix3d aprioriPosVar = Matrix3d::Zero(); + Matrix3d filterVar = Matrix3d::Zero(); + string str; + + if (key.type == KF::REC_POS) + { + if (key.rec_ptr == nullptr) + { + BOOST_LOG_TRIVIAL(error) << "rec_ptr is null during mincon"; + + continue; + } + + auto& rec = *key.rec_ptr; + auto& recOpts = acsConfig.getRecOpts(rec.id); + + 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; + + hasStations = true; + } + else if (key.type == KF::ORBIT) + { + auto& satNav = nav.satNavMap[key.Sat]; + + if (satNav.aprioriPos.isZero()) + { + zeroAndPush(index, R, kfStateTrans, measList); + continue; + } + + auto& satOpts = acsConfig.getSatOpts(key.Sat); + + 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(); + + hasSatellites = true; + } + + bool used = true; + + MatrixXd noise = aprioriPosVar + filterVar; + + if (aprioriPosVar(0, 0) <= 0 && acsConfig.minconOpts.transform_unweighted == false) + { + zeroAndPush(index, R, kfStateTrans, measList); + continue; + } + + if (aprioriPosVar(0, 0) <= 0 || aprioriPos.isZero()) + { + R.row(index).setZero(); + R.col(index).setZero(); + + used = false; + } + + if (key.num != 0) + { + // deal with all position dimensions at same time when (num == 0) + continue; + } + + BOOST_LOG_TRIVIAL(debug) << "\n" + << str << "Noises" << "\n" + << aprioriPosVar << "\n" + << filterVar; + + // get all of the position elements for this thing + Vector3d filterPos = Vector3d::Zero(); + Vector3d filterVel = Vector3d::Zero(); + + for (int i = 0; i < 3; i++) + { + KFKey kfKey = key; + kfKey.num = i; + kfStateStations.getKFValue(kfKey, filterPos(i)); + } + + if (key.type == KF::ORBIT) + for (int i = 0; i < 3; i++) + { + KFKey kfKey = key; + kfKey.num = i + 3; + kfStateStations.getKFValue(kfKey, filterVel(i)); + } + + Vector3d dRdX; + Vector3d dRdY; + Vector3d dRdZ; + Vector3d dRdT; + Vector3d dRdThetaX; + Vector3d dRdThetaY; + Vector3d dRdThetaZ; + + if (key.type == KF::REC_POS) + { + dRdX = xUnitEcef; + dRdY = yUnitEcef; + dRdZ = zUnitEcef; + dRdT = filterVel; + dRdThetaX = aprioriPos.cross(xUnitEcef); + dRdThetaY = aprioriPos.cross(yUnitEcef); + dRdThetaZ = aprioriPos.cross(zUnitEcef); + } + else if (key.type == KF::ORBIT) + { + dRdX = xUnitEci; + dRdY = yUnitEci; + dRdZ = zUnitEci; + dRdT = filterVel; + dRdThetaX = aprioriPos.cross(xUnitEci); + dRdThetaY = aprioriPos.cross(yUnitEci); + dRdThetaZ = aprioriPos.cross(zUnitEci); + } + + Vector3d deltaR = filterPos - aprioriPos; + + if (acsConfig.minconOpts.full_vcv == false) + { + R.middleRows(index, 3).setZero(); + R.middleCols(index, 3).setZero(); + } + + if (acsConfig.minconOpts.full_vcv == false && used) + { + R.block(index, index, 3, 3) = noise; + } + + auto addRate = [&](const InitialState& rateInit, const KFKey& key) + { + if (rateInit.estimate == false) + return; + + KFKey rateKey = key; + rateKey.type = static_cast( + static_cast(rateKey.type) + + (static_cast(KF::XFORM_XLATE_RATE) - static_cast(KF::XFORM_XLATE)) + ); + rateKey.comment += "/DAY"; + + kfStateTrans.setKFTransRate(key, rateKey, 1 / S_IN_DAY, rateInit); + }; + + for (short xyz = 0; xyz < 3; xyz++) + { + KFKey obsKey; + obsKey.str = str; + obsKey.num = xyz; + obsKey.comment = "MINCON"; + + KFMeasEntry meas(&kfStateTrans, obsKey); + + if (xlateInit.estimate) + { + { + KFKey key{KF::XFORM_XLATE, {}, "", 0, "M"}; + meas.addDsgnEntry(key, dRdX(xyz), xlateInit); + addRate(xlateRateInit, key); + } + { + KFKey key{KF::XFORM_XLATE, {}, "", 1, "M"}; + meas.addDsgnEntry(key, dRdY(xyz), xlateInit); + addRate(xlateRateInit, key); + } + { + KFKey key{KF::XFORM_XLATE, {}, "", 2, "M"}; + meas.addDsgnEntry(key, dRdZ(xyz), xlateInit); + addRate(xlateRateInit, key); + } + } + + if (rtateInit.estimate) + { + { + KFKey key{KF::XFORM_RTATE, {}, "", 0, "MAS"}; + meas.addDsgnEntry(key, dRdThetaX(xyz) * MAS2R, rtateInit); + addRate(rtateRateInit, key); + } + { + KFKey key{KF::XFORM_RTATE, {}, "", 1, "MAS"}; + meas.addDsgnEntry(key, dRdThetaY(xyz) * MAS2R, rtateInit); + addRate(rtateRateInit, key); + } + { + KFKey key{KF::XFORM_RTATE, {}, "", 2, "MAS"}; + meas.addDsgnEntry(key, dRdThetaZ(xyz) * MAS2R, rtateInit); + addRate(rtateRateInit, key); + } + } + + if (scaleInit.estimate) + { + { + KFKey key{KF::XFORM_SCALE, {}, "", 0, "PPB"}; + meas.addDsgnEntry(key, aprioriPos(xyz) * 1e-9, scaleInit); + addRate(scaleRateInit, key); + } + } + + if (delayInit.estimate) + { + { + KFKey key{KF::XFORM_DELAY, {}, "", 0, "S"}; + meas.addDsgnEntry(key, dRdT(xyz), delayInit); + addRate(delayRateInit, key); + } + } + + double value = deltaR(xyz); + + meas.setValue(value); + // todo Eugene: set noise + + // Add null measurement and continue, its needed for inverse later + + measList.push_back(meas); + + if (used) + { + int xIndex = index + xyz; + indices.push_back(xIndex); + usedMap[xIndex] = true; + meas.metaDataMap["used_ptr"] = &usedMap[xIndex]; + meas.metaDataMap["otherIndex"] = + (void*)(measList.size() - 1); // need an index into the big measurement matrix + // for deweighting to get applied in 2 places + + measListCulled.push_back(meas); + } + } + } + + // use a state transition to initialise elements + kfStateTrans.stateTransition(trace, kfStateStations.time); + + // std::cout << "\n" << "R" << "\n" << R << "\n"; + + MatrixXd RR = R(indices, indices); + + KFMeas combinedMeas(kfStateTrans, measList, GTime::noTime(), &R); + KFMeas combinedMeasCulled(kfStateTrans, measListCulled, GTime::noTime(), &RR); + + for (auto& metaDataMap : combinedMeasCulled.metaDataMaps) + { + metaDataMap["otherNoiseMatrix_ptr"] = &combinedMeas.R; + } + + if (estimateTransform) + { + string suffix = "/MINCON_TRANSFORM"; + if (kfStateTrans.lsqRequired) + { + trace << "\n------- LEAST SQUARES FOR MINIMUM CONSTRAINTS TRANSFORMATION --------\n"; + + string suffixLsq = suffix + "_LSQ"; + kfStateTrans.leastSquareInitStates(trace, combinedMeasCulled, suffixLsq); + + kfStateTrans.dx = VectorXd::Zero(kfStateTrans.x.rows()); + + kfStateTrans.outputStates(trace, suffixLsq); + } + + trace << "\n------- FILTERING FOR MINIMUM CONSTRAINTS TRANSFORMATION --------\n"; + + kfStateTrans.filterKalman(trace, combinedMeasCulled, suffix); + + kfStateTrans.outputStates(trace, suffix); + + if (kfStateTransform_ptr) + { + *kfStateTransform_ptr = kfStateTrans; + } + + mongoStates( + kfStateTrans, + {.suffix = suffix, + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + } + + // remove any rates before continuing + kfStateTrans = kfStateTrans.getSubState( + {KF::ONE, KF::XFORM_RTATE, KF::XFORM_XLATE, KF::XFORM_DELAY, KF::XFORM_SCALE}, + &combinedMeas + ); + + KFState oldStateStations = kfStateStations; + + // Do kalman filter on original state using pseudomeasurements + MatrixXd K; + VectorXd v; + MatrixXd H; + + auto& P = kfStateStations.P; + + switch (acsConfig.minconOpts.application_mode) + { + case E_Mincon::PSEUDO_OBS: + { + // rename for some semblance of resemblance to the 'bible' + auto& T = combinedMeas.H; + auto& Theta = kfStateTrans.x; + + // create a subset matrix for positions only + vector posIndices; + for (int i = 0; i < T.rows(); i++) + { + if (T.row(i).any()) + { + posIndices.push_back(i); + } + } + + // calculate position deltas and their variances according to transform + VectorXd tTheta = T * Theta; // will correspond to (but not be) pseudoobservations + MatrixXd Omega = + T * kfStateTrans.P * T.transpose(); // will be pseudoobservations variances + + // just get the subset that are positions + tTheta = tTheta(posIndices).eval(); + Omega = Omega(posIndices, posIndices).eval(); + + // standard kalman filter stuff using the ordinary states + // filter using pseudoobs for each position state + + // design matrix is subset of identity matrix + H = MatrixXd::Identity(kfStateStations.x.rows(), kfStateStations.x.rows()); + H = H(posIndices, all).eval(); + + // calculate kalman gain + K = P * H.transpose() * (H * P * H.transpose() + Omega).inverse(); + + // do some algebra to compute the required v values such that a subset of kalman state + // adjustments are the desired dx calculated previously ie, find v + + // H. K. v = tTheta + // Kt. Ht. H. K. v = Kt. Ht. tTheta //normal equations + + // solve A.x = b, where: + // x = v + auto A = K.transpose() * H.transpose() * H * K; + auto b = K.transpose() * H.transpose() * tTheta; + + v = A.ldlt().solve(b); + + if (0) + { + VectorXd errors = tTheta - H * K * v; + std::cout << "\n" + << "tTheta:" << "\n" + << tTheta << "\n"; + std::cout << "\n" + << "H:" << "\n" + << H << "\n"; + std::cout << "\n" + << "errors:" << "\n" + << errors << "\n"; + } + break; + } + case E_Mincon::WEIGHT_MATRIX: + case E_Mincon::VARIANCE_INVERSE: + case E_Mincon::COVARIANCE_INVERSE: + { + int numXform = kfStateTrans.x.rows() - 1; + int numStates = combinedMeas.R.rows(); + + v = kfStateTrans.x.bottomRows(numXform); + + // generalised inverse (Ref:E.3) + MatrixXd T = combinedMeas.H.bottomRightCorner(numStates, numXform); + MatrixXd W = MatrixXd::Zero(numStates, numStates); + + switch (acsConfig.minconOpts.application_mode) + { + case E_Mincon::WEIGHT_MATRIX: + { + for (int i = 0; i < numStates; i++) + { + double val = combinedMeas.R(i, i); + if (val) + { + W(i, i) = 1 / val; + } + } + break; + } + case E_Mincon::VARIANCE_INVERSE: + { + for (auto& [kfKey, index] : kfStateStations.kfIndexMap) + { + if (kfStateStations.P(index, index)) + { + W(index, index) = 1 / kfStateStations.P(index, index); + } + } + break; + } + case E_Mincon::COVARIANCE_INVERSE: + { + vector validP; + for (auto& [kfKey, index] : kfStateStations.kfIndexMap) + { + if (kfStateStations.P(index, index)) + { + validP.push_back(index); + } + } + + MatrixXd inverse = kfStateStations.P(validP, validP).inverse(); + + int row = 0; + for (auto& i : validP) + { + int col = 0; + for (auto& j : validP) + { + W(i, j) = inverse(row, col); + col++; + } + row++; + } + + break; + } + } + + W = ((W + W.transpose()) / 2).eval(); + + // std::cout << "\n" << "P" << "\n" << kfStateStations.P << "\n"; + // std::cout << "\n" << "W_" << "\n" << W << "\n"; + // + // std::cout << "\n" << "T" << "\n" << T << "\n"; + + MatrixXd TW = T.transpose() * W; + + MatrixXd TWT = TW * T; + + // std::cout << "\n" << "TWT" << "\n" << TWT << "\n"; + + auto QQ = TWT.triangularView().transpose(); + LDLT solver; + solver.compute(QQ); + if (solver.info() != Eigen::ComputationInfo::Success) + { + std::cout << "Mincon borked." << "\n"; + return; + } + + H = MatrixXd::Zero(numXform, numStates); + H.rightCols(numStates) = solver.solve(TW); + if (solver.info() != Eigen::ComputationInfo::Success) + { + std::cout << "Mincon borked!" << "\n"; + return; + } + + // std::cout << "\n" << "TWT" << "\n" << TWT << "\n"; + // std::cout << "\n" << "TW" << "\n" << TW << "\n"; + // std::cout << "\n" << "TDash" << "\n" << H << "\n"; + + // calculate kalman gain + K = P * H.transpose() * (H * P * H.transpose() /* + Omega*/).inverse(); + + break; + } + } + + // standard kalman filter things again, but do manually since already have K calculated + { + KFState& kfState = kfStateStations; + + trace << "\n" + << " -------DOING KALMAN FILTER WITH PSEUDO ELEMENTS FOR MINIMUM CONSTRAINTS --------" + << "\n"; + + if (kfState.rts_basename.empty() == false) + { + spitFilterToFile( + kfState, + E_SerialObject::FILTER_MINUS, + kfState.rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + } + + kfState.dx = (K * v).eval(); + kfState.x = (kfState.x - kfState.dx).eval(); + kfState.P = (kfState.P - K * H * P).eval(); + + if (isPositiveSemiDefinite(P) == false) + { + std::cout << "\n" + << "WARNING, NOT PSD"; + } + + if (kfState.rts_basename.empty() == false) + { + kfState.metaDataMap["SKIP_PREV_RTS"] = "TRUE"; + spitFilterToFile( + kfState.metaDataMap, + E_SerialObject::METADATA, + kfState.rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + + spitFilterToFile( + kfState, + E_SerialObject::FILTER_PLUS, + kfState.rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + } + } + + if (outputPrePost) + if (hasStations) + { + minSiteData(trace, oldStateStations, " Pre Constraint", usedMap); + minSiteData(trace, kfStateStations, " Post Constraint", usedMap); + } + + if (outputPrePost) + if (hasSatellites) + { + minOrbitData(trace, oldStateStations, " Pre Constraint", usedMap, frameSwapper); + minOrbitData(trace, kfStateStations, " Post Constraint", usedMap, frameSwapper); + } + + for (auto type : {KF::REC_POS, KF::ORBIT}) + { + if (type == KF::REC_POS && hasStations == false) + continue; + if (type == KF::ORBIT && hasSatellites == false) + continue; + + for (auto before : {true, false}) + for (auto& [key, index] : kfStateStations.kfIndexMap) + { + if (key.type != type) + { + continue; + } + if (key.num != 0) + { + continue; + } + + Vector3d filterPos; + for (int i = 0; i < 3; i++) + { + if (before) + filterPos(i) = oldStateStations.x(index + i); + else + filterPos(i) = kfStateStations.x(index + i); + } + + Vector3d filterVel; + if (type == KF::ORBIT) + for (int i = 0; i < 3; i++) + { + if (before) + filterVel(i) = oldStateStations.x(index + i + 3); + else + filterVel(i) = kfStateStations.x(index + i + 3); + } + + string aggregatedUsed = "Agg-Used"; + string aggregatedAll = "Agg-All"; + string str; + Vector3d aprioriPos = Vector3d::Zero(); + if (type == KF::REC_POS) + { + auto& rec = *key.rec_ptr; + aprioriPos = rec.minconApriori; + str = key.str; + } + else if (type == KF::ORBIT) + { + auto& satNav = nav.satNavMap[key.Sat]; + aprioriPos = satNav.aprioriPos; + str = key.Sat.id(); + } + + Vector3d deltaR = filterPos - aprioriPos; + + Matrix3d E = Matrix3d::Zero(); + + if (type == KF::REC_POS) + { + VectorPos pos = ecef2pos(filterPos); + pos2enu(pos, E.data()); + } + else if (type == KF::ORBIT) + { + E = ecef2rac(filterPos, filterVel); + } + + Vector3d frameResidual = E * deltaR; + + for (auto& rmsStatisticsMap_ptr : {minconStatistics_ptr0, minconStatistics_ptr1}) + for (auto& entry : {aggregatedUsed, aggregatedAll, str}) + { + if (rmsStatisticsMap_ptr == nullptr) + { + continue; + } + + if (entry == aggregatedUsed && usedMap[index] == false) + { + continue; + } + + auto& rmsStatisticsMap = *rmsStatisticsMap_ptr; + + for (int i = 0; i < 4; i++) + { + string component = "3D"; + if (type == KF::REC_POS) + switch (i) + { + case 0: + component = "E"; + break; + case 1: + component = "N"; + break; + case 2: + component = "U"; + break; + } + else if (type == KF::ORBIT) + switch (i) + { + case 0: + component = "R"; + break; + case 1: + component = "T"; + break; + case 2: + component = "N"; + break; + } + + if (i < 3) + rmsStatisticsMap[entry][component][before].sum += + SQR(frameResidual(i)); + else + rmsStatisticsMap[entry][component][before].sum += + frameResidual.squaredNorm(); + + rmsStatisticsMap[entry][component][before].count++; + } + } + } + } + + if (commentSinex) + { + for (auto& [key, index] : kfStateStations.kfIndexMap) + { + if (key.num == 0 && key.type == KF::REC_POS) + { + sinexAddComment( + (string) " Minimum Constraints Stations: " + key.str + + (usedMap[index] ? " used" : " unused") + ); + } + } + + for (auto& [key, index] : kfStateTrans.kfIndexMap) + { + if (key.type == KF::ONE) + continue; + + char line[128]; + snprintf( + line, + sizeof(line), + " Minimum Constraints Transform: %12s:%c %+9f %6s +- %8f", + enum_to_string(key.type).c_str(), + 'X' + key.num, // Eugene: convert num to code? + kfStateTrans.x(index), + key.comment.c_str(), + sqrt(kfStateTrans.P(index, index)) + ); + + sinexAddComment(line); + } + } } -void outputMinconStatistics( - Trace& trace, - MinconStatistics& minconStatistics, - const string& suffix) +void outputMinconStatistics(Trace& trace, MinconStatistics& minconStatistics, const string& suffix) { - Block block(trace, (string)"MINCON STATISTICS" + suffix); - trace << "# Position RMS changed in meters:\n"; - - for (auto& [str, rmsStatistics] : minconStatistics) - for (auto& [component, statistics] : rmsStatistics) - for (auto before : {true, false}) - { - auto& entry = statistics[before]; - double val = sqrt(entry.sum / entry.count); - if (isnan(val)) - { - val = 0; - } - - int lvl = 3; - if (str.length() > 4) - { - lvl = 0; - } - - if (before) tracepdeex(lvl, trace, "& %-10s - from: %12.6f ", str.c_str(), val); - else tracepdeex(lvl, trace, "to: %12.6f in %2s over %5d entries\n", val, component.c_str(), entry.count); - } + Block block(trace, (string) "MINCON STATISTICS" + suffix); + trace << "# Position RMS changed in meters:\n"; + + for (auto& [str, rmsStatistics] : minconStatistics) + for (auto& [component, statistics] : rmsStatistics) + for (auto before : {true, false}) + { + auto& entry = statistics[before]; + double val = sqrt(entry.sum / entry.count); + if (isnan(val)) + { + val = 0; + } + + int lvl = 3; + if (str.length() > 4) + { + lvl = 0; + } + + if (before) + tracepdeex(lvl, trace, "& %-10s - from: %12.6f ", str.c_str(), val); + else + tracepdeex( + lvl, + trace, + "to: %12.6f in %2s over %5d entries\n", + val, + component.c_str(), + entry.count + ); + } } -KFState minconOnly( - Trace& trace, - ReceiverMap& receiverMap) +KFState minconOnly(Trace& trace, ReceiverMap& receiverMap) { - long int startPos = -1; - E_SerialObject type = getFilterTypeFromFile(startPos, acsConfig.mincon_filename); - if (type != +E_SerialObject::FILTER_PLUS) - { - return KFState(); - } + long int startPos = -1; + E_SerialObject type = getFilterTypeFromFile(startPos, acsConfig.mincon_filename); + if (type != E_SerialObject::FILTER_PLUS) + { + return KFState(); + } - trace << "\n" << "Performing minimum constraints using dataset saved to " << acsConfig.mincon_filename << "\n"; + trace << "\n" + << "Performing minimum constraints using dataset saved to " << acsConfig.mincon_filename + << "\n"; - KFState kfState; - bool pass = getFilterObjectFromFile(type, kfState, startPos, acsConfig.mincon_filename); - if (pass == false) - { - return KFState(); - } + KFState kfState; + bool pass = getFilterObjectFromFile(type, kfState, startPos, acsConfig.mincon_filename); + if (pass == false) + { + return KFState(); + } - GTime time = kfState.time; + GTime time = kfState.time; - ERPValues erpv = getErp(nav.erp, time); + ERPValues erpv = getErp(nav.erp, time); - FrameSwapper frameSwapper(time, erpv); + FrameSwapper frameSwapper(time, erpv); - tryPrepareFilterPointers(kfState, receiverMap); + tryPrepareFilterPointers(kfState, receiverMap); - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( key.type != KF::ORBIT - ||key.num != 0) - { - continue; - } + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::ORBIT || key.num != 0) + { + continue; + } - auto& satNav = nav.satNavMap[key.Sat]; + auto& satNav = nav.satNavMap[key.Sat]; - SatPos satPos; - satPos.Sat = key.Sat; - satPos.satNav_ptr = &satNav; + SatPos satPos; + satPos.Sat = key.Sat; + satPos.satNav_ptr = &satNav; - bool pass = satpos(nullStream, time, time, satPos, {E_Source::PRECISE, E_Source::BROADCAST}, E_OffsetType::COM, nav); - if (pass == false) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No sat pos found for " << satPos.Sat.id() << "."; - continue; - } + bool pass = satpos( + nullStream, + time, + time, + satPos, + {E_Source::PRECISE, E_Source::BROADCAST}, + E_OffsetType::COM, + nav + ); + if (pass == false) + { + BOOST_LOG_TRIVIAL(warning) << "No sat pos found for " << satPos.Sat.id() << "."; + continue; + } - satNav.aprioriPos = frameSwapper(satPos.rSatCom); - } + satNav.aprioriPos = frameSwapper(satPos.rSatCom); + } - for (auto& [id, rec] : receiverMap) - { - sinexPerEpochPerStation(nullStream, time, rec); + for (auto& [id, rec] : receiverMap) + { + sinexPerEpochPerStation(nullStream, time, rec); - bool dummy; - selectAprioriSource(nullStream, rec, time, dummy, kfState); + bool dummy; + selectAprioriSource(nullStream, rec, time, dummy, kfState); - rec.minconApriori = rec.aprioriPos; - } + rec.minconApriori = rec.aprioriPos; + } - for (auto& [kfKey, index] : kfState.kfIndexMap) - { - kfState.stateTransitionMap[kfKey][kfKey][0] = 1; - } + for (auto& [kfKey, index] : kfState.kfIndexMap) + { + kfState.stateTransitionMap[kfKey][kfKey][0] = 1; + } - kfState.outputStates(trace, "/UNCONSTRAINED"); + kfState.outputStates(trace, "/UNCONSTRAINED"); - { - MinconStatistics minconStatistics; + { + MinconStatistics minconStatistics; - mincon(trace, kfState, &minconStatistics); + mincon(trace, kfState, &minconStatistics); - outputMinconStatistics(trace, minconStatistics); - } + outputMinconStatistics(trace, minconStatistics); + } - kfState.outputStates(trace, "/CONSTRAINED"); + kfState.outputStates(trace, "/CONSTRAINED"); - exit(0); + exit(0); } diff --git a/src/cpp/pea/minimumConstraints.hpp b/src/cpp/pea/minimumConstraints.hpp index a67f4a738..a2faab1e4 100644 --- a/src/cpp/pea/minimumConstraints.hpp +++ b/src/cpp/pea/minimumConstraints.hpp @@ -1,41 +1,40 @@ - #pragma once -#include #include +#include +#include "common/trace.hpp" -using std::string; using std::map; - -#include "trace.hpp" +using std::string; struct ReceiverMap; struct KFState; struct SumCount { - double sum = 0; - int count = 0; + double sum = 0; + int count = 0; }; struct MinconStatistics : map> { - }; void mincon( - Trace& trace, - KFState& kfStateStations, - MinconStatistics* minconStatistics_ptr0 = nullptr, - MinconStatistics* minconStatistics_ptr1 = nullptr, - bool commentSinex = false, - KFState* kfStateTransform_ptr = nullptr); + Trace& trace, + KFState& kfStateStations, + MinconStatistics* minconStatistics_ptr0 = nullptr, + MinconStatistics* minconStatistics_ptr1 = nullptr, + bool commentSinex = false, + KFState* kfStateTransform_ptr = nullptr, + bool estimateTransform = true, + bool outputPrePost = true +); void outputMinconStatistics( - Trace& trace, - MinconStatistics& minconStatistics, - const string& suffix = ""); + Trace& trace, + MinconStatistics& minconStatistics, + const string& suffix = "" +); -KFState minconOnly( - Trace& trace, - ReceiverMap& receiverMap); +KFState minconOnly(Trace& trace, ReceiverMap& receiverMap); diff --git a/src/cpp/pea/outputs.cpp b/src/cpp/pea/outputs.cpp index d25d50508..f1da1e371 100644 --- a/src/cpp/pea/outputs.cpp +++ b/src/cpp/pea/outputs.cpp @@ -1,913 +1,1252 @@ - // #pragma GCC optimize ("O0") +#include +#include #include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/algebraTrace.hpp" +#include "common/biases.hpp" +#include "common/constants.hpp" +#include "common/cost.hpp" +#include "common/enums.h" +#include "common/fileLog.hpp" +#include "common/gpx.hpp" +#include "common/metaData.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/ntripBroadcast.hpp" +#include "common/orbexWrite.hpp" +#include "common/pos.hpp" +#include "common/receiver.hpp" +#include "common/rinexClkWrite.hpp" +#include "common/rinexNavWrite.hpp" +#include "common/rinexObsWrite.hpp" +#include "common/rtsSmoothing.hpp" +#include "common/sinex.hpp" +#include "common/sp3Write.hpp" +#include "common/streamCustom.hpp" +#include "common/streamParser.hpp" +#include "common/streamRtcm.hpp" +#include "common/streamUbx.hpp" +#include "common/summary.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/orbitProp.hpp" +#include "pea/inputsOutputs.hpp" +#include "pea/minimumConstraints.hpp" +#include "sbas/sbas.hpp" + +using boost::date_time::not_a_date_time; +using std::max; +using std::this_thread::sleep_for; Output Outputs__() { - DOCS_REFERENCE(Trace_Files__); - DOCS_REFERENCE(IGS_Files__); - DOCS_REFERENCE(GPX__); - DOCS_REFERENCE(JSON__); - DOCS_REFERENCE(COST__); - DOCS_REFERENCE(RTCM__); - DOCS_REFERENCE(POS__); - DOCS_REFERENCE(Mongo_Database__); + DOCS_REFERENCE(Trace_Files__); + DOCS_REFERENCE(IGS_Files__); + DOCS_REFERENCE(GPX__); + DOCS_REFERENCE(JSON__); + DOCS_REFERENCE(COST__); + DOCS_REFERENCE(RTCM__); + DOCS_REFERENCE(POS__); + DOCS_REFERENCE(Mongo_Database__); } - -#include -#include - -using std::this_thread::sleep_for; - - -#include "interactiveTerminal.hpp" -#include "minimumConstraints.hpp" -#include "rinexObsWrite.hpp" -#include "ntripBroadcast.hpp" -#include "inputsOutputs.hpp" -#include "rinexNavWrite.hpp" -#include "rinexObsWrite.hpp" -#include "rinexClkWrite.hpp" -#include "algebraTrace.hpp" -#include "rtsSmoothing.hpp" -#include "streamCustom.hpp" -#include "streamParser.hpp" -#include "navigation.hpp" -#include "orbexWrite.hpp" -#include "mongoWrite.hpp" -#include "streamRtcm.hpp" -#include "acsConfig.hpp" -#include "streamUbx.hpp" -#include "orbitProp.hpp" -#include "ionoModel.hpp" -#include "constants.hpp" -#include "sp3Write.hpp" -#include "metaData.hpp" -#include "receiver.hpp" -#include "summary.hpp" -#include "fileLog.hpp" -#include "biases.hpp" -#include "sinex.hpp" -#include "cost.hpp" -#include "sbas.hpp" -#include "enums.h" -#include "gpx.hpp" -#include "pos.hpp" - - /** Replace macros for times with numeric values. -* Available replacements are "
    " -*/ + * Available replacements are "
    " + */ void replaceTimes( - string& str, ///< String to replace macros within - boost::posix_time::ptime time) ///< Time to use for replacements + string& str, ///< String to replace macros within + boost::posix_time::ptime time ///< Time to use for replacements +) { - string DDD; - string D; - string WWWW; - string YYYY; - string YY; - string MM; - string DD; - string HH; - string mm; - - if (!time.is_not_a_date_time()) - { - string gpsWeek0 = "1980-01-06 00:00:00.000"; - auto gpsZero = boost::posix_time::time_from_string(gpsWeek0); - string time_string = boost::posix_time::to_iso_string(time); - - auto tm = to_tm(time); - std::ostringstream ss; - ss << std::setw(3) << std::setfill('0') << tm.tm_yday+1; - string ddd = ss.str(); - - auto gpsWeek = (time - gpsZero); - int weeks = gpsWeek.hours() / 24 / 7; - ss.str(""); - ss << std::setw(4) << std::setfill('0') << weeks; - string wwww = ss.str(); - - DDD = ddd; - D = std::to_string(tm.tm_wday); - WWWW = wwww; - YYYY = time_string.substr(0, 4); - YY = time_string.substr(2, 2); - MM = time_string.substr(4, 2); - DD = time_string.substr(6, 2); - HH = time_string.substr(9, 2); - mm = time_string.substr(11, 2); - } - - bool replaced = false; - - replaced |= replaceString(str, "", "--
    _:", false); - replaced |= replaceString(str, "", DDD, false); - replaced |= replaceString(str, "", D, false); - replaced |= replaceString(str, "", WWWW, false); - replaced |= replaceString(str, "", YYYY, false); - replaced |= replaceString(str, "", YY, false); - replaced |= replaceString(str, "", MM, false); - replaced |= replaceString(str, "
    ", DD, false); - replaced |= replaceString(str, "", HH, false); - replaced |= replaceString(str, "", HH, false); - replaced |= replaceString(str, "", mm, false); - - if ( YY.empty() - && replaced) - { - //replacing with nothing here may cause issues - kill the entire string to prevent damage - str = ""; - } + string DDD; + string D; + string WWWW; + string YYYY; + string YY; + string MM; + string DD; + string HH; + string mm; + + if (time.is_not_a_date_time() == false) + { + string gpsWeek0 = "1980-01-06 00:00:00.000"; + auto gpsZero = boost::posix_time::time_from_string(gpsWeek0); + string time_string = boost::posix_time::to_iso_string(time); + + auto tm = to_tm(time); + std::ostringstream ss; + ss << std::setw(3) << std::setfill('0') << tm.tm_yday + 1; + string ddd = ss.str(); + + auto gpsWeek = (time - gpsZero); + int weeks = gpsWeek.hours() / 24 / 7; + ss.str(""); + ss << std::setw(4) << std::setfill('0') << weeks; + string wwww = ss.str(); + + DDD = ddd; + D = std::to_string(tm.tm_wday); + WWWW = wwww; + YYYY = time_string.substr(0, 4); + YY = time_string.substr(2, 2); + MM = time_string.substr(4, 2); + DD = time_string.substr(6, 2); + HH = time_string.substr(9, 2); + mm = time_string.substr(11, 2); + } + + string origStr = str; + bool replaced = false; + + replaced |= replaceString(str, "", "--
    _:", false); + replaced |= replaceString(str, "", DDD, false); + replaced |= replaceString(str, "", D, false); + replaced |= replaceString(str, "", WWWW, false); + replaced |= replaceString(str, "", YYYY, false); + replaced |= replaceString(str, "", YY, false); + replaced |= replaceString(str, "", MM, false); + replaced |= replaceString(str, "
    ", DD, false); + replaced |= replaceString(str, "", HH, false); + replaced |= replaceString(str, "", HH, false); + replaced |= replaceString(str, "", mm, false); + + if (YY.empty() && replaced) + { + // replacing with nothing here may cause issues - kill the entire string to prevent damage + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": time to replace with is invalid, setting " + << origStr << " to empty string"; + + str = ""; + } } -void replaceTimes( - vector& strs, - boost::posix_time::ptime time) +void replaceTimes(vector& strs, boost::posix_time::ptime time) { - for (auto& str : strs) - { - replaceTimes(str, time); - } + for (auto& str : strs) + { + replaceTimes(str, time); + } } /** Create directories if required -*/ -void createDirectories( - boost::posix_time::ptime logptime) + */ +void createDirectories(boost::posix_time::ptime logptime) { - // Ensure the output directories exist - for (auto directory : { - acsConfig.sp3_directory, - acsConfig.erp_directory, - acsConfig.gpx_directory, - acsConfig.pos_directory, - acsConfig.ems_directory, - acsConfig.log_directory, - acsConfig.cost_directory, - acsConfig.ionex_directory, - acsConfig.orbex_directory, - acsConfig.sinex_directory, - acsConfig.trace_directory, - acsConfig.clocks_directory, - acsConfig.slr_obs_directory, - acsConfig.ionstec_directory, - acsConfig.rtcm_nav_directory, - acsConfig.rtcm_obs_directory, - acsConfig.orbit_ics_directory, - acsConfig.rinex_obs_directory, - acsConfig.rinex_nav_directory, - acsConfig.raw_custom_directory, - acsConfig.trop_sinex_directory, - acsConfig.bias_sinex_directory, - acsConfig.pppOpts.rts_directory, - acsConfig.decoded_rtcm_json_directory, - acsConfig.encoded_rtcm_json_directory, - acsConfig.network_statistics_json_directory - }) - { - replaceTimes(directory, logptime); - - if (directory == ".") continue; - if (directory == "./") continue; - if (directory.empty()) continue; - - try - { - std::filesystem::create_directories(directory); - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) << "Error: Could not create directory: \"" << directory << "\""; - } - } + // Ensure the output directories exist + for (auto directory : + {acsConfig.sp3_directory, + acsConfig.erp_directory, + acsConfig.gpx_directory, + acsConfig.pos_directory, + acsConfig.ems_directory, + acsConfig.log_directory, + acsConfig.cost_directory, + acsConfig.ionex_directory, + acsConfig.orbex_directory, + acsConfig.sinex_directory, + acsConfig.trace_directory, + acsConfig.clocks_directory, + acsConfig.slr_obs_directory, + acsConfig.ionstec_directory, + acsConfig.rtcm_nav_directory, + acsConfig.rtcm_obs_directory, + acsConfig.orbit_ics_directory, + acsConfig.rinex_obs_directory, + acsConfig.rinex_nav_directory, + acsConfig.raw_custom_directory, + acsConfig.trop_sinex_directory, + acsConfig.bias_sinex_directory, + acsConfig.pppOpts.rts_directory, + acsConfig.decoded_rtcm_json_directory, + acsConfig.encoded_rtcm_json_directory, + acsConfig.network_statistics_json_directory}) + { + replaceTimes(directory, logptime); + + if (directory == ".") + continue; + if (directory == "./") + continue; + if (directory.empty()) + continue; + + try + { + std::filesystem::create_directories(directory); + } + catch (...) + { + BOOST_LOG_TRIVIAL(error) << "Could not create directory: \"" << directory << "\""; + } + } } map fileNames; /** Create new empty trace files only when required when the filename is changed -*/ -void createTracefiles( - ReceiverMap& receiverMap, - Network& pppNet, - Network& ionNet) + */ +void createTracefiles(ReceiverMap& receiverMap, Network& pppNet, Network& ionNet) { - boost::posix_time::ptime logptime = currentLogptime(); - createDirectories(logptime); - - startNewMongoDb("PRIMARY", logptime, acsConfig.mongoOpts[E_Mongo::PRIMARY] .database, E_Mongo::PRIMARY); - startNewMongoDb("SECONDARY", logptime, acsConfig.mongoOpts[E_Mongo::SECONDARY] .database, E_Mongo::SECONDARY); - - auto insertSuffix = [](string str, string suffix) - { - auto pos = str.find_last_of('.'); - if (pos == string::npos) - { - return str + suffix; - } - return str.substr(0, pos) + suffix + str.substr(pos); - }; - - for (auto rts : {false, true}) - { - if ( rts - &&( acsConfig.process_rts == false - ||acsConfig.pppOpts.rts_lag == 0)) - { - continue; - } - - - string suff; - string metaSuff; - - if (rts) - { - suff = acsConfig.pppOpts.rts_smoothed_suffix; - metaSuff = SMOOTHED_SUFFIX; - - if (acsConfig.process_ppp) - { - bool newTraceFile = createNewTraceFile(pppNet.id, boost::posix_time::not_a_date_time, acsConfig.pppOpts.rts_filename, pppNet.kfState.rts_basename); - - if (newTraceFile) - { - // std::cout << "\n" << "new trace file"; - std::remove((pppNet.kfState.rts_basename ).c_str()); - std::remove((pppNet.kfState.rts_basename + FORWARD_SUFFIX ).c_str()); - std::remove((pppNet.kfState.rts_basename + BACKWARD_SUFFIX ).c_str()); - } - } - - if (acsConfig.process_ionosphere) - { - bool newTraceFile = createNewTraceFile(ionNet.id, boost::posix_time::not_a_date_time, acsConfig.pppOpts.rts_filename, ionNet.kfState.rts_basename); - - if (newTraceFile) - { - // std::cout << "\n" << "new trace file"; - std::remove((ionNet.kfState.rts_basename ).c_str()); - std::remove((ionNet.kfState.rts_basename + FORWARD_SUFFIX ).c_str()); - std::remove((ionNet.kfState.rts_basename + BACKWARD_SUFFIX ).c_str()); - } - } - } - - bool newTraceFile = false; - - for (auto& [Sat, satNav] : nav.satNavMap) - { - if ( acsConfig.output_satellite_trace - && suff.empty()) - { - newTraceFile |= createNewTraceFile(Sat, logptime, insertSuffix(acsConfig.satellite_trace_filename, suff), satNav.traceFilename, true, acsConfig.output_config); - } - } - - for (auto& [id, rec] : receiverMap) - { - if (acsConfig.output_receiver_trace) - { - //dont add suff for this as we dont want smoothed version - newTraceFile |= createNewTraceFile(id, logptime, acsConfig.receiver_trace_filename, rec.metaDataMap[TRACE_FILENAME_STR + metaSuff], true, acsConfig.output_config); - - if (suff.empty()) - { - rec.traceFilename = rec.metaDataMap[TRACE_FILENAME_STR]; - } - } - - if (acsConfig.output_json_trace) - { - //dont add suff for this as we dont want smoothed version - string jsonTraceFilename = acsConfig.receiver_trace_filename; - auto pos = jsonTraceFilename.find_last_of('.'); - if (pos != string::npos) - { - jsonTraceFilename = jsonTraceFilename.substr(0, pos); - } - jsonTraceFilename += ".json"; - - newTraceFile |= createNewTraceFile(id, logptime, jsonTraceFilename, rec.metaDataMap[JSON_FILENAME_STR + metaSuff]); - - if (suff.empty()) - { - rec.jsonTraceFilename = rec.metaDataMap[JSON_FILENAME_STR]; - } - } - - if (acsConfig.output_cost) - { - newTraceFile |= createNewTraceFile(id, logptime, insertSuffix(acsConfig.cost_filename, suff), pppNet.kfState .metaDataMap[COST_FILENAME_STR + id + metaSuff]); - } - - if (acsConfig.output_gpx) - { - newTraceFile |= createNewTraceFile(id, logptime, insertSuffix(acsConfig.gpx_filename, suff), pppNet.kfState .metaDataMap[GPX_FILENAME_STR + id + metaSuff]); - } - - if (acsConfig.output_pos) - { - newTraceFile |= createNewTraceFile(id, logptime, insertSuffix(acsConfig.pos_filename, suff), pppNet.kfState .metaDataMap[POS_FILENAME_STR + id + metaSuff]); - } - } - - if (acsConfig.output_network_trace) - { - newTraceFile |= createNewTraceFile(pppNet.id, logptime, insertSuffix(acsConfig.network_trace_filename, suff), pppNet.kfState.metaDataMap[TRACE_FILENAME_STR + metaSuff], true, acsConfig.output_config); - - if (suff.empty()) - { - pppNet.traceFilename = pppNet.kfState.metaDataMap[TRACE_FILENAME_STR]; - } - } - - if (acsConfig.output_ionosphere_trace) - { - newTraceFile |= createNewTraceFile("IONO", logptime, insertSuffix(acsConfig.ionosphere_trace_filename, suff), ionNet.kfState.metaDataMap[TRACE_FILENAME_STR + metaSuff], true, acsConfig.output_config); - - if (suff.empty()) - { - ionNet.traceFilename = ionNet.kfState.metaDataMap[TRACE_FILENAME_STR]; - } - } - - if (acsConfig.output_ionex) - { - newTraceFile |= createNewTraceFile("", logptime, insertSuffix(acsConfig.ionex_filename, suff), pppNet.kfState.metaDataMap[IONEX_FILENAME_STR + metaSuff]); - } - - if (acsConfig.output_ionstec) - { - newTraceFile |= createNewTraceFile("", logptime, insertSuffix(acsConfig.ionstec_filename, suff), pppNet.kfState.metaDataMap[IONSTEC_FILENAME_STR + metaSuff]); - } - - if (acsConfig.output_trop_sinex) - { - newTraceFile |= createNewTraceFile(pppNet.id, logptime, insertSuffix(acsConfig.trop_sinex_filename, suff), pppNet.kfState.metaDataMap[TROP_FILENAME_STR + metaSuff]); - } - - if (acsConfig.output_bias_sinex) - { - newTraceFile |= createNewTraceFile(pppNet.id, logptime, insertSuffix(acsConfig.bias_sinex_filename, suff), pppNet.kfState.metaDataMap[BSX_FILENAME_STR + metaSuff]); - newTraceFile |= createNewTraceFile(pppNet.id, logptime, insertSuffix(acsConfig.bias_sinex_filename, suff), ionNet.kfState.metaDataMap[BSX_FILENAME_STR + metaSuff]); - } - - if (acsConfig.output_erp) - { - newTraceFile |= createNewTraceFile(pppNet.id, logptime, insertSuffix(acsConfig.erp_filename, suff), pppNet.kfState.metaDataMap[ERP_FILENAME_STR + metaSuff]); - } - - if (acsConfig.output_clocks) - { - auto singleFilenameMap = getSysOutputFilenames(acsConfig.clocks_filename, tsync, false); - auto filenameMap = getSysOutputFilenames(acsConfig.clocks_filename, tsync); - for (auto& [filename, dummy] : filenameMap) - { - newTraceFile |= createNewTraceFile(pppNet.id, logptime, insertSuffix(filename, suff), fileNames[filename + metaSuff]); - } - - pppNet.kfState.metaDataMap[CLK_FILENAME_STR + metaSuff] = insertSuffix(singleFilenameMap.begin()->first, suff); - } - - if (acsConfig.output_sp3) - { - auto singleFilenameMap = getSysOutputFilenames(acsConfig.sp3_filename, tsync, false); - auto filenameMap = getSysOutputFilenames(acsConfig.sp3_filename, tsync); - for (auto& [filename, dummy] : filenameMap) - { - newTraceFile |= createNewTraceFile(pppNet.id, logptime, insertSuffix(filename, suff), fileNames[filename + metaSuff]); - } - - pppNet.kfState.metaDataMap[SP3_FILENAME_STR + metaSuff] = insertSuffix(singleFilenameMap.begin()->first, suff); - } - - if (acsConfig.output_orbex) - { - auto singleFilenameMap = getSysOutputFilenames(acsConfig.orbex_filename, tsync, false); - auto filenameMap = getSysOutputFilenames(acsConfig.orbex_filename, tsync); - for (auto& [filename, dummy] : filenameMap) - { - newTraceFile |= createNewTraceFile(pppNet.id, logptime, insertSuffix(filename, suff), fileNames[filename + metaSuff]); - } - - pppNet.kfState.metaDataMap[ORBEX_FILENAME_STR + metaSuff] = insertSuffix(singleFilenameMap.begin()->first, suff); - } - - if (acsConfig.output_sbas_ems) - { - newTraceFile |= createNewTraceFile("", logptime, acsConfig.ems_filename, pppNet.kfState.metaDataMap[EMS_FILENAME_STR]); - } - - if ( rts - && newTraceFile) - { - spitFilterToFile(pppNet.kfState.metaDataMap, E_SerialObject::METADATA, pppNet.kfState.rts_basename + FORWARD_SUFFIX, acsConfig.pppOpts.queue_rts_outputs); - } - } - - if (acsConfig.output_log) - { - createNewTraceFile("", logptime, acsConfig.log_filename, FileLog::path_log); - } - - if (acsConfig.output_ntrip_log) - { - for (auto& [id, stream_ptr] : ntripBroadcaster.ntripUploadStreams) - { - auto& stream = *stream_ptr; - - createNewTraceFile(id, logptime, acsConfig.ntrip_log_filename, stream.networkTraceFilename); - } - - for (auto& [id, streamParser_ptr] : streamParserMultimap) - try - { - auto& ntripStream = dynamic_cast(streamParser_ptr->stream); - - createNewTraceFile(id, logptime, acsConfig.ntrip_log_filename, ntripStream.networkTraceFilename); - } - catch(std::bad_cast& e){/* Ignore expected bad casts for different types */} - } - - if (acsConfig.output_rinex_obs) - for (auto& [id, rec] : receiverMap) - { - auto filenameMap = getSysOutputFilenames(acsConfig.rinex_obs_filename, tsync, true, id); - for (auto& [filename, dummy] : filenameMap) - { - createNewTraceFile(id, logptime, filename, fileNames[filename]); - } - } - - if (acsConfig.output_rinex_nav) - { - auto filenameMap = getSysOutputFilenames(acsConfig.rinex_nav_filename, tsync); - for (auto& [filename, dummy] : filenameMap) - { - createNewTraceFile("Navs", logptime, filename, fileNames[filename]); - } - } - - for (auto& [id, streamParser_ptr] : streamParserMultimap) - try - { - auto& rtcmParser = dynamic_cast(streamParser_ptr->parser); - - if (acsConfig.output_decoded_rtcm_json) - { - string filename = acsConfig.decoded_rtcm_json_filename; - - replaceString(filename, "", rtcmParser.rtcmMountpoint); - - createNewTraceFile(id, logptime, filename, rtcmParser.rtcmTraceFilename); - } - - for (auto nav : {false, true}) - { - bool isNav = true; - try - { - auto& obsStream = dynamic_cast(*streamParser_ptr); - - isNav = false; - } - catch(std::bad_cast& e){/* Ignore expected bad casts for different types */} - - if ( (acsConfig.record_rtcm_nav && isNav == true && nav == true) - ||(acsConfig.record_rtcm_obs && isNav == false && nav == false)) - { - string filename; - - if (nav) filename = acsConfig.rtcm_nav_filename; - else filename = acsConfig.rtcm_obs_filename; - - replaceString(filename, "", rtcmParser.rtcmMountpoint); - - createNewTraceFile(id, logptime, filename, rtcmParser.recordFilename); - } - } - } - catch(std::bad_cast& e){/* Ignore expected bad casts for different types */} - - for (auto& [id, streamParser_ptr] : streamParserMultimap) - try - { - auto& ubxParser = dynamic_cast(streamParser_ptr->parser); - - if (acsConfig.record_raw_ubx) - { - string filename = acsConfig.raw_ubx_filename; - - createNewTraceFile(id, logptime, filename, ubxParser.raw_ubx_filename); - } - } - catch(std::bad_cast& e){/* Ignore expected bad casts for different types */} - - for (auto& [id, streamParser_ptr] : streamParserMultimap) - try - { - auto& customParser = dynamic_cast(streamParser_ptr->parser); - - if (acsConfig.record_raw_custom) - { - string filename = acsConfig.raw_custom_filename; - - createNewTraceFile(id, logptime, filename, customParser.raw_custom_filename); - } - } - catch(std::bad_cast& e){/* Ignore expected bad casts for different types */} + boost::posix_time::ptime logptime = currentLogptime(); + createDirectories(logptime); + + startNewMongoDb( + "PRIMARY", + logptime, + acsConfig.mongoOpts[static_cast(E_Mongo::PRIMARY)].database, + E_Mongo::PRIMARY + ); + startNewMongoDb( + "SECONDARY", + logptime, + acsConfig.mongoOpts[static_cast(E_Mongo::SECONDARY)].database, + E_Mongo::SECONDARY + ); + + auto insertSuffix = [](string str, string suffix) + { + auto pos = str.find_last_of('.'); + if (pos == string::npos) + { + return str + suffix; + } + return str.substr(0, pos) + suffix + str.substr(pos); + }; + + for (auto rts : {false, true}) + { + if (rts && (acsConfig.process_rts == false || acsConfig.pppOpts.rts_lag == 0)) + { + continue; + } + + string suff; + string metaSuff; + + if (rts) + { + suff = acsConfig.pppOpts.rts_smoothed_suffix; + metaSuff = SMOOTHED_SUFFIX; + + if (acsConfig.process_ppp) + { + bool newTraceFile = createNewTraceFile( + pppNet.id, + "Network", + not_a_date_time, + acsConfig.pppOpts.rts_filename, + pppNet.kfState.rts_basename + ); + + if (newTraceFile) + { + // std::cout << "\n" << "new trace file"; + std::remove((pppNet.kfState.rts_basename).c_str()); + std::remove((pppNet.kfState.rts_basename + FORWARD_SUFFIX).c_str()); + std::remove((pppNet.kfState.rts_basename + BACKWARD_SUFFIX).c_str()); + } + } + + if (acsConfig.process_ionosphere) + { + bool newTraceFile = createNewTraceFile( + ionNet.id, + "Network", + not_a_date_time, + acsConfig.pppOpts.rts_filename, + ionNet.kfState.rts_basename + ); + + if (newTraceFile) + { + // std::cout << "\n" << "new trace file"; + std::remove((ionNet.kfState.rts_basename).c_str()); + std::remove((ionNet.kfState.rts_basename + FORWARD_SUFFIX).c_str()); + std::remove((ionNet.kfState.rts_basename + BACKWARD_SUFFIX).c_str()); + } + } + } + + bool newTraceFile = false; + + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (acsConfig.output_satellite_trace) + if (rts == false) + { + newTraceFile |= createNewTraceFile( + Sat, + "Sats", + logptime, + acsConfig.satellite_trace_filename, + satNav.traceFilename, + true, + acsConfig.output_config + ); + } + } + + for (auto& [id, rec] : receiverMap) + { + if (acsConfig.output_receiver_trace) + if (rts == false) + { + newTraceFile |= createNewTraceFile( + id, + rec.source, + logptime, + acsConfig.receiver_trace_filename, + rec.traceFilename, + true, + acsConfig.output_config + ); + } + + if (acsConfig.output_json_trace) + if (rts == false) + { + newTraceFile |= createNewTraceFile( + id, + rec.source, + logptime, + acsConfig.receiver_json_filename, + rec.jsonTraceFilename + ); + } + + if (acsConfig.output_cost) + { + newTraceFile |= createNewTraceFile( + id, + rec.source, + logptime, + insertSuffix(acsConfig.cost_filename, suff), + pppNet.kfState.metaDataMap[COST_FILENAME_STR + id + metaSuff] + ); + } + + if (acsConfig.output_gpx) + { + newTraceFile |= createNewTraceFile( + id, + rec.source, + logptime, + insertSuffix(acsConfig.gpx_filename, suff), + pppNet.kfState.metaDataMap[GPX_FILENAME_STR + id + metaSuff] + ); + } + + if (acsConfig.output_pos) + { + newTraceFile |= createNewTraceFile( + id, + rec.source, + logptime, + insertSuffix(acsConfig.pos_filename, suff), + pppNet.kfState.metaDataMap[POS_FILENAME_STR + id + metaSuff] + ); + } + + if (acsConfig.output_spp && rts == false) + { + newTraceFile |= createNewTraceFile( + id, + rec.source, + logptime, + acsConfig.spp_filename, + rec.sppOutputFile + ); + } + } + + if (acsConfig.output_network_trace) + { + newTraceFile |= createNewTraceFile( + pppNet.id, + "Network", + logptime, + insertSuffix(acsConfig.network_trace_filename, suff), + pppNet.kfState.metaDataMap[TRACE_FILENAME_STR + metaSuff], + true, + acsConfig.output_config + ); + + if (suff.empty()) + { + pppNet.traceFilename = pppNet.kfState.metaDataMap[TRACE_FILENAME_STR]; + } + } + + if (acsConfig.output_ionosphere_trace) + { + newTraceFile |= createNewTraceFile( + "IONO", + "Network", + logptime, + insertSuffix(acsConfig.ionosphere_trace_filename, suff), + ionNet.kfState.metaDataMap[TRACE_FILENAME_STR + metaSuff], + true, + acsConfig.output_config + ); + + if (suff.empty()) + { + ionNet.traceFilename = ionNet.kfState.metaDataMap[TRACE_FILENAME_STR]; + } + } + + if (acsConfig.output_ionex) + { + newTraceFile |= createNewTraceFile( + "", + "Network", + logptime, + insertSuffix(acsConfig.ionex_filename, suff), + pppNet.kfState.metaDataMap[IONEX_FILENAME_STR + metaSuff] + ); + } + + if (acsConfig.output_ionstec) + { + newTraceFile |= createNewTraceFile( + "", + "Network", + logptime, + insertSuffix(acsConfig.ionstec_filename, suff), + pppNet.kfState.metaDataMap[IONSTEC_FILENAME_STR + metaSuff] + ); + } + + if (acsConfig.output_trop_sinex) + { + newTraceFile |= createNewTraceFile( + pppNet.id, + "Network", + logptime, + insertSuffix(acsConfig.trop_sinex_filename, suff), + pppNet.kfState.metaDataMap[TROP_FILENAME_STR + metaSuff] + ); + } + + if (acsConfig.output_bias_sinex) + { + newTraceFile |= createNewTraceFile( + pppNet.id, + "Network", + logptime, + insertSuffix(acsConfig.bias_sinex_filename, suff), + pppNet.kfState.metaDataMap[BSX_FILENAME_STR + metaSuff] + ); + newTraceFile |= createNewTraceFile( + pppNet.id, + "Network", + logptime, + insertSuffix(acsConfig.bias_sinex_filename, suff), + ionNet.kfState.metaDataMap[BSX_FILENAME_STR + metaSuff] + ); + } + + if (acsConfig.output_erp) + { + newTraceFile |= createNewTraceFile( + pppNet.id, + "Network", + logptime, + insertSuffix(acsConfig.erp_filename, suff), + pppNet.kfState.metaDataMap[ERP_FILENAME_STR + metaSuff] + ); + } + + if (acsConfig.output_clocks) + { + auto singleFilenameMap = getSysOutputFilenames(acsConfig.clocks_filename, tsync, false); + auto filenameMap = getSysOutputFilenames(acsConfig.clocks_filename, tsync); + for (auto& [filename, dummy] : filenameMap) + { + newTraceFile |= createNewTraceFile( + pppNet.id, + "Network", + logptime, + insertSuffix(filename, suff), + fileNames[filename + metaSuff] + ); + } + + pppNet.kfState.metaDataMap[CLK_FILENAME_STR + metaSuff] = + insertSuffix(singleFilenameMap.begin()->first, suff); + } + + if (acsConfig.output_sp3) + { + auto singleFilenameMap = getSysOutputFilenames(acsConfig.sp3_filename, tsync, false); + auto filenameMap = getSysOutputFilenames(acsConfig.sp3_filename, tsync); + for (auto& [filename, dummy] : filenameMap) + { + newTraceFile |= createNewTraceFile( + pppNet.id, + "Network", + logptime, + insertSuffix(filename, suff), + fileNames[filename + metaSuff] + ); + } + + pppNet.kfState.metaDataMap[SP3_FILENAME_STR + metaSuff] = + insertSuffix(singleFilenameMap.begin()->first, suff); + } + + if (acsConfig.output_orbex) + { + auto singleFilenameMap = getSysOutputFilenames(acsConfig.orbex_filename, tsync, false); + auto filenameMap = getSysOutputFilenames(acsConfig.orbex_filename, tsync); + for (auto& [filename, dummy] : filenameMap) + { + newTraceFile |= createNewTraceFile( + pppNet.id, + "Network", + logptime, + insertSuffix(filename, suff), + fileNames[filename + metaSuff] + ); + } + + pppNet.kfState.metaDataMap[ORBEX_FILENAME_STR + metaSuff] = + insertSuffix(singleFilenameMap.begin()->first, suff); + } + + if (acsConfig.output_sbas_ems) + { + newTraceFile |= createNewTraceFile( + "", + "Network", + logptime, + acsConfig.ems_filename, + pppNet.kfState.metaDataMap[EMS_FILENAME_STR] + ); + } + + if (rts && newTraceFile) + { + spitFilterToFile( + pppNet.kfState.metaDataMap, + E_SerialObject::METADATA, + pppNet.kfState.rts_basename + FORWARD_SUFFIX, + acsConfig.pppOpts.queue_rts_outputs + ); + } + } + + if (acsConfig.output_log) + { + createNewTraceFile("", "Network", logptime, acsConfig.log_filename, FileLog::path_log); + } + + if (acsConfig.output_ntrip_log) + { + for (auto& [id, stream_ptr] : ntripBroadcaster.ntripUploadStreams) + { + auto& stream = *stream_ptr; + + createNewTraceFile( + id, + "NTRIP", + logptime, + acsConfig.ntrip_log_filename, + stream.networkTraceFilename + ); + } + + for (auto& [id, streamParser_ptr] : streamParserMultimap) + try + { + auto& ntripStream = dynamic_cast(streamParser_ptr->stream); + + createNewTraceFile( + id, + "NTRIP", + logptime, + acsConfig.ntrip_log_filename, + ntripStream.networkTraceFilename + ); + } + catch (std::bad_cast& e) + { /* Ignore expected bad casts for different types */ + } + } + + if (acsConfig.output_rinex_obs) + for (auto& [id, rec] : receiverMap) + { + auto filenameMap = getSysOutputFilenames(acsConfig.rinex_obs_filename, tsync, true, id); + for (auto& [filename, dummy] : filenameMap) + { + createNewTraceFile(id, rec.source, logptime, filename, fileNames[filename]); + } + } + + if (acsConfig.output_rinex_nav) + { + auto filenameMap = getSysOutputFilenames(acsConfig.rinex_nav_filename, tsync); + for (auto& [filename, dummy] : filenameMap) + { + createNewTraceFile("Navs", "Network", logptime, filename, fileNames[filename]); + } + } + + for (auto& [id, streamParser_ptr] : streamParserMultimap) + try + { + auto& rtcmParser = dynamic_cast(streamParser_ptr->parser); + + if (acsConfig.output_decoded_rtcm_json) + { + createNewTraceFile( + id, + rtcmParser.rtcmMountpoint, + logptime, + acsConfig.decoded_rtcm_json_filename, + rtcmParser.rtcmTraceFilename + ); + } + + for (auto nav : {false, true}) + { + bool isNav = true; + try + { + auto& obsStream = dynamic_cast(*streamParser_ptr); + + isNav = false; + } + catch (std::bad_cast& e) + { /* Ignore expected bad casts for different types */ + } + + if ((acsConfig.record_rtcm_nav && isNav == true && nav == true) || + (acsConfig.record_rtcm_obs && isNav == false && nav == false)) + { + string filename; + + if (nav) + filename = acsConfig.rtcm_nav_filename; + else + filename = acsConfig.rtcm_obs_filename; + + createNewTraceFile( + id, + rtcmParser.rtcmMountpoint, + logptime, + filename, + rtcmParser.recordFilename + ); + } + } + } + catch (std::bad_cast& e) + { /* Ignore expected bad casts for different types */ + } + + for (auto& [id, streamParser_ptr] : streamParserMultimap) + try + { + auto& ubxParser = dynamic_cast(streamParser_ptr->parser); + + if (acsConfig.record_raw_ubx) + { + createNewTraceFile( + id, + streamParser_ptr->stream.sourceString, + logptime, + acsConfig.raw_ubx_filename, + ubxParser.raw_ubx_filename + ); + } + } + catch (std::bad_cast& e) + { /* Ignore expected bad casts for different types */ + } + + for (auto& [id, streamParser_ptr] : streamParserMultimap) + try + { + auto& customParser = dynamic_cast(streamParser_ptr->parser); + + if (acsConfig.record_raw_custom) + { + createNewTraceFile( + id, + streamParser_ptr->stream.sourceString, + logptime, + acsConfig.raw_custom_filename, + customParser.raw_custom_filename + ); + } + } + catch (std::bad_cast& e) + { /* Ignore expected bad casts for different types */ + } } - -void outputPredictedStates( - Trace& trace, - KFState& kfState) +void outputPredictedStates(Trace& trace, KFState& kfState) { - if (acsConfig.mongoOpts.output_predictions == +E_Mongo::NONE) - { - return; - } + if (acsConfig.mongoOpts.output_predictions == E_Mongo::NONE) + { + return; + } - InteractiveTerminal::setMode(E_InteractiveMode::PredictingStates); - BOOST_LOG_TRIVIAL(info) << " ------- PREDICTING STATES --------" << "\n"; + BOOST_LOG_TRIVIAL(info) << " ------- PREDICTING STATES --------" << "\n"; - tuple forward = {+1, acsConfig.mongoOpts.forward_prediction_duration}; - tuple reverse = {-1, acsConfig.mongoOpts.reverse_prediction_duration}; + tuple forward = {+1, acsConfig.mongoOpts.forward_prediction_duration}; + tuple reverse = {-1, acsConfig.mongoOpts.reverse_prediction_duration}; - MongoStatesOptions mongoStatesOpts; - mongoStatesOpts.suffix = "/PREDICTED"; - mongoStatesOpts.force = true; - mongoStatesOpts.queue = acsConfig.mongoOpts.queue_outputs; - mongoStatesOpts.instances = acsConfig.mongoOpts.output_predictions; - mongoStatesOpts.updated = tsync; + MongoStatesOptions mongoStatesOpts; + mongoStatesOpts.suffix = "/PREDICTED"; + mongoStatesOpts.force = true; + mongoStatesOpts.queue = acsConfig.mongoOpts.queue_outputs; + mongoStatesOpts.instances = acsConfig.mongoOpts.output_predictions; + mongoStatesOpts.updated = tsync; - for (auto& duo : {forward, reverse}) - { - auto& [sign, duration] = duo; + for (auto& duo : {forward, reverse}) + { + auto& [sign, duration] = duo; - if (duration < 0) - { - continue; - } + if (duration < 0) + { + continue; + } - GTime startTime = tsync + acsConfig.mongoOpts.prediction_offset; - GTime stopTime = tsync + acsConfig.mongoOpts.prediction_offset + sign * duration; - double timeDelta = sign * acsConfig.mongoOpts.prediction_interval; + GTime startTime = tsync + acsConfig.mongoOpts.prediction_offset; + GTime stopTime = tsync + acsConfig.mongoOpts.prediction_offset + sign * duration; + double timeDelta = sign * acsConfig.mongoOpts.prediction_interval; - Orbits orbits = prepareOrbits(trace, kfState); + Orbits orbits = prepareOrbits(trace, kfState); - GTime orbitsTime = tsync; + GTime orbitsTime = tsync; - KFState copyState = kfState; + KFState copyState = kfState; - for (GTime time = startTime; sign * (time - stopTime).to_double() <= 0; time += timeDelta) - { - //remove orbits because they're done separately - for (auto& [kfKey, index] : copyState.kfIndexMap) - { - if (kfKey.type == +KF::ORBIT) - { - copyState.removeState(kfKey); - } - } + for (GTime time = startTime; sign * (time - stopTime).to_double() <= 0; time += timeDelta) + { + // remove orbits because they're done separately + for (auto& [kfKey, index] : copyState.kfIndexMap) + { + if (kfKey.type == KF::ORBIT) + { + copyState.removeState(kfKey); + } + } - copyState.stateTransition(nullStream, time); + copyState.stateTransition(nullStream, time); - auto sent_predictions = acsConfig.mongoOpts.sent_predictions; + auto sent_predictions = acsConfig.mongoOpts.sent_predictions; - auto orbitIt = std::find(sent_predictions.begin(), sent_predictions.end(), +KF::ORBIT); - auto allIt = std::find(sent_predictions.begin(), sent_predictions.end(), +KF::ALL); + auto orbitIt = std::find(sent_predictions.begin(), sent_predictions.end(), KF::ORBIT); + auto allIt = std::find(sent_predictions.begin(), sent_predictions.end(), KF::ALL); - bool doOrbits = orbitIt != sent_predictions.end(); - bool doAll = allIt != sent_predictions.end(); + bool doOrbits = orbitIt != sent_predictions.end(); + bool doAll = allIt != sent_predictions.end(); - if (doOrbits) - sent_predictions.erase(orbitIt); + if (doOrbits) + sent_predictions.erase(orbitIt); - { - KFState subState = copyState.getSubState(sent_predictions); + { + KFState subState = copyState.getSubState(sent_predictions); - mongoStates(subState, mongoStatesOpts); - } + mongoStates(subState, mongoStatesOpts); + } - if ( orbits.empty() == false - &&( doOrbits - ||doAll)) - { - OrbitIntegrator integrator; - integrator.timeInit = orbitsTime; + if (orbits.empty() == false && (doOrbits || doAll)) + { + OrbitIntegrator integrator; + integrator.timeInit = orbitsTime; - double tgap = (time - orbitsTime).to_double(); + double tgap = (time - orbitsTime).to_double(); - integrateOrbits(integrator, orbits, tgap, acsConfig.propagationOptions.integrator_time_step); + integrateOrbits( + integrator, + orbits, + tgap, + acsConfig.propagationOptions.integrator_time_step + ); - BOOST_LOG_TRIVIAL(info) << "Propagated " << tgap << "s to " << time.to_string(); + BOOST_LOG_TRIVIAL(info) << "Propagated " << tgap << "s to " << time.to_string(); - orbitsTime = time; + orbitsTime = time; - KFState propState; - propState.time = time; + KFState propState; + propState.time = time; - int s = 6 * orbits.size(); + int s = 6 * orbits.size(); - propState.x .resize(s); - propState.dx.resize(s); - propState.P .resize(s, s); + propState.x.resize(s); + propState.dx.resize(s); + propState.P.resize(s, s); - int index = 0; - for (int o = 0; o < orbits.size(); o++) - { - auto& orbit = orbits[o]; + int index = 0; + for (int o = 0; o < orbits.size(); o++) + { + auto& orbit = orbits[o]; - for (auto& [key, i] : orbit.subState_ptr->kfIndexMap) - { - if (key.type != KF::ORBIT) - { - continue; - } + for (auto& [key, i] : orbit.subState_ptr->kfIndexMap) + { + if (key.type != KF::ORBIT) + { + continue; + } - if (key.num < 3) propState.x(index) = orbit.pos(i); - else propState.x(index) = orbit.vel(i-3); + if (key.num < 3) + propState.x(index) = orbit.pos(i); + else + propState.x(index) = orbit.vel(i - 3); - propState.P(index, index) = orbit.posVelSTM(i, i); + propState.P(index, index) = orbit.posVelSTM(i, i); - propState.kfIndexMap[key] = index; - index++; - } - } + propState.kfIndexMap[key] = index; + index++; + } + } - mongoStates(propState, mongoStatesOpts); - } + mongoStates(propState, mongoStatesOpts); + } - //update to allow use of just-written values - mongoStatesAvailable(time, mongoStatesOpts); - } - } + // update to allow use of just-written values + mongoStatesAvailable(time, mongoStatesOpts); + } + } } void configureUploadingStreams() { - for (auto& [outLabel, outStreamData] : acsConfig.netOpts.uploadingStreamData) - { - auto it = ntripBroadcaster.ntripUploadStreams.find(outLabel); - - // Create stream if it does not already exist. - if (it == ntripBroadcaster.ntripUploadStreams.end()) - { - auto outStream_ptr = std::make_shared(outStreamData.url); - auto& outStream = *outStream_ptr.get(); - ntripBroadcaster.ntripUploadStreams[outLabel] = std::move(outStream_ptr); - - it = ntripBroadcaster.ntripUploadStreams.find(outLabel); - } - - auto& [label, outStream_ptr] = *it; - auto& outStream = *outStream_ptr; - - outStream.streamConfig.rtcmMsgOptsMap = outStreamData.rtcmMsgOptsMap; - outStream.streamConfig.itrf_datum = outStreamData.itrf_datum; - outStream.streamConfig.provider_id = outStreamData.provider_id; - outStream.streamConfig.solution_id = outStreamData.solution_id; - } - - for (auto it = ntripBroadcaster.ntripUploadStreams.begin(); it != ntripBroadcaster.ntripUploadStreams.end();) - { - if (acsConfig.netOpts.uploadingStreamData.find(it->first) == acsConfig.netOpts.uploadingStreamData.end()) - { - auto& [label, outStream_ptr] = *it; - auto& outStream = *outStream_ptr; - outStream.disconnect(); - it = ntripBroadcaster.ntripUploadStreams.erase(it); - } - else - { - it++; - } - } - - if ( acsConfig.process_ppp == false - &&acsConfig.process_spp == false - &&acsConfig.slrOpts.process_slr == false - &&acsConfig.process_preprocessor == false - &&acsConfig.process_ionosphere == false) - while (1) - { - BOOST_LOG_TRIVIAL(info) << "Running with no processing modes enabled"; - - sleep_for(std::chrono::seconds(10)); - } + for (auto& [outLabel, outStreamData] : acsConfig.netOpts.uploadingStreamData) + { + auto it = ntripBroadcaster.ntripUploadStreams.find(outLabel); + + // Create stream if it does not already exist. + if (it == ntripBroadcaster.ntripUploadStreams.end()) + { + auto outStream_ptr = std::make_shared(outStreamData.url); + auto& outStream = *outStream_ptr.get(); + ntripBroadcaster.ntripUploadStreams[outLabel] = std::move(outStream_ptr); + + it = ntripBroadcaster.ntripUploadStreams.find(outLabel); + } + + auto& [label, outStream_ptr] = *it; + auto& outStream = *outStream_ptr; + + outStream.streamConfig.rtcmMsgOptsMap = outStreamData.rtcmMsgOptsMap; + outStream.streamConfig.itrf_datum = outStreamData.itrf_datum; + outStream.streamConfig.provider_id = outStreamData.provider_id; + outStream.streamConfig.solution_id = outStreamData.solution_id; + } + + for (auto it = ntripBroadcaster.ntripUploadStreams.begin(); + it != ntripBroadcaster.ntripUploadStreams.end();) + { + if (acsConfig.netOpts.uploadingStreamData.find(it->first) == + acsConfig.netOpts.uploadingStreamData.end()) + { + auto& [label, outStream_ptr] = *it; + auto& outStream = *outStream_ptr; + outStream.disconnect(); + it = ntripBroadcaster.ntripUploadStreams.erase(it); + } + else + { + it++; + } + } + + if (acsConfig.process_ppp == false && acsConfig.process_spp == false && + acsConfig.slrOpts.process_slr == false && acsConfig.process_preprocessor == false && + acsConfig.process_ionosphere == false) + while (1) + { + BOOST_LOG_TRIVIAL(info) << "Running with no processing modes enabled"; + + sleep_for(std::chrono::seconds(10)); + } } - - void perEpochPostProcessingAndOutputs( - Trace& pppTrace, - Network& pppNet, - Network& ionNet, - ReceiverMap& receiverMap, - KFState& kfState, - KFState& ionState, - const GTime& time, - bool emptyEpoch) + Trace& pppTrace, + GTime time, + Network& ionNet, + ReceiverMap& receiverMap, + KFState& kfState, + bool emptyEpoch, + bool inRts, + bool firstRtsEpoch +) { - InteractiveTerminal::setMode(E_InteractiveMode::Outputs); - - auto ionTrace = getTraceFile(ionNet); - - if (acsConfig.process_ppp) - { - mongoStates(kfState, - { - .suffix = "/PPP", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - nav.erp.filterValues = getErpFromFilter(pppNet.kfState); - - if ( acsConfig.process_ppp - && acsConfig.ambrOpts.mode != +E_ARmode::OFF - && acsConfig.ambrOpts.once_per_epoch) - { - //while this may fix and hold ambiguities on this state, this state may be a copy of the main state from somewhere else - fixAndHoldAmbiguities(pppTrace, kfState); - - mongoStates(kfState, - { - .suffix = "/AR", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - if ( acsConfig.ionModelOpts.model - && acsConfig.ssrOpts.atmosphere_sources.front() == +E_Source::KALMAN) - { - ionosphereSsrUpdate(ionTrace, kfState); - } - - if (acsConfig.process_ionosphere) - { - obsIonoDataFromFilter(ionTrace, receiverMap, kfState); - - filterIonosphere(ionTrace, ionNet.kfState, receiverMap, time); - - if (acsConfig.ssrOpts.atmosphere_sources.front() == +E_Source::KALMAN) - { - ionosphereSsrUpdate(ionTrace, ionNet.kfState); - } - } - - KFState tempAugmentedKF = kfState; - - if (acsConfig.process_ppp) - { - if (acsConfig.pivot_receiver != "NO_PIVOT") - { - KFState pivotedState = propagateUncertainty(pppTrace, kfState); - - pivotedState.outputStates(pppTrace, "/PIVOT"); - - mongoStates(pivotedState, - { - .suffix = "/PIVOT", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - if ( acsConfig.process_minimum_constraints - && acsConfig.minconOpts.once_per_epoch) - { - BOOST_LOG_TRIVIAL(info) << " ------- PERFORMING MIN-CONSTRAINTS --------" << "\n"; - - for (auto& [id, rec] : receiverMap) - { - rec.minconApriori = rec.aprioriPos; - } - - MinconStatistics minconStatistics; - - InteractiveTerminal minconTrace("MinimumConstraints", pppTrace); - - mincon(minconTrace, tempAugmentedKF, &minconStatistics); //todo aaron, orbits apriori need etting - - tempAugmentedKF.outputStates(minconTrace, "/CONSTRAINED"); - - outputMinconStatistics(minconTrace, minconStatistics); - - mongoStates(tempAugmentedKF, - { - .suffix = "/CONSTRAINED", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - static double epochsPerRtsInterval = acsConfig.pppOpts.rts_interval / acsConfig.epoch_interval; - static double intervalRtsEpoch = epochsPerRtsInterval; - - if ( acsConfig.process_rts - && acsConfig.pppOpts.rts_interval - && epoch >= intervalRtsEpoch) - { - while (intervalRtsEpoch <= epoch) - { - intervalRtsEpoch += epochsPerRtsInterval; - } - - rtsSmoothing(pppNet.kfState, receiverMap, true); - } - - for (auto& [recId, rec] : receiverMap) - { - auto trace = getTraceFile(rec); - - { - outputPppNmea(trace, kfState, rec.id); - } - - if (acsConfig.output_cost) { outputCost (kfState.metaDataMap[COST_FILENAME_STR + recId], kfState, rec); } - if (acsConfig.output_gpx) { writeGPX (kfState.metaDataMap[GPX_FILENAME_STR + recId], kfState, rec); } - if (acsConfig.output_pos) { writePOS (kfState.metaDataMap[POS_FILENAME_STR + recId], kfState, rec); } - - } - - outputStatistics(pppTrace, pppNet.kfState.statisticsMap, pppNet.kfState.statisticsMapSum); - } - - static GTime clkOutputTime = time.floorTime(acsConfig.clocks_output_interval); - static GTime obxOutputTime = time.floorTime(acsConfig.orbex_output_interval); - static GTime sp3OutputTime = time.floorTime(acsConfig.sp3_output_interval); - - if (acsConfig.output_rinex_nav) - { - writeRinexNav(acsConfig.rinex_nav_version); - } - - if (acsConfig.output_clocks) - while (clkOutputTime <= time) - { - outputClocks(acsConfig.clocks_filename, clkOutputTime, acsConfig.clocks_receiver_sources, acsConfig.clocks_satellite_sources, tempAugmentedKF, &receiverMap); clkOutputTime += std::max(acsConfig.epoch_interval, acsConfig.clocks_output_interval); - } - - if (acsConfig.output_sp3) - while (sp3OutputTime <= time) - { - outputSp3(acsConfig.sp3_filename, sp3OutputTime, acsConfig.sp3_orbit_sources, acsConfig.sp3_clock_sources, &tempAugmentedKF, emptyEpoch); sp3OutputTime += std::max(acsConfig.epoch_interval, acsConfig.sp3_output_interval); - } - - - if (acsConfig.output_orbex) - while (obxOutputTime <= time) - { - outputOrbex(acsConfig.orbex_filename, obxOutputTime, acsConfig.orbex_orbit_sources, acsConfig.orbex_clock_sources, acsConfig.orbex_attitude_sources, &kfState); obxOutputTime += std::max(acsConfig.epoch_interval, acsConfig.orbex_output_interval); - } - - if (acsConfig.output_ionex) - { - if (acsConfig.process_ionosphere) ionexFileWrite(ionTrace, kfState.metaDataMap[IONEX_FILENAME_STR], time, ionNet.kfState); - else ionexFileWrite(pppTrace, kfState.metaDataMap[IONEX_FILENAME_STR], time, kfState); - } - - if (acsConfig.output_ionstec) - { - writeIonStec(kfState.metaDataMap[IONSTEC_FILENAME_STR], kfState); - } - - if (acsConfig.output_bias_sinex) - { - writeBiasSinex(pppTrace, time, kfState, ionState, kfState.metaDataMap[BSX_FILENAME_STR], receiverMap); - } - - if (acsConfig.output_orbit_ics) - { - outputOrbitConfig(kfState); - } - - if (acsConfig.output_sbas_ems) - { - writeEMSdata(pppTrace, kfState.metaDataMap[EMS_FILENAME_STR]); - } - - if (acsConfig.output_erp) - { - writeErpFromNetwork(kfState.metaDataMap[ERP_FILENAME_STR], kfState); - } - - - if (acsConfig.output_trop_sinex) - { - outputTropSinex(kfState.metaDataMap[TROP_FILENAME_STR], kfState.time, kfState, "MIX"); - } - - mongoMeasSatStat (receiverMap); - outputApriori (receiverMap); - outputPredictedStates (pppTrace, tempAugmentedKF); - prepareSsrStates (pppTrace, tempAugmentedKF, ionState, time); + string _RTS; + string META_SUFFIX; + + if (inRts) + { + if (kfState.metaDataMap["SKIP_RTS_OUTPUT"] == "TRUE") + { + return; + } + + _RTS = "_RTS"; + META_SUFFIX = SMOOTHED_SUFFIX; + } + + // check whether we can write to the main state or need to make a copy (remember it will store + // in rts too) + bool hold = false; + + if (inRts == false && acsConfig.ambrOpts.fix_and_hold) + { + hold = true; + } + + if (time == GTime::noTime()) + time = kfState.time; + + static GTime clkOutputTime; + static GTime obxOutputTime; + static GTime sp3OutputTime; + + static bool firstEpoch = true; + + if (firstRtsEpoch) + { + // reset the first epoch things when starting rts + firstEpoch = true; + } + + if (firstEpoch) + { + // dont move above, rts resets these + clkOutputTime = time.floorTime(acsConfig.clocks_output_interval); + obxOutputTime = time.floorTime(acsConfig.orbex_output_interval); + sp3OutputTime = time.floorTime(acsConfig.sp3_output_interval); + + firstEpoch = false; + } + + tryPrepareFilterPointers(kfState, receiverMap); + + if (acsConfig.process_ppp) + { + mongoStates( + kfState, + {.suffix = "/PPP" + _RTS, + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + if (acsConfig.output_network_trace) + kfState.outputStates(pppTrace, "/PPP" + _RTS); + } + + nav.erp.filterValues = getErpFromFilter(kfState); + + if (acsConfig.ionModelOpts.model != E_IonoModel::NONE && + acsConfig.ssrOpts.atmosphere_sources.front() == E_Source::KALMAN) + { + auto ionTrace = getTraceFile(ionNet); + + ionosphereSsrUpdate(ionTrace, kfState); + } + + if (acsConfig.process_ionosphere) + { + auto ionTrace = getTraceFile(ionNet); + + obsIonoDataFromFilter(ionTrace, receiverMap, kfState); + + filterIonosphere(ionTrace, ionNet.kfState, receiverMap, time); + + if (acsConfig.ssrOpts.atmosphere_sources.front() == E_Source::KALMAN) + { + ionosphereSsrUpdate(ionTrace, ionNet.kfState); + } + } + + KFState augmentedKF = kfState; + + if (acsConfig.process_ppp) + { + if (acsConfig.reference_clock != "NO_REFERENCE" || + acsConfig.reference_bias != "NO_REFERENCE") + { + augmentedKF = propagateUncertainty(pppTrace, kfState); + + augmentedKF.outputStates(pppTrace, "/PIVOT" + _RTS); + + mongoStates( + augmentedKF, + {.suffix = "/PIVOT" + _RTS, + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + + if (hold) + { + BOOST_LOG_TRIVIAL(error) << "Ambiguity fix_and_hold requested but is not " + "possible with pre-pivoted states"; + hold = false; + } + } + + if (acsConfig.process_minimum_constraints && acsConfig.minconOpts.once_per_epoch) + { + BOOST_LOG_TRIVIAL(info) << " ------- PERFORMING MIN-CONSTRAINTS --------" << "\n"; + + for (auto& [id, rec] : receiverMap) + { + rec.minconApriori = rec.aprioriPos; + } + + MinconStatistics minconStatistics; + + mincon( + pppTrace, + augmentedKF, + &minconStatistics + ); // todo aaron, orbits apriori need etting + + augmentedKF.outputStates(pppTrace, "/CONSTRAINED" + _RTS); + + outputMinconStatistics(pppTrace, minconStatistics); + + mongoStates( + augmentedKF, + {.suffix = "/CONSTRAINED" + _RTS, + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + + // erp values have probably changed due to mincon, update the nav vars before all the + // outputs will have to revert this below because mincon is supposed to be temporary in + // once_per_epoch mode + nav.erp.filterValues = getErpFromFilter(augmentedKF); + + if (hold) + { + BOOST_LOG_TRIVIAL(error) << "Ambiguity fix_and_hold requested but is not " + "possible with minimally constrained states"; + hold = false; + } + } + + bool arPossible = true; + if (inRts && acsConfig.ambrOpts.fix_and_hold) + { + // was fixed on the forward run, dont do again + arPossible = false; + } + + if (arPossible && acsConfig.ambrOpts.mode != E_ARmode::OFF && + acsConfig.ambrOpts.once_per_epoch) + { + KFState* arState_ptr; + + if (hold) + { + arState_ptr = &kfState; + BOOST_LOG_TRIVIAL(info) << "Performing AR with fix and hold"; + } + else + { + arState_ptr = &augmentedKF; + } + + auto& arState = *arState_ptr; + + fixAndHoldAmbiguities(pppTrace, arState); + + arState.outputStates(pppTrace, "/AR" + _RTS); + + mongoStates( + arState, + {.suffix = "/AR" + _RTS, + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + + // erp values have probably changed due to AR, update the nav vars before all the + // outputs will have to revert this below because AR is supposed to be temporary in + // once_per_epoch mode + nav.erp.filterValues = getErpFromFilter(arState); + } + + for (auto& [id, rec] : receiverMap) + { + auto recTrace = getTraceFile(rec); + { + outputPppNmea(recTrace, augmentedKF, id); + } + if (acsConfig.output_cost) + { + outputCost( + kfState.metaDataMap[COST_FILENAME_STR + id + META_SUFFIX], + augmentedKF, + rec + ); + } + if (acsConfig.output_gpx) + { + writeGPX( + kfState.metaDataMap[GPX_FILENAME_STR + id + META_SUFFIX], + augmentedKF, + rec + ); + } + if (acsConfig.output_pos) + { + writePOS( + kfState.metaDataMap[POS_FILENAME_STR + id + META_SUFFIX], + augmentedKF, + rec + ); + } + } + } + + if (acsConfig.process_spp && acsConfig.output_spp) + for (auto& [id, rec] : receiverMap) + { + writeSPP(rec.sppOutputFile, rec); + } + + if (1) + { + if (acsConfig.output_orbit_ics) + { + outputOrbitConfig(augmentedKF, inRts); + } + if (acsConfig.output_trop_sinex) + { + outputTropSinex( + kfState.metaDataMap[TROP_FILENAME_STR + META_SUFFIX], + time, + augmentedKF, + "MIX", + inRts + ); + } + if (acsConfig.output_bias_sinex) + { + writeBiasSinex( + pppTrace, + kfState.metaDataMap[BSX_FILENAME_STR + META_SUFFIX], + time, + augmentedKF, + ionNet.kfState, + receiverMap + ); + } + if (acsConfig.output_clocks) + while (clkOutputTime <= time) + { + outputClocks( + kfState.metaDataMap[CLK_FILENAME_STR + META_SUFFIX], + clkOutputTime, + augmentedKF, + acsConfig.clocks_receiver_sources, + acsConfig.clocks_satellite_sources, + &receiverMap + ); + clkOutputTime += max(acsConfig.epoch_interval, acsConfig.clocks_output_interval); + } + if (acsConfig.output_orbex) + while (obxOutputTime <= time) + { + outputOrbex( + kfState.metaDataMap[ORBEX_FILENAME_STR + META_SUFFIX], + obxOutputTime, + augmentedKF, + acsConfig.orbex_orbit_sources, + acsConfig.orbex_clock_sources, + acsConfig.orbex_attitude_sources + ); + obxOutputTime += max(acsConfig.epoch_interval, acsConfig.orbex_output_interval); + } + if (acsConfig.output_sp3) + while (sp3OutputTime <= time) + { + outputSp3( + kfState.metaDataMap[SP3_FILENAME_STR + META_SUFFIX], + sp3OutputTime, + augmentedKF, + acsConfig.sp3_orbit_sources, + acsConfig.sp3_clock_sources, + emptyEpoch + ); + sp3OutputTime += max(acsConfig.epoch_interval, acsConfig.sp3_output_interval); + } + if (acsConfig.output_erp) + { + writeErpFromNetwork(kfState.metaDataMap[ERP_FILENAME_STR + META_SUFFIX], augmentedKF); + } + if (acsConfig.output_ionstec) + { + writeIonStec(kfState.metaDataMap[IONSTEC_FILENAME_STR + META_SUFFIX], augmentedKF); + } + if (acsConfig.output_ionex) + { + auto ionTrace = getTraceFile(ionNet); + + if (acsConfig.process_ionosphere) + { + ionexFileWrite( + ionTrace, + kfState.metaDataMap[IONEX_FILENAME_STR + META_SUFFIX], + time, + ionNet.kfState + ); + } + else + { + ionexFileWrite( + pppTrace, + kfState.metaDataMap[IONEX_FILENAME_STR + META_SUFFIX], + time, + augmentedKF + ); + } + } + } + + if (inRts == false) + { + if (acsConfig.output_rinex_nav) + { + writeRinexNav(acsConfig.rinex_nav_version); + } + if (acsConfig.output_sbas_ems) + { + writeEMSdata(pppTrace, kfState.metaDataMap[EMS_FILENAME_STR]); + } + + mongoMeasSatStat(receiverMap); + outputApriori(receiverMap); + outputPredictedStates(pppTrace, augmentedKF); + prepareSsrStates(pppTrace, augmentedKF, ionNet.kfState, time); + + // Only do rts if its not already in progress + static double epochsPerRtsInterval = + acsConfig.pppOpts.rts_interval / acsConfig.epoch_interval; + static double intervalRtsEpoch = epochsPerRtsInterval; + + if (acsConfig.process_rts && acsConfig.pppOpts.rts_interval && epoch >= intervalRtsEpoch) + { + while (intervalRtsEpoch <= epoch) + { + intervalRtsEpoch += epochsPerRtsInterval; + } + + rtsSmoothing(kfState, receiverMap, true); + } + + outputStatistics(pppTrace, kfState.statisticsMap, kfState.statisticsMapSum); + } + + // revert the erp filter values since we are done with the tempAugmentedKF + nav.erp.filterValues = getErpFromFilter(kfState); } diff --git a/src/cpp/pea/peaCommitStrings.cpp b/src/cpp/pea/peaCommitStrings.cpp index 23acb4f67..2c2d5f52a 100644 --- a/src/cpp/pea/peaCommitStrings.cpp +++ b/src/cpp/pea/peaCommitStrings.cpp @@ -1,102 +1,140 @@ +#include "pea/peaCommitStrings.hpp" +#include +#include "pea/peaCommitVersion.h" +#include "pea/peaLibVersion.h" -#include "peaCommitStrings.hpp" -#include "peaCommitVersion.h" +string ginanCommitHash() +{ + return GINAN_COMMIT_HASH; +} -#include +string ginanCommitVersion() +{ + return GINAN_COMMIT_VERSION; +} +string ginanBranchName() +{ + return GINAN_BRANCH_NAME; +} -string ginanCommitHash (){ return GINAN_COMMIT_HASH; } -string ginanCommitVersion (){ return GINAN_COMMIT_VERSION; } -string ginanBranchName (){ return GINAN_BRANCH_NAME; } -string ginanCommitDate (){ return GINAN_COMMIT_DATE; } -string ginanEigenVersion (){ return GINAN_EIGEN; } -string ginanMongoVersion (){ return GINAN_MONGOCXX; } -string ginanCompilerVersion (){ return GINAN_COMPILER; } -string ginanBoostVersion (){ return GINAN_BOOST; } +string ginanCommitDate() +{ + return GINAN_COMMIT_DATE; +} +string ginanEigenVersion() +{ + return GINAN_EIGEN; +} -string ginanOsName() +string ginanMongoVersion() { -#ifdef _WIN32 + return GINAN_MONGOCXX; +} - return "Windows 32-bit"; +string ginanCompilerVersion() +{ + return GINAN_COMPILER; +} -#elif _WIN64 +string ginanBoostVersion() +{ + return GINAN_BOOST; +} - return "Windows 64-bit"; +string ginanOsName() +{ +#if defined(_WIN32) || defined(_WIN64) + // Under MSVC/MinGW, _WIN32 is defined for both 32-bit and 64-bit. + // Use pointer size to determine bitness robustly (works under Wine too). + if (sizeof(void*) == 8) + { + return "Windows 64-bit"; + } + else + { + return "Windows 32-bit"; + } #elif __APPLE__ || __MACH__ - std::ifstream fileStream("/System/Library/CoreServices/SystemVersion.plist"); - if (!fileStream) - { - return "Mac OSX"; - } - - string productName; - string productVersion; - - string line; - while (getline(fileStream, line)) - { - if (line.find("ProductName") != string::npos) { productName = "next"; continue; } - if (line.find("ProductVersion") != string::npos) { productVersion = "next"; continue; } - - for (auto str_ptr : {&productName, &productVersion}) - { - auto& str = *str_ptr; - - if (str != "next") - { - continue; - } - - auto startStr = line.find(""); - auto stopStr = line.find(""); - - if ( startStr == string::npos - ||stopStr == string::npos) - { - str = ""; - continue; - } - - str = line.substr(startStr + 8, stopStr - startStr - 8); - } - } - - if ( productName .empty() - ||productVersion.empty()) - { - return "Mac OSX"; - } - - return productName + " " + productVersion; - -#elif __linux__ - - std::ifstream fileStream("/etc/os-release"); - if (!fileStream) - { - return "Linux"; - } - - string line; - while (getline(fileStream, line)) - { - const string prefix = "PRETTY_NAME="; - - if (line.substr(0, prefix.size()) == prefix) - { - return line.substr(prefix.size()); - } - } - - return "Linux"; + std::ifstream fileStream("/System/Library/CoreServices/SystemVersion.plist"); + if (!fileStream) + { + return "Mac OSX"; + } + + string productName; + string productVersion; + + string line; + while (getline(fileStream, line)) + { + if (line.find("ProductName") != string::npos) + { + productName = "next"; + continue; + } + if (line.find("ProductVersion") != string::npos) + { + productVersion = "next"; + continue; + } + + for (auto str_ptr : {&productName, &productVersion}) + { + auto& str = *str_ptr; + + if (str != "next") + { + continue; + } + + auto startStr = line.find(""); + auto stopStr = line.find(""); + + if (startStr == string::npos || stopStr == string::npos) + { + str = ""; + continue; + } + + str = line.substr(startStr + 8, stopStr - startStr - 8); + } + } + + if (productName.empty() || productVersion.empty()) + { + return "Mac OSX"; + } + + return productName + " " + productVersion; + +#elif defined(__linux__) + + std::ifstream fileStream("/etc/os-release"); + if (!fileStream) + { + return "Linux"; + } + + string line; + while (getline(fileStream, line)) + { + const string prefix = "PRETTY_NAME="; + + if (line.substr(0, prefix.size()) == prefix) + { + return line.substr(prefix.size()); + } + } + + return "Linux"; #else - return "Other"; + return "Other"; #endif } \ No newline at end of file diff --git a/src/cpp/pea/peaCommitStrings.hpp b/src/cpp/pea/peaCommitStrings.hpp index 32c43f00b..63db6f899 100644 --- a/src/cpp/pea/peaCommitStrings.hpp +++ b/src/cpp/pea/peaCommitStrings.hpp @@ -1,17 +1,16 @@ - #pragma once #include using std::string; -string ginanCommitHash (); -string ginanCommitVersion (); -string ginanBranchName (); -string ginanCommitDate (); -string ginanEigenVersion (); -string ginanMongoVersion (); -string ginanCompilerVersion (); -string ginanBoostVersion (); +string ginanCommitHash(); +string ginanCommitVersion(); +string ginanBranchName(); +string ginanCommitDate(); +string ginanEigenVersion(); +string ginanMongoVersion(); +string ginanCompilerVersion(); +string ginanBoostVersion(); -string ginanOsName (); \ No newline at end of file +string ginanOsName(); \ No newline at end of file diff --git a/src/cpp/pea/peaCommitVersion.h.in b/src/cpp/pea/peaCommitVersion.h.in index 762645954..62c6590ef 100644 --- a/src/cpp/pea/peaCommitVersion.h.in +++ b/src/cpp/pea/peaCommitVersion.h.in @@ -5,8 +5,3 @@ #define GINAN_COMMIT_VERSION "@GINAN_COMMIT_VERSION@" #define GINAN_BRANCH_NAME "@GINAN_BRANCH_NAME@" #define GINAN_COMMIT_DATE "@GINAN_COMMIT_DATE@" -#define GINAN_COMPILER "@CMAKE_CXX_COMPILER_ID@ @CMAKE_CXX_COMPILER_VERSION@" -#define GINAN_MONGOCXX "@mongocxx_VERSION@" -#define GINAN_BOOST "@Boost_VERSION@" -#define GINAN_EIGEN "@Eigen3_VERSION@" - diff --git a/src/cpp/pea/peaLibVersion.h.in b/src/cpp/pea/peaLibVersion.h.in new file mode 100644 index 000000000..04f3dcfb9 --- /dev/null +++ b/src/cpp/pea/peaLibVersion.h.in @@ -0,0 +1,7 @@ + +#pragma once + +#define GINAN_COMPILER "@CMAKE_CXX_COMPILER_ID@ @CMAKE_CXX_COMPILER_VERSION@" +#define GINAN_MONGOCXX "@mongocxx_VERSION@" +#define GINAN_BOOST "@Boost_VERSION@" +#define GINAN_EIGEN "@Eigen3_VERSION@" diff --git a/src/cpp/pea/pea_snx.cpp b/src/cpp/pea/pea_snx.cpp index cef9aff17..9e0b09cc9 100644 --- a/src/cpp/pea/pea_snx.cpp +++ b/src/cpp/pea/pea_snx.cpp @@ -1,214 +1,233 @@ - // #pragma GCC optimize ("O0") #include #include +#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/sinex.hpp" +#include "pea/inputsOutputs.hpp" +#include "pea/peaCommitStrings.hpp" +#include "slr/slr.hpp" using boost::algorithm::to_lower_copy; -#include "eigenIncluder.hpp" -#include "inputsOutputs.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "gTime.hpp" -#include "sinex.hpp" -#include "slr.hpp" - -void getStationsFromSinex( - map& receiverMap, - KFState& kfState) -{ - -} +void getStationsFromSinex(map& receiverMap, KFState& kfState) {} -void sinexPostProcessing( - GTime time, - map& receiverMap, - KFState& netKFState) +void sinexPostProcessing(GTime time, map& receiverMap, KFState& netKFState) { - theSinex.inputFiles. clear(); - theSinex.acknowledgements. clear(); - theSinex.inputHistory. clear(); - - sinexCheckAddGaReference("PPP Solution", "2.1", false); - - // add in the files used to create the solution - for (auto& [id, ubxinput] : acsConfig.ubx_inputs) { sinexAddFiles(acsConfig.analysis_agency, time, ubxinput, "UBX"); } - for (auto& [id, rnxinput] : acsConfig.rnx_inputs) { sinexAddFiles(acsConfig.analysis_agency, time, rnxinput, "RINEX v3.x"); } - { sinexAddFiles(acsConfig.analysis_agency, time, acsConfig.sp3_files, "SP3"); } - { sinexAddFiles(acsConfig.analysis_agency, time, acsConfig.snx_files, "SINEX"); } - - // Add other statistics as they become available... - sinexAddStatistic("SAMPLING INTERVAL (SECONDS)", acsConfig.epoch_interval); - - char obsCode = 'P'; //GNSS measurements - char constCode = ' '; - - string solcont = "ST"; - // uncomment next bit once integrated - // if (acsConfig.orbit_output) solcont += 'O'; - - string data_agc = ""; - - PTime startTime; - startTime.bigTime = boost::posix_time::to_time_t(acsConfig.start_epoch); //todo aaron, make these constructors for ptime. - - KFState sinexSubstate = mergeFilters({&netKFState}, {KF::ONE, KF::REC_POS, KF::REC_POS_RATE}); - - updateSinexHeader(acsConfig.analysis_agency, data_agc, (GTime) startTime, time, obsCode, constCode, solcont, sinexSubstate.x.rows() - 1, 2.02); //Change this if the sinex format gets updated - - string filename = acsConfig.sinex_filename; - - replaceTimes(filename, acsConfig.start_epoch); - - writeSinex(filename, sinexSubstate, receiverMap); + theSinex.inputFiles.clear(); + theSinex.acknowledgements.clear(); + theSinex.inputHistory.clear(); + + sinexCheckAddGaReference("PPP Solution", ginanCommitVersion(), false); + + // add in the files used to create the solution + for (auto& [id, ubxinput] : acsConfig.ubx_inputs) + { + sinexAddFiles(acsConfig.analysis_agency, time, ubxinput, "UBX"); + } + for (auto& [id, rnxinput] : acsConfig.rnx_inputs) + { + sinexAddFiles(acsConfig.analysis_agency, time, rnxinput, "RINEX v3.x"); + } + { + sinexAddFiles(acsConfig.analysis_agency, time, acsConfig.sp3_files, "SP3"); + } + { + sinexAddFiles(acsConfig.analysis_agency, time, acsConfig.snx_files, "SINEX"); + } + + // Add other statistics as they become available... + sinexAddStatistic("SAMPLING INTERVAL (SECONDS)", acsConfig.epoch_interval); + + char obsCode = 'P'; // GNSS measurements + char constCode = ' '; + + string solcont = "ST"; + // uncomment next bit once integrated + // if (acsConfig.orbit_output) solcont += 'O'; + + string data_agc = ""; + + PTime startTime; + startTime.bigTime = boost::posix_time::to_time_t(acsConfig.start_epoch + ); // todo aaron, make these constructors for ptime. + + KFState sinexSubstate = mergeFilters({&netKFState}, {KF::ONE, KF::REC_POS, KF::REC_POS_RATE}); + + updateSinexHeader( + acsConfig.analysis_agency, + data_agc, + (GTime)startTime, + time, + obsCode, + constCode, + solcont, + sinexSubstate.x.rows() - 1, + 2.02 + ); // Change this if the sinex format gets updated + + string filename = acsConfig.sinex_filename; + + replaceTimes(filename, acsConfig.start_epoch); + + writeSinex(filename, sinexSubstate, receiverMap); } -void sinexPerEpochPerStation( - Trace& trace, - GTime time, - Receiver& rec) +void sinexPerEpochPerStation(Trace& trace, 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); - - if ( rec.snx.stop > yds - && rec.snx.stop > defaultStop) - { - //already have valid data - return; - } - - 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); - } - - rec.failureEccentricity = rec.antDelta.isZero(); - - auto& recOpts = acsConfig.getRecOpts(rec.id); - { - auto& eccModel = recOpts.eccentricityModel; - if (rec.antDelta .isZero() && eccModel.enable) { rec.antDelta = recOpts.eccentricityModel.eccentricity; rec.failureEccentricity = false; } - 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) { 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; - } - - if ( result.failureSiteId) - { - rec.failureSinex = true; - } - - if ( result.failureEstimate - &&recOpts.apriori_pos.isZero()) - { - rec.failureAprioriPos = true; - } - - if ( refSys != "UNE") - { - rec.failureEccentricity = true; - - BOOST_LOG_TRIVIAL(error) - << "Error: Receiver eccentricity referency system != UNE"; //todo aaron, this needs duplication elsewhere, rs unchecked - } - - if (rec.receiverType.empty() == false) - { - string receiverType = to_lower_copy(rec.receiverType); - receiverType = receiverType.substr(0, receiverType.find(" ")); - - auto [it, inserted] = acsConfig.customAliasesMap[rec.id].insert(receiverType); - if (inserted) - { - auto& baseRecOpts = acsConfig.getRecOpts((string) "_" + rec.id); - - for (auto& [id, inheritor] : baseRecOpts.inheritors) - { - inheritor->_initialised = false; - } - } - } - - // Initialise the receiver antenna information - for (bool once : {1}) - { - string nullstring = ""; - string tmpant = rec.antennaType; - - if (tmpant.empty()) - { - trace - << "Antenna name not specified" - << rec.id << ": Antenna name not specified"; - - rec.failureAntenna = true; - - break; - } - - bool found; - found = findAntenna(tmpant, E_Sys::GPS, time, nav, F1); - if (found) - { - //all good, carry on - rec.antennaId = tmpant; - break; - } - - // Try searching under the antenna type with DOME => NONE - radome2none(tmpant); - - found = findAntenna(tmpant, E_Sys::GPS, time, nav, F1); - if (found) - { - trace - << "Using '" << tmpant - << "' instead of: '" << rec.antennaType - << "' for radome of " << rec.id; - - rec.antennaId = tmpant; - break; - } - - trace - << "No information for antenna " << rec.antennaType; - - rec.failureAntenna = true; - } + 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); + + if (rec.snx.stop > yds && rec.snx.stop > defaultStop) + { + // already have valid data + return; + } + + 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); + } + + rec.failureEccentricity = rec.antDelta.isZero(); + + auto& recOpts = acsConfig.getRecOpts(rec.id); + { + auto& eccModel = recOpts.eccentricityModel; + if (rec.antDelta.isZero() && eccModel.enable) + { + rec.antDelta = recOpts.eccentricityModel.eccentricity; + rec.failureEccentricity = false; + } + 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) + { + 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; + } + + if (result.failureSiteId) + { + rec.failureSinex = true; + } + + if (result.failureEstimate && recOpts.apriori_pos.isZero()) + { + rec.failureAprioriPos = true; + } + + if (refSys != "UNE") + { + rec.failureEccentricity = true; + + BOOST_LOG_TRIVIAL( + error + ) << "Receiver eccentricity referency system != UNE"; // todo aaron, this needs + // duplication elsewhere, rs + // unchecked + } + + if (rec.receiverType.empty() == false) + { + string receiverType = to_lower_copy(rec.receiverType); + receiverType = receiverType.substr(0, receiverType.find(" ")); + + auto [it, inserted] = acsConfig.customAliasesMap[rec.id].insert(receiverType); + if (inserted) + { + auto& baseRecOpts = acsConfig.getRecOpts((string) "_" + rec.id); + + for (auto& [id, inheritor] : baseRecOpts.inheritors) + { + inheritor->_initialised = false; + } + } + } + + // Initialise the receiver antenna information + for (bool once : {1}) + { + string nullstring = ""; + string tmpant = rec.antennaType; + + if (tmpant.empty()) + { + trace << "Antenna name not specified" << rec.id << ": Antenna name not specified"; + + rec.failureAntenna = true; + + break; + } + + bool found; + found = findAntenna(tmpant, E_Sys::GPS, time, nav, F1); + if (found) + { + // all good, carry on + rec.antennaId = tmpant; + break; + } + + // Try searching under the antenna type with DOME => NONE + radome2none(tmpant); + + found = findAntenna(tmpant, E_Sys::GPS, time, nav, F1); + if (found) + { + trace << "Using '" << tmpant << "' instead of: '" << rec.antennaType + << "' for radome of " << rec.id; + + rec.antennaId = tmpant; + break; + } + + trace << "No information for antenna " << rec.antennaType; + + rec.failureAntenna = true; + } } diff --git a/src/cpp/pea/ppp.cpp b/src/cpp/pea/ppp.cpp index 6c66b4f95..6b4f36300 100644 --- a/src/cpp/pea/ppp.cpp +++ b/src/cpp/pea/ppp.cpp @@ -1,162 +1,154 @@ - // #pragma GCC optimize ("O0") +#include "pea/ppp.hpp" #include - #include #include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/algebraTrace.hpp" +#include "common/antenna.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/ephPrecise.hpp" +#include "common/ephemeris.hpp" +#include "common/erp.hpp" +#include "common/gTime.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/satStat.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" using std::vector; - -#include "eigenIncluder.hpp" -#include "observations.hpp" -#include "algebraTrace.hpp" -#include "coordinates.hpp" -#include "ephPrecise.hpp" -#include "navigation.hpp" -#include "mongoWrite.hpp" -#include "ephPrecise.hpp" -#include "testUtils.hpp" -#include "ephemeris.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "receiver.hpp" -#include "satStat.hpp" -#include "algebra.hpp" -#include "antenna.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "enums.h" -#include "ppp.hpp" -#include "erp.hpp" - -void outputApriori( - ReceiverMap& receiverMap) +void outputApriori(ReceiverMap& receiverMap) { - if (acsConfig.mongoOpts.output_states == +E_Mongo::NONE) - { - return; - } - - ERPValues erpv = getErp(nav.erp, tsync); - - FrameSwapper frameSwapper(tsync, erpv); - - KFState aprioriState; - KFState brdcState; - - for (auto& [id, rec] : receiverMap) - { - // apriori_positions: - { - KFKey kfKey; - kfKey.str = id; - kfKey.type = KF::REC_POS; - - for (int i = 0; i < 3; i++) - { - kfKey.num = i; - double apriori = rec.aprioriPos[i]; - - if (apriori == 0) - { - continue; - } - - aprioriState.addKFState(kfKey, {.x = apriori}); - } - } - - // apriori_clocks: - { - KFKey kfKey; - kfKey.str = id; - kfKey.Sat = SatSys(E_Sys::GPS); - kfKey.type = KF::REC_CLOCK; - - double apriori = rec.aprioriClk; - - if (apriori == 0) - { - continue; - } - - aprioriState.addKFState(kfKey, {.x = apriori}); - } - } - - for (auto& [Sat, satNav] : nav.satNavMap) - { - if (acsConfig.process_sys[Sat.sys] == false) - { - continue; - } - - auto& satOpts = acsConfig.getSatOpts(Sat); - - if (satOpts.exclude) - { - continue; - } - - SatPos satPos; - satPos.Sat = Sat; - - //apriori_clocks: - { - KFKey kfKey; - kfKey.Sat = Sat; - kfKey.type = KF::SAT_CLOCK; - - aprioriState.addKFState(kfKey, {.x = CLIGHT * satNav.aprioriClk}); - - bool brdcPass = satClkBroadcast(nullStream, tsync, tsync, satPos, nav); - if (brdcPass) - { - brdcState.addKFState(kfKey, {.x = CLIGHT * satPos.satClk}); - - satPos.rSatEci0 = frameSwapper(satPos.rSatApc); - } - } - - //apriori_positions: - { - bool brdcPass = satPosBroadcast(nullStream, tsync, tsync, satPos, nav); - if (brdcPass) - { - satPos.rSatEci0 = frameSwapper(satPos.rSatApc); - } - - for (int i = 0; i < 3; i++) - { - KFKey kfKey; - kfKey.Sat = Sat; - kfKey.num = i; - kfKey.type = KF::ORBIT; - - aprioriState.addKFState(kfKey, {.x = satNav.aprioriPos (i)}); - if (brdcPass) brdcState .addKFState(kfKey, {.x = satPos.rSatEci0 (i)}); - } - } - } - - aprioriState.stateTransition(nullStream, tsync); - brdcState .stateTransition(nullStream, tsync); - - mongoStates(aprioriState, - { - .suffix = "/APRIORI", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - - mongoStates(brdcState, - { - .suffix = "/BROADCAST", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); + if (acsConfig.mongoOpts.output_states == E_Mongo::NONE) + { + return; + } + + ERPValues erpv = getErp(nav.erp, tsync); + + FrameSwapper frameSwapper(tsync, erpv); + + KFState aprioriState; + KFState brdcState; + + for (auto& [id, rec] : receiverMap) + { + // apriori_positions: + { + KFKey kfKey; + kfKey.str = id; + kfKey.type = KF::REC_POS; + + for (int i = 0; i < 3; i++) + { + kfKey.num = i; + double apriori = rec.aprioriPos[i]; + + if (apriori == 0) + { + continue; + } + + aprioriState.addKFState(kfKey, {.x = apriori}); + } + } + + // apriori_clocks: + { + KFKey kfKey; + kfKey.str = id; + kfKey.Sat = SatSys(E_Sys::GPS); + kfKey.type = KF::REC_CLOCK; + + double apriori = rec.aprioriClk; + + if (apriori == 0) + { + continue; + } + + aprioriState.addKFState(kfKey, {.x = apriori}); + } + } + + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (acsConfig.process_sys[Sat.sys] == false) + { + continue; + } + + auto& satOpts = acsConfig.getSatOpts(Sat); + + if (satOpts.exclude) + { + continue; + } + + SatPos satPos; + satPos.Sat = Sat; + + // apriori_clocks: + { + KFKey kfKey; + kfKey.Sat = Sat; + kfKey.type = KF::SAT_CLOCK; + + aprioriState.addKFState(kfKey, {.x = CLIGHT * satNav.aprioriClk}); + + bool brdcPass = satClkBroadcast(nullStream, tsync, tsync, satPos, nav); + if (brdcPass) + { + brdcState.addKFState(kfKey, {.x = CLIGHT * satPos.satClk}); + } + } + + // apriori_positions: + { + bool brdcPass = satPosBroadcast(nullStream, tsync, tsync, satPos, nav); + if (brdcPass) + { + satPos.rSatEci0 = frameSwapper(satPos.rSatApc); + } + + for (int i = 0; i < 3; i++) + { + KFKey kfKey; + kfKey.Sat = Sat; + kfKey.num = i; + kfKey.type = KF::ORBIT; + + aprioriState.addKFState(kfKey, {.x = satNav.aprioriPos(i)}); + if (brdcPass) + brdcState.addKFState(kfKey, {.x = satPos.rSatEci0(i)}); + } + } + } + + aprioriState.stateTransition(nullStream, tsync); + brdcState.stateTransition(nullStream, tsync); + + mongoStates( + aprioriState, + {.suffix = "/APRIORI", + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + + mongoStates( + brdcState, + {.suffix = "/BROADCAST", + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); } /** Compare estimated station position with benchmark in SINEX file @@ -210,14 +202,15 @@ void outputApriori( // if (!fout) // { // BOOST_LOG_TRIVIAL(warning) -// << "Warning: Could not open trace file for PPP solution at \"" << filename << "\""; +// << "Could not open trace file for PPP solution at \"" << filename << "\""; // // return; // } // // if (fout.tellp() == 0) // { -// tracepdeex(1, fout, " Date UTC time Sta. A priori X A priori Y A priori Z Estimated X Estimated Y Estimated Z Dif. X Dif. Y Dif. Z Dif. E Dif. N Dif. U\n"); +// tracepdeex(1, fout, " Date UTC time Sta. A priori X A priori Y A priori Z +// Estimated X Estimated Y Estimated Z Dif. X Dif. Y Dif. Z Dif. E Dif. N Dif. U\n"); // } // // fout << kfState.time.to_string() << " "; @@ -239,14 +232,16 @@ void outputApriori( // int solStat, ///< Solution quiality (2: ambiguity float, 5: ambiguity fix) // int numSat, ///< Number of satellites // double hdop, ///< Horizontal DOP -// bool lng) ///< Modified GPGGA format (false: GPGGA format according to NMEA 0183) +// bool lng) ///< Modified GPGGA format (false: GPGGA format according to NMEA +// 0183) // { // GEpoch ep = kfState.time; // // std::ofstream fpar(outfile, std::ios::out | std::ios::app); // // if (fpar.tellp() == 0) -// tracepdeex(1,fpar,"!GPGGA, UTC time, Latitude, N/S, Longitude, E/W, State, # Sat, HDOP, Height, , Geoid,\n"); +// tracepdeex(1,fpar,"!GPGGA, UTC time, Latitude, N/S, Longitude, E/W, State, # Sat, HDOP, +// Height, , Geoid,\n"); // // VectorEcef ecef; // for (short i = 0; i < 3; i++) @@ -277,753 +272,1077 @@ void outputApriori( // tracepdeex(1,fpar,",M,0.0,M,,,\n"); // } -void selectAprioriSource( - SatSys& Sat, - GTime& time, - KFState& kfState, - KFState* remote_ptr) +void selectAprioriSource(SatSys& Sat, GTime& time, KFState& kfState, KFState* remote_ptr) +{ + auto& satOpts = acsConfig.getSatOpts(Sat); + + auto& satNav = nav.satNavMap[Sat]; + auto& satPos0 = satNav.satPos0; + + auto trace = getTraceFile(satNav); + + satPos0.Sat = Sat; + satPos0.satNav_ptr = &satNav; + + auto posModelSources = satOpts.posModel.sources; + auto clkModelSources = satOpts.clockModel.sources; + + // remove kalman from the list to not corrupt the apriori states + auto posSource_it = std::find(posModelSources.begin(), posModelSources.end(), E_Source::KALMAN); + auto clkSource_it = std::find(clkModelSources.begin(), clkModelSources.end(), E_Source::KALMAN); + + if (posSource_it != posModelSources.end()) + { + posModelSources.erase(posSource_it); + } + if (clkSource_it != clkModelSources.end()) + { + clkModelSources.erase(clkSource_it); + } + + bool posPass = satpos( + trace, + time, + time, + satPos0, + posModelSources, + E_OffsetType::COM, + nav, + &kfState, + remote_ptr + ); + bool clkPass = satclk(trace, time, time, satPos0, clkModelSources, nav, &kfState, remote_ptr); + + tracepdeex(1, trace, "\n"); + + if (posPass) + { + ERPValues erpv = getErp(nav.erp, time); + + FrameSwapper frameSwapper(time, erpv); + + satNav.aprioriPos = frameSwapper(satPos0.rSatCom); + + tracepdeex( + 1, + trace, + "\nSelecting apriori pos (ECI) at %s, found %-10s: [%f, %f, %f]", + time.to_string(), + enum_to_string(satPos0.posSource).c_str(), + satNav.aprioriPos.x(), + satNav.aprioriPos.x(), + satNav.aprioriPos.x() + ); + + updateSatAtts(satPos0); + } + else + { + BOOST_LOG_TRIVIAL(warning) << "No sat pos found for " << satPos0.Sat.id() << "."; + } + + if (clkPass) + { + satNav.aprioriClk = satPos0.satClk; + + tracepdeex( + 1, + trace, + "\nSelecting apriori clk (m) at %s, found %-10s: %f", + time.to_string(), + enum_to_string(satPos0.clkSource).c_str(), + satNav.aprioriClk * CLIGHT + ); + } + else + { + BOOST_LOG_TRIVIAL(warning) << "No sat clk found for " << satPos0.Sat.id() << "."; + } +} + +void updateAprioriRecPos( + Trace& trace, + Receiver& rec, + ReceiverOptions& recOpts, + bool& sppUsed, + KFState* remote_ptr +) +{ + E_Source foundSource = E_Source::NONE; + for (auto source : recOpts.posModel.sources) + { + switch (source) + { + case E_Source::CONFIG: + { + if (recOpts.apriori_pos.isZero()) + { + continue; + } + + rec.aprioriPos = recOpts.apriori_pos; + + break; + } + case E_Source::PRECISE: + { + if (rec.snx.pos.isZero()) + { + continue; + } + + rec.aprioriPos = rec.snx.pos; + rec.primaryApriori = rec.snx.primary; + for (int i = 0; i < 3; i++) + { + rec.aprioriTime[i] = rec.snx.start[i]; + } + + break; + } + case E_Source::REMOTE: + { + if (remote_ptr == nullptr) + { + continue; + } + + bool found = true; + for (int i = 0; i < 3; i++) + { + KFKey kfKey; + kfKey.type = KF::REC_POS; + kfKey.str = rec.id; + kfKey.num = i; + + found = found && + (remote_ptr->getKFValue(kfKey, rec.aprioriPos(i)) != E_Source::NONE); + } + + if (!found) + { + continue; + } + + break; + } + case E_Source::SPP: + { + if (rec.sol.sppPos.isZero()) + { + continue; + } + + rec.aprioriTime = rec.sol.sppTime; + rec.aprioriPos = rec.sol.sppPos; + + sppUsed = true; + + break; + } + case E_Source::BROADCAST: + { + // todo for satellite receivers + continue; + } + case E_Source::KALMAN: + { + // skipping this for receivers because it breaks minimum constraints + continue; + } + default: + { + BOOST_LOG_TRIVIAL(warning) + << "Unknown receiver apriori position source found: " << enum_to_string(source); + + continue; + } + } + + foundSource = source; + break; + } + + if (foundSource == E_Source::NONE) + { + BOOST_LOG_TRIVIAL(warning) << "No receiver apriori position found for " << rec.id; + } + + tracepdeex( + 4, + trace, + "\nUsing %s as source for receiver apriori position: %f %f %f", + enum_to_string(foundSource), + rec.aprioriPos.x(), + rec.aprioriPos.y(), + rec.aprioriPos.z() + ); +} + +void updateAprioriRecClk( + Trace& trace, + Receiver& rec, + ReceiverOptions& recOpts, + GTime& time, + KFState& kfState, + KFState* remote_ptr +) { - auto& satOpts = acsConfig.getSatOpts(Sat); + E_Source foundSource = E_Source::NONE; + for (auto source : recOpts.clockModel.sources) + { + switch (source) + { + case E_Source::PRECISE: + { + double dtRec = 0; + double dtRecVar = 0; + bool pass = pephclk(trace, time, rec.id, nav, dtRec, &dtRecVar); + if (pass == false) + { + continue; + } + + rec.aprioriClk = dtRec * CLIGHT; + rec.aprioriClkVar = dtRecVar * SQR(CLIGHT); + + break; + } + case E_Source::KALMAN: + case E_Source::REMOTE: + { + if (source == E_Source::REMOTE && remote_ptr == nullptr) + continue; + + E_Source found; + { + KFKey kfKey; + kfKey.type = KF::REC_CLOCK; + kfKey.str = rec.id; + + double dummy; + + if (source == E_Source::KALMAN) + found = kfState.getKFValue( + kfKey, + rec.aprioriClk, + &rec.aprioriClkVar, + &dummy, + false + ); + if (source == E_Source::REMOTE) + found = remote_ptr->getKFValue( + kfKey, + rec.aprioriClk, + &rec.aprioriClkVar, + &dummy, + false + ); + } + + if (found == E_Source::NONE) + { + continue; + } + + break; + } + case E_Source::SPP: + { + if (rec.sol.sppClk == 0) + { + continue; + } + + rec.aprioriClk = rec.sol.sppClk; + rec.aprioriClkVar = SQR(30); // todo Eugene: use estimated var + + break; + } + case E_Source::BROADCAST: + { + // ignore broadcast thats in the common default list for satellites benefit + continue; + } + default: + { + BOOST_LOG_TRIVIAL(warning) + << "Unknown receiver apriori clock source found: " << enum_to_string(source); + continue; + } + } + + foundSource = source; + break; + } + + if (foundSource == E_Source::NONE) + { + BOOST_LOG_TRIVIAL(warning) << "No receiver apriori clock found for " << rec.id; + } + + tracepdeex( + 4, + trace, + "\nUsing %s as source for receiver apriori clock: %f", + enum_to_string(foundSource), + rec.aprioriClk + ); +} - auto& satNav = nav.satNavMap[Sat]; - auto& satPos0 = satNav.satPos0; +void selectAprioriSource( + Trace& trace, + Receiver& rec, + GTime& time, + bool& sppUsed, + KFState& kfState, + KFState* remote_ptr +) +{ + sppUsed = false; - auto trace = getTraceFile(satNav); + auto& recOpts = acsConfig.getRecOpts(rec.id); - satPos0.Sat = Sat; - satPos0.satNav_ptr = &satNav; + updateAprioriRecPos(trace, rec, recOpts, sppUsed, remote_ptr); + updateAprioriRecClk(trace, rec, recOpts, time, kfState, remote_ptr); - auto posModelSources = satOpts.posModel.sources; - auto clkModelSources = satOpts.clockModel.sources; + if (recOpts.apriori_sigma_enu.empty() == false) + { + Matrix3d enuNoise = Matrix3d::Zero(); + for (int i = 0; i < 3; i++) + { + int j = i; - //remove kalman from the list to not corrupt the apriori states - auto brdcPosIt = std::find(posModelSources.begin(), posModelSources.end(), +E_Source::KALMAN); - auto brdcClkIt = std::find(clkModelSources.begin(), clkModelSources.end(), +E_Source::KALMAN); - posModelSources.erase(brdcPosIt); - clkModelSources.erase(brdcClkIt); + if (j >= recOpts.apriori_sigma_enu.size()) + j = recOpts.apriori_sigma_enu.size() - 1; - bool posPass = satpos(trace, time, time, satPos0, posModelSources, E_OffsetType::COM, nav, &kfState, remote_ptr); - bool clkPass = satclk(trace, time, time, satPos0, clkModelSources, nav, &kfState, remote_ptr); + enuNoise(i, i) = SQR(recOpts.apriori_sigma_enu[j]); + } - if (posPass == false) { BOOST_LOG_TRIVIAL(warning) << "Warning: No sat pos found for " << satPos0.Sat.id() << "."; return; } - if (clkPass == false) { BOOST_LOG_TRIVIAL(warning) << "Warning: No sat clk found for " << satPos0.Sat.id() << "."; return; } + VectorPos pos = ecef2pos(rec.aprioriPos); - ERPValues erpv = getErp(nav.erp, time); + Matrix3d E; + pos2enu(pos, E.data()); - FrameSwapper frameSwapper(time, erpv); + Matrix3d varianceXYZ = E.transpose() * enuNoise * E; - satNav.aprioriPos = frameSwapper(satPos0.rSatCom); - satNav.aprioriClk = satPos0.satClk; + rec.aprioriPosVar = varianceXYZ; // todo Eugene: use estimated var for SPP + } + else + { + rec.aprioriPosVar = rec.snx.var.asDiagonal(); + } - tracepdeex(1, trace, "\n"); - tracepdeex(1, trace, "\nSelecting apriori pos (ECI) at %s, found %-10s: [%f, %f, %f]", time.to_string(), satPos0.posSource._to_string(), satNav.aprioriPos.x(), satNav.aprioriPos.x(), satNav.aprioriPos.x()); - tracepdeex(1, trace, "\nSelecting apriori clk (m) at %s, found %-10s: %f", time.to_string(), satPos0.clkSource._to_string(), satNav.aprioriClk * CLIGHT); + if (rec.sol.sppPos.norm() < 0.001) + { + return; + } - updateSatAtts(satPos0); -} + Vector3d delta = rec.aprioriPos - rec.sol.sppPos; + double distance = delta.norm(); + if (distance > 20) + { + BOOST_LOG_TRIVIAL(warning) + << "Apriori for " << rec.id << " is " << distance << "m from SPP estimate"; + } +} -void selectAprioriSource( - Trace& trace, - Receiver& rec, - GTime& time, - bool& sppUsed, - KFState& kfState, - KFState* remote_ptr) +string ft2string(E_FType ft) { - sppUsed = false; - - auto& recOpts = acsConfig.getRecOpts(rec.id); - - E_Source foundSource = E_Source::NONE; - for (auto source : recOpts.posModel.sources) - { - switch (source) - { - case E_Source::CONFIG: - { - if (recOpts.apriori_pos.isZero()) - { - continue; - } - - rec.aprioriPos = recOpts.apriori_pos; - - break; - } - case E_Source::PRECISE: - { - if (rec.snx.pos.isZero()) - { - continue; - } - - rec.aprioriPos = rec.snx.pos; - rec.primaryApriori = rec.snx.primary; - for (int i = 0; i < 3; i++) - { - rec.aprioriTime[i] = rec.snx.start[i]; - } - - break; - } - case E_Source::REMOTE: - { - if (remote_ptr == nullptr) - { - continue; - } - - bool found = true; - for (int i = 0; i < 3; i++) - { - KFKey kfKey; - kfKey.type = KF::REC_POS; - kfKey.str = rec.id; - kfKey.num = i; - - found &= remote_ptr->getKFValue(kfKey, rec.aprioriPos(i)); - } - - if (found == false) - { - continue; - } - - break; - } - case E_Source::SPP: - { - rec.aprioriTime = rec.sol.sppTime; - rec.aprioriPos = rec.sol.sppRRec; - - sppUsed = true; - - break; - } - case E_Source::BROADCAST: - { - //todo for satellite receivers - continue; - } - case E_Source::KALMAN: - { - //skipping this for receivers because it breaks minimum constraints - continue; - } - default: - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Unknown receiver apriori position source found: " << source._to_string(); - - continue; - } - } - - foundSource = source; - break; - } - - if (foundSource == +E_Source::NONE) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No receiver apriori position found for " << rec.id; - } - - tracepdeex(4, trace, "\nUsing %s as source for receiver apriori position: %f %f %f", - foundSource._to_string(), - rec.aprioriPos.x(), - rec.aprioriPos.y(), - rec.aprioriPos.z()); - - foundSource = E_Source::NONE; - - for (auto source : recOpts.clockModel.sources) - { - switch (source) - { - case E_Source::PRECISE: - { - double dtRec = 0; - double dtRecVar = 0; - bool pass = pephclk(trace, time, rec.id, nav, dtRec, &dtRecVar); - if (pass == false) - { - continue; - } - - rec.aprioriClk = dtRec * CLIGHT; - rec.aprioriClkVar = dtRecVar * SQR( CLIGHT); - - break; - } - case E_Source::KALMAN: - case E_Source::REMOTE: - { - if (source == +E_Source::REMOTE && remote_ptr == nullptr) continue; - - E_Source found; - { - KFKey kfKey; - kfKey.type = KF::REC_CLOCK; - kfKey.str = rec.id; - - double dummy; - - if (source == +E_Source::KALMAN) found = kfState. getKFValue(kfKey, rec.aprioriClk, &rec.aprioriClkVar, &dummy, false); - if (source == +E_Source::REMOTE) found = remote_ptr->getKFValue(kfKey, rec.aprioriClk, &rec.aprioriClkVar, &dummy, false); - } - - if (found == false) - { - continue; - } - - break; - } - case E_Source::SPP: - { - rec.aprioriClk = rec.sol.dtRec_m[E_Sys::GPS]; - rec.aprioriClkVar = SQR(30); - - break; - } - case E_Source::BROADCAST: - { - //ignore broadcast thats in the common default list for satellites benefit - continue; - } - default: - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Unknown receiver apriori clock source found: " << source._to_string(); - continue; - } - } - - foundSource = source; - break; - } - - if (foundSource == +E_Source::NONE) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No receiver apriori clock found for " << rec.id; - } - - tracepdeex(4, trace, "\nUsing %s as source for receiver apriori clock: %f", - foundSource._to_string(), - rec.aprioriClk); - - if (recOpts.apriori_sigma_enu.empty() == false) - { - Matrix3d enuNoise = Matrix3d::Zero(); - for (int i = 0; i < 3; i++) - { - int j = i; - - if (j >= recOpts.apriori_sigma_enu.size()) - j = recOpts.apriori_sigma_enu.size() - 1; - - enuNoise(i,i) = SQR(recOpts.apriori_sigma_enu[j]); - } - - VectorPos pos = ecef2pos(rec.aprioriPos); - - Matrix3d E; - pos2enu(pos, E.data()); - - Matrix3d varianceXYZ = E.transpose() * enuNoise * E; - - - rec.aprioriVar = varianceXYZ; - } - else - { - rec.aprioriVar = rec.snx.var.asDiagonal(); - } - - Vector3d delta = rec.aprioriPos - - rec.sol.sppRRec; - - double distance = delta.norm(); - - if ( distance > 20 - &&rec.sol.sppRRec.norm() > 0) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Apriori for " << rec.id << " is " << distance << "m from SPP estimate"; - } + return "F" + std::to_string(ft); } -string ft2string( - E_FType ft) +void addRejectDetails( + const GTime& time, + Trace& trace, + KFState& kfState, + const KFKey& key, + const string& action, + const string& reason, + vector details +) { - return "F" + std::to_string(ft); + tracepdeex( + 0, + trace, + "\n%s\t%-24s\t- %7s\t%s", + time.to_string().c_str(), + action.c_str(), + reason.c_str(), + ((string)key).c_str() + ); + + for (auto& detail : details) + { + tracepdeex(0, trace, "\t- %s", detail.name.c_str()); + + if (detail.isBool() == false) + { + tracepdeex(0, trace, ": %s", detail.value().c_str()); + } + }; + + details.push_back({"reason", reason}); + + KFKey subKey; + subKey.str = key.str; + + char buff[64]; + for (auto& description : {reason.c_str(), "TOTAL"}) + { + { + KFKey subKey; + snprintf( + buff, + sizeof(buff), + "%-25s\t%s\t%s", + action.c_str(), + description, + ((string)subKey).c_str() + ); + kfState.statisticsMap[buff]++; + } + { + KFKey subKey; + subKey = key; + snprintf( + buff, + sizeof(buff), + "%-25s\t%s\t%s", + action.c_str(), + description, + ((string)subKey).c_str() + ); + kfState.statisticsMap[buff]++; + } + { + KFKey subKey; + subKey.str = key.str; + snprintf( + buff, + sizeof(buff), + "%-25s\t%s\t%s", + action.c_str(), + description, + ((string)subKey).c_str() + ); + kfState.statisticsMap[buff]++; + } + // { + // KFKey subKey; + // subKey.Sat = key.Sat; + // snprintf( + // buff, + // sizeof(buff), + // "%-25s\t%s\t%s", + // action.c_str(), + // description, + // ((string)subKey).c_str() + // ); + // kfState.statisticsMap[buff]++; + // } + // { + // KFKey subKey; + // subKey.num = key.num; + // snprintf( + // buff, + // sizeof(buff), + // "%-25s\t%s\t%s", + // action.c_str(), + // description, + // ((string)subKey).c_str() + // ); + // kfState.statisticsMap[buff]++; + // } + // { + // KFKey subKey; + // subKey.type = key.type; + // snprintf( + // buff, + // sizeof(buff), + // "%-25s\t%s\t%s", + // action.c_str(), + // description, + // ((string)subKey).c_str() + // ); + // kfState.statisticsMap[buff]++; + // } + } + traceJson( + 0, + trace, + tsync, + {{"data", action}, + {Constants::Mongo::SAT_VAR, key.Sat.id()}, + {Constants::Mongo::STR_VAR, key.str}, + {Constants::Mongo::NUM_VAR, std::to_string(key.num)}}, // Eugene: convert num to code? + details + ); } /** Remove ambiguity states from filter when they deemed old or bad - * This effectively reinitialises them on the following epoch as a new state, and can be used for simple - * resolution of cycle-slips + * This effectively reinitialises them on the following epoch as a new state, and can be used for + * simple resolution of cycle-slips */ void removeBadAmbiguities( - Trace& trace, ///< Trace to output to - KFState& kfState, ///< Filter to remove states from - ReceiverMap& receiverMap) ///< List of stations containing observations for this epoch + Trace& trace, ///< Trace to output to + KFState& kfState, ///< Filter to remove states from + ReceiverMap& receiverMap ///< List of receivers containing observations for this epoch +) { - for (auto [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::AMBIGUITY) - { - continue; - } - - if (key.rec_ptr == nullptr) - { - continue; - } - - auto& rec = *key.rec_ptr; - auto& satStat = rec.satStatMap[key.Sat]; - - string preprocSigName; - string sigName; - - if (acsConfig.process_ppp) - { - E_ObsCode obsCode = E_ObsCode::_from_integral(key.num); - E_FType ft = code2Freq[key.Sat.sys][obsCode]; - - preprocSigName = ft2string(ft); - sigName = obsCode._to_string(); - } - else - { - E_FType ft = (E_FType) key.num; - - preprocSigName = ft2string(ft); //todo aaron, is this redundant now that network is gone? - sigName = preprocSigName; - } - - auto& sigStat = satStat.sigStatMap[sigName]; - auto& preprocSigStat = satStat.sigStatMap[preprocSigName]; - - if ( sigStat.lastPhaseTime != GTime::noTime() - &&(tsync - sigStat.lastPhaseTime).to_double() > acsConfig.ambErrors.outage_reset_limit) - { - sigStat.lastPhaseTime = GTime::noTime(); - - trace << "\n" << "Phase ambiguity removed due to long outage: " << sigName << " " << key; - - kfState.statisticsMap["Phase outage resets"]++; - - char buff[64]; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-OUTAGE", key.str.c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-OUTAGE", key.Sat.id().c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-TOTAL", key.str.c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-TOTAL", key.Sat.id().c_str()); kfState.statisticsMap[buff]++; - - kfState.removeState(key); - continue; - } - - if (sigStat.phaseRejectCount >= acsConfig.ambErrors.phase_reject_limit) - { - sigStat.phaseRejectCount = 0; - - trace << "\n" << "Phase ambiguity removed due to high reject count: " << sigName << " " << key; - - kfState.statisticsMap["Phase reject resets"]++; - - char buff[64]; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-REJECT", key.str.c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-REJECT", key.Sat.id().c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-TOTAL", key.str.c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-TOTAL", key.Sat.id().c_str()); kfState.statisticsMap[buff]++; - - kfState.removeState(key); - - InitialState init = initialStateFromConfig(acsConfig.getRecOpts("global").ion_stec); - - if (init.estimate == false) - { - KFKey kfKey = key; - for (kfKey.num = 0; kfKey.num < NUM_FTYPES; kfKey.num++) - { - kfState.removeState(kfKey); - continue; - } - } - } - - //reset slipping signals - if ( preprocSigStat.savedSlip.any - && ( (acsConfig.ambErrors.resetOnSlip.LLI && preprocSigStat.savedSlip.LLI) - ||(acsConfig.ambErrors.resetOnSlip.GF && preprocSigStat.savedSlip.GF) - ||(acsConfig.ambErrors.resetOnSlip.MW && preprocSigStat.savedSlip.MW) - ||(acsConfig.ambErrors.resetOnSlip.SCDIA && preprocSigStat.savedSlip.SCDIA))) - { - trace << "\n" << "Phase ambiguity removed due cycle slip detection: " << key; - - if (acsConfig.ambErrors.resetOnSlip.LLI && preprocSigStat.savedSlip.LLI) trace << "\t - LLI"; - if (acsConfig.ambErrors.resetOnSlip.GF && preprocSigStat.savedSlip.GF) trace << "\t - GF"; - if (acsConfig.ambErrors.resetOnSlip.MW && preprocSigStat.savedSlip.MW) trace << "\t - MW"; - if (acsConfig.ambErrors.resetOnSlip.SCDIA && preprocSigStat.savedSlip.SCDIA) trace << "\t - SCDIA"; - - kfState.statisticsMap["Cycle slip resets"]++; - - char buff[64]; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-PREPROC", key.str.c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-PREPROC", key.Sat.id().c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-TOTAL", key.str.c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-TOTAL", key.Sat.id().c_str()); kfState.statisticsMap[buff]++; - - kfState.removeState(key); - } - - //for ionosphere free, need to reset all connected singals - if (acsConfig.pppOpts.ionoOpts.use_if_combo) - for (auto& [sigNam, sigStat] : satStat.sigStatMap) - { - if (sigStat.savedSlip.any - &&( ( acsConfig.ambErrors.resetOnSlip.LLI && sigStat.savedSlip.LLI) - ||(acsConfig.ambErrors.resetOnSlip.GF && sigStat.savedSlip.GF) - ||(acsConfig.ambErrors.resetOnSlip.MW && sigStat.savedSlip.MW) - ||(acsConfig.ambErrors.resetOnSlip.SCDIA && sigStat.savedSlip.SCDIA))) - { - trace << "\n" << "Phase ambiguity removed due cycle slip detection: " << key; - - if (acsConfig.ambErrors.resetOnSlip.LLI && sigStat.savedSlip.LLI) trace << "\t - LLI"; - if (acsConfig.ambErrors.resetOnSlip.GF && sigStat.savedSlip.GF) trace << "\t - GF"; - if (acsConfig.ambErrors.resetOnSlip.MW && sigStat.savedSlip.MW) trace << "\t - MW"; - if (acsConfig.ambErrors.resetOnSlip.SCDIA && sigStat.savedSlip.SCDIA) trace << "\t - SCDIA"; - - kfState.statisticsMap["Cycle slip resets*"]++; - - char buff[64]; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-PREPROC", key.str.c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-PREPROC", key.Sat.id().c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-TOTAL", key.str.c_str()); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Ambiguity Reset-%4s-TOTAL", key.Sat.id().c_str()); kfState.statisticsMap[buff]++; - - kfState.removeState(key); - } - } - } - - for (auto& [id, rec] : receiverMap) - for (auto& [sat, satStat] : rec.satStatMap) - for (auto& [sig, sigStat] : satStat.sigStatMap) - { - sigStat.savedSlip.any = false; - } + for (auto& [testKey, dummy] : kfState.kfIndexMap) + for (auto& key : kfState.decomposedStateKeys(testKey)) + { + if (key.type != KF::AMBIGUITY) + { + continue; + } + + if (key.rec_ptr == nullptr) + { + continue; + } + + auto& rec = *key.rec_ptr; + auto& satStat = rec.satStatMap[key.Sat]; + + string preprocSigName; + string sigName; + + if (acsConfig.process_ppp) + { + E_ObsCode obsCode = int_to_enum(key.num); + E_FType ft = code2Freq[key.Sat.sys][obsCode]; + + preprocSigName = ft2string(ft); + sigName = enum_to_string(obsCode); + } + else + { + E_FType ft = (E_FType)key.num; + + preprocSigName = + ft2string(ft); // todo aaron, is this redundant now that network is gone? + sigName = preprocSigName; + } + + auto& sigStat = satStat.sigStatMap[sigName]; + auto& preprocSigStat = satStat.sigStatMap[preprocSigName]; + + if (sigStat.phaseRejectCount >= acsConfig.ambErrors.phase_reject_limit) + { + sigStat.phaseRejectCount = + 0; // Reset phase reject count once an ambiguity state is removed + + addRejectDetails(tsync, trace, kfState, key, "Ambiguity Removed", "REJECT"); + + kfState.removeState(key); + + InitialState init = initialStateFromConfig(acsConfig.getRecOpts("global").ion_stec); + + if (init.estimate == false) + { + KFKey kfKey = key; + for (kfKey.num = 0; kfKey.num < NUM_FTYPES; kfKey.num++) + { + kfState.removeState(kfKey); + continue; + } + } + } + + // reset slipping signals + 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.GF && preprocSigStat.savedSlip.GF) || + (acsConfig.ambErrors.resetOnSlip.MW && preprocSigStat.savedSlip.MW) || + (acsConfig.ambErrors.resetOnSlip.SCDIA && preprocSigStat.savedSlip.SCDIA))) + { + vector details; + + if (acsConfig.ambErrors.resetOnSlip.LLI && preprocSigStat.savedSlip.LLI) + details.push_back({"LLI", true}); + if (acsConfig.ambErrors.resetOnSlip.GF && preprocSigStat.savedSlip.GF) + details.push_back({"GF", true}); + if (acsConfig.ambErrors.resetOnSlip.MW && preprocSigStat.savedSlip.MW) + details.push_back({"MW", true}); + if (acsConfig.ambErrors.resetOnSlip.SCDIA && preprocSigStat.savedSlip.SCDIA) + details.push_back({"SCDIA", true}); + if (acsConfig.ambErrors.resetOnSlip.retrack && preprocSigStat.savedSlip.retrack) + details.push_back({"retrack", true}); + if (acsConfig.ambErrors.resetOnSlip.single_freq && + preprocSigStat.savedSlip.singleFreq) + details.push_back({"singleFreq", true}); + + addRejectDetails( + tsync, + trace, + kfState, + key, + "Ambiguity Removed", + "PREPROC", + details + ); + + kfState.removeState(key); + } + + // for ionosphere free, need to reset all connected singals + if (acsConfig.pppOpts.ionoOpts.use_if_combo) + for (auto& [sigNam, sigStat] : satStat.sigStatMap) + { + if (sigStat.savedSlip.any && + ((acsConfig.ambErrors.resetOnSlip.LLI && sigStat.savedSlip.LLI) || + (acsConfig.ambErrors.resetOnSlip.GF && sigStat.savedSlip.GF) || + (acsConfig.ambErrors.resetOnSlip.retrack && sigStat.savedSlip.retrack) || + (acsConfig.ambErrors.resetOnSlip.single_freq && + sigStat.savedSlip.singleFreq) || + (acsConfig.ambErrors.resetOnSlip.MW && sigStat.savedSlip.MW) || + (acsConfig.ambErrors.resetOnSlip.SCDIA && sigStat.savedSlip.SCDIA))) + { + vector details; + + if (acsConfig.ambErrors.resetOnSlip.LLI && sigStat.savedSlip.LLI) + details.push_back({"LLI", true}); + if (acsConfig.ambErrors.resetOnSlip.GF && sigStat.savedSlip.GF) + details.push_back({"GF", true}); + if (acsConfig.ambErrors.resetOnSlip.MW && sigStat.savedSlip.MW) + details.push_back({"MW", true}); + if (acsConfig.ambErrors.resetOnSlip.SCDIA && sigStat.savedSlip.SCDIA) + details.push_back({"SCDIA", true}); + if (acsConfig.ambErrors.resetOnSlip.single_freq && + sigStat.savedSlip.singleFreq) + details.push_back({"singleFreq", true}); + if (acsConfig.ambErrors.resetOnSlip.retrack && sigStat.savedSlip.retrack) + details.push_back({"retrack", true}); + + addRejectDetails( + tsync, + trace, + kfState, + key, + "Ambiguity Removed", + "PREPROC", + details + ); + + kfState.removeState(key); + } + } + } + + for (auto& [id, rec] : receiverMap) + for (auto& [sat, satStat] : rec.satStatMap) + for (auto& [sig, sigStat] : satStat.sigStatMap) + { + sigStat.savedSlip.any = false; + } } -void removeBadReceivers( - Trace& trace, ///< Trace to output to - KFState& kfState, ///< Filter to remove states from - ReceiverMap& receiverMap) ///< List of stations containing observations for this epoch +void removeBadSatellites( + Trace& trace, ///< Trace to output to + KFState& kfState ///< Filter to remove states from +) { - if (acsConfig.errorAccumulation.enable == false) - { - return; - } - - for (auto& [id, rec] : receiverMap) - { - if (rec.receiverErrorCount >= acsConfig.errorAccumulation.receiver_error_count_threshold) rec.receiverErrorEpochs++; - else rec.receiverErrorEpochs = 0; - - rec.receiverErrorCount = 0; - - if (rec.receiverErrorEpochs < acsConfig.errorAccumulation.receiver_error_epochs_threshold) - { - continue; - } - - rec.receiverErrorEpochs = 0; - - for (auto [key, index] : kfState.kfIndexMap) - if (key.str == rec.id) - { - trace << "\n" << "State removed due to high receiver error counts: " << key; - - kfState.removeState(key); - } + if (acsConfig.errorAccumulation.enable == false) + { + return; + } + + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (acsConfig.errorAccumulation.satellite_error_count_threshold > 0 && + satNav.satelliteErrorCount >= + acsConfig.errorAccumulation.satellite_error_count_threshold) + { + satNav.satelliteErrorEpochs++; + + char idStr[100]; + snprintf(idStr, sizeof(idStr), "%10s\t%4s\t%4s\t%5s", "", Sat.id().c_str(), "", ""); + + trace << "\n" + << kfState.time << "\tIncrementing satelliteErrorEpochs on\t" << idStr << "\tto " + << satNav.satelliteErrorEpochs; + } + else + { + satNav.satelliteErrorEpochs = + 0; // Reset error epochs if error count doen't exceed threshold at this epoch + } + + satNav.satelliteErrorCount = 0; // Reset error count each epoch + + if (acsConfig.errorAccumulation.satellite_error_epochs_threshold <= 0 || + satNav.satelliteErrorEpochs < + acsConfig.errorAccumulation.satellite_error_epochs_threshold) + { + continue; + } + + satNav.satelliteErrorEpochs = 0; // Reset error epochs once a satellite is removed + + for (auto [key, index] : kfState.kfIndexMap) + if (key.Sat == Sat) + { + kfState.removeState(key); + + trace << "\n" + << "State removed due to high satellite error counts: " << key; + } + + kfState.statisticsMap["Sat error resets"]++; + } +} - kfState.statisticsMap["Rec error resets"]++; - } +void removeBadReceivers( + Trace& trace, ///< Trace to output to + KFState& kfState, ///< Filter to remove states from + ReceiverMap& receiverMap ///< List of receivers containing observations for this epoch +) +{ + if (acsConfig.errorAccumulation.enable == false) + { + return; + } + + for (auto& [id, rec] : receiverMap) + { + if (acsConfig.errorAccumulation.receiver_error_count_threshold > 0 && + rec.receiverErrorCount >= acsConfig.errorAccumulation.receiver_error_count_threshold) + { + rec.receiverErrorEpochs++; + + char idStr[100]; + snprintf(idStr, sizeof(idStr), "%10s\t%4s\t%4s\t%5s", "", id.c_str(), "", ""); + + trace << "\n" + << kfState.time << "\tIncrementing receiverErrorEpochs on\t" << idStr << "\tto " + << rec.receiverErrorEpochs; + } + else + { + rec.receiverErrorEpochs = + 0; // Reset error epochs if error count doen't exceed threshold at this epoch + } + + rec.receiverErrorCount = 0; // Reset error count each epoch + + if (acsConfig.errorAccumulation.receiver_error_epochs_threshold <= 0 || + rec.receiverErrorEpochs < acsConfig.errorAccumulation.receiver_error_epochs_threshold) + { + continue; + } + + rec.receiverErrorEpochs = 0; // Reset error epochs once a receiver is removed + + for (auto [key, index] : kfState.kfIndexMap) + if (key.str == rec.id) + { + kfState.removeState(key); + + trace << "\n" + << "State removed due to high receiver error counts: " << key; + } + + kfState.statisticsMap["Rec error resets"]++; + + rec.sol.clkAdjustReady = false; + } } /** Remove ambiguity states from filter when they deemed old or bad - * This effectively reinitialises them on the following epoch as a new state, and can be used for simple - * resolution of cycle-slips + * This effectively reinitialises them on the following epoch as a new state, and can be used for + * simple resolution of cycle-slips */ void removeBadIonospheres( - Trace& trace, ///< Trace to output to - KFState& kfState) ///< Filter to remove states from + Trace& trace, ///< Trace to output to + KFState& kfState ///< Filter to remove states from +) { - for (auto [key, index] : kfState.kfIndexMap) - { - if (key.type == KF::IONOSPHERIC) - { - double state; - double variance; - kfState.getKFValue(key, state, &variance); - - auto& recOpts = acsConfig.getRecOpts(key.str); - - if (variance > SQR(recOpts.iono_sigma_limit)) - { - trace << "\n" << "Ionosphere removed due to high variance: " << key; - - kfState.removeState(key); - } - - if (key.rec_ptr == nullptr) - { - auto& rec = *key.rec_ptr; - auto& satStat = rec.satStatMap[key.Sat]; - - if ( satStat.lastIonTime != GTime::noTime() - &&(tsync - satStat.lastIonTime).to_double() > acsConfig.ionErrors.outage_reset_limit) - { - kfState.removeState(key); - } - } - - continue; - } - - if (key.type != KF::IONO_STEC) - { - continue; - } - - if (key.rec_ptr == nullptr) - { - continue; - } - - auto& rec = *key.rec_ptr; - auto& satStat = rec.satStatMap[key.Sat]; - auto& recOpts = acsConfig.getRecOpts(key.str); - - if ( satStat.lastIonTime != GTime::noTime() - &&(tsync - satStat.lastIonTime).to_double() > acsConfig.ionErrors.outage_reset_limit) - { - satStat.lastIonTime = GTime::noTime(); - - trace << "\n" << "Ionosphere removed due to long outage: " << key; - - kfState.statisticsMap["Iono outage resets"]++; - - kfState.removeState(key); - continue; - } - - double state; - double variance; - kfState.getKFValue(key, state, &variance); - - if (variance > SQR(recOpts.iono_sigma_limit)) - { - trace << "\n" << "Ionosphere removed due to high variance: " << key; - - kfState.removeState(key); - } - } + for (auto [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::IONOSPHERIC) + { + continue; + } + + auto& recOpts = acsConfig.getRecOpts(key.str); + + if (key.rec_ptr == nullptr) + { + auto& rec = *key.rec_ptr; + auto& satStat = rec.satStatMap[key.Sat]; + + if (satStat.lastIonTime != GTime::noTime() && + (tsync - satStat.lastIonTime).to_double() > + acsConfig.ionErrors.outage_reset_limit) // Eugene: don't think this ever works + { + kfState.removeState(key); + + trace << "\n" + << "State removed due to long ionosphere signal outage: " << key; + } + } + } } -void postFilterChecks( - const GTime& time, - KFMeas& kfMeas) +void updateSppPppClkOffsets(ReceiverMap& receiverMap, KFState& kfState) { - for (int i = 0; i < kfMeas.V.rows(); i++) - { - resetPhaseSignalError (time, kfMeas, i); - resetPhaseSignalOutage (time, kfMeas, i); - resetIonoSignalOutage (time, kfMeas, i); - } + if (acsConfig.adjust_rec_clocks_by_spp == false) + { + return; + } + + for (auto& [id, rec] : receiverMap) + { + if (rec.isPseudoRec) + { + continue; + } + + if (rec.sol.status != E_Solution::SINGLE) + { + continue; + } + + auto trace = getTraceFile(rec); + auto& recOpts = acsConfig.getRecOpts(id); + + InitialState init = initialStateFromConfig(recOpts.clk); + + if (init.estimate == false) + { + continue; + } + + KFKey clkKey; + clkKey.type = KF::REC_CLOCK; + clkKey.str = id; + clkKey.rec_ptr = &rec; + + double recClk_m = 0; + E_Source foundSrc = kfState.getKFValue(clkKey, recClk_m); + + if (foundSrc != E_Source::NONE) + { + rec.sol.sppPppClkOffset = + recClk_m - rec.sol.sppClk; // Save SPP to PPP clock offset for next epoch + rec.sol.clkAdjustReady = true; + } + } } -/* write solution status for PPP -*/ -void outputPppNmea( - Trace& trace, - KFState& kfState, - string id) +void postFilterChecks(const GTime& time, ReceiverMap& receiverMap, KFState& kfState, KFMeas& kfMeas) { - GWeek week = kfState.time; - GTow tow = kfState.time; - - Block block(trace, "NMEA"); - - for (auto& [kfKey, index] : kfState.kfIndexMap) - { - KFKey key = kfKey; - if ( key.type == KF::REC_POS - &&key.num == 0 - &&key.str == id) - { - double x[3] = {}; - double v[3] = {}; - - for (key.num = 0; key.num < 3; key.num++) - { - kfState.getKFValue(key, x[key.num], &v[key.num]); - } - tracepdeex(0, trace, "$POS,%d,%.3f,%.4f,%.4f,%.4f,%.7f,%.7f,%.7f\n", - week, - tow, - x[0], - x[1], - x[2], - sqrt(v[0]), - sqrt(v[1]), - sqrt(v[2])); - } - -// if (key.type == KF::PHASE_BIAS) -// { -// double phase_bias = 0; -// double phase_biasVar = 0; -// kfState.getKFValue(key, phase_bias, &phase_biasVar); -// tracepdeex(1, trace, "$AMB,%d,%.3f,%d,%s,%d,%.4f,%.7f\n", -// week, -// tow, -// solStat, -// key.Sat.id().c_str(), -// key.num, -// phase_bias, -// sqrt(phase_biasVar)); -// } - - if ( key.type == KF::TROP //todo aaron needs iteration - &&key.str == id) - { - string grad; - double trop = 0; - double tropVar = 0; - if (key.num == 1) grad = "_N"; - if (key.num == 2) grad = "_E"; - kfState.getKFValue(key, trop, &tropVar); - tracepdeex(0, trace, "$TROP%s,%d,%.3f,%f,%.7f\n", - grad.c_str(), - week, - tow, - trop, - sqrt(tropVar)); - } - - if ( key.type == KF::REC_SYS_BIAS - &&key.Sat == SatSys(E_Sys::GPS) - &&key.str == id) - { - double rClkGPS = 0; - double rClkGLO = 0; - double rClkGAL = 0; - double rClkBDS = 0; - double GPSclkVar = 0; - double GLOclkVar = 0; - double GALclkVar = 0; - double BDSclkVar = 0; - - key.Sat = SatSys(E_Sys::GPS); kfState.getKFValue(key, rClkGPS, &GPSclkVar); - key.Sat = SatSys(E_Sys::GLO); kfState.getKFValue(key, rClkGLO, &GLOclkVar); - key.Sat = SatSys(E_Sys::GAL); kfState.getKFValue(key, rClkGAL, &GALclkVar); - key.Sat = SatSys(E_Sys::BDS); kfState.getKFValue(key, rClkBDS, &BDSclkVar); - - tracepdeex(0, trace, "$CLK,%d,%.3f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f\n", - week, - tow, - rClkGPS * 1E9 / CLIGHT, - rClkGLO * 1E9 / CLIGHT, - rClkGAL * 1E9 / CLIGHT, - rClkBDS * 1E9 / CLIGHT, - sqrt(GPSclkVar) * 1E9 / CLIGHT, - sqrt(GLOclkVar) * 1E9 / CLIGHT, - sqrt(GALclkVar) * 1E9 / CLIGHT, - sqrt(BDSclkVar) * 1E9 / CLIGHT); - } - } -// -// /* receiver velocity and acceleration */ -// { -// ecef2pos(rtk->sol.rr, pos); -// ecef2enu(pos, rtk->xx + 3, vel); -// ecef2enu(pos, rtk->xx + 6, acc); -// p += sprintf(p, "$VELACC,%d,%.3f,%d,%.4f,%.4f,%.4f,%.5f,%.5f,%.5f,%.4f,%.4f," -// "%.4f,%.5f,%.5f,%.5f\n", week, tow, rtk->sol.stat, vel[0], vel[1], -// vel[2], acc[0], acc[1], acc[2], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); -// } + kfState.errorCountMap.clear(); // Reset all state error counts each epoch + + for (int i = 0; i < kfMeas.V.rows(); i++) + { + resetPhaseSignalError(time, kfMeas, i); + resetIonoSignalOutage(time, kfMeas, i); + } + + updateSppPppClkOffsets(receiverMap, kfState); } +/* write solution status for PPP + */ +void outputPppNmea(Trace& trace, KFState& kfState, string id) +{ + GWeek week = kfState.time; + GTow tow = kfState.time; + + Block block(trace, "NMEA"); + + for (auto& [kfKey, index] : kfState.kfIndexMap) + { + KFKey key = kfKey; + if (key.type == KF::REC_POS && key.num == 0 && key.str == id) + { + double x[3] = {}; + double v[3] = {}; + + for (key.num = 0; key.num < 3; key.num++) + { + kfState.getKFValue(key, x[key.num], &v[key.num]); + } + tracepdeex( + 0, + trace, + "$POS,%d,%.3f,%.4f,%.4f,%.4f,%.7f,%.7f,%.7f\n", + week, + tow, + x[0], + x[1], + x[2], + sqrt(v[0]), + sqrt(v[1]), + sqrt(v[2]) + ); + } + + // if (key.type == KF::PHASE_BIAS) + // { + // double phase_bias = 0; + // double phase_biasVar = 0; + // kfState.getKFValue(key, phase_bias, &phase_biasVar); + // tracepdeex(1, trace, "$AMB,%d,%.3f,%d,%s,%d,%.4f,%.7f\n", + // week, + // tow, + // solStat, + // key.Sat.id().c_str(), + // key.num, + // phase_bias, + // sqrt(phase_biasVar)); + // } + + if (key.type == KF::TROP // todo aaron needs iteration + && key.str == id) + { + string grad; + double trop = 0; + double tropVar = 0; + if (key.num == 1) // Eugene: key.type == KF::TROP_GRAD? + grad = "_N"; + if (key.num == 2) // Eugene: key.type == KF::TROP_GRAD? + grad = "_E"; + kfState.getKFValue(key, trop, &tropVar); + tracepdeex( + 0, + trace, + "$TROP%s,%d,%.3f,%f,%.7f\n", + grad.c_str(), + week, + tow, + trop, + sqrt(tropVar) + ); + } + + if (key.type == KF::REC_SYS_BIAS && key.Sat == SatSys(E_Sys::GPS) && key.str == id) + { + double rClkGPS = 0; + double rClkGLO = 0; + double rClkGAL = 0; + double rClkBDS = 0; + double GPSclkVar = 0; + double GLOclkVar = 0; + double GALclkVar = 0; + double BDSclkVar = 0; + + key.Sat = SatSys(E_Sys::GPS); + kfState.getKFValue(key, rClkGPS, &GPSclkVar); + key.Sat = SatSys(E_Sys::GLO); + kfState.getKFValue(key, rClkGLO, &GLOclkVar); + key.Sat = SatSys(E_Sys::GAL); + kfState.getKFValue(key, rClkGAL, &GALclkVar); + key.Sat = SatSys(E_Sys::BDS); + kfState.getKFValue(key, rClkBDS, &BDSclkVar); + + tracepdeex( + 0, + trace, + "$CLK,%d,%.3f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f\n", + week, + tow, + rClkGPS * 1E9 / CLIGHT, + rClkGLO * 1E9 / CLIGHT, + rClkGAL * 1E9 / CLIGHT, + rClkBDS * 1E9 / CLIGHT, + sqrt(GPSclkVar) * 1E9 / CLIGHT, + sqrt(GLOclkVar) * 1E9 / CLIGHT, + sqrt(GALclkVar) * 1E9 / CLIGHT, + sqrt(BDSclkVar) * 1E9 / CLIGHT + ); + } + } + // + // /* receiver velocity and acceleration */ + // { + // ecef2pos(rtk->sol.rr, pos); + // ecef2enu(pos, rtk->xx + 3, vel); + // ecef2enu(pos, rtk->xx + 6, acc); + // p += sprintf(p, "$VELACC,%d,%.3f,%d,%.4f,%.4f,%.4f,%.5f,%.5f,%.5f,%.4f,%.4f," + // "%.4f,%.5f,%.5f,%.5f\n", week, tow, rtk->sol.stat, vel[0], vel[1], + // vel[2], acc[0], acc[1], acc[2], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + // } +} -double netResidualAndChainOutputs( - Trace& trace, - Observation& obs, - KFMeasEntry& measEntry) +double netResidualAndChainOutputs(Trace& trace, Observation& obs, KFMeasEntry& measEntry) { - double residual = 0; - double residualVar = 0; - - for (auto& [component, details] : measEntry.componentsMap) - { - auto& [componentVal, eq, var] = details; - - residual -= componentVal; - - if (var > 0) - { - residualVar += var; - } - - if (acsConfig.output_residual_chain) - { - tracepdeex(0, trace, "\n"); - tracepdeex(4, trace, "%s", obs.time.to_string()); - tracepdeex(3, trace, "%30s", ((string)measEntry.obsKey).c_str()); - tracepdeex(0, trace, " %-23s %+14.4f", component._to_string(), -componentVal); - - if (var >= 0) tracepdeex(2, trace, " ~ %5.3e", var); - else if (var == 0) tracepdeex(2, trace, " ~ 0 "); - else tracepdeex(2, trace, " ~ Estimated"); - - tracepdeex(2, trace, " -> %13.4f", residual); - tracepdeex(3, trace, " ~ %.2e ", residualVar); - } - } - - for (auto& [component, details] : measEntry.componentsMap) - { - auto& [componentVal, eq, var] = details; - - if (var > 100) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Unestimated component '" << component._to_string() << "' for '" << measEntry.obsKey - << "' has large variance (" << var << "), valid inputs may not (yet) be available"; - - trace << "\n" - << "Warning: Unestimated component '" << component._to_string() << "' for '" << measEntry.obsKey - << "' has large variance (" << var << "), valid inputs may not (yet) be available"; - } - } - - if (acsConfig.output_residual_chain) - { - trace << "\n" << "\n" << "0 ="; - - for (auto& [component, details] : measEntry.componentsMap) - { - auto& [componentVal, eq, var] = details; - - tracepdeex(0, trace, " %s", eq.c_str()); - } - } - - if (abs(residual) > 1e30) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: " << measEntry.obsKey << " has very large residual: " << residual; - } - - return residual; + double residual = 0; + double residualVar = 0; + + for (auto& [component, details] : measEntry.componentsMap) + { + auto& [componentVal, eq, var] = details; + + residual -= componentVal; + + if (var > 0) + { + residualVar += var; + } + + if (acsConfig.output_residual_chain) + { + tracepdeex(0, trace, "\n"); + tracepdeex(4, trace, "%s", obs.time.to_string()); + tracepdeex(3, trace, "%30s", ((string)measEntry.obsKey).c_str()); + tracepdeex(0, trace, " %-23s %+14.4f", enum_to_string(component), -componentVal); + + if (var >= 0) + tracepdeex(2, trace, " ~ %5.3e", var); + else if (var == 0) + tracepdeex(2, trace, " ~ 0 "); + else + tracepdeex(2, trace, " ~ Estimated"); + + tracepdeex(2, trace, " -> %13.4f", residual); + tracepdeex(3, trace, " ~ %.2e ", residualVar); + } + } + + for (auto& [component, details] : measEntry.componentsMap) + { + auto& [componentVal, eq, var] = details; + + if (var > 100) + { + BOOST_LOG_TRIVIAL(warning) << "Unestimated component '" << enum_to_string(component) + << "' for '" << measEntry.obsKey << "' has large variance (" + << var << "), valid inputs may not (yet) be available"; + + trace << "\n" + << "Warning: Unestimated component '" << enum_to_string(component) << "' for '" + << measEntry.obsKey << "' has large variance (" << var + << "), valid inputs may not (yet) be available"; + } + } + + if (acsConfig.output_residual_chain) + { + trace << "\n" + << "\n" + << "0 ="; + + for (auto& [component, details] : measEntry.componentsMap) + { + auto& [componentVal, eq, var] = details; + + tracepdeex(0, trace, " %s", eq.c_str()); + } + } + + if (abs(residual) > 1e30) + { + BOOST_LOG_TRIVIAL(warning) << measEntry.obsKey << " has very large residual: " << residual; + } + + return residual; } diff --git a/src/cpp/pea/ppp.hpp b/src/cpp/pea/ppp.hpp index d24d98c9a..cc1500ab3 100644 --- a/src/cpp/pea/ppp.hpp +++ b/src/cpp/pea/ppp.hpp @@ -1,14 +1,10 @@ - #pragma once #include +#include "common/algebra.hpp" using std::map; - - -#include "algebra.hpp" - struct PhaseCenterData; struct FrameSwapper; struct Observation; @@ -21,281 +17,196 @@ struct ObsList; struct GTime; struct Vmf3; struct GObs; +struct ERPValues; +struct AzEl; +struct SatPos; +double relativity2(VectorEcef& rSat, VectorEcef& rRec); -double relativity2( - VectorEcef& rSat, - VectorEcef& rRec); - -double recAntDelta( - VectorEcef& e, - Receiver& rec); +double recAntDelta(VectorEcef& e, Receiver& rec); -tuple tideDelta( - Trace& trace, - GTime time, - Receiver& rec, - VectorEcef& rRec, - ReceiverOptions& recOpts); +tuple +tideDelta(Trace& trace, GTime time, Receiver& rec, VectorEcef& rRec, ReceiverOptions& recOpts); void eopAdjustment( - GTime& time, - VectorEcef& e, - ERPValues& erpv, - FrameSwapper& frameSwapper, - Receiver& rec, - VectorEcef& rRec, - KFMeasEntry& measEntry, - const KFState& kfState); - -double netResidualAndChainOutputs( - Trace& trace, - Observation& obs, - KFMeasEntry& measEntry); - -void removeUnmeasuredAmbiguities( - Trace& trace, - KFState& kfState, - map measuredStates); - -void outputPppNmea( - Trace& trace, - KFState& kfState, - string id); + GTime& time, + VectorEcef& e, + ERPValues& erpv, + FrameSwapper& frameSwapper, + Receiver& rec, + VectorEcef& rRec, + KFMeasEntry& measEntry, + KFState& kfState +); + +double netResidualAndChainOutputs(Trace& trace, Observation& obs, KFMeasEntry& measEntry); + +void outputPppNmea(Trace& trace, KFState& kfState, string id); void spp( - Trace& trace, - ObsList& obsList, - Solution& sol, - string id, - KFState* kfState_ptr = nullptr, - KFState* remote_ptr = nullptr); - -void testEclipse( - ObsList& obsList); - -void pppCorrections( - Trace& trace, - ObsList& obsList, - Vector3d& rRec, - Receiver& rec); - -void ppp( - Trace& trace, - ReceiverMap& receiverMap, - KFState& kfState, - KFState& remoteState); - -void phaseWindup( - GObs& obs, - Receiver& rec, - double& phw); + Trace& trace, + Receiver& rec, + KFState* kfState_ptr = nullptr, + KFState* remote_ptr = nullptr +); + +void testEclipse(ObsList& obsList); + +void pppCorrections(Trace& trace, ObsList& obsList, Vector3d& rRec, Receiver& rec); + +void ppp(Trace& trace, ReceiverMap& receiverMap, KFState& kfState, KFState& remoteState); + +void phaseWindup(GObs& obs, Receiver& rec, double& phw); bool ionoModel( - GTime& time, - VectorPos& pos, - AzEl& azel, - E_IonoMapFn mapFn, - E_IonoMode mode, - double layerHeight, - double ionoState, - double& dion, - double& var); - -void outputApriori( - ReceiverMap& receiverMap); + GTime& time, + VectorPos& pos, + AzEl& azel, + E_IonoMapFn mapFn, + E_IonoMode mode, + double layerHeight, + double ionoState, + double& dion, + double& var +); + +void outputApriori(ReceiverMap& receiverMap); + +void updateAprioriRecPos( + Trace& trace, + Receiver& rec, + ReceiverOptions& recOpts, + bool& sppUsed, + KFState* remote_ptr = nullptr +); + +void updateAprioriRecClk( + Trace& trace, + Receiver& rec, + ReceiverOptions& recOpts, + GTime& time, + KFState& kfState, + KFState* remote_ptr = nullptr +); void selectAprioriSource( - Trace& trace, - Receiver& rec, - GTime& time, - bool& sppUsed, - KFState& kfState, - KFState* remote_ptr = nullptr); + Trace& trace, + Receiver& rec, + GTime& time, + bool& sppUsed, + KFState& kfState, + KFState* remote_ptr = nullptr +); -void selectAprioriSource( - SatSys& Sat, - GTime& time, - KFState& kfState, - KFState* remote_ptr = nullptr); +void selectAprioriSource(SatSys& Sat, GTime& time, KFState& kfState, KFState* remote_ptr = nullptr); void postFilterChecks( - const GTime& time, - KFMeas& kfMeas); - -bool deweightMeas( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit); - -bool pseudoMeasTest( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit); - -bool deweightStationMeas( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit); - -bool countSignalErrors( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit); - -bool incrementPhaseSignalError( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit); - -bool incrementReceiverError( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit); - -bool resetPhaseSignalError( - const GTime& time, - KFMeas& kfMeas, - int index); - -bool resetPhaseSignalOutage( - const GTime& time, - KFMeas& kfMeas, - int index); - -bool resetIonoSignalOutage( - const GTime& time, - KFMeas& kfMeas, - int index); - -bool rejectByState( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - const KFKey& kfKey, - bool postFit); - -bool clockGlitchReaction( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - const KFKey& kfKey, - bool postFit); - -bool orbitGlitchReaction( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - const KFKey& kfKey, - bool postFit); - - + const GTime& time, + ReceiverMap& receiverMap, + KFState& kfState, + KFMeas& kfMeas +); + +bool deweightMeas(RejectCallbackDetails rejectDetails); +bool pseudoMeasTest(RejectCallbackDetails rejectDetails); +bool deweightStationMeas(RejectCallbackDetails rejectDetails); +bool incrementPhaseSignalError(RejectCallbackDetails rejectDetails); +bool incrementReceiverErrors(RejectCallbackDetails rejectDetails); +bool incrementSatelliteErrors(RejectCallbackDetails rejectDetails); +bool incrementStateErrors(RejectCallbackDetails rejectDetails); +bool rejectWorstMeasByState(RejectCallbackDetails rejectDetails); +bool rejectAllMeasByState(RejectCallbackDetails rejectDetails); +bool clockGlitchReaction(RejectCallbackDetails rejectDetails); +bool satelliteGlitchReaction(RejectCallbackDetails rejectDetails); +bool relaxState(RejectCallbackDetails rejectDetails); + +void resetPhaseSignalError(const GTime& time, KFMeas& kfMeas, int index); +void resetIonoSignalOutage(const GTime& time, KFMeas& kfMeas, int index); void receiverUducGnss( - Trace& pppTrace, - Receiver& rec, - const KFState& kfState, - KFMeasEntryList& kfMeasEntryList, - const KFState& remoteState); + Trace& pppTrace, + Receiver& rec, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList, + KFState& remoteState +); void orbitPseudoObs( - Trace& pppTrace, - Receiver& rec, - const KFState& kfState, - KFMeasEntryList& kfMeasEntryList); + Trace& pppTrace, + Receiver& rec, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList +); -void initPseudoObs( - Trace& pppTrace, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList); +void initPseudoObs(Trace& pppTrace, KFState& kfState, KFMeasEntryList& kfMeasEntryList); -void filterPseudoObs( - Trace& pppTrace, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList); +void filterPseudoObs(Trace& pppTrace, KFState& kfState, KFMeasEntryList& kfMeasEntryList); void receiverPseudoObs( - Trace& pppTrace, - Receiver& rec, - const KFState& kfState, - KFMeasEntryList& kfMeasEntryList, - ReceiverMap& receiverMap); - + Trace& pppTrace, + Receiver& rec, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList, + ReceiverMap& receiverMap +); -void readPseudosFromFile( - string& file); +void readPseudosFromFile(string& file); void receiverSlr( - Trace& pppTrace, - Receiver& rec, - const KFState& kfState, - KFMeasEntryList& kfMeasEntryList); + Trace& pppTrace, + Receiver& rec, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList +); +bool satQuat(SatPos& satPos, vector attitudeTypes, Quaterniond& quat); -bool satQuat( - SatPos& satPos, - vector attitudeTypes, - Quaterniond& quat); - -void fixAndHoldAmbiguities( - Trace& trace, - KFState& kfState); +void fixAndHoldAmbiguities(Trace& trace, KFState& kfState); bool queryBiasUC( - Trace& trace, - GTime time, - KFState& kfState, - SatSys sat, - string rec, - E_ObsCode code, - double& bias, - double& vari, - E_MeasType typ); - -void pseudoRecDcb( - Trace& trace, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList); - -void ambgPseudoObs( - Trace& trace, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList); + Trace& trace, + GTime time, + KFState& kfState, + SatSys sat, + string rec, + E_ObsCode code, + double& bias, + double& vari, + E_MeasType type +); + +void pseudoRecDcb(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList); + +void ambgPseudoObs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList); + +void phasePseudoObs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList); void ionoPseudoObs( - Trace& trace, - ReceiverMap& receiverMap, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList); + Trace& trace, + ReceiverMap& receiverMap, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList +); void tropPseudoObs( - Trace& trace, - ReceiverMap& receiverMap, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList); - -void satClockPivotPseudoObs( - Trace& trace, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList); - -KFState propagateUncertainty( - Trace& trace, - KFState& kfState); - -void explainMeasurements( - Trace& trace, - KFMeas& meas, - KFState& kfState); + Trace& trace, + ReceiverMap& receiverMap, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList +); + +void satClockPivotPseudoObs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList); + +KFState propagateUncertainty(Trace& trace, KFState& kfState); + +void explainMeasurements(Trace& trace, KFMeas& meas, KFState& kfState); + +void addRejectDetails( + const GTime& time, + Trace& trace, + KFState& kfState, + const KFKey& key, + const string& action, + const string& reason, + vector details = {} +); diff --git a/src/cpp/pea/ppp_ambres.cpp b/src/cpp/pea/ppp_ambres.cpp index f99e0f4a3..71fd434c7 100644 --- a/src/cpp/pea/ppp_ambres.cpp +++ b/src/cpp/pea/ppp_ambres.cpp @@ -1,349 +1,366 @@ - // #pragma GCC optimize ("O0") - /**------------------------------------------------------------------------------ -* reference : -* [1] P.J.G.Teunissen, The least-square ambiguity decorrelation adjustment: -* a method for fast GPS ambiguity estimation, J.Geodesy, Vol.70, 65-82, -* 1995 -* [2] X.-W.Chang, X.Yang, T.Zhou, MLAMBDA: A modified LAMBDA method for -* integer least-squares estimation, J.Geodesy, Vol.79, 552-565, 2005 -*-----------------------------------------------------------------------------*/ + * reference : + * [1] P.J.G.Teunissen, The least-square ambiguity decorrelation adjustment: + * a method for fast GPS ambiguity estimation, J.Geodesy, Vol.70, 65-82, + * 1995 + * [2] X.-W.Chang, X.Yang, T.Zhou, MLAMBDA: A modified LAMBDA method for + * integer least-squares estimation, J.Geodesy, Vol.79, 552-565, 2005 + *-----------------------------------------------------------------------------*/ #include #include - -#include "eigenIncluder.hpp" -#include "GNSSambres.hpp" -#include "acsConfig.hpp" -#include "algebra.hpp" -#include "biases.hpp" -#include "common.hpp" -#include "trace.hpp" +#include "ambres/GNSSambres.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/biases.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/trace.hpp" static bool filterError = false; -bool recordFilterError( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit) +bool recordFilterError(RejectCallbackDetails rejectDetails) { - filterError = true; + filterError = true; - return true; + return true; } bool applyBestIntegerAmbiguity( - Trace& trace, ///< Debug trace - KFState& kfState) ///< Reference to Kalman filter containing float solutions + Trace& trace, ///< Debug trace + KFState& kfState ///< Reference to Kalman filter containing float solutions +) { - KFKey bestKey; - double smallestVar = 1e10; + KFKey bestKey; + double smallestVar = 1e10; - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::AMBIGUITY) - { - continue; - } + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::AMBIGUITY) + { + continue; + } - double var = kfState.P(index, index); + double var = kfState.P(index, index); - if ( var > smallestVar - ||var < FIXED_AMB_VAR * 5) - { - continue; - } + if (var > smallestVar || var < FIXED_AMB_VAR * 5) + { + continue; + } - smallestVar = var; - bestKey = key; - } + smallestVar = var; + bestKey = key; + } - if (bestKey.type == KF::NONE) - { - return false; - } + if (bestKey.type == KF::NONE) + { + return false; + } - KFMeasEntryList kfMeasEntryList; + KFMeasEntryList kfMeasEntryList; - int index = kfState.kfIndexMap[bestKey]; + int index = kfState.kfIndexMap[bestKey]; - double closest = round(kfState.x(index)); + double closest = round(kfState.x(index)); - KFMeasEntry measEntry(&kfState); + KFMeasEntry measEntry(&kfState); - measEntry.obsKey = bestKey; + measEntry.obsKey = bestKey; - measEntry.addDsgnEntry(bestKey, 1); + measEntry.addDsgnEntry(bestKey, 1); - measEntry.setValue(closest); - measEntry.setNoise(FIXED_AMB_VAR); + measEntry.setValue(closest); + measEntry.setNoise(FIXED_AMB_VAR); - kfMeasEntryList.push_back(measEntry); + kfMeasEntryList.push_back(measEntry); - KFMeas kfMeas(kfState, kfMeasEntryList, kfState.time); + KFMeas kfMeas(kfState, kfMeasEntryList, kfState.time); - filterError = false; - kfState.measRejectCallbacks.push_back(recordFilterError); - { - kfState.filterKalman(trace, kfMeas); - } - kfState.measRejectCallbacks.pop_back(); + filterError = false; + kfState.measRejectCallbacks.push_back(recordFilterError); + { + kfState.filterKalman(trace, kfMeas); + } + kfState.measRejectCallbacks.pop_back(); - if (filterError) - { - return false; - } + if (filterError) + { + return false; + } - kfState.outputStates(trace, "/AR1"); + kfState.outputStates(trace, "/AR1"); - return true; + return true; } - void applyUCAmbiguities( - Trace& trace, ///< Debug trace - KFState& kfState, ///< Reference to Kalman filter containing float solutions - GinAR_mtx& mtrx) ///< Reference to structure containing fixed ambiguities and Z transformations + Trace& trace, ///< Debug trace + KFState& kfState, ///< Reference to Kalman filter containing float solutions + GinAR_mtx& mtrx ///< Reference to structure containing fixed ambiguities and Z transformations +) { - int nz = mtrx.zfix.size(); - int nx = mtrx.ambmap.size(); + int nz = mtrx.zfix.size(); + int nx = mtrx.ambmap.size(); - tracepdeex(1, trace, " %d out of %d ambiguities resolved, applying...\n", nz, nx); + tracepdeex(1, trace, " %d out of %d ambiguities resolved, applying...\n", nz, nx); - MatrixXd Z = mtrx.Ztrs; - VectorXd zfix = mtrx.zfix; + MatrixXd Z = mtrx.Ztrs; + VectorXd zfix = mtrx.zfix; - if (AR_VERBO) - { - trace << "\n" << "zfix =" << "\n" << zfix.transpose() << "\n"; - trace << "\n" << "Ztrs =" << "\n" << Z << "\n"; - } + if (AR_VERBO) + { + trace << "\n" + << "zfix =" << "\n" + << zfix.transpose() << "\n"; + trace << "\n" + << "Ztrs =" << "\n" + << Z << "\n"; + } - KFMeasEntryList kfMeasEntryList; + KFMeasEntryList kfMeasEntryList; - for (int i = 0; i < nz; i++) - { - double residual = zfix(i); + for (int i = 0; i < nz; i++) + { + double residual = zfix(i); - KFMeasEntry measEntry(&kfState); + KFMeasEntry measEntry(&kfState); - measEntry.obsKey.type = KF::Z_AMB; - measEntry.obsKey.comment = "Ambiguity Psueodobs"; + measEntry.obsKey.type = KF::Z_AMB; + measEntry.obsKey.comment = "Ambiguity Psueodobs"; - measEntry.addNoiseEntry(measEntry.obsKey, 1, FIXED_AMB_VAR); + measEntry.addNoiseEntry(measEntry.obsKey, 1, FIXED_AMB_VAR); - tracepdeex(4, trace, " Applying: "); + tracepdeex(4, trace, " Applying: "); - for (int j = 0; j < nx; j++) - { - if (Z(i,j) == 0) - { - continue; - } + for (int j = 0; j < nx; j++) + { + if (Z(i, j) == 0) + { + continue; + } - double ambiguity = 0; + double ambiguity = 0; - KFKey key = mtrx.ambmap[j]; - kfState.getKFValue(key, ambiguity); + KFKey key = mtrx.ambmap[j]; + kfState.getKFValue(key, ambiguity); - residual -= Z(i,j) * ambiguity; + residual -= Z(i, j) * ambiguity; - tracepdeex(4,trace,"%+3.0f A(%s,%s,%2d) ",Z(i,j),key.str.c_str(),key.Sat.id().c_str(),key.num); + tracepdeex( + 4, + trace, + "%+3.0f A(%s,%s,%3s) ", + Z(i, j), + key.str.c_str(), + key.Sat.id().c_str(), + key.code().c_str() + ); - InitialState init; - init.x = ambiguity; - init.P = 3600; + InitialState init; + init.x = ambiguity; + init.P = 3600; - measEntry.addDsgnEntry(mtrx.ambmap[j], Z(i,j), init); - } + measEntry.addDsgnEntry(mtrx.ambmap[j], Z(i, j), init); + } - tracepdeex(4, trace, "= %+10.5f\n", zfix(i)); + tracepdeex(4, trace, "= %+10.5f\n", zfix(i)); - measEntry.setInnov(residual); + measEntry.setInnov(residual); - kfMeasEntryList.push_back(measEntry); - } + kfMeasEntryList.push_back(measEntry); + } - KFMeas kfMeas(kfState, kfMeasEntryList, kfState.time); + KFMeas kfMeas(kfState, kfMeasEntryList, kfState.time); - kfState.filterKalman(trace, kfMeas, "/AR", true); - - kfState.outputStates(trace, "/AR"); + kfState.filterKalman(trace, kfMeas, "/AR", true); } void fixAndHoldAmbiguities( - Trace& trace, ///< Debug trace - KFState& kfState) ///< Filter state + Trace& trace, ///< Debug trace + KFState& kfState ///< Filter state +) { - tracepdeex(3, trace, "%s: %s\n", __FUNCTION__, kfState.time.to_string().c_str()); - - if (acsConfig.ambrOpts.mode == +E_ARmode::OFF) - { - return; - } - - GinAR_mtx ARmtx; - map nsat; // number of satellites visible by station - map nsta; // number of stations visible by satellite - - int ind = 0; - vector indices; - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::AMBIGUITY) - { - continue; - } - - if (acsConfig.solve_amb_for[key.Sat.sys] == false) - { - continue; - } - - indices.push_back(index); - - ARmtx.ambmap[ind] = key; - ind++; - } - - if (ind == 0) - return; - - ARmtx.aflt = kfState.x(indices); - ARmtx.Paflt = kfState.P(indices, indices); - - GinAR_opt ARopt; - ARopt.mode = acsConfig.ambrOpts.mode; - ARopt.sucthr = acsConfig.ambrOpts.succsThres; - ARopt.ratthr = acsConfig.ambrOpts.ratioThres; - ARopt.nset = acsConfig.ambrOpts.lambda_set; - ARopt.nitr = acsConfig.ambrOpts.AR_max_itr; - - if (traceLevel > 4) - AR_VERBO = true; - - // Resolve and apply ambiguities - int nfix = GNSS_AR(trace, ARmtx, ARopt); - if (nfix > 0) - { - applyUCAmbiguities(trace, kfState, ARmtx); - } - - while (0) - { - bool applied = applyBestIntegerAmbiguity(trace, kfState); - - if (applied == false) - { - break; - } - } + tracepdeex(3, trace, "%s: %s\n", __FUNCTION__, kfState.time.to_string().c_str()); + + if (acsConfig.ambrOpts.mode == E_ARmode::OFF) + { + return; + } + + GinAR_mtx ARmtx; + map nsat; // number of satellites visible by station + map nsta; // number of stations visible by satellite + + int ind = 0; + vector indices; + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::AMBIGUITY) + { + continue; + } + + if (acsConfig.solve_amb_for[key.Sat.sys] == false) + { + continue; + } + + indices.push_back(index); + + ARmtx.ambmap[ind] = key; + ind++; + } + + if (ind == 0) + return; + + ARmtx.aflt = kfState.x(indices); + ARmtx.Paflt = kfState.P(indices, indices); + + GinAR_opt ARopt; + ARopt.mode = acsConfig.ambrOpts.mode; + ARopt.sucthr = acsConfig.ambrOpts.succsThres; + ARopt.ratthr = acsConfig.ambrOpts.ratioThres; + ARopt.nset = acsConfig.ambrOpts.lambda_set; + ARopt.nitr = acsConfig.ambrOpts.AR_max_itr; + + if (traceLevel > 4) + AR_VERBO = true; + + // Resolve and apply ambiguities + int nfix = GNSS_AR(trace, ARmtx, ARopt); + if (nfix > 0) + { + applyUCAmbiguities(trace, kfState, ARmtx); + } + + while (0) + { + bool applied = applyBestIntegerAmbiguity(trace, kfState); + + if (applied == false) + { + break; + } + } } - - bool queryBiasUC( - Trace& trace, ///< debug stream - GTime time, ///< time of biases - KFState& kfState, ///< filter state to take biases from - SatSys Sat, ///< satellite (for receiver biases, sat.sys needs to be set to the appropriate system, and sat.prn must be 0) - string rec, ///< receiver (for satellite biases nees to be "") - E_ObsCode code, ///< signal code - double& bias, ///< bias value - double& var, ///< bias variance - E_MeasType type) ///< measurement type + Trace& trace, ///< debug stream + GTime time, ///< time of biases + KFState& kfState, ///< filter state to take biases from + SatSys Sat, ///< satellite (for receiver biases, sat.sys needs to be set to the appropriate + ///< system, and sat.prn must be 0) + string rec, ///< receiver (for satellite biases nees to be "") + E_ObsCode code, ///< signal code + double& bias, ///< bias value + double& var, ///< bias variance + E_MeasType type ///< measurement type +) { - KFKey kfKey; - kfKey.str = rec; - kfKey.Sat = Sat; - kfKey.num = code; - - if (Sat.prn == 0) //todo aaron, check if needed and reverse logic - { - auto& recOpts = acsConfig.getRecOpts(rec, {Sat.sys._to_string(), code._to_string()}); - - if (type == CODE) - { - if (recOpts.codeBiasModel.enable == false) - return true; - - InitialState init = initialStateFromConfig(recOpts.code_bias); - if (init.estimate == false) - { - getBias(trace, time, rec, Sat, code, CODE, bias, var); - return true; - } - - kfKey.type = KF::CODE_BIAS; - - return kfState.getKFValue(kfKey, bias, &var); - } - - if (type == PHAS) - { - if (recOpts.phaseBiasModel.enable == false) - return true; - - InitialState init = initialStateFromConfig(recOpts.phase_bias); - if (init.estimate == false) - { - getBias(trace, time, rec, Sat, code, PHAS, bias, var); - - return true; - } - - kfKey.type = KF::PHASE_BIAS; - - return kfState.getKFValue(kfKey, bias, &var); - } - } - else if (rec.empty()) - { - auto& satOpts = acsConfig.getSatOpts(Sat); - - if (type == CODE) - { - if (!satOpts.codeBiasModel.enable) - return true; - - InitialState init = initialStateFromConfig(satOpts.code_bias); - if (init.estimate == false) - { - getBias(trace, time, Sat.id(), Sat, code, CODE, bias, var); - return true; - } - - kfKey.type = KF::CODE_BIAS; - bool pass = kfState.getKFValue(kfKey, bias, &var); - - tracepdeex(5,trace,"\n Searching UC %s - %s", ((string)kfKey).c_str(), pass?"found":"not found"); - - return pass; - } - - if (type == PHAS) - { - if (satOpts.phaseBiasModel.enable == false) - return true; - - InitialState init = initialStateFromConfig(satOpts.phase_bias); - if (init.estimate == false) - { - getBias(trace, time, Sat.id(), Sat, code, PHAS, bias, var); - return true; - } - - kfKey.type = KF::PHASE_BIAS; - bool pass = kfState.getKFValue(kfKey, bias, &var); - - tracepdeex(5,trace,"\n Searching UC %s - %s", ((string)kfKey).c_str(), pass?"found":"not found"); - - return pass; - } - } - - return false; + KFKey kfKey; + kfKey.str = rec; + kfKey.Sat = Sat; + kfKey.num = static_cast(code); + + if (Sat.prn == 0) // todo aaron, check if needed and reverse logic + { + auto& recOpts = acsConfig.getRecOpts(rec, {Sat.sysName(), enum_to_string(code)}); + + if (type == CODE) + { + if (recOpts.codeBiasModel.enable == false) + return true; + + InitialState init = initialStateFromConfig(recOpts.code_bias); + if (init.estimate == false) + { + getBias(trace, time, rec, Sat, code, CODE, bias, var); + return true; + } + + kfKey.type = KF::CODE_BIAS; + + return kfState.getKFValue(kfKey, bias, &var) != E_Source::NONE; + } + + if (type == PHAS) + { + if (recOpts.phaseBiasModel.enable == false) + return true; + + InitialState init = initialStateFromConfig(recOpts.phase_bias); + if (init.estimate == false) + { + getBias(trace, time, rec, Sat, code, PHAS, bias, var); + + return true; + } + + kfKey.type = KF::PHASE_BIAS; + + return kfState.getKFValue(kfKey, bias, &var) != E_Source::NONE; + } + } + else if (rec.empty()) + { + auto& satOpts = acsConfig.getSatOpts(Sat); + + if (type == CODE) + { + if (!satOpts.codeBiasModel.enable) + return true; + + InitialState init = initialStateFromConfig(satOpts.code_bias); + if (init.estimate == false) + { + getBias(trace, time, Sat.id(), Sat, code, CODE, bias, var); + return true; + } + + kfKey.type = KF::CODE_BIAS; + E_Source passSrc = kfState.getKFValue(kfKey, bias, &var); + bool pass = passSrc != E_Source::NONE; + + tracepdeex( + 5, + trace, + "\n Searching UC %s - %s", + ((string)kfKey).c_str(), + pass ? "found" : "not found" + ); + + return pass; + } + + if (type == PHAS) + { + if (satOpts.phaseBiasModel.enable == false) + return true; + + InitialState init = initialStateFromConfig(satOpts.phase_bias); + if (init.estimate == false) + { + getBias(trace, time, Sat.id(), Sat, code, PHAS, bias, var); + return true; + } + + kfKey.type = KF::PHASE_BIAS; + E_Source passSrc = kfState.getKFValue(kfKey, bias, &var); + bool pass = passSrc != E_Source::NONE; + + tracepdeex( + 5, + trace, + "\n Searching UC %s - %s", + ((string)kfKey).c_str(), + pass ? "found" : "not found" + ); + + return pass; + } + } + + return false; } diff --git a/src/cpp/pea/ppp_callbacks.cpp b/src/cpp/pea/ppp_callbacks.cpp index e78f8a1e6..e664df7a3 100644 --- a/src/cpp/pea/ppp_callbacks.cpp +++ b/src/cpp/pea/ppp_callbacks.cpp @@ -1,425 +1,673 @@ - // #pragma GCC optimize ("O0") #include - #include +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/receiver.hpp" +#include "common/satStat.hpp" +#include "pea/ppp.hpp" using std::ostringstream; -#include "interactiveTerminal.hpp" -#include "acsConfig.hpp" -#include "receiver.hpp" -#include "satStat.hpp" -#include "algebra.hpp" -#include "ppp.hpp" - /** Deweight worst measurement */ -bool deweightMeas( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit) +bool deweightMeas(RejectCallbackDetails rejectDetails) { - if (acsConfig.measErrors.enable == false) - { - return true; - } - - double deweightFactor = acsConfig.measErrors.deweight_factor; + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfMeas = rejectDetails.kfMeas; + auto& measIndex = rejectDetails.measIndex; + auto& stage = rejectDetails.stage; + + if (acsConfig.measErrors.enable == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Warning: Bad measurement detected but `meas_deweighting` not enabled"; + + return true; + } + + double deweightFactor = acsConfig.measErrors.deweight_factor; + + auto& key = kfMeas.obsKeys[measIndex]; + + double preSigma = sqrt(kfMeas.R(measIndex, measIndex)); + double residual = 0; + + string description; + if (stage == E_FilterStage::LSQ) + { + description = "Least Squares"; + residual = kfMeas.VV(measIndex); + } + else if (stage == E_FilterStage::PREFIT) + { + description = "Prefit"; + residual = kfMeas.V(measIndex); + } + else if (stage == E_FilterStage::POSTFIT) + { + description = "Postfit"; + residual = kfMeas.VV(measIndex); + } + + addRejectDetails( + kfState.time, + trace, + kfState, + key, + "Measurement Deweighted", + description, + {{description + "Residual", residual}, + {"preDeweightSigma", preSigma}, + {"postDeweightSigma", preSigma * deweightFactor}} + ); + + kfMeas.R.row(measIndex) *= deweightFactor; + kfMeas.R.col(measIndex) *= deweightFactor; + if (kfMeas.H_star.size() > 0) + kfMeas.H_star.row(measIndex) *= deweightFactor; + + map& metaDataMap = kfMeas.metaDataMaps[measIndex]; + + GObs* obs_ptr = (GObs*)metaDataMap["sppObs_ptr"]; + + if (obs_ptr) + { + GObs& obs = *obs_ptr; + + obs.excludeOutlier = true; // todo Eugene: exclude signal instead of obs + + trace << "\n" << obs.Sat.id() << " will be excluded next SPP iteration"; + } + + MatrixXd* otherNoiseMatrix_ptr = (MatrixXd*)metaDataMap["otherNoiseMatrix_ptr"]; + long int otherIndex = (long int)metaDataMap["otherIndex"]; + + if (otherNoiseMatrix_ptr) + { + // some measurements have noise matrices in 2 places (mincon) - update the other one too if + // its available. + auto& otherNoiseMatrix = *otherNoiseMatrix_ptr; + + otherNoiseMatrix.row(otherIndex) *= deweightFactor; + otherNoiseMatrix.col(otherIndex) *= deweightFactor; + } + + return false; +} - auto& key = kfMeas.obsKeys[index]; +/** Call state rejection functions when a measurement is a pseudo observation + */ +bool pseudoMeasTest(RejectCallbackDetails rejectDetails) +{ + auto& kfState = rejectDetails.kfState; + auto& kfMeas = rejectDetails.kfMeas; + auto& measIndex = rejectDetails.measIndex; + + if (kfMeas.metaDataMaps[measIndex]["pseudoObs"] == (void*)false) + { + return true; + } + + for (auto& [key, stateIndex] : kfState.kfIndexMap) + { + if (kfMeas.H(measIndex, stateIndex) && + key.type == KF::ORBIT) // Eugene: do this to other pseudoObs as well? + { + rejectDetails.kfKey = key; + rejectDetails.stateIndex = stateIndex; + + return relaxState(rejectDetails); + } + } + + return false; +} - InteractiveTerminal ss("Deweights", trace, false); +/** Deweight measurement and its relatives + */ +bool deweightStationMeas(RejectCallbackDetails rejectDetails) +{ + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfMeas = rejectDetails.kfMeas; + auto& measIndex = rejectDetails.measIndex; + auto& stage = rejectDetails.stage; + + string id = kfMeas.obsKeys[measIndex].str; + + for (int i = 0; i < kfMeas.obsKeys.size(); i++) + { + auto& key = kfMeas.obsKeys[i]; + + if (key.str != id) + { + 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; + kfMeas.R.col(i) *= deweightFactor; + if (kfMeas.H_star.size() > 0) + kfMeas.H_star.row(i) *= deweightFactor; + + map& metaDataMap = kfMeas.metaDataMaps[i]; + + bool* used_ptr = (bool*)metaDataMap["used_ptr"]; + + if (used_ptr) + { + *used_ptr = false; + } + + MatrixXd* otherNoiseMatrix_ptr = (MatrixXd*)metaDataMap["otherNoiseMatrix_ptr"]; + long int otherIndex = (long int)metaDataMap["otherIndex"]; + + if (otherNoiseMatrix_ptr) + { + // some measurements have noise matrices in 2 places (mincon) - update the other one too + // if its available. + auto& otherNoiseMatrix = *otherNoiseMatrix_ptr; + + otherNoiseMatrix.row(otherIndex) *= deweightFactor; + otherNoiseMatrix.col(otherIndex) *= deweightFactor; + } + } + + return false; +} - ss << "\n" << kfState.time.to_string() << "\tDeweighting " << key << " - " << key.comment; +/** Count worst measurement + */ +bool incrementPhaseSignalError(RejectCallbackDetails rejectDetails) +{ + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfMeas = rejectDetails.kfMeas; + auto& measIndex = rejectDetails.measIndex; - kfState.statisticsMap["Meas deweight"]++; + map& metaDataMap = kfMeas.metaDataMaps[measIndex]; - char buff[64]; - snprintf(buff, sizeof(buff), "Meas Deweight-%4s-%s-%sfit", key.str.c_str(), KF::_from_integral(key.type)._to_string(), postFit ? "Post" : "Pre"); kfState.statisticsMap[buff]++; - snprintf(buff, sizeof(buff), "Meas Deweight-%4s-%s-%sfit", key.Sat.id().c_str(), KF::_from_integral(key.type)._to_string(), postFit ? "Post" : "Pre"); kfState.statisticsMap[buff]++; + for (auto suffix : {"", "_alt"}) + { + string metaData = "phaseRejectCount"; + metaData += suffix; - kfMeas.R.row(index) *= deweightFactor; - kfMeas.R.col(index) *= deweightFactor; + unsigned int* phaseRejectCount_ptr = (unsigned int*)metaDataMap[metaData]; - map& metaDataMap = kfMeas.metaDataMaps[index]; + if (phaseRejectCount_ptr == nullptr) + { + continue; + } - MatrixXd* otherNoiseMatrix_ptr = (MatrixXd*) metaDataMap["otherNoiseMatrix_ptr"]; - long int otherIndex = (long int) metaDataMap["otherIndex"]; + unsigned int& phaseRejectCount = *phaseRejectCount_ptr; - if (otherNoiseMatrix_ptr) - { - //some measurements have noise matrices in 2 places (mincon) - update the other one too if its available. - auto& otherNoiseMatrix = *otherNoiseMatrix_ptr; + // Increment counter, and clear the pointer so it cant be reset to zero in subsequent + // operations (because this is a failure) + phaseRejectCount++; + metaDataMap[metaData] = nullptr; - otherNoiseMatrix.row(otherIndex) *= deweightFactor; - otherNoiseMatrix.col(otherIndex) *= deweightFactor; - } + trace << "\n" + << kfState.time << "\tIncrementing phaseRejectCount on\t" + << kfMeas.obsKeys[measIndex] << "\tto " << phaseRejectCount; + } - return true; + return true; } -/** Call state rejection functions when a measurement is a pseudo observation +/** Count all errors on receiver */ -bool pseudoMeasTest( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit) +bool incrementReceiverErrors(RejectCallbackDetails rejectDetails) { - if (kfMeas.metaDataMaps[index]["pseudoObs"] == (void*) false) - { - return true; - } - - for (auto& [key, state] : kfState.kfIndexMap) - { - if ( kfMeas.H(index, state) - &&key.type == KF::ORBIT) - { - orbitGlitchReaction(trace, kfState, kfMeas, key, postFit); - } - } - - return true; -} + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfMeas = rejectDetails.kfMeas; + auto& measIndex = rejectDetails.measIndex; -/** Deweight measurement and its relatives - */ -bool deweightStationMeas( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit) -{ - string id = kfMeas.obsKeys[index].str; + if (acsConfig.errorAccumulation.enable == false) + { + return true; + } - for (int i = 0; i < kfMeas.obsKeys.size(); i++) - { - auto& key = kfMeas.obsKeys[i]; + map& metaDataMap = kfMeas.metaDataMaps[measIndex]; - if (key.str != id) - { - continue; - } + string metaData = "receiverErrorCount"; - double deweightFactor = acsConfig.stateErrors.deweight_factor; + unsigned int* receiverErrorCount_ptr = (unsigned int*)metaDataMap[metaData]; - trace << "\n" << "Deweighting " << key << " - " << key.comment << "\n"; + if (receiverErrorCount_ptr == nullptr) + { + return true; + } - kfState.statisticsMap["Receiver deweight"]++; + unsigned int& receiverErrorCount = *receiverErrorCount_ptr; - char buff[64]; - snprintf(buff, sizeof(buff), "Receiver Deweight-%4s-%sfit", key.str.c_str(), postFit ? "Post" : "Pre"); kfState.statisticsMap[buff]++; + // Increment counter, and clear the pointer so it wont increment again at current epoch + receiverErrorCount++; + metaDataMap[metaData] = nullptr; - kfMeas.R.row(i) *= deweightFactor; - kfMeas.R.col(i) *= deweightFactor; + char idStr[100]; + snprintf( + idStr, + sizeof(idStr), + "%10s\t%4s\t%4s\t%5s", + "", + "", + kfMeas.obsKeys[measIndex].str.c_str(), + "" + ); - map& metaDataMap = kfMeas.metaDataMaps[i]; + trace << "\n" + << kfState.time << "\tIncrementing receiverErrorCount on\t" << idStr << "\tto " + << receiverErrorCount; - bool* used_ptr = (bool*) metaDataMap["used_ptr"]; + return true; +} - if (used_ptr) - { - *used_ptr = false; - } +/** Count all errors on satellite + */ +bool incrementSatelliteErrors(RejectCallbackDetails rejectDetails) +{ + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfMeas = rejectDetails.kfMeas; + auto& measIndex = rejectDetails.measIndex; - MatrixXd* otherNoiseMatrix_ptr = (MatrixXd*) metaDataMap["otherNoiseMatrix_ptr"]; - long int otherIndex = (long int) metaDataMap["otherIndex"]; + if (acsConfig.errorAccumulation.enable == false) + { + return true; + } - if (otherNoiseMatrix_ptr) - { - //some measurements have noise matrices in 2 places (mincon) - update the other one too if its available. - auto& otherNoiseMatrix = *otherNoiseMatrix_ptr; + map& metaDataMap = kfMeas.metaDataMaps[measIndex]; - otherNoiseMatrix.row(otherIndex) *= deweightFactor; - otherNoiseMatrix.col(otherIndex) *= deweightFactor; - } - } - return true; -} + string metaData = "satelliteErrorCount"; -/** Count worst measurement - */ -bool incrementPhaseSignalError( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit) -{ - map& metaDataMap = kfMeas.metaDataMaps[index]; + unsigned int* satelliteErrorCount_ptr = (unsigned int*)metaDataMap[metaData]; - unsigned int* phaseRejectCount_ptr = (unsigned int*) metaDataMap["phaseRejectCount"]; + if (satelliteErrorCount_ptr == nullptr) + { + return true; + } - if (phaseRejectCount_ptr == nullptr) - { - return true; - } + unsigned int& satelliteErrorCount = *satelliteErrorCount_ptr; - unsigned int& phaseRejectCount = *phaseRejectCount_ptr; + // Increment counter, and clear the pointer so it wont increment again at current epoch + satelliteErrorCount++; + metaDataMap[metaData] = nullptr; - //increment counter, and clear the pointer so it cant be reset to zero in subsequent operations (because this is a failure) - phaseRejectCount++; - metaDataMap["phaseRejectCount"] = nullptr; + char idStr[100]; + snprintf( + idStr, + sizeof(idStr), + "%10s\t%4s\t%4s\t%5s", + "", + kfMeas.obsKeys[measIndex].Sat.id().c_str(), + "", + "" + ); - trace << "\n" << "Incrementing phaseRejectCount on " << kfMeas.obsKeys[index].Sat.id() << " to " << phaseRejectCount; + trace << "\n" + << kfState.time << "\tIncrementing satelliteErrorCount on\t" << idStr << "\tto " + << satelliteErrorCount; - return true; + return true; } -/** Count all errors on receiver +/** Count all errors on an individual state */ -bool incrementReceiverError( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - int index, - bool postFit) +bool incrementStateErrors(RejectCallbackDetails rejectDetails) { - map& metaDataMap = kfMeas.metaDataMaps[index]; + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfKey = rejectDetails.kfKey; - unsigned int* receiverErrorCount_ptr = (unsigned int*) metaDataMap["receiverErrorCount"]; + KFKey kfKeyCopy = kfKey; + kfKeyCopy.num = 0; - if (receiverErrorCount_ptr == nullptr) - { - return true; - } + if (acsConfig.errorAccumulation.enable == false) + { + return true; + } - unsigned int& receiverErrorCount = *receiverErrorCount_ptr; + if (kfState.errorCountMap.find(kfKeyCopy) == kfState.errorCountMap.end()) + { + kfState.errorCountMap[kfKeyCopy] = 0; // initialise error count upon first occurance + } - //increment counter, and clear the pointer so it cant be reset to zero in subsequent operations (because this is a failure) - receiverErrorCount++; - metaDataMap["receiverErrorFlag"] = nullptr; + kfState.errorCountMap[kfKeyCopy]++; - trace << "\n" << "Incrementing receiverErrorCount on " << kfMeas.obsKeys[index] << " to " << receiverErrorCount; + trace << "\n" + << kfState.time << "\tIncrementing stateErrorCount on\t" << kfKeyCopy << "\tto " + << kfState.errorCountMap[kfKeyCopy]; - return true; + return true; } -bool resetPhaseSignalError( - const GTime& time, - KFMeas& kfMeas, - int index) +void resetPhaseSignalError(const GTime& time, KFMeas& kfMeas, int index) { - map& metaDataMap = kfMeas.metaDataMaps[index]; + map& metaDataMap = kfMeas.metaDataMaps[index]; - //these will have been set to null if there was an error after adding the measurement to the list - for (auto suffix : {"", "_alt"}) - { - unsigned int* phaseRejectCount_ptr = (unsigned int*) metaDataMap[(string)"phaseRejectCount" + suffix]; + // these will have been set to null if there was an error after adding the measurement to the + // list + for (auto suffix : {"", "_alt"}) + { + unsigned int* phaseRejectCount_ptr = + (unsigned int*)metaDataMap[(string) "phaseRejectCount" + suffix]; - if (phaseRejectCount_ptr == nullptr) - { - return true; - } + if (phaseRejectCount_ptr == nullptr) + { + return; + } - unsigned int& phaseRejectCount = *phaseRejectCount_ptr; + unsigned int& phaseRejectCount = *phaseRejectCount_ptr; - phaseRejectCount = 0; - } + phaseRejectCount = 0; + } - return true; + return; } - -bool resetPhaseSignalOutage( - const GTime& time, - KFMeas& kfMeas, - int index) +void resetIonoSignalOutage(const GTime& time, KFMeas& kfMeas, int index) { - map& metaDataMap = kfMeas.metaDataMaps[index]; + map& metaDataMap = kfMeas.metaDataMaps[index]; - for (auto suffix : {"", "_alt"}) - { - GTime* lastPhaseTime_ptr = (GTime*) metaDataMap[(string)"lastPhaseTime" + suffix]; + for (auto suffix : {"", "_alt"}) + { + GTime* lastIonTime_ptr = (GTime*)metaDataMap[(string) "lastIonTime" + suffix]; - if (lastPhaseTime_ptr == nullptr) - { - return true; - } + if (lastIonTime_ptr == nullptr) + { + return; + } - GTime& lastPhaseTime = *lastPhaseTime_ptr; + GTime& lastIonTime = *lastIonTime_ptr; - lastPhaseTime = time; - } + lastIonTime = time; + } - return true; + return; } -bool resetIonoSignalOutage( - const GTime& time, - KFMeas& kfMeas, - int index) +/** Reject worst measurement attached to worst state using measurement reject callback list + */ +bool rejectWorstMeasByState(RejectCallbackDetails rejectDetails) { - map& metaDataMap = kfMeas.metaDataMaps[index]; + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfMeas = rejectDetails.kfMeas; + auto& kfKey = rejectDetails.kfKey; + auto& measIndex = rejectDetails.measIndex; + + KFKey kfKeyCopy = kfKey; + kfKeyCopy.num = 0; - for (auto suffix : {"", "_alt"}) - { - GTime* lastIonTime_ptr = (GTime*) metaDataMap[(string)"lastIonTime" + suffix]; + if (acsConfig.errorAccumulation.enable && + acsConfig.errorAccumulation.state_error_count_threshold > 0 && + kfState.errorCountMap[kfKeyCopy] >= acsConfig.errorAccumulation.state_error_count_threshold) + { + trace << "\n" + << "High state error counts: " << kfKeyCopy; - if (lastIonTime_ptr == nullptr) - { - return true; - } + return true; + } - GTime& lastIonTime = *lastIonTime_ptr; + trace << "\n" + << "Suspected bad state " << kfKey << " - try rejecting worst referencing measurement " + << kfMeas.obsKeys[measIndex] << "\n"; - lastIonTime = time; - } + kfState.doMeasRejectCallbacks(rejectDetails); - return true; + return false; } -/** Reject measurements attached to worst state using measurement reject callback list +/** Reject all measurements attached to worst state using measurement reject callback list */ -bool rejectByState( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - const KFKey& kfKey, - bool postFit) +bool rejectAllMeasByState(RejectCallbackDetails rejectDetails) { - if (acsConfig.stateErrors.enable == false) - { - return true; - } - - trace << "\n" << "Bad state detected " << kfKey << " - rejecting all referencing measurements" << "\n"; + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfMeas = rejectDetails.kfMeas; + auto& kfKey = rejectDetails.kfKey; + auto& stateIndex = rejectDetails.stateIndex; + + trace << "\n" + << "Bad state detected " << kfKey << " - rejecting all referencing measurements" << "\n"; + + for (int measIndex = 0; measIndex < kfMeas.H.rows(); measIndex++) + { + if (kfMeas.H(measIndex, stateIndex)) + { + rejectDetails.measIndex = measIndex; + + kfState.doMeasRejectCallbacks(rejectDetails); + } + } + + return false; +} - kfState.statisticsMap["State rejection"]++; +/** Immediately executed reaction to orbital state errors. + * Note there is also a 1 epoch delayed reaction function + */ +bool satelliteGlitchReaction(RejectCallbackDetails rejectDetails) +{ + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfKey = rejectDetails.kfKey; + + if (kfKey.type != KF::NONE && kfKey.type != KF::ORBIT && kfKey.type != KF::SAT_CLOCK) + { + return true; + } + + if (acsConfig.satelliteErrors.enable == false) + { + BOOST_LOG_TRIVIAL(warning) << "Bad satellite detected but `satellite_errors` not enabled"; + + return true; + } + + if (kfKey.type != KF::NONE) + trace << "\n" + << "Bad satellite orbit or clock detected " << kfKey; + else + trace << "\n" + << "Bad satellite detected " << kfKey.Sat.id(); + + kfState.statisticsMap["Satellite state reject"]++; + + Exponential exponentialNoise; + exponentialNoise.tau = acsConfig.satelliteErrors.vel_proc_noise_trail_tau; + exponentialNoise.value = SQR(acsConfig.satelliteErrors.vel_proc_noise_trail); + + MatrixXd F = MatrixXd::Identity(kfState.x.rows(), kfState.x.rows()); + MatrixXd Q = MatrixXd::Zero(kfState.x.rows(), kfState.x.rows()); + + bool transitionRequired = false; + + if (kfKey.type == KF::NONE || kfKey.type == KF::ORBIT) + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::ORBIT || key.str != kfKey.str || key.Sat != kfKey.Sat) + { + continue; + } + + if (key.num < 3 && acsConfig.satelliteErrors.pos_proc_noise) + { + trace << "\n - Adding " << acsConfig.satelliteErrors.pos_proc_noise + << " to sigma of " << key; + + Q(index, index) = SQR(acsConfig.satelliteErrors.pos_proc_noise); + + transitionRequired = true; + } + + if (key.num >= 3 && acsConfig.satelliteErrors.vel_proc_noise) + { + trace << "\n - Adding " << acsConfig.satelliteErrors.vel_proc_noise + << " to sigma of " << key; + + Q(index, index) = SQR(acsConfig.satelliteErrors.vel_proc_noise); + + kfState.setExponentialNoise(key, exponentialNoise); + + transitionRequired = true; + } + } + + if (kfKey.type == KF::NONE || kfKey.type == KF::SAT_CLOCK) + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::SAT_CLOCK || key.str != kfKey.str || key.Sat != kfKey.Sat) + { + continue; + } + + if (acsConfig.satelliteErrors.clk_proc_noise) + { + trace << "\n - Adding " << acsConfig.satelliteErrors.clk_proc_noise + << " to sigma of " << key; + + Q(index, index) = SQR(acsConfig.satelliteErrors.clk_proc_noise); + + transitionRequired = true; + } + } + + if (transitionRequired) + { + int index = -1; + + auto it = kfState.kfIndexMap.find(kfKey); + if (it != kfState.kfIndexMap.end()) + { + index = it->second; + } + + if (index >= 0) + { + trace << "\n - Pre-transition state sigma for " << kfKey << ": " + << sqrt(kfState.P(index, index)); + } + + kfState.manualStateTransition(trace, kfState.time, F, Q); - int stateIndex = kfState.getKFIndex(kfKey); + if (index >= 0) + { + trace << "\n - Post-transition state sigma for " << kfKey << ": " + << sqrt(kfState.P(index, index)); + } - for (int meas = 0; meas < kfMeas.H.rows(); meas++) - { - if (kfMeas.H(meas, stateIndex)) - { - kfState.doMeasRejectCallbacks(trace, kfMeas, meas, postFit); - } - } + return false; + } - return true; + return true; } -/** Remove any states connected to a bad clock if it glitches +/** Relax state */ -// bool clockGlitchReaction( //todo aaron orphan -// Trace& trace, -// KFState& kfState, -// KFMeas& kfMeas, -// const KFKey& kfKey) -// { -// if ( kfKey.type != KF::SAT_CLOCK -// && kfKey.type != KF::REC_SYS_BIAS) -// { -// return true; -// } -// -// if (acsConfig.reinit_on_clock_error == false) -// { -// return true; -// } -// -// trace << "\n" << "Bad clock detected " << kfKey << " - resetting linked states" << "\n"; -// -// kfState.statisticsMap["Clock glitch"]++; -// -// for (auto& [key, index] : kfState.kfIndexMap) -// { -// if ( kfKey.type == KF::SAT_CLOCK -// && kfKey.Sat == key.Sat -// &&( key .type == KF::AMBIGUITY -// ||key .type == KF::SAT_CLOCK)) -// { -// //remove the satellite clock, and any ambiguities that are connected to it. -// trace << "- Removing " << key << "\n"; -// -// kfState.removeState(key); -// } -// -// if ( kfKey.type == KF::REC_SYS_BIAS -// && kfKey.str == key.str -// &&( key .type == KF::AMBIGUITY -// ||key .type == KF::REC_SYS_BIAS)) -// { -// //remove the satellite clock, and any ambiguities that are connected to it. -// trace << "- Removing " << key << "\n"; -// -// kfState.removeState(key); -// -// if (kfKey.rec_ptr) -// { -// //make sure receiver clock corrections get reset too. -// trace << "- Resetting clock adjustment" << "\n"; -// -// auto& rec = *kfKey.rec_ptr; -// -// rec.sol.deltaDt_net_old[E_Sys::GPS] = 0; -// } -// } -// } -// -// return true; -// } - - -bool orbitGlitchReaction( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - const KFKey& kfKey, - bool postFit) +bool relaxState(RejectCallbackDetails rejectDetails) { - if (kfKey.type != KF::ORBIT) - { - return true; - } - - if (acsConfig.orbErrors.enable == false) - { - return true; - } - - trace << "\n" << "Bad orbit state detected " << kfKey; - - kfState.statisticsMap["Orbit state reject"]++; - - Exponential exponentialNoise; - exponentialNoise.tau = acsConfig.orbErrors.vel_proc_noise_trail_tau; - exponentialNoise.value = SQR( acsConfig.orbErrors.vel_proc_noise_trail); - - MatrixXd F = MatrixXd::Identity (kfState.x.rows(), kfState.x.rows()); - MatrixXd Q = MatrixXd::Zero (kfState.x.rows(), kfState.x.rows()); - - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( key.type != KF::ORBIT - || key.str != kfKey.str - || key.Sat != kfKey.Sat) - { - continue; - } - - if (key.num < 3) - { - Q(index, index) = SQR(acsConfig.orbErrors.pos_proc_noise); - } - else - { - Q(index, index) = SQR(acsConfig.orbErrors.vel_proc_noise); - - kfState.setExponentialNoise(key, exponentialNoise); - } - } - - kfState.manualStateTransition(trace, kfState.time, F, Q); - - return false; -} + auto& trace = rejectDetails.trace; + auto& kfState = rejectDetails.kfState; + auto& kfKey = rejectDetails.kfKey; + auto& stateIndex = rejectDetails.stateIndex; + auto& stage = rejectDetails.stage; + + if (acsConfig.stateErrors.enable == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Warning: Bad state detected but `state_deweighting` not enabled"; + + return true; + } + + double deweightFactor = 1; + if (stage == E_FilterStage::PREFIT) + { + deweightFactor = abs(kfState.prefitRatios(stateIndex)); + } + else if (stage == E_FilterStage::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"; + + kfState.statisticsMap["State rejection"]++; + + MatrixXd F = MatrixXd::Identity(kfState.x.rows(), kfState.x.rows()); + MatrixXd Q = MatrixXd::Zero(kfState.x.rows(), kfState.x.rows()); + Exponential exponentialNoise; + exponentialNoise.value = SQR(acsConfig.satelliteErrors.vel_proc_noise_trail); + exponentialNoise.tau = acsConfig.satelliteErrors.vel_proc_noise_trail_tau; + + bool transitionRequired = false; + + for (auto& [key, index] : kfState.kfIndexMap) + { + // relax states for all components + if (key.type != kfKey.type || key.str != kfKey.str || key.Sat != kfKey.Sat) + { + continue; + } + + double procNoise = deweightFactor * sqrt(kfState.P(index, index)); + + trace << "\n - Adding " << procNoise << " to sigma of " << key; + + Q(index, index) = SQR(procNoise); + + if (key.type == KF::ORBIT && key.num >= 3 && acsConfig.satelliteErrors.enable && + acsConfig.satelliteErrors.vel_proc_noise_trail && + acsConfig.satelliteErrors.vel_proc_noise_trail_tau) + { + trace << "\n - Adding exponential " << acsConfig.satelliteErrors.vel_proc_noise_trail + << " (per square root second) to process noise sigma of " << key + << " from next epoch"; + + kfState.setExponentialNoise(key, exponentialNoise); + } + + transitionRequired = true; + } + + 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; + } + + return true; +} diff --git a/src/cpp/pea/ppp_obs.cpp b/src/cpp/pea/ppp_obs.cpp index c10f9dd77..446ffd6dd 100644 --- a/src/cpp/pea/ppp_obs.cpp +++ b/src/cpp/pea/ppp_obs.cpp @@ -1,2030 +1,2606 @@ - // #pragma GCC optimize ("O0") +#include +#include #include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/antenna.hpp" +#include "common/biases.hpp" +#include "common/cache.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/ionModels.hpp" +#include "common/receiver.hpp" +#include "common/tides.hpp" +#include "common/trace.hpp" +#include "inertial/posProp.hpp" +#include "iono/geomagField.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/orbitProp.hpp" +#include "trop/tropModels.hpp" + +using std::function; +using std::ostringstream; /** */ -ParallelArchitecture UDUC_GNSS_Measurements__() -{ - -} +ParallelArchitecture UDUC_GNSS_Measurements__() {} /** References -* 1. M.Fritsche, R.Dietrich, C.Knöfel, A.Rülke, S.Vey, M.Rothacher & P.Steigenberger, Impact of higher‐order ionospheric terms on GPS estimates. Geophysical research letters, 2005. -* 2. GAMIT 10.71 -* 3. U.Hugentobler, S.Schaer, G.Beutler, H.Bock, R.Dach, A.Jäggi, M.Meindl, C.Urschl, L.Mervart, M.Rothacher & U.Wild, CODE IGS analysis center technical report 2002, 2002. -*/ - -#include - -using std::ostringstream; - - -#include "interactiveTerminal.hpp" -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "geomagField.hpp" -#include "tropModels.hpp" -#include "acsConfig.hpp" -#include "ionoModel.hpp" -#include "orbitProp.hpp" -#include "ionModels.hpp" -#include "receiver.hpp" -#include "posProp.hpp" -#include "antenna.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "biases.hpp" -#include "cache.hpp" -#include "gTime.hpp" -#include "tides.hpp" -#include "trace.hpp" - -#include - -using std::function; - + * 1. M.Fritsche, R.Dietrich, C.Knöfel, A.Rülke, S.Vey, M.Rothacher & P.Steigenberger, Impact of + * higher‐order ionospheric terms on GPS estimates. Geophysical research letters, 2005. + * 2. GAMIT 10.71 + * 3. U.Hugentobler, S.Schaer, G.Beutler, H.Bock, R.Dach, A.Jäggi, M.Meindl, C.Urschl, L.Mervart, + * M.Rothacher & U.Wild, CODE IGS analysis center technical report 2002, 2002. + */ struct AutoSender { - Trace& trace; - GTime time; -private: - vector baseKVPs; - vector valueKVPs; - -public: - AutoSender( - Trace& trace, - GTime time) - : trace {trace}, - time {time} - { - - } - - void setBaseKVPs( int level, vector&& kvps) { if (level < traceLevel) return; baseKVPs = std::move(kvps); } - void setValueKVPs( int level, vector&& kvps) { if (level < traceLevel) return; valueKVPs = std::move(kvps); } - - void pushBaseKVP( int level, ArbitraryKVP&& kvp) { if (level < traceLevel) return; baseKVPs .push_back(std::move(kvp)); } - void pushValueKVP( int level, ArbitraryKVP&& kvp) { if (level < traceLevel) return; valueKVPs .push_back(std::move(kvp)); } - - ~AutoSender() - { - if ( baseKVPs .empty() - ||valueKVPs .empty()) - { - return; - } - - traceJson(0, trace, time, baseKVPs, valueKVPs); - } + Trace& trace; + GTime time; + + private: + vector baseKVPs; + vector valueKVPs; + + public: + AutoSender(Trace& trace, GTime time) : trace{trace}, time{time} {} + + void setBaseKVPs(int level, vector&& kvps) + { + if (level < traceLevel) + return; + baseKVPs = std::move(kvps); + } + void setValueKVPs(int level, vector&& kvps) + { + if (level < traceLevel) + return; + valueKVPs = std::move(kvps); + } + + void pushBaseKVP(int level, ArbitraryKVP&& kvp) + { + if (level < traceLevel) + return; + baseKVPs.push_back(std::move(kvp)); + } + void pushValueKVP(int level, ArbitraryKVP&& kvp) + { + if (level < traceLevel) + return; + valueKVPs.push_back(std::move(kvp)); + } + + ~AutoSender() + { + if (baseKVPs.empty() || valueKVPs.empty()) + { + return; + } + + traceJson(0, trace, time, baseKVPs, valueKVPs); + } }; - -//this hideousness is a horrible hack to make this code compile on macs. -//blame apple for this. look what they've made me do... - -//this will ust copy and paste the types and variable names for now, this is undefined later to remove the type names to pass in the parameters in the same order -#define COMMON_ARG(type) type - -#define COMMON_PPP_ARGS \ - COMMON_ARG( Trace& ) trace, \ - COMMON_ARG( Trace& ) jsonTrace, \ - COMMON_ARG( GObs& ) obs, \ - COMMON_ARG( Receiver& ) rec, \ - COMMON_ARG( SatSys& ) Sat, \ - COMMON_ARG( Sig& ) sig, \ - COMMON_ARG( string& ) sigName, \ - COMMON_ARG(const E_FType& ) ft, \ - COMMON_ARG( E_MeasType& ) measType, \ - COMMON_ARG( GTime& ) time, \ - COMMON_ARG( SatStat& ) satStat, \ - COMMON_ARG( SatNav& ) satNav, \ - COMMON_ARG( ReceiverOptions& ) recOpts, \ - COMMON_ARG( SatelliteOptions& ) satOpts, \ - COMMON_ARG(const KFState& ) kfState, \ - COMMON_ARG(const KFState& ) remoteKF, \ - COMMON_ARG( KFMeasEntry& ) measEntry, \ - COMMON_ARG( VectorEcef& ) rRec, \ - COMMON_ARG( VectorEcef& ) rSat, \ - COMMON_ARG( double& ) lambda, \ - COMMON_ARG( SatSys& ) sysSat, \ - COMMON_ARG( AutoSender& ) autoSenderTemplate, \ - COMMON_ARG( VectorPos& ) pos, \ - COMMON_ARG( ERPValues& ) erpv, \ - COMMON_ARG( FrameSwapper& ) frameSwapper - - -double relativity2( - VectorEcef& rSat, - VectorEcef& rRec) +// this hideousness is a horrible hack to make this code compile on macs. +// blame apple for this. look what they've made me do... + +// this will ust copy and paste the types and variable names for now, this is undefined later to +// remove the type names to pass in the parameters in the same order +#define COMMON_ARG(type) type + +#define COMMON_PPP_ARGS \ + COMMON_ARG(Trace&) \ + trace, COMMON_ARG(Trace&) jsonTrace, COMMON_ARG(GObs&) obs, COMMON_ARG(Receiver&) rec, \ + COMMON_ARG(SatSys&) Sat, COMMON_ARG(Sig&) sig, COMMON_ARG(string&) sigName, \ + COMMON_ARG(const E_FType&) ft, COMMON_ARG(E_MeasType&) measType, COMMON_ARG(GTime&) time, \ + COMMON_ARG(SatStat&) satStat, COMMON_ARG(SatNav&) satNav, \ + COMMON_ARG(ReceiverOptions&) recOpts, COMMON_ARG(SatelliteOptions&) satOpts, \ + COMMON_ARG(KFState&) kfState, COMMON_ARG(KFState&) remoteKF, \ + COMMON_ARG(KFMeasEntry&) measEntry, COMMON_ARG(VectorEcef&) rRec, \ + COMMON_ARG(VectorEcef&) rSat, COMMON_ARG(double&) lambda, COMMON_ARG(SatSys&) sysSat, \ + COMMON_ARG(AutoSender&) autoSenderTemplate, COMMON_ARG(VectorPos&) pos, \ + COMMON_ARG(ERPValues&) erpv, COMMON_ARG(FrameSwapper&) frameSwapper + +double relativity2(VectorEcef& rSat, VectorEcef& rRec) { - double rRecSat = (rSat - rRec).norm(); + double rRecSat = (rSat - rRec).norm(); - double ln = log( (rSat.norm() + rRec.norm() + rRecSat) - / (rSat.norm() + rRec.norm() - rRecSat)); - return 2 * MU * ln / CLIGHT / CLIGHT / CLIGHT; + double ln = log((rSat.norm() + rRec.norm() + rRecSat) / (rSat.norm() + rRec.norm() - rRecSat)); + return 2 * MU * ln / CLIGHT / CLIGHT / CLIGHT; } -tuple tideDelta( - Trace& trace, - GTime time, - Receiver& rec, - VectorEcef& rRec, - ReceiverOptions& recOpts) +tuple +tideDelta(Trace& trace, GTime time, Receiver& rec, VectorEcef& rRec, ReceiverOptions& recOpts) { - auto& tideVectors = rec.pppTideCache.useCache([&]() -> tuple - { - Vector3d solid = Vector3d::Zero(); - Vector3d otl = Vector3d::Zero(); - Vector3d atl = Vector3d::Zero(); - Vector3d spole = Vector3d::Zero(); - Vector3d opole = Vector3d::Zero(); - - if ( recOpts.tideModels.solid - ||recOpts.tideModels.otl - ||recOpts.tideModels.atl - ||recOpts.tideModels.spole - ||recOpts.tideModels.opole) - { - tideDisp(trace, time, rec, rRec, solid, otl, atl, spole, opole); - } - - return {solid, otl, atl, spole, opole}; - }); - - return tideVectors; + auto& tideVectors = rec.pppTideCache.useCache( + [&]() -> tuple + { + Vector3d solid = Vector3d::Zero(); + Vector3d otl = Vector3d::Zero(); + Vector3d atl = Vector3d::Zero(); + Vector3d spole = Vector3d::Zero(); + Vector3d opole = Vector3d::Zero(); + + if (recOpts.tideModels.solid || recOpts.tideModels.otl || recOpts.tideModels.atl || + recOpts.tideModels.spole || recOpts.tideModels.opole) + { + tideDisp(trace, time, rec.id, rRec, solid, otl, atl, spole, opole); + } + + return {solid, otl, atl, spole, opole}; + } + ); + + return tideVectors; } void eopAdjustment( - GTime& time, - VectorEcef& e, - ERPValues& erpv, - FrameSwapper& frameSwapper, - Receiver& rec, - VectorEcef& rRec, - KFMeasEntry& measEntry, - const KFState& kfState) + GTime& time, + VectorEcef& e, + ERPValues& erpv, + FrameSwapper& frameSwapper, + Receiver& rec, + VectorEcef& rRec, + KFMeasEntry& measEntry, + KFState& kfState +) { - Matrix3d partialMatrix = stationEopPartials(rRec); - Vector3d eopPartials = partialMatrix * e; - - for (int i = 0; i < 3; i++) - { - InitialState init = initialStateFromConfig(acsConfig.pppOpts.eop, i); - InitialState eopRateInit = initialStateFromConfig(acsConfig.pppOpts.eop_rates, i); - - if (init.estimate == false) - { - continue; - } - - KFKey kfKey; - kfKey.type = KF::EOP; - kfKey.num = i; - kfKey.comment = eopComments[i]; - - kfState.getKFValue(kfKey, init.x); - - if (init.x == 0) - switch (i) - { - case 0: init.x = erpv.xp * R2MAS; eopRateInit.x = +erpv.xpr * R2MAS; break; - case 1: init.x = erpv.yp * R2MAS; eopRateInit.x = +erpv.ypr * R2MAS; break; - case 2: init.x = erpv.ut1Utc * S2MTS; eopRateInit.x = -erpv.lod * S2MTS; break; - } - - measEntry.addDsgnEntry(kfKey, eopPartials(i), init); - - if (eopRateInit.estimate == false) - { - continue; - } - - KFKey rateKey; - rateKey.type = KF::EOP_RATE; - rateKey.num = i; - kfKey.comment = (string) eopComments[i] + "/day"; - - kfState.setKFTransRate(kfKey, rateKey, 1/S_IN_DAY, eopRateInit); - } - - if (acsConfig.pppOpts.add_eop_component) - { - auto& [eopAdjustment] = rec.pppEopCache.useCache([&]() -> tuple - { - ERPValues erpvBase = getErp(nav.erp, time, false); - - FrameSwapper frameSwapperBase(time, erpvBase); - - Matrix3d erpEcefAdjustmentMat = frameSwapper.i2t_mat * frameSwapperBase.i2t_mat.transpose() - - Matrix3d::Identity(); - - Vector3d eopAdjustment = erpEcefAdjustmentMat * rRec; - - return {eopAdjustment}; - }); - - double adjustment = e.dot(eopAdjustment); - - measEntry.componentsMap[E_Component::EOP] = {adjustment, "+ eop", -1}; - } + Matrix3d partialMatrix = receiverEopPartials(rRec); + Vector3d eopPartials = partialMatrix * e; + + for (int i = 0; i < 3; i++) + { + InitialState init = initialStateFromConfig(acsConfig.pppOpts.eop, i); + InitialState eopRateInit = initialStateFromConfig(acsConfig.pppOpts.eop_rates, i); + + if (init.estimate == false) + { + continue; + } + + KFKey kfKey; + kfKey.type = KF::EOP; + kfKey.num = i; + kfKey.comment = eopComments[i]; + + kfState.getKFValue(kfKey, init.x); + + if (init.x == 0) + switch (i) + { + case 0: + init.x = erpv.xp * R2MAS; + eopRateInit.x = +erpv.xpr * R2MAS; + break; + case 1: + init.x = erpv.yp * R2MAS; + eopRateInit.x = +erpv.ypr * R2MAS; + break; + case 2: + init.x = erpv.ut1Utc * S2MTS; + eopRateInit.x = -erpv.lod * S2MTS; + break; + } + + measEntry.addDsgnEntry(kfKey, eopPartials(i), init); + + if (eopRateInit.estimate == false) + { + continue; + } + + KFKey rateKey; + rateKey.type = KF::EOP_RATE; + rateKey.num = i; + kfKey.comment = (string)eopComments[i] + "/day"; + + kfState.setKFTransRate(kfKey, rateKey, 1 / S_IN_DAY, eopRateInit); + } + + if (acsConfig.pppOpts.add_eop_component) + { + auto& [eopAdjustment] = rec.pppEopCache.useCache( + [&]() -> tuple + { + ERPValues erpvBase = getErp(nav.erp, time, false); + + FrameSwapper frameSwapperBase(time, erpvBase); + + Matrix3d erpEcefAdjustmentMat = + frameSwapper.i2t_mat * frameSwapperBase.i2t_mat.transpose() - + Matrix3d::Identity(); + + Vector3d eopAdjustment = erpEcefAdjustmentMat * rRec; + + return {eopAdjustment}; + } + ); + + double adjustment = e.dot(eopAdjustment); + + measEntry.componentsMap[E_Component::EOP] = {adjustment, "+ eop", -1}; + } } inline static void pppRecClocks(COMMON_PPP_ARGS) { - double recClk_m = rec.aprioriClk; - double dtRecVar = rec.aprioriClkVar; + double recClk_m = rec.aprioriClk; + double dtRecVar = rec.aprioriClkVar; - KFKey kfKey; - kfKey.type = KF::REC_CLOCK; - kfKey.str = rec.id; - kfKey.rec_ptr = &rec; + KFKey kfKey; + kfKey.type = KF::REC_CLOCK; + kfKey.str = rec.id; + kfKey.rec_ptr = &rec; - E_Source found = kfState.getKFValue(kfKey, recClk_m, &dtRecVar); + E_Source found = kfState.getKFValue(kfKey, recClk_m, &dtRecVar); - for (int i = 0; i < recOpts.clk.estimate.size(); i++) - { - InitialState init = initialStateFromConfig(recOpts.clk, i); + for (int i = 0; i < recOpts.clk.estimate.size(); i++) + { + InitialState init = initialStateFromConfig(recOpts.clk, i); - if (init.estimate == false) - { - continue; - } + if (init.estimate == false) + { + continue; + } - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = dtRecVar; - } + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = dtRecVar; + } - dtRecVar = -1; + dtRecVar = -1; - if (i == 0) - { - init.x = recClk_m; - recClk_m = 0; - } + if (i == 0) + { + init.x = recClk_m; + recClk_m = 0; + } - kfKey.num = i; - kfKey.comment = init.comment; + kfKey.num = i; + kfKey.comment = init.comment; - kfState.getKFValue(kfKey, init.x); + kfState.getKFValue(kfKey, init.x); - recClk_m += init.x; + recClk_m += init.x; - measEntry.addDsgnEntry(kfKey, 1, init); + measEntry.addDsgnEntry(kfKey, 1, init); - InitialState rateInit = initialStateFromConfig(recOpts.clk_rate, i); + InitialState rateInit = initialStateFromConfig(recOpts.clk_rate, i); - if (rateInit.estimate) - { - KFKey rateKey = kfKey; - rateKey.type = KF::REC_CLOCK_RATE; + if (rateInit.estimate) + { + KFKey rateKey = kfKey; + rateKey.type = KF::REC_CLOCK_RATE; - kfState.setKFTransRate(kfKey, rateKey, 1, rateInit); - } - } + kfState.setKFTransRate(kfKey, rateKey, 1, rateInit); + } + } - measEntry.addNoiseEntry(kfKey, 1, dtRecVar); + measEntry.addNoiseEntry(kfKey, 1, dtRecVar); - measEntry.componentsMap[E_Component::REC_CLOCK] = {recClk_m, "+ Cdt_r", dtRecVar}; + measEntry.componentsMap[E_Component::REC_CLOCK] = {recClk_m, "+ Cdt_r", dtRecVar}; } inline static void pppSatClocks(COMMON_PPP_ARGS) { - double satClk_m = obs.satClk * CLIGHT; - double satClkVar = 0; + // Don't obliterate obs.satClk in satclk below, we still need the old one for next signal/phase + SatPos satPos0 = obs; + + // Use nominal epoch for satellite clock initialisation + satclk(trace, tsync, tsync, satPos0, satOpts.clockModel.sources, nav, &kfState, &remoteKF); - KFKey kfKey; - kfKey.type = KF::SAT_CLOCK; - kfKey.Sat = Sat; + double satClk_m = satPos0.satClk * CLIGHT; + double satClkVar = 0; - E_Source found = kfState.getKFValue(kfKey, satClk_m, &satClkVar); + KFKey kfKey; + kfKey.type = KF::SAT_CLOCK; + kfKey.Sat = Sat; - for (int i = 0; i < satOpts.clk.estimate.size(); i++) - { - InitialState init = initialStateFromConfig(satOpts.clk, i); + E_Source found = kfState.getKFValue(kfKey, satClk_m, &satClkVar); - if (init.estimate == false) - { - continue; - } + for (int i = 0; i < satOpts.clk.estimate.size(); i++) + { + InitialState init = initialStateFromConfig(satOpts.clk, i); - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = satClkVar; - } + if (init.estimate == false) + { + continue; + } - satClkVar = -1; + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = satClkVar; + } - if (i == 0) - { - init.x = satClk_m; - satClk_m = 0; - } + satClkVar = -1; - kfKey.num = i; - kfKey.comment = init.comment; + if (i == 0) + { + init.x = satClk_m; + satClk_m = 0; + } - kfState.getKFValue(kfKey, init.x); + kfKey.num = i; + kfKey.comment = init.comment; - satClk_m += init.x; + kfState.getKFValue(kfKey, init.x); - measEntry.addDsgnEntry(kfKey, -1, init); - measEntry.addDsgnEntry(kfKey, -obs.satVel.dot(satStat.e) / CLIGHT, init); + satClk_m += init.x; - InitialState rateInit = initialStateFromConfig(satOpts.clk_rate, i); + measEntry.addDsgnEntry(kfKey, -1, init); + measEntry.addDsgnEntry( + kfKey, + -obs.satVel.dot(satStat.e) / CLIGHT, + init + ); // Changes in satellite position or geometric distance calculation due to adjustment of + // satellite clock offset - if (rateInit.estimate) - { - KFKey rateKey = kfKey; - rateKey.type = KF::SAT_CLOCK_RATE; + InitialState rateInit = initialStateFromConfig(satOpts.clk_rate, i); - kfState.setKFTransRate(kfKey, rateKey, 1, rateInit); - } - } + if (rateInit.estimate) + { + KFKey rateKey = kfKey; + rateKey.type = KF::SAT_CLOCK_RATE; - measEntry.addNoiseEntry(kfKey, 1, satClkVar); + kfState.setKFTransRate(kfKey, rateKey, 1, rateInit); + } + } - measEntry.componentsMap[E_Component::SAT_CLOCK] = {-satClk_m, "- Cdt_s", satClkVar}; + measEntry.addNoiseEntry(kfKey, 1, satClkVar); + + measEntry.componentsMap[E_Component::SAT_CLOCK] = { + -obs.satClk * CLIGHT, + "- Cdt_s", + satClkVar + }; // Account for changes within time of fight due to clock rate in residual calculation } inline static void pppRecAntDelta(COMMON_PPP_ARGS) { - Vector3d bodyAntVector = rec.antDelta; - Vector3d bodyLook = ecef2body(rec.attStatus, satStat.e); + Vector3d bodyAntVector = rec.antDelta; + Vector3d bodyLook = ecef2body(rec.attStatus, satStat.e); - double variance = 0; + double variance = 0; - for (int i = 0; i < 3; i++) - { - int enumNum = KF::ANT_DELTA; + for (int i = 0; i < 3; i++) + { + int enumNum = static_cast(KF::ANT_DELTA); - InitialState init = initialStateFromConfig(recOpts.ant_delta, i); + InitialState init = initialStateFromConfig(recOpts.ant_delta, i); - if (init.estimate == false) - { - continue; - } + if (init.estimate == false) + { + continue; + } - if (init.Q < 0) - { - init.P = variance; - } + if (init.Q < 0) + { + init.P = variance; + } - variance = -1; + variance = -1; - KFKey kfKey; - kfKey.type = KF::ANT_DELTA; - kfKey.str = rec.id; - kfKey.num = i; + KFKey kfKey; + kfKey.type = KF::ANT_DELTA; + kfKey.str = rec.id; + kfKey.num = i; - init.x = bodyAntVector(i); + init.x = bodyAntVector(i); - kfState.getKFValue(kfKey, init.x); + kfState.getKFValue(kfKey, init.x); - bodyAntVector(i) = init.x; + bodyAntVector(i) = init.x; - measEntry.addDsgnEntry(kfKey, -bodyLook(i), init); - } + measEntry.addDsgnEntry(kfKey, -bodyLook(i), init); + } - //todo aaron needs noise + // todo aaron needs noise - double recAntDelta = -bodyAntVector.dot(bodyLook); + double recAntDelta = -bodyAntVector.dot(bodyLook); - measEntry.componentsMap[E_Component::REC_ANTENNA_DELTA] = {recAntDelta, "- E.dR_r", 0}; + measEntry.componentsMap[E_Component::REC_ANTENNA_DELTA] = {recAntDelta, "- E.dR_r", 0}; } inline static void pppRecPCO(COMMON_PPP_ARGS) { - E_FType recAtxFt = ft; - if (acsConfig.common_rec_pco) - { - if (Sat.sys == +E_Sys::GLO) recAtxFt = G1; - else if (Sat.sys == +E_Sys::BDS) recAtxFt = B1; - else recAtxFt = F1; - } - - auto& attStatus = rec.attStatus; - - double variance = 0; - MatrixXd dEdQ; - if (initialStateFromConfig(recOpts.orientation).estimate) - dEdQ = MatrixXd::Zero(3,4); - - Vector3d bodyPCO = antPco(rec.antennaId, Sat.sys, recAtxFt, time, variance, E_Radio::RECEIVER, acsConfig.interpolate_rec_pco); - Vector3d bodyLook = ecef2body(attStatus, satStat.e, &dEdQ); //todo aaron, move this to antDelta instead - - for (int i = 0; i < 3; i++) - { - int enumNum = KF::REC_PCO_X + i; - - InitialState init = initialStateFromConfig(recOpts.pco, i); - - if (init.estimate == false) - { - continue; - } - - if (init.Q < 0) - { -// init.P = variance; //bad things happen (no iflc) if this is zero, as is often the case for varIono - } - - variance = -1; - - KFKey kfKey; - kfKey.type = KF::_from_integral(enumNum); - kfKey.str = rec.id; - kfKey.num = recAtxFt; - - init.x = bodyPCO(i); - - kfState.getKFValue(kfKey, init.x); - - bodyPCO(i) = init.x; - - measEntry.addDsgnEntry(kfKey, -bodyLook(i), init); - } - - if (initialStateFromConfig(recOpts.orientation).estimate) - for (int i = 0; i < 4; i++) - { - InitialState init = initialStateFromConfig(recOpts.orientation, i); - - KFKey kfKey; - kfKey.type = KF::ORIENTATION; - kfKey.str = rec.id; - kfKey.num = i; - - measEntry.addDsgnEntry(kfKey, -bodyPCO.dot(dEdQ.col(i))); - } - - //todo aaron, needs noise - - double recPCODelta = -bodyPCO.dot(bodyLook); - - measEntry.componentsMap[E_Component::REC_PCO] = {recPCODelta, "- E.PCO_r", variance}; + E_FType recAtxFt = ft; + if (acsConfig.common_rec_pco) + { + if (Sat.sys == E_Sys::GLO) + recAtxFt = G1; + else if (Sat.sys == E_Sys::BDS) + recAtxFt = B1; + else + recAtxFt = F1; + } + + auto& attStatus = rec.attStatus; + + double variance = 0; + MatrixXd dEdQ; + if (initialStateFromConfig(recOpts.orientation).estimate) + dEdQ = MatrixXd::Zero(3, 4); + + Vector3d bodyPCO = antPco( + rec.antennaId, + Sat.sys, + recAtxFt, + time, + variance, + E_Radio::RECEIVER, + acsConfig.interpolate_rec_pco + ); + Vector3d bodyLook = + ecef2body(attStatus, satStat.e, &dEdQ); // todo aaron, move this to antDelta instead + + for (int i = 0; i < 3; i++) + { + int enumNum = static_cast(KF::REC_PCO_X) + i; + + InitialState init = initialStateFromConfig(recOpts.pco, i); + + if (init.estimate == false) + { + continue; + } + + if (init.Q < 0) + { + // init.P = variance; //bad things happen (no iflc) if this is zero, as is + // often the case for varIono + } + + variance = -1; + + KFKey kfKey; + kfKey.type = int_to_enum(enumNum); + kfKey.str = rec.id; + kfKey.num = recAtxFt; + + init.x = bodyPCO(i); + + kfState.getKFValue(kfKey, init.x); + + bodyPCO(i) = init.x; + + measEntry.addDsgnEntry(kfKey, -bodyLook(i), init); + } + + if (initialStateFromConfig(recOpts.orientation).estimate) + for (int i = 0; i < 4; i++) + { + InitialState init = initialStateFromConfig(recOpts.orientation, i); + + KFKey kfKey; + kfKey.type = KF::ORIENTATION; + kfKey.str = rec.id; + kfKey.num = i; + + measEntry.addDsgnEntry(kfKey, -bodyPCO.dot(dEdQ.col(i))); // Eugene: init? + } + + // todo aaron, needs noise + + double recPCODelta = -bodyPCO.dot(bodyLook); + + measEntry.componentsMap[E_Component::REC_PCO] = {recPCODelta, "- E.PCO_r", variance}; } inline static void pppSatPCO(COMMON_PPP_ARGS) { - E_FType satAtxFt = ft; + E_FType satAtxFt = ft; - if (acsConfig.common_sat_pco) - { - if (Sat.sys == +E_Sys::GLO) satAtxFt = G1; - else if (Sat.sys == +E_Sys::BDS) satAtxFt = B1; - else satAtxFt = F1; - } + if (acsConfig.common_sat_pco) + { + if (Sat.sys == E_Sys::GLO) + satAtxFt = G1; + else if (Sat.sys == E_Sys::BDS) + satAtxFt = B1; + else + satAtxFt = F1; + } - auto& attStatus = satNav.attStatus; + auto& attStatus = satNav.attStatus; - double variance = 0; + double variance = 0; - Vector3d bodyPCO = antPco(Sat.id(), Sat.sys, satAtxFt, time, variance, E_Radio::TRANSMITTER); - Vector3d bodyLook = ecef2body(attStatus, satStat.e); + Vector3d bodyPCO = antPco(Sat.id(), Sat.sys, satAtxFt, time, variance, E_Radio::TRANSMITTER); + Vector3d bodyLook = ecef2body(attStatus, satStat.e); - for (int i = 0; i < 3; i++) - { - int enumNum = KF::SAT_PCO_X + i; + for (int i = 0; i < 3; i++) + { + int enumNum = static_cast(KF::SAT_PCO_X) + i; - InitialState init = initialStateFromConfig(satOpts.pco, i); + InitialState init = initialStateFromConfig(satOpts.pco, i); - if (init.estimate == false) - { - continue; - } + if (init.estimate == false) + { + continue; + } - if (init.Q < 0) - { -// init.P = variance; //bad things happen (no iflc) if this is zero, as is often the case for varIono - } + if (init.Q < 0) + { + // init.P = variance; //bad things happen (no iflc) if this is zero, as is + // often the case for varIono + } - variance = -1; + variance = -1; - KFKey kfKey; - kfKey.type = KF::_from_integral(enumNum); - kfKey.Sat = obs.Sat; - kfKey.num = satAtxFt; + KFKey kfKey; + kfKey.type = int_to_enum(enumNum); + kfKey.Sat = obs.Sat; + kfKey.num = satAtxFt; - init.x = bodyPCO(i); + init.x = bodyPCO(i); - kfState.getKFValue(kfKey, init.x); + kfState.getKFValue(kfKey, init.x); - bodyPCO(i) = init.x; + bodyPCO(i) = init.x; - measEntry.addDsgnEntry(kfKey, bodyLook(i), init); - } + measEntry.addDsgnEntry(kfKey, bodyLook(i), init); + } - AutoSender autoSender = autoSenderTemplate; + AutoSender autoSender = autoSenderTemplate; - autoSender.pushBaseKVP (4, {"data", __FUNCTION__}); - autoSender.setValueKVPs (4, - { - {"bodyLook[0]", bodyLook[0]}, - {"bodyLook[1]", bodyLook[1]}, - {"bodyLook[2]", bodyLook[2]}, - {"bodyPCO[0]", bodyPCO[0]}, - {"bodyPCO[1]", bodyPCO[1]}, - {"bodyPCO[2]", bodyPCO[2]}, - {"nominalYaw", attStatus.nominalYaw}, - {"modelYaw", attStatus.modelYaw} - }); + autoSender.pushBaseKVP(4, {"data", "attitude"}); + autoSender.setValueKVPs( + 4, + {{"bodyLook[0]", bodyLook[0]}, + {"bodyLook[1]", bodyLook[1]}, + {"bodyLook[2]", bodyLook[2]}, + {"bodyPCO[0]", bodyPCO[0]}, + {"bodyPCO[1]", bodyPCO[1]}, + {"bodyPCO[2]", bodyPCO[2]}, + {"nominalYaw", attStatus.nominalYaw}, + {"modelYaw", attStatus.modelYaw}} + ); - //todo aaron, needs noise + // todo aaron, needs noise - double satPCODelta = bodyPCO.dot(bodyLook); + double satPCODelta = bodyPCO.dot(bodyLook); - measEntry.componentsMap[E_Component::SAT_PCO] = {satPCODelta, "+ E.PCO_s", variance}; + measEntry.componentsMap[E_Component::SAT_PCO] = {satPCODelta, "+ E.PCO_s", variance}; } inline static void pppRecPCV(COMMON_PPP_ARGS) { - E_FType recAtxFt = ft; - - if (acsConfig.common_rec_pco) - { - if (Sat.sys == +E_Sys::GLO) recAtxFt = G1; - else if (Sat.sys == +E_Sys::BDS) recAtxFt = B1; - else recAtxFt = F1; - } - - double az = 0; - double zen = 0; - double recPCVDelta = antPcv(rec.antennaId, Sat.sys, recAtxFt, time, rec.attStatus, satStat.e * +1, &az, &zen); - - InitialState init = initialStateFromConfig(recOpts.pcv); - - if (init.estimate) - { - init.x = recPCVDelta; - recPCVDelta = 0; - - double az_delta = 5; - double el_delta = 5; - - double azFrac = az / az_delta; - double elFrac = zen / el_delta; - - KFKey kfKey; - kfKey.type = KF::REC_PCV; - kfKey.str = rec.id; - - for (auto iAz : {0, 1}) - for (auto iEl : {0, 1}) - { - int indexAz = ((int) (azFrac + iAz)) * az_delta; - int indexEl = ((int) (elFrac + iEl)) * el_delta; - - if (indexAz > 360) indexAz -= 360; - if (indexEl > 90) indexEl = 90; - - kfKey.num = 10000 * indexAz - + 100 * indexEl - + 1 * recAtxFt; - - double scalar = 1; - if (iAz) scalar *= azFrac - (int) azFrac; else scalar *= ((int) (azFrac + 1)) - azFrac; - if (iEl) scalar *= elFrac - (int) elFrac; else scalar *= ((int) (elFrac + 1)) - elFrac; - - kfState.getKFValue(kfKey, init.x); - - recPCVDelta += scalar * init.x; - - measEntry.addDsgnEntry(kfKey, scalar, init); - } - } - - measEntry.componentsMap[E_Component::REC_PCV] = {recPCVDelta, "+ PCV_r", 0}; + E_FType recAtxFt = ft; + + if (acsConfig.common_rec_pco) + { + if (Sat.sys == E_Sys::GLO) + recAtxFt = G1; + else if (Sat.sys == E_Sys::BDS) + recAtxFt = B1; + else + recAtxFt = F1; + } + + double az = 0; + double zen = 0; + double recPCVDelta = + antPcv(rec.antennaId, Sat.sys, recAtxFt, time, rec.attStatus, satStat.e * +1, &az, &zen); + + InitialState init = initialStateFromConfig(recOpts.pcv); + + if (init.estimate) + { + init.x = recPCVDelta; + recPCVDelta = 0; + + double az_delta = 5; + double el_delta = 5; + + double azFrac = az / az_delta; + double elFrac = zen / el_delta; + + KFKey kfKey; + kfKey.type = KF::REC_PCV; + kfKey.str = rec.id; + + for (auto iAz : {0, 1}) + for (auto iEl : {0, 1}) + { + int indexAz = ((int)(azFrac + iAz)) * az_delta; + int indexEl = ((int)(elFrac + iEl)) * el_delta; + + if (indexAz > 360) + indexAz -= 360; + if (indexEl > 90) + indexEl = 90; + + kfKey.num = 10000 * indexAz + 100 * indexEl + 1 * recAtxFt; + + double scalar = 1; + if (iAz) + scalar *= azFrac - (int)azFrac; + else + scalar *= ((int)(azFrac + 1)) - azFrac; + if (iEl) + scalar *= elFrac - (int)elFrac; + else + scalar *= ((int)(elFrac + 1)) - elFrac; + + kfState.getKFValue(kfKey, init.x); + + recPCVDelta += scalar * init.x; + + measEntry.addDsgnEntry(kfKey, scalar, init); + } + } + + measEntry.componentsMap[E_Component::REC_PCV] = {recPCVDelta, "+ PCV_r", 0}; } inline static void pppSatPCV(COMMON_PPP_ARGS) { - E_FType satAtxFt = ft; - - if (acsConfig.common_sat_pco) - { - if (Sat.sys == +E_Sys::GLO) satAtxFt = G1; - else if (Sat.sys == +E_Sys::BDS) satAtxFt = B1; - else satAtxFt = F1; - } - - double satPCVDelta = antPcv(Sat.id(), Sat.sys, satAtxFt, time, satNav.attStatus, satStat.e * -1); - - measEntry.componentsMap[E_Component::SAT_PCV] = {satPCVDelta, "+ PCV_s", 0}; + E_FType satAtxFt = ft; + + if (acsConfig.common_sat_pco) + { + if (Sat.sys == E_Sys::GLO) + satAtxFt = G1; + else if (Sat.sys == E_Sys::BDS) + satAtxFt = B1; + else + satAtxFt = F1; + } + + double satPCVDelta = + antPcv(Sat.id(), Sat.sys, satAtxFt, time, satNav.attStatus, satStat.e * -1); + + measEntry.componentsMap[E_Component::SAT_PCV] = {satPCVDelta, "+ PCV_s", 0}; } inline static void pppTides(COMMON_PPP_ARGS) { - auto [solid, otl, atl, spole, opole] = tideDelta(trace, time, rec, rRec, recOpts); + auto [solid, otl, atl, spole, opole] = tideDelta(trace, time, rec, rRec, recOpts); - measEntry.componentsMap[E_Component::TIDES_SOLID ] = {-satStat.e.dot(solid), "- E.dT1", 0}; - measEntry.componentsMap[E_Component::TIDES_OTL ] = {-satStat.e.dot(otl), "- E.dT2", 0}; - measEntry.componentsMap[E_Component::TIDES_ATL ] = {-satStat.e.dot(atl), "- E.dT3", 0}; - measEntry.componentsMap[E_Component::TIDES_SPOLE ] = {-satStat.e.dot(spole), "- E.dT4", 0}; - measEntry.componentsMap[E_Component::TIDES_OPOLE ] = {-satStat.e.dot(opole), "- E.dT5", 0}; + measEntry.componentsMap[E_Component::TIDES_SOLID] = {-satStat.e.dot(solid), "- E.dT1", 0}; + measEntry.componentsMap[E_Component::TIDES_OTL] = {-satStat.e.dot(otl), "- E.dT2", 0}; + measEntry.componentsMap[E_Component::TIDES_ATL] = {-satStat.e.dot(atl), "- E.dT3", 0}; + measEntry.componentsMap[E_Component::TIDES_SPOLE] = {-satStat.e.dot(spole), "- E.dT4", 0}; + measEntry.componentsMap[E_Component::TIDES_OPOLE] = {-satStat.e.dot(opole), "- E.dT5", 0}; }; inline static void pppRelativity(COMMON_PPP_ARGS) { - /* note that relativity effect to estimate sat clock */ - double dtRel1 = relativity1(rSat, obs.satVel); + /* note that relativity effect to estimate sat clock */ + double dtRel1 = relativity1(rSat, obs.satVel); - measEntry.componentsMap[E_Component::RELATIVITY1] = {dtRel1 * CLIGHT, "+ rel1", 0}; + measEntry.componentsMap[E_Component::RELATIVITY1] = {dtRel1 * CLIGHT, "+ rel1", 0}; } inline static void pppRelativity2(COMMON_PPP_ARGS) { - /* secondary relativity effect (Shapiro effect) */ - double dtRel2 = relativity2(rSat, rRec); + /* secondary relativity effect (Shapiro effect) */ + double dtRel2 = relativity2(rSat, rRec); - measEntry.componentsMap[E_Component::RELATIVITY2] = {dtRel2 * CLIGHT, "+ rel2", 0}; + measEntry.componentsMap[E_Component::RELATIVITY2] = {dtRel2 * CLIGHT, "+ rel2", 0}; } inline static void pppSagnac(COMMON_PPP_ARGS) { - double dSagnac = sagnac(rSat, rRec); + double dSagnac = sagnac(rSat, rRec); - measEntry.componentsMap[E_Component::SAGNAC] = {dSagnac, "+ sag", 0}; + measEntry.componentsMap[E_Component::SAGNAC] = {dSagnac, "+ sag", 0}; } inline static void pppIonStec(COMMON_PPP_ARGS) { - double ionosphere_m = 0; - double varIono = 0; - double freq = CLIGHT / lambda; - double alpha = 40.3e16 / SQR(freq); - double factor = 1; - - if (measType == CODE) factor = +1; - else factor = -1; - - //calculate the ionosphere values, variances, and gradients at the operating points - - InitialState init = initialStateFromConfig(recOpts.ion_stec); - -// if (init.estimate == false) - { - double diono = 0; - double dummy = 0; - bool pass = ionoModel(time, pos, satStat, recOpts.mapping_function, acsConfig.pppOpts.ionoOpts.corr_mode, recOpts.mapping_function_layer_height, dummy, diono, varIono); - if (pass) - { - double ionC = SQR(lambda / genericWavelength[F1]); - - ionosphere_m = factor * ionC * diono; - } - } - - double ionosphereStec = ionosphere_m / (factor * alpha); - - string recStr; - if (acsConfig.pppOpts.equate_ionospheres == false) - recStr = rec.id; - - KFKey kfKey; - kfKey.type = KF::IONO_STEC; - kfKey.str = recStr; - kfKey.Sat = obs.Sat; - kfKey.rec_ptr = &rec; - - if (acsConfig.pppOpts.ionoOpts.common_ionosphere == false) - { - kfKey.num = measType; - - if (measType == CODE) { kfKey.comment = "CODE"; } - else if (measType == PHAS) { kfKey.comment = "PHAS"; } - } - - kfKey.comment += init.comment; - - E_Source found = kfState.getKFValue(kfKey, ionosphereStec, &varIono); - - if (init.estimate) - { - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = varIono; - } - - if (init.Q < 0) - { -// init.P = varIono; //bad things happen (no iflc) if this is zero, as is often the case for varIono - } - - varIono = -1; - - init.x = ionosphereStec; - - ionosphereStec = init.x; - - ionosphere_m = factor * alpha * ionosphereStec; - - measEntry.addDsgnEntry(kfKey, factor * alpha, init); - } - - //todo aaron, needs noise - - measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT] = {ionosphere_m, "+ " + std::to_string(factor * alpha) + ".I", varIono}; + double ionosphere_m = 0; + double varIono = 0; + double freq = CLIGHT / lambda; + double alpha = 40.3e16 / SQR(freq); + double factor = 1; + + if (measType == CODE) + factor = +1; + else + factor = -1; + + // calculate the ionosphere values, variances, and gradients at the operating points + + InitialState init = initialStateFromConfig(recOpts.ion_stec); + + // if (init.estimate == false) + { + double diono = 0; + double dummy = 0; + bool pass = ionoModel( + time, + pos, + satStat, + recOpts.mapping_function, + acsConfig.pppOpts.ionoOpts.corr_mode, + recOpts.mapping_function_layer_height, + dummy, + diono, + varIono + ); + if (pass) + { + double ionC = SQR(lambda / genericWavelength[F1]); + + ionosphere_m = factor * ionC * diono; + } + } + + double ionosphereStec = ionosphere_m / (factor * alpha); + + string recStr; + if (acsConfig.pppOpts.equate_ionospheres == false) + recStr = rec.id; + + KFKey kfKey; + kfKey.type = KF::IONO_STEC; + kfKey.str = recStr; + kfKey.Sat = obs.Sat; + kfKey.rec_ptr = &rec; + + if (acsConfig.pppOpts.ionoOpts.common_ionosphere == false) + { + kfKey.num = measType; + + if (measType == CODE) + { + kfKey.comment = "CODE"; + } + else if (measType == PHAS) + { + kfKey.comment = "PHAS"; + } + } + + kfKey.comment += init.comment; + + E_Source found = kfState.getKFValue(kfKey, ionosphereStec, &varIono); + + if (init.estimate) + { + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = varIono; + } + + if (init.Q < 0) + { + // init.P = varIono; //bad things happen (no iflc) if this is zero, as is + // often the case for varIono + } + + varIono = -1; + + init.x = ionosphereStec; + + ionosphereStec = init.x; + + ionosphere_m = factor * alpha * ionosphereStec; + + measEntry.addDsgnEntry(kfKey, factor * alpha, init); + } + + // todo aaron, needs noise + + measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT] = { + ionosphere_m, + "+ " + std::to_string(factor * alpha) + ".I", + varIono + }; } /** 2nd order ionospheric correction -* See ref [1] -*/ + * See ref [1] + */ inline static void pppIonStec2(COMMON_PPP_ARGS) { - double ionosphere_m = 0; - double varIono = 0; - double freq = CLIGHT / lambda; - double alpha_ = 7527e16 * lambda / SQR(freq); - double factor = 1; - - if (measType == CODE) factor = +1; - else factor = -1 / 2.0; - - VectorPos posp; - ionppp(pos, satStat, RE_MEAN / 1000, recOpts.geomagnetic_field_height, posp); - - Vector3d rB = getGeomagIntensityEcef(time, posp) * 1E-9; // nTesla -> Tesla - - double alpha = -1 * alpha_ * rB.dot(satStat.e); - - //calculate the ionosphere values, variances, and gradients at the operating points - - InitialState init = initialStateFromConfig(recOpts.ion_stec, 1); - - if (init.estimate) - { - if (initialStateFromConfig(recOpts.ion_stec).estimate == false) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Higher order ionosphere estimation requires lower order ionosphere estimation."; - } - - string recStr; - if (acsConfig.pppOpts.equate_ionospheres == false) - recStr = rec.id; - - KFKey kfKey; - kfKey.type = KF::IONO_STEC; - kfKey.str = recStr; - kfKey.Sat = obs.Sat; - kfKey.rec_ptr = &rec; - kfKey.comment = init.comment; - - if (acsConfig.pppOpts.ionoOpts.common_ionosphere == false) - kfKey.num = measType; - - measEntry.addDsgnEntry(kfKey, factor * alpha, init); - - kfState.getKFValue(kfKey, init.x); - - double ionosphere_stec = init.x; - - ionosphere_m = factor * alpha * ionosphere_stec; - - measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT1] = {ionosphere_m, "", 0}; - //component will be added in primary ionospheric model <- F this - } - else - { - double diono = 0; - double dummy = 0; - bool pass = ionoModel(time, pos, satStat, recOpts.mapping_function, acsConfig.pppOpts.ionoOpts.corr_mode, recOpts.mapping_function_layer_height, dummy, diono, varIono); - if (pass) - { - double stec = diono * SQR(FREQ1) / TEC_CONSTANT; // restore STEC - - ionosphere_m = factor * alpha * stec; - } - - measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT1] = {ionosphere_m, "", 0}; - } + if (acsConfig.pppOpts.ionoOpts.use_if_combo) + { + return; + } + + double ionosphere_m = 0; + double varIono = 0; + double freq = CLIGHT / lambda; + double alpha_ = 7527e16 * lambda / SQR(freq); + double factor = 1; + + if (measType == CODE) + factor = +1; + else + factor = -1 / 2.0; + + VectorPos posp; + ionppp(pos, satStat, RE_MEAN / 1000, recOpts.geomagnetic_field_height, posp); + + Vector3d rB = getGeomagIntensityEcef(time, posp) * 1E-9; // nTesla -> Tesla + + double alpha = -1 * alpha_ * rB.dot(satStat.e); + + // calculate the ionosphere values, variances, and gradients at the operating points + + InitialState init = initialStateFromConfig(recOpts.ion_stec, 1); + + if (init.estimate) + { + if (initialStateFromConfig(recOpts.ion_stec).estimate == false) + { + BOOST_LOG_TRIVIAL(warning) << "Higher order ionosphere estimation requires " + "lower order ionosphere estimation."; + } + + string recStr; + if (acsConfig.pppOpts.equate_ionospheres == false) + recStr = rec.id; + + KFKey kfKey; + kfKey.type = KF::IONO_STEC; + kfKey.str = recStr; + kfKey.Sat = obs.Sat; + kfKey.rec_ptr = &rec; + kfKey.comment = init.comment; + + if (acsConfig.pppOpts.ionoOpts.common_ionosphere == false) + kfKey.num = measType; + + measEntry.addDsgnEntry(kfKey, factor * alpha, init); + + kfState.getKFValue(kfKey, init.x); + + double ionosphere_stec = init.x; + + ionosphere_m = factor * alpha * ionosphere_stec; + + measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT1] = {ionosphere_m, "", 0}; + // component will be added in primary ionospheric model <- F this + } + else + { + double diono = 0; + double dummy = 0; + bool pass = ionoModel( + time, + pos, + satStat, + recOpts.mapping_function, + acsConfig.pppOpts.ionoOpts.corr_mode, + recOpts.mapping_function_layer_height, + dummy, + diono, + varIono + ); + if (pass) + { + double stec = diono * SQR(FREQ1) / TEC_CONSTANT; // restore STEC + + ionosphere_m = factor * alpha * stec; + } + + measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT1] = {ionosphere_m, "", 0}; + } } /** 3rd order ionospheric correction -* See ref [1] -*/ + * See ref [1] + */ inline static void pppIonStec3(COMMON_PPP_ARGS) { - double ionosphere_m = 0; - double varIono = 0; - double freq = CLIGHT / lambda; - double alpha = 2437.12557e16 / POW4(freq); - double factor = 1; - - if (measType == CODE) factor = +1; - else factor = -1 / 3.0; - - E_IonoMapFn mapFn; - if (acsConfig.pppOpts.ionoOpts.corr_mode == +E_IonoMode::BROADCAST) mapFn = E_IonoMapFn::KLOBUCHAR; - else mapFn = recOpts.mapping_function; - - double fs = ionmapf(pos, satStat, mapFn, recOpts.mapping_function_layer_height); - - //calculate the ionosphere values, variances, and gradients at the operating points - - InitialState init = initialStateFromConfig(recOpts.ion_stec, 2); - - if (init.estimate) - { - if (initialStateFromConfig(recOpts.ion_stec).estimate == false) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Higher order ionosphere estimation requires lower order ionosphere estimation."; - } - - string recStr; - if (acsConfig.pppOpts.equate_ionospheres == false) - recStr = rec.id; - - KFKey kfKey; - kfKey.type = KF::IONO_STEC; - kfKey.str = recStr; - kfKey.Sat = obs.Sat; - kfKey.rec_ptr = &rec; - kfKey.comment = init.comment; - - if (acsConfig.pppOpts.ionoOpts.common_ionosphere == false) - kfKey.num = measType; - - kfState.getKFValue(kfKey, init.x); - - double ionosphere_stec = init.x; - - double vtec = ionosphere_stec / fs; // restore VTEC for nMax calculation - - double nMax = 20.0e12 + 14.0e12 / 3.17e18 * (vtec * 1e16 - 4.55e18); // calculate nMax with linear interpolation, ref: GAMIT code - - if (nMax < 0) - nMax = 0; // avoid being negative - - measEntry.addDsgnEntry(kfKey, factor * alpha * (2 * nMax + 0.3e12 / 3.17) * 0.66, init); - - ionosphere_m = factor * alpha * nMax * 0.66 * ionosphere_stec; - measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT2] = {ionosphere_m, "", 0}; - //component will be added in primary ionospheric model <- F this - } - else - { - double diono = 0; - double dummy = 0; - bool pass = ionoModel(time, pos, satStat, recOpts.mapping_function, acsConfig.pppOpts.ionoOpts.corr_mode, recOpts.mapping_function_layer_height, dummy, diono, varIono); - if (pass) - { - double stec = diono * SQR(FREQ1) / TEC_CONSTANT; // restore STEC - double vtec = stec / fs; // restore VTEC for nMax calculation - - // nMax = 14.0e12 / 3.17e18 * stec*1e16; // calculate nMax, see ref [1] - double nMax = 20.0e12 + 14.0e12 / 3.17e18 * (vtec * 1e16 - 4.55e18); // calculate nMax with linear interpolation, ref: GAMIT code - - if (nMax < 0) - nMax = 0; // avoid being negative - - ionosphere_m = factor * alpha * nMax * 0.66 * stec; - } - - measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT2] = {ionosphere_m, "", 0}; - } + if (acsConfig.pppOpts.ionoOpts.use_if_combo) + { + return; + } + + double ionosphere_m = 0; + double varIono = 0; + double freq = CLIGHT / lambda; + double alpha = 2437.12557e16 / POW4(freq); + double factor = 1; + + if (measType == CODE) + factor = +1; + else + factor = -1 / 3.0; + + E_IonoMapFn mapFn; + if (acsConfig.pppOpts.ionoOpts.corr_mode == E_IonoMode::BROADCAST) + mapFn = E_IonoMapFn::KLOBUCHAR; + else + mapFn = recOpts.mapping_function; + + double fs = ionmapf(pos, satStat, mapFn, recOpts.mapping_function_layer_height); + + // calculate the ionosphere values, variances, and gradients at the operating points + + InitialState init = initialStateFromConfig(recOpts.ion_stec, 2); + + if (init.estimate) + { + if (initialStateFromConfig(recOpts.ion_stec).estimate == false) + { + BOOST_LOG_TRIVIAL(warning) << "Higher order ionosphere estimation requires " + "lower order ionosphere estimation."; + } + + string recStr; + if (acsConfig.pppOpts.equate_ionospheres == false) + recStr = rec.id; + + KFKey kfKey; + kfKey.type = KF::IONO_STEC; + kfKey.str = recStr; + kfKey.Sat = obs.Sat; + kfKey.rec_ptr = &rec; + kfKey.comment = init.comment; + + if (acsConfig.pppOpts.ionoOpts.common_ionosphere == false) + kfKey.num = measType; + + kfState.getKFValue(kfKey, init.x); + + double ionosphere_stec = init.x; + + double vtec = ionosphere_stec / fs; // restore VTEC for nMax calculation + + // calculate nMax with linear interpolation + double nMax = 20.0e12 + 14.0e12 / 3.17e18 * (vtec * 1e16 - 4.55e18); // ref: GAMIT code + + if (nMax < 0) + nMax = 0; // avoid being negative + + measEntry.addDsgnEntry(kfKey, factor * alpha * (2 * nMax + 0.3e12 / 3.17) * 0.66, init); + + ionosphere_m = factor * alpha * nMax * 0.66 * ionosphere_stec; + measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT2] = {ionosphere_m, "", 0}; + // component will be added in primary ionospheric model <- F this + } + else + { + double diono = 0; + double dummy = 0; + bool pass = ionoModel( + time, + pos, + satStat, + recOpts.mapping_function, + acsConfig.pppOpts.ionoOpts.corr_mode, + recOpts.mapping_function_layer_height, + dummy, + diono, + varIono + ); + if (pass) + { + double stec = diono * SQR(FREQ1) / TEC_CONSTANT; // restore STEC + double vtec = stec / fs; // restore VTEC for nMax calculation + + // Calculate nMax with linear interpolation + // nMax = 14.0e12 / 3.17e18 * stec * 1e16; // ref [1] + double nMax = 20.0e12 + 14.0e12 / 3.17e18 * (vtec * 1e16 - 4.55e18); // ref: GAMIT code + + if (nMax < 0) + nMax = 0; // avoid being negative + + ionosphere_m = factor * alpha * nMax * 0.66 * stec; + } + + measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT2] = {ionosphere_m, "", 0}; + } } inline static void pppIonModel(COMMON_PPP_ARGS) { - if (acsConfig.use_for_iono_model[Sat.sys] == false) - { - return; - } - - double ionosphere_m = 0; - double varIono = 0; - double freq = CLIGHT / lambda; - double alpha = TEC_CONSTANT / SQR(freq); - double sign = 1; - - if (measType == CODE) sign = +1; - else sign = -1; - - InitialState init = initialStateFromConfig(recOpts.ion_model); - - if (init.estimate) - { - for (int i = 0; i < acsConfig.ionModelOpts.layer_heights.size(); i++) - { - VectorPos posp; - obs.ippMap[i].slantFactor = ionppp(pos, satStat, RE_WGS84 / 1000, acsConfig.ionModelOpts.layer_heights[i] / 1000, posp); - obs.ippMap[i].latDeg = posp.latDeg(); - obs.ippMap[i].lonDeg = posp.lonDeg(); - obs.ionoSat = Sat; - } - - tracepdeex(0, trace, "\n Iono Model : %.4f, %.4f %s", obs.ippMap[0].latDeg , obs.ippMap[0].lonDeg, Sat.id()); - - for (int i = 0; i < acsConfig.ionModelOpts.numBasis; i++) - { - double coeff = ionModelCoef(trace, i, obs); - - if (coeff == 0) - continue; - - - KFKey kfKey; - kfKey.type = KF::IONOSPHERIC; - kfKey.rec_ptr = &rec; - kfKey.num = i; - - bool pass = kfState.getKFValue(kfKey, init.x); - - double value = init.x; - - ionosphere_m += sign * alpha * coeff * value; - - tracepdeex(0, trace, "\n basis (%3d); + %.4e x %.4e x %.4e = %.4e", i, alpha, coeff, value, ionosphere_m); - - measEntry.addDsgnEntry(kfKey, sign * alpha * coeff, init); - } - - measEntry.componentsMap[E_Component::IONOSPHERIC_MODEL] = {ionosphere_m, "+ " + std::to_string(sign * alpha) + ".I", -1}; - } + if (acsConfig.use_for_iono_model[Sat.sys] == false) + { + return; + } + + double ionosphere_m = 0; + double varIono = 0; + double freq = CLIGHT / lambda; + double alpha = TEC_CONSTANT / SQR(freq); + double sign = 1; + + if (measType == CODE) + sign = +1; + else + sign = -1; + + InitialState init = initialStateFromConfig(recOpts.ion_model); + + if (init.estimate) + { + for (int i = 0; i < acsConfig.ionModelOpts.layer_heights.size(); i++) + { + VectorPos posp; + obs.ippMap[i].slantFactor = ionppp( + pos, + satStat, + RE_WGS84 / 1000, + acsConfig.ionModelOpts.layer_heights[i] / 1000, + posp + ); + obs.ippMap[i].latDeg = posp.latDeg(); + obs.ippMap[i].lonDeg = posp.lonDeg(); + obs.ionoSat = Sat; + } + + tracepdeex( + 0, + trace, + "\n Iono Model : %.4f, %.4f %s", + obs.ippMap[0].latDeg, + obs.ippMap[0].lonDeg, + Sat.id() + ); + + for (int i = 0; i < acsConfig.ionModelOpts.numBasis; i++) + { + double coeff = ionModelCoef(trace, i, obs); + + if (coeff == 0) + continue; + + KFKey kfKey; + kfKey.type = KF::IONOSPHERIC; + kfKey.rec_ptr = &rec; + kfKey.num = i; + + E_Source passSrc = kfState.getKFValue(kfKey, init.x); + bool pass = passSrc != E_Source::NONE; + + double value = init.x; + + ionosphere_m += sign * alpha * coeff * value; + + tracepdeex( + 0, + trace, + "\n basis (%3d); + %.4e x %.4e x %.4e = %.4e", + i, + alpha, + coeff, + value, + ionosphere_m + ); + + measEntry.addDsgnEntry(kfKey, sign * alpha * coeff, init); + } + + measEntry.componentsMap[E_Component::IONOSPHERIC_MODEL] = { + ionosphere_m, + "+ " + std::to_string(sign * alpha) + ".I", + -1 + }; + } } inline static void pppTropMap(COMMON_PPP_ARGS) { - // double troposphere_m = tropDryZTD(trace, recOpts.tropModel.models, kfState.time, pos); - TropStates tropStates; - TropMapping dTropDx; - double varTrop = 0; - - double modelTroposphere_m = tropModel(trace, recOpts.tropModel.models, time, pos, satStat, tropStates, dTropDx, varTrop); - - InitialState init = initialStateFromConfig(recOpts.trop_maps); - - if (init.estimate) - { - modelTroposphere_m = dTropDx.dryMap * tropDryZTD(trace, recOpts.tropModel.models, time, pos); - - for (int i = 0; i < acsConfig.ssrOpts.nbasis; i++) - { - double geoMap = tropModelCoef(i, pos); - - if (geoMap == 0) - continue; - - KFKey kfKey; - kfKey.type = KF::TROP_MODEL; - kfKey.num = i; - - double value = 0; - bool pass = kfState.getKFValue(kfKey, value); - - modelTroposphere_m += geoMap * dTropDx.wetMap * value; - - tracepdeex(0, trace, "\n Trop Model (%3d); + %.4e x %.4e = %.4e", i, geoMap * dTropDx.wetMap, value, modelTroposphere_m); - - measEntry.addDsgnEntry(kfKey, geoMap * dTropDx.wetMap, init); - } - } - - measEntry.componentsMap[E_Component::TROPOSPHERE_MODEL] = {modelTroposphere_m, "+ " + std::to_string(dTropDx.wetMap) + ".T", -1}; + // double troposphere_m = tropDryZTD(trace, recOpts.tropModel.models, kfState.time, pos); + TropStates tropStates; + TropMapping dTropDx; + double varTrop = 0; + + double modelTroposphere_m = tropModel( + trace, + recOpts.tropModel.models, + time, + pos, + satStat, + tropStates, + dTropDx, + varTrop + ); + + InitialState init = initialStateFromConfig(recOpts.trop_maps); + + if (init.estimate) + { + modelTroposphere_m = + dTropDx.dryMap * tropDryZTD(trace, recOpts.tropModel.models, time, pos); + + for (int i = 0; i < acsConfig.ssrOpts.nbasis; i++) + { + double geoMap = tropModelCoef(i, pos); + + if (geoMap == 0) + continue; + + KFKey kfKey; + kfKey.type = KF::TROP_MODEL; + kfKey.num = i; + + double value = 0; + E_Source passSrc = kfState.getKFValue(kfKey, value); + bool pass = passSrc != E_Source::NONE; + + modelTroposphere_m += geoMap * dTropDx.wetMap * value; + + tracepdeex( + 0, + trace, + "\n Trop Model (%3d); + %.4e x %.4e = %.4e", + i, + geoMap * dTropDx.wetMap, + value, + modelTroposphere_m + ); + + measEntry.addDsgnEntry(kfKey, geoMap * dTropDx.wetMap, init); + } + } + + measEntry.componentsMap[E_Component::TROPOSPHERE_MODEL] = { + modelTroposphere_m, + "+ " + std::to_string(dTropDx.wetMap) + ".T", + -1 + }; } inline static void pppTrop(COMMON_PPP_ARGS) { - TropStates tropStates; - TropMapping dTropDx; - double varTrop = 0; - double filterVar = 0; - double gradVars[2] = {}; - - double troposphere_m = 0; - - string recStr; - if (acsConfig.pppOpts.equate_tropospheres == false) - { - recStr = rec.id; - } - - E_Source found = E_Source::NONE; - E_Source gradsFound = E_Source::NONE; - - //get the previous filter states for linearisation around this operating point - for (int i = 0; i < recOpts.trop.estimate.size(); i++) - { - KFKey kfKey; - kfKey.type = KF::TROP; - kfKey.str = recStr; - kfKey.num = i; - - double value = 0; - found = kfState.getKFValue(kfKey, value, &filterVar); - - tropStates.zenith += value; - } - - for (short i = 0; i < 2; i++) - { - KFKey kfKey; - kfKey.type = KF::TROP_GRAD; - kfKey.str = recStr; - kfKey.num = i; - - gradsFound = kfState.getKFValue(kfKey, tropStates.grads[i], &gradVars[i]); - } - - //calculate the trop values, variances, and gradients at the operating points - troposphere_m = tropModel(trace, recOpts.tropModel.models, time, pos, satStat, tropStates, dTropDx, varTrop); - obs.tropSlant = troposphere_m; - obs.tropSlantVar = varTrop; - - for (int i = 0; i < recOpts.trop.estimate.size(); i++) - { - InitialState init = initialStateFromConfig(recOpts.trop); - - if (init.estimate == false) - { - continue; - } - - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = filterVar; - } - - if (i == 0) - { - init.x = tropStates.zenith; - } - - if (init.Q < 0) - { - init.P = varTrop; - } - - varTrop = -1; - - KFKey kfKey; - kfKey.type = KF::TROP; - kfKey.str = recStr; - kfKey.num = i; - kfKey.comment = init.comment; - - measEntry.addDsgnEntry(kfKey, dTropDx.wetMap, init); - } - - for (short i = 0; i < 2; i++) - { - InitialState init = initialStateFromConfig(recOpts.trop_grads, i); - - if (init.estimate == false) - { - continue; - } - - if ( gradsFound == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = gradVars[i]; - } - - init.x = tropStates.grads[i]; - - KFKey kfKey; - kfKey.type = KF::TROP_GRAD; - kfKey.str = recStr; - kfKey.num = i; - kfKey.comment = init.comment; - - if (i == 0) measEntry.addDsgnEntry(kfKey, dTropDx.northMap, init); - else measEntry.addDsgnEntry(kfKey, dTropDx.eastMap, init); - } - - measEntry.componentsMap[E_Component::TROPOSPHERE] = {troposphere_m, "+ " + std::to_string(dTropDx.wetMap) + ".T", varTrop}; + TropStates tropStates; + TropMapping dTropDx; + double varTrop = 0; + double filterVar = 0; + double gradVars[2] = {}; + + double troposphere_m = 0; + + string recStr; + if (acsConfig.pppOpts.equate_tropospheres == false) + { + recStr = rec.id; + } + + E_Source found = E_Source::NONE; + E_Source gradsFound = E_Source::NONE; + + // get the previous filter states for linearisation around this operating point + for (int i = 0; i < recOpts.trop.estimate.size(); i++) + { + KFKey kfKey; + kfKey.type = KF::TROP; + kfKey.str = recStr; + kfKey.num = i; + + double value = 0; + found = kfState.getKFValue(kfKey, value, &filterVar); + + tropStates.zenith += value; + } + + for (short i = 0; i < 2; i++) + { + KFKey kfKey; + kfKey.type = KF::TROP_GRAD; + kfKey.str = recStr; + kfKey.num = i; + + gradsFound = kfState.getKFValue(kfKey, tropStates.grads[i], &gradVars[i]); + } + + // calculate the trop values, variances, and gradients at the operating points + troposphere_m = tropModel( + trace, + recOpts.tropModel.models, + time, + pos, + satStat, + tropStates, + dTropDx, + varTrop + ); + obs.tropSlant = troposphere_m; + obs.tropSlantVar = varTrop; + + for (int i = 0; i < recOpts.trop.estimate.size(); i++) + { + InitialState init = initialStateFromConfig(recOpts.trop); + + if (init.estimate == false) + { + continue; + } + + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = filterVar; + } + + if (i == 0) + { + init.x = tropStates.zenith; + } + + if (init.Q < 0) + { + init.P = varTrop; + } + + varTrop = -1; + + KFKey kfKey; + kfKey.type = KF::TROP; + kfKey.str = recStr; + kfKey.num = i; + kfKey.comment = init.comment; + + measEntry.addDsgnEntry(kfKey, dTropDx.wetMap, init); + } + + for (short i = 0; i < 2; i++) + { + InitialState init = initialStateFromConfig(recOpts.trop_grads, i); + + if (init.estimate == false) + { + continue; + } + + if (gradsFound == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = gradVars[i]; + } + + init.x = tropStates.grads[i]; + + KFKey kfKey; + kfKey.type = KF::TROP_GRAD; + kfKey.str = recStr; + kfKey.num = i; + kfKey.comment = init.comment; + + if (i == 0) + measEntry.addDsgnEntry(kfKey, dTropDx.northMap, init); + else + measEntry.addDsgnEntry(kfKey, dTropDx.eastMap, init); + } + + measEntry.componentsMap[E_Component::TROPOSPHERE] = { + troposphere_m, + "+ " + std::to_string(dTropDx.wetMap) + ".T", + varTrop + }; } inline static void pppRecPhaseWindup(COMMON_PPP_ARGS) { - phaseWindup(obs, rec, satStat.phw); + phaseWindup(obs, rec, satStat.phw); - double phaseWindup_m = satStat.phw * lambda; + double phaseWindup_m = satStat.phw * lambda; - AutoSender autoSender = autoSenderTemplate; - autoSender.pushValueKVP(4, {"satStat-phw", satStat.phw}); + AutoSender autoSender = autoSenderTemplate; + autoSender.pushValueKVP(4, {"satStat-phw", satStat.phw}); - measEntry.componentsMap[E_Component::PHASE_WIND_UP] = {phaseWindup_m, "+ phi", 0}; + measEntry.componentsMap[E_Component::PHASE_WIND_UP] = {phaseWindup_m, "+ phi", 0}; } inline static void pppSatPhaseWindup(COMMON_PPP_ARGS) { -// phaseWindup(obs, rec, satStat.phw); -// -// double phaseWindup_m = satStat.phw * lambda; -// -// AutoSender autoSender = autoSenderTemplate; -// autoSender.valueKVPs.push_back({"satStat-phw", satStat.phw}); -// -// measEntry.componentsMap[E_Component::PHASE_WIND_UP] = {phaseWindup_m, "+ phi", 0}; + // phaseWindup(obs, rec, satStat.phw); + // + // double phaseWindup_m = satStat.phw * lambda; + // + // AutoSender autoSender = autoSenderTemplate; + // autoSender.valueKVPs.push_back({"satStat-phw", satStat.phw}); + // + // measEntry.componentsMap[E_Component::PHASE_WIND_UP] = {phaseWindup_m, "+ phi", 0}; } inline static void pppIntegerAmbiguity(COMMON_PPP_ARGS) { - double ambiguity = 0; - double ambiguity_m = 0; - double varAmb = 0; + double ambiguity = 0; + double ambiguity_m = 0; + double varAmb = 0; - KFKey kfKey; - kfKey.type = KF::AMBIGUITY; - kfKey.str = rec.id; - kfKey.Sat = obs.Sat; - kfKey.num = sig.code; - kfKey.rec_ptr = &rec; - kfKey.comment = sigName; + KFKey kfKey; + kfKey.type = KF::AMBIGUITY; + kfKey.str = rec.id; + kfKey.Sat = obs.Sat; + kfKey.num = static_cast(sig.code); + kfKey.rec_ptr = &rec; + kfKey.comment = sigName; - if (sig.P) - { - ambiguity = sig.L - sig.P / lambda; - } + if (sig.P) + { + ambiguity = sig.L - sig.P / lambda; + } - E_Source found = kfState.getKFValue(kfKey, ambiguity, &varAmb); + E_Source found = kfState.getKFValue(kfKey, ambiguity, &varAmb); - InitialState init = initialStateFromConfig(recOpts.ambiguity); + InitialState init = initialStateFromConfig(recOpts.ambiguity); - if (init.estimate) - { - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = varAmb; - } + if (init.estimate) + { + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = varAmb; + } - varAmb = -1; + varAmb = -1; - init.x = ambiguity; + init.x = ambiguity; - ambiguity = init.x; + ambiguity = init.x; - ambiguity_m = ambiguity * lambda; + ambiguity_m = ambiguity * lambda; - init.x = ambiguity; - init.P /= SQR(lambda); + init.x = ambiguity; + init.P /= SQR(lambda); - measEntry.addDsgnEntry(kfKey, lambda, init); - } + measEntry.addDsgnEntry(kfKey, lambda, init); + } - measEntry.addNoiseEntry(kfKey, 1, varAmb); + measEntry.addNoiseEntry(kfKey, 1, varAmb); - measEntry.componentsMap[E_Component::PHASE_AMBIGUITY] = {ambiguity_m, "+ " + std::to_string(lambda) + ".N", varAmb}; + measEntry.componentsMap[E_Component::PHASE_AMBIGUITY] = { + ambiguity_m, + "+ " + std::to_string(lambda) + ".N", + varAmb + }; } inline static void pppRecPhasBias(COMMON_PPP_ARGS) { - double recPhasBias = recOpts.phaseBiasModel.default_bias; - double recPhasBiasVar = SQR( recOpts.phaseBiasModel.undefined_sigma); - getBias(trace, time, rec.id, Sat, sig.code, PHAS, recPhasBias, recPhasBiasVar); - - KFKey kfKey; - kfKey.type = KF::PHASE_BIAS; - kfKey.str = rec.id; - kfKey.Sat = sysSat; - kfKey.num = sig.code; - kfKey.rec_ptr = &rec; - kfKey.comment = sigName; - - E_Source found = kfState.getKFValue(kfKey, recPhasBias, &recPhasBiasVar); - - InitialState init = initialStateFromConfig(recOpts.phase_bias); - - if (init.estimate) - { - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = recPhasBiasVar; - } - - init.x = recPhasBias; - - if (init.Q < 0) - { - init.P = recPhasBiasVar; - } - - recPhasBiasVar = -1; - - recPhasBias = init.x; - - measEntry.addDsgnEntry(kfKey, 1, init); - } - - measEntry.addNoiseEntry(kfKey, 1, recPhasBiasVar); - - measEntry.componentsMap[E_Component::REC_PHASE_BIAS] = {recPhasBias, "+ b_" + std::to_string(ft) + "r", recPhasBiasVar}; + double recPhasBias = recOpts.phaseBiasModel.default_bias; + double recPhasBiasVar = SQR(recOpts.phaseBiasModel.undefined_sigma); + bool biasFound = getBias(trace, time, rec.id, Sat, sig.code, PHAS, recPhasBias, recPhasBiasVar); + + KFKey kfKey; + kfKey.type = KF::PHASE_BIAS; + kfKey.str = rec.id; + kfKey.Sat = sysSat; + kfKey.num = static_cast(sig.code); + kfKey.rec_ptr = &rec; + kfKey.comment = sigName; + + E_Source found = kfState.getKFValue(kfKey, recPhasBias, &recPhasBiasVar); + + InitialState init = initialStateFromConfig(recOpts.phase_bias); + + if (init.estimate) + { + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = recPhasBiasVar; + } + + init.x = recPhasBias; + + if (init.Q < 0) + { + init.P = recPhasBiasVar; + } + + recPhasBiasVar = -1; + + recPhasBias = init.x; + + measEntry.addDsgnEntry(kfKey, 1, init); + } + else if (biasFound == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Receiver phase bias not found for " << rec.id << " : " << enum_to_string(sig.code) + << ". Using undefined_sigma: " << recOpts.phaseBiasModel.undefined_sigma; + } + + measEntry.addNoiseEntry(kfKey, 1, recPhasBiasVar); + + measEntry.componentsMap[E_Component::REC_PHASE_BIAS] = { + recPhasBias, + "+ b_" + std::to_string(ft) + "r", + recPhasBiasVar + }; } inline static void pppSatPhasBias(COMMON_PPP_ARGS) { - double satPhasBias = satOpts.phaseBiasModel.default_bias; - double satPhasBiasVar = SQR( satOpts.phaseBiasModel.undefined_sigma); - getBias(trace, time, Sat, Sat, sig.code, PHAS, satPhasBias, satPhasBiasVar); - - KFKey kfKey; - kfKey.type = KF::PHASE_BIAS; - kfKey.Sat = Sat; - kfKey.num = sig.code; - kfKey.comment = sigName; - - E_Source found = kfState.getKFValue(kfKey, satPhasBias, &satPhasBiasVar); - - InitialState init = initialStateFromConfig(satOpts.phase_bias); - - if (init.estimate) - { - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = satPhasBiasVar; - } - - init.x = satPhasBias; - - if (init.Q < 0) - { - init.P = satPhasBiasVar; - } - - satPhasBiasVar = -1; - - satPhasBias = init.x; - - measEntry.addDsgnEntry(kfKey, 1, init); - } - - measEntry.addNoiseEntry(kfKey, 1, satPhasBiasVar); - - measEntry.componentsMap[E_Component::SAT_PHASE_BIAS] = {satPhasBias, "+ b_" + std::to_string(ft) + "s", satPhasBiasVar}; + double satPhasBias = satOpts.phaseBiasModel.default_bias; + double satPhasBiasVar = SQR(satOpts.phaseBiasModel.undefined_sigma); + bool biasFound = getBias(trace, time, Sat, Sat, sig.code, PHAS, satPhasBias, satPhasBiasVar); + + KFKey kfKey; + kfKey.type = KF::PHASE_BIAS; + kfKey.Sat = Sat; + kfKey.num = static_cast(sig.code); + kfKey.comment = sigName; + + E_Source found = kfState.getKFValue(kfKey, satPhasBias, &satPhasBiasVar); + + InitialState init = initialStateFromConfig(satOpts.phase_bias); + + if (init.estimate) + { + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = satPhasBiasVar; + } + + init.x = satPhasBias; + + if (init.Q < 0) + { + init.P = satPhasBiasVar; + } + + satPhasBiasVar = -1; + + satPhasBias = init.x; + + measEntry.addDsgnEntry(kfKey, 1, init); + } + else if (biasFound == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Satellite phase bias not found for " << Sat.id() << " : " + << enum_to_string(sig.code) + << ". Using undefined_sigma: " << satOpts.phaseBiasModel.undefined_sigma; + } + + measEntry.addNoiseEntry(kfKey, 1, satPhasBiasVar); + + measEntry.componentsMap[E_Component::SAT_PHASE_BIAS] = { + satPhasBias, + "+ b_" + std::to_string(ft) + "s", + satPhasBiasVar + }; } inline static void pppRecCodeBias(COMMON_PPP_ARGS) { - double recCodeBias = recOpts.codeBiasModel.default_bias; - double recCodeBiasVar = SQR( recOpts.codeBiasModel.undefined_sigma); - getBias(trace, time, rec.id, Sat, sig.code, CODE, recCodeBias, recCodeBiasVar); - - KFKey kfKey; - kfKey.type = KF::CODE_BIAS; - kfKey.str = rec.id; - kfKey.Sat = sysSat; - kfKey.num = sig.code; - kfKey.rec_ptr = &rec; - kfKey.comment = sigName; - - if (sysSat.sys == +E_Sys::GLO) - { - //Glonass has different frequencies per (pair of) sattelite(s), use separate bias for each (ignore pair because geph may not be available) - kfKey.Sat = obs.Sat; - } - - E_Source found = kfState.getKFValue(kfKey, recCodeBias, &recCodeBiasVar); - - InitialState init = initialStateFromConfig(recOpts.code_bias, sig.code); - - if (init.estimate) - { - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = recCodeBiasVar; - } - - if (recCodeBias != 0) - { - init.x = recCodeBias; - } - - if (init.Q < 0) - { - init.P = recCodeBiasVar; - } - - recCodeBiasVar = -1; - - if (Sat.sys == recOpts.receiver_reference_system) - { - auto& recSysOpts = acsConfig.getRecOpts(rec.id, {sysSat.sys._to_string()}); - - auto thisIt = std::find (recSysOpts.clock_codes.begin(), recSysOpts.clock_codes.end(), sig.code); - auto autoIt = std::find (recSysOpts.clock_codes.begin(), recSysOpts.clock_codes.end(), +E_ObsCode::AUTO); - auto freqIt = std::find_if (recSysOpts.clock_codes.begin(), recSysOpts.clock_codes.end(), [&](E_ObsCode code) - { - return code2Freq[obs.Sat.sys][code] == code2Freq[obs.Sat.sys][sig.code]; - }); - - bool foundCode = thisIt != recSysOpts.clock_codes.end(); - bool foundAuto = autoIt != recSysOpts.clock_codes.end(); - bool foundFreq = freqIt != recSysOpts.clock_codes.end(); - - if ( foundAuto - &&foundFreq) - { - //this frequency is already used in the clock codes, dont use again - foundAuto = false; - } - - if ( foundCode - ||foundAuto) - { - //set the bias to zero, and dont let it change - init.x = 0; - init.P = 0; - init.Q = 0; - - if (foundCode == false) - { - *autoIt = sig.code; - } - } - } - - recCodeBias = init.x; - - measEntry.addDsgnEntry(kfKey, 1, init); - } - - measEntry.addNoiseEntry(kfKey, 1, recCodeBiasVar); - - measEntry.componentsMap[E_Component::REC_CODE_BIAS] = {recCodeBias, "+ d_" + std::to_string(ft) + "r", recCodeBiasVar}; + double recCodeBias = recOpts.codeBiasModel.default_bias; + double recCodeBiasVar = SQR(recOpts.codeBiasModel.undefined_sigma); + bool biasFound = getBias(trace, time, rec.id, Sat, sig.code, CODE, recCodeBias, recCodeBiasVar); + + KFKey kfKey; + kfKey.type = KF::CODE_BIAS; + kfKey.str = rec.id; + kfKey.Sat = sysSat; + kfKey.num = static_cast(sig.code); + kfKey.rec_ptr = &rec; + kfKey.comment = sigName; + + if (sysSat.sys == E_Sys::GLO) + { + // Glonass has different frequencies per (pair of) sattelite(s), use separate bias for each + // (ignore pair because geph may not be available) + kfKey.Sat = obs.Sat; + } + + E_Source found = kfState.getKFValue(kfKey, recCodeBias, &recCodeBiasVar); + + InitialState init = initialStateFromConfig(recOpts.code_bias, static_cast(sig.code)); + + if (init.estimate) + { + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = recCodeBiasVar; + } + + if (recCodeBias != 0) + { + init.x = recCodeBias; + } + + if (init.Q < 0) + { + init.P = recCodeBiasVar; + } + + recCodeBiasVar = -1; + + if (Sat.sys == recOpts.receiver_reference_system) + { + auto& recSysOpts = acsConfig.getRecOpts(rec.id, {sysSat.sysName()}); + + auto thisIt = + std::find(recSysOpts.clock_codes.begin(), recSysOpts.clock_codes.end(), sig.code); + auto autoIt = std::find( + recSysOpts.clock_codes.begin(), + recSysOpts.clock_codes.end(), + E_ObsCode::AUTO + ); + auto freqIt = std::find_if( + recSysOpts.clock_codes.begin(), + recSysOpts.clock_codes.end(), + [&](E_ObsCode code) + { return code2Freq[obs.Sat.sys][code] == code2Freq[obs.Sat.sys][sig.code]; } + ); + + bool foundCode = thisIt != recSysOpts.clock_codes.end(); + bool foundAuto = autoIt != recSysOpts.clock_codes.end(); + bool foundFreq = freqIt != recSysOpts.clock_codes.end(); + + if (foundAuto && foundFreq) + { + // this frequency is already used in the clock codes, dont use again + foundAuto = false; + } + + if (foundCode || foundAuto) + { + // set the bias to zero, and dont let it change + init.x = 0; + init.P = 0; + init.Q = 0; + + if (foundCode == false) + { + *autoIt = sig.code; + } + } + } + + recCodeBias = init.x; + + measEntry.addDsgnEntry(kfKey, 1, init); + } + else if (biasFound == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Receiver code bias not found for " << rec.id << " : " << enum_to_string(sig.code) + << ". Using undefined_sigma: " << recOpts.codeBiasModel.undefined_sigma; + } + + measEntry.addNoiseEntry(kfKey, 1, recCodeBiasVar); + + measEntry.componentsMap[E_Component::REC_CODE_BIAS] = { + recCodeBias, + "+ d_" + std::to_string(ft) + "r", + recCodeBiasVar + }; } inline static void pppSatCodeBias(COMMON_PPP_ARGS) { - double satCodeBias = satOpts.codeBiasModel.default_bias; - double satCodeBiasVar = SQR( satOpts.codeBiasModel.undefined_sigma); - getBias(trace, time, Sat, Sat, sig.code, CODE, satCodeBias, satCodeBiasVar); - - KFKey kfKey; - kfKey.type = KF::CODE_BIAS; - kfKey.Sat = Sat; - kfKey.num = sig.code; - kfKey.comment = sigName; - - E_Source found = kfState.getKFValue(kfKey, satCodeBias, &satCodeBiasVar); - - InitialState init = initialStateFromConfig(satOpts.code_bias); - - if (init.estimate) - { - if ( found == +E_Source::REMOTE - &&init.use_remote_sigma) - { - init.P = satCodeBiasVar; - } - - init.x = satCodeBias; - - if (init.Q < 0) - { - init.P = satCodeBiasVar; - } - - satCodeBiasVar = -1; - - auto& satSysOpts = acsConfig.getSatOpts(obs.Sat); - - auto thisIt = std::find (satSysOpts.clock_codes.begin(), satSysOpts.clock_codes.end(), sig.code); - auto autoIt = std::find (satSysOpts.clock_codes.begin(), satSysOpts.clock_codes.end(), +E_ObsCode::AUTO); - auto freqIt = std::find_if (satSysOpts.clock_codes.begin(), satSysOpts.clock_codes.end(), [&](E_ObsCode code) - { - return code2Freq[obs.Sat.sys][code] == code2Freq[obs.Sat.sys][sig.code]; - }); - - bool foundCode = thisIt != satSysOpts.clock_codes.end(); - bool foundAuto = autoIt != satSysOpts.clock_codes.end(); - bool foundFreq = freqIt != satSysOpts.clock_codes.end(); - - if ( foundAuto - &&foundFreq) - { - //this frequency is already used in the clock codes, dont use again - foundAuto = false; - } - - if ( foundCode - ||foundAuto) - { - //set the bias to zero, and dont let it change - init.x = 0; - init.P = 0; - init.Q = 0; - - if (foundCode == false) - { - *autoIt = sig.code; - } - } - - satCodeBias = init.x; - - measEntry.addDsgnEntry (kfKey, 1, init); - } - - measEntry.addNoiseEntry(kfKey, 1, satCodeBiasVar); - - measEntry.componentsMap[E_Component::SAT_CODE_BIAS] = {satCodeBias, "+ d_" + std::to_string(ft) + "s", satCodeBiasVar}; + double satCodeBias = satOpts.codeBiasModel.default_bias; + double satCodeBiasVar = SQR(satOpts.codeBiasModel.undefined_sigma); + bool biasFound = getBias(trace, time, Sat, Sat, sig.code, CODE, satCodeBias, satCodeBiasVar); + + KFKey kfKey; + kfKey.type = KF::CODE_BIAS; + kfKey.Sat = Sat; + kfKey.num = static_cast(sig.code); + kfKey.comment = sigName; + + E_Source found = kfState.getKFValue(kfKey, satCodeBias, &satCodeBiasVar); + + InitialState init = initialStateFromConfig(satOpts.code_bias); + + if (init.estimate) + { + if (found == E_Source::REMOTE && init.use_remote_sigma) + { + init.P = satCodeBiasVar; + } + + init.x = satCodeBias; + + if (init.Q < 0) + { + init.P = satCodeBiasVar; + } + + satCodeBiasVar = -1; + + auto& satSysOpts = acsConfig.getSatOpts(obs.Sat); + + auto thisIt = + std::find(satSysOpts.clock_codes.begin(), satSysOpts.clock_codes.end(), sig.code); + auto autoIt = std::find( + satSysOpts.clock_codes.begin(), + satSysOpts.clock_codes.end(), + E_ObsCode::AUTO + ); + auto freqIt = std::find_if( + satSysOpts.clock_codes.begin(), + satSysOpts.clock_codes.end(), + [&](E_ObsCode code) + { return code2Freq[obs.Sat.sys][code] == code2Freq[obs.Sat.sys][sig.code]; } + ); + + bool foundCode = thisIt != satSysOpts.clock_codes.end(); + bool foundAuto = autoIt != satSysOpts.clock_codes.end(); + bool foundFreq = freqIt != satSysOpts.clock_codes.end(); + + if (foundAuto && foundFreq) + { + // this frequency is already used in the clock codes, dont use again + foundAuto = false; + } + + if (foundCode || foundAuto) + { + // set the bias to zero, and dont let it change + init.x = 0; + init.P = 0; + init.Q = 0; + + if (foundCode == false) + { + *autoIt = sig.code; + } + } + + satCodeBias = init.x; + + measEntry.addDsgnEntry(kfKey, 1, init); + } + else if (biasFound == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Satellite code bias not found for " << Sat.id() << " : " << enum_to_string(sig.code) + << ". Using undefined_sigma: " << satOpts.codeBiasModel.undefined_sigma; + } + + measEntry.addNoiseEntry(kfKey, 1, satCodeBiasVar); + + measEntry.componentsMap[E_Component::SAT_CODE_BIAS] = { + satCodeBias, + "+ d_" + std::to_string(ft) + "s", + satCodeBiasVar + }; } inline static void pppEopAdjustment(COMMON_PPP_ARGS) { - if (acsConfig.pppOpts.eop.estimate[0] == false) - { - return; - } + if (acsConfig.pppOpts.eop.estimate[0] == false) + { + return; + } - eopAdjustment(time, satStat.e, erpv, frameSwapper, rec, rRec, measEntry, kfState); + eopAdjustment(time, satStat.e, erpv, frameSwapper, rec, rRec, measEntry, kfState); } - -void checkModels( - ReceiverOptions& recOpts, - SatelliteOptions& satOpts) +void checkModels(ReceiverOptions& recOpts, SatelliteOptions& satOpts) { - auto test = []( - string label, - bool estimated, - bool& enable) - { - if ( estimated - && enable == false) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: " << label << " is estimated but model is not enabled"; - } - }; - - test("eop", acsConfig.pppOpts.eop. estimate[0], recOpts.eop); - test("rec_phase_bias", recOpts.phase_bias. estimate[0], recOpts.phaseBiasModel.enable); - test("rec_code_bias", recOpts.code_bias. estimate[0], recOpts.codeBiasModel. enable); - test("sat_phase_bias", satOpts.phase_bias. estimate[0], satOpts.phaseBiasModel.enable); - test("sat_code_bias", satOpts.code_bias. estimate[0], satOpts.codeBiasModel. enable); - test("emp_d_0", satOpts.emp_d_0. estimate[0], satOpts.empirical); - - if (acsConfig.minimise_sat_clock_offsets && nav.ephMap.empty()) { BOOST_LOG_TRIVIAL(warning) << "Warning: `minimise_sat_clock_offsets` configured, but no broadcast ephemerides are available"; } - if (acsConfig.minimise_sat_orbit_offsets && nav.ephMap.empty()) { BOOST_LOG_TRIVIAL(warning) << "Warning: `minimise_sat_orbit_offsets` configured, but no broadcast ephemerides are available"; } + auto test = [](string label, bool estimated, bool& enable) + { + if (estimated && enable == false) + { + BOOST_LOG_TRIVIAL(warning) << label << " is estimated but model is not enabled"; + } + }; + + test("eop", acsConfig.pppOpts.eop.estimate[0], recOpts.eop); + test("rec_phase_bias", recOpts.phase_bias.estimate[0], recOpts.phaseBiasModel.enable); + test("rec_code_bias", recOpts.code_bias.estimate[0], recOpts.codeBiasModel.enable); + test("sat_phase_bias", satOpts.phase_bias.estimate[0], satOpts.phaseBiasModel.enable); + test("sat_code_bias", satOpts.code_bias.estimate[0], satOpts.codeBiasModel.enable); + test("emp_d_0", satOpts.emp_d_0.estimate[0], satOpts.empirical); + + if (acsConfig.minimise_sat_clock_offsets.enable && nav.ephMap.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "`minimise_sat_clock_offsets` configured, but no " + "broadcast ephemerides are available"; + } + if (acsConfig.minimise_sat_orbit_offsets && nav.ephMap.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "`minimise_sat_orbit_offsets` configured, but no " + "broadcast ephemerides are available"; + } } - - - - - - - - - - - -//redefine this to replace with nothing from now on - ie, use the argument name but not its type -#undef COMMON_ARG -#define COMMON_ARG(type) - +// redefine this to replace with nothing from now on - ie, use the argument name but not its type +#undef COMMON_ARG +#define COMMON_ARG(type) void receiverUducGnss( - Trace& pppTrace, ///< Trace to output to - Receiver& rec, ///< Receiver to perform calculations for - const KFState& kfState, ///< Kalman filter object containing the state parameters - KFMeasEntryList& kfMeasEntryList, ///< List to append kf measurements to - const KFState& remoteKF) ///< Kalman filter object containing remote filter values + Trace& pppTrace, ///< Trace to output to + Receiver& rec, ///< Receiver to perform calculations for + KFState& kfState, ///< Kalman filter object containing the state parameters + KFMeasEntryList& kfMeasEntryList, ///< List to append kf measurements to + KFState& remoteKF ///< Kalman filter object containing remote filter values +) { - DOCS_REFERENCE(UDUC_GNSS_Measurements__); - - auto trace = getTraceFile(rec); - auto jsonTrace = getTraceFile(rec, true); - - tracepdeex(0, trace, "\n--------------------- Performing PPP -------------------"); - - if ( rec.obsList.empty() - || rec.invalid) - { - tracepdeex(1, trace, "\n\nReceiver not ready for PPP. Obs=%d", rec.obsList.size()); - - return; - } - - for (auto& obs : only(rec.obsList)) - { - if (acsConfig.process_sys[obs.Sat.sys] == false) - { - continue; - } - - auto& satOpts = acsConfig.getSatOpts(obs.Sat); - - satPosClk(trace, obs.time, obs, nav, satOpts.posModel.sources, satOpts.clockModel.sources, &kfState, &remoteKF, E_OffsetType::COM, E_Relativity::OFF); - - traceJson(1, jsonTrace, tsync, - { - {"data", __FUNCTION__ }, - {"Sat", obs.Sat.id() }, - {"Rec", obs.mount } - }, - { - {"rSatCom[0]", obs.rSatCom[0]}, - {"rSatCom[1]", obs.rSatCom[1]}, - {"rSatCom[2]", obs.rSatCom[2]}, - {"satClk", obs.satClk} - }); - } - - ERPValues erpv = getErp(nav.erp, tsync); - - FrameSwapper frameSwapper(tsync, erpv); - - for (auto& obs : only(rec.obsList)) - for (auto& [ft, sigList] : obs.sigsLists) - for (auto& sig : sigList) - for (auto measType : {PHAS, CODE}) - { - string sigName = sig.code._to_string(); - - AutoSender autoSenderTemplate(jsonTrace, tsync); - - autoSenderTemplate.setBaseKVPs(0, - { - {"Sat", obs.Sat.id() }, - {"Rec", obs.mount }, - {"Sig", sigName }, - {"Type", (long int) measType } - }); - - char measDescription[64]; - snprintf(measDescription, sizeof(measDescription), "%s %s %s", obs.Sat.id().c_str(), sig.code._to_string(), (measType == PHAS) ? "L" : "P"); - - if (acsConfig.process_sys[obs.Sat.sys] == false) - { - tracepdeex(4, trace, "\n%s - System skipped", measDescription); - continue; - } - - if (acsConfig.process_meas[measType] == false) - { - tracepdeex(4, trace, "\n%s - Measurement type skipped", measDescription); - continue; - } - - tracepdeex(1, trace, "\n\n----------------------------------------------------"); - tracepdeex(1, trace, "\nProcessing %s: ", measDescription); - - if ( obs.ephPosValid == false - ||obs.ephClkValid == false) - { - tracepdeex(2,trace, "\n%s excludeSvh", obs.Sat.id().c_str()); - - obs.excludeSVH = true; - } - - if (obs.exclude) - { - auto& ast = autoSenderTemplate; - - if (acsConfig.exclude.bad_spp && obs.excludeBadSPP) { tracepdeex(5, trace, " - excludeBadSPP"); ast.pushValueKVP(2, {"exclude", "bad_spp" }); continue; } - if (acsConfig.exclude.config && obs.excludeConfig) { tracepdeex(5, trace, " - excludeConfig"); ast.pushValueKVP(2, {"exclude", "config" }); continue; } - if (acsConfig.exclude.eclipse && obs.excludeEclipse) { tracepdeex(5, trace, " - excludeEclipse"); ast.pushValueKVP(2, {"exclude", "eclipse" }); continue; } - if (acsConfig.exclude.elevation && obs.excludeElevation) { tracepdeex(5, trace, " - excludeElevation"); ast.pushValueKVP(2, {"exclude", "elevation" }); continue; } - if (acsConfig.exclude.outlier && obs.excludeOutlier) { tracepdeex(5, trace, " - excludeOutlier"); ast.pushValueKVP(2, {"exclude", "outlier" }); continue; } - if (acsConfig.exclude.system && obs.excludeSystem) { tracepdeex(5, trace, " - excludeSystem"); ast.pushValueKVP(2, {"exclude", "system" }); continue; } - if (acsConfig.exclude.svh && obs.excludeSVH) { tracepdeex(5, trace, " - excludeSVH"); ast.pushValueKVP(2, {"exclude", "svh" }); continue; } - } - - SatNav& satNav = *obs.satNav_ptr; - SatStat& satStat = *obs.satStat_ptr; - SigStat& sigStat = satStat.sigStatMap[sigName]; - SigStat& preprocSigStat = satStat.sigStatMap[ft2string(ft)]; - - if (preprocSigStat.slip.any) - { - auto& ast = autoSenderTemplate; - - if (acsConfig.exclude.LLI && preprocSigStat.slip.LLI) { tracepdeex(2, trace, " - LLI slip excluded"); ast.pushValueKVP(2, {"excludeSlip","LLI" }); continue; } - if (acsConfig.exclude.GF && preprocSigStat.slip.GF) { tracepdeex(2, trace, " - GF slip excluded"); ast.pushValueKVP(2, {"excludeSlip","GF" }); continue; } - if (acsConfig.exclude.MW && preprocSigStat.slip.MW) { tracepdeex(2, trace, " - MW slip excluded"); ast.pushValueKVP(2, {"excludeSlip","MW" }); continue; } - if (acsConfig.exclude.SCDIA && preprocSigStat.slip.SCDIA) { tracepdeex(2, trace, " - SCDIA slip excluded"); ast.pushValueKVP(2, {"excludeSlip","SCDIA" }); continue; } - } - - auto& satOpts = acsConfig.getSatOpts(obs.Sat, {sigName}); - auto& recOpts = acsConfig.getRecOpts(rec.id, {obs.Sat.sys._to_string(), sigName}); - - checkModels(recOpts, satOpts); - - GTime time = obs.time; - - auto Sat = obs.Sat; - auto sys = Sat.sys; - auto sysSat = SatSys(sys); - double lambda = obs.satNav_ptr->lamMap[ft]; - auto code = sig.code; - - string recSatId; - if (recOpts.sat_id.empty()) recSatId = rec.id; - else recSatId = (string)SatSys(recOpts.sat_id.c_str()); - - SatSys recSat(recSatId.c_str()); - - double observed = 0; - if (measType == CODE) observed = sig.P; - else if (measType == PHAS) observed = sig.L * lambda; - - if (observed == 0) - { - tracepdeex(2,trace,"\n No %s observable for %s %s %s", (measType==CODE)?"CODE":"PHASE", obs.Sat.id().c_str(), rec.id.c_str(), sig.code._to_string()); - continue; - } - - KFMeasEntry measEntry(&kfState); - - measEntry.metaDataMap["obs_ptr"] = &obs; - - { - measEntry.metaDataMap["receiverErrorCount"] = &rec.receiverErrorCount; - measEntry.metaDataMap["lastIonTime"] = &satStat.lastIonTime; - } - - if (measType == PHAS) - { - measEntry.metaDataMap["phaseRejectCount"] = &sigStat.phaseRejectCount; - measEntry.metaDataMap["lastPhaseTime"] = &sigStat.lastPhaseTime; - tracepdeex(2,trace,"\n PPP Phase count: %s %s %s %d", rec.id.c_str(), obs.Sat.id().c_str(), sig.code._to_string(), sigStat.phaseRejectCount); - } - - measEntry.obsKey.Sat = obs.Sat; - measEntry.obsKey.str = rec.id; - measEntry.obsKey.num = sig.code; - if (measType == CODE) { measEntry.obsKey.type = KF::CODE_MEAS; measEntry.obsKey.comment = "P-" + (string)sig.code._to_string(); } - else if (measType == PHAS) { measEntry.obsKey.type = KF::PHAS_MEAS; measEntry.obsKey.comment = "L-" + (string)sig.code._to_string(); } - - //Start with the observed measurement and its noise - { - KFKey obsKey; - obsKey.str = rec.id; - obsKey.Sat = obs.Sat; - obsKey.type = measEntry.obsKey.type; - obsKey.num = sig.code; - - double var = 0; - if (measType == CODE) { var = sig.codeVar; } - else if (measType == PHAS) { var = sig.phasVar; } - - measEntry.addNoiseEntry(obsKey, 1, var); - - measEntry.componentsMap[E_Component::OBSERVED] = {-observed, "- Phi", var}; - } - - //Calculate the basic range - - VectorEcef rRec = rec.aprioriPos; - VectorEci rRecInertial = frameSwapper(rRec); - - vector> delayedInits; - - Vector3d recPosVars = -1 * Vector3d::Ones(); - Vector3d recOrbitVars = -1 * Vector3d::Ones(); - Vector3d satOrbitVars = -1 * Vector3d::Ones(); - - //get keys early to declutter other code - KFKey recPosKey; - recPosKey.type = KF::REC_POS; - recPosKey.str = rec.id; - recPosKey.rec_ptr = &rec; - - KFKey recVelKey; - recVelKey.type = KF::REC_POS_RATE; - recVelKey.str = rec.id; - - KFKey recOrbitPosKey; - recOrbitPosKey.type = KF::ORBIT; - if (recSat.prn) recOrbitPosKey.Sat = recSat; - else recOrbitPosKey.str = recSatId; - - KFKey satOrbitPosKey; - satOrbitPosKey.type = KF::ORBIT; - satOrbitPosKey.Sat = obs.Sat; - - KFKey satOrbitVelKey = satOrbitPosKey; - - KFKey recOrbitVelKey = recOrbitPosKey; - - //Linear receiver positions - for (int i = 0; i < 3; i++) - { - InitialState posInit = initialStateFromConfig(recOpts.pos, i); - InitialState velInit = initialStateFromConfig(recOpts.pos_rate, i); - - recPosKey.num = i; - recPosKey.comment = posInit.comment; - - E_Source found = kfState.getKFValue(recPosKey, rRec[i], &recPosVars[i]); - - if (posInit.estimate == false) - { - continue; - } - - if ( found == +E_Source::REMOTE - &&posInit.use_remote_sigma) - { - posInit.P = recPosVars[i]; - } - - if (posInit.x == 0) posInit.x = rRec[i]; - - recPosVars[i] = -1; - - delayedInits.push_back([recPosKey, posInit, i, &measEntry] - (Vector3d satStat_e, Vector3d eSatInertial) - { - measEntry.addDsgnEntry(recPosKey, -satStat_e[i], posInit); - }); - - if (velInit.estimate == false) - { - continue; - } - - recVelKey.num = i; - recVelKey.comment = velInit.comment; - - delayedInits.push_back([recPosKey, recVelKey, velInit, &kfState] - (Vector3d satStat_e, Vector3d eSatInertial) - { - kfState.setKFTransRate(recPosKey, recVelKey, 1, velInit); - }); - } - - //Orbital receiver positions - bool orbitalReceiver = false; - for (int i = 0; i < 3; i++) - { - InitialState posInit = initialStateFromConfig(recOpts.orbit, i); - InitialState velInit = initialStateFromConfig(recOpts.orbit, i + 3); - - recOrbitPosKey.num = i; - recOrbitPosKey.comment = posInit.comment; - - E_Source found = kfState.getKFValue(recOrbitPosKey, rRecInertial[i], &recOrbitVars[i]); - - if (found) - { - orbitalReceiver = true; - } - - if (posInit.estimate == false) - { - continue; - } - - if ( found == +E_Source::REMOTE - &&posInit.use_remote_sigma) - { - posInit.P = recOrbitVars[i]; - } - - if (posInit.x == 0) posInit.x = rRecInertial[i]; - - recOrbitVars[i] = -1; - - recOrbitVelKey.num = i + 3; - recOrbitVelKey.comment = velInit.comment; - - delayedInits.push_back([recOrbitPosKey, posInit, i, &measEntry] - (Vector3d satStat_e, Vector3d eSatInertial) - { - measEntry.addDsgnEntry(recOrbitPosKey, -eSatInertial[i], posInit); - }); - - kfState.addKFState(recOrbitVelKey, velInit); - } - - if (orbitalReceiver) - { - rRec = frameSwapper(rRecInertial); - } - - auto& pos = rec.pos; - - pos = ecef2pos(rRec); - - VectorEcef& rSat = obs.rSatCom; - - if (rSat.isZero()) - { - BOOST_LOG_TRIVIAL(error) << "Error: Satpos is unexpectedly zero for " << rec.id << " - " << Sat.id(); - continue; - } - - if (rRec.isZero()) - { - BOOST_LOG_TRIVIAL(error) << "Error: rRec is unexpectedly zero for " << rec.id; - continue; - } - - if ( initialStateFromConfig(satOpts.orbit).estimate - &&obs.rSatEci0.isZero()) - { - //dont obliterate obs.rSat in satpos below, we still need the old one for next signal/phase - SatPos satPos0 = obs; - - //use this to avoid adding the dt component of position - satpos(trace, tsync, tsync, satPos0, satOpts.posModel.sources, E_OffsetType::COM, nav, &kfState, &remoteKF); - - obs.rSatEci0 = frameSwapper(satPos0.rSatCom, &satPos0.satVel, &obs.vSatEci0); - } - - //Orbital satellite positions. This is mainly for setting up estimation, the positions estimated by this are already calculated elsewhere - for (int i = 0; i < 3; i++) - { - InitialState posInit = initialStateFromConfig(satOpts.orbit, i); - InitialState velInit = initialStateFromConfig(satOpts.orbit, i + 3); - - satOrbitPosKey.num = i; - satOrbitPosKey.comment = posInit.comment; - - double dummyPos = 0; - - E_Source found = kfState.getKFValue(satOrbitPosKey, dummyPos, &satOrbitVars[i]); - - if (posInit.estimate == false) - { - continue; - } - - if ( found == +E_Source::REMOTE - &&posInit.use_remote_sigma) - { - posInit.P = satOrbitVars[i]; - } - - if (posInit.x == 0) posInit.x = obs.rSatEci0[i]; - if (velInit.x == 0) velInit.x = obs.vSatEci0[i]; - - satOrbitVars[i] = -1; - - satOrbitVelKey.num = i + 3; - satOrbitVelKey.comment = velInit.comment; - - delayedInits.push_back([satOrbitPosKey, satOrbitVelKey, posInit, velInit, i, &obs, &measEntry] - (Vector3d satStat_e, Vector3d eSatInertial) - { - measEntry.addDsgnEntry(satOrbitPosKey, +eSatInertial[i], posInit); - measEntry.addDsgnEntry(satOrbitVelKey, -eSatInertial[i] * (obs.tof + obs.satClk), velInit); - }); - } - - if (initialStateFromConfig(satOpts.orbit).estimate) addEmpStates(satOpts, kfState, Sat); - if (initialStateFromConfig(recOpts.orbit).estimate) addEmpStates(recOpts, kfState, recSatId); - - if (initialStateFromConfig(recOpts.orientation).estimate) - { - Matrix3d body2Ecef = rotBasisMat(rec.attStatus.eXBody, rec.attStatus.eYBody, rec.attStatus.eZBody); - - Quaterniond quat = Quaterniond(body2Ecef); - - Quaterniond roter(Eigen::AngleAxis(90 * PI/ 180, Vector3d::UnitZ())); - - Quaterniond extra2 = quat * roter; - - for (int i = 0; i < 4; i++) - { - InitialState init = initialStateFromConfig(recOpts.orientation, i); - - KFKey kfKey; - kfKey.type = KF::ORIENTATION; - kfKey.str = rec.id; - kfKey.num = i; - - if (i == 0) { init.x = extra2.w(); kfState.getKFValue(kfKey, init.x); quat.w() = init.x; } - if (i == 1) { init.x = extra2.x(); kfState.getKFValue(kfKey, init.x); quat.x() = init.x; } - if (i == 2) { init.x = extra2.y(); kfState.getKFValue(kfKey, init.x); quat.y() = init.x; } - if (i == 3) { init.x = extra2.z(); kfState.getKFValue(kfKey, init.x); quat.z() = init.x; } - - kfState.addKFState(kfKey, init); - } - } - - //Range and geometry - - double rRecSat = (rSat - rRec).norm(); - satStat.e = (rSat - rRec).normalized(); - satStat.nadir = acos(satStat.e.dot(rSat.normalized())); - - VectorEci eSatInertial = frameSwapper(satStat.e); - - satazel(pos, satStat.e, satStat); - - //add initialisations for things waiting for an up-to-date satstat - for (auto& delayedInit : delayedInits) - { - delayedInit(satStat.e, eSatInertial); - } - - //add 3d noise for unestimated positions - for (int i = 0; i < 3; i++) - { - recPosKey. num = i; - recOrbitPosKey. num = i; - satOrbitPosKey. num = i; - - measEntry.addNoiseEntry(recPosKey, 1, SQR(satStat.e [i]) * recPosVars [i]); - measEntry.addNoiseEntry(recOrbitPosKey, 1, SQR(eSatInertial [i]) * recOrbitVars [i]); - measEntry.addNoiseEntry(satOrbitPosKey, 1, SQR(eSatInertial [i]) * satOrbitVars [i]); - } - - - tracepdeex(3, trace, "\n%s satstat.e : %20.4f\t%20.4f\t%20.4f", obs.Sat.id().c_str(), satStat.e .x(), satStat.e .y(), satStat.e .z()); - tracepdeex(3, trace, "\n%s apriori : %20.4f\t%20.4f\t%20.4f", obs.Sat.id().c_str(), rec.aprioriPos .x(), rec.aprioriPos .y(), rec.aprioriPos .z()); - tracepdeex(3, trace, "\n%s rSatEcef : %20.4f\t%20.4f\t%20.4f", obs.Sat.id().c_str(), obs.rSatCom .x(), obs.rSatCom .y(), obs.rSatCom .z()); - tracepdeex(3, trace, "\n%s vSatEcef : %20.4f\t%20.4f\t%20.4f", obs.Sat.id().c_str(), obs.satVel .x(), obs.satVel .y(), obs.satVel .z()); - tracepdeex(4, trace, "\n%s rSatEciDt : %20.4f\t%20.4f\t%20.4f", obs.Sat.id().c_str(), obs.rSatEciDt .x(), obs.rSatEciDt .y(), obs.rSatEciDt .z()); - tracepdeex(4, trace, "\n%s rSatEci0 : %20.4f\t%20.4f\t%20.4f", obs.Sat.id().c_str(), obs.rSatEci0 .x(), obs.rSatEci0 .y(), obs.rSatEci0 .z()); - - if (recOpts.range) - { - measEntry.componentsMap[E_Component::RANGE] = {rRecSat, "+ rho", 0}; - } - - if (acsConfig.output_residual_chain) - { - tracepdeex(0, trace, "\n----------------------------------------------------"); - tracepdeex(0, trace, "\nMeasurement for %s %s\n", ((string) measEntry.obsKey).c_str(), sig.code._to_string()); - } - - //Add modelled adjustments and estimated parameter - { - if (recOpts.clockModel.enable) { pppRecClocks (COMMON_PPP_ARGS); } - if (satOpts.clockModel.enable) { pppSatClocks (COMMON_PPP_ARGS); } - if (recOpts.eccentricityModel.enable) { pppRecAntDelta (COMMON_PPP_ARGS); } - // if (acsConfig.model.sat_ant_delta) { pppSatAntDelta (COMMON_PPP_ARGS); } - if (recOpts.pcoModel.enable) { pppRecPCO (COMMON_PPP_ARGS); } - if (satOpts.pcoModel.enable) { pppSatPCO (COMMON_PPP_ARGS); } - if (recOpts.pcvModel.enable) { pppRecPCV (COMMON_PPP_ARGS); } - if (satOpts.pcvModel.enable) { pppSatPCV (COMMON_PPP_ARGS); } - if (recOpts.tideModels.enable) { pppTides (COMMON_PPP_ARGS); } - if (recOpts.relativity) { pppRelativity (COMMON_PPP_ARGS); } - if (recOpts.relativity2) { pppRelativity2 (COMMON_PPP_ARGS); } - if (recOpts.sagnac) { pppSagnac (COMMON_PPP_ARGS); } - if (recOpts.ionospheric_component) { pppIonStec (COMMON_PPP_ARGS); } - if (recOpts.ionospheric_component2) { pppIonStec2 (COMMON_PPP_ARGS); } - if (recOpts.ionospheric_component3) { pppIonStec3 (COMMON_PPP_ARGS); } - if (recOpts.ionospheric_model) { pppIonModel (COMMON_PPP_ARGS); } - if (recOpts.tropModel.enable) { pppTrop (COMMON_PPP_ARGS); } - if (recOpts.eop) { pppEopAdjustment (COMMON_PPP_ARGS); } - } - - if (measType == PHAS) - { - if (recOpts.phaseWindupModel.enable) { pppRecPhaseWindup (COMMON_PPP_ARGS); } - if (satOpts.phaseWindupModel.enable) { pppSatPhaseWindup (COMMON_PPP_ARGS); } - if (recOpts.integer_ambiguity) { pppIntegerAmbiguity (COMMON_PPP_ARGS); } - if (recOpts.phaseBiasModel.enable) { pppRecPhasBias (COMMON_PPP_ARGS); } - if (satOpts.phaseBiasModel.enable) { pppSatPhasBias (COMMON_PPP_ARGS); } - } - - if (measType == CODE) - { - if (recOpts.codeBiasModel.enable) { pppRecCodeBias (COMMON_PPP_ARGS); } - if (satOpts.codeBiasModel.enable) { pppSatCodeBias (COMMON_PPP_ARGS); } - } - - addNilDesignStates(recOpts.gyro_bias, kfState, KF::GYRO_BIAS, 3, recSatId); - addNilDesignStates(recOpts.accelerometer_bias, kfState, KF::ACCL_BIAS, 3, recSatId); - addNilDesignStates(recOpts.gyro_scale, kfState, KF::GYRO_SCALE, 3, recSatId); - addNilDesignStates(recOpts.accelerometer_scale, kfState, KF::ACCL_SCALE, 3, recSatId); - addNilDesignStates(recOpts.imu_offset, kfState, KF::IMU_OFFSET, 3, recSatId); - - //Calculate residuals and form up the measurement - - measEntry.componentsMap[E_Component::NET_RESIDUAL] = {0, "", 0}; - - AutoSender autoSender = autoSenderTemplate; - autoSender.pushBaseKVP (0, {"data", __FUNCTION__}); - - autoSender.pushValueKVP (1, {"rRec[0]", rRec[0]}); - autoSender.pushValueKVP (1, {"rRec[1]", rRec[1]}); - autoSender.pushValueKVP (1, {"rRec[2]", rRec[2]}); - autoSender.pushValueKVP (1, {"El", satStat.el}); - autoSender.pushValueKVP (1, {"Az", satStat.az}); - autoSender.pushValueKVP (1, {"Nadir", satStat.nadir}); - - InteractiveTerminal ss(string("Chains/") + (string) measEntry.obsKey, trace); - - double residual = netResidualAndChainOutputs(ss, obs, measEntry); - - measEntry.setInnov(residual); - - // measEntry.metaDataMap["explain"] = (void*) true; - - kfMeasEntryList.push_back(measEntry); - } - - trace << "\n" << "\n"; + DOCS_REFERENCE(UDUC_GNSS_Measurements__); + + auto trace = getTraceFile(rec); + auto jsonTrace = getTraceFile(rec, true); + + tracepdeex(0, trace, "\n--------------------- Performing PPP -------------------"); + + if (rec.obsList.empty() || rec.invalid) + { + tracepdeex(1, trace, "\n\nReceiver not ready for PPP. Obs=%d", rec.obsList.size()); + + return; + } + + for (auto& obs : only(rec.obsList)) + { + if (acsConfig.process_sys[obs.Sat.sys] == false) + { + continue; + } + + auto& satOpts = acsConfig.getSatOpts(obs.Sat); + + satPosClk( + trace, + obs.time, + obs, + nav, + satOpts.posModel.sources, + satOpts.clockModel.sources, + &kfState, + &remoteKF, + E_OffsetType::COM, + E_Relativity::OFF + ); + + traceJson( + 1, + jsonTrace, + tsync, + {{"data", "uducGnss"}, {"Sat", obs.Sat.id()}, {"Rec", obs.mount}}, + {{"rSatCom[0]", obs.rSatCom[0]}, + {"rSatCom[1]", obs.rSatCom[1]}, + {"rSatCom[2]", obs.rSatCom[2]}, + {"satClk", obs.satClk}} + ); + } + + ERPValues erpv = getErp(nav.erp, tsync); + + FrameSwapper frameSwapper(tsync, erpv); + + for (auto& obs : only(rec.obsList)) + for (auto& [ft, sigList] : obs.sigsLists) + for (auto& sig : sigList) + for (auto measType : {PHAS, CODE}) + { + string sigName = enum_to_string(sig.code); + + AutoSender autoSenderTemplate(jsonTrace, tsync); + + autoSenderTemplate.setBaseKVPs( + 0, + {{"Sat", obs.Sat.id()}, + {"Rec", obs.mount}, + {"Sig", sigName}, + {"Type", std::to_string(measType)}} + ); + + char measDescription[64]; + snprintf( + measDescription, + sizeof(measDescription), + "%s %s %s", + obs.Sat.id().c_str(), + enum_to_string(sig.code).c_str(), + (measType == PHAS) ? "L" : "P" + ); + + if (acsConfig.process_sys[obs.Sat.sys] == false) + { + tracepdeex(4, trace, "\n%s - System skipped", measDescription); + continue; + } + + if (acsConfig.pppOpts.use_primary_signals && &sig != &(*sigList.begin())) + { + tracepdeex(4, trace, "\n%s - Secondary signal skipped", measDescription); + continue; + } + + if (acsConfig.process_meas[measType] == false) + { + tracepdeex(4, trace, "\n%s - Measurement type skipped", measDescription); + continue; + } + + tracepdeex( + 1, + trace, + "\n\n----------------------------------------------------" + ); + tracepdeex(1, trace, "\nProcessing %s: ", measDescription); + + if (obs.ephPosValid == false || obs.ephClkValid == false) + { + tracepdeex(2, trace, "\n%s excludeSvh", obs.Sat.id().c_str()); + + obs.excludeSVH = true; + } + + if (obs.exclude) + { + auto& ast = autoSenderTemplate; + + if (acsConfig.exclude.bad_spp && obs.excludeBadSPP) + { + tracepdeex(5, trace, " - excludeBadSPP"); + ast.pushValueKVP(2, {"exclude", "bad_spp"}); + continue; + } + if (acsConfig.exclude.config && obs.excludeConfig) + { + tracepdeex(5, trace, " - excludeConfig"); + ast.pushValueKVP(2, {"exclude", "config"}); + continue; + } + if (acsConfig.exclude.eclipse && obs.excludeEclipse) + { + tracepdeex(5, trace, " - excludeEclipse"); + ast.pushValueKVP(2, {"exclude", "eclipse"}); + continue; + } + if (acsConfig.exclude.outlier && obs.excludeOutlier) + { + tracepdeex(5, trace, " - excludeOutlier"); + ast.pushValueKVP(2, {"exclude", "outlier"}); + continue; + } + if (acsConfig.exclude.system && obs.excludeSystem) + { + tracepdeex(5, trace, " - excludeSystem"); + ast.pushValueKVP(2, {"exclude", "system"}); + continue; + } + if (acsConfig.exclude.svh && obs.excludeSVH) + { + tracepdeex(5, trace, " - excludeSVH"); + ast.pushValueKVP(2, {"exclude", "svh"}); + continue; + } + // elevation exclusion moved far below to ensure that elevation is computed + // first + // if (acsConfig.exclude.elevation && obs.excludeElevation) { + // tracepdeex(5, + // trace, " - excludeElevation"); ast.pushValueKVP(2, {"exclude", + // "elevation" }); continue; + //} + } + + SatNav& satNav = *obs.satNav_ptr; + SatStat& satStat = *obs.satStat_ptr; + SigStat& sigStat = satStat.sigStatMap[sigName]; + SigStat& preprocSigStat = satStat.sigStatMap[ft2string(ft)]; + + if (preprocSigStat.slip.any) + { + auto& ast = autoSenderTemplate; + + if (acsConfig.exclude.LLI && preprocSigStat.slip.LLI) + { + tracepdeex(2, trace, " - LLI slip excluded"); + ast.pushValueKVP(2, {"excludeSlip", "LLI"}); + continue; + } + if (acsConfig.exclude.retrack && preprocSigStat.slip.retrack) + { + tracepdeex(2, trace, " - Retrack slip excluded"); + ast.pushValueKVP(2, {"excludeSlip", "retrack"}); + continue; + } + if (acsConfig.exclude.GF && preprocSigStat.slip.GF) + { + tracepdeex(2, trace, " - GF slip excluded"); + ast.pushValueKVP(2, {"excludeSlip", "GF"}); + continue; + } + if (acsConfig.exclude.MW && preprocSigStat.slip.MW) + { + tracepdeex(2, trace, " - MW slip excluded"); + ast.pushValueKVP(2, {"excludeSlip", "MW"}); + continue; + } + if (acsConfig.exclude.SCDIA && preprocSigStat.slip.SCDIA) + { + tracepdeex(2, trace, " - SCDIA slip excluded"); + ast.pushValueKVP(2, {"excludeSlip", "SCDIA"}); + continue; + } + if (acsConfig.exclude.single_freq && preprocSigStat.slip.singleFreq) + { + tracepdeex(2, trace, " - single freqency data excluded"); + ast.pushValueKVP(2, {"excludeSlip", "singleFreq"}); + continue; + } + } + + auto& satOpts = acsConfig.getSatOpts(obs.Sat, {sigName}); + auto& recOpts = acsConfig.getRecOpts(rec.id, {obs.Sat.sysName(), sigName}); + + checkModels(recOpts, satOpts); + + GTime time = obs.time; + + auto Sat = obs.Sat; + auto sys = Sat.sys; + auto sysSat = SatSys(sys); + double lambda = obs.satNav_ptr->lamMap[ft]; + auto code = sig.code; + + string recSatId; + if (recOpts.sat_id.empty()) + recSatId = rec.id; + else + recSatId = (string)SatSys(recOpts.sat_id.c_str()); + + SatSys recSat(recSatId.c_str()); + + double observed = 0; + if (measType == CODE) + observed = sig.P; + else if (measType == PHAS) + observed = sig.L * lambda; + + if (observed == 0) + { + tracepdeex( + 2, + trace, + "\n No %s observable for %s %s %s", + (measType == CODE) ? "CODE" : "PHASE", + obs.Sat.id().c_str(), + rec.id.c_str(), + enum_to_string(sig.code) + ); + continue; + } + + KFMeasEntry measEntry(&kfState); + + measEntry.metaDataMap["pppObs_ptr"] = &obs; + + { + measEntry.metaDataMap["satelliteErrorCount"] = &satNav.satelliteErrorCount; + measEntry.metaDataMap["satelliteErrorEpochs"] = + &satNav.satelliteErrorEpochs; + measEntry.metaDataMap["receiverErrorCount"] = &rec.receiverErrorCount; + measEntry.metaDataMap["receiverErrorEpochs"] = &rec.receiverErrorEpochs; + measEntry.metaDataMap["lastIonTime"] = &satStat.lastIonTime; + } + + if (measType == PHAS) + { + measEntry.metaDataMap["phaseRejectCount"] = &sigStat.phaseRejectCount; + tracepdeex( + 2, + trace, + "\n PPP Phase count: %s %s %s %d", + rec.id.c_str(), + obs.Sat.id().c_str(), + enum_to_string(sig.code), + sigStat.phaseRejectCount + ); + } + + measEntry.obsKey.Sat = obs.Sat; + measEntry.obsKey.str = rec.id; + measEntry.obsKey.num = static_cast(sig.code); + if (measType == CODE) + { + measEntry.obsKey.type = KF::CODE_MEAS; + measEntry.obsKey.comment = "P-" + std::string(enum_to_string(sig.code)); + } + else if (measType == PHAS) + { + measEntry.obsKey.type = KF::PHAS_MEAS; + measEntry.obsKey.comment = "L-" + std::string(enum_to_string(sig.code)); + } + + // Start with the observed measurement and its noise + { + KFKey obsKey; + obsKey.str = rec.id; + obsKey.Sat = obs.Sat; + obsKey.type = measEntry.obsKey.type; + obsKey.num = static_cast(sig.code); + + double var = 0; + if (measType == CODE) + { + var = sig.codeVar; + } + else if (measType == PHAS) + { + var = sig.phasVar; + } + + measEntry.addNoiseEntry(obsKey, 1, var); + + measEntry.componentsMap[E_Component::OBSERVED] = {-observed, "- Phi", var}; + } + + // Calculate the basic range + + VectorEcef rRec = rec.aprioriPos; + VectorEci rRecInertial = frameSwapper(rRec); + + vector> delayedInits; + + Vector3d recPosVars = -1 * Vector3d::Ones(); + Vector3d recOrbitVars = -1 * Vector3d::Ones(); + Vector3d satOrbitVars = -1 * Vector3d::Ones(); + + // get keys early to declutter other code + KFKey recPosKey; + recPosKey.type = KF::REC_POS; + recPosKey.str = rec.id; + recPosKey.rec_ptr = &rec; + + KFKey recVelKey; + recVelKey.type = KF::REC_POS_RATE; + recVelKey.str = rec.id; + + KFKey recOrbitPosKey; + recOrbitPosKey.type = KF::ORBIT; + if (recSat.prn) + recOrbitPosKey.Sat = recSat; + else + recOrbitPosKey.str = recSatId; + + KFKey satOrbitPosKey; + satOrbitPosKey.type = KF::ORBIT; + satOrbitPosKey.Sat = obs.Sat; + + KFKey satOrbitVelKey = satOrbitPosKey; + + KFKey recOrbitVelKey = recOrbitPosKey; + + // Linear receiver positions + for (int i = 0; i < 3; i++) + { + InitialState posInit = initialStateFromConfig(recOpts.pos, i); + InitialState velInit = initialStateFromConfig(recOpts.pos_rate, i); + + recPosKey.num = i; + recPosKey.comment = posInit.comment; + + E_Source found = kfState.getKFValue(recPosKey, rRec[i], &recPosVars[i]); + + if (posInit.estimate == false) + { + continue; + } + + if (found == E_Source::REMOTE && posInit.use_remote_sigma) + { + posInit.P = recPosVars[i]; + } + + if (posInit.x == 0) + posInit.x = rRec[i]; + + recPosVars[i] = -1; + + delayedInits.push_back( + [recPosKey, posInit, i, &measEntry]( + Vector3d satStat_e, + Vector3d eSatInertial + ) { measEntry.addDsgnEntry(recPosKey, -satStat_e[i], posInit); } + ); + + if (velInit.estimate == false) + { + continue; + } + + recVelKey.num = i; + recVelKey.comment = velInit.comment; + + delayedInits.push_back( + [recPosKey, recVelKey, velInit, &kfState]( + Vector3d satStat_e, + Vector3d eSatInertial + ) { kfState.setKFTransRate(recPosKey, recVelKey, 1, velInit); } + ); + } + + // Orbital receiver positions + bool orbitalReceiver = false; + for (int i = 0; i < 3; i++) + { + InitialState posInit = initialStateFromConfig(recOpts.orbit, i); + InitialState velInit = initialStateFromConfig(recOpts.orbit, i + 3); + + recOrbitPosKey.num = i; + recOrbitPosKey.comment = posInit.comment; + + recOrbitVelKey.num = i + 3; + recOrbitVelKey.comment = velInit.comment; + + E_Source found = + kfState.getKFValue(recOrbitPosKey, rRecInertial[i], &recOrbitVars[i]); + + if (found != E_Source::NONE) + { + orbitalReceiver = true; + } + + if (posInit.estimate == false) + { + continue; + } + + if (posInit.x == 0) + posInit.x = rRecInertial[i]; + + if (found == E_Source::REMOTE && posInit.use_remote_sigma) + { + posInit.P = recOrbitVars[i]; + } + + recOrbitVars[i] = -1; + + delayedInits.push_back( + [recOrbitPosKey, posInit, i, &measEntry]( + Vector3d satStat_e, + Vector3d eSatInertial + ) { measEntry.addDsgnEntry(recOrbitPosKey, -eSatInertial[i], posInit); } + ); + + kfState.addKFState(recOrbitVelKey, velInit); + } + + if (orbitalReceiver) + { + rRec = frameSwapper(rRecInertial); + } + + auto& pos = rec.pos; + + pos = ecef2pos(rRec); + + VectorEcef& rSat = obs.rSatCom; + + if (rSat.isZero()) + { + BOOST_LOG_TRIVIAL(error) + << "Sat pos is unexpectedly zero for " << rec.id << " - " << Sat.id(); + continue; + } + + if (rRec.isZero()) + { + BOOST_LOG_TRIVIAL(error) << "Rec pos is unexpectedly zero for " << rec.id; + continue; + } + + if (initialStateFromConfig(satOpts.orbit).estimate && obs.rSatEci0.isZero()) + { + // Don't obliterate obs.rSat in satpos below, we still need the old one for + // next signal/phase + SatPos satPos0 = obs; + + // Use this to avoid adding the dt component of position + satpos( + trace, + tsync, + tsync, + satPos0, + satOpts.posModel.sources, + E_OffsetType::COM, + nav, + &kfState, + &remoteKF + ); + + obs.rSatEci0 = + frameSwapper(satPos0.rSatCom, &satPos0.satVel, &obs.vSatEci0); + } + + // Orbital satellite positions. This is mainly for setting up estimation, the + // positions estimated by this are already calculated elsewhere + for (int i = 0; i < 3; i++) + { + InitialState posInit = initialStateFromConfig(satOpts.orbit, i); + InitialState velInit = initialStateFromConfig(satOpts.orbit, i + 3); + + satOrbitPosKey.num = i; + satOrbitPosKey.comment = posInit.comment; + + satOrbitVelKey.num = i + 3; + satOrbitVelKey.comment = velInit.comment; + + double dummyPos = 0; + E_Source found = + kfState.getKFValue(satOrbitPosKey, dummyPos, &satOrbitVars[i]); + + if (posInit.estimate == false) + { + continue; + } + + if (posInit.x == 0) + posInit.x = obs.rSatEci0[i]; + if (velInit.x == 0) + velInit.x = obs.vSatEci0[i]; + + if (found == E_Source::REMOTE && posInit.use_remote_sigma) + { + posInit.P = satOrbitVars[i]; + } + + satOrbitVars[i] = -1; + + delayedInits.push_back( + [satOrbitPosKey, satOrbitVelKey, posInit, velInit, i, &obs, &measEntry]( + Vector3d satStat_e, + Vector3d eSatInertial + ) + { + measEntry.addDsgnEntry(satOrbitPosKey, +eSatInertial[i], posInit); + measEntry.addDsgnEntry( + satOrbitVelKey, + -eSatInertial[i] * (obs.tof + obs.satClk), + velInit + ); + } + ); + } + + if (initialStateFromConfig(satOpts.orbit).estimate) + addEmpStates(satOpts, kfState, Sat); + if (initialStateFromConfig(recOpts.orbit).estimate) + addEmpStates(recOpts, kfState, recSatId); + +#ifdef _ESTIMATE_CRCD + if (initialStateFromConfig(recOpts.cr).estimate) + { + InitialState CrInit = initialStateFromConfig(recOpts.cr, 0); + = 0; + kfState.addKFState({KF::CR, rec.id.c_str(), 0}, CrInit); + } + + if (initialStateFromConfig(satOpts.cr).estimate) + { + InitialState CrInit = initialStateFromConfig(satOpts.cr, 0); + kfState.addKFState({KF::CR, Sat, 0}, CrInit); + } + + if (initialStateFromConfig(recOpts.cd).estimate) + { + InitialState CdInit = initialStateFromConfig(recOpts.cd, 0); + kfState.addKFState({KF::CR, rec.id.c_str(), 0}, CdInit); + } + if (initialStateFromConfig(satOpts.cd).estimate) + { + InitialState CdInit = initialStateFromConfig(satOpts.cd, 0); + kfState.addKFState({KF::CD, Sat, 0}, CdInit); + } +#endif + if (initialStateFromConfig(recOpts.orientation).estimate) + { + Matrix3d body2Ecef = rotBasisMat( + rec.attStatus.eXBody, + rec.attStatus.eYBody, + rec.attStatus.eZBody + ); + + Quaterniond quat = Quaterniond(body2Ecef); + + Quaterniond roter(Eigen::AngleAxis(90 * PI / 180, Vector3d::UnitZ())); + + Quaterniond extra2 = quat * roter; + + for (int i = 0; i < 4; i++) + { + InitialState init = initialStateFromConfig(recOpts.orientation, i); + + KFKey kfKey; + kfKey.type = KF::ORIENTATION; + kfKey.str = rec.id; + kfKey.num = i; + + if (i == 0) + { + init.x = extra2.w(); + kfState.getKFValue(kfKey, init.x); + quat.w() = init.x; + } + if (i == 1) + { + init.x = extra2.x(); + kfState.getKFValue(kfKey, init.x); + quat.x() = init.x; + } + if (i == 2) + { + init.x = extra2.y(); + kfState.getKFValue(kfKey, init.x); + quat.y() = init.x; + } + if (i == 3) + { + init.x = extra2.z(); + kfState.getKFValue(kfKey, init.x); + quat.z() = init.x; + } + + kfState.addKFState(kfKey, init); + } + } + + // Range and geometry + + double rRecSat = (rSat - rRec).norm(); + satStat.e = (rSat - rRec).normalized(); + satStat.nadir = acos(satStat.e.dot(rSat.normalized())); + + VectorEci eSatInertial = frameSwapper(satStat.e); + + satazel(pos, satStat.e, satStat); + + // continue the exclusion from above now that some elevation is guaranteed + if (satStat.el < recOpts.elevation_mask_deg * D2R) + { + obs.excludeElevation = true; + } + + auto& ast = autoSenderTemplate; + + if (acsConfig.exclude.elevation && obs.excludeElevation) + { + tracepdeex(5, trace, " - excludeElevation"); + ast.pushValueKVP(2, {"exclude", "elevation"}); + continue; + } + + // add initialisations for things waiting for an up-to-date satstat + for (auto& delayedInit : delayedInits) + { + delayedInit(satStat.e, eSatInertial); + } + + // add 3d noise for unestimated positions + for (int i = 0; i < 3; i++) + { + recPosKey.num = i; + recOrbitPosKey.num = i; + satOrbitPosKey.num = i; + + measEntry.addNoiseEntry(recPosKey, 1, SQR(satStat.e[i]) * recPosVars[i]); + measEntry.addNoiseEntry( + recOrbitPosKey, + 1, + SQR(eSatInertial[i]) * recOrbitVars[i] + ); + measEntry.addNoiseEntry( + satOrbitPosKey, + 1, + SQR(eSatInertial[i]) * satOrbitVars[i] + ); + } + + tracepdeex( + 3, + trace, + "\n%s satstat.e : %20.4f\t%20.4f\t%20.4f", + obs.Sat.id().c_str(), + satStat.e.x(), + satStat.e.y(), + satStat.e.z() + ); + tracepdeex( + 3, + trace, + "\n%s apriori : %20.4f\t%20.4f\t%20.4f", + obs.Sat.id().c_str(), + rec.aprioriPos.x(), + rec.aprioriPos.y(), + rec.aprioriPos.z() + ); + tracepdeex( + 3, + trace, + "\n%s rSatEcef : %20.4f\t%20.4f\t%20.4f", + obs.Sat.id().c_str(), + obs.rSatCom.x(), + obs.rSatCom.y(), + obs.rSatCom.z() + ); + tracepdeex( + 3, + trace, + "\n%s vSatEcef : %20.4f\t%20.4f\t%20.4f", + obs.Sat.id().c_str(), + obs.satVel.x(), + obs.satVel.y(), + obs.satVel.z() + ); + tracepdeex( + 4, + trace, + "\n%s rSatEciDt : %20.4f\t%20.4f\t%20.4f", + obs.Sat.id().c_str(), + obs.rSatEciDt.x(), + obs.rSatEciDt.y(), + obs.rSatEciDt.z() + ); + tracepdeex( + 4, + trace, + "\n%s rSatEci0 : %20.4f\t%20.4f\t%20.4f", + obs.Sat.id().c_str(), + obs.rSatEci0.x(), + obs.rSatEci0.y(), + obs.rSatEci0.z() + ); + + if (recOpts.range) + { + measEntry.componentsMap[E_Component::RANGE] = {rRecSat, "+ rho", 0}; + } + + if (acsConfig.output_residual_chain) + { + tracepdeex( + 0, + trace, + "\n----------------------------------------------------" + ); + tracepdeex( + 0, + trace, + "\nMeasurement for %s %s\n", + ((string)measEntry.obsKey).c_str(), + enum_to_string(sig.code) + ); + } + + // Add modelled adjustments and estimated parameter + { + if (recOpts.clockModel.enable) + { + pppRecClocks(COMMON_PPP_ARGS); + } + if (satOpts.clockModel.enable) + { + pppSatClocks(COMMON_PPP_ARGS); + } + if (recOpts.eccentricityModel.enable) + { + pppRecAntDelta(COMMON_PPP_ARGS); + } + // if (acsConfig.model.sat_ant_delta) { pppSatAntDelta + // (COMMON_PPP_ARGS); } + if (recOpts.pcoModel.enable) + { + pppRecPCO(COMMON_PPP_ARGS); + } + if (satOpts.pcoModel.enable) + { + pppSatPCO(COMMON_PPP_ARGS); + } + if (recOpts.pcvModel.enable) + { + pppRecPCV(COMMON_PPP_ARGS); + } + if (satOpts.pcvModel.enable) + { + pppSatPCV(COMMON_PPP_ARGS); + } + if (recOpts.tideModels.enable) + { + pppTides(COMMON_PPP_ARGS); + } + if (recOpts.relativity) + { + pppRelativity(COMMON_PPP_ARGS); + } + if (recOpts.relativity2) + { + pppRelativity2(COMMON_PPP_ARGS); + } + if (recOpts.sagnac) + { + pppSagnac(COMMON_PPP_ARGS); + } + if (recOpts.ionospheric_component) + { + pppIonStec(COMMON_PPP_ARGS); + } + if (recOpts.ionospheric_component2) + { + pppIonStec2(COMMON_PPP_ARGS); + } + if (recOpts.ionospheric_component3) + { + pppIonStec3(COMMON_PPP_ARGS); + } + if (recOpts.ionospheric_model) + { + pppIonModel(COMMON_PPP_ARGS); + } + if (recOpts.tropModel.enable) + { + pppTrop(COMMON_PPP_ARGS); + } + if (recOpts.eop) + { + pppEopAdjustment(COMMON_PPP_ARGS); + } + } + + if (measType == PHAS) + { + if (recOpts.phaseWindupModel.enable) + { + pppRecPhaseWindup(COMMON_PPP_ARGS); + } + if (satOpts.phaseWindupModel.enable) + { + pppSatPhaseWindup(COMMON_PPP_ARGS); + } + if (recOpts.integer_ambiguity) + { + pppIntegerAmbiguity(COMMON_PPP_ARGS); + } + if (recOpts.phaseBiasModel.enable) + { + pppRecPhasBias(COMMON_PPP_ARGS); + } + if (satOpts.phaseBiasModel.enable) + { + pppSatPhasBias(COMMON_PPP_ARGS); + } + } + + if (measType == CODE) + { + if (recOpts.codeBiasModel.enable) + { + pppRecCodeBias(COMMON_PPP_ARGS); + } + if (satOpts.codeBiasModel.enable) + { + pppSatCodeBias(COMMON_PPP_ARGS); + } + } + + addNilDesignStates(recOpts.gyro_bias, kfState, KF::GYRO_BIAS, 3, recSatId); + addNilDesignStates( + recOpts.accelerometer_bias, + kfState, + KF::ACCL_BIAS, + 3, + recSatId + ); + addNilDesignStates(recOpts.gyro_scale, kfState, KF::GYRO_SCALE, 3, recSatId); + addNilDesignStates( + recOpts.accelerometer_scale, + kfState, + KF::ACCL_SCALE, + 3, + recSatId + ); + addNilDesignStates(recOpts.imu_offset, kfState, KF::IMU_OFFSET, 3, recSatId); + + // Calculate residuals and form up the measurement + + measEntry.componentsMap[E_Component::NET_RESIDUAL] = {0, "", 0}; + + AutoSender autoSender = autoSenderTemplate; + autoSender.pushBaseKVP(0, {"data", "position"}); + + autoSender.pushValueKVP(1, {"rRec[0]", rRec[0]}); + autoSender.pushValueKVP(1, {"rRec[1]", rRec[1]}); + autoSender.pushValueKVP(1, {"rRec[2]", rRec[2]}); + autoSender.pushValueKVP(1, {"El", satStat.el}); + autoSender.pushValueKVP(1, {"Az", satStat.az}); + autoSender.pushValueKVP(1, {"Nadir", satStat.nadir}); + + double residual = netResidualAndChainOutputs(trace, obs, measEntry); + + measEntry.setInnov(residual); + + // measEntry.metaDataMap["explain"] = (void*) true; + + kfMeasEntryList.push_back(measEntry); + } + + trace << "\n" + << "\n"; } - diff --git a/src/cpp/pea/ppp_pseudoobs.cpp b/src/cpp/pea/ppp_pseudoobs.cpp index 33e9f2434..208e5a2a5 100644 --- a/src/cpp/pea/ppp_pseudoobs.cpp +++ b/src/cpp/pea/ppp_pseudoobs.cpp @@ -1,896 +1,1028 @@ - // #pragma GCC optimize ("O0") -#include #include - -#include "minimumConstraints.hpp" +#include #include "architectureDocs.hpp" -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "acsConfig.hpp" -#include "tropModels.hpp" -#include "ionoModel.hpp" -#include "constants.hpp" -#include "orbitProp.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "trace.hpp" -#include "sinex.hpp" - - -#define PIVOT_MEAS_VARIANCE SQR(1E-5) - - -Architecture Pseudo_Observations__() -{ - -} - +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/sinex.hpp" +#include "common/trace.hpp" +#include "iono/ionoModel.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/orbitProp.hpp" +#include "pea/minimumConstraints.hpp" +#include "trop/tropModels.hpp" + +constexpr double PIVOT_MEAS_VARIANCE = 1e-5 * 1e-5; + +Architecture Pseudo_Observations__() {} void initPseudoObs( - Trace& trace, ///< Trace to output to - KFState& kfState, ///< Kalman filter object containing the network state parameters - KFMeasEntryList& kfMeasEntryList) ///< List to append kf measurements to + Trace& trace, ///< Trace to output to + KFState& kfState, ///< Kalman filter object containing the network state parameters + KFMeasEntryList& kfMeasEntryList ///< List to append kf measurements to +) { - if ( kfMeasEntryList.empty() == false - ||epoch != 1) - { - return; - } - - for (auto& [Sat, satNav] : nav.satNavMap) - { - if (acsConfig.process_sys[Sat.sys] == false) - { - continue; - } - - auto& satOpts = acsConfig.getSatOpts(Sat); - - if (satOpts.exclude) - { - continue; - } - - bool newState = false; - - for (int i = 0; i < 3; i++) - { - InitialState posInit = initialStateFromConfig(satOpts.orbit, i); - InitialState velInit = initialStateFromConfig(satOpts.orbit, i + 3); - - if ( posInit.estimate == false - ||posInit.x == 0 - ||velInit.x == 0) - { - continue; - } - - KFKey satPosKey; - KFKey satVelKey; - - satPosKey.type = KF::ORBIT; - satPosKey.Sat = Sat; - satPosKey.num = i; - - satVelKey.type = KF::ORBIT; - satVelKey.Sat = Sat; - satVelKey.num = i + 3; - - newState |= kfState.addKFState(satPosKey, posInit); - newState |= kfState.addKFState(satVelKey, velInit); - } - - if (newState == false) - { - continue; - } - - addEmpStates(satOpts, kfState, Sat); - } + if (kfMeasEntryList.empty() == false || epoch != 1) + { + return; + } + + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (acsConfig.process_sys[Sat.sys] == false) + { + continue; + } + + auto& satOpts = acsConfig.getSatOpts(Sat); + + if (satOpts.exclude) + { + continue; + } + + bool newState = false; + + for (int i = 0; i < 3; i++) + { + InitialState posInit = initialStateFromConfig(satOpts.orbit, i); + InitialState velInit = initialStateFromConfig(satOpts.orbit, i + 3); + + if (posInit.estimate == false || posInit.x == 0 || velInit.x == 0) + { + continue; + } + + KFKey satPosKey; + KFKey satVelKey; + + satPosKey.type = KF::ORBIT; + satPosKey.Sat = Sat; + satPosKey.num = i; + + satVelKey.type = KF::ORBIT; + satVelKey.Sat = Sat; + satVelKey.num = i + 3; + + newState |= kfState.addKFState(satPosKey, posInit); + newState |= kfState.addKFState(satVelKey, velInit); + } + + if (newState == false) + { + continue; + } + + addEmpStates(satOpts, kfState, Sat); +#ifdef _ESTIMATE_CRCD + if (initialStateFromConfig(satOpts.cr).estimate) + { + InitialState CrInit = initialStateFromConfig(satOpts.cr, 0); + kfState.addKFState({KF::CR, Sat, 0}, CrInit); + } + if (initialStateFromConfig(satOpts.cd).estimate) + { + InitialState CdInit = initialStateFromConfig(satOpts.cd, 0); + kfState.addKFState({KF::CD, Sat, 0}, CdInit); + } +#endif + // addKFSatStates(satOpts.cr, kfState, KF::CR, obs.Sat, 3); + } } struct Pseudo { - GTime time; - KFKey kfKey; - double value = 0; - double sigma = 0; + GTime time; + KFKey kfKey; + double value = 0; + double sigma = 0; }; map> pseudoListMap; - -void readPseudosFromFile( - string& file) +void readPseudosFromFile(string& file) { - std::ifstream fileStream(file); - if (!fileStream) - { - return; - } - - std::ofstream output(file + "_read", std::fstream::app); - if (!output) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Error opening read file '" << file << "_read'\n"; - return; - } - + std::ifstream fileStream(file); + if (!fileStream) + { + return; + } + + std::ofstream output(file + "_read", std::fstream::app); + if (!output) + { + BOOST_LOG_TRIVIAL(warning) << "Error opening read file '" << file << "_read'\n"; + return; + } + + while (fileStream) + { + string line; + + getline(fileStream, line); + + output << line << "\n"; + + vector tokens; + boost::split(tokens, line, boost::is_any_of("\t")); + + if (tokens.size() < 8) + { + continue; + } + + for (auto& token : tokens) + { + boost::trim(token); + } + + Pseudo pseudo; + pseudo.kfKey.type = string_to_enum_nocase(tokens[2]); + pseudo.kfKey.str = tokens[3]; + pseudo.kfKey.Sat = SatSys(tokens[4].c_str()); + pseudo.kfKey.num = std::stoi(tokens[5].c_str()); + pseudo.value = std::stod(tokens[6].c_str()); + pseudo.sigma = std::stod(tokens[7].c_str()); + + vector timeTokens; + boost::split(timeTokens, tokens[1], boost::is_any_of(" -:")); + + GEpoch epoch; + epoch.year = std::stoi(timeTokens[0].c_str()); + epoch.month = std::stoi(timeTokens[1].c_str()); + epoch.day = std::stoi(timeTokens[2].c_str()); + epoch.hour = std::stoi(timeTokens[3].c_str()); + epoch.min = std::stoi(timeTokens[4].c_str()); + epoch.sec = std::stoi(timeTokens[5].c_str()); + + pseudo.time = epoch; + + // std::cout << "\n" << pseudo.time << " " << pseudo.kfKey; + + pseudoListMap[pseudo.time].push_back(pseudo); + } + + remove(file.c_str()); +} +void filterPseudoObs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList) +{ + for (auto it = pseudoListMap.begin(); it != pseudoListMap.end();) + { + auto& [time, pseudoList] = *it; - while (fileStream) - { - string line; + if (time > tsync) + { + it++; + continue; + } - getline(fileStream, line); + it = pseudoListMap.erase(it); - output << line << "\n"; + for (auto& pseudo : pseudoList) + { + double filterVal; - vector tokens; - boost::split(tokens, line, boost::is_any_of("\t")); + E_Source foundSrc = kfState.getKFValue(pseudo.kfKey, filterVal); + bool found = foundSrc != E_Source::NONE; - if (tokens.size() < 8) - { - continue; - } + if (found == false) + { + continue; + } + KFMeasEntry kfMeasEntry(&kfState); - for (auto& token : tokens) - { - boost::trim(token); - } + kfMeasEntry.obsKey = pseudo.kfKey; - Pseudo pseudo; - pseudo.kfKey.type = KF::_from_string_nocase( tokens[2].c_str()); - pseudo.kfKey.str = tokens[3]; - pseudo.kfKey.Sat = SatSys( tokens[4].c_str()); - pseudo.kfKey.num = std::stoi( tokens[5].c_str()); - pseudo.value = std::stod( tokens[6].c_str()); - pseudo.sigma = std::stod( tokens[7].c_str()); + kfMeasEntry.obsKey.comment = "Fitler PseudoObs"; - vector timeTokens; - boost::split(timeTokens, tokens[1], boost::is_any_of(" -:")); + kfMeasEntry.metaDataMap["pseudoObs"] = (void*)true; - GEpoch epoch; - epoch.year = std::stoi( timeTokens[0].c_str()); - epoch.month = std::stoi( timeTokens[1].c_str()); - epoch.day = std::stoi( timeTokens[2].c_str()); - epoch.hour = std::stoi( timeTokens[3].c_str()); - epoch.min = std::stoi( timeTokens[4].c_str()); - epoch.sec = std::stoi( timeTokens[5].c_str()); + kfMeasEntry.setInnov(pseudo.value - filterVal); - pseudo.time = epoch; + kfMeasEntry.addDsgnEntry(pseudo.kfKey, 1); - // std::cout << "\n" << pseudo.time << " " << pseudo.kfKey; + pseudo.kfKey.type = KF::FILTER_MEAS; - pseudoListMap[pseudo.time].push_back(pseudo); - } + kfMeasEntry.addNoiseEntry(pseudo.kfKey, 1, SQR(pseudo.sigma)); - remove(file.c_str()); + kfMeasEntryList.push_back(kfMeasEntry); + } + } } -void filterPseudoObs( - Trace& trace, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList) +void orbitPseudoObs( + Trace& trace, ///< Trace to output to + Receiver& rec, ///< Receiver to perform calculations for + KFState& kfState, ///< Kalman filter object containing the network state parameters + KFMeasEntryList& kfMeasEntryList ///< List to append kf measurements to +) { - for (auto it = pseudoListMap.begin(); it != pseudoListMap.end(); it = pseudoListMap.erase(it)) - { - auto& [time, pseudoList] = *it; + GTime time = rec.obsList.front()->time; - if (time > tsync) - { - continue; - } + ERPValues erpv = getErp(nav.erp, time); + + FrameSwapper frameSwapper(time, erpv); - for (auto& pseudo : pseudoList) - { - double filterVal; + for (auto& obs : only(rec.obsList)) + { + if (acsConfig.process_sys[obs.Sat.sys] == false) + { + continue; + } - bool found = kfState.getKFValue(pseudo.kfKey, filterVal); + auto& satOpts = acsConfig.getSatOpts(obs.Sat); - if (found == false) - { - continue; - } + if (satOpts.exclude) + { + continue; + } - KFMeasEntry kfMeasEntry(&kfState); + obs.satNav_ptr = &nav.satNavMap[obs.Sat]; + SatNav& satNav = *obs.satNav_ptr; - kfMeasEntry.obsKey = pseudo.kfKey; + SatPos satPos; + satPos.Sat = obs.Sat; - kfMeasEntry.obsKey.comment = "Fitler PseudoObs"; + satpos(trace, time, time, satPos, satOpts.posModel.sources, E_OffsetType::COM, nav); - kfMeasEntry.metaDataMap["pseudoObs"] = (void*) true; + satPos.rSatEci0 = frameSwapper(satPos.rSatCom, &satPos.satVel, &satPos.vSatEci0); - kfMeasEntry.setInnov(pseudo.value - filterVal); + VectorEcef rSatEcef; + VectorEcef vSatEcef; - kfMeasEntry.addDsgnEntry(pseudo.kfKey, 1); - - pseudo.kfKey.type = KF::FILTER_MEAS; - - kfMeasEntry.addNoiseEntry(pseudo.kfKey, 1, SQR(pseudo.sigma)); - - kfMeasEntryList.push_back(kfMeasEntry); - } - } + VectorEci rSatEci; + VectorEci vSatEci; + + Matrix3d eopPartialMatrixEci = Matrix3d::Zero(); + + if (acsConfig.eci_pseudoobs) + { + if (obs.vel.isZero()) + { + obs.vel = satPos.vSatEci0; + } + + // Get framed vectors because obs is undefined + rSatEci = obs.pos; + vSatEci = obs.vel; + + // No EOP Partials needed for ECI pseudo obs + } + else + { + if (obs.vel.isZero()) + { + obs.vel = satPos.satVel; + } + + // Get framed vectors because obs is undefined + rSatEcef = obs.pos; + vSatEcef = obs.vel; + + rSatEci = frameSwapper(rSatEcef, &vSatEcef, &vSatEci); + + if (acsConfig.pppOpts.eop.estimate[0]) + { + eopPartialMatrixEci = receiverEopPartials(satPos.rSatCom) * frameSwapper.i2t_mat; + } + } + + KFKey satPosKeys[3]; + KFKey satVelKeys[3]; + KFKey eopKeys[3]; + KFKey rateKeys[3]; + for (int i = 0; i < 3; i++) + { + satPosKeys[i].type = KF::ORBIT; + satPosKeys[i].Sat = obs.Sat; + satPosKeys[i].num = i; + + satVelKeys[i].type = KF::ORBIT; + satVelKeys[i].Sat = obs.Sat; + satVelKeys[i].num = i + 3; + + eopKeys[i].type = KF::EOP; + eopKeys[i].num = i; + eopKeys[i].comment = eopComments[i]; + + rateKeys[i].type = KF::EOP_RATE; + rateKeys[i].num = i; + rateKeys[i].comment = (string)eopComments[i] + "/day"; + } + + VectorEci statePosEci; + + for (int i = 0; i < 3; i++) + { + InitialState posInit = initialStateFromConfig(satOpts.orbit, i); + InitialState velInit = initialStateFromConfig(satOpts.orbit, i + 3); + + if (posInit.estimate == false) + { + continue; + } + + if (posInit.x == 0) + posInit.x = satPos.rSatEci0[i]; + if (velInit.x == 0) + velInit.x = satPos.vSatEci0[i]; + + statePosEci[i] = posInit.x; + + kfState.getKFValue(satPosKeys[i], statePosEci[i]); + + KFMeasEntry kfMeasEntry(&kfState); + kfMeasEntry.addDsgnEntry(satPosKeys[i], 1, posInit); + + kfState.addKFState(satVelKeys[i], velInit); + + for (int num = 0; num < 3; num++) + { + InitialState init = initialStateFromConfig(acsConfig.pppOpts.eop, num); + InitialState eopRateInit = initialStateFromConfig(acsConfig.pppOpts.eop_rates, num); + + if (init.estimate == false) + { + continue; + } + + if (init.x == 0) + switch (num) + { + case 0: + init.x = erpv.xp * R2MAS; + eopRateInit.x = +erpv.xpr * R2MAS; + break; + case 1: + init.x = erpv.yp * R2MAS; + eopRateInit.x = +erpv.ypr * R2MAS; + break; + case 2: + init.x = erpv.ut1Utc * S2MTS; + eopRateInit.x = -erpv.lod * S2MTS; + break; + } + + kfMeasEntry.addDsgnEntry(eopKeys[num], eopPartialMatrixEci(num, i), init); + + if (eopRateInit.estimate == false) + { + continue; + } + + kfState.setKFTransRate(eopKeys[num], rateKeys[num], 1 / S_IN_DAY, eopRateInit); + } + + double omc = rSatEci[i] - statePosEci[i]; + + kfMeasEntry.setInnov(omc); + + kfMeasEntry.obsKey.comment = "ECI PseudoPos"; + kfMeasEntry.obsKey.type = KF::ORBIT_MEAS; + kfMeasEntry.obsKey.Sat = obs.Sat; + kfMeasEntry.obsKey.num = i; + kfMeasEntry.metaDataMap["pseudoObs"] = (void*)true; + + kfMeasEntry.metaDataMap["satelliteErrorCount"] = &satNav.satelliteErrorCount; + kfMeasEntry.metaDataMap["satelliteErrorEpochs"] = &satNav.satelliteErrorEpochs; + + kfMeasEntry.addNoiseEntry(kfMeasEntry.obsKey, 1, SQR(satOpts.pseudo_sigma)); + + kfMeasEntryList.push_back(kfMeasEntry); + } + + addEmpStates(satOpts, kfState, obs.Sat); +#ifdef _ESTIMATE_CRCD + if (initialStateFromConfig(satOpts.cr).estimate) + { + InitialState CrInit = initialStateFromConfig(satOpts.cr, 0); + kfState.addKFState({KF::CR, obs.Sat, 0}, CrInit); + } + if (initialStateFromConfig(satOpts.cd).estimate) + { + InitialState CdInit = initialStateFromConfig(satOpts.cd, 0); + kfState.addKFState({KF::CD, obs.Sat, 0}, CdInit); + } +#endif + } } -void orbitPseudoObs( - Trace& trace, ///< Trace to output to - Receiver& rec, ///< Receiver to perform calculations for - const KFState& kfState, ///< Kalman filter object containing the network state parameters - KFMeasEntryList& kfMeasEntryList) ///< List to append kf measurements to +void pseudoRecDcb( + Trace& trace, ///< Trace to output to + KFState& kfState, ///< Kalman filter object containing the network state parameters + KFMeasEntryList& kfMeasEntryList ///< List to append kf measurements to +) { - GTime time = rec.obsList.front()->time; - - ERPValues erpv = getErp(nav.erp, time); - - FrameSwapper frameSwapper(time, erpv); - - for (auto& obs : only(rec.obsList)) - { - if (acsConfig.process_sys[obs.Sat.sys] == false) - { - continue; - } - - auto& satOpts = acsConfig.getSatOpts(obs.Sat); - - if (satOpts.exclude) - { - continue; - } - - VectorEci rSatEci; - VectorEci vSatEci; - - SatPos satPos; - - Matrix3d eopPartialMatrixEci = Matrix3d::Zero(); - - if (acsConfig.eci_pseudoobs) - { - rSatEci = obs.pos; - vSatEci = obs.vel; - } - else - { - satPos.Sat = obs.Sat; - - satpos(trace, time, time, satPos, satOpts.posModel.sources, E_OffsetType::COM, nav); - - if (obs.vel.isZero()) - { - obs.vel = satPos.satVel; - } - - //Get framed vectors because obs is undefined - VectorEcef rSat = obs.pos; - VectorEcef vSat = obs.vel; - - rSatEci = frameSwapper(rSat, &vSat, &vSatEci); - satPos.rSatEci0 = frameSwapper(satPos.rSatCom, &satPos.satVel, &satPos.vSatEci0); - - if (acsConfig.pppOpts.eop.estimate[0]) - { - eopPartialMatrixEci = stationEopPartials(rSat) * frameSwapper.i2t_mat; - } - } - - KFKey satPosKeys[3]; - KFKey satVelKeys[3]; - KFKey eopKeys [3]; - KFKey rateKeys [3]; - for (int i = 0; i < 3; i++) - { - satPosKeys[i].type = KF::ORBIT; - satPosKeys[i].Sat = obs.Sat; - satPosKeys[i].num = i; - - satVelKeys[i].type = KF::ORBIT; - satVelKeys[i].Sat = obs.Sat; - satVelKeys[i].num = i + 3; - - eopKeys[i].type = KF::EOP; - eopKeys[i].num = i; - eopKeys[i].comment = eopComments[i]; - - rateKeys[i].type = KF::EOP_RATE; - rateKeys[i].num = i; - rateKeys[i].comment = (string) eopComments[i] + "/day"; - } - - for (int i = 0; i < 3; i++) - { - InitialState posInit = initialStateFromConfig(satOpts.orbit, i); - InitialState velInit = initialStateFromConfig(satOpts.orbit, i + 3); - if (posInit.estimate) - { - VectorEci statePosEci = rSatEci; - - KFMeasEntry kfMeasEntry(&kfState); - - kfState.getKFValue(satPosKeys[i], statePosEci[i]); - - if (posInit.x == 0) posInit.x = satPos.rSatEci0[i]; - if (velInit.x == 0) velInit.x = satPos.vSatEci0[i]; - - bool newState = false; - newState |= kfState.addKFState(satPosKeys[i], posInit); - newState |= kfState.addKFState(satVelKeys[i], velInit); - - if (newState) - { - statePosEci[i] = posInit.x; - } - - kfMeasEntry.addDsgnEntry(satPosKeys[i], 1, posInit); - - for (int num = 0; num < 3; num++) - { - InitialState init = initialStateFromConfig(acsConfig.pppOpts.eop, num); - InitialState eopRateInit = initialStateFromConfig(acsConfig.pppOpts.eop_rates, num); - - if (init.estimate == false) - { - continue; - } - - if (init.x == 0) - switch (num) - { - case 0: init.x = erpv.xp * R2MAS; eopRateInit.x = +erpv.xpr * R2MAS; break; - case 1: init.x = erpv.yp * R2MAS; eopRateInit.x = +erpv.ypr * R2MAS; break; - case 2: init.x = erpv.ut1Utc * S2MTS; eopRateInit.x = -erpv.lod * S2MTS; break; - } - - kfMeasEntry.addDsgnEntry(eopKeys[num], eopPartialMatrixEci(num, i), init); - - if (eopRateInit.estimate == false) - { - continue; - } - - kfState.setKFTransRate(eopKeys[num], rateKeys[num], 1/S_IN_DAY, eopRateInit); - } - - double omc = rSatEci[i] - - statePosEci[i]; - - kfMeasEntry.setInnov(omc); - - kfMeasEntry.obsKey.comment = "ECI PseudoPos"; - kfMeasEntry.obsKey.type = KF::ORBIT_MEAS; - kfMeasEntry.obsKey.Sat = obs.Sat; - kfMeasEntry.obsKey.num = i; - kfMeasEntry.metaDataMap["pseudoObs"] = (void*) true; - - kfMeasEntry.addNoiseEntry(kfMeasEntry.obsKey, 1, SQR(satOpts.pseudo_sigma)); - - kfMeasEntryList.push_back(kfMeasEntry); - } - } - - addEmpStates(satOpts, kfState, obs.Sat); - } + string doneRec; + SatSys doneSys; + + static map setSatMap; + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::CODE_BIAS || key.str.empty()) + { + // only do receiver code biases + continue; + } + + SatSys sysSat(key.Sat.sys); + + // there are code biases for this receiver+system, check the dcbs all at once at the first + // one + if (key.str == doneRec && sysSat == doneSys) + { + continue; + } + + auto sys = sysSat.sys; + + KFKey sysKey = key; + sysKey.Sat = SatSys(sys); + sysKey.num = 0; + + auto& setSat = setSatMap[sysKey]; + if (setSat.sys != E_Sys::NONE && setSat != key.Sat) + { + // have used a different key before, only use that one again. + continue; + } + + doneRec = key.str; + doneSys = sysSat; + + auto& recSysOpts = acsConfig.getRecOpts(key.str, {enum_to_string(sys)}); + + if (recSysOpts.zero_dcb_codes.size() != 2) + { + continue; + } + + auto& firstCode = recSysOpts.zero_dcb_codes[0]; + auto& secondCode = recSysOpts.zero_dcb_codes[1]; + + list codeBiasKeys; + + auto it = kfState.kfIndexMap.find(key); + + KFKey testKey; + + while (it != kfState.kfIndexMap.end() && (testKey = it->first, true) && + testKey.type == KF::CODE_BIAS && testKey.str == key.str && testKey.Sat == key.Sat) + { + codeBiasKeys.push_back(testKey); + ++it; + if (it == kfState.kfIndexMap.end()) + { + break; + } + } + + if (firstCode == E_ObsCode::AUTO || secondCode == E_ObsCode::AUTO) + { + // get all available codes in priority order to resolve the autos + + codeBiasKeys.sort( + [sys](KFKey& a, KFKey& b) + { + auto& code_priorities = acsConfig.code_priorities[sys]; + + auto iterA = std::find( + code_priorities.begin(), + code_priorities.end(), + int_to_enum(a.num) + ); + auto iterB = std::find( + code_priorities.begin(), + code_priorities.end(), + int_to_enum(b.num) + ); + + if (iterA < iterB) + return true; + else + return false; + } + ); + + for (auto& code_ptr : {&firstCode, &secondCode}) + { + auto& code = *code_ptr; + + if (code != E_ObsCode::AUTO) + { + continue; + } + + // need to replace this code with the first one found in the sorted code_priorities + + try + { + for (auto& codeBiasKey : codeBiasKeys) + { + E_ObsCode keyCode = int_to_enum(codeBiasKey.num); + + if (code2Freq[sys][keyCode] == code2Freq[sys][firstCode]) + { + // would duplicate frequency, skip. (also works for auto in firstCode) + continue; + } + + code = keyCode; + + BOOST_LOG_TRIVIAL(debug) << "Setting zero_dcb_code for " << key.str << " " + << enum_to_string(sys) << " to " << code; + + break; + } + } + catch (...) + { + continue; + } + } + } + + KFKey key1 = key; + key1.num = static_cast(firstCode); + KFKey key2 = key; + key2.num = static_cast(secondCode); + + if (std::find(codeBiasKeys.begin(), codeBiasKeys.end(), key1) == codeBiasKeys.end() || + std::find(codeBiasKeys.begin(), codeBiasKeys.end(), key2) == codeBiasKeys.end()) + { + // both biases not found + continue; + } + + KFMeasEntry measEntry(&kfState); + measEntry.obsKey.type = KF::CODE_BIAS; + measEntry.obsKey.Sat = sysSat; + measEntry.obsKey.str = key.str; + measEntry.obsKey.comment = "Zero DCB"; + + measEntry.metaDataMap["pseudoObs"] = (void*)true; + measEntry.metaDataMap["explain"] = (void*)true; + + InitialState init1; + InitialState init2; + + kfState.getKFValue(key1, init1.x); + kfState.getKFValue(key2, init2.x); + + double bias1 = init1.x; + double bias2 = init2.x; + + measEntry.addDsgnEntry(key1, +1, init1); + measEntry.addDsgnEntry(key2, -1, init2); + + measEntry.setInnov(bias2 - bias1); + + measEntry.addNoiseEntry(measEntry.obsKey, 1, PIVOT_MEAS_VARIANCE); + + setSat = key1.Sat; + + kfMeasEntryList.push_back(measEntry); + } } -void pseudoRecDcb( - Trace& trace, ///< Trace to output to - KFState& kfState, ///< Kalman filter object containing the network state parameters - KFMeasEntryList& kfMeasEntryList) ///< List to append kf measurements to +void receiverPseudoObs( + Trace& trace, ///< Trace to output to + Receiver& rec, ///< (Pseudo) Receiver to perform calculations for + KFState& kfState, ///< Kalman filter object containing the network state parameters + KFMeasEntryList& kfMeasEntryList, ///< List to append kf measurements to + ReceiverMap& receiverMap ///< Map of stations to retrieve receiver metadata from +) { - string doneRec; - SatSys doneSat; - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::CODE_BIAS) - { - continue; - } + GTime time = rec.obsList.front()->time; - if (key.rec_ptr == nullptr) - { - continue; - } + vector indices; - auto& rec = *key.rec_ptr; - - //there are code biases for this receiver+system, check the dcbs all at once at the first one - if ( key.str == doneRec - &&key.Sat == doneSat) - { - continue; - } - - auto sys = key.Sat.sys; + for (auto& obs : only(rec.obsList)) + { + for (auto& [key, index] : obs.obsState.kfIndexMap) + { + if (key.type != KF::REC_POS || key.num != 0) + { + continue; + } - doneRec = key.str; - doneSat = key.Sat; + if (key.rec_ptr == nullptr) + { + continue; + } - auto& recSysOpts = acsConfig.getRecOpts(rec.id, {sys._to_string()}); + auto& rec = *key.rec_ptr; + // try to get apriori from the existing state, otherwise use sinex. - if (recSysOpts.zero_dcb_codes.size() != 2) - { - continue; - } + Vector3d apriori = Vector3d::Zero(); - auto& firstCode = recSysOpts.zero_dcb_codes[0]; - auto& secondCode = recSysOpts.zero_dcb_codes[1]; + bool found = true; + for (int i = 0; i < 3; i++) + { + KFKey posKey = key; + posKey.num = i; - list codeBiasKeys; + { + E_Source src = kfState.getKFValue(posKey, apriori(i)); + found &= (src != E_Source::NONE); + } + } - auto it = kfState.kfIndexMap.find(key); + // make sure this receiver is initialised since this might be the first time anyone has + // seen it - KFKey testKey; - while ( it != kfState.kfIndexMap.end() - && (testKey = it->first, true) - && testKey.type == +KF::CODE_BIAS - && testKey.str == rec.id) - { - codeBiasKeys.push_back(testKey); - ++it; - if (it == kfState.kfIndexMap.end()) - { - break; - } - } + // if (found == false) + { + rec.id = key.str; + getRecSnx(rec.id, obs.time, rec.snx); + apriori = rec.snx.pos; + } - if ( firstCode == +E_ObsCode::AUTO - ||secondCode == +E_ObsCode::AUTO) - { - //get all available codes in priority order to resolve the autos + rec.minconApriori = apriori; + } + obs.obsState.time = time; - codeBiasKeys.sort([sys](KFKey& a, KFKey& b) - { - auto& code_priorities = acsConfig.code_priorities[sys]; + mincon(trace, obs.obsState); - auto iterA = std::find(code_priorities.begin(), code_priorities.end(), E_ObsCode::_from_integral(a.num)); - auto iterB = std::find(code_priorities.begin(), code_priorities.end(), E_ObsCode::_from_integral(b.num)); + for (auto& [key, index] : obs.obsState.kfIndexMap) + { + auto& recOpts = acsConfig.getRecOpts(key.str); - if (iterA < iterB) return true; - else return false; - }); + if (key.type != KF::REC_POS) + { + continue; + } - for (auto& code_ptr : {&firstCode, &secondCode}) - { - auto& code = *code_ptr; + KFKey kfKey = key; - if (code != +E_ObsCode::AUTO) - { - continue; - } + auto& rec = receiverMap[key.str]; + kfKey.rec_ptr = &rec; - //need to replace this code with the first one found in the sorted code_priorities + InitialState posInit = initialStateFromConfig(recOpts.pos, kfKey.num); + if (posInit.estimate == false) + { + continue; + } - for (auto& codeBiasKey : codeBiasKeys) - { - E_ObsCode keyCode = E_ObsCode::_from_integral(codeBiasKey.num); + double obsX = obs.obsState.x[index]; - if (code2Freq[sys][keyCode] == code2Freq[sys][firstCode]) - { - //would duplicate frequency, skip. (also works for auto in firstCode) - continue; - } + KFMeasEntry kfMeasEntry(&kfState, kfKey); - code = keyCode; + double stateX = obsX; + kfState.getKFValue(kfKey, stateX); - BOOST_LOG_TRIVIAL(debug) << "Setting zero_dcb_code for " << key.str << " " << sys._to_string() << " to " << code; + posInit.x = obsX; - break; - } - } - } + kfMeasEntry.addDsgnEntry(kfKey, 1, posInit); - KFKey key1 = key; key1.num = firstCode; - KFKey key2 = key; key2.num = secondCode; + InitialState velInit = initialStateFromConfig(recOpts.strain_rate, kfKey.num); + if (velInit.estimate) + { + KFKey velKey = kfKey; + velKey.type = KF::STRAIN_RATE; + velKey.comment = "mm/year"; - if ( std::find(codeBiasKeys.begin(), codeBiasKeys.end(), key1) == codeBiasKeys.end() - ||std::find(codeBiasKeys.begin(), codeBiasKeys.end(), key2) == codeBiasKeys.end()) - { - //both biases not found - continue; - } + kfState.setKFTransRate(kfKey, velKey, 1 / (365.25 * 24 * 60 * 60 * 1e3), velInit); + } - KFMeasEntry measEntry(&kfState); - measEntry.obsKey.type = KF::CODE_BIAS; - measEntry.obsKey.Sat = key.Sat; - measEntry.obsKey.str = key.str; - measEntry.obsKey.comment = "Zero DCB"; + double omc = obsX - stateX; - measEntry.metaDataMap["pseudoObs"] = (void*) true; - measEntry.metaDataMap["explain"] = (void*) true; + kfMeasEntry.setInnov(omc); + kfMeasEntry.setNoise(obs.obsState.P(index, index)); - InitialState init1; - InitialState init2; + indices.push_back(index); - kfState.getKFValue(key1, init1.x); - kfState.getKFValue(key2, init2.x); - - double bias1 = init1.x; - double bias2 = init2.x; - - measEntry.addDsgnEntry(key1, +1, init1); - measEntry.addDsgnEntry(key2, -1, init2); - - measEntry.setInnov(bias2 - bias1); - - measEntry.addNoiseEntry(measEntry.obsKey, 1, PIVOT_MEAS_VARIANCE); - - kfMeasEntryList.push_back(measEntry); - } + kfMeasEntryList.push_back(kfMeasEntry); + } + } } -void receiverPseudoObs( - Trace& trace, ///< Trace to output to - Receiver& rec, ///< (Pseudo) Receiver to perform calculations for - const KFState& kfState, ///< Kalman filter object containing the network state parameters - KFMeasEntryList& kfMeasEntryList, ///< List to append kf measurements to - ReceiverMap& receiverMap) ///< Map of stations to retrieve receiver metadata from +/** Add pseudo-observations to set one satellite's phase biases variances to zero. + * This shouldnt occur in the parallel section because multiple receivers may be trying to set + * different satellites to 0 if they see different satellites. + */ +void phasePseudoObs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList) { - GTime time = rec.obsList.front()->time; - - vector indices; - - for (auto& obs : only(rec.obsList)) - { - for (auto& [key, index] : obs.obsState.kfIndexMap) - { - if ( key.type != KF::REC_POS - ||key.num != 0) - { - continue; - } - - if (key.rec_ptr == nullptr) - { - continue; - } + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::PHASE_BIAS) + { + continue; + } - auto& rec = *key.rec_ptr; - //try to get apriori from the existing state, otherwise use sinex. + string& constrain_phase_bias = acsConfig.constrain_phase_bias[key.Sat.sys]; - Vector3d apriori = Vector3d::Zero(); + if (constrain_phase_bias.empty()) + { + continue; + } - bool found = true; - for (int i = 0; i < 3; i++) - { - KFKey posKey = key; - posKey.num = i; + if (constrain_phase_bias == "") + { + constrain_phase_bias = key.Sat.id(); + } - found &= kfState.getKFValue(posKey, apriori(i)); - } + if (key.Sat.id() != constrain_phase_bias) + { + continue; + } - //make sure this receiver is initialised since this might be the first time anyone has seen it + double dummy; + double var; + kfState.getKFValue(key, dummy, &var); -// if (found == false) - { - rec.id = key.str; - getRecSnx(rec.id, obs.time, rec.snx); - apriori = rec.snx.pos; - } + if (var < 2 * PIVOT_MEAS_VARIANCE) + { + continue; + } - rec.minconApriori = apriori; - } + KFMeasEntry kfMeasEntry(&kfState, key); - obs.obsState.time = time; + kfMeasEntry.obsKey.comment = "Phase bias constraint"; - mincon(trace, obs.obsState); + kfMeasEntry.addDsgnEntry(key, +1); + kfMeasEntry.addNoiseEntry(key, +1, PIVOT_MEAS_VARIANCE); - for (auto& [key, index] : obs.obsState.kfIndexMap) - { - auto& recOpts = acsConfig.getRecOpts(key.str); - - if (key.type != KF::REC_POS) - { - continue; - } - - KFKey kfKey = key; - - auto& rec = receiverMap[key.str]; - kfKey.rec_ptr = &rec; - - InitialState posInit = initialStateFromConfig(recOpts.pos, kfKey.num); - if (posInit.estimate == false) - { - continue; - } - - double obsX = obs.obsState.x[index]; - - KFMeasEntry kfMeasEntry(&kfState, kfKey); - - double stateX = obsX; - kfState.getKFValue(kfKey, stateX); - - posInit.x = obsX; - - kfMeasEntry.addDsgnEntry(kfKey, 1, posInit); - - - InitialState velInit = initialStateFromConfig(recOpts.strain_rate, kfKey.num); - if (velInit.estimate) - { - KFKey velKey = kfKey; - velKey.type = KF::STRAIN_RATE; - velKey.comment = "mm/year"; - - kfState.setKFTransRate(kfKey, velKey, 1/(365.25*24*60*60*1e3), velInit); - } - - double omc = obsX - - stateX; - - kfMeasEntry.setInnov(omc); - kfMeasEntry.setNoise(obs.obsState.P(index, index)); - - indices.push_back(index); - - kfMeasEntryList.push_back(kfMeasEntry); - } - } + kfMeasEntryList.push_back(kfMeasEntry); + } } -void ambgPseudoObs( - Trace& trace, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList) +void ambgPseudoObs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList) { - map>> recBound; - map> satBound; - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::AMBIGUITY) - continue; - - if (key.rec_ptr == nullptr) - continue; + static map>> bestForSysCode; - auto& rec = *key.rec_ptr; + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::AMBIGUITY) + { + continue; + } - auto& satStat = rec.satStatMap[key.Sat]; - auto& recOpts = acsConfig.getRecOpts(key.str); + if (acsConfig.constrain_best_ambiguity_integer[key.Sat.sys] == false) + { + continue; + } - if (satStat.el < (recOpts.elevation_mask_deg + 5) * D2R) //todo aaron wrong mask - continue; + double dummy; + double var = 0; - KFKey satKey = key; - satKey.type = KF::PHASE_BIAS; - satKey.str = ""; + kfState.getKFValue(key, dummy, &var); - KFKey recKey = key; - recKey.type = KF::PHASE_BIAS; - recKey.Sat = SatSys(key.Sat.sys); + auto& [bestKey, bestVar, done] = bestForSysCode[key.Sat.sys][key.num]; - double sbias = 0; - double svar = 0; - double rbias = 0; - double rvar = 0; + if (done) + { + continue; + } - kfState.getKFValue(satKey, sbias, &svar); - kfState.getKFValue(recKey, rbias, &rvar); + if (bestVar == 0 || var < bestVar) + { + // new best variance for this frequency + bestForSysCode[key.Sat.sys][key.num] = {key, var, false}; + } + } - bool apply = false; - if (acsConfig.network_amb_pivot[key.Sat.sys]) - { - bool recBind = ( rvar > acsConfig.fixed_phase_bias_var * 2 - && svar < acsConfig.fixed_phase_bias_var - && recBound[key.str][key.Sat.sys].find(key.num) == recBound[key.str][key.Sat.sys].end()); - if (recBind) - { - recBound[key.str][key.Sat.sys][key.num] = key.Sat; - apply = true; - } + for (auto& [sys, codeMap] : bestForSysCode) + for (auto& [code, best] : codeMap) + { + auto& [key, var, done] = best; - bool satBind = ( svar > acsConfig.fixed_phase_bias_var * 2 - && rvar < acsConfig.fixed_phase_bias_var - && satBound[key.Sat].find(key.num) == satBound[key.Sat].end()); - if (satBind) - { - satBound[key.Sat][key.num] = key.str; - apply = true; - } - } - else if (acsConfig.receiver_amb_pivot[key.Sat.sys]) - { - apply = ( rvar > acsConfig.fixed_phase_bias_var - && recBound[key.str][key.Sat.sys].find(key.num) == recBound[key.str][key.Sat.sys].end()); + if (done) + { + continue; + } - if (apply) - recBound[key.str][key.Sat.sys][key.num] = key.Sat; - } + if (var < 2 * PIVOT_MEAS_VARIANCE) + { + continue; + } - if (apply == false) - { - continue; - } + // this is the most constrained ambiguity of an unconstrained system, constrain it to an + // integer value - double floatAmb = 0; + KFMeasEntry measEntry(&kfState); + measEntry.obsKey = key; + measEntry.obsKey.comment = "Integer ambiguity constraint"; - kfState.getKFValue(key, floatAmb); + measEntry.addDsgnEntry(key, +1); - double fixedAmb = ROUND(floatAmb); + double floatAmb; + kfState.getKFValue(key, floatAmb); - tracepdeex(4, trace, "\nAmbiguity pseudo-obs %s", key); + double fixedAmb = round(floatAmb); - KFMeasEntry measEntry(&kfState); - measEntry.obsKey = key; - measEntry.obsKey.comment = "phase binding"; + measEntry.setInnov(fixedAmb - floatAmb); - measEntry.addDsgnEntry(key, +1); + measEntry.metaDataMap["pseudoObs"] = (void*)true; + // measEntry.metaDataMap["explain"] = (void*) true; - measEntry.setInnov(fixedAmb - floatAmb); + measEntry.addNoiseEntry(measEntry.obsKey, 1, PIVOT_MEAS_VARIANCE); - measEntry.metaDataMap["pseudoObs"] = (void*) true; -// measEntry.metaDataMap["explain"] = (void*) true; + kfMeasEntryList.push_back(measEntry); - measEntry.addNoiseEntry(measEntry.obsKey, 1, PIVOT_MEAS_VARIANCE); - - kfMeasEntryList.push_back(measEntry); - } + done = true; + } } -void ionoPseudoObs( //todo aaron, move to model section - Trace& pppTrace, - ReceiverMap& receiverMap, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList) +void ionoPseudoObs( // todo aaron, move to model section + Trace& pppTrace, + ReceiverMap& receiverMap, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList +) { - for (auto& [id, rec] : receiverMap) - for (auto& obs : only(rec.obsList)) - { - if (acsConfig.use_iono_corrections[obs.Sat.sys] == false) - continue; + for (auto& [id, rec] : receiverMap) + for (auto& obs : only(rec.obsList)) + { + if (acsConfig.use_iono_corrections[obs.Sat.sys] == false) + continue; - if (obs.satStat_ptr == nullptr) - { - continue; - } + if (obs.satStat_ptr == nullptr) + { + continue; + } - auto& satStat = *obs.satStat_ptr; + auto& satStat = *obs.satStat_ptr; - double extvar = 0; - double extion = getSSRIono(pppTrace, obs.time, rec.aprioriPos, satStat, extvar, obs.Sat); //todo aaron get from other sources too + double extvar = 0; + double extion = getSSRIono( + pppTrace, + obs.time, + rec.aprioriPos, + satStat, + extvar, + obs.Sat + ); // todo aaron get from other sources too - if (extvar <= 0) - continue; + if (extvar <= 0) + continue; - auto& recOpts = acsConfig.getRecOpts(rec.id); + auto& recOpts = acsConfig.getRecOpts(rec.id); - InitialState init = initialStateFromConfig(recOpts.ion_stec); + InitialState init = initialStateFromConfig(recOpts.ion_stec); - KFKey kfKey; - kfKey.type = KF::IONO_STEC; - kfKey.str = rec.id; - kfKey.Sat = obs.Sat; + KFKey kfKey; + kfKey.type = KF::IONO_STEC; + kfKey.str = rec.id; + kfKey.Sat = obs.Sat; - kfState.getKFValue(kfKey, init.x); + kfState.getKFValue(kfKey, init.x); - double kfion = init.x; + double kfion = init.x; - tracepdeex(2, pppTrace, " Checking Ionosphere pseudos: %s %s, %.4f, %.4f, %.2e\n", rec.id.c_str(), obs.Sat.id().c_str(), extion, kfion, extvar); + tracepdeex( + 2, + pppTrace, + " Checking Ionosphere pseudos: %s %s, %.4f, %.4f, %.2e\n", + rec.id.c_str(), + obs.Sat.id().c_str(), + extion, + kfion, + extvar + ); - KFMeasEntry measEntry(&kfState); - measEntry.obsKey.type = KF::IONOSPHERIC; - measEntry.obsKey.str = rec.id; - measEntry.obsKey.Sat = obs.Sat; + KFMeasEntry measEntry(&kfState); + measEntry.obsKey.type = KF::IONOSPHERIC; + measEntry.obsKey.str = rec.id; + measEntry.obsKey.Sat = obs.Sat; - measEntry.addDsgnEntry(kfKey, +1, init); + measEntry.addDsgnEntry(kfKey, +1, init); - measEntry.setInnov(extion - kfion); + measEntry.setInnov(extion - kfion); - measEntry.metaDataMap["pseudoObs"] = (void*) true; -// measEntry.metaDataMap["explain"] = (void*) true; + measEntry.metaDataMap["pseudoObs"] = (void*)true; + // measEntry.metaDataMap["explain"] = (void*) true; - measEntry.addNoiseEntry(measEntry.obsKey, 1, extvar); + measEntry.addNoiseEntry(measEntry.obsKey, 1, extvar); - kfMeasEntryList.push_back(measEntry); - } + kfMeasEntryList.push_back(measEntry); + } } void tropPseudoObs( - Trace& trace, - ReceiverMap& receiverMap, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList) + Trace& trace, + ReceiverMap& receiverMap, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList +) { - if (acsConfig.use_trop_corrections == false) - { - return; - } - - for (auto& [id, rec] : receiverMap) - { - auto& recOpts = acsConfig.getRecOpts(id); - - if (recOpts.exclude) - { - continue; - } - - double dryZTD; - double wetZTD; - double dryMap; - double wetMap; - double extVar; - double extZTD = tropCSSR(trace, kfState.time, rec.pos, PI/2, dryZTD, dryMap, wetZTD, wetMap, extVar); //todo aaron, take this from other places optionally - - if (extVar <= 0) - continue; - - KFKey kfKey; - kfKey.type = KF::TROP; - kfKey.str = rec.id; - - InitialState init = initialStateFromConfig(recOpts.trop); - - kfState.getKFValue(kfKey, init.x); - - double kftrop = init.x; - - tracepdeex(2, trace, " Checking troposphere pseudos: %s, %.4f + %.4f = %.4f, %.4f, %.2e\n", rec.id.c_str(), dryZTD, wetZTD, extZTD, kftrop, extVar); - - KFMeasEntry measEntry(&kfState); - measEntry.obsKey.type = KF::TROP; - measEntry.obsKey.str = rec.id; - - measEntry.addDsgnEntry(kfKey, +1, init); - - measEntry.setInnov(extZTD - kftrop); - - measEntry.metaDataMap["pseudoObs"] = (void*) true; - - measEntry.addNoiseEntry(measEntry.obsKey, 1, extVar); - - kfMeasEntryList.push_back(measEntry); - } + if (acsConfig.use_trop_corrections == false) + { + return; + } + + for (auto& [id, rec] : receiverMap) + { + auto& recOpts = acsConfig.getRecOpts(id); + + if (recOpts.exclude) + { + continue; + } + + double dryZTD; + double wetZTD; + double dryMap; + double wetMap; + double extVar; + double extZTD = tropCSSR( + trace, + kfState.time, + rec.pos, + PI / 2, + dryZTD, + dryMap, + wetZTD, + wetMap, + extVar + ); // todo aaron, take this from other places optionally + + if (extVar <= 0) + continue; + + KFKey kfKey; + kfKey.type = KF::TROP; + kfKey.str = rec.id; + + InitialState init = initialStateFromConfig(recOpts.trop); + + kfState.getKFValue(kfKey, init.x); + + double kftrop = init.x; + + tracepdeex( + 2, + trace, + " Checking troposphere pseudos: %s, %.4f + %.4f = %.4f, %.4f, %.2e\n", + rec.id.c_str(), + dryZTD, + wetZTD, + extZTD, + kftrop, + extVar + ); + + KFMeasEntry measEntry(&kfState); + measEntry.obsKey.type = KF::TROP; + measEntry.obsKey.str = rec.id; + + measEntry.addDsgnEntry(kfKey, +1, init); + + measEntry.setInnov(extZTD - kftrop); + + measEntry.metaDataMap["pseudoObs"] = (void*)true; + + measEntry.addNoiseEntry(measEntry.obsKey, 1, extVar); + + kfMeasEntryList.push_back(measEntry); + } } -void satClockPivotPseudoObs( - Trace& trace, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList) +void satClockPivotPseudoObs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList) { - if (acsConfig.pivot_satellite == "NO_PIVOT") - { - return; - } + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::SAT_CLOCK) + { + continue; + } - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::SAT_CLOCK) - { - continue; - } + string& constrain_clock = acsConfig.constrain_clock[key.Sat.sys]; - if ( acsConfig.pivot_satellite != "" - &&key.Sat != SatSys(acsConfig.pivot_satellite.c_str())) - { - continue; - } + if (constrain_clock.empty()) + { + continue; + } - KFMeasEntry measEntry(&kfState); - measEntry.obsKey.type = KF::SAT_CLOCK; - measEntry.obsKey.Sat = key.Sat; + if (constrain_clock != "" && constrain_clock != key.Sat.id()) + { + continue; + } - measEntry.addDsgnEntry(key, +1); + KFMeasEntry measEntry(&kfState); + measEntry.obsKey.type = KF::SAT_CLOCK; + measEntry.obsKey.Sat = key.Sat; + measEntry.obsKey.comment = "Satellite pivot constraint"; - measEntry.setInnov(0); + measEntry.addDsgnEntry(key, +1); - measEntry.metaDataMap["pseudoObs"] = (void*) true; + measEntry.setInnov(0); - measEntry.addNoiseEntry(measEntry.obsKey, 1, 1e-6); + measEntry.metaDataMap["pseudoObs"] = (void*)true; - kfMeasEntryList.push_back(measEntry); - } -} + measEntry.addNoiseEntry(measEntry.obsKey, 1, 1e-6); + kfMeasEntryList.push_back(measEntry); + } +} diff --git a/src/cpp/pea/ppp_slr.cpp b/src/cpp/pea/ppp_slr.cpp index 8fb63bd21..dfb68505c 100644 --- a/src/cpp/pea/ppp_slr.cpp +++ b/src/cpp/pea/ppp_slr.cpp @@ -1,508 +1,520 @@ - // #pragma GCC optimize ("O0") +#include #include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/antenna.hpp" +#include "common/biases.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" +#include "common/receiver.hpp" +#include "common/tides.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/orbitProp.hpp" +#include "pea/ppp.hpp" +#include "slr/slr.hpp" +#include "trop/tropModels.hpp" + +using std::function; /** Satellite Laser Ranging. * * */ -ParallelArchitecture SLR_Mesaurements__() -{ - -} - -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "tropModels.hpp" -#include "acsConfig.hpp" -#include "orbitProp.hpp" -#include "receiver.hpp" -#include "antenna.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "biases.hpp" -#include "gTime.hpp" -#include "tides.hpp" -#include "ppp.hpp" -#include "slr.hpp" - -#include - -using std::function; +ParallelArchitecture SLR_Mesaurements__() {} #define DEFAULT_RANG_BIAS_VAR SQR(0.001) #define DEFAULT_TIME_BIAS_VAR SQR(0.001) +// this hideousness is a horrible hack to make this code compile on macs. +// blame apple for this. look what they've made me do... +// this will ust copy and paste the type for now, this is undefined later to remove the type names +// to pass in the parameters in the same order +#define COMMON_ARG(type) type - -//this hideousness is a horrible hack to make this code compile on macs. -//blame apple for this. look what they've made me do... - -//this will ust copy and paste the type for now, this is undefined later to remove the type names to pass in the parameters in the same order -#define COMMON_ARG(type) type - -#define COMMON_PPP_ARGS \ - COMMON_ARG( Trace& ) trace, \ - COMMON_ARG( LObs& ) obs, \ - COMMON_ARG( GTime& ) time, \ - COMMON_ARG( SatStat& ) satStat, \ - COMMON_ARG( ReceiverOptions& ) recOpts, \ - COMMON_ARG( SatelliteOptions& ) satOpts, \ - COMMON_ARG( SatNav& ) satNav, \ - COMMON_ARG( VectorEcef& ) rRec, \ - COMMON_ARG( VectorEcef& ) rSat, \ - COMMON_ARG( double& ) rRecSat, \ - COMMON_ARG( VectorPos& ) pos, \ - COMMON_ARG( Receiver& ) rec, \ - COMMON_ARG(const KFState& ) kfState, \ - COMMON_ARG( ERPValues& ) erpv, \ - COMMON_ARG( FrameSwapper& ) frameSwapper, \ - COMMON_ARG( KFMeasEntry& ) measEntry - +#define COMMON_PPP_ARGS \ + COMMON_ARG(Trace&) \ + trace, COMMON_ARG(LObs&) obs, COMMON_ARG(GTime&) time, COMMON_ARG(SatStat&) satStat, \ + COMMON_ARG(ReceiverOptions&) recOpts, COMMON_ARG(SatelliteOptions&) satOpts, \ + COMMON_ARG(SatNav&) satNav, COMMON_ARG(VectorEcef&) rRec, COMMON_ARG(VectorEcef&) rSat, \ + COMMON_ARG(double&) rRecSat, COMMON_ARG(VectorPos&) pos, COMMON_ARG(Receiver&) rec, \ + COMMON_ARG(KFState&) kfState, COMMON_ARG(ERPValues&) erpv, \ + COMMON_ARG(FrameSwapper&) frameSwapper, COMMON_ARG(KFMeasEntry&) measEntry /** Satellite retroreflector delta from CoM */ inline void slrReflectorPCO(COMMON_PPP_ARGS) { - // satellite CoM to LRA offset correction - Vector3d satReflectorCom = Vector3d::Zero(); + // satellite CoM to LRA offset correction + Vector3d satReflectorCom = Vector3d::Zero(); - if (isSpherical(obs.satName)) satReflectorCom = satComOffSphere (obs) * satStat.e * -1; - else satReflectorCom = satComOffGnss (obs); + if (isSpherical(obs.satName)) + satReflectorCom = satComOffSphere(obs) * satStat.e * -1; + else + satReflectorCom = satComOffGnss(obs); - double satReflectorDelta = satReflectorCom.dot(satStat.e); + double satReflectorDelta = satReflectorCom.dot(satStat.e); - measEntry.componentsMap[E_Component::SAT_REFLECTOR_DELTA] = {satReflectorDelta * 2, "+ 2*satRefl", 0}; + measEntry.componentsMap[E_Component::SAT_REFLECTOR_DELTA] = { + satReflectorDelta * 2, + "+ 2*satRefl", + 0 + }; } /** Tide delta */ inline void slrTides(COMMON_PPP_ARGS) { - auto [solid, otl, atl, spole, opole] = tideDelta(trace, time, rec, rRec, recOpts); + auto [solid, otl, atl, spole, opole] = tideDelta(trace, time, rec, rRec, recOpts); - measEntry.componentsMap[E_Component::TIDES_SOLID ] = {-2 * satStat.e.dot(solid), "- 2*E.dT1", 0}; - measEntry.componentsMap[E_Component::TIDES_OTL ] = {-2 * satStat.e.dot(otl), "- 2*E.dT2", 0}; - measEntry.componentsMap[E_Component::TIDES_ATL ] = {-2 * satStat.e.dot(atl), "- 2*E.dT3", 0}; - measEntry.componentsMap[E_Component::TIDES_SPOLE ] = {-2 * satStat.e.dot(spole), "- 2*E.dT4", 0}; - measEntry.componentsMap[E_Component::TIDES_OPOLE ] = {-2 * satStat.e.dot(opole), "- 2*E.dT5", 0}; + measEntry.componentsMap[E_Component::TIDES_SOLID] = {-2 * satStat.e.dot(solid), "- 2*E.dT1", 0}; + measEntry.componentsMap[E_Component::TIDES_OTL] = {-2 * satStat.e.dot(otl), "- 2*E.dT2", 0}; + measEntry.componentsMap[E_Component::TIDES_ATL] = {-2 * satStat.e.dot(atl), "- 2*E.dT3", 0}; + measEntry.componentsMap[E_Component::TIDES_SPOLE] = {-2 * satStat.e.dot(spole), "- 2*E.dT4", 0}; + measEntry.componentsMap[E_Component::TIDES_OPOLE] = {-2 * satStat.e.dot(opole), "- 2*E.dT5", 0}; } /** Relativity corrections */ inline void slrRelativity2(COMMON_PPP_ARGS) { - // secondary relativity effect (Shapiro effect) - double dtRel2 = relativity2(rSat, rRec); + // secondary relativity effect (Shapiro effect) + double dtRel2 = relativity2(rSat, rRec); - measEntry.componentsMap[E_Component::RELATIVITY2] = {dtRel2 * CLIGHT * 2, "+ rel2", 0}; + measEntry.componentsMap[E_Component::RELATIVITY2] = {dtRel2 * CLIGHT * 2, "+ rel2", 0}; } /** Sagnac effect */ 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 dSagnacOut = sagnac(rSat, rRec); + double dSagnacIn = sagnac(rRec, rSat); // todo aaron, is it that simple? look at area - measEntry.componentsMap[E_Component::SAGNAC] = {dSagnacOut + dSagnacIn, "+ sag", 0}; + measEntry.componentsMap[E_Component::SAGNAC] = {dSagnacOut + dSagnacIn, "+ sag", 0}; } /** Tropospheric delay */ inline void slrTrop(COMMON_PPP_ARGS) { - TropStates tropStates; - TropMapping dTropDx; - double varTrop = 0; + TropStates tropStates; + TropMapping dTropDx; + double varTrop = 0; - //calculate the trop values, variances, and gradients at the operating points - double troposphere_m = laserTropDelay(obs, pos, satStat, tropStates, dTropDx, varTrop); + // calculate the trop values, variances, and gradients at the operating points + double troposphere_m = laserTropDelay(obs, pos, satStat, tropStates, dTropDx, varTrop); - InitialState init = initialStateFromConfig(recOpts.trop); + InitialState init = initialStateFromConfig(recOpts.trop); - if (init.estimate) - { - KFKey obsKey; - obsKey.type = KF::TROP; - obsKey.str = obs.recName; - obsKey.Sat = obs.Sat; + if (init.estimate) + { + KFKey obsKey; + obsKey.type = KF::TROP; + obsKey.str = obs.recName; + obsKey.Sat = obs.Sat; - measEntry.addNoiseEntry(obsKey, 1, varTrop); + measEntry.addNoiseEntry(obsKey, 1, varTrop); - varTrop = -1; - } + varTrop = -1; + } - measEntry.componentsMap[E_Component::TROPOSPHERE] = {troposphere_m * 2, "+ 2*" + std::to_string(dTropDx.dryMap) + ".T", varTrop}; + measEntry.componentsMap[E_Component::TROPOSPHERE] = { + troposphere_m * 2, + "+ 2*" + std::to_string(dTropDx.dryMap) + ".T", + varTrop + }; } inline void slrRecAntDelta(COMMON_PPP_ARGS) { - Vector3d recAntVector = body2ecef(rec.attStatus, rec.antDelta); + Vector3d recAntVector = body2ecef(rec.attStatus, rec.antDelta); - double recAntDelta = -recAntVector.dot(satStat.e); + double recAntDelta = -recAntVector.dot(satStat.e); - measEntry.componentsMap[E_Component::REC_ANTENNA_DELTA] = {recAntDelta * 2, "- 2*E.dR_r", 0}; + measEntry.componentsMap[E_Component::REC_ANTENNA_DELTA] = {recAntDelta * 2, "- 2*E.dR_r", 0}; }; /** Rec range bias */ inline void slrRecRangeBias(COMMON_PPP_ARGS) { - double recRangeBias = obs.rangeBias; - double recRangeBiasVar = DEFAULT_RANG_BIAS_VAR; // todo Eugene: use actual var? + double recRangeBias = obs.rangeBias; + double recRangeBiasVar = DEFAULT_RANG_BIAS_VAR; // todo Eugene: use actual var? - InitialState init = initialStateFromConfig(recOpts.slr_range_bias); + InitialState init = initialStateFromConfig(recOpts.slr_range_bias); - if (init.estimate) - { - if (init.Q < 0) - { - init.P = recRangeBiasVar; - } + if (init.estimate) + { + if (init.Q < 0) + { + init.P = recRangeBiasVar; + } - KFKey kfKey; - kfKey.type = KF::SLR_REC_RANGE_BIAS; - kfKey.str = obs.recName; + KFKey kfKey; + kfKey.type = KF::SLR_REC_RANGE_BIAS; + kfKey.str = obs.recName; - init.x = recRangeBias; + init.x = recRangeBias; - kfState.getKFValue(kfKey, init.x); + kfState.getKFValue(kfKey, init.x); - recRangeBias = init.x; + recRangeBias = init.x; - measEntry.addDsgnEntry(kfKey, 1, init); + measEntry.addDsgnEntry(kfKey, 1, init); - recRangeBiasVar = -1; - } + recRangeBiasVar = -1; + } - measEntry.componentsMap[E_Component::REC_RANGE_BIAS] = {recRangeBias, "+ recRangeBias", recRangeBiasVar}; + measEntry.componentsMap[E_Component::REC_RANGE_BIAS] = { + recRangeBias, + "+ recRangeBias", + recRangeBiasVar + }; } /** Rec time bias */ 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 recTimeBiasPartial = -obs.satVel.dot(satStat.e) / CLIGHT; + // 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 recTimeBiasPartial = -obs.satVel.dot(satStat.e) / CLIGHT; - InitialState init = initialStateFromConfig(recOpts.slr_time_bias); + InitialState init = initialStateFromConfig(recOpts.slr_time_bias); - if (init.estimate) - { - if (init.Q < 0) - { - init.P = recTimeBiasVar; - } + if (init.estimate) + { + if (init.Q < 0) + { + init.P = recTimeBiasVar; + } - KFKey kfKey; - kfKey.type = KF::SLR_REC_TIME_BIAS; - kfKey.str = obs.recName; + KFKey kfKey; + kfKey.type = KF::SLR_REC_TIME_BIAS; + kfKey.str = obs.recName; - init.x = recTimeBias; + init.x = recTimeBias; - kfState.getKFValue(kfKey, init.x); + kfState.getKFValue(kfKey, init.x); - recTimeBias = init.x; + recTimeBias = init.x; - measEntry.addDsgnEntry(kfKey, recTimeBiasPartial, init); + measEntry.addDsgnEntry(kfKey, recTimeBiasPartial, init); - recTimeBiasVar = -1; - } + recTimeBiasVar = -1; + } - measEntry.componentsMap[E_Component::REC_TIME_BIAS] = {recTimeBiasPartial * recTimeBias * 2, "+ 2*recTimeBiasPartial*recTimeBias", recTimeBiasVar}; + measEntry.componentsMap[E_Component::REC_TIME_BIAS] = { + recTimeBiasPartial * recTimeBias * 2, + "+ 2*recTimeBiasPartial*recTimeBias", + recTimeBiasVar + }; } inline void slrEopAdjustment(COMMON_PPP_ARGS) { - if (acsConfig.pppOpts.eop.estimate[0] == false) - { - return; - } + if (acsConfig.pppOpts.eop.estimate[0] == false) + { + return; + } - VectorEcef e2 = satStat.e * 2; // eopPartials * 2 & adjustment * 2 + VectorEcef e2 = satStat.e * 2; // eopPartials * 2 & adjustment * 2 - eopAdjustment(time, e2, erpv, frameSwapper, rec, rRec, measEntry, kfState); + eopAdjustment(time, e2, erpv, frameSwapper, rec, rRec, measEntry, kfState); } - - - - - - - - - - - -//redefine this to replace with nothing from now on - ie, use the argument name but not its type -#undef COMMON_ARG -#define COMMON_ARG(type) +// redefine this to replace with nothing from now on - ie, use the argument name but not its type +#undef COMMON_ARG +#define COMMON_ARG(type) void receiverSlr( - Trace& pppTrace, ///< Trace to output to - Receiver& rec, ///< Receiver to perform calculations for - const KFState& kfState, ///< Kalman filter object containing the network state parameters - KFMeasEntryList& kfMeasEntryList) ///< List to append kf measurements to + Trace& pppTrace, ///< Trace to output to + Receiver& rec, ///< Receiver to perform calculations for + KFState& kfState, ///< Kalman filter object containing the network state parameters + KFMeasEntryList& kfMeasEntryList ///< List to append kf measurements to +) { - DOCS_REFERENCE(SLR_Mesaurements__); - - if (acsConfig.slrOpts.process_slr == false) - { - return; - } - - auto trace = getTraceFile(rec); - - tracepdeex(0, trace, "\n--------------------- Processing SLR -------------------"); - - if ( rec.obsList.empty() - || rec.invalid) - { - tracepdeex(1, trace, "\n\nReceiver not ready for SLR. Obs=%d", rec.obsList.size()); return; - } - - ERPValues erpv = getErp(nav.erp, tsync); - FrameSwapper frameSwapper(tsync, erpv); - - int obsNum = 0; - for (auto& obs : only(rec.obsList)) - { - updateSlrRecBiases(obs); - - if (obs.exclude) - { - continue; - } + DOCS_REFERENCE(SLR_Mesaurements__); - SatNav& satNav = *obs.satNav_ptr; - SatStat& satStat = *obs.satStat_ptr; + if (acsConfig.slrOpts.process_slr == false) + { + return; + } - auto& satOpts = acsConfig.getSatOpts(obs.Sat); - auto& recOpts = acsConfig.getRecOpts(rec.id); + auto trace = getTraceFile(rec); - if (satOpts.exclude) - { - continue; - } + tracepdeex(0, trace, "\n--------------------- Processing SLR -------------------"); - if (recOpts.exclude) - { - continue; - } + if (rec.obsList.empty() || rec.invalid) + { + tracepdeex(1, trace, "\n\nReceiver not ready for SLR. Obs=%d", rec.obsList.size()); + return; + } - GTime time = obs.time; + ERPValues erpv = getErp(nav.erp, tsync); + FrameSwapper frameSwapper(tsync, erpv); - satpos(trace, time, time, obs, satOpts.posModel.sources, E_OffsetType::COM, nav, &kfState); + int obsNum = 0; + for (auto& obs : only(rec.obsList)) + { + updateSlrRecBiases(obs); - if (obs.ephPosValid == false) - BOOST_LOG_TRIVIAL(warning) << "Warning: Invalid position for " << obs.Sat.id() << " in " << __FUNCTION__; + if (obs.exclude) + { + continue; + } - auto Sat = obs.Sat; - auto sys = Sat.sys; + SatNav& satNav = *obs.satNav_ptr; + SatStat& satStat = *obs.satStat_ptr; - double observed = obs.twoWayTimeOfFlight * CLIGHT; + auto& satOpts = acsConfig.getSatOpts(obs.Sat); + auto& recOpts = acsConfig.getRecOpts(rec.id); - if (observed == 0) - { - continue; - } + if (satOpts.exclude) + { + continue; + } - KFMeasEntry measEntry(&kfState); + if (recOpts.exclude) + { + continue; + } - measEntry.metaDataMap["obs_ptr"] = &obs; + GTime time = obs.time; - measEntry.obsKey.Sat = obs.Sat; - measEntry.obsKey.str = rec.id; - measEntry.obsKey.type = KF::RANGE; + satpos(trace, time, time, obs, satOpts.posModel.sources, E_OffsetType::COM, nav, &kfState); - //Start with the observed measurement and its noise - { - KFKey obsKey; - obsKey.type = KF::LASER_MEAS; - obsKey.str = obs.recName; - obsKey.Sat = obs.Sat; - obsKey.num = obsNum; - obsKey.comment = measEntry.obsKey.comment; + if (obs.ephPosValid == false) + BOOST_LOG_TRIVIAL(warning) + << "Invalid position for " << obs.Sat.id() << " in " << __FUNCTION__; - double var = SQR(recOpts.laser_sigma); + auto Sat = obs.Sat; + auto sys = Sat.sys; - measEntry.addNoiseEntry(obsKey, 1, var); + double observed = obs.twoWayTimeOfFlight * CLIGHT; - measEntry.componentsMap[E_Component::OBSERVED] = {-observed, "- obs", var}; - } + if (observed == 0) + { + continue; + } - //Calculate the basic range + KFMeasEntry measEntry(&kfState); - VectorEcef rRec = rec.aprioriPos; + measEntry.metaDataMap["slrObs_ptr"] = &obs; - vector> delayedInits; + measEntry.obsKey.Sat = obs.Sat; + measEntry.obsKey.str = rec.id; + measEntry.obsKey.type = KF::RANGE; - { - for (int i = 0; i < 3; i++) - { - InitialState init = initialStateFromConfig(recOpts.pos, i); + // Start with the observed measurement and its noise + { + KFKey obsKey; + obsKey.type = KF::LASER_MEAS; + obsKey.str = obs.recName; + obsKey.Sat = obs.Sat; + obsKey.num = obsNum; + obsKey.comment = measEntry.obsKey.comment; - if (init.estimate) - { - KFKey kfKey; - kfKey.type = KF::REC_POS; - kfKey.str = obs.recName; - kfKey.num = i; + double var = SQR(recOpts.laser_sigma); - init.x = rRec[i]; + measEntry.addNoiseEntry(obsKey, 1, var); - kfState.getKFValue(kfKey, init.x); + measEntry.componentsMap[E_Component::OBSERVED] = {-observed, "- obs", var}; + } - rRec[i] = init.x; + // Calculate the basic range - delayedInits.push_back([kfKey, init, i, &measEntry] - (Vector3d satStat_e, Vector3d eSatInertial) - { - measEntry.addDsgnEntry(kfKey, -satStat_e[i] * 2, init); - }); + VectorEcef rRec = rec.aprioriPos; + + vector> delayedInits; + + { + for (int i = 0; i < 3; i++) + { + InitialState init = initialStateFromConfig(recOpts.pos, i); - // measEntry.addDsgnEntry(kfKey, -satStat.e[i] * 2, init); - } - } - } - - auto& pos = rec.pos; - - pos = ecef2pos(rRec); - - VectorEcef& rSat = obs.rSatCom; - - if (obs.rSatCom.isZero()) - { - BOOST_LOG_TRIVIAL(error) << "Error: Satpos is unexpectedly zero for " << rec.id << " - " << Sat.id(); - continue; - } - - if (initialStateFromConfig(satOpts.orbit).estimate) - { - double tgap = (time - obs.timeBias - tsync).to_double(); - - if (obs.rSatEci0.isZero()) - { - //dont obliterate obs.rSat below, we dont need it, but maintain consistency with ppp_obs.cpp - SatPos satPos0 = obs; - - //use this to avoid adding the dt component of position - bool pass = satpos(trace, tsync, tsync, satPos0, satOpts.posModel.sources, E_OffsetType::COM, nav, &kfState); - - obs.rSatEci0 = frameSwapper(satPos0.rSatCom, &satPos0.satVel, &obs.vSatEci0); - } - - for (int i = 0; i < 3; i++) - { - InitialState posInit = initialStateFromConfig(satOpts.orbit, i); - InitialState velInit = initialStateFromConfig(satOpts.orbit, i + 3); - - KFKey posKey; - posKey.type = KF::ORBIT; - posKey.Sat = obs.Sat; - posKey.num = i; - posKey.comment = posInit.comment; - - KFKey velKey = posKey; - velKey.num = i + 3; - velKey.comment = velInit.comment; - - if (posInit.x == 0) posInit.x = obs.rSatEci0[i]; - if (velInit.x == 0) velInit.x = obs.vSatEci0[i]; - - delayedInits.push_back([posKey, velKey, posInit, velInit, i, &measEntry, tgap] - (Vector3d satStat_e, Vector3d eSatInertial) - { - 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(posKey, +eSatInertial[i] * 2, posInit); - } - - addEmpStates(satOpts, kfState, Sat); - } - else - { - KFKey obsKey; - obsKey.type = KF::ORBIT; - obsKey.Sat = obs.Sat; -// obsKey.num = i; - - measEntry.addNoiseEntry(obsKey, 1, obs.ephVar); //todo aaron, need many more noise entries - } - - - //Range and geometry - - double rRecSat = (rSat - rRec).norm(); - satStat.e = (rSat - rRec).normalized(); - - VectorEci eSatInertial = frameSwapper(satStat.e); - - satazel(pos, satStat.e, satStat); - - if (satStat.el < recOpts.elevation_mask_deg * D2R) - { - obs.excludeElevation = true; - continue; - } - - //add initialisations for things waiting for an up-to-date satstat - for (auto& delayedInit : delayedInits) - { - delayedInit(satStat.e, eSatInertial); - } - - if (recOpts.range) - { - measEntry.componentsMap[E_Component::RANGE] = {rRecSat * 2, "+ range", 0}; - } - - if (acsConfig.output_residual_chain) - { - tracepdeex(0, trace, "\n----------------------------------------------------"); - tracepdeex(0, trace, "\nMeasurement for %s", ((string) measEntry.obsKey).c_str()); - } - - //Add modelled adjustments and estimated parameter - { - slrRecAntDelta (COMMON_PPP_ARGS); - slrReflectorPCO (COMMON_PPP_ARGS); - slrTides (COMMON_PPP_ARGS); - slrRelativity2 (COMMON_PPP_ARGS); - slrSagnac (COMMON_PPP_ARGS); - slrTrop (COMMON_PPP_ARGS); - slrRecRangeBias (COMMON_PPP_ARGS); - slrRecTimeBias (COMMON_PPP_ARGS); - slrEopAdjustment (COMMON_PPP_ARGS); - } - - if (obs.exclude) // slrReflectorPCO() may have observations excluded - { - continue; - } - - //Calculate residuals and form up the measurement - - measEntry.componentsMap[E_Component::NET_RESIDUAL] = {0, "", 0}; - - double residual = netResidualAndChainOutputs(trace, obs, measEntry); - - measEntry.setInnov(residual); - - kfMeasEntryList.push_back(measEntry); - - obsNum++; - } - - trace << "\n" << "\n"; + if (init.estimate) + { + KFKey kfKey; + kfKey.type = KF::REC_POS; + kfKey.str = obs.recName; + kfKey.num = i; + + init.x = rRec[i]; + + kfState.getKFValue(kfKey, init.x); + + rRec[i] = init.x; + + delayedInits.push_back( + [kfKey, init, i, &measEntry](Vector3d satStat_e, Vector3d eSatInertial) + { measEntry.addDsgnEntry(kfKey, -satStat_e[i] * 2, init); } + ); + + // measEntry.addDsgnEntry(kfKey, -satStat.e[i] * 2, init); + } + } + } + + auto& pos = rec.pos; + + pos = ecef2pos(rRec); + + VectorEcef& rSat = obs.rSatCom; + + if (obs.rSatCom.isZero()) + { + BOOST_LOG_TRIVIAL(error) + << "Sat pos is unexpectedly zero for " << rec.id << " - " << Sat.id(); + continue; + } + + if (initialStateFromConfig(satOpts.orbit).estimate) + { + double tgap = (time - obs.timeBias - tsync).to_double(); + + if (obs.rSatEci0.isZero()) + { + // dont obliterate obs.rSat below, we dont need it, but maintain consistency with + // ppp_obs.cpp + SatPos satPos0 = obs; + + // use this to avoid adding the dt component of position + bool pass = satpos( + trace, + tsync, + tsync, + satPos0, + satOpts.posModel.sources, + E_OffsetType::COM, + nav, + &kfState + ); + + obs.rSatEci0 = frameSwapper(satPos0.rSatCom, &satPos0.satVel, &obs.vSatEci0); + } + + for (int i = 0; i < 3; i++) + { + InitialState posInit = initialStateFromConfig(satOpts.orbit, i); + InitialState velInit = initialStateFromConfig(satOpts.orbit, i + 3); + + KFKey posKey; + posKey.type = KF::ORBIT; + posKey.Sat = obs.Sat; + posKey.num = i; + posKey.comment = posInit.comment; + + KFKey velKey = posKey; + velKey.num = i + 3; + velKey.comment = velInit.comment; + + if (posInit.x == 0) + posInit.x = obs.rSatEci0[i]; + if (velInit.x == 0) + velInit.x = obs.vSatEci0[i]; + + delayedInits.push_back( + [posKey, velKey, posInit, velInit, i, &measEntry, tgap]( + Vector3d satStat_e, + Vector3d eSatInertial + ) + { + 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(posKey, +eSatInertial[i] * 2, posInit); + } + + addEmpStates(satOpts, kfState, Sat); + } + else + { + KFKey obsKey; + obsKey.type = KF::ORBIT; + obsKey.Sat = obs.Sat; + // obsKey.num = i; + + measEntry + .addNoiseEntry(obsKey, 1, obs.ephVar); // todo aaron, need many more noise entries + } + + // Range and geometry + + double rRecSat = (rSat - rRec).norm(); + satStat.e = (rSat - rRec).normalized(); + + VectorEci eSatInertial = frameSwapper(satStat.e); + + satazel(pos, satStat.e, satStat); + + if (satStat.el < recOpts.elevation_mask_deg * D2R) + { + obs.excludeElevation = true; + continue; + } + + // add initialisations for things waiting for an up-to-date satstat + for (auto& delayedInit : delayedInits) + { + delayedInit(satStat.e, eSatInertial); + } + + if (recOpts.range) + { + measEntry.componentsMap[E_Component::RANGE] = {rRecSat * 2, "+ range", 0}; + } + + if (acsConfig.output_residual_chain) + { + tracepdeex(0, trace, "\n----------------------------------------------------"); + tracepdeex(0, trace, "\nMeasurement for %s", ((string)measEntry.obsKey).c_str()); + } + + // Add modelled adjustments and estimated parameter + { + slrRecAntDelta(COMMON_PPP_ARGS); + slrReflectorPCO(COMMON_PPP_ARGS); + slrTides(COMMON_PPP_ARGS); + slrRelativity2(COMMON_PPP_ARGS); + slrSagnac(COMMON_PPP_ARGS); + slrTrop(COMMON_PPP_ARGS); + slrRecRangeBias(COMMON_PPP_ARGS); + slrRecTimeBias(COMMON_PPP_ARGS); + slrEopAdjustment(COMMON_PPP_ARGS); + } + + if (obs.exclude) // slrReflectorPCO() may have observations excluded + { + continue; + } + + // Calculate residuals and form up the measurement + + measEntry.componentsMap[E_Component::NET_RESIDUAL] = {0, "", 0}; + + double residual = netResidualAndChainOutputs(trace, obs, measEntry); + + measEntry.setInnov(residual); + + kfMeasEntryList.push_back(measEntry); + + obsNum++; + } + + trace << "\n" + << "\n"; } - diff --git a/src/cpp/pea/ppppp.cpp b/src/cpp/pea/ppppp.cpp index 6f03c5681..ac7a6b3a1 100644 --- a/src/cpp/pea/ppppp.cpp +++ b/src/cpp/pea/ppppp.cpp @@ -1,1414 +1,1917 @@ - // #pragma GCC optimize ("O0") +#include "pea/ppp.hpp" +#include +#include +#include +#include +#include +#include #include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/eigenIncluder.hpp" +#include "common/metaData.hpp" +#include "common/mongoRead.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/trace.hpp" +#include "inertial/posProp.hpp" +#include "iono/ionoModel.hpp" +#include "omp.h" +#include "orbprop/coordinates.hpp" +#include "orbprop/orbitProp.hpp" +#include "rtklib/lambda.h" +using std::map; +using std::string; +using std::stringstream; +using std::tuple; /** Primary estimation and filtering. * - * While there are other auxiliary filters and states used within the Pea, all PPP processing flows through a common filtering stage. + * While there are other auxiliary filters and states used within the Pea, all PPP processing flows + * through a common filtering stage. * - * The residuals for all observations are first computed in an undifferenced-uncombined state, which leads to the greatest generality and extensibility. - * As each receiver's observations are then independent of each other, these computations are computed in parallel, using openMP directives, increasing thoughput. + * The residuals for all observations are first computed in an undifferenced-uncombined state, which + * leads to the greatest generality and extensibility. As each receiver's observations are then + * independent of each other, these computations are computed in parallel, using openMP directives, + * increasing thoughput. * - * Bookkeeping around the initialisation of state elements and their state transitions is taken care of automatically at the point they are first referenced in the observation equations, - * using values as configured in the yaml file. + * Bookkeeping around the initialisation of state elements and their state transitions is taken care + * of automatically at the point they are first referenced in the observation equations, using + * values as configured in the yaml file. * - * The distinction between "Network" and "User" positioning modes that may be used in other software packages is not required in Ginan. - * All receivers are always stored in a large single filter. - * Depending on the configuration, the state and it's covariance matrix may turn out to be block-diagonal (user-mode), which will automatically be treated optimally upon the estimation stage by applying 'chunking'. + * The distinction between "Network" and "User" positioning modes that may be used in other software + * packages is not required in Ginan. All receivers are always stored in a large single filter. + * Depending on the configuration, the state and it's covariance matrix may turn out to be + * block-diagonal (user-mode), which will automatically be treated optimally upon the estimation + * stage by applying 'chunking'. * */ Architecture Main_Filter__() { - DOCS_REFERENCE(UDUC_GNSS_Measurements__); - DOCS_REFERENCE(SLR_Mesaurements__); - DOCS_REFERENCE(Combinators__); - DOCS_REFERENCE(Pseudo_Observations__); - DOCS_REFERENCE(Kalman_Filter__); - DOCS_REFERENCE(Error_Handling__); + DOCS_REFERENCE(UDUC_GNSS_Measurements__); + DOCS_REFERENCE(SLR_Mesaurements__); + DOCS_REFERENCE(Combinators__); + DOCS_REFERENCE(Pseudo_Observations__); + DOCS_REFERENCE(Kalman_Filter__); + DOCS_REFERENCE(Error_Handling__); } /** */ -Architecture Combinators__() -{ - -} - -#include -#include -#include -#include -#include +Architecture Combinators__() {} #ifdef ENABLE_PARALLELISATION - #include "omp.h" #endif -using std::string; -using std::tuple; -using std::map; - - -#include "interactiveTerminal.hpp" -#include "eigenIncluder.hpp" -#include "observations.hpp" -#include "coordinates.hpp" -#include "mongoWrite.hpp" -#include "navigation.hpp" -#include "mongoRead.hpp" -#include "orbitProp.hpp" -#include "ionoModel.hpp" -#include "acsConfig.hpp" -#include "metaData.hpp" -#include "receiver.hpp" -#include "posProp.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "trace.hpp" -#include "lambda.h" -#include "ppp.hpp" - struct Duo { - map& indexMap; - MatrixXd& designMatrix; + map& indexMap; + MatrixXd& designMatrix; }; -void explainMeasurements( - Trace& trace, - KFMeas& kfMeas, - KFState& kfState) +void explainMeasurements(Trace& trace, KFMeas& kfMeas, KFState& kfState) { - for (int i = 0; i < kfMeas.obsKeys.size(); i++) - { - auto& obsKey = kfMeas.obsKeys [i]; - auto& metaDataMap = kfMeas.metaDataMaps [i]; - - if (metaDataMap["explain"] == nullptr) - { - continue; - } - - InteractiveTerminal output((string)"Partials/" + (string)obsKey, trace); - - output << "\n" << "============================"; - output << "\n" << "Explaining " << obsKey << " : " << obsKey.comment; - - for (auto duo : { - Duo{kfState .kfIndexMap, kfMeas.H}, - Duo{kfMeas .noiseIndexMap, kfMeas.H_star} - }) - for (int col = 0; col < duo.designMatrix.cols(); col++) - { - double entry = duo.designMatrix(i, col); - - if (entry == 0) - { - continue; - } - - for (auto& [kfKey, index] : duo.indexMap) - { - if (index == col) - { - output << "\n"; - if (traceLevel >= 4) - output << kfState.time << " " << obsKey; - - output << kfKey << " : " << entry; - break; - } - } - } - output << "\n"; - } + for (int i = 0; i < kfMeas.obsKeys.size(); i++) + { + auto& obsKey = kfMeas.obsKeys[i]; + auto& metaDataMap = kfMeas.metaDataMaps[i]; + + if (metaDataMap["explain"] == nullptr) + { + continue; + } + + trace << "\n" + << "============================"; + trace << "\n" + << "Explaining " << obsKey << " : " << obsKey.comment; + + for (auto duo : + {Duo{kfState.kfIndexMap, kfMeas.H}, Duo{kfMeas.noiseIndexMap, kfMeas.H_star}}) + for (int col = 0; col < duo.designMatrix.cols(); col++) + { + if (col == 0) + { + trace << "\n" + << "============================"; + } + + double entry = duo.designMatrix(i, col); + + if (entry == 0) + { + continue; + } + + for (auto& [kfKey, index] : duo.indexMap) + { + if (index == col) + { + trace << "\n"; + if (traceLevel >= 4) + trace << kfState.time << " " << obsKey; + + trace << kfKey << " : " << entry; + break; + } + } + } + trace << "\n"; + } } -void alternatePostfits( - Trace& trace, - KFMeas& kfMeas, - KFState& kfState) +void alternatePostfits(Trace& trace, KFMeas& kfMeas, KFState& kfState) { - for (auto& [kfKey, col] : kfMeas.noiseIndexMap) - { - if ( kfKey.type > KF::BEGIN_MEAS_STATES - &&kfKey.type < KF::END_MEAS_STATES) - { - continue; - } - - bool first = true; - - for (int row = 0; row < kfMeas.H_star.rows(); row++) - { - double& entry = kfMeas.H_star(row, col); - - if (entry == 0) - { - continue; - } - - if (first) - { - first = false; - - trace << "\n" << "Removing " << kfKey << " from postfit residual calculations"; - } - - entry = 0; - } - } + if (kfState.advanced_postfits == false) + { + return; + } + + for (auto& [kfKey, col] : kfMeas.noiseIndexMap) + { + if (kfKey.type > KF::BEGIN_MEAS_STATES && kfKey.type < KF::END_MEAS_STATES) + { + continue; + } + + bool first = true; + + for (int row = 0; row < kfMeas.H_star.rows(); row++) + { + double& entry = kfMeas.H_star(row, col); + + if (entry == 0) + { + continue; + } + + if (first) + { + first = false; + + trace << "\n" + << "Removing " << kfKey << " from postfit residual calculations"; + } + + entry = 0; + } + } } -void makeIFLCs( - Trace& trace, - const KFState& kfState, - KFMeasEntryList& kfMeasEntryList) +void makeIFLCs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList) { - bool iflcMade = false; - - for (int i = 0; i < kfMeasEntryList.size(); i++) - { - auto& kfMeasEntryI = kfMeasEntryList[i]; - - if (kfMeasEntryI.valid == false) - { - continue; - } - - double coeff_i = 0; - KFKey ionKey_i; - - for (auto& [key, value] : kfMeasEntryI.designEntryMap) - { - if (key.type == KF::IONO_STEC) - { - ionKey_i = key; - coeff_i = value; - break; - } - } - - if (coeff_i == 0) - { - //no ionosphere reference - continue; - } - - //either this will be combined with something below, or it wont be, in either case this one is not needed and invalid now - kfMeasEntryI.valid = false; - - for (int j = i + 1; j < kfMeasEntryList.size(); j++) - { - auto& kfMeasEntryJ = kfMeasEntryList[j]; - - if (kfMeasEntryI.metaDataMap["IFLCcombined"]) { continue; } - if (kfMeasEntryJ.metaDataMap["IFLCcombined"]) { continue; } - - auto it = kfMeasEntryJ.designEntryMap.find(ionKey_i); - if (it == kfMeasEntryJ.designEntryMap.end()) - { - continue; - } - - auto& [ionKey_j, coeff_j] = *it; - - double coefj = coeff_j; - - if (coeff_i * coeff_j < 0) { continue; } //only combine similarly signed (code/phase) components - if (coeff_i == coeff_j) { continue; } //dont combine if it will eliminate the entire measurement - - //these measurements both share a common ionosphere, remove it. - - iflcMade = true; - - double scalar = sqrt( (SQR(coeff_i) + SQR(coeff_j)) / SQR(coeff_i - coeff_j) ); - - kfMeasEntryJ.obsKey.num = 100 * kfMeasEntryI.obsKey.num - + kfMeasEntryJ.obsKey.num; - - kfMeasEntryJ.obsKey.comment = kfMeasEntryI.obsKey.comment - + "-" + kfMeasEntryJ.obsKey.comment; - - kfMeasEntryJ.innov = coeff_j * scalar * kfMeasEntryI.innov - - coeff_i * scalar * kfMeasEntryJ.innov; - - map newDesignEntryMap; - map newNoiseEntryMap; - map newComponentsMap; - - for (auto& [key, valueI] : kfMeasEntryI.usedValueMap) kfMeasEntryJ.usedValueMap [key] = valueI; - - for (auto& [key, valueI] : kfMeasEntryI.noiseElementMap) kfMeasEntryJ.noiseElementMap[key] = valueI; - - for (auto& [key, valueI] : kfMeasEntryI.designEntryMap) newDesignEntryMap [key] += valueI * coeff_j * scalar * +1; - for (auto& [key, valueJ] : kfMeasEntryJ.designEntryMap) newDesignEntryMap [key] += valueJ * coeff_i * scalar * -1; - - for (auto& [key, valueI] : kfMeasEntryI.noiseEntryMap) newNoiseEntryMap [key] += valueI * coeff_j * scalar * +1; - for (auto& [key, valueJ] : kfMeasEntryJ.noiseEntryMap) newNoiseEntryMap [key] += valueJ * coeff_i * scalar * -1; - - for (auto& [key, valueI] : kfMeasEntryI.componentsMap) newComponentsMap [key] += valueI * coeff_j * scalar * +1; - for (auto& [key, valueJ] : kfMeasEntryJ.componentsMap) newComponentsMap [key] += valueJ * coeff_i * scalar * -1; - - for (auto& [id, value] : kfMeasEntryI.metaDataMap) - { - kfMeasEntryJ.metaDataMap[id + "_alt"] = value; - } - - kfMeasEntryI.metaDataMap["IFLCcombined"] = (void*) true; - kfMeasEntryJ.metaDataMap["IFLCcombined"] = (void*) true; -// kfMeasEntryJ.metaDataMap["explain"] = (void*) true; - - newDesignEntryMap[ionKey_j] = 0; - kfMeasEntryJ.designEntryMap = std::move(newDesignEntryMap); - kfMeasEntryJ.noiseEntryMap = std::move(newNoiseEntryMap); - kfMeasEntryJ.componentsMap = std::move(newComponentsMap); - - kfState.removeState(ionKey_i); - kfState.removeState(ionKey_j); - break; - } - } - - if ( kfMeasEntryList.empty() == false - &&iflcMade == false) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No IONO_STEC measurements found - 'use_if_combo' requires 'ion_stec' estimation to be enabled in the config file."; - } + if (kfMeasEntryList.empty()) + { + return; + } + + bool iflcMade = false; + + for (int i = 0; i < kfMeasEntryList.size(); i++) + { + auto& kfMeasEntryI = kfMeasEntryList[i]; + + if (kfMeasEntryI.valid == false) + { + continue; + } + + double coeff_i = 0; + KFKey ionKey_i; + bool useDesignMap = true; + + for (auto designMap : {true, false}) + { + map* entryMapPtr; + if (designMap) + entryMapPtr = &kfMeasEntryI.designEntryMap; + else + entryMapPtr = &kfMeasEntryI.noiseEntryMap; + + for (auto& [key, value] : *entryMapPtr) + { + if (key.type == KF::IONO_STEC) + { + ionKey_i = key; + coeff_i = value; + useDesignMap = designMap; + goto breakI; + } + } + } + breakI: + + if (coeff_i == 0) + { + // no ionosphere reference + continue; + } + + // either this will be combined with something below, or it wont be, in either case this one + // is not needed and invalid now + kfMeasEntryI.valid = false; + + for (int j = i + 1; j < kfMeasEntryList.size(); j++) + { + auto& kfMeasEntryJ = kfMeasEntryList[j]; + + if (kfMeasEntryI.metaDataMap["IFLCcombined"]) + { + continue; + } + if (kfMeasEntryJ.metaDataMap["IFLCcombined"]) + { + continue; + } + + map* entryMapPtr; + if (useDesignMap) + entryMapPtr = &kfMeasEntryJ.designEntryMap; + else + entryMapPtr = &kfMeasEntryJ.noiseEntryMap; + + auto it = entryMapPtr->find(ionKey_i); + if (it == entryMapPtr->end()) + { + continue; + } + + auto& [ionKey_j, coeff_j] = *it; + + if (coeff_i * coeff_j < 0) + { + continue; + } // only combine similarly signed (code/phase) components + if (coeff_i == coeff_j) + { + continue; + } // dont combine if it will eliminate the entire measurement + + // these measurements both share a common ionosphere, remove it. + + iflcMade = true; + + double scalar = sqrt((SQR(coeff_i) + SQR(coeff_j)) / SQR(coeff_i - coeff_j)); + + kfMeasEntryJ.obsKey.num = 100 * kfMeasEntryI.obsKey.num + kfMeasEntryJ.obsKey.num; + + kfMeasEntryJ.obsKey.comment = + kfMeasEntryI.obsKey.comment + "-" + kfMeasEntryJ.obsKey.comment; + + kfMeasEntryJ.innov = + coeff_j * scalar * kfMeasEntryI.innov - coeff_i * scalar * kfMeasEntryJ.innov; + + map newDesignEntryMap; + map newNoiseEntryMap; + map newComponentsMap; + + for (auto& [key, valueI] : kfMeasEntryI.usedValueMap) + kfMeasEntryJ.usedValueMap[key] = valueI; + + for (auto& [key, valueI] : kfMeasEntryI.noiseElementMap) + kfMeasEntryJ.noiseElementMap[key] = valueI; + + for (auto& [key, valueI] : kfMeasEntryI.designEntryMap) + newDesignEntryMap[key] += valueI * coeff_j * scalar * +1; + for (auto& [key, valueJ] : kfMeasEntryJ.designEntryMap) + newDesignEntryMap[key] += valueJ * coeff_i * scalar * -1; + + for (auto& [key, valueI] : kfMeasEntryI.noiseEntryMap) + newNoiseEntryMap[key] += valueI * coeff_j * scalar * +1; + for (auto& [key, valueJ] : kfMeasEntryJ.noiseEntryMap) + newNoiseEntryMap[key] += valueJ * coeff_i * scalar * -1; + + for (auto& [key, valueI] : kfMeasEntryI.componentsMap) + newComponentsMap[key] += valueI * coeff_j * scalar * +1; + for (auto& [key, valueJ] : kfMeasEntryJ.componentsMap) + newComponentsMap[key] += valueJ * coeff_i * scalar * -1; + + for (auto& [id, value] : kfMeasEntryI.metaDataMap) + { + kfMeasEntryJ.metaDataMap[id + "_alt"] = value; + } + + kfMeasEntryI.metaDataMap["IFLCcombined"] = (void*)true; + kfMeasEntryJ.metaDataMap["IFLCcombined"] = (void*)true; + // kfMeasEntryJ.metaDataMap["explain"] = (void*) true; + + if (useDesignMap) + newDesignEntryMap[ionKey_i] = 0; + else + newNoiseEntryMap[ionKey_i] = 0; + kfMeasEntryJ.designEntryMap = std::move(newDesignEntryMap); + kfMeasEntryJ.noiseEntryMap = std::move(newNoiseEntryMap); + kfMeasEntryJ.componentsMap = std::move(newComponentsMap); + + kfState.removeState(ionKey_i); + break; + } + } + + if (iflcMade == false) + { + BOOST_LOG_TRIVIAL(warning) + << "No IONO_STEC measurements found - 'use_if_combo' requires 'ion_stec' " + "estimation to be enabled in the config file."; + } } /** Replace individual measurements with linear combinations */ -KFMeas makeGFLCs( - KFMeas& kfMeas, - KFState& kfState) +KFMeas makeGFLCs(KFMeas& kfMeas, KFState& kfState) { - int meas = 0; - - vector> tripletList; - decltype(KFMeas::metaDataMaps) newMetaDataMaps; - decltype(KFMeas::obsKeys) newObsKeys; - - for (auto duo : { - Duo{kfState .kfIndexMap, kfMeas.H}, - Duo{kfMeas .noiseIndexMap, kfMeas.H_star} - }) - for (auto& [kfKey, index] : duo.indexMap) { if (kfKey.type != KF::IONO_STEC) continue; - for (int i_2 = 0; i_2 < kfMeas.obsKeys.size(); i_2++) { double coeff_2 = duo.designMatrix(i_2, index); if (coeff_2 == 0) continue; - for (int i_1 = 0; i_1 < i_2; i_1++) { double coeff_1 = duo.designMatrix(i_1, index); if (coeff_1 == 0) continue; - { - if (coeff_1 * coeff_2 < 0) { continue; } - if (coeff_1 == coeff_2) { continue; } //dont combine if it will eliminate the entire measurement - - if (kfMeas.metaDataMaps[i_1]["GFLCcombined"]) { continue; } - if (kfMeas.metaDataMaps[i_2]["GFLCcombined"]) { continue; } - - //these measurements probably both share a common geometry, remove it. - - double scalar = 0.5; - - tripletList.push_back({meas, i_1, +1 * scalar}); - tripletList.push_back({meas, i_2, -1 * scalar}); - meas++; - - auto& obsKey_1 = kfMeas.obsKeys[i_1]; - auto& obsKey_2 = kfMeas.obsKeys[i_2]; - - auto newObsKey = obsKey_2; - - newObsKey.num = 100 * obsKey_1.num - + 1 * obsKey_2.num; - - newObsKey.comment = obsKey_1.comment + "-" - + obsKey_2.comment; - - newObsKeys .push_back(newObsKey); - - //copy metadata into the new measurement - map newMetaData; - for (auto& [id, value] : kfMeas.metaDataMaps[i_1]) { newMetaData[id] = value; } - for (auto& [id, value] : kfMeas.metaDataMaps[i_2]) { newMetaData[id + "_alt"] = value; } - newMetaData["explain"] = (void*) true; - - newMetaDataMaps .push_back(std::move(newMetaData)); - - kfMeas.metaDataMaps[i_1]["GFLCcombined"] = (void*) true; - kfMeas.metaDataMaps[i_2]["GFLCcombined"] = (void*) true; - }}}} - - if (meas == 0) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: No IONO_STEC measurements found - 'use_gf_combo' requires 'iono_stec' estimation to be enabled in the config file."; - } - - for (int i = 0; i < kfMeas.obsKeys.size(); i++) - { - if (kfMeas.metaDataMaps[i]["pseudoObs"] == (void*) false) - { - continue; - } - - //need to keep this measurement even if its not a valid ionospheric one, copy it over - - tripletList.push_back({meas, i, 1}); - meas++; - - newObsKeys .push_back(kfMeas.obsKeys [i]); - newMetaDataMaps .push_back(kfMeas.metaDataMaps [i]); - } - - return KFMeas(kfMeas, std::move(tripletList), std::move(newObsKeys), std::move(newMetaDataMaps)); + int meas = 0; + + vector> tripletList; + decltype(KFMeas::metaDataMaps) newMetaDataMaps; + decltype(KFMeas::obsKeys) newObsKeys; + + for (auto duo : {Duo{kfState.kfIndexMap, kfMeas.H}, Duo{kfMeas.noiseIndexMap, kfMeas.H_star}}) + for (auto& [kfKey, index] : duo.indexMap) + { + if (kfKey.type != KF::IONO_STEC) + continue; + for (int i_2 = 0; i_2 < kfMeas.obsKeys.size(); i_2++) + { + double coeff_2 = duo.designMatrix(i_2, index); + if (coeff_2 == 0) + continue; + for (int i_1 = 0; i_1 < i_2; i_1++) + { + double coeff_1 = duo.designMatrix(i_1, index); + if (coeff_1 == 0) + continue; + { + if (coeff_1 * coeff_2 < 0) + { + continue; + } + if (coeff_1 == coeff_2) + { + continue; + } // dont combine if it will eliminate the entire measurement + + if (kfMeas.metaDataMaps[i_1]["GFLCcombined"]) + { + continue; + } + if (kfMeas.metaDataMaps[i_2]["GFLCcombined"]) + { + continue; + } + + // these measurements probably both share a common geometry, remove it. + + double scalar = 0.5; + + tripletList.push_back({meas, i_1, +1 * scalar}); + tripletList.push_back({meas, i_2, -1 * scalar}); + meas++; + + auto& obsKey_1 = kfMeas.obsKeys[i_1]; + auto& obsKey_2 = kfMeas.obsKeys[i_2]; + + auto newObsKey = obsKey_2; + + newObsKey.num = 100 * obsKey_1.num + 1 * obsKey_2.num; + + newObsKey.comment = obsKey_1.comment + "-" + obsKey_2.comment; + + newObsKeys.push_back(newObsKey); + + // copy metadata into the new measurement + map newMetaData; + for (auto& [id, value] : kfMeas.metaDataMaps[i_1]) + { + newMetaData[id] = value; + } + for (auto& [id, value] : kfMeas.metaDataMaps[i_2]) + { + newMetaData[id + "_alt"] = value; + } + newMetaData["explain"] = (void*)true; + + newMetaDataMaps.push_back(std::move(newMetaData)); + + kfMeas.metaDataMaps[i_1]["GFLCcombined"] = (void*)true; + kfMeas.metaDataMaps[i_2]["GFLCcombined"] = (void*)true; + } + } + } + } + + if (meas == 0) + { + BOOST_LOG_TRIVIAL(warning) + << "No IONO_STEC measurements found - 'use_gf_combo' requires 'iono_stec' " + "estimation to be enabled in the config file."; + } + + for (int i = 0; i < kfMeas.obsKeys.size(); i++) + { + if (kfMeas.metaDataMaps[i]["pseudoObs"] == (void*)false) + { + continue; + } + + // need to keep this measurement even if its not a valid ionospheric one, copy it over + + tripletList.push_back({meas, i, 1}); + meas++; + + newObsKeys.push_back(kfMeas.obsKeys[i]); + newMetaDataMaps.push_back(kfMeas.metaDataMaps[i]); + } + + return KFMeas( + kfMeas, + std::move(tripletList), + std::move(newObsKeys), + std::move(newMetaDataMaps) + ); } - /** Replace individual measurements with linear combinations */ -KFMeas makeRTKLCs1( - KFMeas& kfMeas, - KFState& kfState) +KFMeas makeRTKLCs1(KFMeas& kfMeas, KFState& kfState) { - int meas = 0; - - vector> tripletList; - decltype(KFMeas::metaDataMaps) newMetaDataMaps; - decltype(KFMeas::obsKeys) newObsKeys; - - for (auto duo : { - Duo{kfState .kfIndexMap, kfMeas.H}, - Duo{kfMeas .noiseIndexMap, kfMeas.H_star} - }) - for (auto& [kfKey, index] : duo.indexMap) { if (kfKey.type != KF::PHASE_BIAS && kfKey.type != KF::CODE_BIAS) continue; - for (int i_2 = 0; i_2 < kfMeas.obsKeys.size(); i_2++) { double coeff_2 = duo.designMatrix(i_2, index); if (coeff_2 == 0) continue; - for (int i_1 = 0; i_1 < i_2; i_1++) { double coeff_1 = duo.designMatrix(i_1, index); if (coeff_1 == 0) continue; - { - if (kfMeas.metaDataMaps[i_1]["RTKcombined1"]) { continue; } - if (kfMeas.metaDataMaps[i_2]["RTKcombined1"]) { continue; } - - auto& obsKey1 = kfMeas.obsKeys[i_1]; - auto& obsKey2 = kfMeas.obsKeys[i_2]; - - if (kfKey.str.empty() == false) - { - continue; - } - - if (obsKey1.str == obsKey2.str) - { - continue; - } - - //these measurements both share a common satellite bias, remove it. - - // std::cout << kfKey << " " << kfMeas.obsKeys[i_2] << " " << kfMeas.obsKeys[i_1] << "\n"; - - tripletList.push_back({meas, i_1, +coeff_2}); - tripletList.push_back({meas, i_2, -coeff_1}); - meas++; - - auto& obsKey_1 = kfMeas.obsKeys[i_1]; - auto& obsKey_2 = kfMeas.obsKeys[i_2]; - - auto newObsKey = obsKey_2; - - newObsKey.str.clear(); - - newObsKey.comment = (string) obsKey_1 + "-" - + (string) obsKey_2 + " " - + obsKey_1.comment + "-" - + obsKey_2.comment; - - newObsKeys .push_back(newObsKey); - - //copy metadata into the new measurement - map newMetaData; - for (auto& [id, value] : kfMeas.metaDataMaps[i_1]) { newMetaData[id] = value; } - for (auto& [id, value] : kfMeas.metaDataMaps[i_2]) { newMetaData[id + "_alt"] = value; } - newMetaData["explain"] = (void*) true; - - newMetaDataMaps .push_back(std::move(newMetaData)); - - kfMeas.metaDataMaps[i_1]["RTKcombined"] = (void*) true; - kfMeas.metaDataMaps[i_2]["RTKcombined"] = (void*) true; - }}}} - - for (int i = 0; i < kfMeas.obsKeys.size(); i++) - { - if (kfMeas.metaDataMaps[i]["pseudoObs"] == (void*) false) - { - continue; - } - - //need to keep this measurement even if its not a valid rtk one, copy it over - - tripletList.push_back({meas, i, 1}); - meas++; - - newObsKeys .push_back(kfMeas.obsKeys [i]); - newMetaDataMaps .push_back(kfMeas.metaDataMaps [i]); - } - - return KFMeas(kfMeas, std::move(tripletList), std::move(newObsKeys), std::move(newMetaDataMaps)); + int meas = 0; + + vector> tripletList; + decltype(KFMeas::metaDataMaps) newMetaDataMaps; + decltype(KFMeas::obsKeys) newObsKeys; + + for (auto duo : {Duo{kfState.kfIndexMap, kfMeas.H}, Duo{kfMeas.noiseIndexMap, kfMeas.H_star}}) + for (auto& [kfKey, index] : duo.indexMap) + { + if (kfKey.type != KF::PHASE_BIAS && kfKey.type != KF::CODE_BIAS) + continue; + for (int i_2 = 0; i_2 < kfMeas.obsKeys.size(); i_2++) + { + double coeff_2 = duo.designMatrix(i_2, index); + if (coeff_2 == 0) + continue; + for (int i_1 = 0; i_1 < i_2; i_1++) + { + double coeff_1 = duo.designMatrix(i_1, index); + if (coeff_1 == 0) + continue; + { + if (kfMeas.metaDataMaps[i_1]["RTKcombined1"]) + { + continue; + } + if (kfMeas.metaDataMaps[i_2]["RTKcombined1"]) + { + continue; + } + + auto& obsKey1 = kfMeas.obsKeys[i_1]; + auto& obsKey2 = kfMeas.obsKeys[i_2]; + + if (kfKey.str.empty() == false) + { + continue; + } + + if (obsKey1.str == obsKey2.str) + { + continue; + } + + // these measurements both share a common satellite bias, remove it. + + // std::cout << kfKey << " " << kfMeas.obsKeys[i_2] << " " << + // kfMeas.obsKeys[i_1] << "\n"; + + tripletList.push_back({meas, i_1, +coeff_2}); + tripletList.push_back({meas, i_2, -coeff_1}); + meas++; + + auto& obsKey_1 = kfMeas.obsKeys[i_1]; + auto& obsKey_2 = kfMeas.obsKeys[i_2]; + + auto newObsKey = obsKey_2; + + newObsKey.str.clear(); + + newObsKey.comment = (string)obsKey_1 + "-" + (string)obsKey_2 + " " + + obsKey_1.comment + "-" + obsKey_2.comment; + + newObsKeys.push_back(newObsKey); + + // copy metadata into the new measurement + map newMetaData; + for (auto& [id, value] : kfMeas.metaDataMaps[i_1]) + { + newMetaData[id] = value; + } + for (auto& [id, value] : kfMeas.metaDataMaps[i_2]) + { + newMetaData[id + "_alt"] = value; + } + newMetaData["explain"] = (void*)true; + + newMetaDataMaps.push_back(std::move(newMetaData)); + + kfMeas.metaDataMaps[i_1]["RTKcombined"] = (void*)true; + kfMeas.metaDataMaps[i_2]["RTKcombined"] = (void*)true; + } + } + } + } + + for (int i = 0; i < kfMeas.obsKeys.size(); i++) + { + if (kfMeas.metaDataMaps[i]["pseudoObs"] == (void*)false) + { + continue; + } + + // need to keep this measurement even if its not a valid rtk one, copy it over + + tripletList.push_back({meas, i, 1}); + meas++; + + newObsKeys.push_back(kfMeas.obsKeys[i]); + newMetaDataMaps.push_back(kfMeas.metaDataMaps[i]); + } + + return KFMeas( + kfMeas, + std::move(tripletList), + std::move(newObsKeys), + std::move(newMetaDataMaps) + ); } - /** Replace individual measurements with linear combinations */ -KFMeas makeRTKLCs2( - KFMeas& kfMeas, - KFState& kfState) +KFMeas makeRTKLCs2(KFMeas& kfMeas, KFState& kfState) { - int meas = 0; - - vector> tripletList; - decltype(KFMeas::metaDataMaps) newMetaDataMaps; - decltype(KFMeas::obsKeys) newObsKeys; - - for (auto duo : { - Duo{kfState .kfIndexMap, kfMeas.H}, - Duo{kfMeas .noiseIndexMap, kfMeas.H_star} - }) - for (auto& [kfKey, index] : duo.indexMap) { if (kfKey.type != KF::PHASE_BIAS && kfKey.type != KF::CODE_BIAS) continue; - for (int i_2 = 0; i_2 < kfMeas.obsKeys.size(); i_2++) { double coeff_2 = duo.designMatrix(i_2, index); if (coeff_2 == 0) continue; - for (int i_1 = 0; i_1 < i_2; i_1++) { double coeff_1 = duo.designMatrix(i_1, index); if (coeff_1 == 0) continue; - { - // if (kfMeas.metaDataMaps[i_1]["RTKcombined2"]) { continue; } - if (kfMeas.metaDataMaps[i_2]["RTKcombined2"]) { continue; } - - auto& obsKey1 = kfMeas.obsKeys[i_1]; - auto& obsKey2 = kfMeas.obsKeys[i_2]; - - if (kfKey.str.empty()) - { - continue; - } - - if (obsKey1.Sat == obsKey2.Sat) - { - continue; - } - - //these measurements both share a common receiver bias, remove it. - - tripletList.push_back({meas, i_1, +coeff_2}); - tripletList.push_back({meas, i_2, -coeff_1}); - meas++; - - auto& obsKey_1 = kfMeas.obsKeys[i_1]; - auto& obsKey_2 = kfMeas.obsKeys[i_2]; - - auto newObsKey = obsKey_2; - newObsKey.Sat = SatSys(); - - newObsKey.comment = (string) obsKey_1 + "-" - + (string) obsKey_2 + " " - + obsKey_1.comment + "-" - + obsKey_2.comment; - - newObsKeys .push_back(newObsKey); - - //copy metadata into the new measurement - map newMetaData; - for (auto& [id, value] : kfMeas.metaDataMaps[i_1]) { newMetaData[id] = value; } - for (auto& [id, value] : kfMeas.metaDataMaps[i_2]) { newMetaData[id + "_alt"] = value; } - newMetaData["explain"] = (void*) true; - - newMetaDataMaps .push_back(std::move(newMetaData)); - - kfMeas.metaDataMaps[i_1]["RTKcombined2"] = (void*) true; - kfMeas.metaDataMaps[i_2]["RTKcombined2"] = (void*) true; - }}}} - - for (int i = 0; i < kfMeas.obsKeys.size(); i++) - { - if (kfMeas.metaDataMaps[i]["pseudoObs"] == (void*) false) - { - continue; - } - - //need to keep this measurement even if its not a valid rtk one, copy it over - - tripletList.push_back({meas, i, 1}); - meas++; - - newObsKeys .push_back(kfMeas.obsKeys [i]); - newMetaDataMaps .push_back(kfMeas.metaDataMaps [i]); - } + int meas = 0; + + vector> tripletList; + decltype(KFMeas::metaDataMaps) newMetaDataMaps; + decltype(KFMeas::obsKeys) newObsKeys; + + for (auto duo : {Duo{kfState.kfIndexMap, kfMeas.H}, Duo{kfMeas.noiseIndexMap, kfMeas.H_star}}) + for (auto& [kfKey, index] : duo.indexMap) + { + if (kfKey.type != KF::PHASE_BIAS && kfKey.type != KF::CODE_BIAS) + continue; + for (int i_2 = 0; i_2 < kfMeas.obsKeys.size(); i_2++) + { + double coeff_2 = duo.designMatrix(i_2, index); + if (coeff_2 == 0) + continue; + for (int i_1 = 0; i_1 < i_2; i_1++) + { + double coeff_1 = duo.designMatrix(i_1, index); + if (coeff_1 == 0) + continue; + { + // if (kfMeas.metaDataMaps[i_1]["RTKcombined2"]) { continue; } + if (kfMeas.metaDataMaps[i_2]["RTKcombined2"]) + { + continue; + } + + auto& obsKey1 = kfMeas.obsKeys[i_1]; + auto& obsKey2 = kfMeas.obsKeys[i_2]; + + if (kfKey.str.empty()) + { + continue; + } + + if (obsKey1.Sat == obsKey2.Sat) + { + continue; + } + + // these measurements both share a common receiver bias, remove it. + + tripletList.push_back({meas, i_1, +coeff_2}); + tripletList.push_back({meas, i_2, -coeff_1}); + meas++; + + auto& obsKey_1 = kfMeas.obsKeys[i_1]; + auto& obsKey_2 = kfMeas.obsKeys[i_2]; + + auto newObsKey = obsKey_2; + newObsKey.Sat = SatSys(); + + newObsKey.comment = (string)obsKey_1 + "-" + (string)obsKey_2 + " " + + obsKey_1.comment + "-" + obsKey_2.comment; + + newObsKeys.push_back(newObsKey); + + // copy metadata into the new measurement + map newMetaData; + for (auto& [id, value] : kfMeas.metaDataMaps[i_1]) + { + newMetaData[id] = value; + } + for (auto& [id, value] : kfMeas.metaDataMaps[i_2]) + { + newMetaData[id + "_alt"] = value; + } + newMetaData["explain"] = (void*)true; + + newMetaDataMaps.push_back(std::move(newMetaData)); + + kfMeas.metaDataMaps[i_1]["RTKcombined2"] = (void*)true; + kfMeas.metaDataMaps[i_2]["RTKcombined2"] = (void*)true; + } + } + } + } + + for (int i = 0; i < kfMeas.obsKeys.size(); i++) + { + if (kfMeas.metaDataMaps[i]["pseudoObs"] == (void*)false) + { + continue; + } + + // need to keep this measurement even if its not a valid rtk one, copy it over + + tripletList.push_back({meas, i, 1}); + meas++; + + newObsKeys.push_back(kfMeas.obsKeys[i]); + newMetaDataMaps.push_back(kfMeas.metaDataMaps[i]); + } + + return KFMeas( + kfMeas, + std::move(tripletList), + std::move(newObsKeys), + std::move(newMetaDataMaps) + ); +} - return KFMeas(kfMeas, std::move(tripletList), std::move(newObsKeys), std::move(newMetaDataMaps)); +void mergeCorrelated(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryList) +{ + // count the number of measurements that refer to a given state in the design entry map + map refCountMap; + + static map dontMergeMap; + + for (auto& kfMeasEntry : kfMeasEntryList) + for (auto& [key, value] : kfMeasEntry.designEntryMap) + { + if (kfMeasEntry.valid == false) + { + continue; + } + + if (value == 0) + { + continue; + } + + refCountMap[key]++; + } + + for (auto& kfMeasEntry : kfMeasEntryList) + { + if (kfMeasEntry.valid == false) + { + continue; + } + + map correlatedMap; + + for (auto& [key, value] : kfMeasEntry.designEntryMap) + { + if (value == 0) + { + continue; + } + + if (dontMergeMap[key]) + { + continue; + } + + if (refCountMap[key] != 1) + { + dontMergeMap[key] = true; + continue; + } + + // this key appears in this measurement only. + + correlatedMap[key] = value; + } + + if (correlatedMap.size() < 2) + { + continue; + } + + stringstream ss; + + ss << "\n" + << "Combining "; + + int scalar = 1; + + KFKey bigKey; + for (auto& [key, value] : correlatedMap) + { + if (bigKey.type == KF::NONE) + { + bigKey = key; + } + else + { + scalar *= 100; + bigKey.num += scalar * key.num; + bigKey.comment += (string) ", " + key.comment; + } + + ss << "\t[" << key.commaString() << "]: \t" << value << ","; + + kfMeasEntry.designEntryMap[key] = 0; + } + + // add the new key and its design entry + + ss << "\tto create " << bigKey; + + try + { + bool stateCreated = kfState.addPseudoState(bigKey, correlatedMap); + + if (stateCreated) + { + trace << ss.str(); + } + + kfMeasEntry.designEntryMap[bigKey] = 1; + } + catch (...) + { + // trying to re-merge can alrady merged state + BOOST_LOG_TRIVIAL(warning) << "Removing measurement " << kfMeasEntry.obsKey; + kfMeasEntry.valid = false; + } + } } -/** Prepare receiver clocks using spp values to minimise pre-fit residuals +/** Prepare receiver clocks using SPP values to minimise pre-fit residuals */ void updateRecClocks( - Trace& trace, ///< Trace to output to - ReceiverMap& receiverMap, ///< List of stations containing observations for this epoch - KFState& kfState) ///< Kalman filter object containing the network state parameters + Trace& trace, ///< Trace to output to + ReceiverMap& receiverMap, ///< List of receivers containing observations for this epoch + KFState& kfState ///< Kalman filter object containing the network state parameters +) { - if (acsConfig.adjust_rec_clocks_by_spp == false) - { - return; - } - - for (auto& [id, rec] : receiverMap) - { - if (rec.isPseudoRec) - { - continue; - } - - auto trace = getTraceFile(rec); - auto& recOpts = acsConfig.getRecOpts(id); - - InitialState init = initialStateFromConfig(recOpts.clk); - - if (init.estimate == false) - { - continue; - } - - KFKey clkKey; - clkKey.type = KF::REC_CLOCK; - clkKey.str = id; - clkKey.rec_ptr = &rec; - - double C_dtRecAdj = rec.sol.dtRec_m[E_Sys::GPS] - - rec.sol.dtRec_m_pppp_old[E_Sys::GPS]; - - //for non-first epochs, and if enabled, do rounding - if ( rec.sol.dtRec_m_pppp_old[E_Sys::GPS] - && acsConfig.adjust_clocks_for_jumps_only) - { - const double scalar = 1000 * 2 / CLIGHT; - - double halfMilliseconds = C_dtRecAdj * scalar; - - C_dtRecAdj = round(halfMilliseconds) / scalar; - - if (C_dtRecAdj) - { - trace << "\n" - << "Jump of " << halfMilliseconds * 0.5 << "ms found, rounding"; - } - } - - // if (C_dtRecAdj) - { - trace << "\n" - << "Adjusting " << clkKey.str - << " clock by " << C_dtRecAdj; - } - - rec.sol.dtRec_m_pppp_old[E_Sys::GPS] = rec.sol.dtRec_m[E_Sys::GPS]; - - kfState.setKFTrans(clkKey, KFState::oneKey, C_dtRecAdj, init); - } + if (acsConfig.adjust_rec_clocks_by_spp == false) + { + return; + } + + for (auto& [id, rec] : receiverMap) + { + auto trace = getTraceFile(rec); + + if (rec.isPseudoRec) + { + continue; + } + + if (rec.sol.status != E_Solution::SINGLE) + { + trace << "\n" + << "Receiver clock of " << id << " won't be adjusted due to bad SPP"; + continue; + } + + auto& recOpts = acsConfig.getRecOpts(id); + + InitialState clkInit = initialStateFromConfig(recOpts.clk); + + if (clkInit.estimate == false) + { + continue; + } + + KFKey clkKey; + clkKey.type = KF::REC_CLOCK; + clkKey.str = id; + + double recClk_m = 0; + E_Source found = kfState.getKFValue( + clkKey, + recClk_m + ); // Get filtered rec clock of last epoch (not updated yet) + + if (found == E_Source::KALMAN && rec.sol.clkAdjustReady) + { + KFKey rateKey = clkKey; + rateKey.type = KF::REC_CLOCK_RATE; + + double clkRate = 0; + found = kfState.getKFValue(rateKey, clkRate); + + double dt = (tsync - kfState.time).to_double(); + recClk_m += clkRate * dt; // Expected rec clock prediction + double recClkAdj = rec.sol.sppClk + rec.sol.sppPppClkOffset - + recClk_m; // Adjust rec clock prediction by SPP clock change + + // Do rounding if only adjust receiver clock jumps (i.e. integer times of + // half-milliseconds) + if (acsConfig.adjust_clocks_for_jumps_only) + { + const double scalar = 1000 * 2 / CLIGHT; + + double halfMilliseconds = recClkAdj * scalar; + + recClkAdj = round(halfMilliseconds) / scalar; + + if (recClkAdj) + { + trace << "\n" + << "Jump of " << halfMilliseconds * 0.5 << "ms found, rounding"; + } + } + + trace << "\n" + << tsync << "\tAdjusting " << clkKey.str << " clock by " << recClkAdj + << "\t- Pre-adjustment: " << recClk_m + << "\t- Post-adjustment: " << recClk_m + recClkAdj; + + if (recClkAdj) + { + kfState.setKFTrans(clkKey, KFState::oneKey, recClkAdj, clkInit); + + InitialState rateInit = initialStateFromConfig(recOpts.clk_rate); + if (rateInit.estimate && found == E_Source::KALMAN) + { + double clkRateAjd = recClkAdj / dt; + kfState.setKFTrans(rateKey, KFState::oneKey, clkRateAjd, rateInit); + } + } + } + } } /** Prepare stec values clocks to minimise residuals to klobuchar model */ void updateAvgIonosphere( - Trace& trace, ///< Trace to output to - GTime time, ///< Time - KFState& kfState) ///< Kalman filter object containing the network state parameters + Trace& trace, ///< Trace to output to + GTime time, ///< Time + KFState& kfState ///< Kalman filter object containing the network state parameters +) { - if (acsConfig.minimise_ionosphere_offsets == false) - { - return; - } - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::IONO_STEC) - { - continue; - } - - if (key.rec_ptr == nullptr) - { - continue; - } - - auto& rec = *key.rec_ptr; - - auto& satStat = rec.satStatMap[key.Sat]; - auto& satNav = nav.satNavMap[key.Sat]; - auto& recOpts = acsConfig.getRecOpts(rec.id); - - double diono = 0; - double dummy = 0; - double dummy2 = 0; - bool pass = ionoModel(time, rec.pos, satStat, E_IonoMapFn::KLOBUCHAR, E_IonoMode::BROADCAST, 0, dummy, diono, dummy2); - if (pass == false) - { - continue; - } - - double alpha = 40.3e16 / SQR(CLIGHT / genericWavelength[F1]); - - double ionosphereStec = diono / alpha; - - //update the mu value but dont use the state thing - it will re-add it after its deleted - // kfState.addKFState(key, init); - kfState.gaussMarkovMuMap[key] = ionosphereStec; - } + if (acsConfig.minimise_ionosphere_offsets == false) + { + return; + } + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::IONO_STEC) + { + continue; + } + + if (key.rec_ptr == nullptr) + { + continue; + } + + auto& rec = *key.rec_ptr; + + auto& satStat = rec.satStatMap[key.Sat]; + + double diono = 0; + double dummy = 0; + double dummy2 = 0; + bool pass = ionoModel( + time, + rec.pos, + satStat, + E_IonoMapFn::KLOBUCHAR, + E_IonoMode::BROADCAST, + 0, + dummy, + diono, + dummy2 + ); + if (pass == false) + { + continue; + } + + double alpha = 40.3e16 / SQR(CLIGHT / genericWavelength[F1]); + + double ionosphereStec = diono / alpha; + + // update the mu value but dont use the state thing - it will re-add it after its deleted + // kfState.addKFState(key, init); + kfState.gaussMarkovMuMap[key] = ionosphereStec; + } } -/** Prepare Satellite clocks to minimise residuals to broadcast clocks +/** Prepare Satellite clocks to minimise residuals to broadcast orbits + * Provide a weak tiedown using mu values, which the state will attempt to exponentially decay + * toward using the configured tau value in stateTransition */ void updateAvgOrbits( - Trace& trace, ///< Trace to output to - GTime time, ///< Time - KFState& kfState) ///< Kalman filter object containing the network state parameters + Trace& trace, ///< Trace to output to + GTime time, ///< Time + KFState& kfState ///< Kalman filter object containing the network state parameters +) { - if (acsConfig.minimise_sat_orbit_offsets == false) - { - return; - } + if (acsConfig.minimise_sat_orbit_offsets == false) + { + return; + } - ERPValues erpv = getErp(nav.erp, time); + ERPValues erpv = getErp(nav.erp, time); - FrameSwapper frameSwapper(time, erpv); + FrameSwapper frameSwapper(time, erpv); - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( key.type != KF::ORBIT - ||key.num != 0) - { - continue; - } + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::ORBIT || key.num != 0) + { + continue; + } - SatPos satPos; - satPos.Sat = key.Sat; - satPos.satNav_ptr = &nav.satNavMap[key.Sat]; + SatPos satPos; + satPos.Sat = key.Sat; + satPos.satNav_ptr = &nav.satNavMap[key.Sat]; - bool pass = satPosBroadcast(trace, time, time, satPos, nav); - if (pass == false) - { - continue; - } + bool pass = satPosBroadcast(trace, time, time, satPos, nav); + if (pass == false) + { + continue; + } - auto& satOpts = acsConfig.getSatOpts(key.Sat); + auto& satOpts = acsConfig.getSatOpts(key.Sat); - satPos.rSatEci0 = frameSwapper(satPos.rSatCom); + satPos.rSatEci0 = frameSwapper(satPos.rSatCom); - for (int i = 0; i < 3; i++) - { - InitialState init = initialStateFromConfig(satOpts.orbit, i); + for (int i = 0; i < 3; i++) + { + InitialState init = initialStateFromConfig(satOpts.orbit, i); - init.mu = satPos.rSatEci0(i); + init.mu = satPos.rSatEci0(i); - //update the mu value - kfState.addKFState(key, init); - } - } + // update the mu value, + kfState.addKFState(key, init); + } + } } -/** Prepare Satellite clocks to minimise residuals to broadcast clocks +/** Prepare satellite clocks to minimise residuals to broadcast clocks. + * Provide a weak tiedown using mu values, which the state will attempt to exponentially decay + * toward using the configured tau value in stateTransition */ void updateAvgClocks( - Trace& trace, ///< Trace to output to - GTime time, ///< Time - KFState& kfState) ///< Kalman filter object containing the network state parameters + Trace& trace, ///< Trace to output to + GTime time, ///< Time + KFState& kfState ///< Kalman filter object containing the network state parameters +) { - if (acsConfig.minimise_sat_clock_offsets == false) - { - return; - } - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::SAT_CLOCK) - { - continue; - } - - SatPos satPos; - satPos.Sat = key.Sat; - satPos.satNav_ptr = &nav.satNavMap[key.Sat]; - - bool pass = satClkBroadcast(trace, time, time, satPos, nav); - if (pass == false) - { - continue; - } - - auto& satOpts = acsConfig.getSatOpts(key.Sat); - - InitialState init = initialStateFromConfig(satOpts.clk); - - init.mu = satPos.satClk * CLIGHT; - - //update the mu value - kfState.addKFState(key, init); - } + if (acsConfig.minimise_sat_clock_offsets.enable == false) + { + return; + } + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::SAT_CLOCK) + { + continue; + } + + auto& Sat = key.Sat; + SatPos satPosBrdc; + SatPos satPosKf; + satPosBrdc.Sat = Sat; + satPosKf.Sat = Sat; + + bool pass = satClkBroadcast(trace, time, time, satPosBrdc, nav); + if (pass == false) + { + continue; + } + + pass = satClkKalman(trace, time, satPosKf, &kfState); + if (pass == false) + { + continue; + } + + auto& satOpts = acsConfig.getSatOpts(Sat); + + // Restore tau value from config + InitialState init = initialStateFromConfig(satOpts.clk); + + double satClkBrdc = satPosBrdc.satClk * CLIGHT; + double satClkKf = satPosKf.satClk * CLIGHT; + double satClkDiff = satClkBrdc - satClkKf; + + if (abs(satClkDiff) > acsConfig.minimise_sat_clock_offsets.max_offset) + { + // Exclude this satellite clock from alignment if diff between estimated and broadcast + // satellite clock offsets > 10 m + trace << "\nLarge clock diff > " << acsConfig.minimise_sat_clock_offsets.max_offset + << " (m), excluding satellite from braodcast clock alignment:" << "\tSat: " + << Sat.id() << "\tBrdc sat clock (m): " << satClkBrdc + << "\tKf sat clock (m): " << satClkKf << "\tDiff: " << satClkDiff; + + init.tau = -1; // Disable FOGM so that this satellite clock won't be tied down to old + // mu value // Eugene: What if the clock doesn't jump + } + else + { + // Align this satellite clock with broadcast ephemeris + init.mu = satClkBrdc; + } + + // update tau & mu values + kfState.addKFState(key, init); + } } -KFState propagateUncertainty( - Trace& trace, - KFState& kfState) +KFState propagateUncertainty(Trace& trace, KFState& kfState) { - MatrixXd F1; - - KFMeasEntryList kfMeasEntryList; - - map kfMeasEntryMap; - - string pivotRec = acsConfig.pivot_receiver; - -// if (0) - for (auto& [key, index] : kfState.kfIndexMap) - { - if ( key.type != KF::REC_CLOCK - &&key.type != KF::SAT_CLOCK - &&key.type != KF::REC_CLOCK_RATE - &&key.type != KF::SAT_CLOCK_RATE) - { - continue; - } - - if ( pivotRec == "" - &&key.type == KF::REC_CLOCK - &&key.num == 0) - { - pivotRec = key.str; - } - - auto subKey = key; - subKey.num = 0; - - auto& kfMeasEntry = kfMeasEntryMap[subKey]; - - kfMeasEntry.addDsgnEntry(key, +1); - - string newComment = kfMeasEntry.obsKey.comment; - if (newComment.empty()) - { - newComment += "=>"; - } - newComment += " + [" + key.commaString() + "]"; - - kfMeasEntry.obsKey = subKey; - - kfMeasEntry.obsKey.comment = newComment; - } - - - if (0) - for (int i = 1; i < kfState.kfIndexMap.size(); i++) - for (int j = 0; j < i; j++) - { - auto iti = kfState.kfIndexMap.begin(); - auto itj = kfState.kfIndexMap.begin(); - - std::advance(iti, i); - std::advance(itj, j); - - auto& [keyi, indexi] = *iti; - auto& [keyj, indexj] = *itj; - - if (keyi.type != KF::AMBIGUITY) - { - continue; - } - - if (keyj.type != KF::AMBIGUITY) - { - continue; - } - - if (keyi.Sat != keyj.Sat) - { - continue; - } - - if (keyi.str != keyj.str) - { - continue; - } - - { - // auto& kfMeasEntry = kfMeasEntryMap[key.str + key.Sat.id()]; - // kfMeasEntry.addDsgnEntry(keyi, -60.0/600); - // kfMeasEntry.addDsgnEntry(keyj, +77.0/600); - // - // kfMeasEntry.obsKey.str = keyi.str + " " + keyi.Sat.id() + "C"; - // } - // { - // auto& kfMeasEntry = kfMeasEntryMap[key.str + key.Sat.id()]; - // kfMeasEntry.addDsgnEntry(keyi, +60); - // kfMeasEntry.addDsgnEntry(keyj, -77); - // - // kfMeasEntry.obsKey.str = keyi.str + " " + keyi.Sat.id() + "D"; - } - } - - for (auto& [id, entry] : kfMeasEntryMap) - { - if (pivotRec != "NO_PIVOT") - for (auto& [pivotKey, pivotEntry] : kfState.kfIndexMap) - { - if ( pivotKey.type != KF::REC_CLOCK - &&pivotKey.type != KF::REC_CLOCK_RATE) - { - continue; - } - - if (pivotKey.str != pivotRec) - { - continue; - } - - if ( pivotKey.type == KF::REC_CLOCK - && id.type != KF::REC_CLOCK - && id.type != KF::SAT_CLOCK) - { - continue; - } - - entry.addDsgnEntry(pivotKey, -1); - - entry.obsKey.comment += " - [" + pivotKey.commaString() + "]"; - } - - kfMeasEntryList.push_back(entry); - } - - KFState propagatedState; - - if (kfMeasEntryList.empty()) - { - return propagatedState; - } - - KFMeas kfMeas(kfState, kfMeasEntryList); - - propagatedState.time = kfState.time; - propagatedState.P = kfMeas.H * kfState.P * kfMeas.H.transpose(); - propagatedState.x = kfMeas.H * kfState.x; - propagatedState.dx = kfMeas.H * kfState.dx; - - propagatedState.kfIndexMap.clear(); - - int i = 0; - for (auto& obsKey : kfMeas.obsKeys) - { - propagatedState.kfIndexMap[obsKey] = i; - i++; - } - - return propagatedState; + MatrixXd F1; + + KFMeasEntryList kfMeasEntryList; + + map kfMeasEntryMap; + + string& reference_clock = acsConfig.reference_clock; + string& reference_bias = acsConfig.reference_bias; + + static int pivotNum = 0; + + // add measEntries for each suitable state in the map. + // if (0) + for (auto& [key, index] : kfState.kfIndexMap) + { + if ((key.type < KF::BEGIN_CLOCK_STATES || key.type > KF::END_CLOCK_STATES) && + (key.type != KF::PHASE_BIAS)) + { + continue; + } + + // use first receiver clock for auto reference + if (reference_clock == "" && key.type == KF::REC_CLOCK && key.num == 0) + { + reference_clock = key.str; + } + + // use first satellite bias for auto reference + if (reference_bias == "" && key.type == KF::PHASE_BIAS && key.str.empty()) + { + reference_bias = key.Sat.id(); + pivotNum = key.num; + } + + auto subKey = key; + if (key.type > KF::BEGIN_CLOCK_STATES && key.type < KF::END_CLOCK_STATES) + { + // accumulate all clock nums together + subKey.num = 0; + } + + auto& kfMeasEntry = kfMeasEntryMap[subKey]; + + kfMeasEntry.addDsgnEntry(key, +1); + + string newComment = kfMeasEntry.obsKey.comment; + if (newComment.empty()) + { + newComment += "=>"; + } + newComment += " + [" + key.commaString() + "]"; + + kfMeasEntry.obsKey = subKey; + + kfMeasEntry.obsKey.comment = newComment; + } + + // add the right hand side for all the clock states + for (auto& [id, entry] : kfMeasEntryMap) + { + if (id.type != KF::REC_CLOCK && id.type != KF::SAT_CLOCK) + { + continue; + } + + if (reference_clock != "NO_REFERENCE") + for (auto& [pivotKey, pivotEntry] : kfState.kfIndexMap) + { + if (pivotKey.type != KF::REC_CLOCK) + { + continue; + } + + if (pivotKey.str != reference_clock) + { + continue; + } + + entry.addDsgnEntry(pivotKey, -1); + + entry.obsKey.comment += " - [" + pivotKey.commaString() + "]"; + } + + kfMeasEntryList.push_back(entry); + } + + // add the right hand side for all the bias states + for (auto& [id, entry] : kfMeasEntryMap) + { + if (id.type != KF::PHASE_BIAS) + { + continue; + } + + if (reference_bias != "NO_REFERENCE") + for (auto& [pivotKey, pivotEntry] : kfState.kfIndexMap) + { + if (pivotKey.type != KF::PHASE_BIAS) + { + continue; + } + + if (pivotKey.Sat.id() != reference_bias || pivotKey.num != pivotNum) + { + continue; + } + + entry.addDsgnEntry(pivotKey, -1); + + entry.obsKey.comment += " - [" + pivotKey.commaString() + "]"; + } + + kfMeasEntryList.push_back(entry); + } + + KFState propagatedState; + + if (kfMeasEntryList.empty()) + { + return propagatedState; + } + + KFMeas kfMeas(kfState, kfMeasEntryList); + + propagatedState.time = kfState.time; + propagatedState.P = kfMeas.H * kfState.P * kfMeas.H.transpose(); + propagatedState.x = kfMeas.H * kfState.x; + propagatedState.dx = kfMeas.H * kfState.dx; + + propagatedState.kfIndexMap.clear(); + + int i = 0; + for (auto& obsKey : kfMeas.obsKeys) + { + propagatedState.kfIndexMap[obsKey] = i; + i++; + } + + return propagatedState; } 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 +) { - if (acsConfig.pppOpts.receiver_chunking == false) - { - return; - } - - map begH; - map endH; - map begX; - map endX; - - for (auto& [kfKey, x] : kfState.kfIndexMap) - { - if (kfKey.type == KF::ONE) - { - continue; - } - - string chunkId = kfKey.str; - - if (begX.find(chunkId) == begX.end()) { begX[chunkId] = x; } - { endX[chunkId] = x; } - - - for (int h = 0; h < kfMeas.H.rows(); h++) - if (kfMeas.H(h, x)) - { - if (begH.find(chunkId) == begH.end() || h < begH[chunkId]) { begH[chunkId] = h; } - if ( h > endH[chunkId]) { endH[chunkId] = h; } - } - } - - bool chunkX = true; - bool chunkH = true; - - //check for overlapping entries - for (auto& [str1, beg1] : begH) - for (auto& [str2, beg2] : begH) - { - auto& end1 = endH[str1]; - - if (str1 == str2) - { - continue; - } - - auto& end2 = endH[str2]; - - if ( (beg2 >= beg1 && beg2 <= end1) // 2 starts in the middle of beg,end - ||(end2 >= beg1 && end2 <= end1)) // 2 ends in the middle of beg,end - { - chunkH = false; - } - } - - if (chunkH == false) - { - return; - } - - for (auto& [str, dummy] : begH) - { - FilterChunk filterChunk; - - if (str.empty() == false) - { - auto& rec = receiverMap[str]; - - traceList[str] = getTraceFile(rec); - - filterChunk.id = str; - filterChunk.trace_ptr = &traceList[str]; - } - else - { - filterChunk.trace_ptr = &trace; - } - -// std::cout << "\n" << "Chunk : " << str << " " << begH[str] << " " << endH[str] << " " << begX[str] << " " << endX[str]; - if (chunkH) { filterChunk.begH = begH[str]; filterChunk.numH = endH[str] - begH[str] + 1; } - else { filterChunk.begH = 0; filterChunk.numH = kfMeas.H.rows(); } - if (chunkX) { filterChunk.begX = begX[str]; filterChunk.numX = endX[str] - begX[str] + 1; } - else { filterChunk.begX = 0; filterChunk.numX = kfState.x.rows(); } - - filterChunkMap[str] = filterChunk; - } - - //check all states are filtered despite not needing to be (required for rts) - //assume all chunks are in receiver order == state order - { - int x = 0; - map filterChunkExtras; - - auto addChunk = [&](int lastX, int nextX) - { - FilterChunk filterChunk; - filterChunk.id = "dummy " + std::to_string(x); - filterChunk.begX = lastX + 1; - filterChunk.numX = nextX - lastX - 1; - filterChunk.begH = 0; - filterChunk.numH = 0; - - filterChunkExtras[filterChunk.id] = filterChunk; - }; - - for (auto& [str, filterChunk] : filterChunkMap) - { - if (filterChunk.begX != x + 1) - { - addChunk(x, filterChunk.begX); - } - - x = filterChunk.begX + filterChunk.numX - 1; - } - - if (x != kfState.x.rows() - 1) - { - addChunk(x, kfState.x.rows()); - } - - for (auto& [id, fc] : filterChunkExtras) - { - filterChunkMap[id] = std::move(fc); - } - } - - if (acsConfig.pppOpts.chunk_size) - { - map newFilterChunkMap; - - FilterChunk bigFilterChunk; - - int chunks = (double) filterChunkMap.size() / acsConfig.pppOpts.chunk_size + 0.5; - int chunkTarget = -1; - if (chunks) - chunkTarget = (double) filterChunkMap.size() / chunks + 0.5; - - int count = 0; - for (auto& [id, filterChunk] : filterChunkMap) - { - if (count == 0) - { - bigFilterChunk = filterChunk; - bigFilterChunk.trace_ptr = &trace; - } - else - { - bigFilterChunk.id += "-"; - bigFilterChunk.id += filterChunk.id; - } - - int chunkEndX = filterChunk.begX + filterChunk.numX - 1; - int chunkEndH = filterChunk.begH + filterChunk.numH - 1; - - if (bigFilterChunk.begX > filterChunk.begX) { bigFilterChunk.begX = filterChunk.begX; } - if (bigFilterChunk.begH > filterChunk.begH) { bigFilterChunk.begH = filterChunk.begH; } - - if (bigFilterChunk.begX + bigFilterChunk.numX < filterChunk.begX + filterChunk.numX) { bigFilterChunk.numX = chunkEndX - bigFilterChunk.begX + 1; } - if (bigFilterChunk.begH + bigFilterChunk.numH < filterChunk.begH + filterChunk.numH) { bigFilterChunk.numH = chunkEndH - bigFilterChunk.begH + 1; } - - count++; - - if (count == chunkTarget) - { - newFilterChunkMap[bigFilterChunk.id] = bigFilterChunk; - count = 0; - } - } - - if (count) - { - newFilterChunkMap[bigFilterChunk.id] = bigFilterChunk; - } - - filterChunkMap = std::move(newFilterChunkMap); - } + filterChunkMap.clear(); + // Handle case when chunking is disabled + if (acsConfig.pppOpts.receiver_chunking == false) + { + ///@todo temporary fix to double printing. removed the concept of "ALL" chunk + return; + + FilterChunk filterChunk; + filterChunk.id = "ALL"; + filterChunk.begH = 0; + filterChunk.numH = kfMeas.H.rows(); + filterChunk.begX = 0; + filterChunk.numX = kfState.x.rows(); + filterChunk.trace_ptr = &trace; + filterChunkMap["ALL"] = filterChunk; + return; + } + + map begH; + map endH; + map begX; + map endX; + + // Find chunks based on state variables + for (auto& [kfKey, x] : kfState.kfIndexMap) + { + if (kfKey.type == KF::ONE) + { + continue; + } + + string chunkId = kfKey.str; + + // Initialize or update X bounds for ALL receivers with states + if (begX.find(chunkId) == begX.end()) + { + begX[chunkId] = x; + } + { + endX[chunkId] = x; + } + + // Find corresponding measurements in H matrix + for (int h = 0; h < kfMeas.H.rows(); h++) + { + // std::cout << "Checking measurement " << h << " for chunkId " << chunkId << " : " << + // kfMeas.H(h,x) << + // "\n"; + if (kfMeas.H(h, x)) + { + // Fixed initialization bug: check if chunkId exists before accessing + if (begH.find(chunkId) == begH.end() || h < begH[chunkId]) + { + begH[chunkId] = h; + } + if (endH.find(chunkId) == endH.end() || h > endH[chunkId]) + { + endH[chunkId] = h; + } + } + } + } + + // Early return if no chunks were found + if (begX.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "No filter chunks found - creating default chunk"; + FilterChunk filterChunk; + filterChunk.id = "DEFAULT"; + filterChunk.begH = 0; + filterChunk.numH = kfMeas.H.rows(); + filterChunk.begX = 0; + filterChunk.numX = kfState.x.rows(); + filterChunk.trace_ptr = &trace; + filterChunkMap["DEFAULT"] = filterChunk; + return; + } + + bool chunkH = true; + bool chunkX = true; + + // Check for overlapping entries in both H and X dimensions + for (auto& [str1, beg1] : begX) // Changed from begH to begX + for (auto& [str2, beg2] : begX) // Changed from begH to begX + { + if (str1 >= str2) + continue; // Avoid double-checking and self-comparison + + auto& end1 = endX[str1]; + auto& end2 = endX[str2]; + + // Check X overlap + if ((beg2 <= end1 && end2 >= beg1)) + { + chunkX = false; + BOOST_LOG_TRIVIAL(debug) + << "X overlap detected between chunks " << str1 << " and " << str2; + } + + // Check H overlap if both chunks exist in H maps + if (begH.find(str1) != begH.end() && begH.find(str2) != begH.end()) + { + auto& begH1 = begH[str1]; + auto& endH1 = endH[str1]; + auto& begH2 = begH[str2]; + auto& endH2 = endH[str2]; + + if ((begH2 <= endH1 && endH2 >= begH1)) + { + chunkH = false; + BOOST_LOG_TRIVIAL(debug) + << "H overlap detected between chunks " << str1 << " and " << str2; + } + } + } + + // If overlaps detected, disable chunking + if (chunkX == false || chunkH == false) + { + BOOST_LOG_TRIVIAL(warning) << "Overlaps detected - chunking disabled"; + return; + } + + // Create filter chunks - iterate over ALL receivers with states + for (auto& [str, dummy] : begX) // Changed from begH to begX + { + FilterChunk filterChunk; + + // Set up trace pointer + if (str.empty() == false) + { + auto recIt = receiverMap.find(str); + if (recIt != receiverMap.end()) + { + auto& rec = recIt->second; + traceList[str] = getTraceFile(rec); + filterChunk.trace_ptr = &traceList[str]; + } + else + { + BOOST_LOG_TRIVIAL(warning) << "Receiver " << str << " not found in receiverMap"; + filterChunk.trace_ptr = &trace; + } + filterChunk.id = str; + } + else + { + filterChunk.id = "GLOBAL"; + filterChunk.trace_ptr = &trace; + } + + // Set H dimensions - check if this receiver has measurements + if (chunkH && begH.find(str) != begH.end()) + { + filterChunk.begH = begH[str]; + filterChunk.numH = endH[str] - begH[str] + 1; + } + else + { + // No measurements for this receiver + filterChunk.begH = 0; + filterChunk.numH = 0; + } + + // Set X dimensions - all receivers with states should have X dimensions + if (chunkX && begX.find(str) != begX.end()) + { + filterChunk.begX = begX[str]; + filterChunk.numX = endX[str] - begX[str] + 1; + } + else + { + filterChunk.begX = 0; + filterChunk.numX = kfState.x.rows(); + } + + // Validate chunk dimensions + if (filterChunk.numX < 0) + { + BOOST_LOG_TRIVIAL(error) + << "Invalid chunk dimensions for " << str << ": numH=" << filterChunk.numH + << ", numX=" << filterChunk.numX; + continue; + } + + filterChunkMap[str] = filterChunk; + } + + // Add dummy chunks for states not covered by any receiver chunk (required for RTS) + // Assume all chunks are in receiver order == state order + { + int x = 0; + map filterChunkExtras; + + auto addDummyChunk = [&](int lastX, int nextX) + { + if (nextX <= lastX + 1) + return; // No gap to fill + + FilterChunk filterChunk; + filterChunk.id = + "dummy_" + std::to_string(lastX + 1) + "_to_" + std::to_string(nextX - 1); + filterChunk.begX = lastX + 1; + filterChunk.numX = nextX - lastX - 1; + filterChunk.begH = 0; + filterChunk.numH = 0; + filterChunk.trace_ptr = &trace; + + filterChunkExtras[filterChunk.id] = filterChunk; + }; + + // Sort chunks by begX to ensure proper ordering + vector> sortedChunks; + for (auto& [str, filterChunk] : filterChunkMap) + { + sortedChunks.push_back({str, &filterChunk}); + } + std::sort( + sortedChunks.begin(), + sortedChunks.end(), + [](const auto& a, const auto& b) { return a.second->begX < b.second->begX; } + ); + + for (auto& [str, filterChunkPtr] : sortedChunks) + { + auto& filterChunk = *filterChunkPtr; + + if (filterChunk.begX > x) + { + addDummyChunk(x - 1, filterChunk.begX); + } + + x = filterChunk.begX + filterChunk.numX; + } + + // Check if we need a final dummy chunk + if (x < kfState.x.rows()) + { + addDummyChunk(x - 1, kfState.x.rows()); + } + + // Add dummy chunks to main map + for (auto& [id, fc] : filterChunkExtras) + { + filterChunkMap[id] = std::move(fc); + } + } + + // Handle chunk size configuration + if (acsConfig.pppOpts.chunk_size > 0) + { + map newFilterChunkMap; + + FilterChunk bigFilterChunk; + + int chunks = + std::max(1, (int)((double)filterChunkMap.size() / acsConfig.pppOpts.chunk_size + 0.5)); + int chunkTarget = std::max(1, (int)((double)filterChunkMap.size() / chunks + 0.5)); + + int count = 0; + for (auto& [id, filterChunk] : filterChunkMap) + { + if (count == 0) + { + bigFilterChunk = filterChunk; + bigFilterChunk.trace_ptr = &trace; + } + else + { + bigFilterChunk.id += "-" + filterChunk.id; + } + + int chunkEndX = filterChunk.begX + filterChunk.numX - 1; + int chunkEndH = filterChunk.begH + filterChunk.numH - 1; + + // Update bounds to encompass all merged chunks + bigFilterChunk.begX = std::min(bigFilterChunk.begX, filterChunk.begX); + bigFilterChunk.begH = std::min(bigFilterChunk.begH, filterChunk.begH); + + int bigEndX = bigFilterChunk.begX + bigFilterChunk.numX - 1; + int bigEndH = bigFilterChunk.begH + bigFilterChunk.numH - 1; + + if (chunkEndX > bigEndX) + { + bigFilterChunk.numX = chunkEndX - bigFilterChunk.begX + 1; + } + if (chunkEndH > bigEndH) + { + bigFilterChunk.numH = chunkEndH - bigFilterChunk.begH + 1; + } + + count++; + + if (count == chunkTarget) + { + newFilterChunkMap[bigFilterChunk.id] = bigFilterChunk; + count = 0; + } + } + + // Add final chunk if there are remaining + if (count > 0) + { + newFilterChunkMap[bigFilterChunk.id] = bigFilterChunk; + } + + filterChunkMap = std::move(newFilterChunkMap); + } + + // Log chunk information + BOOST_LOG_TRIVIAL(debug) << "Created " << filterChunkMap.size() << " filter chunks"; + for (auto& [id, fc] : filterChunkMap) + { + BOOST_LOG_TRIVIAL(debug) << "Chunk " << id << ": X[" << fc.begX << ":" + << (fc.begX + fc.numX - 1) << "]" << ", H[" << fc.begH << ":" + << (fc.begH + fc.numH - 1) << "]" << ", size of H" + << kfMeas.H.rows() << "x" << kfMeas.H.cols() << ", size of X" + << kfState.x.rows() << "x" << kfState.x.cols(); + } } -void updatePseudoPulses( - Trace& trace, - KFState& kfState) +/** + * @brief Checks if a reset interval has been reached between two epochs. + * + * Determines whether the current epoch (`epoch`) or the transition + * from the previous epoch (`prev_epoch`) to the current epoch crosses a + * specified reset interval (`reset_interval`). + * + * @param epoch The current epoch time. + * @param prev_epoch The previous epoch time. + * @param reset_interval The interval at which resets occur. + * @return True if a reset interval is reached or crossed, false otherwise. + */ +bool isIntervalReset(double epoch, double prev_epoch, double reset_interval) { - static map nextEpochMap; - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::ORBIT) - { - continue; - } - - auto& satOpts = acsConfig.getSatOpts(key.Sat); - - if ( satOpts.pseudoPulses.enable == false - ||satOpts.pseudoPulses.interval == 0) - { - continue; - } - - auto& nextEpoch = nextEpochMap[key]; - - if (epoch < nextEpoch) - { - continue; - } - - double epochsPerInterval = satOpts.pseudoPulses.interval / acsConfig.epoch_interval; + bool reset_epoch = std::fmod(epoch, reset_interval) == 0; + bool prev_epoch_reset = std::fmod(prev_epoch, reset_interval) == 0; + bool reset_between_epochs = int(prev_epoch / reset_interval) < int(epoch / reset_interval); - if (nextEpoch == 0) - { - nextEpoch = epochsPerInterval + 1; - } + return reset_epoch || (!prev_epoch_reset && reset_between_epochs); +} - while (epoch >= nextEpoch) - { - nextEpoch += satOpts.pseudoPulses.interval / acsConfig.epoch_interval; - } +/** + * @brief Checks if a specific time reset has occurred between two epochs. + * + * Determines whether a given epoch or the transition between two epochs + * corresponds to any of the specified reset times within a day. A reset time can + * either match the exact time of the epoch or occur between the two epochs. + * + * @param epoch The current epoch time in seconds since the start of the day. + * @param prev_epoch The previous epoch time in seconds since the start of the day. + * @param resetTimes A vector of reset times in seconds within a day (0 to 86400). + * + * @return True If a reset time matches the current epoch or occurs between the + * previous and current epochs, False otherwise. + */ +bool isSpecificTimeReset(double epoch, double prev_epoch, const std::vector& resetTimes) +{ + double seconds_in_day = 86400; + double epoch_mod = std::fmod(epoch, seconds_in_day); + double prev_epoch_mod = std::fmod(prev_epoch, seconds_in_day); + + for (double resetTime : resetTimes) + { + double reset_mod = std::fmod(resetTime, seconds_in_day); + bool reset_epoch = epoch_mod == reset_mod; + bool prev_epoch_reset = prev_epoch_mod == reset_mod; + bool reset_between_epochs = (prev_epoch_mod < reset_mod && epoch_mod > reset_mod) || + (prev_epoch_mod > epoch_mod && reset_mod == 0); + + if (reset_epoch || reset_between_epochs) + { + return true; + } + } + return false; +} - if (key.num < 3) kfState.setExponentialNoise(key, {SQR(satOpts.pseudoPulses.pos_proc_noise)}); - else kfState.setExponentialNoise(key, {SQR(satOpts.pseudoPulses.vel_proc_noise)}); - } +void updatePseudoPulses(Trace& trace, KFState& kfState) +{ + static map nextEpochMap; + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::ORBIT) + { + continue; + } + + auto& satOpts = acsConfig.getSatOpts(key.Sat); + if (satOpts.pseudoPulses.enable == false || + (satOpts.pseudoPulses.interval == 0 && satOpts.pseudoPulses.epochs.empty())) + { + continue; + } + + double yds[3]; + time2yds(tsync, yds, E_TimeSys::GPST); + double sod = yds[2]; + double prev_epoch = sod - acsConfig.epoch_interval; + + bool resetEpochInterval = isIntervalReset(sod, prev_epoch, satOpts.pseudoPulses.interval); + bool resetEpochSpecific = isSpecificTimeReset(sod, prev_epoch, satOpts.pseudoPulses.epochs); + + if (!resetEpochInterval && !resetEpochSpecific) + { + return; + } + + if (key.num < 3) + kfState.setExponentialNoise(key, {SQR(satOpts.pseudoPulses.pos_proc_noise)}); + else + kfState.setExponentialNoise(key, {SQR(satOpts.pseudoPulses.vel_proc_noise)}); + } } -void updateNukeFilter( - Trace& trace, - KFState& kfState) +void resetFilterbyConfig(Trace& trace, KFState& kfState) { - if ( acsConfig.pppOpts.nuke_enable == false - ||acsConfig.pppOpts.nuke_interval == 0) - { - return; - } - - static double epochsPerInterval = acsConfig.pppOpts.nuke_interval / acsConfig.epoch_interval; - static double nukeEpoch = epochsPerInterval + 1; - - if (epoch < nukeEpoch) - { - return; - } - - while (epoch >= nukeEpoch) - { - nukeEpoch += epochsPerInterval; - } - - auto& nuke_states = acsConfig.pppOpts.nuke_states; - - bool foundAll = (std::find(nuke_states.begin(), nuke_states.end(), +KF::ALL) != nuke_states.end()); - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type == KF::ONE) - { - continue; - } - - if ( foundAll - ||std::find(nuke_states.begin(), nuke_states.end(), key.type) != nuke_states.end()) - { - trace << "\n" << "State removed due to nuclear config: " << key; - kfState.removeState(key); - } - } + if (acsConfig.pppOpts.filter_reset_enable == false || + (acsConfig.pppOpts.reset_interval == 0 && acsConfig.pppOpts.reset_epochs.empty())) + { + return; + } + + if (epoch == 0) + { + // No need to reset as it is the first epoch. + return; + } + + double yds[3]; + time2yds(tsync, yds, E_TimeSys::GPST); + double sod = yds[2]; + double prev_epoch = sod - acsConfig.epoch_interval; + + bool resetEpochInterval = isIntervalReset(sod, prev_epoch, acsConfig.pppOpts.reset_interval); + bool resetEpochSpecific = isSpecificTimeReset(sod, prev_epoch, acsConfig.pppOpts.reset_epochs); + + if (!resetEpochInterval && !resetEpochSpecific) + { + return; + } + + // here will be a message to mongo about the reset (waiting on another PR to go in) + + auto& reset_states = acsConfig.pppOpts.reset_states; + + bool foundAll = + (std::find(reset_states.begin(), reset_states.end(), KF::ALL) != reset_states.end()); + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type == KF::ONE) + { + continue; + } + + if (foundAll || + std::find(reset_states.begin(), reset_states.end(), key.type) != reset_states.end()) + { + trace << "\n" + << "State removed due to reset config: " << key; + kfState.removeState(key); + } + } + // generate a string of the reset_states values. + string reset_states_str = ""; + for (auto& state : reset_states) + { + reset_states_str += std::string(enum_to_string(state)) + " "; + } + mongoEditing("", "", tsync, "reset", "", 1, reset_states_str); } -void removeBadAmbiguities( - Trace& trace, - KFState& kfState, - ReceiverMap& receiverMap); +void removeBadAmbiguities(Trace& trace, KFState& kfState, ReceiverMap& receiverMap); -void removeBadReceivers( - Trace& trace, - KFState& kfState, - ReceiverMap& receiverMap); +void removeBadSatellites(Trace& trace, KFState& kfState); -void removeBadIonospheres( - Trace& trace, - KFState& kfState); +void removeBadReceivers(Trace& trace, KFState& kfState, ReceiverMap& receiverMap); -void checkOrbits( - Trace& trace, - KFState& kfState); +void removeBadIonospheres(Trace& trace, KFState& kfState); + +void checkOrbits(Trace& trace, KFState& kfState); void updateFilter( - Trace& trace, ///< Trace to output to - ReceiverMap& receiverMap, ///< List of receivers containing observations for this epoch - KFState& kfState) ///< Kalman filter object containing the network state parameters + Trace& trace, ///< Trace to output to + ReceiverMap& receiverMap, ///< List of receivers containing observations for this epoch + KFState& kfState ///< Kalman filter object containing the network state parameters +) { - removeBadReceivers (trace, kfState, receiverMap); - removeBadAmbiguities(trace, kfState, receiverMap); - removeBadIonospheres(trace, kfState); - - updateNukeFilter (trace, kfState); - updateRecClocks (trace, receiverMap, kfState); - updateAvgClocks (trace, tsync, kfState); - updateAvgOrbits (trace, tsync, kfState); - updateAvgIonosphere (trace, tsync, kfState); - updatePseudoPulses (trace, kfState); + removeBadSatellites(trace, + kfState); // todo Eugene: revisit this as it doesn't work well + removeBadReceivers(trace, kfState, receiverMap); // todo Eugene: revisit this as well + removeBadAmbiguities(trace, kfState, receiverMap); + removeBadIonospheres(trace, kfState); + resetFilterbyConfig(trace, kfState); + updateRecClocks(trace, receiverMap, kfState); + updateAvgClocks(trace, tsync, kfState); + updateAvgOrbits(trace, tsync, kfState); + updateAvgIonosphere(trace, tsync, kfState); + updatePseudoPulses(trace, kfState); } void perRecMeasurements( - Trace& trace, - Receiver& rec, - ReceiverMap& receiverMap, - KFMeasEntryList& kfMeasEntryList, - const KFState& kfState, - const KFState& remoteState) + Trace& trace, + Receiver& rec, + ReceiverMap& receiverMap, + KFMeasEntryList& kfMeasEntryList, + KFState& kfState, + KFState& remoteState +) { - rec.pppTideCache.uninit(); - rec.pppEopCache .uninit(); + rec.pppTideCache.uninit(); + rec.pppEopCache.uninit(); - orbitPseudoObs (trace, rec, kfState, kfMeasEntryList); - receiverUducGnss (trace, rec, kfState, kfMeasEntryList, remoteState); - receiverSlr (trace, rec, kfState, kfMeasEntryList); - receiverPseudoObs (trace, rec, kfState, kfMeasEntryList, receiverMap); + orbitPseudoObs(trace, rec, kfState, kfMeasEntryList); + receiverUducGnss(trace, rec, kfState, kfMeasEntryList, remoteState); + receiverSlr(trace, rec, kfState, kfMeasEntryList); + receiverPseudoObs(trace, rec, kfState, kfMeasEntryList, receiverMap); - if (acsConfig.pppOpts.ionoOpts.use_if_combo) makeIFLCs(trace, kfState, kfMeasEntryList); + if (acsConfig.pppOpts.ionoOpts.use_if_combo) + makeIFLCs(trace, kfState, kfMeasEntryList); } -void pppLinearCombinations( - KFMeas& kfMeas, - KFState& kfState) +void pppLinearCombinations(KFMeas& kfMeas, KFState& kfState) { - if (acsConfig.pppOpts.ionoOpts .use_gf_combo) { kfMeas = makeGFLCs (kfMeas, kfState); } - if (acsConfig.pppOpts .use_rtk_combo) { kfMeas = makeRTKLCs1(kfMeas, kfState); } - if (acsConfig.pppOpts .use_rtk_combo) { kfMeas = makeRTKLCs2(kfMeas, kfState); } + if (acsConfig.pppOpts.ionoOpts.use_gf_combo) + { + kfMeas = makeGFLCs(kfMeas, kfState); + } + if (acsConfig.pppOpts.use_rtk_combo) + { + kfMeas = makeRTKLCs1(kfMeas, kfState); + } + if (acsConfig.pppOpts.use_rtk_combo) + { + kfMeas = makeRTKLCs2(kfMeas, kfState); + } } void pppPseudoObs( - Trace& trace, - ReceiverMap& receiverMap, - KFState& kfState, - KFMeasEntryList& kfMeasEntryList) + Trace& trace, + ReceiverMap& receiverMap, + KFState& kfState, + KFMeasEntryList& kfMeasEntryList +) { - DOCS_REFERENCE(Pseudo_Observations__); - - // apply external estimates - ionoPseudoObs (trace, receiverMap, kfState, kfMeasEntryList); - tropPseudoObs (trace, receiverMap, kfState, kfMeasEntryList); - - //apply pseudoobs to states available from before - pseudoRecDcb (trace, kfState, kfMeasEntryList); - ambgPseudoObs (trace, kfState, kfMeasEntryList); - initPseudoObs (trace, kfState, kfMeasEntryList); - satClockPivotPseudoObs (trace, kfState, kfMeasEntryList); - filterPseudoObs (trace, kfState, kfMeasEntryList); + DOCS_REFERENCE(Pseudo_Observations__); + + // apply external estimates + ionoPseudoObs(trace, receiverMap, kfState, kfMeasEntryList); + tropPseudoObs(trace, receiverMap, kfState, kfMeasEntryList); + + // apply pseudoobs to states available from before + pseudoRecDcb(trace, kfState, kfMeasEntryList); + ambgPseudoObs(trace, kfState, kfMeasEntryList); + initPseudoObs(trace, kfState, kfMeasEntryList); + satClockPivotPseudoObs(trace, kfState, kfMeasEntryList); + filterPseudoObs(trace, kfState, kfMeasEntryList); + phasePseudoObs(trace, kfState, kfMeasEntryList); } void ppp( - Trace& trace, ///< Trace to output to - ReceiverMap& receiverMap, ///< List of receivers containing observations for this epoch - KFState& kfState, ///< Kalman filter object containing the network state parameters - KFState& remoteState) ///< Optional pointer to remote kalman filter + Trace& trace, ///< Trace to output to + ReceiverMap& receiverMap, ///< List of receivers containing observations for this epoch + KFState& kfState, ///< Kalman filter object containing the network state parameters + KFState& remoteState ///< Optional pointer to remote kalman filter +) { - DOCS_REFERENCE(Main_Filter__); - - updateFilter(trace, receiverMap, kfState); - - //add process noise and dynamics to existing states as a prediction of current state - if (kfState.assume_linearity == false) - { - InteractiveTerminal::setMode(E_InteractiveMode::StateTransition1); - - BOOST_LOG_TRIVIAL(info) << " ------- DOING STATE TRANSITION --------" << "\n"; - - kfState.stateTransition(trace, tsync); - - if (acsConfig.output_predicted_states) - { - kfState.outputStates(trace, "/PREDICTED"); - - mongoStates(kfState, - { - .suffix = "/PREDICTED", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - } - - //prepare a map of lists of measurements for use below - map stationKFEntryListMap; - for (auto& [id, rec] : receiverMap) - { - stationKFEntryListMap[rec.id] = KFMeasEntryList(); - } - - { - InteractiveTerminal::setMode(E_InteractiveMode::OMCCalculations); - - BOOST_LOG_TRIVIAL(info) << " ------- CALCULATING PPP MEASUREMENTS --------" << "\n"; - - //calculate the measurements for each station -# ifdef ENABLE_PARALLELISATION - Eigen::setNbThreads(1); -# pragma omp parallel for -# endif - for (int i = 0; i < receiverMap.size(); i++) - { - auto rec_iterator = receiverMap.begin(); - std::advance(rec_iterator, i); - - auto& [id, rec] = *rec_iterator; - - if ( 0 - // rec.ready == false - ||rec.obsList.empty()) - { - continue; - } - - auto& kfMeasEntryList = stationKFEntryListMap[rec.id]; + DOCS_REFERENCE(Main_Filter__); - perRecMeasurements(trace, rec, receiverMap, kfMeasEntryList, kfState, remoteState); - } - Eigen::setNbThreads(0); - } + updateFilter(trace, receiverMap, kfState); - //combine all lists of measurements into a single list - KFMeasEntryList kfMeasEntryList; - for (auto& [rec, stationKFEntryList] : stationKFEntryListMap) - for (auto& kfMeasEntry : stationKFEntryList) - { - if (kfMeasEntry.valid) - { - kfMeasEntryList.push_back(std::move(kfMeasEntry)); - } - } + // add process noise and dynamics to existing states as a prediction of current state + if (kfState.assume_linearity == false) + { + BOOST_LOG_TRIVIAL(info) << " ------- DOING STATE TRANSITION --------" << "\n"; - pppPseudoObs(trace, receiverMap, kfState, kfMeasEntryList); + kfState.stateTransition(trace, tsync); - //use state transition to initialise new state elements - InteractiveTerminal::setMode(E_InteractiveMode::StateTransition2); + if (acsConfig.output_predicted_states) + { + kfState.outputStates(trace, "/PREDICTED"); - BOOST_LOG_TRIVIAL(info) << " ------- DOING STATE TRANSITION --------" << "\n"; + mongoStates( + kfState, + {.suffix = "/PREDICTED", + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + } + } - kfState.stateTransition(trace, tsync); + // prepare a map of lists of measurements for use below + map receiverKFEntryListMap; + for (auto& [id, rec] : receiverMap) + { + receiverKFEntryListMap[rec.id] = KFMeasEntryList(); + } - if (acsConfig.output_initialised_states) - { - kfState.outputStates(trace, "/INITIALISED"); + { + BOOST_LOG_TRIVIAL(info) << " ------- CALCULATING PPP MEASUREMENTS --------" << "\n"; - mongoStates(kfState, - { - .suffix = "/INITIALISED", - .instances = acsConfig.mongoOpts.output_states, - .queue = acsConfig.mongoOpts.queue_outputs - }); - } - - std::sort(kfMeasEntryList.begin(), kfMeasEntryList.end(), [](KFMeasEntry& a, KFMeasEntry& b) {return a.obsKey < b.obsKey;}); - - KFMeas kfMeas(kfState, kfMeasEntryList, tsync); - - pppLinearCombinations(kfMeas, kfState); - - if (acsConfig.explain_measurements) - { - explainMeasurements(trace, kfMeas, kfState); - } - - alternatePostfits(trace, kfMeas, kfState); - - if (kfState.lsqRequired) - { - BOOST_LOG_TRIVIAL(info) << "-------INITIALISING PPPPP USING LEAST SQUARES--------" << "\n"; - - VectorXd dx; - kfState.leastSquareInitStates(trace, kfMeas, false, &dx, true); - - kfState.outputStates(trace, "/LSQ"); - } - - map filterChunkMap; - map traceList; //keep in large scope as we're using pointers - - chunkFilter(trace, kfState, kfMeas, receiverMap, filterChunkMap, traceList); - - - InteractiveTerminal::setMode(E_InteractiveMode::Filtering); - BOOST_LOG_TRIVIAL(info) << " ------- DOING PPPPP KALMAN FILTER --------" << "\n"; - - kfState.filterKalman(trace, kfMeas, "/PPP", true, &filterChunkMap); - - postFilterChecks(tsync, kfMeas); - - //output chunks if we are actually chunking still - if ( acsConfig.pppOpts.receiver_chunking - ||acsConfig.pppOpts.satellite_chunking) - for (auto& [id, filterChunk] : filterChunkMap) - { - if (filterChunk.trace_ptr) - { - kfState.outputStates(*filterChunk.trace_ptr, (string)"/PPPChunk/" + id, filterChunk.begX, filterChunk.numX); - } - } - - kfState.outputStates(trace, "/PPP"); + // calculate the measurements for each receiver +#ifdef ENABLE_PARALLELISATION + Eigen::setNbThreads(1); +#pragma omp parallel for +#endif + for (int i = 0; i < receiverMap.size(); i++) + { + auto recIterator = receiverMap.begin(); + std::advance(recIterator, i); + + auto& [id, rec] = *recIterator; + + if (rec.ready == false || rec.obsList.empty()) + { + continue; + } + + auto& kfMeasEntryList = receiverKFEntryListMap[rec.id]; + + perRecMeasurements(trace, rec, receiverMap, kfMeasEntryList, kfState, remoteState); + } + Eigen::setNbThreads(0); + } + + // combine all lists of measurements into a single list + KFMeasEntryList kfMeasEntryList; + for (auto& [rec, receiverKFEntryList] : receiverKFEntryListMap) + for (auto& kfMeasEntry : receiverKFEntryList) + { + if (kfMeasEntry.valid) + { + kfMeasEntryList.push_back(std::move(kfMeasEntry)); + } + } + + pppPseudoObs(trace, receiverMap, kfState, kfMeasEntryList); + + if (acsConfig.pppOpts.merge_correlated_states) + { + mergeCorrelated(trace, kfState, kfMeasEntryList); + } + + // use state transition to initialise new state elements + BOOST_LOG_TRIVIAL(info) << " ------- DOING STATE TRANSITION --------" << "\n"; + + kfState.stateTransition(trace, tsync); + + if (acsConfig.output_initialised_states) + { + kfState.outputStates(trace, "/INITIALISED"); + + mongoStates( + kfState, + {.suffix = "/INITIALISED", + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); + } + + std::sort( + kfMeasEntryList.begin(), + kfMeasEntryList.end(), + [](KFMeasEntry& a, KFMeasEntry& b) { return a.obsKey < b.obsKey; } + ); + + KFMeas kfMeas(kfState, kfMeasEntryList, tsync); + + pppLinearCombinations(kfMeas, kfState); + + if (acsConfig.explain_measurements) + { + explainMeasurements(trace, kfMeas, kfState); + } + + alternatePostfits(trace, kfMeas, kfState); + + if (kfState.lsqRequired) + { + BOOST_LOG_TRIVIAL(info) << "-------INITIALISING PPPPP USING LEAST SQUARES--------" << "\n"; + + string suffix = "/LSQ"; + kfState.leastSquareInitStates(trace, kfMeas, suffix, false, true); + + kfState.outputStates(trace, suffix); + } + + map filterChunkMap; + map traceList; // keep in large scope as we're using pointers + + chunkFilter(trace, kfState, kfMeas, receiverMap, filterChunkMap, traceList); + + BOOST_LOG_TRIVIAL(info) << " ------- DOING PPPPP KALMAN FILTER --------" << "\n"; + + kfState.filterKalman(trace, kfMeas, "/PPP", true, &filterChunkMap); + + postFilterChecks(tsync, receiverMap, kfState, kfMeas); + + // output chunks if we are actually chunking still + if (acsConfig.pppOpts.receiver_chunking) + for (auto& [id, filterChunk] : filterChunkMap) + { + if (filterChunk.trace_ptr) + { + kfState.outputStates( + *filterChunk.trace_ptr, + (string) "/PPPChunk/" + id, + filterChunk.begX, + filterChunk.numX + ); + } + } } - - - - diff --git a/src/cpp/pea/preprocessor.cpp b/src/cpp/pea/preprocessor.cpp index e077362fc..06e9c65af 100644 --- a/src/cpp/pea/preprocessor.cpp +++ b/src/cpp/pea/preprocessor.cpp @@ -1,272 +1,795 @@ - // #pragma GCC optimize ("O0") +#include +#include +#include +#include "ambres/GNSSambres.hpp" #include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/acsQC.hpp" +#include "common/constants.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/satStat.hpp" +#include "common/sinex.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "pea/ppp.hpp" /** Use linear combinations of primary signals to detect jumps in carrier phase observations */ -Architecture Cycle_Slip_Detection__() -{ - -} +Architecture Cycle_Slip_Detection__() {} /** Perform basic quality checks on observations */ Architecture Preprocessing__() { - DOCS_REFERENCE(Cycle_Slip_Detection__); - DOCS_REFERENCE(SPP__); + DOCS_REFERENCE(Cycle_Slip_Detection__); + DOCS_REFERENCE(SPP__); } -#include "observations.hpp" -#include "navigation.hpp" -#include "GNSSambres.hpp" -#include "testUtils.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "receiver.hpp" -#include "satStat.hpp" -#include "trace.hpp" -#include "sinex.hpp" -#include "acsQC.hpp" -#include "ppp.hpp" - - -void outputObservations( - Trace& trace, - Trace& jsonTrace, - ObsList& obsList) +#include "ambres/GNSSambres.hpp" +#include "common/acsConfig.hpp" +#include "common/acsQC.hpp" +#include "common/constants.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/satStat.hpp" +#include "common/sinex.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "pea/ppp.hpp" + +// Observation status for signal tracking +enum class E_ObsStatus +{ + OBSERVED, // Signal was observed by receiver (both code and phase) + CODE_ONLY, // Only code measurement available + PHASE_ONLY, // Only phase measurement available (this is unlikely as code is demodulated first + // and then try to extract phase) + MISSING, // Signal was expected, and we have other signals for this sat, but this one was not + // in rinex + NOT_TRACKED // Satellite not observed at all (above elevation mask but not in observation data) +}; + +// Convert observation status to string +const char* obsStatusToString(E_ObsStatus status) { - for (auto& obs : only(obsList)) - for (auto& [ft, sigs] : obs.sigsLists) - for (auto& sig : sigs) - { - if (obs.exclude) - { - continue; - } - - tracepdeex(4, trace, "\n%s %5s %5s %14.4f %14.4f", obs.time.to_string().c_str(), obs.Sat.id().c_str(), sig.code._to_string(), sig.L, sig.P); - - traceJson(4, jsonTrace, obs.time, - { - {"data", __FUNCTION__}, - {"Sat", obs.Sat.id()}, - {"Rec", obs.mount}, - {"Sig", sig.code._to_string()} - }, - { - {"SNR", sig.snr}, - {"L", sig.L}, - {"P", sig.P}, - {"D", sig.D}, - // {"LLI", sig.lli}, - }); - } + switch (status) + { + case E_ObsStatus::OBSERVED: + return "OBSERVED"; + case E_ObsStatus::CODE_ONLY: + return "CODE_ONLY"; + case E_ObsStatus::PHASE_ONLY: + return "PHASE_ONLY"; + case E_ObsStatus::MISSING: + return "MISSING"; + case E_ObsStatus::NOT_TRACKED: + return "NOT_TRACKED"; + default: + return "UNKNOWN"; + } } -void obsVariances( - ObsList& obsList) +struct SatelliteVisibility +{ + SatSys sat; + double el = 0; // Elevation in radians + double az = 0; // Azimuth in radians + set expectedSignals; + bool wasObserved = false; + GObs* obs = nullptr; // Pointer to observation if satellite was observed +}; + +// Observation record for output +struct ObservationRecord +{ + GTime time; + SatSys sat; + string recId; + E_ObsCode code; + double P = 0; + double L = 0; + double snr = 0; + double el_deg = 0; + double az_deg = 0; + E_ObsStatus status; + string blockType; +}; + +// Output a single observation record to trace files +void obsRec(Trace& trace, Trace& jsonTrace, const ObservationRecord& rec) { - for (auto& obs : only(obsList)) - if (obs.satNav_ptr) - if (obs.satStat_ptr) - if (obs.exclude == false) - if (acsConfig.process_sys[obs.Sat.sys]) - { - auto& recOpts = acsConfig.getRecOpts(obs.mount); - auto& satOpts = acsConfig.getSatOpts(obs.Sat); - - double el = obs.satStat_ptr->el; - if (el == 0) - el = PI/8; - - double recElScaling = 1; - switch (recOpts.error_model) - { - case E_NoiseModel::UNIFORM: { recElScaling = 1; break; } - case E_NoiseModel::ELEVATION_DEPENDENT: { recElScaling = 1 / sin(el); break; } - } - - double satElScaling = 1; - switch (satOpts.error_model) - { - case E_NoiseModel::UNIFORM: { satElScaling = 1; break; } - case E_NoiseModel::ELEVATION_DEPENDENT: { satElScaling = 1 / sin(el); break; } - } - - for (auto& [ft, sig] : obs.sigs) - { - if (sig.P == 0) - continue; - - string sigName = sig.code._to_string(); - - auto& satOpts = acsConfig.getSatOpts(obs.Sat, {sigName}); - auto& recOpts = acsConfig.getRecOpts(obs.mount, {obs.Sat.sys._to_string(), sigName}); - - sig.codeVar = 0; - sig.phasVar = 0; - - sig.codeVar += SQR(recElScaling * recOpts.code_sigma); sig.codeVar += SQR(satElScaling * satOpts.code_sigma); - sig.phasVar += SQR(recElScaling * recOpts.phase_sigma); sig.phasVar += SQR(satElScaling * satOpts.phase_sigma); - } - - for (auto& [ft, sigList] : obs.sigsLists) - for (auto& sig : sigList) - { - string sigName = sig.code._to_string(); - - auto& satOpts = acsConfig.getSatOpts(obs.Sat, {sigName}); - auto& recOpts = acsConfig.getRecOpts(obs.mount, {obs.Sat.sys._to_string(), sigName}); - - sig.codeVar = 0; - sig.phasVar = 0; - - sig.codeVar += SQR(recElScaling * recOpts.code_sigma); sig.codeVar += SQR(satElScaling * satOpts.code_sigma); - sig.phasVar += SQR(recElScaling * recOpts.phase_sigma); sig.phasVar += SQR(satElScaling * satOpts.phase_sigma); - } - } + const char* statusStr = obsStatusToString(rec.status); + GTime time = rec.time; // Make mutable copy for traceJson (expects non-const reference) + + // Determine which values to show based on status + bool hasCode = (rec.status == E_ObsStatus::OBSERVED || rec.status == E_ObsStatus::CODE_ONLY); + bool hasPhase = (rec.status == E_ObsStatus::OBSERVED || rec.status == E_ObsStatus::PHASE_ONLY); + bool hasSnr = + (rec.status == E_ObsStatus::OBSERVED || rec.status == E_ObsStatus::CODE_ONLY || + rec.status == E_ObsStatus::PHASE_ONLY); + + // Format values as strings (without width specifiers - will be applied in tracepdeex format) + char pStr[32], lStr[32], sStr[32]; + if (hasCode) + snprintf(pStr, sizeof(pStr), "%.6f", rec.P); + else + snprintf(pStr, sizeof(pStr), "%s", "NaN"); + + if (hasPhase) + snprintf(lStr, sizeof(lStr), "%.6f", rec.L); + else + snprintf(lStr, sizeof(lStr), "%s", "NaN"); + + if (hasSnr) + snprintf(sStr, sizeof(sStr), "%.2f", rec.snr); + else + snprintf(sStr, sizeof(sStr), "%s", "NaN"); + + tracepdeex( + 0, + trace, + "\n%s: epoch= %s sat= %5s sig= %5s P= %16s L= %16s S= %8s el= %6.2f az= %6.2f block= %12s " + "status= %s", + __FUNCTION__, + rec.time.to_string().c_str(), + rec.sat.id().c_str(), + enum_to_string(rec.code).c_str(), + pStr, + lStr, + sStr, + rec.el_deg, + rec.az_deg, + rec.blockType.c_str(), + statusStr + ); + + // Output JSON trace - always include all fields, use NaN for unavailable measurements + traceJson( + 0, + jsonTrace, + time, + {{"data", "observations"}, + {"Sat", rec.sat.id()}, + {"Rec", rec.recId}, + {"Sig", enum_to_string(rec.code)}}, + {{"SNR", hasSnr ? rec.snr : std::nan("")}, + {"L", hasPhase ? rec.L : std::nan("")}, + {"P", hasCode ? rec.P : std::nan("")}, + {"D", 0.0}, // Not stored in record currently + {"el", rec.el_deg}, + {"az", rec.az_deg}, + {"blockType", rec.blockType}, + {"status", statusStr}} + ); } -void excludeUnprocessed( - ObsList& obsList) +// Classify observed signals vs expected signals +void classifySignals( + GObs* obs, + const set& expectedSignals, + double el_deg, + double az_deg, + vector& records +) { - for (auto& obs : only(obsList)) - { - if (acsConfig.process_sys[obs.Sat.sys] == false) - { - obs.excludeSystem = true; - } - } + set observedSignals; + string blockType = obs->Sat.blockType(); + + // Collect OBSERVED signals + for (auto& [ft, sigs] : obs->sigsLists) + { + for (auto& sig : sigs) + { + // Only process signals that are in expectedSignals (already filtered by + // code_priorities) + if (expectedSignals.find(sig.code) == expectedSignals.end()) + { + continue; + } + + observedSignals.insert(sig.code); + + ObservationRecord rec; + rec.time = obs->time; + rec.sat = obs->Sat; + rec.recId = obs->mount; + rec.code = sig.code; + rec.P = sig.P; + rec.L = sig.L; + rec.snr = sig.snr; + rec.el_deg = el_deg; + rec.az_deg = az_deg; + + // Determine status based on which measurements are available + bool hasCode = (sig.P != 0); + bool hasPhase = (sig.L != 0); + + if (hasCode && hasPhase) + { + rec.status = E_ObsStatus::OBSERVED; + } + else if (hasCode && !hasPhase) + { + rec.status = E_ObsStatus::CODE_ONLY; + } + else if (!hasCode && hasPhase) + { + rec.status = E_ObsStatus::PHASE_ONLY; + } + else + { + rec.status = E_ObsStatus::MISSING; + } + + rec.blockType = blockType; + records.push_back(rec); + } + } + + // Collect MISSING signals (expected but not observed) + for (auto& expectedCode : expectedSignals) + { + if (observedSignals.find(expectedCode) == observedSignals.end()) + { + ObservationRecord rec; + rec.time = obs->time; + rec.sat = obs->Sat; + rec.recId = obs->mount; + rec.code = expectedCode; + rec.el_deg = el_deg; + rec.az_deg = az_deg; + rec.status = E_ObsStatus::MISSING; + rec.blockType = blockType; + records.push_back(rec); + } + } } -void recordSlips( - Receiver& rec) +// Create NOT_TRACKED records for a satellite that was not observed +void createNotTrackedRecords( + GTime time, + SatSys sat, + string recId, + const set& expectedSignals, + double el_deg, + double az_deg, + vector& records +) { - for (auto& obs : only(rec.obsList)) - for (auto& [ft, sig] : obs.sigs) - if (obs.satStat_ptr) - { - SigStat& sigStat = obs.satStat_ptr->sigStatMap[ft2string(ft)]; - - if ( sigStat.slip.any - &&( (acsConfig.exclude.LLI && sigStat.slip.LLI) - ||(acsConfig.exclude.GF && sigStat.slip.GF) - ||(acsConfig.exclude.MW && sigStat.slip.MW) - ||(acsConfig.exclude.SCDIA && sigStat.slip.SCDIA))) - { - rec.savedSlips[obs.Sat] = obs.time; - } - } + string blockType = sat.blockType(); + + for (auto& expectedCode : expectedSignals) + { + ObservationRecord rec; + rec.time = time; + rec.sat = sat; + rec.recId = recId; + rec.code = expectedCode; + rec.el_deg = el_deg; + rec.az_deg = az_deg; + rec.status = E_ObsStatus::NOT_TRACKED; + rec.blockType = blockType; + records.push_back(rec); + } } -void preprocessor( - Trace& trace, - Receiver& rec, - bool realEpoch) +// Compute satellite position and elevation/azimuth +// Returns true if position was successfully computed +bool computeSatellitePosition( + Trace& trace, + GTime time, + SatSys sat, + SatNav& satNav, + Receiver& rec, + VectorPos& recPos, + GObs* obs, // If satellite was observed, use existing obs; otherwise nullptr + double& el, + double& az +) { - DOCS_REFERENCE(Preprocessing__); - - if ( (acsConfig.process_preprocessor == false) - ||(acsConfig.preprocOpts.preprocess_all_data == true && realEpoch == true) - ||(acsConfig.preprocOpts.preprocess_all_data == false && realEpoch == false)) - { - return; - } - - auto jsonTrace = getTraceFile(rec, true); - - acsConfig.getRecOpts(rec.id); - - auto& obsList = rec.obsList; - - if (obsList.empty()) - { - return; - } - - PTime start_time; - start_time.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; - - if ( acsConfig.start_epoch.is_not_a_date_time() == false - && obsList.front()->time < (GTime) start_time - tol) - { - return; - } - - //prepare and connect navigation objects to the observations - for (auto& obs : only(obsList)) - { - obs.mount = rec.id; - - if (acsConfig.process_sys[obs.Sat.sys] == false) - { - obs.excludeSystem = true; - - continue; - } - - auto& satOpts = acsConfig.getSatOpts(obs.Sat); + auto& satStat = rec.satStatMap[sat]; + auto& satOpts = acsConfig.getSatOpts(sat); + + // If satellite was observed, use pre-computed position + if (obs && obs->satStat_ptr) + { + el = obs->satStat_ptr->el; + az = obs->satStat_ptr->az; + return true; + } + + // Satellite not observed - compute position ourselves + // For NOT_TRACKED detection, we only need satellite position, not clock or pseudorange + SatPos satPos = {}; + satPos.Sat = sat; + satPos.satNav_ptr = &satNav; + + bool posFound = satpos( + trace, + time, + time, // teph = time + satPos, + satOpts.posModel.sources, + E_OffsetType::APC, + nav + ); + + if (!posFound || satPos.rSatApc.isZero()) + { + return false; + } + + Vector3d rSat = satPos.rSatApc; + + Vector3d e; + double r = geodist(rSat, rec.aprioriPos, e); + satazel(recPos, e, satStat); + el = satStat.el; + az = satStat.az; + return true; +} - if (satOpts.exclude) - { - obs.excludeConfig = true; +// Determine expected signals for a satellite-receiver pair +// Returns the intersection of: (satellite frequencies) AND (receiver tracked signals) AND (code +// priorities) +set determineExpectedSignals(SatSys sat, Receiver& rec) +{ + set expectedSignals; + + // Get satellite block type and convert to enum + string blockTypeStr = sat.blockType(); + if (blockTypeStr.empty()) + { + return expectedSignals; // Unknown block type + } + + string enumStr = blockTypeStr; + std::replace(enumStr.begin(), enumStr.end(), '-', '_'); + + E_Block blockType; + try + { + blockType = string_to_enum(enumStr); + } + catch (...) + { + return expectedSignals; // Unknown block type + } + + // Get frequencies that this satellite broadcasts + auto freqIt = blockTypeFrequencies.find(blockType); + if (freqIt == blockTypeFrequencies.end()) + { + return expectedSignals; // Block type not found + } + + vector satFrequencies = freqIt->second; + + // Convert to set for faster lookup + set satFreqSet(satFrequencies.begin(), satFrequencies.end()); + + // Get receiver's tracked signals for this constellation + auto trackedIt = rec.trackedSignals.find(sat.sys); + if (trackedIt == rec.trackedSignals.end()) + { + return expectedSignals; + } + + auto& receiverSignals = trackedIt->second; + auto& codePriorities = acsConfig.code_priorities[sat.sys]; + + // Get code2Freq map for this constellation + auto sysCodeIt = code2Freq.find(sat.sys); + if (sysCodeIt == code2Freq.end()) + { + return expectedSignals; + } + + // Find receiver signals that match satellite frequencies AND are in code priorities + for (auto& recSig : receiverSignals) + { + // Use code2Freq to get frequency type for this signal + auto codeIt = sysCodeIt->second.find(recSig); + if (codeIt != sysCodeIt->second.end()) + { + E_FType sigFreq = codeIt->second; + + // Check if satellite broadcasts this frequency AND signal is in code priorities + bool inSatFreqs = satFreqSet.count(sigFreq); + bool inCodePriorities = + std::find(codePriorities.begin(), codePriorities.end(), recSig) != + codePriorities.end(); + + // Filter out signals not supported by this block type (e.g., L2C not on older GPS + // blocks) This prevents false "MISSING" reports for signals that a satellite block type + // physically cannot transmit. + bool supportedByBlock = isSignalSupportedByBlockType(recSig, blockType); + + if (inSatFreqs && inCodePriorities && supportedByBlock) + { + expectedSignals.insert(recSig); + } + } + } + + return expectedSignals; +} - continue; - } +void outputObservations( + Trace& trace, + Trace& jsonTrace, + ObsList& obsList, + Receiver& rec, + VectorPos& recPos +) +{ + if (obsList.empty()) + { + return; + } + + GTime time = obsList.front()->time; + auto& recOpts = acsConfig.getRecOpts(rec.id); + double elevationMask = recOpts.elevation_mask_deg * D2R; + + // Build map of satellites that were observed in RINEX + map observedSatMap; + for (auto& obs : only(obsList)) + { + if (obs.exclude == false) + { + observedSatMap[obs.Sat] = &obs; + } + } + + // Iterate through ALL satellites with available ephemeris + for (auto& [sat, satNav] : nav.satNavMap) + { + // Check if this satellite system is being processed + if (acsConfig.process_sys[sat.sys] == false) + { + continue; + } + + auto& satOpts = acsConfig.getSatOpts(sat); + if (satOpts.exclude) + { + continue; + } + + // Check if satellite was observed + auto obsIt = observedSatMap.find(sat); + GObs* obs = (obsIt != observedSatMap.end()) ? obsIt->second : nullptr; + + // Compute satellite position and elevation/azimuth + double el = 0, az = 0; + if (!computeSatellitePosition(trace, time, sat, satNav, rec, recPos, obs, el, az)) + { + continue; // Position not available + } + + // Determine expected signals for this satellite-receiver pair + set expectedSignals = determineExpectedSignals(sat, rec); + if (expectedSignals.empty()) + { + continue; // No matching signals + } + + // For satellites NOT observed, only output if above elevation mask + if (!obs && el < elevationMask) + { + continue; + } + + double el_deg = el * R2D; + double az_deg = az * R2D; + + // Classify signals and create observation records + vector records; + + if (obs) + { + // Satellite was observed - classify as OBSERVED or MISSING + classifySignals(obs, expectedSignals, el_deg, az_deg, records); + } + else + { + // Satellite was NOT observed - all expected signals are NOT_TRACKED + createNotTrackedRecords(time, sat, rec.id, expectedSignals, el_deg, az_deg, records); + } + + // Output all records to trace files + for (const auto& record : records) + { + obsRec(trace, jsonTrace, record); + } + } +} - auto& satNav = nav.satNavMap[obs.Sat]; +void obsVariance(GObs& obs) +{ + if (obs.satNav_ptr) + if (obs.satStat_ptr) + if (obs.exclude == false) + if (acsConfig.process_sys[obs.Sat.sys]) + { + auto& recOpts = acsConfig.getRecOpts(obs.mount); + auto& satOpts = acsConfig.getSatOpts(obs.Sat); + + double el = obs.satStat_ptr->el; + if (el == 0) // Eugene: Check if (el <= 0)? + el = PI / 8; + + double recElScaling = 1; + switch (recOpts.error_model) + { + case E_NoiseModel::UNIFORM: + { + recElScaling = 1; + break; + } + case E_NoiseModel::ELEVATION_DEPENDENT: + { + recElScaling = 1 / sin(el); + break; + } + } + + double satElScaling = 1; + switch (satOpts.error_model) + { + case E_NoiseModel::UNIFORM: + { + satElScaling = 1; + break; + } + case E_NoiseModel::ELEVATION_DEPENDENT: + { + satElScaling = 1 / sin(el); + break; + } + } + + for (auto& [ft, sig] : obs.sigs) + { + if (sig.P == 0) + continue; + + string sigName = enum_to_string(sig.code); + + auto& satOpts = acsConfig.getSatOpts(obs.Sat, {sigName}); + auto& recOpts = + acsConfig.getRecOpts(obs.mount, {obs.Sat.sysName(), sigName}); + + sig.codeVar = 0; + sig.phasVar = 0; + + sig.codeVar += SQR(recElScaling * recOpts.code_sigma); + sig.codeVar += SQR(satElScaling * satOpts.code_sigma); + sig.phasVar += SQR(recElScaling * recOpts.phase_sigma); + sig.phasVar += SQR(satElScaling * satOpts.phase_sigma); + } + + for (auto& [ft, sigList] : obs.sigsLists) + for (auto& sig : sigList) + { + string sigName = enum_to_string(sig.code); + + auto& satOpts = acsConfig.getSatOpts(obs.Sat, {sigName}); + auto& recOpts = + acsConfig.getRecOpts(obs.mount, {obs.Sat.sysName(), sigName}); + + sig.codeVar = 0; + sig.phasVar = 0; + + sig.codeVar += SQR(recElScaling * recOpts.code_sigma); + sig.codeVar += SQR(satElScaling * satOpts.code_sigma); + sig.phasVar += SQR(recElScaling * recOpts.phase_sigma); + sig.phasVar += SQR(satElScaling * satOpts.phase_sigma); + } + } +} - obs.rec_ptr = &rec; - obs.satNav_ptr = &satNav; - obs.satStat_ptr = &rec.satStatMap[obs.Sat]; +void obsVariances(ObsList& obsList) +{ + for (auto& obs : only(obsList)) + { + obsVariance(obs); + } +} - updateLamMap(obs.time, obs); - } +void excludeUnprocessed(ObsList& obsList) +{ + for (auto& obs : only(obsList)) + { + if (acsConfig.process_sys[obs.Sat.sys] == false) + { + obs.excludeSystem = true; + } + } +} - for (auto& obs : only(obsList)) - { - if (acsConfig.process_sys[obs.Sat.sys] == false) - { - continue; - } +void recordSlips(Receiver& rec) +{ + for (auto& obs : only(rec.obsList)) + { + for (auto& [ft, sig] : obs.sigs) + { + if (obs.satStat_ptr) + { + SigStat& sigStat = obs.satStat_ptr->sigStatMap[ft2string(ft)]; + + if (sigStat.slip.any && + ((acsConfig.exclude.LLI && sigStat.slip.LLI) || + (acsConfig.exclude.GF && sigStat.slip.GF) || + (acsConfig.exclude.retrack && sigStat.slip.retrack) || + (acsConfig.exclude.single_freq && sigStat.slip.singleFreq) || + (acsConfig.exclude.MW && sigStat.slip.MW) || + (acsConfig.exclude.SCDIA && sigStat.slip.SCDIA))) + { + rec.savedSlips[obs.Sat] = obs.time; + mongoEditing( + obs.Sat.id(), + rec.id, + obs.time, + "PreprocSlip", + enum_to_string(sig.code), + static_cast(sigStat.slip.any) + ); + } + } + } + if (obs.satStat_ptr) + { + double gf0 = obs.satStat_ptr->gf; + double mw0 = obs.satStat_ptr->mw; + mongoEditing(obs.Sat.id(), rec.id, obs.time, "gf", "", gf0); + mongoEditing(obs.Sat.id(), rec.id, obs.time, "mw", "", mw0); + } + } +} - obs.satNav_ptr = &nav.satNavMap[obs.Sat]; - obs.satStat_ptr = &rec.satStatMap[obs.Sat]; - } +void preprocessor( + Trace& trace, + Receiver& rec, + bool realEpoch, + KFState* kfState_ptr, ///< Optional pointer to filter to take ephemerides from + KFState* remote_ptr ///< Optional pointer to filter to take ephemerides from +) +{ + DOCS_REFERENCE(Preprocessing__); - clearSlips(obsList); + if ((acsConfig.process_preprocessor == false) || + (acsConfig.preprocOpts.preprocess_all_data == true && realEpoch == true) || + (acsConfig.preprocOpts.preprocess_all_data == false && realEpoch == false)) + { + return; + } - excludeUnprocessed(obsList); + auto jsonTrace = getTraceFile(rec, true); - outputObservations(trace, jsonTrace, obsList); + auto& recOpts = acsConfig.getRecOpts(rec.id); - /* linear combinations */ - for (auto& obs : only(obsList)) - if (obs.satStat_ptr) - { - obs.satStat_ptr->lc_pre = obs.satStat_ptr->lc_new; - obs.satStat_ptr->lc_new = {}; - } - obs2lcs (trace, obsList); + auto& obsList = rec.obsList; - detectslips (trace, obsList); + if (obsList.empty()) + { + return; + } - recordSlips(rec); + PTime startTime; + startTime.bigTime = boost::posix_time::to_time_t(acsConfig.start_epoch); - for (auto& obs : only(obsList)) - for (auto& [ft, Sig] : obs.sigs) - if (obs.satStat_ptr) - { - if (obs.satStat_ptr->sigStatMap[ft2string(ft)].slip.any) - { - rec.slipCount++; - break; - } - } + 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) + { + return; + } + + getRecSnx(rec.id, time, rec.snx); + + bool dummy; + updateAprioriRecPos(trace, rec, recOpts, dummy, remote_ptr); + + VectorPos pos = ecef2pos(rec.aprioriPos); + + // prepare and connect navigation objects to the observations + for (auto& obs : only(obsList)) + { + obs.mount = rec.id; + + if (acsConfig.process_sys[obs.Sat.sys] == false) + { + obs.excludeSystem = true; + + continue; + } + + auto& satOpts = acsConfig.getSatOpts(obs.Sat); + + if (satOpts.exclude) + { + obs.excludeConfig = true; + + continue; + } + + auto& satNav = nav.satNavMap[obs.Sat]; + auto& satStat = rec.satStatMap[obs.Sat]; + + obs.rec_ptr = &rec; + obs.satNav_ptr = &satNav; + obs.satStat_ptr = &satStat; + + updateLamMap(obs.time, obs); + + satPosClk( + trace, + obs.time, + obs, + nav, + satOpts.posModel.sources, + satOpts.clockModel.sources, + kfState_ptr, + remote_ptr, + E_OffsetType::APC + ); + + Vector3d rSat = obs.rSatApc; + if (rSat.isZero()) + { + obs.failureRSat = true; + + continue; + } + + double r = geodist(rSat, rec.aprioriPos, satStat.e); + + satazel(pos, satStat.e, satStat); + } + + clearSlips(obsList); + + excludeUnprocessed(obsList); + + if (acsConfig.output_observations) + { + outputObservations(trace, jsonTrace, obsList, rec, pos); + } + + /* linear combinations */ + for (auto& obs : only(obsList)) + if (obs.satStat_ptr) + { + obs.satStat_ptr->lc_pre = obs.satStat_ptr->lc_new; + obs.satStat_ptr->lc_new = {}; + } + obs2lcs(trace, obsList); + obsVariances(obsList); + detectslips(trace, obsList); + + recordSlips(rec); + + for (auto& obs : only(obsList)) + for (auto& [ft, Sig] : obs.sigs) + if (obs.satStat_ptr) + { + if (obs.satStat_ptr->sigStatMap[ft2string(ft)].slip.any) + { + rec.slipCount++; + break; + } + } } diff --git a/src/cpp/pea/preprocessor.hpp b/src/cpp/pea/preprocessor.hpp index 660d1720e..bb1539ac3 100644 --- a/src/cpp/pea/preprocessor.hpp +++ b/src/cpp/pea/preprocessor.hpp @@ -1,13 +1,10 @@ - #pragma once - struct Receiver; - void preprocessor( - Trace& trace, - Receiver& rec, - bool realEpoch = false); - -void obsVariances( - ObsList& obsList); - + Trace& trace, + Receiver& rec, + bool realEpoch = false, + KFState* kfState_ptr = nullptr, + KFState* remote_ptr = nullptr +); +void obsVariances(ObsList& obsList); \ No newline at end of file diff --git a/src/cpp/pea/spp.cpp b/src/cpp/pea/spp.cpp index b85fcb35a..e757e293a 100644 --- a/src/cpp/pea/spp.cpp +++ b/src/cpp/pea/spp.cpp @@ -1,930 +1,1322 @@ - // #pragma GCC optimize ("O0") -#include "architectureDocs.hpp" - -Architecture SPP__() -{ - -} - #include +#include #include #include -#include +#include "architectureDocs.hpp" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/biases.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/ephPrecise.hpp" +#include "common/ionModels.hpp" +#include "common/mongoWrite.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "common/satStat.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" +#include "pea/ppp.hpp" +#include "trop/tropModels.hpp" using std::ostringstream; -#include "interactiveTerminal.hpp" -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "ephPrecise.hpp" -#include "navigation.hpp" -#include "tropModels.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "ionModels.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "satStat.hpp" -#include "common.hpp" -#include "biases.hpp" -#include "trace.hpp" -#include "enums.h" -#include "ppp.hpp" - -#define ERR_ION 7.0 ///< ionospheric delay std (m) -#define ERR_BRDCI 0.5 ///< broadcast iono model error factor -#define ERR_CBIAS 0.3 ///< code bias error std (m) - -/** Calculate pseudorange with code bias correction -*/ -bool prange( - Trace& trace, ///< Trace file to output to - GObs& obs, ///< Observation to calculate pseudorange for - int ionomode, ///< Ionospheric correction mode - double& range, ///< Pseudorange value output - double& measVar, ///< Pseudorange variance output - double& biasVar, ///< Bias variance output - KFState* kfState_ptr) ///< Optional kfstate to retrieve biases from -{ - SatNav& satNav = *obs.satNav_ptr; - auto& lam = satNav.lamMap; - - range = 0; - measVar = 0; - biasVar = SQR(ERR_CBIAS); - - E_Sys sys = obs.Sat.sys; - if (sys == +E_Sys::NONE) - { - return false; - } - - E_FType f_1; - E_FType f_2; - E_FType f_3; - if (!satFreqs(sys,f_1,f_2,f_3)) - return false; - - if ( obs.sigs[f_1].P == 0 - || lam[f_1] == 0) - { - return false; - } - - //get a bias if the default invalid value is still present - double& B1 = obs.sigs[f_1].biases [CODE]; - double& var1 = obs.sigs[f_1].biasVars[CODE]; - if (isnan(B1)) - { - B1 = 0; - var1 = 0; - getBias(trace, obs.time, obs.Sat.id(), obs.Sat, obs.sigs[f_1].code, CODE, B1, var1, kfState_ptr); - } - - double P1 = obs.sigs[f_1].P - B1; - - double PC = 0; - if (ionomode == E_IonoMode::IONO_FREE_LINEAR_COMBO) /* dual-frequency */ - { - if ( obs.sigs[f_2].P == 0 - || lam[f_2] == 0) - { - return false; - } - - double gamma= SQR(lam[f_2]) / SQR(lam[f_1]); /* f1^2/f2^2 */ - - double& B2 = obs.sigs[f_2].biases [CODE]; - double& var2 = obs.sigs[f_2].biasVars[CODE]; - - if (isnan(B2)) - { - B2 = 0; - var2 = 0; - getBias(trace, obs.time, obs.Sat.id(), obs.Sat, obs.sigs[f_2].code, CODE, B2, var2, kfState_ptr); - } - - double P2 = obs.sigs[f_2].P - B2; - - /* iono-free combination */ - PC = (gamma * P1 - P2) / (gamma - 1); - } - else /* single-frequency */ - { - if (P1 == 0) - { - return false; - } - - double varP1 = 0; - - //get a bias if the default invalid value is still present - if (isnan(obs.sigs[f_1].biases[CODE])) - { - bool pass = getBias(trace, obs.time, obs.Sat.id(), obs.Sat, obs.sigs[f_1].code, CODE, obs.sigs[f_1].biases[CODE], varP1, kfState_ptr); - if (pass == false) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Bias not found in " << __FUNCTION__ << " for " << obs.Sat.id(); - } - } - - PC = P1 - obs.sigs[f_1].biases[CODE]; - } - - range = PC; - measVar = obs.sigs[f_1].codeVar; //todo aaron, use combo? - - if (var1) - biasVar = var1; - - return true; -} +Architecture SPP__() {} -/** Compute ionospheric corrections -*/ -bool ionocorr( - GTime time, ///< Time - VectorPos& pos, ///< Receiver position in LLH - AzEl& azel, ///< Azimuth and elevation - E_IonoMode ionoMode, ///< Ionospheric correction model - double& dion, ///< Ionospheric delay (L1) value output - double& var) ///< Ionospheric delay (L1) variance output +/** Calculate pseudorange and code bias correction + */ +bool prange( + Trace& trace, ///< Trace file to output to + GObs& obs, ///< Observation to calculate pseudorange for + E_IonoMode& ionoMode, ///< Ionospheric correction mode + E_FType& ft_A, ///< Primary frequency used for calculating pseudorange + E_FType& ft_B, ///< Secondary frequency used for calculating pseudorange + double& range, ///< Pseudorange value output + double& measVar, ///< Pseudorange variance output + double& bias, ///< Bias value output + double& biasVar, ///< Bias variance output + KFState* kfState_ptr, ///< Optional kfstate to retrieve biases from + bool smooth = false ///< Update smoothing filter +) { -// trace(4, "ionocorr: time=%s opt=%d sat=%s pos=%.3f %.3f azel=%.3f %.3f\n", -// time.to_string(3).c_str(), -// ionoopt, -// id, -// pos.latDeg(), -// pos.lonDeg(), -// azel[0] *R2D, -// azel[1] *R2D); - - if (ionoMode == +E_IonoMode::IONO_FREE_LINEAR_COMBO) - { - dion = 0; - var = 0; - - return true; - } - - /* broadcast model */ - if (ionoMode == +E_IonoMode::BROADCAST) - { - E_Sys sys = E_Sys::GPS; - E_NavMsgType type = defNavMsgType[sys]; - - auto ion_ptr = seleph(std::cout, time, sys, type, nav); - - double* vals = nullptr; - if (ion_ptr != nullptr) - vals = ion_ptr->vals; - - dion = ionmodel(time, vals, pos, azel); - var = SQR(dion * ERR_BRDCI); - - return true; - } - - - dion = 0; - var = SQR(ERR_ION); - - /* tmp fix : KH - if (ionoopt == E_IonoMode::TOTAL_ELECTRON_CONTENT) - { - int res = iontec(time, &nav, pos, azel, 1, dion, var); - if (!res) - { - dion = 0; - var = SQR(ERR_ION); - } - return res; - } - */ - //if (ionoopt != E_IonoMode::OFF) fprintf(stderr,"SPP not unsupporting ionosphere mode %s", ionoopt._to_string()); - - return true; + ft_A = NONE; + ft_B = NONE; + range = 0; + measVar = 0; + bias = 0; + biasVar = 0; + + E_Sys sys = obs.Sat.sys; + if (sys == E_Sys::NONE) + { + return false; + } + + E_FType f_1; + E_FType f_2; + E_FType f_3; + if (!satFreqs(sys, f_1, f_2, f_3)) + { + return false; + } + + // list ftList = {f_1, f_2, f_3}; + list ftList = {f_1}; // Temporary fix + if (f_2 != f_1) + { + ftList.push_back(f_2); + } + if (f_3 != f_1 && f_3 != f_2) + { + ftList.push_back(f_3); + } + + SatNav& satNav = *obs.satNav_ptr; + auto& lam = satNav.lamMap; + + // Get the first available pseudorange and its code bias according to ftList and update + // ftList + auto getPrange = [&](E_FType& ft, double& meas, double& varMeas, double& bias, double& varBias) + { + while (ftList.size() > 0) + { + ft = ftList.front(); + ftList.pop_front(); + + if (obs.sigs[ft].P == 0 || lam[ft] == 0) + { + ft = NONE; + + continue; + } + + meas = obs.sigs[ft].P; + varMeas = obs.sigs[ft].codeVar; + + // Get a bias if the default invalid value is still present + string sigName = enum_to_string(obs.sigs[ft].code); + auto& satOpts = acsConfig.getSatOpts(obs.Sat, {sigName}); + + bias = 0; + varBias = SQR(satOpts.codeBiasModel.undefined_sigma); + bool pass = getBias( + trace, + obs.time, + obs.Sat.id(), + obs.Sat, + obs.sigs[ft].code, + CODE, + bias, + varBias, + kfState_ptr + ); + + if (pass == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Bias not found for " << obs.Sat.id() << " on " + << enum_to_string(obs.sigs[ft].code) + << ", using undefined_sigma: " << satOpts.codeBiasModel.undefined_sigma; + } + + break; + } + }; + + double P_A = 0; + double var_A = 0; + double bias_A = 0; + double varBias_A = 0; + getPrange(ft_A, P_A, var_A, bias_A, varBias_A); + + if (P_A == 0) // No pseudorange available + { + return false; + } + + range = P_A; + measVar = var_A; + bias = bias_A; + biasVar = varBias_A; + + if (ionoMode == E_IonoMode::IONO_FREE_LINEAR_COMBO) + { + double P_B = 0; + double var_B = 0; + double bias_B = 0; + double varBias_B = 0; + getPrange(ft_B, P_B, var_B, bias_B, varBias_B); + + if (P_B == 0 || ft_B == NONE) + { + BOOST_LOG_TRIVIAL(warning) + << "Code measurement not available on secondary frequency for " << obs.Sat.id() + << " at " << obs.mount << ", falling back to single-frequency"; + + ionoMode = E_IonoMode::BROADCAST; + } + else + { + // Iono-free combination + double c1 = SQR(lam[ft_B]) / (SQR(lam[ft_B]) - SQR(lam[ft_A])); + double c2 = 1 - c1; + + range = c1 * P_A + c2 * P_B; + bias = c1 * bias_A + c2 * bias_B; + + measVar = SQR(c1) * var_A + SQR(c2) * var_B; + biasVar = abs(SQR(c1) * varBias_A - SQR(c2) * varBias_B); // Eugene: bias_A and + // bias_B are expected to be fully correlated? + } + } + + if (acsConfig.sbsInOpts.smth_win > 0) + { + double LC = obs.sigs[ft_A].L * lam[ft_A]; + double varL = obs.sigs[ft_A].phasVar; + if (LC == 0) + return false; + + if (ionoMode == E_IonoMode::IONO_FREE_LINEAR_COMBO) + { + double L2 = obs.sigs[ft_B].L * lam[ft_B]; + if (L2 == 0) + return false; + + double c1 = SQR(lam[ft_B]) / (SQR(lam[ft_B]) - SQR(lam[ft_A])); + double c2 = 1 - c1; + LC = c1 * LC + c2 * L2; + varL = c1 * c1 * varL + c2 * c2 * obs.sigs[ft_B].phasVar; + } + + range = sbasSmoothedPsudo( + trace, + obs.time, + obs.Sat, + obs.mount, + range, + LC, + measVar, + varL, + measVar, + smooth + ); + biasVar = 0; + + if (measVar < 0) + return false; + } + + return true; } - - /** Validate Dilution of Precision of solution -*/ + */ bool validateDOP( - Trace& trace, ///< Trace file to output to - ObsList& obsList, ///< List of observations for this epoch - double elevationMaskDeg, ///< Elevation mask - Dops* dops_ptr = nullptr) ///< Optional pointer to output for DOP + ObsList& obsList, ///< List of observations for this epoch + double elevationMaskDeg, ///< Elevation mask + Dops* dops_ptr = nullptr ///< Optional pointer to output for DOP +) { - vector azels; - azels.reserve(8); - double dop[4] = {}; -// tracepde(3, trace, "valsol : n=%d nv=%d\n", obsList.size(), nv); + vector azels; + azels.reserve(8); + double dop[4] = {}; - // large gdop check - for (auto& obs : only(obsList)) - { - if (obs.exclude) - { - continue; - } + // Large GDOP check + for (auto& obs : only(obsList)) + { + if (obs.exclude) + { + continue; + } - if (obs.sppValid == false) - continue; + if (obs.sppValid == false) + continue; - auto& satStat = *obs.satStat_ptr; + auto& satStat = *obs.satStat_ptr; - if (satStat.el < elevationMaskDeg * D2R) - { - continue; - } + if (satStat.el < elevationMaskDeg * D2R) + { + continue; + } - azels.push_back(satStat); - } + azels.push_back(satStat); + } - Dops dops = dopCalc(azels); + Dops dops = dopCalc(azels); - if (dops_ptr != nullptr) - { - *dops_ptr = dops; - } + if (dops_ptr != nullptr) + { + *dops_ptr = dops; + } - if ( dops.gdop <= 0 - ||dops.gdop > acsConfig.sppOpts.max_gdop) - { - BOOST_LOG_TRIVIAL(info) << "DOP Validation failed with gdop = " << dops.gdop << " on " << obsList.front()->mount; + if (dops.gdop <= 0 || dops.gdop > acsConfig.sppOpts.max_gdop) + { + BOOST_LOG_TRIVIAL(warning) + << "DOP Validation failed for " << obsList.front()->mount << ", gdop=" << dops.gdop; - return false; - } + return false; + } - return true; + return true; } void printFailures( - const string& id, - ObsList& obsList) + const string& id, ///< Id of receiver + ObsList& obsList ///< List of observations for this epoch +) { - InteractiveTerminal ss(string("Failures/") + id, nullStream); - - tracepdeex(4, ss, "\nFailures:"); - tracepdeex(4, ss, "\n%20s ","" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%c", obs.Sat.sysChar() ); - tracepdeex(4, ss, "\n%20s ","" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", obs.Sat.prn/10%10 ); - tracepdeex(4, ss, "\n%20s ","" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", obs.Sat.prn%10 ); - - tracepdeex(4, ss, "\n%20s:","failExclude" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureExclude ); - tracepdeex(4, ss, "\n%20s:","failNoSatPos" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureNoSatPos ); - tracepdeex(4, ss, "\n%20s:","failNoSatClock" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureNoSatClock ); - tracepdeex(4, ss, "\n%20s:","failNoPseudorange" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureNoPseudorange ); - tracepdeex(4, ss, "\n%20s:","failIodeConsistency" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureIodeConsistency ); - tracepdeex(4, ss, "\n%20s:","failBroadcastEph" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureBroadcastEph ); - tracepdeex(4, ss, "\n%20s:","failRSat" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureRSat ); - tracepdeex(4, ss, "\n%20s:","failSSRFail" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSSRFail ); - tracepdeex(4, ss, "\n%20s:","failSsrPosEmpty" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSsrPosEmpty ); - tracepdeex(4, ss, "\n%20s:","failSsrClkEmpty" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSsrClkEmpty ); - tracepdeex(4, ss, "\n%20s:","failSsrPosTime" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSsrPosTime ); - tracepdeex(4, ss, "\n%20s:","failSsrClkTime" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSsrClkTime ); - tracepdeex(4, ss, "\n%20s:","failSsrPosMag" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSsrPosMag ); - tracepdeex(4, ss, "\n%20s:","failSsrClkMag" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSsrClkMag ); - tracepdeex(4, ss, "\n%20s:","failSsrPosUdi" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSsrPosUdi ); - tracepdeex(4, ss, "\n%20s:","failSsrClkUdi" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureSsrClkUdi ); - tracepdeex(4, ss, "\n%20s:","failGeodist" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureGeodist ); - tracepdeex(4, ss, "\n%20s:","failElevation" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureElevation ); - tracepdeex(4, ss, "\n%20s:","failPrange" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failurePrange ); - tracepdeex(4, ss, "\n%20s:","failIonocorr" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.failureIonocorr ); - tracepdeex(4, ss, "\n%20s:","excludeElevation" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.excludeElevation ); - tracepdeex(4, ss, "\n%20s:","excludeEclipse" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.excludeEclipse ); - tracepdeex(4, ss, "\n%20s:","excludeSystem" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.excludeSystem ); - tracepdeex(4, ss, "\n%20s:","excludeOutlier" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.excludeOutlier ); - tracepdeex(4, ss, "\n%20s:","excludeBadSPP" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.excludeBadSPP ); - tracepdeex(4, ss, "\n%20s:","excludeConfig" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.excludeConfig ); - tracepdeex(4, ss, "\n%20s:","excludeSVH" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.excludeSVH ); - tracepdeex(4, ss, "\n%20s:","excludeBadRange" ); for (auto& obs : only(obsList)) tracepdeex(4, ss, "%d", (bool)obs.excludeBadRange ); - - tracepdeex(4, ss, "\n\n"); - - BOOST_LOG_TRIVIAL(debug) << ss.str(); + tracepdeex(4, std::cout, "\nFailures:"); + tracepdeex(4, std::cout, "\n%20s ", ""); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%c", obs.Sat.sysChar()); + tracepdeex(4, std::cout, "\n%20s ", ""); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", obs.Sat.prn / 10 % 10); + tracepdeex(4, std::cout, "\n%20s ", ""); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", obs.Sat.prn % 10); + + tracepdeex(4, std::cout, "\n%20s:", "failExclude"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureExclude); + tracepdeex(4, std::cout, "\n%20s:", "failNoSatPos"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureNoSatPos); + tracepdeex(4, std::cout, "\n%20s:", "failNoSatClock"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureNoSatClock); + tracepdeex(4, std::cout, "\n%20s:", "failNoPseudorange"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureNoPseudorange); + tracepdeex(4, std::cout, "\n%20s:", "failIodeConsistency"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureIodeConsistency); + tracepdeex(4, std::cout, "\n%20s:", "failBroadcastEph"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureBroadcastEph); + tracepdeex(4, std::cout, "\n%20s:", "failRSat"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureRSat); + tracepdeex(4, std::cout, "\n%20s:", "failSSRFail"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSSRFail); + tracepdeex(4, std::cout, "\n%20s:", "failSsrPosEmpty"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSsrPosEmpty); + tracepdeex(4, std::cout, "\n%20s:", "failSsrClkEmpty"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSsrClkEmpty); + tracepdeex(4, std::cout, "\n%20s:", "failSsrPosTime"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSsrPosTime); + tracepdeex(4, std::cout, "\n%20s:", "failSsrClkTime"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSsrClkTime); + tracepdeex(4, std::cout, "\n%20s:", "failSsrPosMag"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSsrPosMag); + tracepdeex(4, std::cout, "\n%20s:", "failSsrClkMag"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSsrClkMag); + tracepdeex(4, std::cout, "\n%20s:", "failSsrPosUdi"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSsrPosUdi); + tracepdeex(4, std::cout, "\n%20s:", "failSsrClkUdi"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureSsrClkUdi); + tracepdeex(4, std::cout, "\n%20s:", "failGeodist"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureGeodist); + tracepdeex(4, std::cout, "\n%20s:", "failElevation"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failureElevation); + tracepdeex(4, std::cout, "\n%20s:", "failPrange"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.failurePrange); + tracepdeex(4, std::cout, "\n%20s:", "excludeElevation"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.excludeElevation); + tracepdeex(4, std::cout, "\n%20s:", "excludeEclipse"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.excludeEclipse); + tracepdeex(4, std::cout, "\n%20s:", "excludeSystem"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.excludeSystem); + tracepdeex(4, std::cout, "\n%20s:", "excludeOutlier"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.excludeOutlier); + tracepdeex(4, std::cout, "\n%20s:", "excludeBadSPP"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.excludeBadSPP); + tracepdeex(4, std::cout, "\n%20s:", "excludeConfig"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.excludeConfig); + tracepdeex(4, std::cout, "\n%20s:", "excludeSVH"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.excludeSVH); + tracepdeex(4, std::cout, "\n%20s:", "excludeBadRange"); + for (auto& obs : only(obsList)) + tracepdeex(4, std::cout, "%d", (bool)obs.excludeBadRange); + + tracepdeex(4, std::cout, "\n\n"); } - - - - - - - - - - - void removeUnmeasuredStates( - Trace& trace, ///< Trace to output to - KFState& kfState, ///< Filter to remove states from - KFMeasEntryList& kfMeasEntryList) ///< List of measurements for this filter iteration + KFState& kfState, ///< Filter to remove states from + KFMeasEntryList& kfMeasEntryList ///< List of measurements for this filter iteration +) { - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type == +KF::ONE) - { - continue; - } - - bool found = false; - - for (auto& measEntry : kfMeasEntryList) - { - auto it = measEntry.designEntryMap.find(key); - if (it != measEntry.designEntryMap.end()) - { - found = true; - break; - } - } - - if (found) - { - continue; - } - - kfState.removeState(key); - } + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type == KF::ONE) + { + continue; + } + + bool found = false; + + for (auto& measEntry : kfMeasEntryList) + { + auto it = measEntry.designEntryMap.find(key); + if (it != measEntry.designEntryMap.end()) + { + found = true; + break; + } + } + + if (found) + { + continue; + } + + kfState.removeState(key); + } } -/** Estimate receiver position and biases using code measurements -*/ +/** Estimate receiver position and clock biases using pseudorange measurements + */ E_Solution estpos( - Trace& trace, ///< Trace file to output to - ObsList& obsList, ///< List of observations for this epoch - Solution& sol, ///< Solution object containing initial conditions and results - string id, ///< Id of receiver - KFState* kfState_ptr = nullptr, ///< Optional kfstate pointer to retrieve ppp values from - string description = "SPP") ///< Description to prepend to clarify outputs + Trace& trace, ///< Trace file to output to + ObsList& obsList, ///< List of observations for this epoch + Solution& sol, ///< Solution object containing initial conditions and results + string id, ///< Id of receiver + KFState* kfState_ptr = nullptr, ///< Optional kfstate pointer to retrieve ppp values from + string description = "SPP", ///< Description to prepend to clarify outputs + bool inRaim = false ///< Is in RAIM +) { - int numMeas = 0; - - if (obsList.empty()) - { - return E_Solution::NONE; - } - - auto& kfState = sol.sppState; - if (acsConfig.sppOpts.always_reinitialise) - { - kfState = KFState(); //reset to zero to prevent lock-in of bad positions - } - - kfState.FilterOptions::operator=(acsConfig.sppOpts); - - int iter; - int removals = 0; - double adjustment = 10000; - - auto& recOpts = acsConfig.getRecOpts(id); - - tracepdeex(5, trace, "\n ---- STARTING SPP LSQ ----"); - for (iter = 0; iter < acsConfig.sppOpts.max_lsq_iterations; iter++) - { - tracepdeex(5, trace, "\nSPP It: %d", iter); - kfState.initFilterEpoch(); - - Vector3d rRec = Vector3d::Zero(); - double dtRec = 0; - - KFKey recPosKeys[3]; - KFKey recSysBiasKey = {KF::REC_SYS_BIAS}; - - for (short i = 0; i < 3; i++) - { - recPosKeys[i].type = KF::REC_POS; - recPosKeys[i].num = i; - recPosKeys[i].str = id; - - kfState.getKFValue(recPosKeys[i], rRec(i)); - } - - VectorPos pos = ecef2pos(rRec); - - if (pos.hgt() > 60'000'000) - { - tracepdeex(5, trace, "\nSPP found unfeasible position with height: %f", pos.hgt()); - return E_Solution::NONE; - } - - KFMeasEntryList kfMeasEntryList; - - for (auto& obs : only(obsList)) - { - if (obs.exclude) - { - obs.failureExclude = true; - - tracepdeex(5, trace, "\n%s spp exclusion : %x", obs.Sat.id().c_str(), obs.exclude); - continue; - } - - recSysBiasKey.Sat = SatSys(obs.Sat.sys); - recSysBiasKey.str = id; - - SatStat& satStat = *obs.satStat_ptr; - - KFMeasEntry codeMeas(&kfState); - - kfState.getKFValue(recSysBiasKey, dtRec); - - obs.sppValid = false; - - Vector3d rSat = obs.rSatApc; - if (rSat.isZero()) - { - obs.failureRSat = true; - - continue; - } - - -// rSat += dtRec / CLIGHT * obs.satVel; - - double r = geodist(rSat, rRec, satStat.e); - int debuglvl = 2; - - // psudorange with code bias correction - double range; - double vMeas; - double vBias; - int pass = prange(trace, obs, acsConfig.sppOpts.iono_mode, range, vMeas, vBias, kfState_ptr); - if (pass == false) - { - obs.failurePrange = true; - - continue; - } - - if (r <= 0) - { - obs.failureGeodist = true; - - tracepdeex(debuglvl, trace, "\n %s Geodist fail", obs.Sat.id().c_str()); - continue; - } - else - { - tracepdeex(debuglvl, trace, "\nSPP sat, %s, %14.3f, %12.3f", obs.Sat.id().c_str(), r, -CLIGHT * obs.satClk); - } - - if (obs.ephPosValid == false) - { - obs.failureNoSatPos = true; - - tracepdeex(debuglvl, trace, " ... Eph Pos fail"); - continue; - } - -// if (obs.ephClkValid == false) -// { -// obs.failureNoSatClock = true; -// -// tracepdeex(debuglvl, trace, " ... Eph clk fail"); -// continue; -// } - - tracepdeex(debuglvl, trace, ", %14.3f", range); + if (obsList.empty()) + { + return E_Solution::NONE; + } + + string suffix = (string) "/" + description; + + auto& recOpts = acsConfig.getRecOpts(id); + + auto& kfState = sol.sppState; + if (acsConfig.sppOpts.always_reinitialise || inRaim) + { + kfState = KFState(); // Reset to apriori to prevent lock-in of bad states + } + + kfState.FilterOptions::operator=(acsConfig.sppOpts); + + if (inRaim) + { + kfState.chiSquareTest.enable = true; // RAIM requires Chi-square test + } + + kfState.output_residuals = false; // Residuals can be outputted each SPP iteration but not each + // least squares iteration + + kfState.measRejectCallbacks.clear(); + kfState.measRejectCallbacks.push_back(deweightMeas); + + int iter; + int numMeas = 0; + double adjustment = 1E7; + bool skipLsqCheck = true; // Don't do outlier screening before nearly converge or in RAIM + Vector3d rRec = receiverMap[id].aprioriPos; + double dtRec = receiverMap[id].aprioriClk; + + tracepdeex(5, trace, "\n\n ---- STARTING SPP LSQ ----"); + + for (iter = 0; iter < acsConfig.sppOpts.max_lsq_iterations; iter++) + { + tracepdeex(2, trace, "\n\nSPP Iteration: %d", iter); + + kfState.initFilterEpoch(trace); + + // Rec apriori pos + KFKey recPosKeys[3]; + for (short i = 0; i < 3; i++) + { + recPosKeys[i].type = KF::REC_POS; + recPosKeys[i].num = i; + recPosKeys[i].str = id; + + kfState.getKFValue(recPosKeys[i], rRec(i)); + } + + tracepdeex(4, trace, "\nSPP apriori pos: %f %f %f", rRec(0), rRec(1), rRec(2)); + + VectorPos pos = ecef2pos(rRec); + if (pos.hgt() > 6E7) + { + tracepdeex( + 3, + trace, + "\n%s\tSPP found unfeasible position with height: %f", + tsync.to_string().c_str(), + pos.hgt() + ); + + return E_Solution::FAILED; + } + + KFMeasEntryList kfMeasEntryList; + + for (auto& obs : only(obsList)) + { + // Reset valid and failure flags that may be updated across iterations + obs.sppValid = false; + obs.failureGeodist = false; + obs.failureElevation = false; + obs.failurePrange = false; + + std::stringstream traceBuffer; + traceBuffer << "\nSPP meas: sat=" << obs.Sat.id().c_str(); + + if (obs.exclude) + { + obs.failureExclude = true; + + traceBuffer << " ... SPP exclusion: " << obs.exclude; + if (obs.excludeSystem) + tracepdeex(6, trace, "%s", traceBuffer.str()); + else + tracepdeex(2, trace, "%s", traceBuffer.str()); + + continue; + } + + // Sat pos + Vector3d rSat = obs.rSatApc; + double varSatPos = obs.posVar; + if (obs.ephPosValid == false || rSat.isZero()) + { + obs.failureNoSatPos = true; + + traceBuffer << " ... Sat pos fail"; + tracepdeex(2, trace, "%s", traceBuffer.str()); + + continue; + } + + SatStat& satStat = *obs.satStat_ptr; + + // Update line-of-sight vector, elevation and observation variance, these should go + // before prange() + double r = geodist(rSat, rRec, satStat.e); + satazel(pos, satStat.e, satStat); + obsVariance(obs); + + // Pseudorange and code bias + E_IonoMode ionoMode = acsConfig.sppOpts.iono_mode; + E_FType ft1; + E_FType ft2; + double range; + double varMeas; + double bias; + double varBias; + bool smooth = (inRaim == false && iter == 0); + bool pass = prange( + trace, + obs, + ionoMode, + ft1, + ft2, + range, + varMeas, + bias, + varBias, + kfState_ptr, + smooth + ); + if (pass == false) + { + obs.failurePrange = true; + + traceBuffer << " ... Pseudorange fail"; + tracepdeex(2, trace, "%s", traceBuffer.str()); + + continue; + } + + string codeStr = enum_to_string(obs.sigs[ft1].code); + if (ft2 != NONE) + codeStr = codeStr + "-" + enum_to_string(obs.sigs[ft2].code); + + tracepdeex( + 2, + trace, + "%s, obs=%s, range=%.3f, bias=%.3f", + traceBuffer.str(), + codeStr, + range, + bias + ); + + // Geodistance + if (r <= 0) + { + obs.failureGeodist = true; + + tracepdeex(2, trace, " ... Geodist fail"); + + continue; + } + + tracepdeex(2, trace, ", dist=%.3f", r); + + // Elevation mask + double elevation = satStat.el * R2D; + if (elevation < acsConfig.sppOpts.elevation_mask_deg && adjustment < 5E4) + { + obs.failureElevation = true; + + tracepdeex(2, trace, " ... Elevation mask fail"); + + continue; + } + + tracepdeex(2, trace, ", el=%.2f", elevation); + + // Sat clock + if (obs.ephClkValid == false) + { + obs.failureNoSatClock = true; + + tracepdeex(2, trace, " ... Sat clk fail"); + + continue; + } + + double dtSat = -obs.satClk * CLIGHT; + double varSatClk = obs.satClkVar * SQR(CLIGHT); + + tracepdeex(2, trace, ", satClk=%.3f", dtSat); + + // Rec clock + KFKey recSysBiasKey{KF::REC_SYS_BIAS, {obs.Sat.sys}, id}; + kfState.getKFValue(recSysBiasKey, dtRec); + + tracepdeex(2, trace, ", recClk=%.3f", dtRec); + + // Ionospheric correction + double dummy = 0; + double dIono = 0; + double varIono = 0; + ionoModel( + obs.time, + pos, + satStat, + recOpts.mapping_function, + ionoMode, + recOpts.mapping_function_layer_height, + dummy, + dIono, + varIono + ); + + if (dIono) + { + double ionC = SQR(obs.satNav_ptr->lamMap[ft1] / genericWavelength[F1]); + dIono *= ionC; + varIono *= SQR(ionC); + } + + if (acsConfig.sbsInOpts.dfmc_uire) + varIono = SQR(0.018 + 40 / (261 + SQR(satStat.el * R2D))); + + tracepdeex(2, trace, ", dIono=%.5f", dIono); + + // Tropospheric correction + double varTrop; + TropStates tropStates; + TropMapping dTropDx; + double dTrop = tropModel( + trace, + acsConfig.sppOpts.trop_models, + obs.time, + pos, + satStat, + tropStates, + dTropDx, + varTrop + ); + + tracepdeex(2, trace, ", dTrop=%.5f", dTrop); + + // Pseudorange residual + double expected = r + bias + dtSat + dtRec + dIono + dTrop; + double res = range - expected; + + if (abs(res) > 7E6) // Assume worst apriori rec pos is 0 and rec clock offset + // can be up to 2ms (usually less than 1ms) + { + obs.failurePrange = true; - satazel(pos, satStat.e, satStat); + tracepdeex(2, trace, " ... Residual fail"); + + continue; + } - if ( satStat.el < recOpts.elevation_mask_deg * D2R - && adjustment < 1000) - { - obs.failureElevation = true; - - tracepdeex(debuglvl, trace, " ... Elevation_mask fail"); - - continue; - } - - tracepdeex(debuglvl, trace, ", %6.2f", satStat.el * R2D); - - // ionospheric corrections - double dion; - double vion; - pass = ionocorr(obs.time, pos, satStat, acsConfig.sppOpts.iono_mode, dion, vion); - if (pass == false) - { - obs.failureIonocorr = true; - - tracepdeex(debuglvl, trace, " ... Ion fail"); - continue; - } - - // GPS-L1 -> L1/B1 - if (obs.Sat.sys == +E_Sys::GLO) { double lamG1 = obs.satNav_ptr->lamMap[G1]; if (lamG1 > 0) dion *= SQR(lamG1 * FREQ1 / CLIGHT); } - if (obs.Sat.sys == +E_Sys::BDS) { double lamB1 = obs.satNav_ptr->lamMap[B1]; if (lamB1 > 0) dion *= SQR(lamB1 * FREQ1 / CLIGHT); } - - tracepdeex(debuglvl, trace, ", %9.5f", dion); - - // tropospheric corrections - double vtrp; - double dryZTD; - double dryMap; - double wetZTD; - double wetMap; - - double dtrp = tropSAAS(trace, obs.time, pos, satStat.el, dryZTD, dryMap, wetZTD, wetMap, vtrp); - - tracepdeex(debuglvl, trace, ", %9.5f", dtrp); - - // pseudorange residual - double expected = r - + dtRec - - obs.satClk * CLIGHT - + dion - + dtrp; - - double res = range - expected; - - for (short i = 0; i < 3; i++) - { - codeMeas.addDsgnEntry(recPosKeys[i], -satStat.e[i]); - } - { -// codeMeas.addDsgnEntry(recClockKey, 1); - } -// if (recSysBiasKey.num != recClockKey.num) - { - codeMeas.addDsgnEntry(recSysBiasKey, 1); - } - - // error variance - if (acsConfig.sppOpts.iono_mode == +E_IonoMode::IONO_FREE_LINEAR_COMBO) - vMeas *= 3; - - double var = vMeas - + obs.satClkVar - + obs.posVar - + vBias - + vion - + vtrp; - - var *= SQR(acsConfig.sppOpts.sigma_scaling); - - codeMeas.obsKey.Sat = obs.Sat; - - codeMeas.setValue(res); - codeMeas.setNoise(var); - - codeMeas.metaDataMap["obs_ptr"] = (void*) &obs; - - kfMeasEntryList.push_back(codeMeas); - - obs.sppValid = true; //todo aaron, this is messy, lots of excludes dont work if spp not run, harmonise the spp/ppp exclusion methods. - obs.sppCodeResidual = res; - } - - //force reinitialisation of everything by least squares - kfState.P.setIdentity(); - kfState.P *= -1; - - removeUnmeasuredStates(trace, kfState, kfMeasEntryList); - - //use state transition to initialise states - kfState.stateTransition(trace, obsList.front()->time); - - //combine the measurement list into a single matrix - numMeas = kfMeasEntryList.size(); - KFMeas kfMeas(kfState, kfMeasEntryList); - - if ( numMeas < kfMeas.H.cols() - 1 - ||numMeas == 0) - { - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << ": lack of valid measurements ns=" << numMeas - << " on " << id << " after " << iter << " iterations." - << " (has " << obsList.size() << " total observations)"; - - printFailures(id, obsList); - - tracepdeex(5, trace, "\nlack of valid measurements, END OF SPP LSQ"); - return E_Solution::NONE; - } - - if ( rRec.isZero() - &&iter == 0) - { - kfMeas.Y /= 2; - } - - VectorXd dx; - kfState.leastSquareInitStates(trace, kfMeas, true, &dx); - - if (traceLevel >= 4) - { - kfMeas.V = kfMeas.Y; - outputResiduals(trace, kfMeas, iter, (string)"/" + description, 0, kfMeas.H.rows()); - } - adjustment = dx.norm(); - tracepdeex(4, trace, "\nSPP dx: %15.4f\n", adjustment); - - - if ( adjustment < 4000 - &&acsConfig.sppOpts.postfitOpts.sigma_check - &&removals < acsConfig.sppOpts.postfitOpts.max_iterations) - { - //use 'array' for component-wise calculations - auto measVariations = kfMeas.Y.array().square(); //delta squared - - auto measVariances = kfMeas.R.diagonal().array().max(SQR(adjustment)); - - // trace << measVariances.sqrt(); - - ArrayXd measRatios = measVariations / measVariances; - measRatios = measRatios.isFinite() .select(measRatios, 0); - - //if any are outside the expected values, flag an error - - Eigen::ArrayXd::Index measIndex; - -// std::cout << "\nmeasRatios\n" << measRatios; - - double maxMeasRatio = measRatios .maxCoeff(&measIndex); - - if (maxMeasRatio > SQR(acsConfig.sppOpts.postfitOpts.meas_sigma_threshold)) - { - trace << "\n" << "LARGE MEAS ERROR OF " << maxMeasRatio << " AT " << measIndex << " : " << kfMeas.obsKeys[measIndex]; - - GObs& badObs = *(GObs*) kfMeas.metaDataMaps[measIndex]["obs_ptr"]; - - badObs.excludeOutlier = true; - continue; - } - } - - if (adjustment < 1E-4) - { - double dtRec_m = 0; - kfState.getKFValue({KF::REC_SYS_BIAS, SatSys(E_Sys::GPS), id}, dtRec_m); - - sol.numMeas = numMeas; - sol.sppTime = obsList.front()->time - dtRec_m / CLIGHT; - - if (traceLevel >= 4) - { - kfState.outputStates(trace, (string)"/" + description); - } - - if (kfState.chiSquareTest.enable) - { - double a = sqrt( kfState.P(1,1) + kfState.P(2,2) + kfState.P(3,3) ) * kfState.chi / kfState.dof; - double b = sqrt( kfState.P(4,4) ) * kfState.chi / kfState.dof; - - tracepdeex(4, trace, "chi2stats: chi = %10f\n", kfState.chi); - tracepdeex(4, trace, "chi2stats: dof = %10f\n", kfState.dof); - tracepdeex(5, trace, "chi2stats: sqrt(var_pos) * chi^2/dof = %10f\n", a); - tracepdeex(5, trace, "chi2stats: sqrt(var_dclk)* chi^2/dof = %10f\n", b); - - if (kfState.chiQCPass == false) - { - tracepdeex(4, trace, " - Bad chiQC"); - - return E_Solution::SINGLE_X; - } - } - - bool dopPass = validateDOP(trace, obsList, recOpts.elevation_mask_deg, &sol.dops); - if (dopPass == false) - { - tracepdeex(4, trace, " - Bad DOP %f", sol.dops.gdop); - - return E_Solution::SINGLE_X; - } - - return E_Solution::SINGLE; - } - } - tracepdeex(5, trace, "\n ---- END OF SPP LSQ, iterations = %d ----", iter); - - if (iter >= acsConfig.sppOpts.max_lsq_iterations) - { - BOOST_LOG_TRIVIAL(debug) << "SPP failed to converge after " << iter << " iterations for " << id << " - " << description; - } - - return E_Solution::NONE; + tracepdeex(2, trace, ", res=%.6f", res); + + // Error variance + double var = varMeas + varBias + varSatPos + varSatClk + varIono + varTrop; + var *= SQR(acsConfig.sppOpts.sigma_scaling); + + tracepdeex( + 5, + trace, + "\nSPP meas: sat=%s, obs=%s, varMeas=%f, varBias=%f, varSatPos=%f, varSatClk=%f, " + "varIono=%f, varTrop=%f, scale=%f, var=%f", + obs.Sat.id().c_str(), + codeStr, + varMeas, + varBias, + varSatPos, + varSatClk, + varIono, + varTrop, + acsConfig.sppOpts.sigma_scaling, + var + ); + + // Calculate design entry and form up the measurement + KFMeasEntry codeMeas(&kfState); + + for (short i = 0; i < 3; i++) + { + InitialState posInit; + posInit.x = rRec[i]; + + codeMeas.addDsgnEntry(recPosKeys[i], -satStat.e[i], posInit); + } + { + InitialState clkInit; + clkInit.x = dtRec; + + codeMeas.addDsgnEntry(recSysBiasKey, 1, clkInit); + } + + 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[ft1].code); + codeMeas.obsKey.type = KF::CODE_MEAS; + codeMeas.obsKey.comment = ""; + + codeMeas.setInnov(res); + codeMeas.setNoise(var); + + codeMeas.metaDataMap["sppObs_ptr"] = &obs; + + kfMeasEntryList.push_back(codeMeas); + + obs.sppValid = true; // todo aaron, this is messy, lots of excludes dont work if spp + // not run, harmonise the spp/ppp exclusion methods. + obs.sppCodeResidual = res; + } + + tracepdeex(2, trace, "\n"); + + // Force reinitialisation of everything by least squares + kfState.P.setIdentity(); + kfState.P *= -1; + + removeUnmeasuredStates(kfState, kfMeasEntryList); + + // Use state transition to initialise states + kfState.stateTransition(trace, tsync); + + // Combine the measurement list into a single matrix + numMeas = kfMeasEntryList.size(); + sol.numMeas = numMeas; + + KFMeas kfMeas(kfState, kfMeasEntryList, tsync); + + if (kfMeas.H.cols() == 0) + { + printFailures(id, obsList); + + tracepdeex(3, trace, "\nNo valid states, END OF SPP LSQ"); + + return E_Solution::NONE; + } + + if (numMeas < kfMeas.H.cols() - 1 || numMeas == 0) + { + printFailures(id, obsList); + + tracepdeex(3, trace, "\nLack of valid measurements, END OF SPP LSQ"); + + return E_Solution::FAILED; + } + + // Least squares estimation + bool pass = kfState.leastSquareInitStates(trace, kfMeas, suffix, true, true, skipLsqCheck); + + if (pass == false) + { + tracepdeex(4, trace, "\n%s\tSPP failed due to LSQ failure", tsync.to_string().c_str()); + return E_Solution::FAILED; + } + + if (traceLevel >= 4) + { + outputResiduals(trace, kfMeas, suffix, iter, 0, kfMeas.H.rows()); + } + + if (traceLevel >= 5) + { + kfState.outputStates(trace, suffix, iter); + } + + adjustment = + kfState.dx.cwiseAbs().maxCoeff(); // Avoid using norm() as numX may vary w/ multi-GNSS + tracepdeex(4, trace, "\nSPP dx: %15.4f\n", adjustment); + + if (inRaim == false && adjustment < 2E5) + { + // Only check outliers (if turned on) after nearly converge + skipLsqCheck = false; + } + + if ((kfState.lsqOpts.sigma_check || kfState.lsqOpts.omega_test) && skipLsqCheck == false && + kfState.sigmaPass == false) + { + // Outlier(s) still present, iterate SPP again despite convergence + continue; + } + + // Least squares converged + if (adjustment < 1E-4) + { + if (traceLevel == 4) + { + kfState.outputStates(trace, suffix); + } + + if (kfState.chiSquareTest.enable) // todo Eugene: use meas chi-square test in algebra + { + double a = + sqrt(kfState.P(1, 1) + kfState.P(2, 2) + kfState.P(3, 3)) * kfState.chi2PerDof; + double b = sqrt(kfState.P(4, 4)) * kfState.chi2PerDof; + + tracepdeex(4, trace, "\nchi2stats: chi^2 = %10f", kfState.chi2); + tracepdeex(4, trace, "\nchi2stats: dof = %10f", kfState.dof); + tracepdeex(4, trace, "\nchi2stats: chi^2/dof = %10f", kfState.chi2PerDof); + tracepdeex(5, trace, "\nchi2stats: sqrt(varPos) * chi^2/dof = %10f", a); + tracepdeex(5, trace, "\nchi2stats: sqrt(varClk) * chi^2/dof = %10f", b); + + if (kfState.chiQCPass == false) + { + tracepdeex(3, trace, "\n%s\tSPP error - Bad chiQC", tsync.to_string().c_str()); + + return E_Solution::SINGLE_X; + } + } + + bool dopPass = validateDOP(obsList, acsConfig.sppOpts.elevation_mask_deg, &sol.dops); + if (dopPass == false) + { + tracepdeex( + 3, + trace, + "\n%s\tSPP error - Bad DOP: %f", + tsync.to_string().c_str(), + sol.dops.gdop + ); + + return E_Solution::SINGLE_X; + } + + return E_Solution::SINGLE; + } + } + + tracepdeex(5, trace, "\n ---- END OF SPP LSQ, iterations = %d ----", iter); + + if (iter >= acsConfig.sppOpts.max_lsq_iterations) + { + tracepdeex( + 3, + trace, + "\n%s\tSPP failed to converge after %d iterations", + tsync.to_string().c_str(), + iter + ); + } + + if (traceLevel == 4) + { + // Still output states if fails + kfState.outputStates(trace, suffix); + } + + return E_Solution::FAILED; } /** Receiver autonomous integrity monitoring (RAIM) failure detection and exclution -*/ + * Note: This is a simplified version of RAIM algorithm that tries to exclude multiple outliers + * iteratively instead of checking all possible subsets when more than one outliers present + */ bool raim( - Trace& trace, ///< Trace file to output to - ObsList& obsList, ///< List of observations for this epoch - Solution& sol, ///< Solution object containing initial conditions and results - string id, ///< Id of receiver - KFState* kfState_ptr = nullptr) + Trace& trace, ///< Trace file to output to + ObsList& obsList, ///< List of observations for this epoch + Solution& sol, ///< Solution object containing initial conditions and results + string id, ///< Id of receiver + KFState* kfState_ptr = nullptr +) { - trace << "\n" << "Performing RAIM."; - - SatSys exSat; - double bestRms = 100; - - map satStatBak; - map satStatBest; - - auto backupSatStats = [&](map& dest, bool backup) - { - for (auto& obs : only(obsList)) - { - if (obs.exclude) - { - continue; - } - - if (backup) dest[obs.Sat] = *obs.satStat_ptr; - else *obs.satStat_ptr = dest[obs.Sat]; - } - }; - - backupSatStats(satStatBak, true); - - for (auto& testObs : only(obsList)) - { - if (testObs.exclude) - { - continue; - } - - map satStatBak; - - backupSatStats(satStatBak, false); - - ObsList testList; - - //push a list of everything thats not the test observation - for (auto& obs : only(obsList)) - { - if (&obs == &testObs) { continue; } - if (obs.exclude) { continue; } - - testList.push_back((shared_ptr)obs); - } - - Solution sol_e = sol; - - //avoid lock-in for raim despite config - sol_e.sppState = KFState(); - - //try to get position using test subset of all observations - E_Solution status = estpos(trace, testList, sol_e, id, kfState_ptr, (string)"RAIM/" + id + "/" + testObs.Sat.id()); - if (status != +E_Solution::SINGLE) - { - continue; - } - - int numSat = 0; - double testRms = 0; - - for (auto& obs : only(testList)) - { - if (obs.sppValid == false) - continue; - - testRms += SQR(obs.sppCodeResidual); - numSat++; - } - - testRms = sqrt(testRms / numSat); - - if (numSat < 5) - { - tracepdeex(3, trace, "%s: exsat=%s lack of satellites numSat=%2d\n", __FUNCTION__, testObs.Sat.id().c_str(), numSat); - continue; - } - - tracepdeex(3, trace, "%s: exsat=%s rms=%8.3f\n", __FUNCTION__, testObs.Sat.id().c_str(), testRms); - - if (testRms > bestRms) - { - //this solution is worse - continue; - } - - // copy test obs to real result - for (auto& testObs : only(testList)) - for (auto& origObs : only(obsList)) - { - if (testObs.Sat != origObs.Sat) - { - //only use the equivalent obs in the real list according to the test list - continue; - } - - origObs.sppValid = testObs.sppValid; - origObs.sppCodeResidual = testObs.sppCodeResidual; - } - - backupSatStats(satStatBest, true); - - sol = sol_e; - sol.status = E_Solution::SINGLE; - exSat = testObs.Sat; - bestRms = testRms; - testObs.sppValid = false; - } - - if ((int) exSat) - { - backupSatStats(satStatBest, false); - - tracepdeex(3, trace, "\n%s: %s excluded by RAIM", obsList.front()->time.to_string().c_str(), exSat.id().c_str()); - BOOST_LOG_TRIVIAL(debug) << "SPP converged after " << exSat.id() << " was excluded for " << obsList.front()->mount; - - return true; - } - - return false; + trace << "\n" << tsync << "\tPerforming RAIM."; + + vector exList; + ObsList testList = obsList; + ObsList bestList; + + Solution bestSol = sol; + double bestVar = bestSol.sppState.chi2PerDof; + + map origSatStats; + map bestSatStats; + + auto backupSatStats = [&](map& dest, bool backup) + { + for (auto& obs : only(obsList)) + { + if (obs.exclude) + { + continue; + } + + if (backup) + dest[obs.Sat] = *obs.satStat_ptr; + else + *obs.satStat_ptr = dest[obs.Sat]; + } + }; + + // Backup original satStats + backupSatStats(origSatStats, true); + + int iter; + for (iter = 0; iter < acsConfig.sppOpts.raim.max_iterations; iter++) + { + tracepdeex(4, trace, "\n\nRAIM Iteration: %d", iter); + + if (bestSol.sppState.dof < 2) // Need dof >=2, otherwise all candidate subsets pass equally + { + tracepdeex( + 3, + trace, + "\n%s: lack of satellites to perform RAIM, numSat=%2d", + __FUNCTION__, + bestSol.numMeas + ); + return false; + } + + GObs* exObs_ptr = nullptr; + + for (auto& testObs : only(testList)) + { + if (testObs.exclude) + { + continue; + } + + // Restore original satStats before each test + backupSatStats(origSatStats, false); + + ObsList candList; + + // Push a list of candidate subset, i.e. everything that's not the test observation + for (auto& obs : only(testList)) + { + if (&obs == &testObs) + { + continue; + } + if (obs.exclude) + { + continue; + } + + candList.push_back((shared_ptr)obs); + } + + Solution candSol = sol; + + // Try to get position using candidate subset of all observations + E_Solution status = estpos( + trace, + candList, + candSol, + id, + kfState_ptr, + (string) "RAIM/" + id + "/" + testObs.Sat.id(), + true // In RAIM, skip sigma check and omega test to avoid interferance with RAIM + ); + if (status == E_Solution::NONE) // Could have multiple bad measurements and may need + // to exclude one worst obs first (in case all sebsets + // fail but NONE solutions) + { + continue; + } + + if (candSol.sppState.dof < 1) + { + tracepdeex( + 3, + trace, + "\n%s: exSat=%s, lack of satellites nSat=%2d", + __FUNCTION__, + testObs.Sat.id().c_str(), + candSol.numMeas + ); + + continue; + } + + double candVar = candSol.sppState.chi2PerDof; + tracepdeex( + 3, + trace, + "\n%s: exSat=%s, chi^2/dof=%f, status=%s", + __FUNCTION__, + testObs.Sat.id().c_str(), + candVar, + enum_to_string(status) + ); + tracepdeex( + 5, + trace, + "\nBest: exSat=%s, chi^2/dof=%f, status=%s", + (exObs_ptr ? exObs_ptr->Sat.id() : "Non"), + bestVar, + enum_to_string(bestSol.status) + ); + + if ((status < bestSol.status) || (status == bestSol.status && candVar > bestVar)) + { + // This solution is worse + continue; + } + + bestSol = candSol; + bestSol.status = status; + bestVar = candVar; + exObs_ptr = &testObs; + bestList = candList; + + // Store 'best' satStats for later use + backupSatStats(bestSatStats, true); + } + + if (exObs_ptr) + { + exList.push_back(exObs_ptr); + + // Exclude corresponding obs from test list + for (auto it = testList.begin(); it != testList.end(); it++) + { + if (it->get() == exObs_ptr) + { + testList.erase(it); + + break; + } + } + + if (bestSol.status == E_Solution::SINGLE) + { + sol = bestSol; + + backupSatStats( + bestSatStats, + false + ); // Update satStats (AzEl and line-of-sight unit vector) w/ best SPP solution + + // Copy best obs to real result + for (auto& bestObs : only(bestList)) + for (auto& origObs : only(obsList)) + { + if (bestObs.Sat != origObs.Sat) + { + // Only use the equivalent obs in the real list according to the best + // list + continue; + } + + origObs.sppValid = bestObs.sppValid; + origObs.sppCodeResidual = bestObs.sppCodeResidual; + } + + for (auto& obs_ptr : exList) + { + obs_ptr->excludeOutlier = true; + + tracepdeex( + 3, + trace, + "\n%s\t%s excluded by RAIM", + tsync.to_string().c_str(), + obs_ptr->Sat.id().c_str() + ); + BOOST_LOG_TRIVIAL(debug) << obs_ptr->Sat.id() << " was excluded from " + << obsList.front()->mount << " by RAIM"; + } + + BOOST_LOG_TRIVIAL(debug) << "SPP converged after RAIM"; + + return true; + } + } + else + { + break; // No observation excluded, stop iterating as testList hasn't changed + } + } + + // Restore original satStats if fails + backupSatStats(origSatStats, false); + + tracepdeex(3, trace, "\n%s\tRAIM failed after %d iterations", tsync.to_string().c_str(), iter); + BOOST_LOG_TRIVIAL(debug) << "RAIM failed after " << iter << " iterations"; + + return false; } -/** Compute receiver position, velocity, clock bias by single-point positioning with pseudorange observables -*/ +/** Compute receiver position, clock biases by single-point positioning with pseudorange + * measurements + */ void spp( - Trace& trace, ///< Trace file to output to - ObsList& obsList, ///< List of observations for this epoch - Solution& sol, ///< Solution object containing initial state and results - string id, ///< Id of receiver - KFState* kfState_ptr, ///< Optional pointer to filter to take ephemerides from - KFState* remote_ptr) ///< Optional pointer to filter to take ephemerides from + Trace& trace, ///< Trace file to output to + Receiver& rec, ///< Receiver to perform SPP for + KFState* kfState_ptr, ///< Optional pointer to filter to take ephemerides from + KFState* remote_ptr ///< Optional pointer to filter to take ephemerides from +) { - if (obsList.empty()) - { - BOOST_LOG_TRIVIAL(info) << "SPP failed due to no observation data on " << id; - - sol.status = E_Solution::NONE; - - return; - } - - for (auto& obs : only(obsList)) - { - if (acsConfig.process_sys[obs.Sat.sys] == false) - { - continue; - } - - auto& satOpts = acsConfig.getSatOpts(obs.Sat); - - satPosClk(trace, obs.time, obs, nav, satOpts.posModel.sources, satOpts.clockModel.sources, kfState_ptr, remote_ptr, E_OffsetType::APC); - } - - tracepdeex(3,trace, "\n%s : tobs=%s n=%zu", __FUNCTION__, obsList.front()->time.to_string().c_str(), obsList.size()); - - //estimate receiver position with pseudorange - sol.status = estpos(trace, obsList, sol, id, kfState_ptr, (string) "SPP/" + id); //todo aaron, remote too? - - if (sol.status != +E_Solution::SINGLE) - { - trace << "\n" << "Spp error with " << sol.numMeas << " measurements."; - - //Receiver Autonomous Integrity Monitoring - if ( sol.numMeas >= 6 //need 6 so that 6-1 is still overconstrained, otherwise they all pass equally. - &&acsConfig.sppOpts.raim) - { - raim(trace, obsList, sol, id); - } - } - - if (sol.status != +E_Solution::SINGLE) - { - BOOST_LOG_TRIVIAL(debug) << "SPP failed for " << id; - trace << "SPP failed for " << id; - } - - //set observations that were valid - for (auto& obs : only(obsList)) - { - if ( sol.status != +E_Solution::SINGLE - &&sol.status != +E_Solution::SINGLE_X) - { - //all measurements are bad if we cant get spp - // printf("\n all spp bad"); - obs.excludeBadSPP = true; - continue; - } - - if ( obs.exclude - ||obs.sppValid) - { - continue; - } - - // printf("\n %s spp bad", obs.Sat.id().c_str()); - obs.excludeBadSPP = true; - } - - sol.sppState.outputStates(trace, "/SPP/" + id); - - //copy states to often-used vectors - for (short i = 0; i < 3; i++) { sol.sppState.getKFValue({KF::REC_POS, {}, id, i}, sol.sppRRec[i]); } - for (short i = E_Sys::GPS; i < +E_Sys::SUPPORTED; i++) { E_Sys sys = E_Sys::_values()[i]; sol.sppState.getKFValue({KF::REC_SYS_BIAS, {sys}, id}, sol.dtRec_m[sys]); } - - tracepdeex(3, trace, "\n%s sol: %f %f %f", __FUNCTION__, sol.sppRRec[0], sol.sppRRec[1], sol.sppRRec[2]); - tracepdeex(3, trace, "\n%s clk: %f\n", __FUNCTION__, sol.dtRec_m[E_Sys::GPS]); + auto& obsList = rec.obsList; + auto& sol = rec.sol; + string id = rec.id; + + if (obsList.empty()) + { + BOOST_LOG_TRIVIAL(error) << "SPP failed due to no observation data on " << id; + + sol.status = E_Solution::NONE; + + return; + } + + for (auto& obs : only(obsList)) + { + if (acsConfig.process_sys[obs.Sat.sys] == false) + { + continue; + } + + if (obs.ephPosValid == false || obs.ephClkValid == false || + acsConfig.preprocOpts.preprocess_all_data == + true) // KALMAN or REMOTE is not available, or SSR is not updated when + // preprocessing all data + { + auto& satOpts = acsConfig.getSatOpts(obs.Sat); + + satPosClk( + trace, + obs.time, + obs, + nav, + satOpts.posModel.sources, + satOpts.clockModel.sources, + kfState_ptr, + remote_ptr, + E_OffsetType::APC + ); + } + } + + tracepdeex( + 3, + trace, + "\n\n%s: time=%s, nObs=%zu", + __FUNCTION__, + tsync.to_string().c_str(), + obsList.size() + ); + + // Estimate receiver position with pseudorange + sol.status = estpos(trace, obsList, sol, id, kfState_ptr, (string) "SPP/" + id); // todo aaron, + // remote too? + + auto& sppState = sol.sppState; + + if (sol.status != E_Solution::SINGLE) + { + // Receiver Autonomous Integrity Monitoring + if (acsConfig.sppOpts.raim.enable && + sol.status != E_Solution::NONE) // Meaningless to perform RAIM for NONE solution + { + int numMeas = 0; + for (auto& obs : only(obsList)) + { + obs.excludeOutlier = + false; // Clear outlier flags from SPP and let RAIM do the exclusion + + if (obs.exclude) + { + continue; + } + + numMeas++; + } + + sppState.dof = numMeas - (sppState.x.rows() - 1); + sppState.chi2PerDof = INFINITY; + + bool pass = raim(trace, obsList, sol, id, kfState_ptr); + + if (pass && traceLevel >= 4) + { + sppState.outputStates( + trace, + "/SPP/" + id + ); // Only output states again when RAIM is successful + } + } + } + + if (sol.status != E_Solution::SINGLE && sol.status != E_Solution::SINGLE_X) + { + string stringBuffer = acsConfig.exclude.bad_spp ? ", excluding all observations" : ""; + BOOST_LOG_TRIVIAL(warning) << "SPP failed for " << id << " at " << tsync << stringBuffer; + trace << "\n" << tsync << "\tSPP failed for " << id << stringBuffer; + } + + // Set observations that were valid + for (auto& obs : only(obsList)) + { + if (sol.status != E_Solution::SINGLE && sol.status != E_Solution::SINGLE_X) + { + // All measurements are bad if we cant get SPP + obs.excludeBadSPP = true; + continue; + } + + if (obs.exclude || obs.sppValid) + { + continue; + } + + if (obs.failureElevation) // Don't exclude low elevation obs here as PPP may use a + // different elevation mask + { + continue; + } + + obs.excludeBadSPP = true; + } + + if (sol.status == E_Solution::NONE) + { + return; + } + + // Copy states to often-used vectors + for (short i = 0; i < 3; i++) + { + sppState.getKFValue({KF::REC_POS, {}, id, i}, sol.sppPos[i]); + } + + auto& recOpts = acsConfig.getRecOpts(id); + sol.clkRefSys = recOpts.receiver_reference_system; + + KFKey clkKey{KF::REC_SYS_BIAS, {sol.clkRefSys}, id}; + E_Source foundSrc = sol.sppState.getKFValue(clkKey, sol.sppClk); + bool found = foundSrc != E_Source::NONE; + + if (found == false) + { + for (auto [sys, proc] : acsConfig.process_sys) + { + if (proc == false || sys == sol.clkRefSys) // Has already checked sol.clkRefSys + { + continue; + } + + clkKey.Sat = SatSys(sys); + foundSrc = sol.sppState.getKFValue(clkKey, sol.sppClk); + found = foundSrc != E_Source::NONE; + + if (found) // Rec clock must be available for at least one system + { + sol.clkRefSys = sys; + break; + } + } + + BOOST_LOG_TRIVIAL(warning) + << "Receiver clock for " << id << " of " + << enum_to_string(recOpts.receiver_reference_system) << " system not found, using " + << enum_to_string(sol.clkRefSys) << " as reference clock"; + } + + sol.sppTime = tsync - sol.sppClk / CLIGHT; + + sol.horzPL = -1; + sol.vertPL = -1; + if (sol.status == E_Solution::SINGLE) + { + Matrix3d ecefP = sol.sppState.P.block(1, 1, 3, 3); + estimateSBASProtLvl(sol.sppPos, ecefP, sol.horzPL, sol.vertPL); + } + + tracepdeex( + 2, + trace, + "\n%s pos: %f %f %f", + __FUNCTION__, + sol.sppPos[0], + sol.sppPos[1], + sol.sppPos[2] + ); + tracepdeex( + 2, + trace, + "\n%s clk (%s): %f\n", + __FUNCTION__, + enum_to_string(sol.clkRefSys), + sol.sppClk + ); + + mongoStates( + sppState, + {.suffix = "/SPP", + .instances = acsConfig.mongoOpts.output_states, + .queue = acsConfig.mongoOpts.queue_outputs} + ); } - diff --git a/src/cpp/rtklib/lambda.cpp b/src/cpp/rtklib/lambda.cpp index e38ed51a0..a7d7ecdbc 100644 --- a/src/cpp/rtklib/lambda.cpp +++ b/src/cpp/rtklib/lambda.cpp @@ -1,443 +1,670 @@ /*------------------------------------------------------------------------------ -* lambda.c : integer ambiguity resolution -* -* Copyright (C) 2007-2008 by T.TAKASU, All rights reserved. -* -* reference : -* [1] P.J.G.Teunissen, The least-square ambiguity decorrelation adjustment: -* a method for fast GPS ambiguity estimation, J.Geodesy, Vol.70, 65-82, -* 1995 -* [2] X.-W.Chang, X.Yang, T.Zhou, MLAMBDA: A modified LAMBDA method for -* integer least-squares estimation, J.Geodesy, Vol.79, 552-565, 2005 -* -*-----------------------------------------------------------------------------*/ - - + * lambda.c : integer ambiguity resolution + * + * Copyright (C) 2007-2008 by T.TAKASU, All rights reserved. + * + * reference : + * [1] P.J.G.Teunissen, The least-square ambiguity decorrelation adjustment: + * a method for fast GPS ambiguity estimation, J.Geodesy, Vol.70, 65-82, + * 1995 + * [2] X.-W.Chang, X.Yang, T.Zhou, MLAMBDA: A modified LAMBDA method for + * integer least-squares estimation, J.Geodesy, Vol.79, 552-565, 2005 + * + *-----------------------------------------------------------------------------*/ + +#include #include - -#include "algebra.hpp" -#include "common.hpp" -#include "trace.hpp" - +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/trace.hpp" /* constants/macros ----------------------------------------------------------*/ -#define LOOPMAX 10000 /* maximum count of search loop */ - -const double table1[34][41] = -{ - {0.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000}, - {0.0010,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000}, - {0.0020,0.7796,0.7839,0.8158,0.8053,0.8151,0.8233,0.8312,0.8408,0.8515,0.8621,0.8623,0.8625,0.8710,0.8795,0.8820,0.8874,0.8888,0.8902,0.9190,0.9478,0.8985,0.9057,0.9075,0.9092,0.9129,0.9165,0.9091,0.9203,0.9231,0.9259,0.9348,0.9436,0.9436,0.9436,0.9436,0.9436,0.9436,0.9436,0.9436,0.9436}, - {0.0050,0.5408,0.5432,0.5715,0.5735,0.5889,0.6388,0.6604,0.6848,0.6940,0.7031,0.7096,0.7162,0.7339,0.7516,0.7484,0.7643,0.7771,0.7898,0.8020,0.8142,0.8033,0.8079,0.8127,0.8175,0.8264,0.8353,0.8316,0.8419,0.8463,0.8507,0.8600,0.8692,0.8692,0.8692,0.8692,0.8692,0.8692,0.8692,0.8692,0.8692}, - {0.0100,0.3651,0.3869,0.4131,0.4338,0.4491,0.5084,0.5358,0.5679,0.5814,0.5948,0.6051,0.6153,0.6355,0.6557,0.6762,0.6826,0.6964,0.7102,0.7174,0.7247,0.7315,0.7453,0.7470,0.7488,0.7632,0.7777,0.7764,0.7892,0.7930,0.7967,0.8215,0.8463,0.8463,0.8463,0.8463,0.8463,0.8463,0.8463,0.8463,0.8463}, - {0.0150,0.2687,0.2984,0.3271,0.3608,0.3767,0.4340,0.4647,0.5123,0.5256,0.5390,0.5530,0.5670,0.5863,0.6056,0.6278,0.6369,0.6471,0.6572,0.6692,0.6812,0.7035,0.7161,0.7208,0.7255,0.7372,0.7490,0.7482,0.7644,0.7677,0.7709,0.7989,0.8270,0.8270,0.8270,0.8270,0.8270,0.8270,0.8270,0.8270,0.8270}, - {0.0200,0.2138,0.2428,0.2694,0.3009,0.3301,0.3860,0.4186,0.4797,0.4934,0.5072,0.5183,0.5294,0.5516,0.5737,0.5952,0.6100,0.6168,0.6236,0.6434,0.6632,0.6816,0.6959,0.7016,0.7072,0.7188,0.7303,0.7296,0.7461,0.7497,0.7532,0.7814,0.8095,0.8095,0.8095,0.8095,0.8095,0.8095,0.8095,0.8095,0.8095}, - {0.0250,0.1885,0.2177,0.2418,0.2656,0.2904,0.3493,0.3858,0.4531,0.4672,0.4813,0.4910,0.5007,0.5248,0.5489,0.5714,0.5920,0.5999,0.6079,0.6285,0.6491,0.6645,0.6816,0.6870,0.6925,0.7060,0.7194,0.7128,0.7327,0.7375,0.7422,0.7680,0.7939,0.7939,0.7939,0.7939,0.7939,0.7939,0.7939,0.7939,0.7939}, - {0.0300,0.1709,0.1983,0.2195,0.2418,0.2640,0.3202,0.3595,0.4317,0.4458,0.4599,0.4714,0.4829,0.5079,0.5329,0.5518,0.5765,0.5859,0.5953,0.6166,0.6379,0.6515,0.6697,0.6752,0.6808,0.6961,0.7114,0.6987,0.7240,0.7294,0.7348,0.7574,0.7800,0.7800,0.7800,0.7800,0.7800,0.7800,0.7800,0.7800,0.7800}, - {0.0350,0.1580,0.1833,0.2014,0.2221,0.2439,0.2989,0.3374,0.4147,0.4285,0.4423,0.4552,0.4680,0.4957,0.5234,0.5371,0.5634,0.5740,0.5847,0.6068,0.6288,0.6416,0.6582,0.6650,0.6717,0.6879,0.7042,0.6889,0.7187,0.7234,0.7282,0.7480,0.7677,0.7677,0.7677,0.7677,0.7677,0.7677,0.7677,0.7677,0.7677}, - {0.0400,0.1473,0.1713,0.1867,0.2058,0.2257,0.2826,0.3201,0.4011,0.4145,0.4279,0.4412,0.4546,0.4845,0.5145,0.5276,0.5524,0.5641,0.5758,0.5987,0.6216,0.6339,0.6480,0.6562,0.6644,0.6810,0.6977,0.6847,0.7138,0.7180,0.7222,0.7396,0.7570,0.7570,0.7570,0.7570,0.7570,0.7570,0.7570,0.7570,0.7570}, - {0.0450,0.1382,0.1609,0.1745,0.1921,0.2093,0.2693,0.3063,0.3902,0.4031,0.4160,0.4293,0.4426,0.4745,0.5063,0.5199,0.5435,0.5560,0.5684,0.5921,0.6157,0.6277,0.6395,0.6491,0.6586,0.6752,0.6919,0.6823,0.7093,0.7131,0.7170,0.7323,0.7477,0.7477,0.7477,0.7477,0.7477,0.7477,0.7477,0.7477,0.7477}, - {0.0500,0.1305,0.1508,0.1640,0.1803,0.1947,0.2568,0.2940,0.3808,0.3933,0.4059,0.4191,0.4323,0.4655,0.4987,0.5133,0.5365,0.5495,0.5625,0.5865,0.6106,0.6223,0.6331,0.6433,0.6536,0.6702,0.6868,0.6801,0.7050,0.7087,0.7123,0.7260,0.7397,0.7397,0.7397,0.7397,0.7397,0.7397,0.7397,0.7397,0.7397}, - {0.0550,0.1240,0.1418,0.1544,0.1697,0.1818,0.2447,0.2832,0.3718,0.3844,0.3971,0.4103,0.4236,0.4577,0.4918,0.5075,0.5313,0.5445,0.5577,0.5818,0.6059,0.6177,0.6291,0.6390,0.6489,0.6657,0.6825,0.6780,0.7011,0.7047,0.7083,0.7206,0.7329,0.7329,0.7329,0.7329,0.7329,0.7329,0.7329,0.7329,0.7329}, - {0.0600,0.1182,0.1337,0.1459,0.1597,0.1707,0.2333,0.2742,0.3632,0.3763,0.3893,0.4030,0.4167,0.4511,0.4855,0.5027,0.5273,0.5406,0.5539,0.5777,0.6015,0.6138,0.6262,0.6354,0.6445,0.6617,0.6788,0.6761,0.6975,0.7012,0.7048,0.7160,0.7273,0.7273,0.7273,0.7273,0.7273,0.7273,0.7273,0.7273,0.7273}, - {0.0650,0.1130,0.1264,0.1384,0.1507,0.1613,0.2230,0.2670,0.3551,0.3689,0.3828,0.3972,0.4116,0.4457,0.4797,0.4986,0.5236,0.5373,0.5510,0.5742,0.5974,0.6105,0.6235,0.6320,0.6404,0.6582,0.6759,0.6743,0.6943,0.6981,0.7019,0.7122,0.7226,0.7226,0.7226,0.7226,0.7226,0.7226,0.7226,0.7226,0.7226}, - {0.0700,0.1080,0.1197,0.1318,0.1427,0.1535,0.2141,0.2612,0.3472,0.3622,0.3771,0.3928,0.4085,0.4415,0.4745,0.4952,0.5202,0.5343,0.5484,0.5710,0.5936,0.6076,0.6211,0.6289,0.6366,0.6552,0.6738,0.6727,0.6913,0.6954,0.6995,0.7092,0.7189,0.7189,0.7189,0.7189,0.7189,0.7189,0.7189,0.7189,0.7189}, - {0.0750,0.1030,0.1135,0.1257,0.1357,0.1473,0.2069,0.2559,0.3398,0.3560,0.3723,0.3893,0.4062,0.4381,0.4699,0.4925,0.5170,0.5315,0.5459,0.5680,0.5902,0.6053,0.6188,0.6260,0.6333,0.6528,0.6723,0.6712,0.6886,0.6930,0.6975,0.7068,0.7160,0.7160,0.7160,0.7160,0.7160,0.7160,0.7160,0.7160,0.7160}, - {0.0800,0.0977,0.1074,0.1201,0.1297,0.1420,0.2017,0.2511,0.3326,0.3503,0.3680,0.3860,0.4040,0.4349,0.4658,0.4900,0.5141,0.5289,0.5436,0.5653,0.5871,0.6033,0.6167,0.6236,0.6304,0.6509,0.6713,0.6698,0.6861,0.6911,0.6961,0.7049,0.7138,0.7138,0.7138,0.7138,0.7138,0.7138,0.7138,0.7138,0.7138}, - {0.0850,0.0917,0.1014,0.1147,0.1246,0.1372,0.1976,0.2467,0.3259,0.3449,0.3639,0.3829,0.4019,0.4321,0.4622,0.4877,0.5115,0.5265,0.5414,0.5628,0.5842,0.6016,0.6148,0.6214,0.6280,0.6492,0.6703,0.6685,0.6840,0.6894,0.6949,0.7035,0.7122,0.7122,0.7122,0.7122,0.7122,0.7122,0.7122,0.7122,0.7122}, - {0.0900,0.0849,0.0952,0.1094,0.1196,0.1327,0.1937,0.2427,0.3194,0.3397,0.3599,0.3799,0.3998,0.4295,0.4592,0.4855,0.5091,0.5242,0.5394,0.5605,0.5817,0.6001,0.6131,0.6196,0.6262,0.6478,0.6694,0.6673,0.6821,0.6879,0.6938,0.7024,0.7111,0.7111,0.7111,0.7111,0.7111,0.7111,0.7111,0.7111,0.7111}, - {0.0950,0.0768,0.0886,0.1040,0.1148,0.1286,0.1900,0.2391,0.3133,0.3347,0.3561,0.3770,0.3978,0.4272,0.4566,0.4834,0.5069,0.5222,0.5375,0.5585,0.5794,0.5988,0.6115,0.6182,0.6249,0.6467,0.6685,0.6662,0.6805,0.6866,0.6928,0.7016,0.7104,0.7104,0.7104,0.7104,0.7104,0.7104,0.7104,0.7104,0.7104}, - {0.1000,0.0673,0.0814,0.0982,0.1101,0.1248,0.1865,0.2359,0.3075,0.3300,0.3525,0.3741,0.3958,0.4251,0.4545,0.4815,0.5049,0.5204,0.5358,0.5566,0.5774,0.5975,0.6100,0.6171,0.6242,0.6459,0.6677,0.6652,0.6791,0.6855,0.6919,0.7008,0.7097,0.7097,0.7097,0.7097,0.7097,0.7097,0.7097,0.7097,0.7097}, - {0.1200,0.0000,0.0000,0.0693,0.0935,0.1132,0.1744,0.2261,0.2871,0.3133,0.3395,0.3640,0.3884,0.4188,0.4492,0.4752,0.4991,0.5147,0.5303,0.5508,0.5712,0.5925,0.6051,0.6136,0.6220,0.6433,0.6646,0.6619,0.6759,0.6826,0.6892,0.6983,0.7074,0.7074,0.7074,0.7074,0.7074,0.7074,0.7074,0.7074,0.7074}, - {0.1500,0.0000,0.0000,0.0000,0.0755,0.1035,0.1612,0.2146,0.2642,0.2949,0.3256,0.3522,0.3789,0.4108,0.4427,0.4705,0.4941,0.5102,0.5263,0.5450,0.5638,0.5851,0.5993,0.6092,0.6192,0.6401,0.6611,0.6585,0.6737,0.6804,0.6871,0.6956,0.7041,0.7041,0.7041,0.7041,0.7041,0.7041,0.7041,0.7041,0.7041}, - {0.2000,0.0000,0.0000,0.0000,0.0639,0.0929,0.1480,0.1987,0.2405,0.2763,0.3121,0.3393,0.3665,0.4001,0.4337,0.4670,0.4876,0.5057,0.5238,0.5399,0.5561,0.5758,0.5903,0.6030,0.6158,0.6366,0.6574,0.6534,0.6712,0.6777,0.6842,0.6919,0.6996,0.6996,0.6996,0.6996,0.6996,0.6996,0.6996,0.6996,0.6996}, - {0.2500,0.0000,0.0000,0.0000,0.0563,0.0872,0.1380,0.1864,0.2237,0.2629,0.3021,0.3301,0.3580,0.3923,0.4266,0.4641,0.4824,0.5024,0.5223,0.5374,0.5524,0.5700,0.5831,0.5983,0.6136,0.6349,0.6562,0.6486,0.6696,0.6757,0.6818,0.6889,0.6961,0.6961,0.6961,0.6961,0.6961,0.6961,0.6961,0.6961,0.6961}, - {0.3000,0.0000,0.0000,0.0000,0.0000,0.0846,0.1295,0.1767,0.2112,0.2534,0.2955,0.3241,0.3527,0.3870,0.4213,0.4618,0.4785,0.4998,0.5210,0.5355,0.5499,0.5647,0.5786,0.5953,0.6119,0.6338,0.6557,0.6444,0.6683,0.6741,0.6798,0.6866,0.6934,0.6934,0.6934,0.6934,0.6934,0.6934,0.6934,0.6934,0.6934}, - {0.3500,0.0000,0.0000,0.0000,0.0000,0.0827,0.1228,0.1688,0.2025,0.2472,0.2920,0.3208,0.3496,0.3835,0.4174,0.4600,0.4754,0.4975,0.5196,0.5338,0.5479,0.5601,0.5764,0.5934,0.6104,0.6329,0.6554,0.6410,0.6670,0.6726,0.6782,0.6849,0.6915,0.6915,0.6915,0.6915,0.6915,0.6915,0.6915,0.6915,0.6915}, - {0.4000,0.0000,0.0000,0.0000,0.0000,0.0816,0.1182,0.1615,0.1966,0.2430,0.2894,0.3187,0.3480,0.3814,0.4147,0.4587,0.4730,0.4957,0.5183,0.5320,0.5457,0.5565,0.5746,0.5915,0.6085,0.6318,0.6552,0.6384,0.6658,0.6714,0.6771,0.6837,0.6903,0.6903,0.6903,0.6903,0.6903,0.6903,0.6903,0.6903,0.6903}, - {0.4500,0.0000,0.0000,0.0000,0.0000,0.0816,0.1160,0.1562,0.1921,0.2398,0.2875,0.3172,0.3468,0.3799,0.4129,0.4580,0.4711,0.4941,0.5171,0.5302,0.5434,0.5540,0.5734,0.5899,0.6064,0.6307,0.6550,0.6368,0.6648,0.6706,0.6764,0.6830,0.6896,0.6896,0.6896,0.6896,0.6896,0.6896,0.6896,0.6896,0.6896}, - {0.4990,0.0000,0.0000,0.0000,0.0000,0.0827,0.1159,0.1556,0.1901,0.2384,0.2868,0.3166,0.3464,0.3790,0.4117,0.4576,0.4697,0.4929,0.5161,0.5286,0.5411,0.5530,0.5729,0.5887,0.6045,0.6297,0.6550,0.6364,0.6642,0.6702,0.6762,0.6828,0.6894,0.6894,0.6894,0.6894,0.6894,0.6894,0.6894,0.6894,0.6894}, - {0.5000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000}, - {1.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000} +constexpr int LOOPMAX = 10000; /* maximum count of search loop */ + +const double table1[34][41] = { + {0.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000}, + {0.0010, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000}, + {0.0020, 0.7796, 0.7839, 0.8158, 0.8053, 0.8151, 0.8233, 0.8312, 0.8408, 0.8515, 0.8621, + 0.8623, 0.8625, 0.8710, 0.8795, 0.8820, 0.8874, 0.8888, 0.8902, 0.9190, 0.9478, 0.8985, + 0.9057, 0.9075, 0.9092, 0.9129, 0.9165, 0.9091, 0.9203, 0.9231, 0.9259, 0.9348, 0.9436, + 0.9436, 0.9436, 0.9436, 0.9436, 0.9436, 0.9436, 0.9436, 0.9436}, + {0.0050, 0.5408, 0.5432, 0.5715, 0.5735, 0.5889, 0.6388, 0.6604, 0.6848, 0.6940, 0.7031, + 0.7096, 0.7162, 0.7339, 0.7516, 0.7484, 0.7643, 0.7771, 0.7898, 0.8020, 0.8142, 0.8033, + 0.8079, 0.8127, 0.8175, 0.8264, 0.8353, 0.8316, 0.8419, 0.8463, 0.8507, 0.8600, 0.8692, + 0.8692, 0.8692, 0.8692, 0.8692, 0.8692, 0.8692, 0.8692, 0.8692}, + {0.0100, 0.3651, 0.3869, 0.4131, 0.4338, 0.4491, 0.5084, 0.5358, 0.5679, 0.5814, 0.5948, + 0.6051, 0.6153, 0.6355, 0.6557, 0.6762, 0.6826, 0.6964, 0.7102, 0.7174, 0.7247, 0.7315, + 0.7453, 0.7470, 0.7488, 0.7632, 0.7777, 0.7764, 0.7892, 0.7930, 0.7967, 0.8215, 0.8463, + 0.8463, 0.8463, 0.8463, 0.8463, 0.8463, 0.8463, 0.8463, 0.8463}, + {0.0150, 0.2687, 0.2984, 0.3271, 0.3608, 0.3767, 0.4340, 0.4647, 0.5123, 0.5256, 0.5390, + 0.5530, 0.5670, 0.5863, 0.6056, 0.6278, 0.6369, 0.6471, 0.6572, 0.6692, 0.6812, 0.7035, + 0.7161, 0.7208, 0.7255, 0.7372, 0.7490, 0.7482, 0.7644, 0.7677, 0.7709, 0.7989, 0.8270, + 0.8270, 0.8270, 0.8270, 0.8270, 0.8270, 0.8270, 0.8270, 0.8270}, + {0.0200, 0.2138, 0.2428, 0.2694, 0.3009, 0.3301, 0.3860, 0.4186, 0.4797, 0.4934, 0.5072, + 0.5183, 0.5294, 0.5516, 0.5737, 0.5952, 0.6100, 0.6168, 0.6236, 0.6434, 0.6632, 0.6816, + 0.6959, 0.7016, 0.7072, 0.7188, 0.7303, 0.7296, 0.7461, 0.7497, 0.7532, 0.7814, 0.8095, + 0.8095, 0.8095, 0.8095, 0.8095, 0.8095, 0.8095, 0.8095, 0.8095}, + {0.0250, 0.1885, 0.2177, 0.2418, 0.2656, 0.2904, 0.3493, 0.3858, 0.4531, 0.4672, 0.4813, + 0.4910, 0.5007, 0.5248, 0.5489, 0.5714, 0.5920, 0.5999, 0.6079, 0.6285, 0.6491, 0.6645, + 0.6816, 0.6870, 0.6925, 0.7060, 0.7194, 0.7128, 0.7327, 0.7375, 0.7422, 0.7680, 0.7939, + 0.7939, 0.7939, 0.7939, 0.7939, 0.7939, 0.7939, 0.7939, 0.7939}, + {0.0300, 0.1709, 0.1983, 0.2195, 0.2418, 0.2640, 0.3202, 0.3595, 0.4317, 0.4458, 0.4599, + 0.4714, 0.4829, 0.5079, 0.5329, 0.5518, 0.5765, 0.5859, 0.5953, 0.6166, 0.6379, 0.6515, + 0.6697, 0.6752, 0.6808, 0.6961, 0.7114, 0.6987, 0.7240, 0.7294, 0.7348, 0.7574, 0.7800, + 0.7800, 0.7800, 0.7800, 0.7800, 0.7800, 0.7800, 0.7800, 0.7800}, + {0.0350, 0.1580, 0.1833, 0.2014, 0.2221, 0.2439, 0.2989, 0.3374, 0.4147, 0.4285, 0.4423, + 0.4552, 0.4680, 0.4957, 0.5234, 0.5371, 0.5634, 0.5740, 0.5847, 0.6068, 0.6288, 0.6416, + 0.6582, 0.6650, 0.6717, 0.6879, 0.7042, 0.6889, 0.7187, 0.7234, 0.7282, 0.7480, 0.7677, + 0.7677, 0.7677, 0.7677, 0.7677, 0.7677, 0.7677, 0.7677, 0.7677}, + {0.0400, 0.1473, 0.1713, 0.1867, 0.2058, 0.2257, 0.2826, 0.3201, 0.4011, 0.4145, 0.4279, + 0.4412, 0.4546, 0.4845, 0.5145, 0.5276, 0.5524, 0.5641, 0.5758, 0.5987, 0.6216, 0.6339, + 0.6480, 0.6562, 0.6644, 0.6810, 0.6977, 0.6847, 0.7138, 0.7180, 0.7222, 0.7396, 0.7570, + 0.7570, 0.7570, 0.7570, 0.7570, 0.7570, 0.7570, 0.7570, 0.7570}, + {0.0450, 0.1382, 0.1609, 0.1745, 0.1921, 0.2093, 0.2693, 0.3063, 0.3902, 0.4031, 0.4160, + 0.4293, 0.4426, 0.4745, 0.5063, 0.5199, 0.5435, 0.5560, 0.5684, 0.5921, 0.6157, 0.6277, + 0.6395, 0.6491, 0.6586, 0.6752, 0.6919, 0.6823, 0.7093, 0.7131, 0.7170, 0.7323, 0.7477, + 0.7477, 0.7477, 0.7477, 0.7477, 0.7477, 0.7477, 0.7477, 0.7477}, + {0.0500, 0.1305, 0.1508, 0.1640, 0.1803, 0.1947, 0.2568, 0.2940, 0.3808, 0.3933, 0.4059, + 0.4191, 0.4323, 0.4655, 0.4987, 0.5133, 0.5365, 0.5495, 0.5625, 0.5865, 0.6106, 0.6223, + 0.6331, 0.6433, 0.6536, 0.6702, 0.6868, 0.6801, 0.7050, 0.7087, 0.7123, 0.7260, 0.7397, + 0.7397, 0.7397, 0.7397, 0.7397, 0.7397, 0.7397, 0.7397, 0.7397}, + {0.0550, 0.1240, 0.1418, 0.1544, 0.1697, 0.1818, 0.2447, 0.2832, 0.3718, 0.3844, 0.3971, + 0.4103, 0.4236, 0.4577, 0.4918, 0.5075, 0.5313, 0.5445, 0.5577, 0.5818, 0.6059, 0.6177, + 0.6291, 0.6390, 0.6489, 0.6657, 0.6825, 0.6780, 0.7011, 0.7047, 0.7083, 0.7206, 0.7329, + 0.7329, 0.7329, 0.7329, 0.7329, 0.7329, 0.7329, 0.7329, 0.7329}, + {0.0600, 0.1182, 0.1337, 0.1459, 0.1597, 0.1707, 0.2333, 0.2742, 0.3632, 0.3763, 0.3893, + 0.4030, 0.4167, 0.4511, 0.4855, 0.5027, 0.5273, 0.5406, 0.5539, 0.5777, 0.6015, 0.6138, + 0.6262, 0.6354, 0.6445, 0.6617, 0.6788, 0.6761, 0.6975, 0.7012, 0.7048, 0.7160, 0.7273, + 0.7273, 0.7273, 0.7273, 0.7273, 0.7273, 0.7273, 0.7273, 0.7273}, + {0.0650, 0.1130, 0.1264, 0.1384, 0.1507, 0.1613, 0.2230, 0.2670, 0.3551, 0.3689, 0.3828, + 0.3972, 0.4116, 0.4457, 0.4797, 0.4986, 0.5236, 0.5373, 0.5510, 0.5742, 0.5974, 0.6105, + 0.6235, 0.6320, 0.6404, 0.6582, 0.6759, 0.6743, 0.6943, 0.6981, 0.7019, 0.7122, 0.7226, + 0.7226, 0.7226, 0.7226, 0.7226, 0.7226, 0.7226, 0.7226, 0.7226}, + {0.0700, 0.1080, 0.1197, 0.1318, 0.1427, 0.1535, 0.2141, 0.2612, 0.3472, 0.3622, 0.3771, + 0.3928, 0.4085, 0.4415, 0.4745, 0.4952, 0.5202, 0.5343, 0.5484, 0.5710, 0.5936, 0.6076, + 0.6211, 0.6289, 0.6366, 0.6552, 0.6738, 0.6727, 0.6913, 0.6954, 0.6995, 0.7092, 0.7189, + 0.7189, 0.7189, 0.7189, 0.7189, 0.7189, 0.7189, 0.7189, 0.7189}, + {0.0750, 0.1030, 0.1135, 0.1257, 0.1357, 0.1473, 0.2069, 0.2559, 0.3398, 0.3560, 0.3723, + 0.3893, 0.4062, 0.4381, 0.4699, 0.4925, 0.5170, 0.5315, 0.5459, 0.5680, 0.5902, 0.6053, + 0.6188, 0.6260, 0.6333, 0.6528, 0.6723, 0.6712, 0.6886, 0.6930, 0.6975, 0.7068, 0.7160, + 0.7160, 0.7160, 0.7160, 0.7160, 0.7160, 0.7160, 0.7160, 0.7160}, + {0.0800, 0.0977, 0.1074, 0.1201, 0.1297, 0.1420, 0.2017, 0.2511, 0.3326, 0.3503, 0.3680, + 0.3860, 0.4040, 0.4349, 0.4658, 0.4900, 0.5141, 0.5289, 0.5436, 0.5653, 0.5871, 0.6033, + 0.6167, 0.6236, 0.6304, 0.6509, 0.6713, 0.6698, 0.6861, 0.6911, 0.6961, 0.7049, 0.7138, + 0.7138, 0.7138, 0.7138, 0.7138, 0.7138, 0.7138, 0.7138, 0.7138}, + {0.0850, 0.0917, 0.1014, 0.1147, 0.1246, 0.1372, 0.1976, 0.2467, 0.3259, 0.3449, 0.3639, + 0.3829, 0.4019, 0.4321, 0.4622, 0.4877, 0.5115, 0.5265, 0.5414, 0.5628, 0.5842, 0.6016, + 0.6148, 0.6214, 0.6280, 0.6492, 0.6703, 0.6685, 0.6840, 0.6894, 0.6949, 0.7035, 0.7122, + 0.7122, 0.7122, 0.7122, 0.7122, 0.7122, 0.7122, 0.7122, 0.7122}, + {0.0900, 0.0849, 0.0952, 0.1094, 0.1196, 0.1327, 0.1937, 0.2427, 0.3194, 0.3397, 0.3599, + 0.3799, 0.3998, 0.4295, 0.4592, 0.4855, 0.5091, 0.5242, 0.5394, 0.5605, 0.5817, 0.6001, + 0.6131, 0.6196, 0.6262, 0.6478, 0.6694, 0.6673, 0.6821, 0.6879, 0.6938, 0.7024, 0.7111, + 0.7111, 0.7111, 0.7111, 0.7111, 0.7111, 0.7111, 0.7111, 0.7111}, + {0.0950, 0.0768, 0.0886, 0.1040, 0.1148, 0.1286, 0.1900, 0.2391, 0.3133, 0.3347, 0.3561, + 0.3770, 0.3978, 0.4272, 0.4566, 0.4834, 0.5069, 0.5222, 0.5375, 0.5585, 0.5794, 0.5988, + 0.6115, 0.6182, 0.6249, 0.6467, 0.6685, 0.6662, 0.6805, 0.6866, 0.6928, 0.7016, 0.7104, + 0.7104, 0.7104, 0.7104, 0.7104, 0.7104, 0.7104, 0.7104, 0.7104}, + {0.1000, 0.0673, 0.0814, 0.0982, 0.1101, 0.1248, 0.1865, 0.2359, 0.3075, 0.3300, 0.3525, + 0.3741, 0.3958, 0.4251, 0.4545, 0.4815, 0.5049, 0.5204, 0.5358, 0.5566, 0.5774, 0.5975, + 0.6100, 0.6171, 0.6242, 0.6459, 0.6677, 0.6652, 0.6791, 0.6855, 0.6919, 0.7008, 0.7097, + 0.7097, 0.7097, 0.7097, 0.7097, 0.7097, 0.7097, 0.7097, 0.7097}, + {0.1200, 0.0000, 0.0000, 0.0693, 0.0935, 0.1132, 0.1744, 0.2261, 0.2871, 0.3133, 0.3395, + 0.3640, 0.3884, 0.4188, 0.4492, 0.4752, 0.4991, 0.5147, 0.5303, 0.5508, 0.5712, 0.5925, + 0.6051, 0.6136, 0.6220, 0.6433, 0.6646, 0.6619, 0.6759, 0.6826, 0.6892, 0.6983, 0.7074, + 0.7074, 0.7074, 0.7074, 0.7074, 0.7074, 0.7074, 0.7074, 0.7074}, + {0.1500, 0.0000, 0.0000, 0.0000, 0.0755, 0.1035, 0.1612, 0.2146, 0.2642, 0.2949, 0.3256, + 0.3522, 0.3789, 0.4108, 0.4427, 0.4705, 0.4941, 0.5102, 0.5263, 0.5450, 0.5638, 0.5851, + 0.5993, 0.6092, 0.6192, 0.6401, 0.6611, 0.6585, 0.6737, 0.6804, 0.6871, 0.6956, 0.7041, + 0.7041, 0.7041, 0.7041, 0.7041, 0.7041, 0.7041, 0.7041, 0.7041}, + {0.2000, 0.0000, 0.0000, 0.0000, 0.0639, 0.0929, 0.1480, 0.1987, 0.2405, 0.2763, 0.3121, + 0.3393, 0.3665, 0.4001, 0.4337, 0.4670, 0.4876, 0.5057, 0.5238, 0.5399, 0.5561, 0.5758, + 0.5903, 0.6030, 0.6158, 0.6366, 0.6574, 0.6534, 0.6712, 0.6777, 0.6842, 0.6919, 0.6996, + 0.6996, 0.6996, 0.6996, 0.6996, 0.6996, 0.6996, 0.6996, 0.6996}, + {0.2500, 0.0000, 0.0000, 0.0000, 0.0563, 0.0872, 0.1380, 0.1864, 0.2237, 0.2629, 0.3021, + 0.3301, 0.3580, 0.3923, 0.4266, 0.4641, 0.4824, 0.5024, 0.5223, 0.5374, 0.5524, 0.5700, + 0.5831, 0.5983, 0.6136, 0.6349, 0.6562, 0.6486, 0.6696, 0.6757, 0.6818, 0.6889, 0.6961, + 0.6961, 0.6961, 0.6961, 0.6961, 0.6961, 0.6961, 0.6961, 0.6961}, + {0.3000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0846, 0.1295, 0.1767, 0.2112, 0.2534, 0.2955, + 0.3241, 0.3527, 0.3870, 0.4213, 0.4618, 0.4785, 0.4998, 0.5210, 0.5355, 0.5499, 0.5647, + 0.5786, 0.5953, 0.6119, 0.6338, 0.6557, 0.6444, 0.6683, 0.6741, 0.6798, 0.6866, 0.6934, + 0.6934, 0.6934, 0.6934, 0.6934, 0.6934, 0.6934, 0.6934, 0.6934}, + {0.3500, 0.0000, 0.0000, 0.0000, 0.0000, 0.0827, 0.1228, 0.1688, 0.2025, 0.2472, 0.2920, + 0.3208, 0.3496, 0.3835, 0.4174, 0.4600, 0.4754, 0.4975, 0.5196, 0.5338, 0.5479, 0.5601, + 0.5764, 0.5934, 0.6104, 0.6329, 0.6554, 0.6410, 0.6670, 0.6726, 0.6782, 0.6849, 0.6915, + 0.6915, 0.6915, 0.6915, 0.6915, 0.6915, 0.6915, 0.6915, 0.6915}, + {0.4000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0816, 0.1182, 0.1615, 0.1966, 0.2430, 0.2894, + 0.3187, 0.3480, 0.3814, 0.4147, 0.4587, 0.4730, 0.4957, 0.5183, 0.5320, 0.5457, 0.5565, + 0.5746, 0.5915, 0.6085, 0.6318, 0.6552, 0.6384, 0.6658, 0.6714, 0.6771, 0.6837, 0.6903, + 0.6903, 0.6903, 0.6903, 0.6903, 0.6903, 0.6903, 0.6903, 0.6903}, + {0.4500, 0.0000, 0.0000, 0.0000, 0.0000, 0.0816, 0.1160, 0.1562, 0.1921, 0.2398, 0.2875, + 0.3172, 0.3468, 0.3799, 0.4129, 0.4580, 0.4711, 0.4941, 0.5171, 0.5302, 0.5434, 0.5540, + 0.5734, 0.5899, 0.6064, 0.6307, 0.6550, 0.6368, 0.6648, 0.6706, 0.6764, 0.6830, 0.6896, + 0.6896, 0.6896, 0.6896, 0.6896, 0.6896, 0.6896, 0.6896, 0.6896}, + {0.4990, 0.0000, 0.0000, 0.0000, 0.0000, 0.0827, 0.1159, 0.1556, 0.1901, 0.2384, 0.2868, + 0.3166, 0.3464, 0.3790, 0.4117, 0.4576, 0.4697, 0.4929, 0.5161, 0.5286, 0.5411, 0.5530, + 0.5729, 0.5887, 0.6045, 0.6297, 0.6550, 0.6364, 0.6642, 0.6702, 0.6762, 0.6828, 0.6894, + 0.6894, 0.6894, 0.6894, 0.6894, 0.6894, 0.6894, 0.6894, 0.6894}, + {0.5000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000}, + {1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000} }; -const double table10[31][41] = -{ - {0.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000}, - {0.0100,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000}, - {0.0110,0.9635,0.9662,0.9690,0.9714,0.9732,0.9781,0.9777,0.9786,0.9801,0.9816,0.9761,0.9706,0.9762,0.9817,0.9811,0.9842,0.9842,0.9842,0.9868,0.9894,0.9874,0.9876,0.9869,0.9862,0.9891,0.9919,0.9880,0.9922,0.9902,0.9882,0.9920,0.9958,0.9958,0.9958,0.9958,0.9958,0.9958,0.9958,0.9958,0.9958}, - {0.0160,0.7998,0.8149,0.8300,0.8406,0.8515,0.8781,0.8800,0.8837,0.8905,0.8973,0.8969,0.8965,0.9054,0.9142,0.9204,0.9141,0.9221,0.9301,0.9342,0.9382,0.9378,0.9415,0.9422,0.9429,0.9476,0.9523,0.9463,0.9591,0.9553,0.9516,0.9635,0.9755,0.9755,0.9755,0.9755,0.9755,0.9755,0.9755,0.9755,0.9755}, - {0.0210,0.6824,0.7028,0.7232,0.7435,0.7621,0.7951,0.7999,0.8191,0.8289,0.8387,0.8424,0.8461,0.8566,0.8670,0.8779,0.8682,0.8793,0.8904,0.8949,0.8995,0.9033,0.9089,0.9109,0.9129,0.9161,0.9194,0.9196,0.9296,0.9277,0.9257,0.9399,0.9541,0.9541,0.9541,0.9541,0.9541,0.9541,0.9541,0.9541,0.9541}, - {0.0260,0.5879,0.6139,0.6400,0.6721,0.7004,0.7297,0.7405,0.7750,0.7873,0.7996,0.8024,0.8053,0.8202,0.8352,0.8446,0.8439,0.8546,0.8653,0.8704,0.8754,0.8811,0.8841,0.8879,0.8917,0.8953,0.8988,0.8982,0.9055,0.9062,0.9069,0.9197,0.9325,0.9325,0.9325,0.9325,0.9325,0.9325,0.9325,0.9325,0.9325}, - {0.0310,0.5198,0.5514,0.5830,0.6154,0.6500,0.6794,0.7006,0.7422,0.7556,0.7690,0.7723,0.7756,0.7928,0.8099,0.8188,0.8263,0.8351,0.8439,0.8494,0.8549,0.8622,0.8667,0.8711,0.8756,0.8790,0.8824,0.8821,0.8890,0.8910,0.8929,0.9026,0.9123,0.9123,0.9123,0.9123,0.9123,0.9123,0.9123,0.9123,0.9123}, - {0.0360,0.4785,0.5086,0.5388,0.5707,0.5993,0.6428,0.6693,0.7145,0.7290,0.7435,0.7490,0.7546,0.7721,0.7896,0.7973,0.8094,0.8176,0.8258,0.8316,0.8374,0.8459,0.8527,0.8577,0.8627,0.8653,0.8680,0.8696,0.8767,0.8796,0.8824,0.8886,0.8947,0.8947,0.8947,0.8947,0.8947,0.8947,0.8947,0.8947,0.8947}, - {0.0410,0.4392,0.4715,0.5039,0.5337,0.5535,0.6109,0.6434,0.6894,0.7046,0.7198,0.7277,0.7356,0.7542,0.7729,0.7787,0.7941,0.8023,0.8104,0.8165,0.8226,0.8319,0.8408,0.8462,0.8516,0.8535,0.8555,0.8588,0.8670,0.8706,0.8743,0.8779,0.8814,0.8814,0.8814,0.8814,0.8814,0.8814,0.8814,0.8814,0.8814}, - {0.0460,0.4110,0.4431,0.4751,0.5024,0.5223,0.5831,0.6201,0.6669,0.6821,0.6973,0.7075,0.7178,0.7378,0.7578,0.7634,0.7802,0.7888,0.7975,0.8039,0.8103,0.8198,0.8309,0.8365,0.8420,0.8434,0.8448,0.8495,0.8597,0.8632,0.8667,0.8700,0.8732,0.8732,0.8732,0.8732,0.8732,0.8732,0.8732,0.8732,0.8732}, - {0.0510,0.3880,0.4187,0.4494,0.4754,0.4960,0.5589,0.5991,0.6470,0.6617,0.6765,0.6895,0.7024,0.7233,0.7442,0.7507,0.7672,0.7768,0.7865,0.7933,0.8001,0.8094,0.8229,0.8283,0.8338,0.8348,0.8359,0.8412,0.8537,0.8567,0.8597,0.8632,0.8666,0.8666,0.8666,0.8666,0.8666,0.8666,0.8666,0.8666,0.8666}, - {0.0560,0.3673,0.3969,0.4265,0.4515,0.4734,0.5380,0.5803,0.6297,0.6440,0.6582,0.6736,0.6890,0.7104,0.7318,0.7394,0.7561,0.7665,0.7769,0.7842,0.7916,0.8001,0.8166,0.8216,0.8266,0.8276,0.8286,0.8337,0.8480,0.8506,0.8532,0.8570,0.8608,0.8608,0.8608,0.8608,0.8608,0.8608,0.8608,0.8608,0.8608}, - {0.0610,0.3492,0.3777,0.4062,0.4303,0.4535,0.5199,0.5639,0.6151,0.6290,0.6429,0.6598,0.6768,0.6989,0.7210,0.7294,0.7469,0.7578,0.7686,0.7763,0.7840,0.7918,0.8109,0.8154,0.8199,0.8214,0.8229,0.8267,0.8426,0.8449,0.8472,0.8514,0.8556,0.8556,0.8556,0.8556,0.8556,0.8556,0.8556,0.8556,0.8556}, - {0.0660,0.3337,0.3611,0.3885,0.4118,0.4352,0.5042,0.5498,0.6030,0.6170,0.6311,0.6483,0.6655,0.6885,0.7115,0.7207,0.7380,0.7496,0.7612,0.7693,0.7774,0.7845,0.8055,0.8095,0.8136,0.8159,0.8181,0.8208,0.8375,0.8396,0.8417,0.8464,0.8510,0.8510,0.8510,0.8510,0.8510,0.8510,0.8510,0.8510,0.8510}, - {0.0710,0.3209,0.3470,0.3731,0.3962,0.4182,0.4902,0.5370,0.5923,0.6072,0.6221,0.6386,0.6550,0.6792,0.7035,0.7131,0.7294,0.7419,0.7545,0.7630,0.7715,0.7782,0.8003,0.8041,0.8078,0.8107,0.8137,0.8160,0.8327,0.8347,0.8367,0.8419,0.8470,0.8470,0.8470,0.8470,0.8470,0.8470,0.8470,0.8470,0.8470}, - {0.0760,0.3100,0.3350,0.3601,0.3834,0.4028,0.4777,0.5248,0.5828,0.5983,0.6138,0.6296,0.6454,0.6709,0.6965,0.7059,0.7213,0.7348,0.7483,0.7574,0.7664,0.7727,0.7955,0.7989,0.8024,0.8060,0.8097,0.8120,0.8283,0.8303,0.8323,0.8379,0.8436,0.8436,0.8436,0.8436,0.8436,0.8436,0.8436,0.8436,0.8436}, - {0.0810,0.3003,0.3245,0.3488,0.3723,0.3894,0.4660,0.5133,0.5743,0.5901,0.6060,0.6214,0.6368,0.6633,0.6899,0.6992,0.7138,0.7281,0.7424,0.7522,0.7620,0.7681,0.7909,0.7941,0.7974,0.8017,0.8060,0.8084,0.8243,0.8263,0.8284,0.8345,0.8406,0.8406,0.8406,0.8406,0.8406,0.8406,0.8406,0.8406,0.8406}, - {0.0860,0.2912,0.3149,0.3386,0.3621,0.3783,0.4547,0.5026,0.5664,0.5826,0.5987,0.6139,0.6291,0.6564,0.6837,0.6929,0.7069,0.7218,0.7367,0.7474,0.7582,0.7637,0.7865,0.7898,0.7930,0.7978,0.8027,0.8052,0.8206,0.8228,0.8250,0.8315,0.8381,0.8381,0.8381,0.8381,0.8381,0.8381,0.8381,0.8381,0.8381}, - {0.0910,0.2825,0.3055,0.3285,0.3528,0.3685,0.4439,0.4928,0.5591,0.5755,0.5920,0.6070,0.6220,0.6499,0.6778,0.6871,0.7009,0.7160,0.7312,0.7430,0.7548,0.7596,0.7824,0.7858,0.7891,0.7944,0.7997,0.8023,0.8172,0.8196,0.8220,0.8289,0.8359,0.8359,0.8359,0.8359,0.8359,0.8359,0.8359,0.8359,0.8359}, - {0.0960,0.2736,0.2958,0.3179,0.3441,0.3595,0.4337,0.4839,0.5519,0.5688,0.5856,0.6008,0.6160,0.6442,0.6723,0.6818,0.6957,0.7109,0.7262,0.7388,0.7515,0.7558,0.7786,0.7821,0.7857,0.7913,0.7969,0.7998,0.8142,0.8167,0.8192,0.8265,0.8339,0.8339,0.8339,0.8339,0.8339,0.8339,0.8339,0.8339,0.8339}, - {0.1200,0.2294,0.2497,0.2700,0.2989,0.3241,0.3954,0.4541,0.5183,0.5395,0.5607,0.5801,0.5995,0.6252,0.6509,0.6622,0.6812,0.6958,0.7104,0.7239,0.7373,0.7418,0.7635,0.7684,0.7733,0.7802,0.7872,0.7907,0.8051,0.8069,0.8087,0.8171,0.8254,0.8254,0.8254,0.8254,0.8254,0.8254,0.8254,0.8254,0.8254}, - {0.1500,0.1736,0.2027,0.2318,0.2595,0.2893,0.3658,0.4295,0.4860,0.5119,0.5378,0.5621,0.5863,0.6100,0.6338,0.6497,0.6696,0.6850,0.7004,0.7120,0.7236,0.7327,0.7517,0.7576,0.7634,0.7718,0.7802,0.7850,0.7985,0.8008,0.8032,0.8103,0.8175,0.8175,0.8175,0.8175,0.8175,0.8175,0.8175,0.8175,0.8175}, - {0.2000,0.0986,0.1392,0.1797,0.2181,0.2581,0.3283,0.4013,0.4499,0.4799,0.5099,0.5390,0.5680,0.5921,0.6162,0.6369,0.6574,0.6725,0.6876,0.6995,0.7114,0.7234,0.7417,0.7475,0.7534,0.7631,0.7728,0.7797,0.7904,0.7943,0.7983,0.8034,0.8085,0.8085,0.8085,0.8085,0.8085,0.8085,0.8085,0.8085,0.8085}, - {0.2500,0.0394,0.0909,0.1424,0.1873,0.2355,0.2974,0.3791,0.4209,0.4555,0.4900,0.5217,0.5535,0.5799,0.6062,0.6283,0.6476,0.6621,0.6766,0.6909,0.7051,0.7173,0.7305,0.7389,0.7474,0.7578,0.7681,0.7749,0.7851,0.7894,0.7937,0.7975,0.8013,0.8013,0.8013,0.8013,0.8013,0.8013,0.8013,0.8013,0.8013}, - {0.3000,0.0210,0.0711,0.1212,0.1689,0.2136,0.2731,0.3608,0.3978,0.4363,0.4748,0.5090,0.5432,0.5711,0.5989,0.6215,0.6396,0.6545,0.6693,0.6852,0.7011,0.7122,0.7202,0.7315,0.7428,0.7534,0.7641,0.7707,0.7816,0.7858,0.7899,0.7927,0.7954,0.7954,0.7954,0.7954,0.7954,0.7954,0.7954,0.7954,0.7954}, - {0.3500,0.0113,0.0600,0.1086,0.1539,0.1971,0.2541,0.3459,0.3831,0.4243,0.4654,0.5010,0.5367,0.5648,0.5930,0.6164,0.6340,0.6490,0.6639,0.6814,0.6988,0.7084,0.7147,0.7270,0.7393,0.7502,0.7610,0.7668,0.7787,0.7830,0.7873,0.7899,0.7925,0.7925,0.7925,0.7925,0.7925,0.7925,0.7925,0.7925,0.7925}, - {0.4000,0.0112,0.0555,0.0997,0.1425,0.1848,0.2390,0.3341,0.3721,0.4153,0.4586,0.4950,0.5315,0.5593,0.5872,0.6129,0.6314,0.6454,0.6593,0.6782,0.6971,0.7068,0.7139,0.7253,0.7366,0.7479,0.7593,0.7657,0.7765,0.7810,0.7855,0.7889,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924}, - {0.4500,0.0111,0.0520,0.0928,0.1337,0.1751,0.2291,0.3245,0.3613,0.4086,0.4558,0.4914,0.5270,0.5544,0.5818,0.6101,0.6298,0.6430,0.6562,0.6760,0.6958,0.7057,0.7134,0.7240,0.7345,0.7464,0.7584,0.7651,0.7758,0.7799,0.7841,0.7882,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924}, - {0.4990,0.0086,0.0484,0.0881,0.1279,0.1677,0.2232,0.3162,0.3570,0.4060,0.4550,0.4888,0.5227,0.5499,0.5771,0.6086,0.6285,0.6417,0.6550,0.6752,0.6954,0.7053,0.7132,0.7232,0.7331,0.7455,0.7579,0.7649,0.7758,0.7796,0.7834,0.7879,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924,0.7924}, - {0.5000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000}, - {1.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000} +const double table10[31][41] = { + {0.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000}, + {0.0100, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, + 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000}, + {0.0110, 0.9635, 0.9662, 0.9690, 0.9714, 0.9732, 0.9781, 0.9777, 0.9786, 0.9801, 0.9816, + 0.9761, 0.9706, 0.9762, 0.9817, 0.9811, 0.9842, 0.9842, 0.9842, 0.9868, 0.9894, 0.9874, + 0.9876, 0.9869, 0.9862, 0.9891, 0.9919, 0.9880, 0.9922, 0.9902, 0.9882, 0.9920, 0.9958, + 0.9958, 0.9958, 0.9958, 0.9958, 0.9958, 0.9958, 0.9958, 0.9958}, + {0.0160, 0.7998, 0.8149, 0.8300, 0.8406, 0.8515, 0.8781, 0.8800, 0.8837, 0.8905, 0.8973, + 0.8969, 0.8965, 0.9054, 0.9142, 0.9204, 0.9141, 0.9221, 0.9301, 0.9342, 0.9382, 0.9378, + 0.9415, 0.9422, 0.9429, 0.9476, 0.9523, 0.9463, 0.9591, 0.9553, 0.9516, 0.9635, 0.9755, + 0.9755, 0.9755, 0.9755, 0.9755, 0.9755, 0.9755, 0.9755, 0.9755}, + {0.0210, 0.6824, 0.7028, 0.7232, 0.7435, 0.7621, 0.7951, 0.7999, 0.8191, 0.8289, 0.8387, + 0.8424, 0.8461, 0.8566, 0.8670, 0.8779, 0.8682, 0.8793, 0.8904, 0.8949, 0.8995, 0.9033, + 0.9089, 0.9109, 0.9129, 0.9161, 0.9194, 0.9196, 0.9296, 0.9277, 0.9257, 0.9399, 0.9541, + 0.9541, 0.9541, 0.9541, 0.9541, 0.9541, 0.9541, 0.9541, 0.9541}, + {0.0260, 0.5879, 0.6139, 0.6400, 0.6721, 0.7004, 0.7297, 0.7405, 0.7750, 0.7873, 0.7996, + 0.8024, 0.8053, 0.8202, 0.8352, 0.8446, 0.8439, 0.8546, 0.8653, 0.8704, 0.8754, 0.8811, + 0.8841, 0.8879, 0.8917, 0.8953, 0.8988, 0.8982, 0.9055, 0.9062, 0.9069, 0.9197, 0.9325, + 0.9325, 0.9325, 0.9325, 0.9325, 0.9325, 0.9325, 0.9325, 0.9325}, + {0.0310, 0.5198, 0.5514, 0.5830, 0.6154, 0.6500, 0.6794, 0.7006, 0.7422, 0.7556, 0.7690, + 0.7723, 0.7756, 0.7928, 0.8099, 0.8188, 0.8263, 0.8351, 0.8439, 0.8494, 0.8549, 0.8622, + 0.8667, 0.8711, 0.8756, 0.8790, 0.8824, 0.8821, 0.8890, 0.8910, 0.8929, 0.9026, 0.9123, + 0.9123, 0.9123, 0.9123, 0.9123, 0.9123, 0.9123, 0.9123, 0.9123}, + {0.0360, 0.4785, 0.5086, 0.5388, 0.5707, 0.5993, 0.6428, 0.6693, 0.7145, 0.7290, 0.7435, + 0.7490, 0.7546, 0.7721, 0.7896, 0.7973, 0.8094, 0.8176, 0.8258, 0.8316, 0.8374, 0.8459, + 0.8527, 0.8577, 0.8627, 0.8653, 0.8680, 0.8696, 0.8767, 0.8796, 0.8824, 0.8886, 0.8947, + 0.8947, 0.8947, 0.8947, 0.8947, 0.8947, 0.8947, 0.8947, 0.8947}, + {0.0410, 0.4392, 0.4715, 0.5039, 0.5337, 0.5535, 0.6109, 0.6434, 0.6894, 0.7046, 0.7198, + 0.7277, 0.7356, 0.7542, 0.7729, 0.7787, 0.7941, 0.8023, 0.8104, 0.8165, 0.8226, 0.8319, + 0.8408, 0.8462, 0.8516, 0.8535, 0.8555, 0.8588, 0.8670, 0.8706, 0.8743, 0.8779, 0.8814, + 0.8814, 0.8814, 0.8814, 0.8814, 0.8814, 0.8814, 0.8814, 0.8814}, + {0.0460, 0.4110, 0.4431, 0.4751, 0.5024, 0.5223, 0.5831, 0.6201, 0.6669, 0.6821, 0.6973, + 0.7075, 0.7178, 0.7378, 0.7578, 0.7634, 0.7802, 0.7888, 0.7975, 0.8039, 0.8103, 0.8198, + 0.8309, 0.8365, 0.8420, 0.8434, 0.8448, 0.8495, 0.8597, 0.8632, 0.8667, 0.8700, 0.8732, + 0.8732, 0.8732, 0.8732, 0.8732, 0.8732, 0.8732, 0.8732, 0.8732}, + {0.0510, 0.3880, 0.4187, 0.4494, 0.4754, 0.4960, 0.5589, 0.5991, 0.6470, 0.6617, 0.6765, + 0.6895, 0.7024, 0.7233, 0.7442, 0.7507, 0.7672, 0.7768, 0.7865, 0.7933, 0.8001, 0.8094, + 0.8229, 0.8283, 0.8338, 0.8348, 0.8359, 0.8412, 0.8537, 0.8567, 0.8597, 0.8632, 0.8666, + 0.8666, 0.8666, 0.8666, 0.8666, 0.8666, 0.8666, 0.8666, 0.8666}, + {0.0560, 0.3673, 0.3969, 0.4265, 0.4515, 0.4734, 0.5380, 0.5803, 0.6297, 0.6440, 0.6582, + 0.6736, 0.6890, 0.7104, 0.7318, 0.7394, 0.7561, 0.7665, 0.7769, 0.7842, 0.7916, 0.8001, + 0.8166, 0.8216, 0.8266, 0.8276, 0.8286, 0.8337, 0.8480, 0.8506, 0.8532, 0.8570, 0.8608, + 0.8608, 0.8608, 0.8608, 0.8608, 0.8608, 0.8608, 0.8608, 0.8608}, + {0.0610, 0.3492, 0.3777, 0.4062, 0.4303, 0.4535, 0.5199, 0.5639, 0.6151, 0.6290, 0.6429, + 0.6598, 0.6768, 0.6989, 0.7210, 0.7294, 0.7469, 0.7578, 0.7686, 0.7763, 0.7840, 0.7918, + 0.8109, 0.8154, 0.8199, 0.8214, 0.8229, 0.8267, 0.8426, 0.8449, 0.8472, 0.8514, 0.8556, + 0.8556, 0.8556, 0.8556, 0.8556, 0.8556, 0.8556, 0.8556, 0.8556}, + {0.0660, 0.3337, 0.3611, 0.3885, 0.4118, 0.4352, 0.5042, 0.5498, 0.6030, 0.6170, 0.6311, + 0.6483, 0.6655, 0.6885, 0.7115, 0.7207, 0.7380, 0.7496, 0.7612, 0.7693, 0.7774, 0.7845, + 0.8055, 0.8095, 0.8136, 0.8159, 0.8181, 0.8208, 0.8375, 0.8396, 0.8417, 0.8464, 0.8510, + 0.8510, 0.8510, 0.8510, 0.8510, 0.8510, 0.8510, 0.8510, 0.8510}, + {0.0710, 0.3209, 0.3470, 0.3731, 0.3962, 0.4182, 0.4902, 0.5370, 0.5923, 0.6072, 0.6221, + 0.6386, 0.6550, 0.6792, 0.7035, 0.7131, 0.7294, 0.7419, 0.7545, 0.7630, 0.7715, 0.7782, + 0.8003, 0.8041, 0.8078, 0.8107, 0.8137, 0.8160, 0.8327, 0.8347, 0.8367, 0.8419, 0.8470, + 0.8470, 0.8470, 0.8470, 0.8470, 0.8470, 0.8470, 0.8470, 0.8470}, + {0.0760, 0.3100, 0.3350, 0.3601, 0.3834, 0.4028, 0.4777, 0.5248, 0.5828, 0.5983, 0.6138, + 0.6296, 0.6454, 0.6709, 0.6965, 0.7059, 0.7213, 0.7348, 0.7483, 0.7574, 0.7664, 0.7727, + 0.7955, 0.7989, 0.8024, 0.8060, 0.8097, 0.8120, 0.8283, 0.8303, 0.8323, 0.8379, 0.8436, + 0.8436, 0.8436, 0.8436, 0.8436, 0.8436, 0.8436, 0.8436, 0.8436}, + {0.0810, 0.3003, 0.3245, 0.3488, 0.3723, 0.3894, 0.4660, 0.5133, 0.5743, 0.5901, 0.6060, + 0.6214, 0.6368, 0.6633, 0.6899, 0.6992, 0.7138, 0.7281, 0.7424, 0.7522, 0.7620, 0.7681, + 0.7909, 0.7941, 0.7974, 0.8017, 0.8060, 0.8084, 0.8243, 0.8263, 0.8284, 0.8345, 0.8406, + 0.8406, 0.8406, 0.8406, 0.8406, 0.8406, 0.8406, 0.8406, 0.8406}, + {0.0860, 0.2912, 0.3149, 0.3386, 0.3621, 0.3783, 0.4547, 0.5026, 0.5664, 0.5826, 0.5987, + 0.6139, 0.6291, 0.6564, 0.6837, 0.6929, 0.7069, 0.7218, 0.7367, 0.7474, 0.7582, 0.7637, + 0.7865, 0.7898, 0.7930, 0.7978, 0.8027, 0.8052, 0.8206, 0.8228, 0.8250, 0.8315, 0.8381, + 0.8381, 0.8381, 0.8381, 0.8381, 0.8381, 0.8381, 0.8381, 0.8381}, + {0.0910, 0.2825, 0.3055, 0.3285, 0.3528, 0.3685, 0.4439, 0.4928, 0.5591, 0.5755, 0.5920, + 0.6070, 0.6220, 0.6499, 0.6778, 0.6871, 0.7009, 0.7160, 0.7312, 0.7430, 0.7548, 0.7596, + 0.7824, 0.7858, 0.7891, 0.7944, 0.7997, 0.8023, 0.8172, 0.8196, 0.8220, 0.8289, 0.8359, + 0.8359, 0.8359, 0.8359, 0.8359, 0.8359, 0.8359, 0.8359, 0.8359}, + {0.0960, 0.2736, 0.2958, 0.3179, 0.3441, 0.3595, 0.4337, 0.4839, 0.5519, 0.5688, 0.5856, + 0.6008, 0.6160, 0.6442, 0.6723, 0.6818, 0.6957, 0.7109, 0.7262, 0.7388, 0.7515, 0.7558, + 0.7786, 0.7821, 0.7857, 0.7913, 0.7969, 0.7998, 0.8142, 0.8167, 0.8192, 0.8265, 0.8339, + 0.8339, 0.8339, 0.8339, 0.8339, 0.8339, 0.8339, 0.8339, 0.8339}, + {0.1200, 0.2294, 0.2497, 0.2700, 0.2989, 0.3241, 0.3954, 0.4541, 0.5183, 0.5395, 0.5607, + 0.5801, 0.5995, 0.6252, 0.6509, 0.6622, 0.6812, 0.6958, 0.7104, 0.7239, 0.7373, 0.7418, + 0.7635, 0.7684, 0.7733, 0.7802, 0.7872, 0.7907, 0.8051, 0.8069, 0.8087, 0.8171, 0.8254, + 0.8254, 0.8254, 0.8254, 0.8254, 0.8254, 0.8254, 0.8254, 0.8254}, + {0.1500, 0.1736, 0.2027, 0.2318, 0.2595, 0.2893, 0.3658, 0.4295, 0.4860, 0.5119, 0.5378, + 0.5621, 0.5863, 0.6100, 0.6338, 0.6497, 0.6696, 0.6850, 0.7004, 0.7120, 0.7236, 0.7327, + 0.7517, 0.7576, 0.7634, 0.7718, 0.7802, 0.7850, 0.7985, 0.8008, 0.8032, 0.8103, 0.8175, + 0.8175, 0.8175, 0.8175, 0.8175, 0.8175, 0.8175, 0.8175, 0.8175}, + {0.2000, 0.0986, 0.1392, 0.1797, 0.2181, 0.2581, 0.3283, 0.4013, 0.4499, 0.4799, 0.5099, + 0.5390, 0.5680, 0.5921, 0.6162, 0.6369, 0.6574, 0.6725, 0.6876, 0.6995, 0.7114, 0.7234, + 0.7417, 0.7475, 0.7534, 0.7631, 0.7728, 0.7797, 0.7904, 0.7943, 0.7983, 0.8034, 0.8085, + 0.8085, 0.8085, 0.8085, 0.8085, 0.8085, 0.8085, 0.8085, 0.8085}, + {0.2500, 0.0394, 0.0909, 0.1424, 0.1873, 0.2355, 0.2974, 0.3791, 0.4209, 0.4555, 0.4900, + 0.5217, 0.5535, 0.5799, 0.6062, 0.6283, 0.6476, 0.6621, 0.6766, 0.6909, 0.7051, 0.7173, + 0.7305, 0.7389, 0.7474, 0.7578, 0.7681, 0.7749, 0.7851, 0.7894, 0.7937, 0.7975, 0.8013, + 0.8013, 0.8013, 0.8013, 0.8013, 0.8013, 0.8013, 0.8013, 0.8013}, + {0.3000, 0.0210, 0.0711, 0.1212, 0.1689, 0.2136, 0.2731, 0.3608, 0.3978, 0.4363, 0.4748, + 0.5090, 0.5432, 0.5711, 0.5989, 0.6215, 0.6396, 0.6545, 0.6693, 0.6852, 0.7011, 0.7122, + 0.7202, 0.7315, 0.7428, 0.7534, 0.7641, 0.7707, 0.7816, 0.7858, 0.7899, 0.7927, 0.7954, + 0.7954, 0.7954, 0.7954, 0.7954, 0.7954, 0.7954, 0.7954, 0.7954}, + {0.3500, 0.0113, 0.0600, 0.1086, 0.1539, 0.1971, 0.2541, 0.3459, 0.3831, 0.4243, 0.4654, + 0.5010, 0.5367, 0.5648, 0.5930, 0.6164, 0.6340, 0.6490, 0.6639, 0.6814, 0.6988, 0.7084, + 0.7147, 0.7270, 0.7393, 0.7502, 0.7610, 0.7668, 0.7787, 0.7830, 0.7873, 0.7899, 0.7925, + 0.7925, 0.7925, 0.7925, 0.7925, 0.7925, 0.7925, 0.7925, 0.7925}, + {0.4000, 0.0112, 0.0555, 0.0997, 0.1425, 0.1848, 0.2390, 0.3341, 0.3721, 0.4153, 0.4586, + 0.4950, 0.5315, 0.5593, 0.5872, 0.6129, 0.6314, 0.6454, 0.6593, 0.6782, 0.6971, 0.7068, + 0.7139, 0.7253, 0.7366, 0.7479, 0.7593, 0.7657, 0.7765, 0.7810, 0.7855, 0.7889, 0.7924, + 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924}, + {0.4500, 0.0111, 0.0520, 0.0928, 0.1337, 0.1751, 0.2291, 0.3245, 0.3613, 0.4086, 0.4558, + 0.4914, 0.5270, 0.5544, 0.5818, 0.6101, 0.6298, 0.6430, 0.6562, 0.6760, 0.6958, 0.7057, + 0.7134, 0.7240, 0.7345, 0.7464, 0.7584, 0.7651, 0.7758, 0.7799, 0.7841, 0.7882, 0.7924, + 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924}, + {0.4990, 0.0086, 0.0484, 0.0881, 0.1279, 0.1677, 0.2232, 0.3162, 0.3570, 0.4060, 0.4550, + 0.4888, 0.5227, 0.5499, 0.5771, 0.6086, 0.6285, 0.6417, 0.6550, 0.6752, 0.6954, 0.7053, + 0.7132, 0.7232, 0.7331, 0.7455, 0.7579, 0.7649, 0.7758, 0.7796, 0.7834, 0.7879, 0.7924, + 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924, 0.7924}, + {0.5000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000}, + {1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, + 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000} }; double erfar(double x) { - /* constants */ - double a1 = 0.254829592; - double a2 = -0.284496736; - double a3 = 1.421413741; - double a4 = -1.453152027; - double a5 = 1.061405429; - double p = 0.3275911; - double t,y; - /* save the sign of x */ - int sign = 1; - if (x < 0) sign = -1; - x = fabs(x); - - /* A&S formula 7.1.26 */ - t = 1.0/(1.0 + p*x); - y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*exp(-x*x); - - return sign*y; + /* constants */ + double a1 = 0.254829592; + double a2 = -0.284496736; + double a3 = 1.421413741; + double a4 = -1.453152027; + double a5 = 1.061405429; + double p = 0.3275911; + double t, y; + /* save the sign of x */ + int sign = 1; + if (x < 0) + sign = -1; + x = fabs(x); + + /* A&S formula 7.1.26 */ + t = 1.0 / (1.0 + p * x); + y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * exp(-x * x); + + return sign * y; } /* normal cumulative distribution function ------------------------------------- -* args : double x I actual ILS fail-rate for the current geometry -* double mu I mean -* double sigma I sigma -* return : cdf -* ---------------------------------------------------------------------------*/ -double normcdfar(double x,double mu,double sigma) + * args : double x I actual ILS fail-rate for the current geometry + * double mu I mean + * double sigma I sigma + * return : cdf + * ---------------------------------------------------------------------------*/ +double normcdfar(double x, double mu, double sigma) { - return 0.5 * (1 + erfar((x - mu) / (sigma * sqrt(2.)))); + return 0.5 * (1 + erfar((x - mu) / (sigma * sqrt(2.)))); } /* sort function --------------------------------------------------------------- -* args : double *Pd I vector to be sorted -* double *index O sort index -* int dim I dimension -* ---------------------------------------------------------------------------*/ -void rsort(double *Pd,int *index,int dim) + * args : double *Pd I vector to be sorted + * double *index O sort index + * int dim I dimension + * ---------------------------------------------------------------------------*/ +void rsort(double* Pd, int* index, int dim) { - int i,j; - double num,ind; - - /* sort ascending */ - for(i=0;iPd[j+1]) - { - num = Pd[j]; - ind = index[j]; - Pd[j] = Pd[j+1]; - Pd[j+1] = num; - - index[j] = index[j+1]; - index[j+1] = ind; - } - } - } + int i, j; + double num, ind; + + /* sort ascending */ + for (i = 0; i < dim; i++) + { + for (j = 0; j + i < dim - 1; j++) + { + if (Pd[j] > Pd[j + 1]) + { + num = Pd[j]; + ind = index[j]; + Pd[j] = Pd[j + 1]; + Pd[j + 1] = num; + + index[j] = index[j + 1]; + index[j + 1] = ind; + } + } + } } /* fixed fail-rate to derive the ratio test critical value --------------------- -* args : double PfILS I actual ILS fail-rate for the current geometry -* int n I number of ambiguities -* double Pf I pre-defined fail-rate (0.01 or 0.001) -* return : critical value for ratio test (inversed) -* ---------------------------------------------------------------------------*/ -double ffratio(double PfILS,int n,double Pf) + * args : double PfILS I actual ILS fail-rate for the current geometry + * int n I number of ambiguities + * double Pf I pre-defined fail-rate (0.01 or 0.001) + * return : critical value for ratio test (inversed) + * ---------------------------------------------------------------------------*/ +double ffratio(double PfILS, int n, double Pf) { - int i,j,dim =34,nt=0,index[34],index1[31]; - double mun[2],Pn[2],mu,diff; - double Pt[34],Pd[34],Pt1[31],Pd1[31]; - - if(Pf==0.001) - { - for (i=0;i0) nt = 1; - else if (diff<0) nt = -1; - - Pn[0] = Pt[index[0]]; - Pn[1] = Pt[index[0]+nt]; - mun[0] = table1[index[0]][n]; - mun[1] = table1[index[0]+nt][n]; - } - else - { - dim = 31; - for (i=0;i0) nt = 1; - else if (diff<0) nt = -1; - - Pn[0] = Pt1[index1[0]]; - Pn[1] = Pt1[index1[0]+nt]; - - mun[0] = table10[index1[0]][n]; - mun[1] = table10[index1[0]+nt][n]; - } - - j = Pn[0] 0) + nt = 1; + else if (diff < 0) + nt = -1; + + Pn[0] = Pt[index[0]]; + Pn[1] = Pt[index[0] + nt]; + mun[0] = table1[index[0]][n]; + mun[1] = table1[index[0] + nt][n]; + } + else + { + dim = 31; + for (i = 0; i < dim; i++) + { + Pt1[i] = table10[i][0]; + Pd1[i] = fabs(Pt1[i] - PfILS); + index1[i] = i; + } + + /* sort Pd and update index */ + rsort(Pd1, index1, dim); + + diff = PfILS - Pt1[index1[0]]; + + if (diff > 0) + nt = 1; + else if (diff < 0) + nt = -1; + + Pn[0] = Pt1[index1[0]]; + Pn[1] = Pt1[index1[0] + nt]; + + mun[0] = table10[index1[0]][n]; + mun[1] = table10[index1[0] + nt][n]; + } + + j = Pn[0] < Pn[1] ? 0 : 1; + + if (Pn[0] - Pn[1] == 0) + mu = mun[0]; + else + mu = mun[j] + (PfILS - Pn[j]) * (mun[0] - mun[1]) / (Pn[0] - Pn[1]); + + return mu; } /* LD factorization (Q=L'*diag(D)*L) -----------------------------------------*/ -int LD(int n, const double *Q, double *L, double *D) +int LD(int n, const double* Q, double* L, double* D) { - int i,j,k,info=0; - double a,*A=mat(n,n); - - memcpy(A,Q,sizeof(double)*n*n); - for (i=n-1;i>=0;i--) - { - if ((D[i]=A[i+i*n])<=0.0) {info=-1; break;} - a=sqrt(D[i]); - for (j=0;j<=i;j++) L[i+j*n]=A[i+j*n]/a; - for (j=0;j<=i-1;j++) for (k=0;k<=j;k++) A[j+k*n]-=L[i+k*n]*L[i+j*n]; - for (j=0;j<=i;j++) L[i+j*n]/=L[i+i*n]; - } - free(A); - if (info) fprintf(stderr,"%s : LD factorization error\n",__FILE__); - return info; + int i, j, k, info = 0; + double a, *A = mat(n, n); + + memcpy(A, Q, sizeof(double) * n * n); + for (i = n - 1; i >= 0; i--) + { + if ((D[i] = A[i + i * n]) <= 0.0) + { + info = -1; + break; + } + a = sqrt(D[i]); + for (j = 0; j <= i; j++) + L[i + j * n] = A[i + j * n] / a; + for (j = 0; j <= i - 1; j++) + for (k = 0; k <= j; k++) + A[j + k * n] -= L[i + k * n] * L[i + j * n]; + for (j = 0; j <= i; j++) + L[i + j * n] /= L[i + i * n]; + } + free(A); + if (info) + fprintf(stderr, "%s : LD factorization error\n", __FILE__); + return info; } /* integer gauss transformation ----------------------------------------------*/ -void gauss(int n, double *L, double *Z, int i, int j) +void gauss(int n, double* L, double* Z, int i, int j) { - int k,mu; - - if ((mu=(int)ROUND(L[i+j*n]))!=0) - { - for (k=i;k=0) - { - if (j<=k) - for (i=j+1;i= 0) + { + if (j <= k) + for (i = j + 1; i < n; i++) + gauss(n, L, Z, i, j); + del = D[j] + L[j + 1 + j * n] * L[j + 1 + j * n] * D[j + 1]; + if (del + 1E-6 < D[j + 1]) + { /* compared considering numerical error */ + perm(n, L, D, j, del, Z); + k = j; + j = n - 2; + } + else + j--; + } } /* modified lambda (mlambda) search (ref. [2]) -------------------------------*/ -int search( - int n, - int m, - const double *L, - const double *D, - const double *zs, - double *zn, - double *s) +int search(int n, int m, const double* L, const double* D, const double* zs, double* zn, double* s) { - int i,j,k,c,nn=0,imax=0; - double newdist,maxdist=1E99,y; - double *S=zeros(n,n),*dist=mat(n,1),*zb=mat(n,1),*z=mat(n,1),*step=mat(n,1); - - k=n-1; dist[k]=0.0; - zb[k]=zs[k]; - z[k]=ROUND(zb[k]); y=zb[k]-z[k]; step[k]=SGN(y); - for (c=0;cs[imax]) - imax=nn; - for (i=0;i=LOOPMAX) - { - fprintf(stderr,"%s : search loop count overflow\n",__FILE__); - return -1; - } - return 0; + int i, j, k, c, nn = 0, imax = 0; + double newdist, maxdist = 1E99, y; + double *S = zeros(n, n), *dist = mat(n, 1), *zb = mat(n, 1), *z = mat(n, 1), *step = mat(n, 1); + + k = n - 1; + dist[k] = 0.0; + zb[k] = zs[k]; + z[k] = ROUND(zb[k]); + y = zb[k] - z[k]; + step[k] = SGN(y); + for (c = 0; c < LOOPMAX; c++) + { + newdist = dist[k] + y * y / D[k]; + if (newdist < maxdist) + { + if (k != 0) + { + dist[--k] = newdist; + for (i = 0; i <= k; i++) + S[k + i * n] = S[k + 1 + i * n] + (z[k + 1] - zb[k + 1]) * L[k + 1 + i * n]; + zb[k] = zs[k] + S[k + k * n]; + z[k] = ROUND(zb[k]); + y = zb[k] - z[k]; + step[k] = SGN(y); + } + else + { + if (nn < m) + { + if (nn == 0 || newdist > s[imax]) + imax = nn; + for (i = 0; i < n; i++) + zn[i + nn * n] = z[i]; + s[nn++] = newdist; + } + else + { + if (newdist < s[imax]) + { + for (i = 0; i < n; i++) + zn[i + imax * n] = z[i]; + s[imax] = newdist; + for (i = imax = 0; i < m; i++) + if (s[imax] < s[i]) + imax = i; + } + maxdist = s[imax]; + } + z[0] += step[0]; + y = zb[0] - z[0]; + step[0] = -step[0] - SGN(step[0]); + } + } + else + { + if (k == n - 1) + break; + else + { + k++; + z[k] += step[k]; + y = zb[k] - z[k]; + step[k] = -step[k] - SGN(step[k]); + } + } + } + for (i = 0; i < m - 1; i++) + { /* sort by s */ + for (j = i + 1; j < m; j++) + { + if (s[i] < s[j]) + continue; + std::swap(s[i], s[j]); + for (k = 0; k < n; k++) + std::swap(zn[k + i * n], zn[k + j * n]); + } + } + free(S); + free(dist); + free(zb); + free(z); + free(step); + + if (c >= LOOPMAX) + { + fprintf(stderr, "%s : search loop count overflow\n", __FILE__); + return -1; + } + return 0; } /* lambda/mlambda integer least-square estimation ------------------------------ -* integer least-square estimation. reduction is performed by lambda (ref.[1]), -* and search by mlambda (ref.[2]). -* args : int n I number of float parameters -* int m I number of fixed solutions -* double *a I float parameters (n x 1) -* double *Q I covariance matrix of float parameters (n x n) -* double *F O fixed solutions (n x m) -* double *s O sum of squared residulas of fixed solutions (1 x m) -* double Pf I pre-defined fail-rate -* double *Nfix O fixed solution (zeros or others in case of cs) -* int *index O validation status (0-failed,1-successful) -* return : status (0:ok,other:error) -* notes : matrix stored by column-major order (fortran convension) -*-----------------------------------------------------------------------------*/ + * integer least-square estimation. reduction is performed by lambda (ref.[1]), + * and search by mlambda (ref.[2]). + * args : int n I number of float parameters + * int m I number of fixed solutions + * double *a I float parameters (n x 1) + * double *Q I covariance matrix of float parameters (n x n) + * double *F O fixed solutions (n x m) + * double *s O sum of squared residulas of fixed solutions (1 x m) + * double Pf I pre-defined fail-rate + * double *Nfix O fixed solution (zeros or others in case of cs) + * int *index O validation status (0-failed,1-successful) + * return : status (0:ok,other:error) + * notes : matrix stored by column-major order (fortran convension) + *-----------------------------------------------------------------------------*/ int lambda( - Trace& trace, - int n, - int m, - const double *a, - const double *Q, - double *F, - double *s, - double Pf, - bool& pass) + Trace& trace, + int n, + int m, + const double* a, + const double* Q, + double* F, + double* s, + double Pf, + bool& pass +) { - int info,i; - double *L,*D,*Z,*z,*E,ratio,cratio=0,srate=1; - - if (n<=0||m<=0) - return -1; - - L=zeros(n,n); D=mat(n,1); Z=eye(n); z=mat(n,1),E=mat(n,m); - - /* LD factorization */ - if (!(info=LD(n,Q,L,D))) - { - /* lambda reduction */ - reduction(n,L,D,Z); - - /* success-rate calculation */ - for (i=0;i -#include - #include +#include +#include #include - -#include "eigenIncluder.hpp" -#include "coordinates.hpp" -#include "navigation.hpp" -#include "constants.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "enums.h" - -void updateLamMap( - const GTime& time, - SatPos& satPos) +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/navigation.hpp" +#include "orbprop/coordinates.hpp" + +void updateLamMap(const GTime& time, SatPos& satPos) { - E_Sys sys = satPos.Sat.sys; - if (satPos.satNav_ptr == nullptr) - { - return; - } - - auto& lamMap = satPos.satNav_ptr->lamMap; - - if (sys == +E_Sys::GLO) - { - auto* eph_ptr = seleph(nullStream, time, satPos.Sat, acsConfig.used_nav_types[sys], ANY_IODE, nav); - - if (eph_ptr) - { - auto& geph = *static_cast(eph_ptr); - - lamMap[G1] = CLIGHT / (FREQ1_GLO + DFRQ1_GLO * geph.frq); - lamMap[G2] = CLIGHT / (FREQ2_GLO + DFRQ2_GLO * geph.frq); - lamMap[G3] = CLIGHT / (FREQ3_GLO); - lamMap[G4] = CLIGHT / (FREQ4_GLO); - lamMap[G6] = CLIGHT / (FREQ6_GLO); - } - } - else - { - lamMap[F1] = CLIGHT / FREQ1; /* L1/E1/B1 */ - lamMap[F2] = CLIGHT / FREQ2; /* L2 */ - lamMap[F5] = CLIGHT / FREQ5; /* L5/E5a/B2a */ - lamMap[F6] = CLIGHT / FREQ6; /* E6/L6 */ - lamMap[F7] = CLIGHT / FREQ7; /* E5b/B2/B2b */ - lamMap[F8] = CLIGHT / FREQ8; /* E5a+b/B2a+b */ - - if (sys == +E_Sys::BDS) - { - lamMap[B1] = CLIGHT / FREQ1_CMP; /* B2-1 */ - lamMap[B3] = CLIGHT / FREQ3_CMP; /* B3 */ - } - } + E_Sys sys = satPos.Sat.sys; + if (satPos.satNav_ptr == nullptr) + { + return; + } + + auto& lamMap = satPos.satNav_ptr->lamMap; + + if (sys == E_Sys::GLO) + { + int freqNum = 100; + + auto it = nav.gloFreqMap.find(satPos.Sat); + if (it != nav.gloFreqMap.end()) + { + auto& [sat, freq] = *it; + + freqNum = freq; + } + else + { + auto* eph_ptr = seleph( + nullStream, + time, + satPos.Sat, + acsConfig.used_nav_types[sys], + ANY_IODE, + nav + ); + + if (eph_ptr) + { + auto& geph = *static_cast(eph_ptr); + + freqNum = geph.frq; + } + } + + if (freqNum > 20) + { + return; + } + + lamMap[G1] = CLIGHT / (FREQ1_GLO + DFRQ1_GLO * freqNum); + lamMap[G2] = CLIGHT / (FREQ2_GLO + DFRQ2_GLO * freqNum); + lamMap[G3] = CLIGHT / (FREQ3_GLO); + lamMap[G4] = CLIGHT / (FREQ4_GLO); + lamMap[G6] = CLIGHT / (FREQ6_GLO); + } + else + { + lamMap[F1] = CLIGHT / FREQ1; /* L1/E1/B1 */ + lamMap[F2] = CLIGHT / FREQ2; /* L2 */ + lamMap[F5] = CLIGHT / FREQ5; /* L5/E5a/B2a */ + lamMap[F6] = CLIGHT / FREQ6; /* E6/L6 */ + lamMap[F7] = CLIGHT / FREQ7; /* E5b/B2/B2b */ + lamMap[F8] = CLIGHT / FREQ8; /* E5a+b/B2a+b */ + + if (sys == E_Sys::BDS) + { + lamMap[B1] = CLIGHT / FREQ1_CMP; /* B2-1 */ + lamMap[B3] = CLIGHT / FREQ3_CMP; /* B3 */ + } + } } /** geometric distance -* compute geometric distance and receiver-to-satellite unit vector -* notes : distance includes sagnac effect correction -*/ + * compute geometric distance and receiver-to-satellite unit vector + * notes : distance includes sagnac effect correction + */ double geodist( - Vector3d& rs, ///< satellilte position (ecef at transmission) (m) - Vector3d& rr, ///< receiver position (ecef at reception) (m) - Vector3d& e) ///< line-of-sight vector (ecef) + Vector3d& rs, ///< satellilte position (ecef at transmission) (m) + Vector3d& rr, ///< receiver position (ecef at reception) (m) + Vector3d& e ///< line-of-sight vector (ecef) +) { - if (rs.norm() < RE_WGS84 * 0.9) - return -1; + if (rs.norm() < RE_WGS84 * 0.9) + return -1; - e = rs - rr; - double r = e.norm(); - e.normalize(); - return r + sagnac(rs, rr); + e = rs - rr; + double r = e.norm(); + e.normalize(); + return r + sagnac(rs, rr); } double sagnac( - Vector3d& rSource, //inertial position of source - Vector3d& rDest, //inertial position of destination - Vector3d vel) //inertial velocity + Vector3d& rSource, // inertial position of source + Vector3d& rDest, // inertial position of destination + Vector3d vel +) // inertial velocity { - if (vel.isZero()) - { - Vector3d omega = Vector3d::Zero(); + if (vel.isZero()) + { + Vector3d omega = Vector3d::Zero(); - omega.z() = OMGE; + omega.z() = OMGE; - vel = omega.cross(rDest); - } + vel = omega.cross(rDest); + } - //todo aaron, check which vel is required for slr things, still dest on outward journey? - return (rDest - rSource).dot(vel) / CLIGHT; + // todo aaron, check which vel is required for slr things, still dest on outward journey? + return (rDest - rSource).dot(vel) / CLIGHT; } /** satellite azimuth/elevation angle */ void satazel( - const VectorPos& pos, ///< geodetic position - const VectorEcef& e, ///< receiver-to-satellilte unit vector - AzEl& azel) ///< azimuth/elevation {az,el} (rad) + const VectorPos& pos, ///< geodetic position + const VectorEcef& e, ///< receiver-to-satellilte unit vector + AzEl& azel ///< azimuth/elevation {az,el} (rad) +) { - azel.az = 0; - azel.el = PI/2; + azel.az = 0; + azel.el = PI / 2; - //printf("pos %f %f %f e: %f enu: %f %f %f\n",pos[0],pos[1],pos[2],e,enu[0],enu[1],enu[2]); - if (pos.hgt() > -RE_WGS84) - { - VectorEnu enu = ecef2enu(pos, e); + // printf("pos %f %f %f e: %f enu: %f %f %f\n",pos[0],pos[1],pos[2],e,enu[0],enu[1],enu[2]); + if (pos.hgt() > -RE_WGS84) + { + VectorEnu enu = ecef2enu(pos, e); - azel.az = dot(enu.data(), enu.data(), 2) < 1E-12 ? 0.0 : atan2(enu.e(), enu.n()); + azel.az = dot(enu.data(), enu.data(), 2) < 1E-12 ? 0.0 : atan2(enu.e(), enu.n()); - if (azel.az < 0) - azel.az += 2 * PI; + if (azel.az < 0) + azel.az += 2 * PI; - azel.el = asin(enu.u()); - } + azel.el = asin(enu.u()); + } } /** compute DOP (dilution of precision) -*/ -Dops dopCalc( - const vector& azels) ///< satellite azimuth/elevation angles + */ +Dops dopCalc(const vector& azels) ///< satellite azimuth/elevation angles { - vector H; - H.reserve(64); - int n = 0; - - for (auto& azel : azels) - { - double cosel = cos(azel.el); - double sinel = sin(azel.el); - - H.push_back(cosel * sin(azel.az)); - H.push_back(cosel * cos(azel.az)); - H.push_back(sinel); - H.push_back(1); - n++; - } - - if (n < 4) - { - fprintf(stderr, "%s: Can not calculate the dops, less than 4 sats\n", __FUNCTION__); - return Dops(); - } - - auto H_mat = MatrixXd::Map(H.data(), 4, n).transpose(); - auto Q_mat = H_mat.transpose() * H_mat; - - auto Q_inv = Q_mat.inverse(); - - Dops dops; - dops.gdop = SQRT(Q_inv(0,0) + Q_inv(1,1) + Q_inv(2,2) + Q_inv(3,3) ); - dops.pdop = SQRT(Q_inv(0,0) + Q_inv(1,1) + Q_inv(2,2) ); - dops.hdop = SQRT(Q_inv(0,0) + Q_inv(1,1) ); - dops.vdop = SQRT( Q_inv(2,2) ); - - return dops; + vector H; + H.reserve(64); + int n = 0; + + for (auto& azel : azels) + { + double cosel = cos(azel.el); + double sinel = sin(azel.el); + + H.push_back(cosel * sin(azel.az)); + H.push_back(cosel * cos(azel.az)); + H.push_back(sinel); + H.push_back(1); + n++; + } + + if (n < 4) + { + fprintf(stderr, "%s: Can not calculate the dops, less than 4 sats\n", __FUNCTION__); + return Dops(); + } + + auto H_mat = MatrixXd::Map(H.data(), 4, n).transpose(); + auto Q_mat = H_mat.transpose() * H_mat; + + auto Q_inv = Q_mat.inverse(); + + Dops dops; + dops.gdop = SQRT(Q_inv(0, 0) + Q_inv(1, 1) + Q_inv(2, 2) + Q_inv(3, 3)); + dops.pdop = SQRT(Q_inv(0, 0) + Q_inv(1, 1) + Q_inv(2, 2)); + dops.hdop = SQRT(Q_inv(0, 0) + Q_inv(1, 1)); + dops.vdop = SQRT(Q_inv(2, 2)); + + return dops; } /** Low pass filter values -*/ -void lowPassFilter( - Average& avg, - double meas, - double procNoise, - double measVar) + */ +void lowPassFilter(Average& avg, double meas, double procNoise, double measVar) { - if (avg.var == 0) - { - avg.mean = meas; - avg.var = measVar; - return; - } + if (avg.var == 0) + { + avg.mean = meas; + avg.var = measVar; + return; + } - avg.var += SQR(procNoise); + avg.var += SQR(procNoise); - double delta = meas - avg.mean; - double varFrac = 1 / (measVar + avg.var); + double delta = meas - avg.mean; + double varFrac = 1 / (measVar + avg.var); - avg.mean += delta * varFrac; + avg.mean += delta * varFrac; - avg.var = 1 / (1 / measVar + 1 / avg.var); + avg.var = 1 / (1 / measVar + 1 / avg.var); } - diff --git a/src/cpp/sbas/decodeL1.cpp b/src/cpp/sbas/decodeL1.cpp new file mode 100644 index 000000000..146e331a8 --- /dev/null +++ b/src/cpp/sbas/decodeL1.cpp @@ -0,0 +1,42 @@ +#include "common/acsConfig.hpp" +#include "common/navigation.hpp" +#include "sbas/sbas.hpp" + +void decodeSBASMessage(Trace& trace, GTime time, SBASMessage& mess, Navigation& nav) +{ + int type = mess.type; + if (type == 0) + type = acsConfig.sbsInOpts.mt0; + + switch (type) + { + // case 1: decodeL1SBASMask(frameTime,nav,mess.data); break; // Satellite mask + // case 2: decodeL1FastCorr(frameTime,nav,mess.data,0); break; // Fast Corrections sat + // 1-13 case 3: decodeL1FastCorr(frameTime,nav,mess.data,13); break; // Fast + // Corrections sat 14-26 case 4: decodeL1FastCorr(frameTime,nav,mess.data,26); break; + // // Fast Corrections sat 27-39 case 5: decodeL1FastCorr(frameTime,nav,mess.data,39); + // break; // Fast Corrections sat 40-51 case 6: + // decodeL1UDREBase(frameTime,nav,mess.data); break; // UDRE case 7: + // decodeL1FastDegr( nav,mess.data); break; // Fast Correction degradation + // case 9: decodeL1GEO_Navg(frameTime,nav,mess.data,mess.prn); break; // GEO satellite + // position data (Ephemeris) case 10: decodeL1SlowDegr( nav,mess.data); + // break; // Slow Correction degradation case 12: + // decodeL1SBASTime(frameTime,nav,mess.data); break; // SBAS Network Time nav,message + // case 17: decodeL1GEO_Almn(frameTime,nav,mess.data); break; // GEO satellite + // position data (Almanac) *not supported case 18: decodeL1IonoGrid( nav,mess.data); + // break; // Ionosphere Grid points definition case 24: + // decodeL1MixdCorr(frameTime,nav,mess.data); break; // Fast & Slow Correction + // degradation *not supported case 25: decodeL1SlowCorr(frameTime,nav,mess.data); + // break; // Slow corrections case 26: decodeL1IonoGIVD(frameTime,nav,mess.data); + // break; // Ionosphere Correction at IGP case 27: + // decodeL1UDRERegn(frameTime,nav,mess.data); break; // UDRE Region definition case + // 28: decodeL1UDRECovr(frameTime,nav,mess.data); break; // UDRE covariance matrix + // case 62: + case 63: + break; + default: + tracepdeex(5, std::cout, "\nSBAS_MT%02d, not supported", type); + break; + } + return; +} diff --git a/src/cpp/sbas/decodeL5.cpp b/src/cpp/sbas/decodeL5.cpp new file mode 100644 index 000000000..f8299343c --- /dev/null +++ b/src/cpp/sbas/decodeL5.cpp @@ -0,0 +1,506 @@ +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/navigation.hpp" +#include "common/rtcmDecoder.hpp" +#include "orbprop/coordinates.hpp" +#include "sbas/sbas.hpp" + +#define DFMC_DEBUG_TRACE_LEVEL 5 + +struct sbsDFREsysDegr +{ + double Icorr = 30; + double Ccorr = 2.55; + double Rcorr = 0.051; +}; + +GTime mt37Time; +double IValidGNSS = -1; +double IValidGEO = -1; +double mt37CER = 31.5; +double mt37Ccov = 12.7; +int degrdType = 0; +map sysDegr; +map DFREtable; + +map> + SBASSatMasks; // Satellite mask updated by MT31, SBASSatMasks[IODP][index] = SatSys; +int lastIODM = -1; + +SatSys l5SatIndex(int sati) +{ + SatSys sat; + if (sati <= 0) + { + sat.sys = E_Sys::NONE; + sat.prn = 0; + } + else if (sati < 33) + { + sat.sys = E_Sys::GPS; + sat.prn = sati; + } + else if (sati > 37 && sati < 70) + { + sat.sys = E_Sys::GLO; + sat.prn = sati - 37; + } + else if (sati > 74 && sati < 111) + { + sat.sys = E_Sys::GAL; + sat.prn = sati - 74; + } + else if (sati > 119 && sati < 159) + { + sat.sys = E_Sys::SBS; + sat.prn = sati - 100; + } + else if (sati > 158 && sati < 196) + { + sat.sys = E_Sys::BDS; + sat.prn = sati - 158; + } + + return sat; +} + +void decodeL5SBASMask(Trace& trace, unsigned char* data) +{ + int iodm = getbitu(data, 224, 2); + + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, "L5 mask IODM: %1d", iodm); + + if (SBASSatMasks.find(iodm) != SBASSatMasks.end()) + SBASSatMasks[iodm].clear(); + + int i = 0; + for (int ind = 1; ind <= 214; ind++) + if (getbitu(data, ind + 9, 1)) + { + SatSys sat = l5SatIndex(ind); + if (!sat) + continue; + SBASSatMasks[iodm][i++] = sat; + + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, ", %s", sat.id().c_str()); + } + lastIODM = iodm; +} + +void decodeL5DFMCCorr(Trace& trace, GTime frameTime, Navigation& nav, unsigned char* data) +{ + int iod = 0; + + int i = 10; + int ns = acsConfig.sbsInOpts.use_do259 ? 8 : 9; + + int sati = getbituInc(data, i, ns); + + SatSys sat = l5SatIndex(sati); + if (!sat) + { + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, " Unknown satellite index %d", sati); + return; + } + SBASSlow sbs; + + int iode = getbituInc(data, i, 10); + sbs.iode = iode; + sbs.dPos[0] = getbitsInc(data, i, 11) * 0.0625; + sbs.dPos[1] = getbitsInc(data, i, 11) * 0.0625; + sbs.dPos[2] = getbitsInc(data, i, 11) * 0.0625; + sbs.dPos[3] = getbitsInc(data, i, 12) * 0.03125; + sbs.ddPos[0] = getbitsInc(data, i, 8) * P2_11; + sbs.ddPos[1] = getbitsInc(data, i, 8) * P2_11; + sbs.ddPos[2] = getbitsInc(data, i, 8) * P2_11; + sbs.ddPos[3] = getbitsInc(data, i, 9) * P2_12; + + int tod_int = getbituInc(data, i, 13); + sbs.Ivalid = IValidGNSS; + + if (acsConfig.sbsInOpts.pvs_on_dfmc) + { + if (tod_int % 2 == 0) + return; + sbs.Ivalid = 100; + } + + double tod1 = 16.0 * tod_int; + GTime teph = adjustDay(tod1, frameTime); + sbs.toe = teph; + sbs.trec = frameTime; + + auto& sbsMap = nav.satNavMap[sat].currentSBAS; + sbsMap.slowCorr[iode] = sbs; + sbsMap.corrUpdt[frameTime] = iode; + + tracepdeex( + DFMC_DEBUG_TRACE_LEVEL, + trace, + " L5 eph: %s, %s, %3d, %f, %f, %f, %f, %f, %f, %f, %f", + sat.id().c_str(), + teph.to_string(0).c_str(), + iode, + sbsMap.slowCorr[iode].dPos[0], + sbsMap.slowCorr[iode].dPos[1], + sbsMap.slowCorr[iode].dPos[2], + sbsMap.slowCorr[iode].dPos[3], + sbsMap.slowCorr[iode].ddPos[0], + sbsMap.slowCorr[iode].ddPos[1], + sbsMap.slowCorr[iode].ddPos[2], + sbsMap.slowCorr[iode].ddPos[3] + ); + + if (IValidGNSS > 0) + { + sbsMap.corrUpdt.erase( + sbsMap.corrUpdt.upper_bound(frameTime - IValidGNSS), + sbsMap.corrUpdt.end() + ); + for (auto it = sbsMap.slowCorr.begin(); it != sbsMap.slowCorr.end();) + { + auto trec = it->second.trec; + if ((frameTime - teph).to_double() > IValidGNSS) + it = sbsMap.slowCorr.erase(it); + else + it++; + } + } + + //-------------------------------------------- + if (lastIODM >= 0) + { + sbsMap.Integrity[lastIODM].trec = frameTime; + + double exponent = getbituInc(data, i, 3) - 5.0; + double scale = pow(2, exponent); + sbsMap.Integrity[lastIODM].REScale = scale; + + MatrixXd E = MatrixXd::Zero(4, 4); + E(0, 0) = getbituInc(data, i, 9); + E(1, 1) = getbituInc(data, i, 9); + E(2, 2) = getbituInc(data, i, 9); + E(3, 3) = getbituInc(data, i, 9); + E(0, 1) = getbitsInc(data, i, 10); + E(0, 2) = getbitsInc(data, i, 10); + E(0, 3) = getbitsInc(data, i, 10); + E(1, 2) = getbitsInc(data, i, 10); + E(1, 3) = getbitsInc(data, i, 10); + E(2, 3) = getbitsInc(data, i, 10); + MatrixXd R = scale * E; + MatrixXd C = R.transpose() * R; + sbsMap.Integrity[lastIODM].covr = C; + + sbsMap.Integrity[lastIODM].REint = getbituInc(data, i, 4); + sbsMap.Integrity[lastIODM].REBoost = false; + + if (acsConfig.sbsInOpts.use_do259) + sbsMap.Integrity[lastIODM].dRcorr = (getbituInc(data, i, 4) + 1) / 15; + else + sbsMap.Integrity[lastIODM].dRcorr = (getbituInc(data, i, 3) + 1) / 8; + + tracepdeex( + DFMC_DEBUG_TRACE_LEVEL, + trace, + ", %2d, %.3f", + sbsMap.Integrity[lastIODM].REint, + sbsMap.Integrity[lastIODM].dRcorr + ); + } +} + +void decodeL5DFMCInt1(Trace& trace, GTime frameTime, Navigation& nav, unsigned char* data) +{ + int iodm = getbitu(data, 224, 2); + if (SBASSatMasks.find(iodm) == SBASSatMasks.end()) + { + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, " corrections for unknown IODM: %1d", iodm); + return; + } + + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, " L5 DFRECI IODM: %1d", iodm); + + int i = 10; + int j = 0; + map changedDFRE; + for (int slot = 0; slot < 92; slot++) + { + if (SBASSatMasks[iodm].find(slot) == SBASSatMasks[iodm].end()) + continue; + SatSys sat = SBASSatMasks[iodm][slot]; + auto& sbs = nav.satNavMap[sat].currentSBAS; + + int DFRECI = getbituInc(data, i, 2); + switch (DFRECI) + { + case 0: + sbs.Integrity[iodm].REBoost = false; + break; + case 2: + sbs.Integrity[iodm].REBoost = true; + break; + case 1: + if (j < 7) + changedDFRE[j++] = sat; + case 3: + sbs.Integrity[iodm].REint = 15; + break; + } + sbs.Integrity[iodm].trec = frameTime; + } + + for (int slot = 0; slot < j; slot++) + { + SatSys sat = changedDFRE[slot]; + auto& sbs = nav.satNavMap[sat].currentSBAS; + + sbs.Integrity[iodm].REint = getbituInc(data, i, 4); + sbs.Integrity[iodm].REBoost = false; + sbs.Integrity[iodm].trec = frameTime; + } +} + +void decodeL5DFMCInt2(Trace& trace, GTime frameTime, Navigation& nav, unsigned char* data) +{ + int iodm = getbitu(data, 224, 2); + if (SBASSatMasks.find(iodm) == SBASSatMasks.end()) + { + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, " corrections for unknown IODM: %1d\n", iodm); + return; + } + + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, " L5 DFREI (1-53) IODM: %1d", iodm); + + int i = 10; + for (int slot = 0; slot < 53; slot++) + { + if (SBASSatMasks[iodm].find(slot) == SBASSatMasks[iodm].end()) + continue; + SatSys sat = SBASSatMasks[iodm][slot]; + auto& sbs = nav.satNavMap[sat].currentSBAS; + sbs.Integrity[iodm].REint = getbituInc(data, i, 4); + sbs.Integrity[iodm].REBoost = false; + sbs.Integrity[iodm].trec = frameTime; + } +} + +void decodeL5DFMCInt3(Trace& trace, GTime frameTime, Navigation& nav, unsigned char* data) +{ + int iodm = getbitu(data, 224, 2); + if (SBASSatMasks.find(iodm) == SBASSatMasks.end()) + { + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, " corrections for unknown IODM: %1d\n", iodm); + return; + } + + tracepdeex(DFMC_DEBUG_TRACE_LEVEL, trace, " L5 DFREI (54-92) IODM: %1d", iodm); + + int i = 10; + for (int slot = 53; slot < 92; slot++) + { + if (SBASSatMasks[iodm].find(slot) == SBASSatMasks[iodm].end()) + continue; + SatSys sat = SBASSatMasks[iodm][slot]; + auto& sbs = nav.satNavMap[sat].currentSBAS; + sbs.Integrity[iodm].REint = getbituInc(data, i, 4); + sbs.Integrity[iodm].REBoost = false; + sbs.Integrity[iodm].trec = frameTime; + } +} + +void decodeL5DFREDegr(Trace& trace, GTime frameTime, unsigned char* data) +{ + int i = 10; + IValidGNSS = getbituInc(data, i, 6) * 6.0 + 30.0; + IValidGEO = getbituInc(data, i, 6) * 6.0 + 30.0; + mt37CER = getbituInc(data, i, 6) * 0.5; + mt37Ccov = getbituInc(data, i, 7) * 0.1; + mt37Time = frameTime; + + sysDegr[E_Sys::GPS].Icorr = getbituInc(data, i, 5) * 6.0 + 30.0; + sysDegr[E_Sys::GPS].Ccorr = getbituInc(data, i, 8) * 0.01; + sysDegr[E_Sys::GPS].Rcorr = getbituInc(data, i, 8) * 0.2; + + sysDegr[E_Sys::GLO].Icorr = getbituInc(data, i, 5) * 6.0 + 30.0; + sysDegr[E_Sys::GLO].Ccorr = getbituInc(data, i, 8) * 0.01; + sysDegr[E_Sys::GLO].Rcorr = getbituInc(data, i, 8) * 0.2; + + sysDegr[E_Sys::GAL].Icorr = getbituInc(data, i, 5) * 6.0 + 30.0; + sysDegr[E_Sys::GAL].Ccorr = getbituInc(data, i, 8) * 0.01; + sysDegr[E_Sys::GAL].Rcorr = getbituInc(data, i, 8) * 0.2; + + sysDegr[E_Sys::BDS].Icorr = getbituInc(data, i, 5) * 6.0 + 30.0; + sysDegr[E_Sys::BDS].Ccorr = getbituInc(data, i, 8) * 0.01; + sysDegr[E_Sys::BDS].Rcorr = getbituInc(data, i, 8) * 0.2; + + sysDegr[E_Sys::SBS].Icorr = getbituInc(data, i, 5) * 6.0 + 30.0; + sysDegr[E_Sys::SBS].Ccorr = getbituInc(data, i, 8) * 0.01; + sysDegr[E_Sys::SBS].Rcorr = getbituInc(data, i, 8) * 0.2; + + i += 21; + + DFREtable[0] = getbituInc(data, i, 4) * 0.0625 + 0.125; + DFREtable[1] = getbituInc(data, i, 4) * 0.125 + 0.25; + DFREtable[2] = getbituInc(data, i, 4) * 0.125 + 0.375; + DFREtable[3] = getbituInc(data, i, 4) * 0.125 + 0.5; + DFREtable[4] = getbituInc(data, i, 4) * 0.125 + 0.625; + DFREtable[5] = getbituInc(data, i, 4) * 0.25 + 0.75; + DFREtable[6] = getbituInc(data, i, 4) * 0.25 + 1.0; + DFREtable[7] = getbituInc(data, i, 4) * 0.25 + 1.25; + DFREtable[8] = getbituInc(data, i, 4) * 0.25 + 1.5; + DFREtable[9] = getbituInc(data, i, 4) * 0.25 + 1.75; + DFREtable[10] = getbituInc(data, i, 4) * 0.5 + 2.0; + DFREtable[11] = getbituInc(data, i, 4) * 0.5 + 2.5; + DFREtable[12] = getbituInc(data, i, 4) * 1.0 + 3.0; + DFREtable[13] = getbituInc(data, i, 4) * 3.0 + 4.0; + DFREtable[14] = getbituInc(data, i, 4) * 6.0 + 10; + + tracepdeex( + DFMC_DEBUG_TRACE_LEVEL, + trace, + " L5 Degradation parameters: %f %f %f", + IValidGNSS, + mt37CER, + mt37Ccov + ); + + int timeRef = getbituInc(data, i, 3); // Only GPS time is supported, for now + if (!acsConfig.sbsInOpts.use_do259) + degrdType = getbituInc(data, i, 1); +} + +void decodeDFMCMessage(Trace& trace, GTime time, SBASMessage& mess, Navigation& nav) +{ + int type = mess.type; + if (type == 0) + type = acsConfig.sbsInOpts.mt0; + + if (type == 65) // Handling of SouthPAN L5 message type 0 + type = 33 + getbitu(mess.data, 222, 2); + + tracepdeex( + DFMC_DEBUG_TRACE_LEVEL, + trace, + "\nDFMCMESS %s Decoding %2d: ", + time.to_string().c_str(), + type + ); + + switch (type) + { + case 31: + decodeL5SBASMask(trace, mess.data); + break; // Satellite mask + case 32: + decodeL5DFMCCorr(trace, time, nav, mess.data); + break; // Satellite Corrections & Covariance + case 34: + decodeL5DFMCInt1(trace, time, nav, mess.data); + break; // Satellite Integrity Information (DFRECI) + case 35: + decodeL5DFMCInt2(trace, time, nav, mess.data); + break; // Satellite Integrity Information (DFREI 1 ~ 53) + case 36: + decodeL5DFMCInt3(trace, time, nav, mess.data); + break; // Satellite Integrity Information (DFREI 54 ~ 92) + case 37: + decodeL5DFREDegr(trace, time, mess.data); + break; // DFRE Correction degradation and scale + // case 39: decodeL5GEONavg1(trace,time,nav,mess.data); break; // GEO Ephemeris, clock + // and covariance 1 case 40: decodeL5GEONavg2(trace,time,nav,mess.data); break; // + // GEO Ephemeris, clock and covariance 2 case 42: + // decodeL5GNSSTime(trace,time,nav,mess.data); break; // GNSS Time Offset case 47: + // decodeL5GEO_Almn(trace,time,nav,mess.data); break; // GEO satellite position data + // (Almanac) case 62: + case 63: + break; + default: + tracepdeex(5, std::cout, "\nSBAS_MT%02d, not supported", type); + break; + } + return; +} + +double estimateDFMCVar(Trace& trace, GTime time, SatPos& satPos, SBASIntg& sbsIntg) +{ + int DFREI = sbsIntg.REint; + if (sbsIntg.REBoost) + DFREI++; + + if (DFREI < 0 || DFREI > 14) + return -1; + + double dt = (time - mt37Time).to_double(); + double maxDt = acsConfig.sbsInOpts.prec_aproach ? 240 : 360; + if (dt > maxDt) + return -1; + + double sigDFRE = DFREtable[DFREI]; + + SatStat& satStat = *satPos.satStat_ptr; + double x = 1e8; + if (satStat.e.norm() > 0) + { + VectorXd e = VectorXd::Zero(4); + for (int i = 0; i < 3; i++) + e[i] = satStat.e[i]; + e[3] = 1; + VectorXd Ce = sbsIntg.covr * e; + x = e.dot(Ce); + } + double dDFRE = sqrt(x) + mt37Ccov * sbsIntg.REScale; + + double rCorr = (dt > IValidGNSS) ? sbsIntg.dRcorr : 1; + auto sys = satPos.Sat.sys; + double eCorr = sysDegr[sys].Ccorr * floor(dt / sysDegr[sys].Icorr) + + sysDegr[sys].Rcorr * rCorr * dt / 1000; + + double eer = (dt > IValidGNSS) ? mt37CER : 0; + + double var = -1; + + if (degrdType == 1) + var = SQR((SQR(sigDFRE) + eCorr + eer) * dDFRE); + else + var = SQR(sigDFRE * dDFRE) + SQR(eCorr) + SQR(eer); + + tracepdeex( + 5, + trace, + "\nSBASVAR %s %s, DFRE= %2d %.4f, dDFRE: %.5e %.5e, eCorr: %.3f %.3f %.3f, eer: %3f, " + "total: %.3f", + time.to_string().c_str(), + satPos.Sat.id().c_str(), + sbsIntg.REint, + sigDFRE, + sqrt(x), + dDFRE, + dt, + rCorr, + eCorr, + eer, + sqrt(var) + ); + + if (acsConfig.sbsInOpts.pvs_on_dfmc) + var = SQRT(0.005); + + return var; +} + +void estimateDFMCPL(Vector3d& staPos, Matrix3d& ecefP, double& horPL, double& verPL) +{ + VectorPos pos = ecef2pos(staPos); + Matrix3d E; + pos2enu(pos, E.data()); + Matrix3d EP = E * ecefP; + Matrix3d enuP = EP * E.transpose(); + + double scaleH = acsConfig.sbsInOpts.prec_aproach ? 6.00 : 6.18; + double scaleV = 5.33; + double aveEN = (enuP(0, 0) + enuP(1, 1)) / 2; + double difEN = (enuP(0, 0) - enuP(1, 1)) / 2; + double covEN = enuP(0, 1); + horPL = scaleH * sqrt(aveEN + sqrt(difEN * difEN + covEN * covEN)); + verPL = scaleV * sqrt(enuP(2, 2)); +} \ No newline at end of file diff --git a/src/cpp/sbas/sbas.cpp b/src/cpp/sbas/sbas.cpp index e495fad0f..eddddca90 100644 --- a/src/cpp/sbas/sbas.cpp +++ b/src/cpp/sbas/sbas.cpp @@ -1,74 +1,357 @@ -#include "inputsOutputs.hpp" -#include "acsConfig.hpp" -#include "common.hpp" -#include "sbas.hpp" +#include "sbas/sbas.hpp" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/navigation.hpp" +#include "common/receiver.hpp" +#include "orbprop/coordinates.hpp" +#include "pea/inputsOutputs.hpp" + +#define MAX_SBAS_CORR_AGE 300 using std::lock_guard; -GTime lastEMSWritten; -string lastEMSFile; +GTime lastEMSLoaded; +GTime lastEMSWritten; +string lastEMSFile; + +struct SBASSmoothControl +{ + GTime lastUpdate; + int numMea = 0; + double ambEst = 0; + double ambVar = -1; +}; + +map> smoothedMeasMap; -mutex sbasMessagesMutex; -map sbasMessages; +mutex sbasMessagesMutex; +map sbasMessages; + +GTime adjustDay(double tod1, GTime nearTime) +{ + GTow tow0 = nearTime; + double tod0 = fmod(tow0, secondsInDay); + double delta = tod1 - tod0; + while (delta > +secondsInDay / 2) + delta -= secondsInDay; + while (delta < -secondsInDay / 2) + delta += secondsInDay; + GTime teph = nearTime + delta; + return teph; +} void writeEMSline( - GTime time, ///< Time of bias to write - SBASMessage& sbasMessage, ///< SBAS message data - Trace& trace) ///< Stream to output to + GTime time, ///< Time of bias to write + SBASMessage& sbasMessage, ///< SBAS message data + Trace& trace ///< Stream to output to +) { - GEpoch epoch = time; - - tracepdeex(0, trace, " %3d %02d %02d %02d %02d %02d %02d %2d %s\r\n", - sbasMessage.prn, - ((int) epoch.year) % 100, - (int) epoch.month, - (int) epoch.day, - (int) epoch.hour, - (int) epoch.min, - (int) epoch.sec, - sbasMessage.type, - sbasMessage.message); + GEpoch epoch = time; + + tracepdeex( + 0, + trace, + " %3d %02d %02d %02d %02d %02d %02d %2d %s\r\n", + sbasMessage.prn, + ((int)epoch.year) % 100, + (int)epoch.month, + (int)epoch.day, + (int)epoch.hour, + (int)epoch.min, + (int)epoch.sec, + sbasMessage.type, + sbasMessage.message + ); } /** Write received SBAS messages into EMS files -*/ + */ void writeEMSdata( - Trace& trace, ///< Trace to output to - string filename) ///< File to write + Trace& trace, ///< Trace to output to + string filename ///< File to write +) +{ + string checkFile = acsConfig.ems_filename; + + lock_guard guard(sbasMessagesMutex); + for (auto [frameTime, sbasData] : sbasMessages) + { + if (frameTime > lastEMSWritten) + { + // todo aaron, use the standard file rotations + PTime pTime = frameTime; + boost::posix_time::ptime otherPTime = + boost::posix_time::from_time_t((time_t)pTime.bigTime); + + replaceTimes(checkFile, otherPTime); + + if (checkFile != lastEMSFile) + { + lastEMSFile = checkFile; + tracepdeex(3, trace, "\nStarting new EMS file: %s\n", lastEMSFile.c_str()); + } + + std::ofstream outputStream(lastEMSFile, std::fstream::app); + if (!outputStream) + { + BOOST_LOG_TRIVIAL(error) << "Cannot open EMS file:" << lastEMSFile; + trace << "ERROR: cannot open EMS file:" << lastEMSFile; + + break; + } + + tracepdeex(4, trace, "\nWriting EMS file line: %s\n", frameTime.to_string().c_str()); + writeEMSline(frameTime, sbasData, outputStream); + + lastEMSWritten = frameTime; + } + } +} + +bool checkType(int frq, int type) +{ + if (type == 0) + return true; + if (type > 61) + return true; + if (frq == 1 && type < 31) + return true; + if (frq == 5 && type > 30) + return true; + return false; +} + +void readEMSdata( ///< Trace to output to + string filename +) ///< File to write { - string checkFile = acsConfig.ems_filename; - - lock_guard guard(sbasMessagesMutex); - for (auto [frameTime, sbasData] : sbasMessages) - { - if (frameTime > lastEMSWritten) - { - // todo aaron, use the standard file rotations - PTime pTime = frameTime; - boost::posix_time::ptime otherPTime = boost::posix_time::from_time_t((time_t)pTime.bigTime); - - replaceTimes(checkFile, otherPTime); - - if (checkFile != lastEMSFile) - { - lastEMSFile = checkFile; - tracepdeex(3, trace, "\nStarting new EMS file: %s\n", lastEMSFile.c_str()); - } - - std::ofstream outputStream(lastEMSFile, std::fstream::app); - if (!outputStream) - { - BOOST_LOG_TRIVIAL(error) << "ERROR: cannot open EMS file:" << lastEMSFile; - trace << "ERROR: cannot open EMS file:" << lastEMSFile; - - break; - } - - tracepdeex(4, trace, "\nWriting EMS file line: %s\n", frameTime.to_string().c_str()); - writeEMSline(frameTime, sbasData, outputStream); - - lastEMSWritten = frameTime; - } - } + std::ifstream fileStream(filename); + if (!fileStream) + { + BOOST_LOG_TRIVIAL(info) << "ems file open error " << filename; + return; + } + + int prn_ = acsConfig.sbsInOpts.prn; + int freq = acsConfig.sbsInOpts.freq; + + string line; + while (fileStream) + { + getline(fileStream, line); + if (line.size() < 88) + continue; + + int prn = std::stoi(line.substr(0, 3), nullptr, 10); + int typ = std::stoi(line.substr(22, 2), nullptr, 10); + + if (prn != prn_) + continue; + if (!checkType(freq, typ)) + continue; + + double ep[6]; + ep[0] = (double)std::stoi(line.substr(4, 2), nullptr, 10); + ep[1] = (double)std::stoi(line.substr(7, 2), nullptr, 10); + ep[2] = (double)std::stoi(line.substr(10, 2), nullptr, 10); + ep[3] = (double)std::stoi(line.substr(13, 2), nullptr, 10); + ep[4] = (double)std::stoi(line.substr(16, 2), nullptr, 10); + ep[5] = (double)std::stoi(line.substr(19, 2), nullptr, 10); + ep[0] += 100 * round((acsConfig.sbsInOpts.ems_year - ep[0]) / 100); + GTime messTime = epoch2time(ep); + + SBASMessage sbas; + sbas.prn = prn_; + sbas.freq = freq; + sbas.type = typ; + sbas.message = line.substr(25, 64); + + for (int i = 0; i < 32; i++) + { + unsigned char byte = + (unsigned char)std::stoi(sbas.message.substr(2 * i, 2), nullptr, 16); + sbas.data[i] = byte; + } + + { + lock_guard guard(sbasMessagesMutex); + sbasMessages[messTime] = sbas; + } + } +} + +void loadSBASdata(Trace& trace, GTime time, Navigation& nav) +{ + tracepdeex( + 5, + trace, + "\nSBASMESS Loading SBAS messages up to %s, %d", + time.to_string().c_str(), + sbasMessages.size() + ); + + for (auto& [sat, satDat] : nav.satNavMap) + { + auto& sbs = satDat.currentSBAS; + for (auto it = sbs.corrUpdt.begin(); it != sbs.corrUpdt.end();) + { + auto teph = it->first; + if ((time - teph).to_double() > MAX_SBAS_CORR_AGE) + it = sbs.corrUpdt.erase(it); + else + it++; + } + for (auto it = sbs.slowCorr.begin(); it != sbs.slowCorr.end();) + { + auto teph = it->second.trec; + if ((time - teph).to_double() > MAX_SBAS_CORR_AGE) + it = sbs.slowCorr.erase(it); + else + it++; + } + } + + lock_guard guard(sbasMessagesMutex); + for (auto it = sbasMessages.begin(); it != sbasMessages.end();) + { + auto [frameTime, mess] = *it; + if ((time - frameTime).to_double() > MAX_SBAS_CORR_AGE) + it = sbasMessages.erase(it); + else + break; + } + + for (auto& [frameTime, mess] : sbasMessages) + { + if ((lastEMSLoaded - frameTime).to_double() >= 0) + continue; + if ((time - frameTime).to_double() < 0) + break; + + if (acsConfig.sbsInOpts.freq == 1) + decodeSBASMessage(trace, frameTime, mess, nav); + if (acsConfig.sbsInOpts.freq == 5) + decodeDFMCMessage(trace, frameTime, mess, nav); + lastEMSLoaded = frameTime; + } +} + +void estimateSBASProtLvl(Vector3d& staPos, Matrix3d& ecefP, double& horPL, double& verPL) +{ + horPL = -1; + verPL = -1; + + switch (acsConfig.sbsInOpts.freq) + { + case 5: + estimateDFMCPL(staPos, ecefP, horPL, verPL); + return; + default: + return; + } +} + +double sbasSmoothedPsudo( + Trace& trace, + GTime time, + SatSys Sat, + string staId, + double measP, + double measL, + double variP, + double variL, + double& varSmooth, + bool update +) +{ + varSmooth = -1; + if (variP < 0 || variL < 0) + return measP; + + auto& smCtrl = smoothedMeasMap[Sat][staId]; + double fact = 1.0 / smCtrl.numMea; + if (update) + { + bool slip = false; + if ((time - smCtrl.lastUpdate).to_double() > acsConfig.sbsInOpts.smth_out) + slip = true; + if (smCtrl.ambVar < 0) + slip = true; + + double ambMea = measP - measL; + if (fabs(ambMea - smCtrl.ambEst) > 4 * sqrt(smCtrl.ambVar + variP + variL)) + slip = true; // Try replacing this with a outputs from preprocessor + + smCtrl.lastUpdate = time; + if (slip) + { + smCtrl.numMea = 0; + smCtrl.ambEst = 0; + smCtrl.ambVar = 0; + } + + smCtrl.numMea++; + if (smCtrl.numMea > acsConfig.sbsInOpts.smth_win) + smCtrl.numMea = acsConfig.sbsInOpts.smth_win; + fact = 1.0 / smCtrl.numMea; + + smCtrl.ambEst += fact * (ambMea - smCtrl.ambEst); + smCtrl.ambVar = SQR(fact) * (variP + variL) + SQR(1 - fact) * smCtrl.ambVar; + } + varSmooth = (1 - 2 * fact) * variL + smCtrl.ambVar; + return smCtrl.ambEst + measL; +} + +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; + return; + } + + output.seekp(0, output.end); // seek to end of file + + if (output.tellp() == 0) + tracepdeex( + 2, + output, + "\n*YYYY-MM-DDTHH:MM:SS.SSS YYYY.YYYYYYYYY X Y Z " + "HDOP VDOP GDOP - - - NLat Elong Height dN " + " dE dU dH HPL VPL - - - -" + ); + + auto& apriori = rec.aprioriPos; + auto& sppPos = rec.sol.sppPos; + auto& sppTime = rec.sol.sppTime; + VectorPos pos = ecef2pos(apriori); + VectorEnu dpos = ecef2enu(pos, sppPos - apriori); + + string tstr = sppTime.to_ISOstring(3); + double tyear = sppTime.to_decYear(); + tracepdeex( + 2, + output, + "\n %s %14.9f %11.5f %11.5f %11.5f %11.4f %11.4f %11.4f - - - %15.10f %15.10f %11.5f " + "%11.5f %11.5f %11.5f %11.5f %11.5f %11.5f - - -", + tstr, + tyear, + sppPos[0], + sppPos[1], + sppPos[2], + rec.sol.dops.hdop, + rec.sol.dops.vdop, + rec.sol.dops.gdop, + pos[0] * R2D, + pos[1] * R2D, + pos[2], + dpos[0], + dpos[1], + dpos[2], + sqrt(dpos[0] * dpos[0] + dpos[1] * dpos[1]), + rec.sol.horzPL, + rec.sol.vertPL + ); } diff --git a/src/cpp/sbas/sbas.hpp b/src/cpp/sbas/sbas.hpp index 13df88916..ec10e248f 100644 --- a/src/cpp/sbas/sbas.hpp +++ b/src/cpp/sbas/sbas.hpp @@ -1,28 +1,157 @@ - #pragma once #include +#include "common/constants.hpp" +#include "common/gTime.hpp" +#include "common/trace.hpp" using std::mutex; -#include "constants.hpp" -#include "trace.hpp" -#include "gTime.hpp" +// forward declarations +struct Navigation; +struct Receiver; +struct SatSys; +struct SatPos; struct SBASMessage { - int prn = -1; - int freq = 1; - int type = 63; - string message; - unsigned char data[32]; + int prn = -1; + int freq = 1; + int type = 63; + string message; + unsigned char data[32]; +}; + +struct SBASFast +{ + GTime toe; + double dClk; + double var; +}; + +struct SBASSlow +{ + GTime toe; + int iode; + Vector4d dPos; + Vector4d ddPos; + GTime trec; + double Ivalid = -1; + bool pvsEnabled = false; +}; + +struct SBASRegn +{ + int priority; + double Lat1; + double Lat2; + double Lon1; + double Lon2; + bool triangular = false; + double in_Factor; + double outFactor; +}; + +struct SBASDegrL1 +{ + double Brrc; + double Cltc_lsb; + double Cltc_v1; + double Iltc_v1; + double Cltc_v0; + double Iltc_v0; + double Cgeo_lsb; + double Cgeo_v; + double Igeo_v; + double Cer; + double Ciono_step; + double Ciono_ramp; + double Iiono; + bool RSSudre; + bool RSSiono; + double Ccovar; +}; + +struct SBASDegrSys +{ + double Icorr; + double Ccorr; + double Rcorr; +}; +struct SBASDegrL5 +{ + double IValidGNSS = -1; + double IValidGEO; + double CER; + double Ccov; + map sysDegr; + map DFREtable; + int type = 0; +}; + +struct SBASIntg +{ + GTime trec; + int REint; + bool REBoost = false; + + double REScale; + MatrixXd covr; + double dRcorr; +}; + +struct SBASMaps +{ + map> corrUpdt; + + map slowCorr; + map Integrity; + + map fastCorr; + map UDRERegn; }; -extern mutex sbasMessagesMutex; -extern map sbasMessages; +struct SBASIono +{ + map IGPLati; + map IGPLong; + map IGPGIVD; + map IGPGIVE; +}; + +extern mutex sbasMessagesMutex; +extern map sbasMessages; +extern Vector3d sbasRoughStaPos; + +void writeEMSdata(Trace& trace, string emsfile); + +void readEMSdata(string emsfile); + +void loadSBASdata(Trace& trace, GTime time, Navigation& nav); + +void decodeSBASMessage(Trace& trace, GTime time, SBASMessage& mess, Navigation& nav); + +void decodeDFMCMessage(Trace& trace, GTime time, SBASMessage& mess, Navigation& nav); + +GTime adjustDay(double tod1, GTime nearTime); + +double estimateDFMCVar(Trace& trace, GTime time, SatPos& satPos, SBASIntg& sbsIntg); + +void estimateSBASProtLvl(Vector3d& staPos, Matrix3d& ecefP, double& horPL, double& verPL); +void estimateDFMCPL(Vector3d& staPos, Matrix3d& ecefP, double& horPL, double& verPL); -void writeEMSdata( - Trace& trace, - string biasfile); +double sbasSmoothedPsudo( + Trace& trace, + GTime time, + SatSys Sat, + string staId, + double measP, + double measL, + double variP, + double variL, + double& varSmooth, + bool update +); +void writeSPP(string filename, Receiver& rec); \ No newline at end of file diff --git a/src/cpp/sbas/sisnet.cpp b/src/cpp/sbas/sisnet.cpp index e9720e3cd..02abb1e19 100644 --- a/src/cpp/sbas/sisnet.cpp +++ b/src/cpp/sbas/sisnet.cpp @@ -1,245 +1,287 @@ -#include "acsConfig.hpp" -#include "common.hpp" -#include "sisnet.hpp" -#include "sbas.hpp" - +#include "sbas/sisnet.hpp" #include +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "sbas/sbas.hpp" + +namespace bp = boost::asio::placeholders; using std::lock_guard; using std::mutex; +#define MAX_SBAS_MESS_AGE 600 -#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.fail()) \ + { \ + inputStream.clear(); \ + inputStream.seekg(pos); \ + return; \ + } namespace bp = boost::asio::placeholders; -void DS2DCParser::parse( - std::istream& inputStream) +void DS2DCParser::parse(std::istream& inputStream) { - string buff; + string buff; while (inputStream) - { - std::getline(inputStream, buff); - - if (buff.empty()) - continue; - - auto strStart = buff.find("*MSG"); - - if (strStart == string::npos) - { - BOOST_LOG_TRIVIAL(error) << "Invalid SISNET message: " << buff; - - continue; - } - - string tmpBuff = buff.substr(strStart + 5); - int week = std::stoi(tmpBuff, &strStart); - - tmpBuff = buff.substr(strStart + 6); - - auto strEnd = tmpBuff.find(","); - - double tow = std::stod(tmpBuff.substr(0,strEnd)); - - string mess = tmpBuff.substr(strEnd + 1); - - size_t index; - - for (auto expander : {'|', '/'}) - while (index = mess.find(expander), index != string::npos) - { - int digits = 0; - if (expander == '|') digits = 1; - if (expander == '/') digits = 2; - - char character = mess[index - 1]; - - int repeat = std::stoi(mess.substr(index+1, digits), nullptr, 16)-1; - - string replacement(repeat, character); - - mess.replace(index, digits + 1, replacement); - } - - auto tail = mess.find("*"); - - SBASMessage sbas; + { + std::getline(inputStream, buff); - sbas.prn = acsConfig.sbsInOpts.prn; - sbas.freq = acsConfig.sbsInOpts.freq; - sbas.message = mess.substr(0, tail); + if (buff.empty()) + continue; - if (sbas.freq == 1) sbas.type = std::stoi(mess.substr(2,2), nullptr, 16) >> 2; - else sbas.type = std::stoi(mess.substr(1,2), nullptr, 16) >> 2; + auto strStart = buff.find("*MSG"); - BOOST_LOG_TRIVIAL(debug) << "SISNET message: " << sbas.message; + if (strStart == string::npos) + { + BOOST_LOG_TRIVIAL(error) << "Invalid SISNET message: " << buff; - /* CRC check */ - unsigned char frame[29] = {}; - unsigned int frameCRC = 0; + continue; + } - for (int i = 0; i < 32; i++) - { - unsigned char byte = (unsigned char) std::stoi(sbas.message.substr(2*i,2), nullptr, 16); + string tmpBuff = buff.substr(strStart + 5); + int week = std::stoi(tmpBuff, &strStart); - sbas.data[i] = byte; + tmpBuff = buff.substr(strStart + 6); + + auto strEnd = tmpBuff.find(","); - if (i < 28) { frame[i]+=((byte>>6) & 0xFFFF); frame[i+1] +=((byte<< 2) & 0x00FFFF); } - else if (i == 28) { frame[i]+=((byte>>6) & 0xFFFF); frameCRC +=((byte<<18) & 0xFC0000); } - else if (i == 29) { frameCRC +=((byte<<10) & 0x03FC00); } - else if (i == 30) { frameCRC +=((byte<< 2) & 0x0003FC); } - else if (i == 31) { frameCRC +=((byte>> 6) & 0x000003); } - } - - unsigned int calcCRC = crc24q(frame, 29); - - if (calcCRC != frameCRC) - { - BOOST_LOG_TRIVIAL(error) << "CRC error in SISNET channel"; - continue; - } - - GTime frametime = gpst2time(week, tow); - - { - lock_guard guard(sbasMessagesMutex); - - for (auto it = sbasMessages.begin(); it != sbasMessages.end(); ) - { - auto& [time, sbasData] = *it; - if ((frametime - time).to_double() > 90) - { - it = sbasMessages.erase(it); - } - else - { - it++; - } - } - - sbasMessages[frametime] = sbas; - } - } - inputStream.clear(); + double tow = std::stod(tmpBuff.substr(0, strEnd)); + + string mess = tmpBuff.substr(strEnd + 1); + + size_t index; + + for (auto expander : {'|', '/'}) + while (index = mess.find(expander), index != string::npos) + { + int digits = 0; + if (expander == '|') + digits = 1; + if (expander == '/') + digits = 2; + + char character = mess[index - 1]; + + int repeat = std::stoi(mess.substr(index + 1, digits), nullptr, 16) - 1; + + string replacement(repeat, character); + + mess.replace(index, digits + 1, replacement); + } + + auto tail = mess.find("*"); + + SBASMessage sbas; + + sbas.prn = acsConfig.sbsInOpts.prn; + sbas.freq = acsConfig.sbsInOpts.freq; + sbas.message = mess.substr(0, tail); + + if (sbas.freq == 1) + sbas.type = std::stoi(mess.substr(2, 2), nullptr, 16) >> 2; + else + sbas.type = std::stoi(mess.substr(1, 2), nullptr, 16) >> 2; + + BOOST_LOG_TRIVIAL(debug) << "SISNET message: " << sbas.message; + + /* CRC check */ + unsigned char frame[29] = {}; + unsigned int frameCRC = 0; + + for (int i = 0; i < 32; i++) + { + unsigned char byte = + (unsigned char)std::stoi(sbas.message.substr(2 * i, 2), nullptr, 16); + + sbas.data[i] = byte; + + if (i < 28) + { + frame[i] += ((byte >> 6) & 0xFFFF); + frame[i + 1] += ((byte << 2) & 0x00FFFF); + } + else if (i == 28) + { + frame[i] += ((byte >> 6) & 0xFFFF); + frameCRC += ((byte << 18) & 0xFC0000); + } + else if (i == 29) + { + frameCRC += ((byte << 10) & 0x03FC00); + } + else if (i == 30) + { + frameCRC += ((byte << 2) & 0x0003FC); + } + else if (i == 31) + { + frameCRC += ((byte >> 6) & 0x000003); + } + } + + unsigned int calcCRC = crc24q(frame, 29); + + if (calcCRC != frameCRC) + { + BOOST_LOG_TRIVIAL(error) << "CRC error in SISNET channel"; + continue; + } + + GTime frametime = gpst2time(week, tow); + + { + lock_guard guard(sbasMessagesMutex); + + for (auto it = sbasMessages.begin(); it != sbasMessages.end();) + { + auto& [time, sbasData] = *it; + if ((frametime - time).to_double() > MAX_SBAS_MESS_AGE) + { + it = sbasMessages.erase(it); + } + else + { + it++; + } + } + + sbasMessages[frametime] = sbas; + } + } + inputStream.clear(); } -void SisnetStream::sisnetHandler( - const boost::system::error_code& err) +void SisnetStream::sisnetHandler(const boost::system::error_code& err) { if (err) - { - delayedReconnect(); - return; - } - - vector responseVec; - int nread = downloadBuf.size(); - responseVec.resize(nread); - buffer_copy(boost::asio::buffer(responseVec), downloadBuf.data()); - downloadBuf.consume(nread); - - dataChunkDownloaded(responseVec); - - boost::asio::async_read_until(*_socket, downloadBuf, "\n", boost::bind(&SisnetStream::sisnetHandler, this, bp::error)); + { + delayedReconnect(); + return; + } + + vector responseVec; + int nread = downloadBuf.size(); + responseVec.resize(nread); + buffer_copy(boost::asio::buffer(responseVec), downloadBuf.data()); + downloadBuf.consume(nread); + + dataChunkDownloaded(responseVec); + + boost::asio::async_read_until( + *_socket, + downloadBuf, + "\n", + boost::bind(&SisnetStream::sisnetHandler, this, bp::error) + ); } -void SisnetStream::sisnetResponseHandler( - const boost::system::error_code& err) +void SisnetStream::sisnetResponseHandler(const boost::system::error_code& err) { if (err) - { - return; - // delayed_reconnect(); - } - - vector responseVec; - - int nread = downloadBuf.size(); - responseVec.resize(nread); - - buffer_copy(boost::asio::buffer(responseVec), downloadBuf.data()); - downloadBuf.consume(nread); - - string private_string; - private_string.assign(responseVec.begin(), responseVec.end()); - - size_t pos = private_string.find("*START"); - if (pos == string::npos) - { - BOOST_LOG_TRIVIAL(error) << "Error in starting string : Invalid Server Response: " << private_string; - delayedReconnect(); - return; - } - - isConnected = true; - - boost::asio::socket_base::keep_alive option(true); - socket_ptr->set_option(option); - - // The connection was successful. Send the request. - boost::asio::async_read_until(*_socket, downloadBuf, "\n", boost::bind(&SisnetStream::sisnetHandler, this, bp::error)); + { + return; + // delayed_reconnect(); + } + + vector responseVec; + + int nread = downloadBuf.size(); + responseVec.resize(nread); + + buffer_copy(boost::asio::buffer(responseVec), downloadBuf.data()); + downloadBuf.consume(nread); + + string private_string; + private_string.assign(responseVec.begin(), responseVec.end()); + + size_t pos = private_string.find("*START"); + if (pos == string::npos) + { + BOOST_LOG_TRIVIAL(error) << "Error in starting string : Invalid Server Response: " + << private_string; + delayedReconnect(); + return; + } + + isConnected = true; + + boost::asio::socket_base::keep_alive option(true); + socket_ptr->set_option(option); + + // The connection was successful. Send the request. + boost::asio::async_read_until( + *_socket, + downloadBuf, + "\n", + boost::bind(&SisnetStream::sisnetHandler, this, bp::error) + ); } -void SisnetStream::sisnetStartHandler( - const boost::system::error_code& err) +void SisnetStream::sisnetStartHandler(const boost::system::error_code& err) { if (err) - { - delayedReconnect(); - return; - } - - timer.expires_from_now(boost::posix_time::seconds(10)); - timer.async_wait(boost::bind(&SisnetStream::timeoutHandler, this, boost::asio::placeholders::error)); - - boost::asio::async_read_until(*_socket, downloadBuf, "\n", boost::bind(&SisnetStream::sisnetResponseHandler, this, boost::asio::placeholders::error)); + { + delayedReconnect(); + return; + } + + timer.expires_from_now(boost::posix_time::seconds(10)); + timer.async_wait( + boost::bind(&SisnetStream::timeoutHandler, this, boost::asio::placeholders::error) + ); + + boost::asio::async_read_until( + *_socket, + downloadBuf, + "\n", + boost::bind(&SisnetStream::sisnetResponseHandler, this, boost::asio::placeholders::error) + ); } -void SisnetStream::requestResponseHandler( - const boost::system::error_code& err) +void SisnetStream::requestResponseHandler(const boost::system::error_code& err) { if (err) - { - delayedReconnect(); - return; - } + { + delayedReconnect(); + return; + } - vector responseVec; + vector responseVec; - int nread = downloadBuf.size(); - responseVec.resize(nread); + int nread = downloadBuf.size(); + responseVec.resize(nread); - buffer_copy(boost::asio::buffer(responseVec), downloadBuf.data()); - downloadBuf.consume(nread); + buffer_copy(boost::asio::buffer(responseVec), downloadBuf.data()); + downloadBuf.consume(nread); - string privateString; - privateString.assign(responseVec.begin(), responseVec.end()); + string privateString; + privateString.assign(responseVec.begin(), responseVec.end()); - size_t pos = privateString.find("*AUTH"); - if (pos == string::npos) - { - BOOST_LOG_TRIVIAL(error) << "Error in Authentication : Invalid Server Response: " << privateString; - delayedReconnect(); - return; - } + size_t pos = privateString.find("*AUTH"); + if (pos == string::npos) + { + BOOST_LOG_TRIVIAL(error) << "Error in Authentication : Invalid Server Response: " + << privateString; + delayedReconnect(); + return; + } - // boost::this_thread::sleep( boost::posix_time::milliseconds(100)); + // boost::this_thread::sleep( boost::posix_time::milliseconds(100)); - int n = request.size(); - request.consume(n); + int n = request.size(); + request.consume(n); - std::ostream requestStream(&request); - requestStream << "START\n"; + std::ostream requestStream(&request); + requestStream << "START\n"; - // The connection was successful. Send the request. - boost::asio::async_write(*_socket, request, boost::bind(&SisnetStream::sisnetStartHandler, this, boost::asio::placeholders::error)); + // The connection was successful. Send the request. + boost::asio::async_write( + *_socket, + request, + boost::bind(&SisnetStream::sisnetStartHandler, this, boost::asio::placeholders::error) + ); } diff --git a/src/cpp/sbas/sisnet.hpp b/src/cpp/sbas/sisnet.hpp index bd90eeeca..15bc6ac99 100644 --- a/src/cpp/sbas/sisnet.hpp +++ b/src/cpp/sbas/sisnet.hpp @@ -1,52 +1,42 @@ - #pragma once #include +#include #include #include +#include "common/streamSerial.hpp" +#include "common/tcpSocket.hpp" using std::string; using std::vector; -#include "streamSerial.hpp" -#include "tcpSocket.hpp" - - -#include - -struct DS2DCParser: Parser +struct DS2DCParser : Parser { - void parse( - std::istream& inputStream); + void parse(std::istream& inputStream); - string parserType() - { - return "DS2DCParser"; - } + string parserType() { return "DS2DCParser"; } }; struct SisnetStream : TcpSocket { - SisnetStream( - const string& path) - : TcpSocket(path, "\n") - { - std::stringstream requestStream; - requestStream << "AUTH,"; - requestStream << url.user; - requestStream << ','; - requestStream << url.pass; - requestStream << '\n'; - - requestString = requestStream.str(); - - connect(); - } - - void sisnetHandler (const boost::system::error_code& err); - void sisnetResponseHandler (const boost::system::error_code& err); - void sisnetStartHandler (const boost::system::error_code& err); - void requestResponseHandler (const boost::system::error_code& err) override; - - ~SisnetStream(){}; + SisnetStream(const string& path) : TcpSocket(path, "\n") + { + std::stringstream requestStream; + requestStream << "AUTH,"; + requestStream << url.user; + requestStream << ','; + requestStream << url.pass; + requestStream << '\n'; + + requestString = requestStream.str(); + + connect(); + } + + void sisnetHandler(const boost::system::error_code& err); + void sisnetResponseHandler(const boost::system::error_code& err); + void sisnetStartHandler(const boost::system::error_code& err); + void requestResponseHandler(const boost::system::error_code& err) override; + + ~SisnetStream() {}; }; diff --git a/src/cpp/slr/slr.hpp b/src/cpp/slr/slr.hpp index 75cf570be..f179447a6 100644 --- a/src/cpp/slr/slr.hpp +++ b/src/cpp/slr/slr.hpp @@ -1,15 +1,13 @@ - #pragma once +#include #include #include -#include - -#include "enums.h" +#include "common/enums.h" +using std::map; using std::shared_ptr; using std::string; -using std::map; struct VectorPos; struct TropStates; @@ -20,57 +18,48 @@ struct ObsList; struct KFState; struct LObs; - struct SphericalCom { - GTime endTime; - GTime startTime; - double comValue = 0; + GTime endTime; + GTime startTime; + double comValue = 0; }; -struct SphericalComMap : map>>> // index by sat, then by rec +struct SphericalComMap + : map>>> // index by sat, then by rec { - }; -void readCom( - string filepath); +void readCom(string filepath); -bool isSpherical( - string satName); +bool isSpherical(string satName); -double satComOffSphere( - LObs& obs); +double satComOffSphere(LObs& obs); -VectorEcef satComOffGnss( - LObs& obs); +VectorEcef satComOffGnss(LObs& obs); double laserTropDelay( - LObs& obs, - VectorPos& pos, - AzEl& azel, - TropStates& tropStates, - TropMapping& dTropDx, - double& var); + LObs& obs, + VectorPos& pos, + AzEl& azel, + TropStates& tropStates, + TropMapping& dTropDx, + double& var +); -void updateSlrRecBiases( - LObs& obs); +void updateSlrRecBiases(LObs& obs); extern map>> slrSiteObsMap; -void readCrd( - string filepath); +void readCrd(string filepath); -void outputSortedSlrObsPerRec( - string filepath, - ObsList& slrObsList); +void outputSortedSlrObsPerRec(string filepath, ObsList& slrObsList); map> outputSortedSlrObs(); -int readSlrObs( - std::istream& inputStream, - ObsList& slrObsList); +int readSlrObs(std::istream& inputStream, ObsList& slrObsList); -extern map> slrObsFiles; -extern SphericalComMap sphericalComMap; -extern map cdpIdMap; +extern map> slrObsFiles; +extern SphericalComMap sphericalComMap; +extern map cdpIdMap; diff --git a/src/cpp/slr/slrCom.cpp b/src/cpp/slr/slrCom.cpp index 9d58a056c..eebaddb13 100644 --- a/src/cpp/slr/slrCom.cpp +++ b/src/cpp/slr/slrCom.cpp @@ -1,166 +1,178 @@ -#include -#include -#include -#include -#include - #include #include - -#include "coordinates.hpp" -#include "navigation.hpp" -#include "planets.hpp" -#include "common.hpp" -#include "tides.hpp" -#include "sinex.hpp" -#include "enums.h" -#include "slr.hpp" +#include +#include +#include +#include +#include +#include "common/common.hpp" +#include "common/enums.h" +#include "common/navigation.hpp" +#include "common/sinex.hpp" +#include "common/tides.hpp" +#include "orbprop/coordinates.hpp" +#include "orbprop/planets.hpp" +#include "slr/slr.hpp" using namespace boost::algorithm; constexpr int MIN_LINE_LEN = 64; -SphericalComMap sphericalComMap; - +SphericalComMap sphericalComMap; /** Read SLR spherical satellite centre-of-mass (CoM) file -*/ -void readCom( - string filepath) ///< Path to CoM file + */ +void readCom(string filepath) ///< Path to CoM file { - std::ifstream comFile(filepath); - if (!comFile) - { - BOOST_LOG_TRIVIAL(error) - << "Error: could not read in CoM file " << filepath; - return; - } - - // Extract name between last '_' and following '.' - // e.g. filepath = "path/to/com_lageos.txt" - vector usFields; - vector dotFields; - boost::split(usFields, filepath, boost::is_any_of("_")); - boost::split(dotFields, usFields.back(), boost::is_any_of(".")); - string satName = to_lower_copy(dotFields.front()); - - string line; - while (std::getline(comFile, line)) - { - if (line.size() < MIN_LINE_LEN) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: line size is too short: " << line.size(); - continue; - } - - // Get all the values from the file - GEpoch startEp = {std::stod(line.substr(11,4)), std::stod(line.substr(8, 2)), std::stod(line.substr(5,2)), 0, 0, 0}; - GEpoch endEp = {std::stod(line.substr(22,4)), std::stod(line.substr(19,2)), std::stod(line.substr(16,2)), 0, 0, 0}; - string recId = line.substr(0, 4); - double comValue = std::stoi(line.substr(60, 4)) * 1e-3; // mm->m - - // Convert to gps time - GTime start = startEp; - GTime end = endEp; - - // Initialise the centre of mass struct - SphericalCom newCom; - newCom.comValue = comValue; - newCom.startTime = start; - newCom.endTime = end; - sphericalComMap[satName][recId][start] = newCom; - } + std::ifstream comFile(filepath); + if (!comFile) + { + BOOST_LOG_TRIVIAL(error) << "Could not read in CoM file " << filepath; + return; + } + + // Extract name between last '_' and following '.' + // e.g. filepath = "path/to/com_lageos.txt" + vector usFields; + vector dotFields; + boost::split(usFields, filepath, boost::is_any_of("_")); + boost::split(dotFields, usFields.back(), boost::is_any_of(".")); + string satName = to_lower_copy(dotFields.front()); + + string line; + while (std::getline(comFile, line)) + { + if (line.size() < MIN_LINE_LEN) + { + BOOST_LOG_TRIVIAL(warning) << "Line size is too short: " << line.size(); + continue; + } + + // Get all the values from the file + GEpoch startEp = { + std::stod(line.substr(11, 4)), + std::stod(line.substr(8, 2)), + std::stod(line.substr(5, 2)), + 0, + 0, + 0 + }; + GEpoch endEp = { + std::stod(line.substr(22, 4)), + std::stod(line.substr(19, 2)), + std::stod(line.substr(16, 2)), + 0, + 0, + 0 + }; + string recId = line.substr(0, 4); + double comValue = std::stoi(line.substr(60, 4)) * 1e-3; // mm->m + + // Convert to gps time + GTime start = startEp; + GTime end = endEp; + + // Initialise the centre of mass struct + SphericalCom newCom; + newCom.comValue = comValue; + newCom.startTime = start; + newCom.endTime = end; + sphericalComMap[satName][recId][start] = newCom; + } } /** Strip numbers from string -*/ -string stripNumbers( - string str) ///< String to strip + */ +string stripNumbers(string str) ///< String to strip { - str.erase(std::remove_if(str.begin(), str.end(), [](unsigned char x) { return std::isdigit(x) || std::isspace(x); }), str.end()); - - return str; + str.erase( + std::remove_if( + str.begin(), + str.end(), + [](unsigned char x) { return std::isdigit(x) || std::isspace(x); } + ), + str.end() + ); + + return str; } /** Check if sat is spherical (i.e. listed in spherical CoM map) -*/ -bool isSpherical( - string satName) + */ +bool isSpherical(string satName) { - auto satFind = sphericalComMap.find(stripNumbers(satName)); // sphericalComMap indexed by sat ID - if (satFind == sphericalComMap.end()) - { - return false; - } - - return true; + auto satFind = + sphericalComMap.find(stripNumbers(satName)); // sphericalComMap indexed by sat ID + if (satFind == sphericalComMap.end()) + { + return false; + } + + return true; } /** Center of mass to laser retroreflector array offset for spherical satellites -*/ -double satComOffSphere( - LObs& obs) ///< SLR observation + */ +double satComOffSphere(LObs& obs) ///< SLR observation { - string satName = stripNumbers(obs.satName); - string recId = std::to_string(obs.recCdpId); - - auto it = sphericalComMap[satName][recId].lower_bound(obs.time); - if (it != sphericalComMap[satName][recId].end()) - { - auto& [dummy, comEntry] = *it; - if (comEntry.endTime > obs.time) - { - return comEntry.comValue; - } - } - - if (acsConfig.require_reflector_com) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Observation rejected due to lack of laser retroreflector COM correction"; - - obs.excludeCom = true; - } - else - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Laser retroreflector COM correction not found"; - } - - return 0; + string satName = stripNumbers(obs.satName); + string recId = std::to_string(obs.recCdpId); + + auto it = sphericalComMap[satName][recId].lower_bound(obs.time); + if (it != sphericalComMap[satName][recId].end()) + { + auto& [dummy, comEntry] = *it; + if (comEntry.endTime > obs.time) + { + return comEntry.comValue; + } + } + + if (acsConfig.require_reflector_com) + { + BOOST_LOG_TRIVIAL(warning) + << "Observation rejected due to lack of laser retroreflector COM correction"; + + obs.excludeCom = true; + } + else + { + BOOST_LOG_TRIVIAL(warning) << "Laser retroreflector COM correction not found"; + } + + return 0; } /** Center of mass to laser retroreflector array offset for GNSS satellites -*/ -VectorEcef satComOffGnss( - LObs& obs) ///< SLR observation + */ +VectorEcef satComOffGnss(LObs& obs) ///< SLR observation { - GTime time = obs.time; + GTime time = obs.time; - SinexSatSnx satSnx; - auto result = getSatSnx(obs.Sat, time, satSnx); - if (!result.failure - &&obs.satNav_ptr != nullptr) - { - auto& satNav = *obs.satNav_ptr; + SinexSatSnx satSnx; + auto result = getSatSnx(obs.Sat, time, satSnx); + if (!result.failure && obs.satNav_ptr != nullptr) + { + auto& satNav = *obs.satNav_ptr; - Vector3d ecc = satSnx.ecc_ptrs[E_EccType::L_LRA]->ecc - - satSnx.com; + Vector3d ecc = satSnx.ecc_ptrs[E_EccType::L_LRA]->ecc - satSnx.com; - VectorEcef comOffset = body2ecef(satNav.attStatus, ecc); + VectorEcef comOffset = body2ecef(satNav.attStatus, ecc); - return comOffset; - } + return comOffset; + } - if (acsConfig.require_reflector_com) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Observation rejected due to lack of laser retroreflector COM correction"; + if (acsConfig.require_reflector_com) + { + BOOST_LOG_TRIVIAL(warning) + << "Observation rejected due to lack of laser retroreflector COM correction"; - obs.excludeCom = true; - } - else - { - BOOST_LOG_TRIVIAL(warning) << "Warning: Laser retroreflector COM correction not found"; - } + obs.excludeCom = true; + } + else + { + BOOST_LOG_TRIVIAL(warning) << "Laser retroreflector COM correction not found"; + } - return VectorEcef(); + return VectorEcef(); } diff --git a/src/cpp/slr/slrObs.cpp b/src/cpp/slr/slrObs.cpp index 24bccdd1e..d45d76f83 100644 --- a/src/cpp/slr/slrObs.cpp +++ b/src/cpp/slr/slrObs.cpp @@ -1,480 +1,633 @@ - // #pragma GCC optimize ("O0") +#include #include #include -#include -#include #include +#include #include #include - -#include "observations.hpp" -#include "acsConfig.hpp" -#include "constants.hpp" -#include "common.hpp" -#include "sinex.hpp" -#include "slr.hpp" +#include "common/acsConfig.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/observations.hpp" +#include "common/sinex.hpp" +#include "slr/slr.hpp" using std::ifstream; using std::ofstream; using std::string; - using namespace boost::algorithm; -constexpr int MIN_LINE_LEN_SATID = 106; -constexpr int MIN_LINE_LEN_SLROBS = 162; +constexpr int MIN_LINE_LEN_SATID = 106; +constexpr int MIN_LINE_LEN_SLROBS = 162; -map cdpIdMap; +map cdpIdMap; /** Read sat ID file -*/ -void readSatId( - string filepath) ///< Filepath to sat ID file + */ +void readSatId(string filepath) ///< Filepath to sat ID file { - std::ifstream satIdFile(filepath); - if (!satIdFile) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: could not read in satellite ID file " << filepath; - return; - } - - string line; - while (std::getline(satIdFile, line)) - { - if ( line.size() < MIN_LINE_LEN_SATID - ||line.at(0) == '#') - { - continue; - } - - // Getting all the values from the file - SatIdentity newSat; - string satName = line.substr( 1, 24); boost::algorithm::trim(satName); - 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.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)); - newSat.inclination = std::stod(line.substr(70, 18)); - string trackStatus = line.substr(88, 18); boost::algorithm::trim(trackStatus); - newSat.tracking = (trackStatus == "On" ? true : false); - - satIdMap[newSat.ilrsId] = newSat; - } + std::ifstream satIdFile(filepath); + if (!satIdFile) + { + BOOST_LOG_TRIVIAL(warning) << "Could not read in satellite ID file " << filepath; + return; + } + + string line; + while (std::getline(satIdFile, line)) + { + if (line.size() < MIN_LINE_LEN_SATID || line.at(0) == '#') + { + continue; + } + + // Getting all the values from the file + SatIdentity newSat; + string satName = line.substr(1, 24); + boost::algorithm::trim(satName); + 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.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)); + newSat.inclination = std::stod(line.substr(70, 18)); + string trackStatus = line.substr(88, 18); + boost::algorithm::trim(trackStatus); + newSat.tracking = (trackStatus == "On" ? true : false); + + satIdMap[newSat.ilrsId] = newSat; + } } /** Parses a CRD file */ -vector readCrdFile( - string filepath) ///< Filepath to CRD file +vector readCrdFile(string filepath) ///< Filepath to CRD file { - vector crdSessions; // One CrdSession per session (pass); May be multiple passes per file - - ifstream fileStream(filepath); - if (!fileStream) - { - BOOST_LOG_TRIVIAL(error) - << "Error opening crd file " << filepath << "\n"; - - return crdSessions; - } - - CrdSession crdSession; - - while (fileStream) - { - string line; - - getline(fileStream, line); - - char* str = &line[0]; - - string recordType = str; - recordType = recordType.substr(0,2); - - for (auto& c: recordType) - c = toupper((unsigned char)c); - - if (recordType == "H1") { CrdH1 h1; read_h1(str, &h1); crdSession.h1 = h1; crdSession.readH1 = true; } - else if (recordType == "H2") { CrdH2 h2; read_h2(str, &h2); crdSession.h2 = h2; crdSession.readH2 = true; } - else if (recordType == "H3") { CrdH3 h3; read_h3(str, &h3); crdSession.h3 = h3; crdSession.readH3 = true; } - else if (recordType == "H4") { CrdH4 h4; read_h4(str, &h4); crdSession.h4 = h4; crdSession.readH4 = true; } - else if (recordType == "H5") { CrdH5 h5; read_h5(str, &h5); crdSession.h5 = h5; crdSession.readH5 = true; } - else if (recordType == "C0") { CrdC0 c0; read_c0(str, &c0); crdSession.c0 = c0; crdSession.readC0 = true; } - else if (recordType == "C1") { CrdC1 c1; read_c1(str, &c1); crdSession.c1 = c1; crdSession.readC1 = true; } - else if (recordType == "C2") { CrdC2 c2; read_c2(str, &c2); crdSession.c2 = c2; crdSession.readC2 = true; } - else if (recordType == "C3") { CrdC3 c3; read_c3(str, &c3); crdSession.c3 = c3; crdSession.readC3 = true; } - else if (recordType == "C4") { CrdC4 c4; read_c4(str, &c4); crdSession.c4 = c4; crdSession.readC4 = true; } - else if (recordType == "C5") { CrdC5 c5; read_c5(str, &c5); crdSession.c5 = c5; crdSession.readC5 = true; } - else if (recordType == "C6") { CrdC6 c6; read_c6(str, &c6); crdSession.c6 = c6; crdSession.readC6 = true; } - else if (recordType == "C7") { CrdC7 c7; read_c7(str, &c7); crdSession.c7 = c7; crdSession.readC7 = true; } - else if (recordType == "10") { CrdD10 d10; read_10(str, &d10); crdSession.d10. push_back(d10); } - else if (recordType == "11") { CrdD11 d11; read_11(str, &d11); crdSession.d11. push_back(d11); } - else if (recordType == "12") { CrdD12 d12; read_12(str, &d12); crdSession.d12. push_back(d12); } - else if (recordType == "20") { CrdD20 d20; read_20(str, &d20); crdSession.d20. push_back(d20); } - else if (recordType == "21") { CrdD21 d21; read_21(str, &d21); crdSession.d21. push_back(d21); } - else if (recordType == "30") { CrdD30 d30; read_30(str, &d30); crdSession.d30. push_back(d30); } - else if (recordType == "40") { CrdD40 d40; read_40(str, &d40); crdSession.d40. push_back(d40); } - else if (recordType == "41") { CrdD40 d41; read_41(str, &d41); crdSession.d41. push_back(d41); } - else if (recordType == "42") { CrdD42 d42; read_42(str, &d42); crdSession.d42. push_back(d42); } - else if (recordType == "50") { CrdD50 d50; read_50(str, &d50); crdSession.d50. push_back(d50); } - else if (recordType == "60") { CrdD60 d60; read_60(str, &d60); crdSession.d60. push_back(d60); } - else if (recordType == "00") { CrdD00 d00; read_00(str, &d00); crdSession.d00. push_back(d00); } - else if (recordType == "H8") { crdSessions.push_back(crdSession); crdSession = {}; } // End of session - else if (recordType == "H9") { } // End of file (ignore) - else if (recordType[0]=='9' ) { } // User-defined (ignore) - else BOOST_LOG_TRIVIAL(warning) << "Warning: Unrecognised CRD recordType in file " << filepath << " - '" << recordType << "'"; - } - - for (auto it = crdSessions.begin(); it != crdSessions.end(); ) - { - auto session = *it; - if ( session.readH2 == false - ||session.readH3 == false - ||session.readH4 == false - ||session.readC0 == false - ||session.d20.empty() - ||session.d11.empty()) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: CRD file " << filepath << " has a session with insufficient data; ignoring data"; - it = crdSessions.erase(it); - } - else - ++it; - } - - return crdSessions; + vector + crdSessions; // One CrdSession per session (pass); May be multiple passes per file + + ifstream fileStream(filepath); + if (!fileStream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening crd file " << filepath << "\n"; + + return crdSessions; + } + + CrdSession crdSession; + + while (fileStream) + { + string line; + + getline(fileStream, line); + + char* str = &line[0]; + + string recordType = str; + recordType = recordType.substr(0, 2); + + for (auto& c : recordType) + c = toupper((unsigned char)c); + + if (recordType == "H1") + { + CrdH1 h1; + read_h1(str, &h1); + crdSession.h1 = h1; + crdSession.readH1 = true; + } + else if (recordType == "H2") + { + CrdH2 h2; + read_h2(str, &h2); + crdSession.h2 = h2; + crdSession.readH2 = true; + } + else if (recordType == "H3") + { + CrdH3 h3; + read_h3(str, &h3); + crdSession.h3 = h3; + crdSession.readH3 = true; + } + else if (recordType == "H4") + { + CrdH4 h4; + read_h4(str, &h4); + crdSession.h4 = h4; + crdSession.readH4 = true; + } + else if (recordType == "H5") + { + CrdH5 h5; + read_h5(str, &h5); + crdSession.h5 = h5; + crdSession.readH5 = true; + } + else if (recordType == "C0") + { + CrdC0 c0; + read_c0(str, &c0); + crdSession.c0 = c0; + crdSession.readC0 = true; + } + else if (recordType == "C1") + { + CrdC1 c1; + read_c1(str, &c1); + crdSession.c1 = c1; + crdSession.readC1 = true; + } + else if (recordType == "C2") + { + CrdC2 c2; + read_c2(str, &c2); + crdSession.c2 = c2; + crdSession.readC2 = true; + } + else if (recordType == "C3") + { + CrdC3 c3; + read_c3(str, &c3); + crdSession.c3 = c3; + crdSession.readC3 = true; + } + else if (recordType == "C4") + { + CrdC4 c4; + read_c4(str, &c4); + crdSession.c4 = c4; + crdSession.readC4 = true; + } + else if (recordType == "C5") + { + CrdC5 c5; + read_c5(str, &c5); + crdSession.c5 = c5; + crdSession.readC5 = true; + } + else if (recordType == "C6") + { + CrdC6 c6; + read_c6(str, &c6); + crdSession.c6 = c6; + crdSession.readC6 = true; + } + else if (recordType == "C7") + { + CrdC7 c7; + read_c7(str, &c7); + crdSession.c7 = c7; + crdSession.readC7 = true; + } + else if (recordType == "10") + { + CrdD10 d10; + read_10(str, &d10); + crdSession.d10.push_back(d10); + } + else if (recordType == "11") + { + CrdD11 d11; + read_11(str, &d11); + crdSession.d11.push_back(d11); + } + else if (recordType == "12") + { + CrdD12 d12; + read_12(str, &d12); + crdSession.d12.push_back(d12); + } + else if (recordType == "20") + { + CrdD20 d20; + read_20(str, &d20); + crdSession.d20.push_back(d20); + } + else if (recordType == "21") + { + CrdD21 d21; + read_21(str, &d21); + crdSession.d21.push_back(d21); + } + else if (recordType == "30") + { + CrdD30 d30; + read_30(str, &d30); + crdSession.d30.push_back(d30); + } + else if (recordType == "40") + { + CrdD40 d40; + read_40(str, &d40); + crdSession.d40.push_back(d40); + } + else if (recordType == "41") + { + CrdD40 d41; + read_41(str, &d41); + crdSession.d41.push_back(d41); + } + else if (recordType == "42") + { + CrdD42 d42; + read_42(str, &d42); + crdSession.d42.push_back(d42); + } + else if (recordType == "50") + { + CrdD50 d50; + read_50(str, &d50); + crdSession.d50.push_back(d50); + } + else if (recordType == "60") + { + CrdD60 d60; + read_60(str, &d60); + crdSession.d60.push_back(d60); + } + else if (recordType == "00") + { + CrdD00 d00; + read_00(str, &d00); + crdSession.d00.push_back(d00); + } + else if (recordType == "H8") + { + crdSessions.push_back(crdSession); + crdSession = {}; + } // End of session + else if (recordType == "H9") + { + } // End of file (ignore) + else if (recordType[0] == '9') + { + } // User-defined (ignore) + else + BOOST_LOG_TRIVIAL(warning) << "Unrecognised CRD recordType in file " << filepath + << " - '" << recordType << "'"; + } + + for (auto it = crdSessions.begin(); it != crdSessions.end();) + { + auto session = *it; + if (session.readH2 == false || session.readH3 == false || session.readH4 == false || + session.readC0 == false || session.d20.empty() || session.d11.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "CRD file " << filepath + << " has a session with insufficient data; ignoring data"; + it = crdSessions.erase(it); + } + else + ++it; + } + + return crdSessions; } /** Converts from ILRS ID (condensed version of COSPAR ID) to COSPAR ID COSPAR ID to ILRS Satellite Identification Algorithm: COSPAR ID Format: (YYYY-XXXA) - YYYY is the four-digit year of when the launch vehicle was put in orbit - XXX is the sequential launch vehicle number for that year - A is the alpha numeric sequence number within a launch + YYYY is the four-digit year of when the launch vehicle was put in orbit + XXX is the sequential launch vehicle number for that year + A is the alpha numeric sequence number within a launch ILRS Satellite Identification Format: (YYXXXAA), based on the COSPAR ID - YY is the two-digit year of when the launch vehicle was put in orbit - XXX is the sequential launch vehicle number for that year - AA is the numeric sequence number within a launch + YY is the two-digit year of when the launch vehicle was put in orbit + XXX is the sequential launch vehicle number for that year + AA is the numeric sequence number within a launch */ -string ilrsIdToCosparId( - int ilrsId) ///< Filepath to +string ilrsIdToCosparId(int ilrsId) ///< Filepath to { - string YYXXXAA = std::to_string(ilrsId); - int YY = stoi(YYXXXAA.substr(0,2)); - int XXX = stoi(YYXXXAA.substr(2,3)); - int AA = stoi(YYXXXAA.substr(5,2)); - double year = YY; - nearestYear(year); - int YYYY = (int)year; - - if (AA > 26) - BOOST_LOG_TRIVIAL(error) << "Error converting IlrsId to CosparId - AA = " << AA; - - char A = 'A' + AA - 1; - string XXXStr = std::to_string(XXX); - string XXXStrZeroPad = string(3 - XXXStr.length(), '0') + XXXStr; - string cosparId = std::to_string(YYYY) + '-' + XXXStrZeroPad + A; - return cosparId; + string YYXXXAA = std::to_string(ilrsId); + int YY = stoi(YYXXXAA.substr(0, 2)); + int XXX = stoi(YYXXXAA.substr(2, 3)); + int AA = stoi(YYXXXAA.substr(5, 2)); + double year = YY; + nearestYear(year); + int YYYY = (int)year; + + if (AA > 26) + BOOST_LOG_TRIVIAL(error) << "Error converting IlrsId to CosparId - AA = " << AA; + + char A = 'A' + AA - 1; + string XXXStr = std::to_string(XXX); + string XXXStrZeroPad = string(3 - XXXStr.length(), '0') + XXXStr; + string cosparId = std::to_string(YYYY) + '-' + XXXStrZeroPad + A; + return cosparId; } /** Converts from COSPAR ID to ILRS ID (condensed version of COSPAR ID) */ -int cosparIdToIlrsId( - string cosparId) ///< COSPAR ID to convert +int cosparIdToIlrsId(string cosparId) ///< COSPAR ID to convert { - assert(cosparId.size() == 9); - string YY = cosparId.substr(2,2); - string XXX = cosparId.substr(5,3); - char A = cosparId.back(); - int AA = 'A' - A + 1; - string AAStr = std::to_string(AA); - string AAStrZeroPad = string(2 - AAStr.length(), '0') + AAStr; - string ilrsId = YY + XXX + AAStrZeroPad; - return stoi(ilrsId); + assert(cosparId.size() == 9); + string YY = cosparId.substr(2, 2); + string XXX = cosparId.substr(5, 3); + char A = cosparId.back(); + int AA = 'A' - A + 1; + string AAStr = std::to_string(AA); + string AAStrZeroPad = string(2 - AAStr.length(), '0') + AAStr; + string ilrsId = YY + XXX + AAStrZeroPad; + return stoi(ilrsId); } -/** Convert seconds-of-day of an event and the start of the session encompassing the event to a GTime - * Assumes session duration is <24hrs +/** Convert seconds-of-day of an event and the start of the session encompassing the event to a + * GTime Assumes session duration is <24hrs */ GTime sessionSod2Time( - double eventSod, ///< Seconds-of-day of the event in UTC time - GTime startSession) ///< session start time + double eventSod, ///< Seconds-of-day of the event in UTC time + GTime startSession ///< session start time +) { - UYds startYds = startSession; + UYds startYds = startSession; - double delta = eventSod - startYds.sod; + double delta = eventSod - startYds.sod; - if (delta > +secondsInDay / 2) delta -= secondsInDay; - if (delta < -secondsInDay / 2) delta += secondsInDay; + if (delta > +secondsInDay / 2) + delta -= secondsInDay; + if (delta < -secondsInDay / 2) + delta += secondsInDay; - GTime recordTime = startSession + delta; + GTime recordTime = startSession + delta; - return recordTime; + return recordTime; } /** Converts from ILRS ID to SatSys object -*/ -SatSys ilrsIdToSatSys( - int ilrsId) ///< ILRS ID + */ +SatSys ilrsIdToSatSys(int ilrsId) ///< ILRS ID { - SatSys Sat; - auto it = satIdMap.find(ilrsId); - if (it == satIdMap.end()) - { - BOOST_LOG_TRIVIAL(error) << "Error - SatId not found in " << __FUNCTION__ << ": " << ilrsId; - return Sat; - } + SatSys Sat; + auto it = satIdMap.find(ilrsId); + if (it == satIdMap.end()) + { + BOOST_LOG_TRIVIAL(error) << "SatId not found in " << __FUNCTION__ << ": " << ilrsId; + return Sat; + } - auto& [dummy, satData] = *it; + auto& [dummy, satData] = *it; - string satId = satData.satId; + string satId = satData.satId; - Sat = SatSys(satData.satId.c_str()); + Sat = SatSys(satData.satId.c_str()); - return Sat; + return Sat; } /** Extracts observations from a given CRD (.npt) file */ -void readCrd( - string filepath) ///< CRD file to read +void readCrd(string filepath) ///< CRD file to read { - // Read fields from CRD file - vector crdSessions = readCrdFile(filepath); - - // Extract relevant CRD data - for (auto& crdSession : crdSessions) - { - LObs obsHeader; - obsHeader.recName = to_upper_copy((string) crdSession.h2.stn_name); - obsHeader.recCdpId = crdSession.h2.cdp_pad_id; - - obsHeader.satName = to_lower_copy((string) crdSession.h3.target_name); - obsHeader.ilrsId = crdSession.h3.ilrs_id; - obsHeader.cosparId = ilrsIdToCosparId (obsHeader.ilrsId); - obsHeader.Sat = ilrsIdToSatSys (obsHeader.ilrsId); - obsHeader.wavelengthNm = crdSession.c0.xmit_wavelength; - - if ( crdSession.h4.refraction_app_ind != false - ||crdSession.h4.CofM_app_ind != false -// ||crdSession.h4.xcv_amp_app_ind != false - ||crdSession.h4.stn_sysdelay_app_ind != true - ||crdSession.h4.SC_sysdelay_app_ind != false - ||crdSession.h4.range_type_ind != E_SlrRangeType::TWO_WAY) - { - BOOST_LOG_TRIVIAL(warning) << "Warning: unexpected H4 flags!"; - - obsHeader.excludeBadFlags = true; - } - - if (crdSession.h4.data_qual_alert_ind != 0) - obsHeader.excludeAlert = true; - - double startEp[6] = - { - crdSession.h4.start_year, - crdSession.h4.start_mon, - crdSession.h4.start_day, - crdSession.h4.start_hour, - crdSession.h4.start_min, - crdSession.h4.start_sec - }; - - double endEp[6] = - { - crdSession.h4.end_year, - crdSession.h4.end_mon, - crdSession.h4.end_day, - crdSession.h4.end_hour, - crdSession.h4.end_min, - crdSession.h4.end_sec - }; - - GTime startSession = epoch2time(startEp, E_TimeSys::UTC); - GTime endSession = epoch2time(endEp, E_TimeSys::UTC); - - double sessionLength = (endSession - startSession).to_double(); - if (sessionLength > S_IN_DAY/2) - BOOST_LOG_TRIVIAL(error) << "Error: CRD session spans more than 12hrs"; - - for (auto& record : crdSession.d11) - { - LObs obs = obsHeader; // copy over header info - GTime obsTime = sessionSod2Time(record.sec_of_day, startSession); - - // Find closest weather data entry - double minDeltaSec = S_IN_DAY; - for (auto& weather : crdSession.d20) - { - GTime weatherTime = sessionSod2Time(weather.sec_of_day, startSession); - - double deltaSec = fabs((obsTime - weatherTime).to_double()); - if (deltaSec < minDeltaSec) - { - minDeltaSec = deltaSec; - obs.pressure = weather.pressure; - obs.temperature = weather.temperature; - obs.humidity = weather.humidity / 100; - } - } - - // Calculate tx & bounce times - obs.epochEvent = E_CrdEpochEvent::_from_integral(record.epoch_event); - switch(obs.epochEvent) - { - case E_CrdEpochEvent::REC_TX: - obs.timeTx.bigTime = obsTime.bigTime; - obs.twoWayTimeOfFlight = record.time_of_flight; - break; - case E_CrdEpochEvent::REC_RX: - obs.timeTx.bigTime = obsTime.bigTime - record.time_of_flight; - obs.twoWayTimeOfFlight = record.time_of_flight; - break; - case E_CrdEpochEvent::SAT_BN: - obs.timeTx.bigTime = obsTime.bigTime - record.time_of_flight / 2; - obs.twoWayTimeOfFlight = record.time_of_flight; - case E_CrdEpochEvent::SAT_RX: - case E_CrdEpochEvent::SAT_TX: - case E_CrdEpochEvent::REC_TX_SAT_RX: - case E_CrdEpochEvent::SAT_TX_REC_RX: - default: - BOOST_LOG_TRIVIAL(warning) - << "Warning: Unexpected epoch event found: " << obs.epochEvent << ", discarding"; - continue; - } - - slrSiteObsMap[obs.recName][obs.timeTx] = (shared_ptr)obs; - } - } + // Read fields from CRD file + vector crdSessions = readCrdFile(filepath); + + // Extract relevant CRD data + for (auto& crdSession : crdSessions) + { + LObs obsHeader; + obsHeader.recName = to_upper_copy((string)crdSession.h2.stn_name); + obsHeader.recCdpId = crdSession.h2.cdp_pad_id; + + obsHeader.satName = to_lower_copy((string)crdSession.h3.target_name); + obsHeader.ilrsId = crdSession.h3.ilrs_id; + obsHeader.cosparId = ilrsIdToCosparId(obsHeader.ilrsId); + obsHeader.Sat = ilrsIdToSatSys(obsHeader.ilrsId); + obsHeader.wavelengthNm = crdSession.c0.xmit_wavelength; + + if (crdSession.h4.refraction_app_ind != false || + crdSession.h4.CofM_app_ind != false + // ||crdSession.h4.xcv_amp_app_ind != false + || crdSession.h4.stn_sysdelay_app_ind != true || + crdSession.h4.SC_sysdelay_app_ind != false || + int_to_enum(crdSession.h4.range_type_ind) != E_SlrRangeType::TWO_WAY) + { + BOOST_LOG_TRIVIAL(warning) << "Unexpected H4 flags!"; + + obsHeader.excludeBadFlags = true; + } + + if (crdSession.h4.data_qual_alert_ind != 0) + obsHeader.excludeAlert = true; + + double startEp[6] = { + crdSession.h4.start_year, + crdSession.h4.start_mon, + crdSession.h4.start_day, + crdSession.h4.start_hour, + crdSession.h4.start_min, + crdSession.h4.start_sec + }; + + double endEp[6] = { + crdSession.h4.end_year, + crdSession.h4.end_mon, + crdSession.h4.end_day, + crdSession.h4.end_hour, + crdSession.h4.end_min, + crdSession.h4.end_sec + }; + + GTime startSession = epoch2time(startEp, E_TimeSys::UTC); + GTime endSession = epoch2time(endEp, E_TimeSys::UTC); + + double sessionLength = (endSession - startSession).to_double(); + if (sessionLength > S_IN_DAY / 2) + BOOST_LOG_TRIVIAL(error) << "CRD session spans more than 12hrs"; + + for (auto& record : crdSession.d11) + { + LObs obs = obsHeader; // copy over header info + GTime obsTime = sessionSod2Time(record.sec_of_day, startSession); + + // Find closest weather data entry + double minDeltaSec = S_IN_DAY; + for (auto& weather : crdSession.d20) + { + GTime weatherTime = sessionSod2Time(weather.sec_of_day, startSession); + + double deltaSec = fabs((obsTime - weatherTime).to_double()); + if (deltaSec < minDeltaSec) + { + minDeltaSec = deltaSec; + obs.pressure = weather.pressure; + obs.temperature = weather.temperature; + obs.humidity = weather.humidity / 100; + } + } + + // Calculate tx & bounce times + obs.epochEvent = int_to_enum(record.epoch_event); + switch (obs.epochEvent) + { + case E_CrdEpochEvent::REC_TX: + obs.timeTx.bigTime = obsTime.bigTime; + obs.twoWayTimeOfFlight = record.time_of_flight; + break; + case E_CrdEpochEvent::REC_RX: + obs.timeTx.bigTime = obsTime.bigTime - record.time_of_flight; + obs.twoWayTimeOfFlight = record.time_of_flight; + break; + case E_CrdEpochEvent::SAT_BN: + obs.timeTx.bigTime = obsTime.bigTime - record.time_of_flight / 2; + obs.twoWayTimeOfFlight = record.time_of_flight; + case E_CrdEpochEvent::SAT_RX: + case E_CrdEpochEvent::SAT_TX: + case E_CrdEpochEvent::REC_TX_SAT_RX: + case E_CrdEpochEvent::SAT_TX_REC_RX: + default: + BOOST_LOG_TRIVIAL(warning) + << "Unexpected epoch event found: " << obs.epochEvent << ", discarding"; + continue; + } + + slrSiteObsMap[obs.recName][obs.timeTx] = (shared_ptr)obs; + } + } } /** Outputs a tabular file with SLR observations in time-order, for 1 receiver -*/ + */ void outputSortedSlrObsPerRec( - string filepath, ///< slr_obs file to write - map>& obsMap) ///< Output observation list + string filepath, ///< slr_obs file to write + map>& obsMap ///< Output observation list +) { - ofstream filestream(filepath); - if (!filestream) - return; - - tracepdeex(0, filestream, "%21s %-7s %7s %27s %-11s %8s %4s %18s %13s %12s %9s %12s\n", - "DateTime", - "Site_ID", - "CDP_ID", - "BigTime", - "Sat_Name", - "ILRS_ID", - "PRN", - "2-Way_Measurement", - "Pressure", - "Temperature", - "Humidity", - "Wave_Length"); - tracepdeex(0, filestream, "%21s %-7s %7s %27s %-11s %8s %4s %18s %13s %12s %9s %12s\n", - " ", - " ", - " ", - "[s (GPS)]", - " ", - " ", - " ", - "[s]", - "[hPa (mbar)]", - "[K]", - "[%]", - "[nm]"); - - for (auto& [time, slrObs_ptr] : obsMap) - { - auto& obs = *slrObs_ptr; - - tracepdeex(0, filestream, "%21s %-7s %7d %27.12lf %-11s %8d %4s %18.12f %13.2f %12.2f %9.1f %12.3f\n", - obs.timeTx.to_string(1), - obs.recName, - obs.recCdpId, - obs.timeTx.bigTime, - obs.satName, - obs.ilrsId, - obs.Sat.id().c_str(), - obs.twoWayTimeOfFlight, - obs.pressure, - obs.temperature, - obs.humidity * 100, - obs.wavelengthNm); - } + ofstream filestream(filepath); + if (!filestream) + return; + + tracepdeex( + 0, + filestream, + "%21s %-7s %7s %27s %-11s %8s %4s %18s %13s %12s %9s %12s\n", + "DateTime", + "Site_ID", + "CDP_ID", + "BigTime", + "Sat_Name", + "ILRS_ID", + "PRN", + "2-Way_Measurement", + "Pressure", + "Temperature", + "Humidity", + "Wave_Length" + ); + tracepdeex( + 0, + filestream, + "%21s %-7s %7s %27s %-11s %8s %4s %18s %13s %12s %9s %12s\n", + " ", + " ", + " ", + "[s (GPS)]", + " ", + " ", + " ", + "[s]", + "[hPa (mbar)]", + "[K]", + "[%]", + "[nm]" + ); + + for (auto& [time, slrObs_ptr] : obsMap) + { + auto& obs = *slrObs_ptr; + + tracepdeex( + 0, + filestream, + "%21s %-7s %7d %27.12lf %-11s %8d %4s %18.12f %13.2f %12.2f %9.1f %12.3f\n", + obs.timeTx.to_string(1), + obs.recName, + obs.recCdpId, + obs.timeTx.bigTime, + obs.satName, + obs.ilrsId, + obs.Sat.id().c_str(), + obs.twoWayTimeOfFlight, + obs.pressure, + obs.temperature, + obs.humidity * 100, + obs.wavelengthNm + ); + } } /** Outputs a tabular file with SLR observations in time-order -*/ + */ map> outputSortedSlrObs() { - map> slrObsFiles; + map> slrObsFiles; - for (auto& [recId, slrObsList] : slrSiteObsMap) - { - string filename = acsConfig.slr_obs_filename; + for (auto& [recId, slrObsList] : slrSiteObsMap) + { + string filename = acsConfig.slr_obs_filename; - replaceString(filename, "", recId); + replaceString(filename, "", recId); - outputSortedSlrObsPerRec(filename, slrObsList); + outputSortedSlrObsPerRec(filename, slrObsList); - slrObsFiles[recId].push_back(filename); - } + slrObsFiles[recId].push_back(filename); + } - return slrObsFiles; + return slrObsFiles; } /** Read one obs record from tabular slr obs file -*/ + */ int readSlrObs( - std::istream& inputStream, ///< File input stream - ObsList& obsList) ///< List of SLR observations + std::istream& inputStream, ///< File input stream + ObsList& obsList ///< List of SLR observations +) { - string line; - - // read slr obs file header if at beginning of file - if (inputStream.tellg() == 0) - { - std::getline(inputStream, line); - std::getline(inputStream, line); - } - - std::getline(inputStream, line); - - if (line.empty()) - return 0; - - if (line.size() < MIN_LINE_LEN_SLROBS) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: incorrect SLR obs file format, line skipped"; - return 0; - } - - LObs obs; - obs.recName = line.substr(23, 4); - obs.recCdpId = stoi( line.substr(31, 7)); - obs.timeTx.bigTime = stod( line.substr(39, 27)); - obs.satName = line.substr(68, 11); - obs.ilrsId = stoi( line.substr(80, 8)); - obs.twoWayTimeOfFlight = stod( line.substr(94, 18)); - obs.pressure = stod( line.substr(113, 13)); - obs.temperature = stod( line.substr(127, 12)); - obs.humidity = stod( line.substr(140, 9)) / 100; - obs.wavelengthNm = stod( line.substr(150, 12)); - - obs.cosparId = ilrsIdToCosparId (obs.ilrsId); - obs.Sat = ilrsIdToSatSys (obs.ilrsId); - obs.time = obs.timeTx + obs.twoWayTimeOfFlight / 2; - - cdpIdMap[obs.recName] = obs.recCdpId; - - obsList.push_back((shared_ptr)obs); - - return 1; + string line; + + // read slr obs file header if at beginning of file + if (inputStream.tellg() == 0) + { + std::getline(inputStream, line); + std::getline(inputStream, line); + } + + std::getline(inputStream, line); + + if (line.empty()) + return 0; + + if (line.size() < MIN_LINE_LEN_SLROBS) + { + BOOST_LOG_TRIVIAL(warning) << "Incorrect SLR obs file format, line skipped"; + return 0; + } + + LObs obs; + obs.recName = line.substr(23, 4); + obs.recCdpId = stoi(line.substr(31, 7)); + obs.timeTx.bigTime = stod(line.substr(39, 27)); + obs.satName = line.substr(68, 11); + obs.ilrsId = stoi(line.substr(80, 8)); + obs.twoWayTimeOfFlight = stod(line.substr(94, 18)); + obs.pressure = stod(line.substr(113, 13)); + obs.temperature = stod(line.substr(127, 12)); + obs.humidity = stod(line.substr(140, 9)) / 100; + obs.wavelengthNm = stod(line.substr(150, 12)); + + obs.cosparId = ilrsIdToCosparId(obs.ilrsId); + obs.Sat = ilrsIdToSatSys(obs.ilrsId); + obs.time = obs.timeTx + obs.twoWayTimeOfFlight / 2; + + cdpIdMap[obs.recName] = obs.recCdpId; + + obsList.push_back((shared_ptr)obs); + + return 1; } diff --git a/src/cpp/slr/slrRec.cpp b/src/cpp/slr/slrRec.cpp index 74be4af10..c18ec4a15 100644 --- a/src/cpp/slr/slrRec.cpp +++ b/src/cpp/slr/slrRec.cpp @@ -1,84 +1,99 @@ - // #pragma GCC optimize ("O0") -#include "observations.hpp" -#include "coordinates.hpp" -#include "tropModels.hpp" -#include "constants.hpp" -#include "iers2010.hpp" -#include "receiver.hpp" -#include "common.hpp" -#include "sinex.hpp" -#include "slr.hpp" +#include "3rdparty/iers2010/iers2010.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/sinex.hpp" +#include "orbprop/coordinates.hpp" +#include "slr/slr.hpp" +#include "trop/tropModels.hpp" -map> slrObsFiles; -map>> slrSiteObsMap; +map> slrObsFiles; +map>> slrSiteObsMap; /** Calculates Water Vapour pressure (hPa)(mbar) using Buck's equation * Ref: https://en.wikipedia.org/wiki/Arden_Buck_equation */ double getWaterVapPressure( - double temperature, ///< Air temperature (K) - double humidity) ///< Percentage humidity (as decimal) + double temperature, ///< Air temperature (K) + double humidity ///< Percentage humidity (as decimal) +) { - assert(humidity <= 1); + assert(humidity <= 1); - double temperatureC = temperature - 273.15; // K to C - double saturationVaporPressureHpa = 6.1121 * exp((18.678 - temperatureC / 234.5) * (temperatureC / (257.14 + temperatureC))); //hPa + double temperatureC = temperature - 273.15; // K to C + double saturationVaporPressureHpa = + 6.1121 * + exp((18.678 - temperatureC / 234.5) * (temperatureC / (257.14 + temperatureC))); // hPa - return saturationVaporPressureHpa * humidity; + return saturationVaporPressureHpa * humidity; } /** Calculate trop delay on SLR observation -*/ + */ double laserTropDelay( - LObs& obs, ///< SLR observation - VectorPos& pos, ///< Receiver position - AzEl& azel, ///< Azimuth/Elevation of sat (azimuth for future use) - TropStates& tropStates, ///< (gradients for future use) - TropMapping& dTropDx, ///< (gradients for future use) - double& var) + LObs& obs, ///< SLR observation + VectorPos& pos, ///< Receiver position + AzEl& azel, ///< Azimuth/Elevation of sat (azimuth for future use) + TropStates& tropStates, ///< (gradients for future use) + TropMapping& dTropDx, ///< (gradients for future use) + double& var +) { - double temperature = obs.temperature; - double humidity = obs.humidity - obs.humidityBias; - double pressure = obs.pressure - obs.pressureBias; - double wavelengthUm = obs.wavelengthNm * 1e-3; + double temperature = obs.temperature; + double humidity = obs.humidity - obs.humidityBias; + double pressure = obs.pressure - obs.pressureBias; + double wavelengthUm = obs.wavelengthNm * 1e-3; - double waterVapourPressure = getWaterVapPressure(temperature, humidity); + double waterVapourPressure = getWaterVapPressure(temperature, humidity); - double ztd; - double zhd; - double zwd; - iers2010::fcul_zd_hpa (pos.latDeg(), pos.hgt(), pressure, waterVapourPressure, wavelengthUm, ztd, zhd, zwd); - double mf = iers2010::fcul_a (pos.latDeg(), pos.hgt(), temperature, azel.el * R2D); + double ztd; + double zhd; + double zwd; + iers2010::fcul_zd_hpa( + pos.latDeg(), + pos.hgt(), + pressure, + waterVapourPressure, + wavelengthUm, + ztd, + zhd, + zwd + ); + double mf = iers2010::fcul_a(pos.latDeg(), pos.hgt(), temperature, azel.el * R2D); - tropStates.zenith = ztd; - dTropDx.dryMap = mf; - dTropDx.wetMap = mf; + tropStates.zenith = ztd; + dTropDx.dryMap = mf; + dTropDx.wetMap = mf; - return ztd * mf; + return ztd * mf; } /** Update a-priori biases -* Don't corrected for raw observations directly in LObs struct -*/ -void updateSlrRecBiases( - LObs& obs) + * Don't corrected for raw observations directly in LObs struct + */ +void updateSlrRecBiases(LObs& obs) { - map biasMap; // Indexed by "M" models codes - Ref: https://ilrs.gsfc.nasa.gov/docs/2024/ILRS_Data_Handling_File_2024.02.13.snx + map + biasMap; // Indexed by "M" models codes - Ref: + // https://ilrs.gsfc.nasa.gov/docs/2024/ILRS_Data_Handling_File_2024.02.13.snx - getSlrRecBias(std::to_string(obs.recCdpId), obs.Sat, obs.time, biasMap); // Always do this to allow checking excluded data + getSlrRecBias( + std::to_string(obs.recCdpId), + obs.Sat, + obs.time, + biasMap + ); // Always do this to allow checking excluded data - obs.rangeBias = biasMap['R'] + biasMap['E']; - obs.timeBias = biasMap['T'] + biasMap['U']; - obs.humidityBias = biasMap['H']; - obs.pressureBias = biasMap['P']; + obs.rangeBias = biasMap['R'] + biasMap['E']; + obs.timeBias = biasMap['T'] + biasMap['U']; + obs.humidityBias = biasMap['H']; + obs.pressureBias = biasMap['P']; - if ( biasMap['X'] - ||biasMap['N'] - ||biasMap['Q'] - ||biasMap['V']) - { - obs.excludeDataHandling = true; - } + if (biasMap['X'] || biasMap['N'] || biasMap['Q'] || biasMap['V']) + { + obs.excludeDataHandling = true; + } } diff --git a/src/cpp/slr/slrSat.cpp b/src/cpp/slr/slrSat.cpp deleted file mode 100644 index 3bf931eb0..000000000 --- a/src/cpp/slr/slrSat.cpp +++ /dev/null @@ -1,8 +0,0 @@ - -#include "observations.hpp" -#include "ephPrecise.hpp" -#include "ephemeris.hpp" -#include "testUtils.hpp" -#include "acsConfig.hpp" -#include "common.hpp" -#include "slr.hpp" diff --git a/src/cpp/trop/tropCSSR.cpp b/src/cpp/trop/tropCSSR.cpp index 32fbf0531..708f92552 100644 --- a/src/cpp/trop/tropCSSR.cpp +++ b/src/cpp/trop/tropCSSR.cpp @@ -1,134 +1,144 @@ -#include "tropModels.hpp" -#include "ionoModel.hpp" -#include "acsConfig.hpp" +#include "common/acsConfig.hpp" +#include "iono/ionoModel.hpp" +#include "trop/tropModels.hpp" -#define ERR_SAAS 0.3 ///< saastamoinen model error std (m) +constexpr double ERR_SAAS = 0.3; ///< saastamoinen model error std (m) /** SSR Troposphere model * compute tropospheric delay using compact SSR contents - * Neill Mapping functions ia used for mapping -* return : tropospheric delay (m) -*/ + * Neill Mapping functions is used for mapping + * return : tropospheric delay (m) + */ double tropCSSR( - Trace& trace, - GTime time, - VectorPos& pos, - double el, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var) + Trace& trace, + GTime time, + VectorPos& pos, + double el, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +) { - tropSAAS(trace, time, pos, el, dryZTD, dryMap, wetZTD, wetMap, var); + tropSAAS(trace, time, pos, el, dryZTD, dryMap, wetZTD, wetMap, var); - if (var < 0) - return 0; + if (var < 0) + return 0; - var = -1; - - int regInd = checkSSRRegion(pos); - - if (regInd <= 0) - return 0; + var = -1; - auto& navAtm = nav.ssrAtm.atmosRegionsMap[regInd]; + int regInd = checkSSRRegion(pos); + + if (regInd <= 0) + return 0; - if (navAtm.tropData.empty()) - return 0; + auto& navAtm = nav.ssrAtm.atmosRegionsMap[regInd]; - auto& [tAtm, trpData] = *navAtm.tropData.begin(); + if (navAtm.tropData.empty()) + return 0; - if (fabs((tAtm - time).to_double()) > acsConfig.ssrInOpts.local_trop_valid_time) - return 0; + auto& [tAtm, trpData] = *navAtm.tropData.begin(); - if (var < 0) - return 0; + if (fabs((tAtm - time).to_double()) > acsConfig.ssrInOpts.local_trop_valid_time) + return 0; - var = SQR(trpData.sigma); + if (var < 0) + return 0; - if (trpData.polyDry.size() > 0) - { - dryZTD = 0; - double dLat = pos.latDeg() - navAtm.gridLatDeg[0]; - double dLon = pos.lonDeg() - navAtm.gridLonDeg[0]; + var = SQR(trpData.sigma); - tracepdeex(2, trace,"Trop poly: %.4f %.4f\n", dLat, dLon); + if (trpData.polyDry.size() > 0) + { + dryZTD = 0; + double dLat = pos.latDeg() - navAtm.gridLatDeg[0]; + double dLon = pos.lonDeg() - navAtm.gridLonDeg[0]; - for (auto& [ind, val] : trpData.polyDry) - { - switch (ind) - { - case 0: dryZTD += val; break; - case 1: dryZTD += val * dLat; break; - case 2: dryZTD += val * dLon; break; - case 3: dryZTD += val * dLat * dLon; break; //todo aaron magic numbers - } - tracepdeex(2, trace,", %.4f\n", val); - } - } + tracepdeex(2, trace, "Trop poly: %.4f %.4f\n", dLat, dLon); - map numerators; - int gridIndex = 0; - double denominator = 0; + for (auto& [ind, val] : trpData.polyDry) + { + switch (ind) + { + case 0: + dryZTD += val; + break; + case 1: + dryZTD += val * dLat; + break; + case 2: + dryZTD += val * dLon; + break; + case 3: + dryZTD += val * dLat * dLon; + break; // todo aaron magic numbers + } + tracepdeex(2, trace, ", %.4f\n", val); + } + } - for (auto& [regID, regData] : nav.ssrAtm.atmosRegionsMap) - { - if (regData.intLatDeg <= 0) - continue; + map numerators; + int gridIndex = 0; + double denominator = 0; - if (regData.intLonDeg <= 0) - continue; + for (auto& [regID, regData] : nav.ssrAtm.atmosRegionsMap) + { + if (regData.intLatDeg <= 0) + continue; - if (regData.tropData.empty()) - continue; + if (regData.intLonDeg <= 0) + continue; - auto& [tTrop, tropData] = *regData.tropData.begin(); + if (regData.tropData.empty()) + continue; - if (fabs((tTrop - time).to_double()) > acsConfig.ssrInOpts.local_trop_valid_time) - continue; + auto& [tTrop, tropData] = *regData.tropData.begin(); - double midLonDeg = (regData.minLonDeg + regData.maxLonDeg) / 2; - double recLonDeg = pos.lonDeg(); + if (fabs((tTrop - time).to_double()) > acsConfig.ssrInOpts.local_trop_valid_time) + continue; - if ((recLonDeg - midLonDeg) > +180) recLonDeg -= 360; - else if ((recLonDeg - midLonDeg) < -180) recLonDeg += 360; + double midLonDeg = (regData.minLonDeg + regData.maxLonDeg) / 2; + double recLonDeg = pos.lonDeg(); - for (auto& [rind, grdval] : tropData.gridWet) - { - if (regData.gridLatDeg.find(rind) == regData.gridLatDeg.end()) - continue; + if ((recLonDeg - midLonDeg) > +180) + recLonDeg -= 360; + else if ((recLonDeg - midLonDeg) < -180) + recLonDeg += 360; - double dLat = fabs(pos.latDeg() - regData.gridLatDeg[rind]) / regData.intLatDeg; - if (dLat > 1) - continue; + for (auto& [rind, grdval] : tropData.gridWet) + { + if (regData.gridLatDeg.find(rind) == regData.gridLatDeg.end()) + continue; - double dLon = fabs(recLonDeg - regData.gridLonDeg[rind]) / regData.intLonDeg; - if (dLon > 1) - continue; + double dLat = fabs(pos.latDeg() - regData.gridLatDeg[rind]) / regData.intLatDeg; + if (dLat > 1) + continue; - double coef = (1 - dLat) * (1 - dLon); - denominator += coef; - numerators[gridIndex] += coef * grdval; - gridIndex++; - } - } + double dLon = fabs(recLonDeg - regData.gridLonDeg[rind]) / regData.intLonDeg; + if (dLon > 1) + continue; - if (gridIndex == 0) - { - var = -1; - return 0; - } + double coef = (1 - dLat) * (1 - dLon); + denominator += coef; + numerators[gridIndex] += coef * grdval; + gridIndex++; + } + } - wetZTD = 0; + if (gridIndex == 0) + { + var = -1; + return 0; + } - for (auto& [ind, val] : numerators) - wetZTD += val; + wetZTD = 0; - wetZTD /= denominator; - - double delay = dryMap * dryZTD - + wetMap * wetZTD; - - return delay; + for (auto& [ind, val] : numerators) + wetZTD += val; + + wetZTD /= denominator; + + double delay = dryMap * dryZTD + wetMap * wetZTD; + + return delay; } diff --git a/src/cpp/trop/tropGPT2.cpp b/src/cpp/trop/tropGPT2.cpp index 8d6b947b0..741c6ea8a 100644 --- a/src/cpp/trop/tropGPT2.cpp +++ b/src/cpp/trop/tropGPT2.cpp @@ -1,399 +1,407 @@ - -#include +#include #include +#include #include -#include +#include "trop/tropModels.hpp" -using std::ifstream; using std::array; - - -#include "tropModels.hpp" +using std::ifstream; struct GPTVals { - double pressure = 0; ///< pressure (hPa) - double temperature = 0; ///< temperature (celsius) - double deltaT = 0; ///< delta temp (deg/km) - double waterVp = 0; ///< water vp (hPa) - double hydroCoef = 0; ///< hydrostatic coef at 0m - double wetCoef = 0; ///< wet coef - double undulation = 0; ///< undulation (m) + double pressure = 0; ///< pressure (hPa) + double temperature = 0; ///< temperature (celsius) + double deltaT = 0; ///< delta temp (deg/km) + double waterVp = 0; ///< water vp (hPa) + double hydroCoef = 0; ///< hydrostatic coef at 0m + double wetCoef = 0; ///< wet coef + double undulation = 0; ///< undulation (m) }; /** gpt grid file contents */ struct GptGrid { - double lat = 0; ///< lat grid (degree) - double lon = 0; ///< lon grid (degree) - array pres; ///< pressure a0 A1 B1 A2 B2 (pascal) - array temp; ///< temperature a0 A1 B1 A2 B2 (kelvin) - array humid; ///< humidity a0 A1 B1 A2 B2 (kg/kg) - array tlaps; ///< elapse rate a0 A1 B1 A2 B2 (kelvin/m) - array ah; ///< hydrostatic mapping function coefficient a0 A1 B1 A2 B2 - array aw; ///< wet mapping function coefficient a0 A1 B1 A2 B2 - double undu = 0; ///< geoid undulation (m) - double hgt = 0; ///< orthometric height (m) + double lat = 0; ///< lat grid (degree) + double lon = 0; ///< lon grid (degree) + array pres; ///< pressure a0 A1 B1 A2 B2 + ///< (pascal) + array temp; ///< temperature a0 A1 B1 A2 B2 + ///< (kelvin) + array humid; ///< humidity a0 A1 B1 A2 B2 + ///< (kg/kg) + array + tlaps; ///< elapse rate a0 A1 B1 A2 B2 (kelvin/m) + array ah; ///< hydrostatic mapping function coefficient a0 A1 B1 A2 B2 + array aw; ///< wet mapping function coefficient a0 A1 B1 A2 B2 + double undu = 0; ///< geoid undulation (m) + double hgt = 0; ///< orthometric height (m) }; -vector globalGPT2Grids = {}; ///< gpt grid information -bool globalGPT2GridsReady = false; ///< gpt grid information read - +vector globalGPT2Grids = {}; ///< gpt grid information +bool globalGPT2GridsReady = false; ///< gpt grid information read /* sign function --------------------------------------------------------------- -* args : double x I input number -* -* return : 1, 0, or -1 -* ---------------------------------------------------------------------------*/ + * args : double x I input number + * + * return : 1, 0, or -1 + * ---------------------------------------------------------------------------*/ int sign(double x) { - if (x > 0) return +1; - else if (x == 0) return 0; - else return -1; + if (x > 0) + return +1; + else if (x == 0) + return 0; + else + return -1; } /** vienna mapping function. * coefficients coming from either GPT2 or from vmf file */ void vmf1( - const double ah, ///< vmf1 dry coefficients - const double aw, ///< vmf1 wet coefficients - double mjd, ///< modified julian date - double lat, ///< ellipsoidal lat (rad) - double hgt, ///< height (m) - double elev, ///< zenith distance (rad) - double& dryMap, - double& wetMap) + const double ah, ///< vmf1 dry coefficients + const double aw, ///< vmf1 wet coefficients + double mjd, ///< modified julian date + double lat, ///< ellipsoidal lat (rad) + double hgt, ///< height (m) + double elev, ///< zenith distance (rad) + double& dryMap, + double& wetMap +) { - double doy = mjd - 44239 + 1 - 28; - - /* hydrostatic mapping function */ - double c0h = 0.062; - double c11h; - double c10h; - double phh; - if (lat < 0) { phh = PI; c11h = 0.007; c10h = 0.002; } /* southern hemisphere */ - else { phh = 0; c11h = 0.005; c10h = 0.001; } /* northern hemisphere */ - - double bh = 0.0029; - double ch = c0h + ((cos(doy / 365.25 * 2*PI + phh) + 1) * c11h / 2 + c10h) * (1 - cos(lat)); - dryMap = mapHerring(elev, ah, bh, ch); - - /* height correction */ - double aht = 2.53e-5; - double bht = 5.49e-3; - double cht = 1.14e-3; - dryMap+= (1 / sin(elev) - mapHerring(elev, aht, bht, cht)) * hgt / 1000; - - /* wet mapping function */ - double bw = 0.00146; - double cw = 0.04391; - wetMap = mapHerring(elev, aw, bw, cw); + double doy = mjd - 44239 + 1 - 28; + + /* hydrostatic mapping function */ + double c0h = 0.062; + double c11h; + double c10h; + double phh; + if (lat < 0) + { + phh = PI; + c11h = 0.007; + c10h = 0.002; + } /* southern hemisphere */ + else + { + phh = 0; + c11h = 0.005; + c10h = 0.001; + } /* northern hemisphere */ + + double bh = 0.0029; + double ch = c0h + ((cos(doy / 365.25 * 2 * PI + phh) + 1) * c11h / 2 + c10h) * (1 - cos(lat)); + dryMap = mapHerring(elev, ah, bh, ch); + + /* height correction */ + double aht = 2.53e-5; + double bht = 5.49e-3; + double cht = 1.14e-3; + dryMap += (1 / sin(elev) - mapHerring(elev, aht, bht, cht)) * hgt / 1000; + + /* wet mapping function */ + double bw = 0.00146; + double cw = 0.04391; + wetMap = mapHerring(elev, aw, bw, cw); } /** read GPT grid file */ -void readgrid( - string filepath) ///< vmf1 coefficients filename +void readgrid(string filepath) ///< vmf1 coefficients filename { - int i = 0; - - auto& gptGrids = globalGPT2Grids; - - ifstream fileStream(filepath); - if (!fileStream) - { - BOOST_LOG_TRIVIAL(error) - << "Error opening gpt grid file" << filepath << "\n"; - return; - } - - while (fileStream) - { - string line; - - getline(fileStream, line); - - char* buff = &line[0]; - - /* ignore the first line */ - if (strchr(buff,'%')) - continue; - - int j = 0; - - /* loop over each line */ - char* p = strtok(buff," "); - double val[40]; - while (p) - { - sscanf(p,"%lf", &val[j]); - j++; - p = strtok(nullptr," "); - } - - GptGrid gptGridPoint; - - /* assign lat lon */ - gptGridPoint.lat = val[0]; - gptGridPoint.lon = val[1]; - - /* assign pres, temp, humid, tlaps, ah, aw */ - for (int k = 0; k < 5; k++) - { - gptGridPoint.pres [k] = val[k+2]; /* pressure */ - gptGridPoint.temp [k] = val[k+7]; /* temperature */ - gptGridPoint.humid [k] = val[k+12]/1000; /* humidity */ - gptGridPoint.tlaps [k] = val[k+17]/1000; /* temperature elapse rate */ - gptGridPoint.ah [k] = val[k+24]/1000; /* ah */ - gptGridPoint.aw [k] = val[k+29]/1000; /* aw */ - } - - /* assign undulation and height */ - gptGridPoint.undu = val[22]; - gptGridPoint.hgt = val[23]; - - gptGrids.push_back(gptGridPoint); - } - - globalGPT2GridsReady = true; + int i = 0; + + auto& gptGrids = globalGPT2Grids; + + ifstream fileStream(filepath); + if (!fileStream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening gpt grid file" << filepath << "\n"; + return; + } + + while (fileStream) + { + string line; + + getline(fileStream, line); + + char* buff = &line[0]; + + /* ignore the first line */ + if (strchr(buff, '%')) + continue; + + int j = 0; + + /* loop over each line */ + char* p = strtok(buff, " "); + double val[40]; + while (p) + { + sscanf(p, "%lf", &val[j]); + j++; + p = strtok(nullptr, " "); + } + + GptGrid gptGridPoint; + + /* assign lat lon */ + gptGridPoint.lat = val[0]; + gptGridPoint.lon = val[1]; + + /* assign pres, temp, humid, tlaps, ah, aw */ + for (int k = 0; k < 5; k++) + { + gptGridPoint.pres[k] = val[k + 2]; /* pressure */ + gptGridPoint.temp[k] = val[k + 7]; /* temperature */ + gptGridPoint.humid[k] = val[k + 12] / 1000; /* humidity */ + gptGridPoint.tlaps[k] = val[k + 17] / 1000; /* temperature elapse rate */ + gptGridPoint.ah[k] = val[k + 24] / 1000; /* ah */ + gptGridPoint.aw[k] = val[k + 29] / 1000; /* aw */ + } + + /* assign undulation and height */ + gptGridPoint.undu = val[22]; + gptGridPoint.hgt = val[23]; + + gptGrids.push_back(gptGridPoint); + } + + globalGPT2GridsReady = true; } /** coefficients multiplication */ -double coef( - const array& a, - double cosfy, - double sinfy, - double coshy, - double sinhy) +double coef(const array& a, double cosfy, double sinfy, double coshy, double sinhy) { - return + a[0] - + a[1] * cosfy - + a[2] * sinfy - + a[3] * coshy - + a[4] * sinhy; + return +a[0] + a[1] * cosfy + a[2] * sinfy + a[3] * coshy + a[4] * sinhy; } -double coefr( - double p1, - double p2, - double l1, - double l2, - double a[4]) +double coefr(double p1, double p2, double l1, double l2, double a[4]) { - double r[2]; + double r[2]; - r[0] = p2 * a[0] + p1 * a[1]; - r[1] = p2 * a[2] + p1 * a[3]; + r[0] = p2 * a[0] + p1 * a[1]; + r[1] = p2 * a[2] + p1 * a[3]; - return + l2 * r[0] - + l1 * r[1]; + return +l2 * r[0] + l1 * r[1]; } /** global pressure and temperature */ GPTVals gpt2( - const vector& gptg, ///< gpt grid information - double mjd, ///< modified julian date - double lat, ///< ellipsoidal lat (rad) - double lon, ///< ellipsoidal lon (rad) - double hell) ///< ellipsoidal height (m) + const vector& gptg, ///< gpt grid information + double mjd, ///< modified julian date + double lat, ///< ellipsoidal lat (rad) + double lon, ///< ellipsoidal lon (rad) + double hell ///< ellipsoidal height (m) +) { - /* change reference epoch to 1/1/2000 */ - double mjd1 = mjd - MJD_j2000; + /* change reference epoch to 1/1/2000 */ + double mjd1 = mjd - MJD_j2000; + + /* factors for amplitudes */ + double cosfy = cos(mjd1 / 365.25 * 2 * PI); + double coshy = cos(mjd1 / 365.25 * 4 * PI); + double sinfy = sin(mjd1 / 365.25 * 2 * PI); + double sinhy = sin(mjd1 / 365.25 * 4 * PI); + + /* positive longitude in degrees */ + double plon; + if (lon < 0) + plon = (lon + 2 * PI) * 180 / PI; + else + plon = lon * 180 / PI; + + /* transform to polar distance in degrees */ + double pdist = (-lat + PI / 2) * 180 / PI; - /* factors for amplitudes */ - double cosfy = cos(mjd1 / 365.25 * 2 * PI); - double coshy = cos(mjd1 / 365.25 * 4 * PI); - double sinfy = sin(mjd1 / 365.25 * 2 * PI); - double sinhy = sin(mjd1 / 365.25 * 4 * PI); + /* find the index of the nearest point */ + int ipod = floor((pdist + 5) / 5); + int ilon = floor((plon + 5) / 5); + + /* normalized (to one) differences */ + double dpod = (pdist - (ipod * 5 - 2.5)) / 5; + double dlon = (plon - (ilon * 5 - 2.5)) / 5; + + if (ipod == 37) + ipod = 36; + + int index[4] = {}; + index[0] = (ipod - 1) * 72 + ilon; + + /* near the pole: nearest neighbour interpolation, otherwise, bilinear */ + int bl = 0; + if (pdist > 2.5 && pdist < 177.5) + { + bl = 1; + } + + GPTVals gptVals; + + if (bl == 0) + { + /* near the pole */ + int i = index[0] - 1; + + auto& gptGridPoint = gptg[i]; + + gptVals.undulation = gptGridPoint.undu; + + double hgt = hell - gptVals.undulation; + + /* pressure, temperature at the height of grid */ + double t0 = coef(gptGridPoint.temp, cosfy, sinfy, coshy, sinhy); + double p0 = coef(gptGridPoint.pres, cosfy, sinfy, coshy, sinhy); + double q0 = coef(gptGridPoint.humid, cosfy, sinfy, coshy, sinhy); + double dt0 = coef(gptGridPoint.tlaps, cosfy, sinfy, coshy, sinhy); + + double con = GRAVITY * MOLARDRY / (UGAS * t0 * (1 + 0.6077 * q0)); + + /* pressure in hPa */ + gptVals.pressure = (p0 * exp(-con * (hgt - gptGridPoint.hgt))) / 100; + + /* temperature at station height in celsius */ + gptVals.temperature = t0 + dt0 * (hgt - gptGridPoint.hgt) - ZEROC; + gptVals.deltaT = dt0 * 1000; + + /* water vapour pressure in hPa */ + gptVals.waterVp = (q0 * gptVals.pressure) / (0.622 + 0.378 * q0); + + /* dry and wet coefficients */ + gptVals.hydroCoef = coef(gptGridPoint.ah, cosfy, sinfy, coshy, sinhy); + gptVals.wetCoef = coef(gptGridPoint.aw, cosfy, sinfy, coshy, sinhy); + } + else + { + double t[4]; + double p[4]; + double q[4]; + double dt[4]; + double ah[4]; + double aw[4]; + double undu[4]; + + double ipod1 = ipod + sign(dpod); + double ilon1 = ilon + sign(dlon); + + if (ilon1 == 73) + ilon1 = 1; + if (ilon1 == 0) + ilon1 = 72; + + index[0] = index[0] - 1; /* starting from 0 */ + index[1] = (ipod1 - 1) * 72 + ilon - 1; + index[2] = (ipod - 1) * 72 + ilon1 - 1; + index[3] = (ipod1 - 1) * 72 + ilon1 - 1; + + for (int k = 0; k < 4; k++) + { + auto& gptGridPointK = gptg[index[k]]; + + undu[k] = gptGridPointK.undu; + + double hgt = hell - undu[k]; + + /* pressure, temperature at the height of the grid */ + double t0 = coef(gptGridPointK.temp, cosfy, sinfy, coshy, sinhy); + double p0 = coef(gptGridPointK.pres, cosfy, sinfy, coshy, sinhy); + q[k] = coef(gptGridPointK.humid, cosfy, sinfy, coshy, sinhy); + dt[k] = coef(gptGridPointK.tlaps, cosfy, sinfy, coshy, sinhy); + + t[k] = t0 + dt[k] * (hgt - gptGridPointK.hgt) - ZEROC; + double con = GRAVITY * MOLARDRY / (UGAS * t0 * (1 + 0.6077 * q[k])); - /* positive longitude in degrees */ - double plon; - if (lon < 0) plon = (lon+2*PI) * 180 / PI; - else plon = lon * 180 / PI; + p[k] = (p0 * exp(-con * (hgt - gptGridPointK.hgt))) / 100; - /* transform to polar distance in degrees */ - double pdist = (-lat + PI/2) * 180 / PI; + ah[k] = coef(gptGridPointK.ah, cosfy, sinfy, coshy, sinhy); + aw[k] = coef(gptGridPointK.aw, cosfy, sinfy, coshy, sinhy); + } + double dnpod1 = fabs(dpod); + double dnpod2 = 1 - dnpod1; + double dnlon1 = fabs(dlon); + double dnlon2 = 1 - dnlon1; - /* find the index of the nearest point */ - int ipod = floor((pdist + 5) / 5); - int ilon = floor((plon + 5) / 5); - - /* normalized (to one) differences */ - double dpod = (pdist - (ipod * 5 - 2.5)) / 5; - double dlon = (plon - (ilon * 5 - 2.5)) / 5; - - if (ipod == 37) - ipod = 36; - - int index[4] = {}; - index[0] = (ipod - 1) * 72 + ilon; + gptVals.pressure = coefr(dnpod1, dnpod2, dnlon1, dnlon2, p); + gptVals.temperature = coefr(dnpod1, dnpod2, dnlon1, dnlon2, t); + gptVals.deltaT = coefr(dnpod1, dnpod2, dnlon1, dnlon2, dt) * 1000; - /* near the pole: nearest neighbour interpolation, otherwise, bilinear */ - int bl=0; - if ( pdist > 2.5 - && pdist < 177.5) - { - bl=1; - } - - GPTVals gptVals; - - if (bl == 0) - { - /* near the pole */ - int i = index[0] - 1; - - auto& gptGridPoint = gptg[i]; - - gptVals.undulation = gptGridPoint.undu; - - double hgt = hell - gptVals.undulation; - - /* pressure, temperature at the height of grid */ - double t0 = coef(gptGridPoint.temp, cosfy, sinfy, coshy, sinhy); - double p0 = coef(gptGridPoint.pres, cosfy, sinfy, coshy, sinhy); - double q0 = coef(gptGridPoint.humid, cosfy, sinfy, coshy, sinhy); - double dt0 = coef(gptGridPoint.tlaps, cosfy, sinfy, coshy, sinhy); - - double con = GRAVITY * MOLARDRY / (UGAS * t0 * (1 + 0.6077 * q0)); - - /* pressure in hPa */ - gptVals.pressure = (p0 * exp(-con * (hgt - gptGridPoint.hgt))) / 100; - - /* temperature at station height in celsius */ - gptVals.temperature = t0 + dt0 * (hgt - gptGridPoint.hgt) - ZEROC; - gptVals.deltaT = dt0 * 1000; - - /* water vapour pressure in hPa */ - gptVals.waterVp = (q0 * gptVals.pressure) / (0.622 + 0.378 * q0); - - /* dry and wet coefficients */ - gptVals.hydroCoef = coef(gptGridPoint.ah, cosfy, sinfy, coshy, sinhy); - gptVals.wetCoef = coef(gptGridPoint.aw, cosfy, sinfy, coshy, sinhy); - } - else - { - double t[4]; - double p[4]; - double q[4]; - double dt[4]; - double ah[4]; - double aw[4]; - double undu[4]; - - double ipod1 = ipod + sign(dpod); - double ilon1 = ilon + sign(dlon); - - if (ilon1 == 73) ilon1 = 1; - if (ilon1 == 0) ilon1 = 72; - - index[0] = index[0] - 1; /* starting from 0 */ - index[1] = (ipod1 - 1) * 72 + ilon - 1; - index[2] = (ipod - 1) * 72 + ilon1 - 1; - index[3] = (ipod1 - 1) * 72 + ilon1 - 1; - - for (int k = 0; k < 4; k++) - { - auto& gptGridPointK = gptg[index[k]]; - - undu[k] = gptGridPointK.undu; - - double hgt = hell - undu[k]; + /* humidity */ + double tp = coefr(dnpod1, dnpod2, dnlon1, dnlon2, q); + gptVals.waterVp = tp * gptVals.pressure / (0.622 + 0.378 * tp); - /* pressure, temperature at the height of the grid */ - double t0 = coef(gptGridPointK.temp, cosfy, sinfy, coshy, sinhy); - double p0 = coef(gptGridPointK.pres, cosfy, sinfy, coshy, sinhy); - q[k] = coef(gptGridPointK.humid, cosfy, sinfy, coshy, sinhy); - dt[k] = coef(gptGridPointK.tlaps, cosfy, sinfy, coshy, sinhy); + gptVals.hydroCoef = coefr(dnpod1, dnpod2, dnlon1, dnlon2, ah); + gptVals.wetCoef = coefr(dnpod1, dnpod2, dnlon1, dnlon2, aw); + gptVals.undulation = coefr(dnpod1, dnpod2, dnlon1, dnlon2, undu); + } - t[k] = t0 + dt[k] * (hgt - gptGridPointK.hgt) - ZEROC; - double con = GRAVITY * MOLARDRY / (UGAS * t0 * (1 + 0.6077 * q[k])); - - p[k] = (p0 * exp(-con * (hgt - gptGridPointK.hgt))) / 100; - - ah[k] = coef(gptGridPointK.ah, cosfy, sinfy, coshy, sinhy); - aw[k] = coef(gptGridPointK.aw, cosfy, sinfy, coshy, sinhy); - } - double dnpod1 = fabs(dpod); - double dnpod2 = 1 - dnpod1; - double dnlon1 = fabs(dlon); - double dnlon2 = 1 - dnlon1; - - gptVals.pressure = coefr(dnpod1, dnpod2, dnlon1, dnlon2, p); - gptVals.temperature = coefr(dnpod1, dnpod2, dnlon1, dnlon2, t); - gptVals.deltaT = coefr(dnpod1, dnpod2, dnlon1, dnlon2, dt) * 1000; - - /* humidity */ - double tp = coefr(dnpod1, dnpod2, dnlon1, dnlon2, q); - gptVals.waterVp = tp * gptVals.pressure / (0.622 + 0.378 * tp); - - gptVals.hydroCoef = coefr(dnpod1, dnpod2, dnlon1, dnlon2, ah); - gptVals.wetCoef = coefr(dnpod1, dnpod2, dnlon1, dnlon2, aw); - gptVals.undulation = coefr(dnpod1, dnpod2, dnlon1, dnlon2, undu); - } - - return gptVals; + return gptVals; } /** Troposphere zenith hydrastatic delay and mapping function. - * gpt2 is used to get pressure, temperature, water vapor pressure and mapping function coefficients and then vmf1 is used to derive dry and wet mapping function. + * gpt2 is used to get pressure, temperature, water vapor pressure and mapping function coefficients + * and then vmf1 is used to derive dry and wet mapping function. */ double tropGPT2( - Trace& trace, - GTime time, - VectorPos& pos, - double el, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var) + Trace& trace, + GTime time, + VectorPos& pos, + double el, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +) { - var = -1; - MjDateTT mjd_= time; - double mjd = mjd_.to_double(); + if (el < 0) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": el < 0, setting it to 1e-6"; + el = 1e-6; + } - /* standard atmosphere */ - double lat = pos.lat(); - double lon = pos.lon(); - double hgt = pos.hgt(); + var = -1; + MjDateTT mjd_ = time; + double mjd = mjd_.to_double(); - /* pressure, temperature, water vapor at station height */ - double pres = 1013.25 * pow((1 - 0.0000226 * hgt), 5.225); - double tp = 15 - 6.5E-3 * hgt + ZEROC; - double ew = 0.5 / 100 * exp( - 37.2465 - + 0.213166 * tp - - 0.000256908 * tp * tp); - double gm = 1 - 0.00266 * cos(2 * lat) - 0.00028 * hgt / 1E3; + /* standard atmosphere */ + double lat = pos.lat(); + double lon = pos.lon(); + double hgt = pos.hgt(); - if (globalGPT2GridsReady == false) - { - return 0; - } + if (hgt < -1000 || hgt > 20000) + return 0; - /* use GPT2 model */ + /* pressure, temperature, water vapor at station height */ + double pres = 1013.25 * pow((1 - 0.0000226 * hgt), 5.225); + double tp = 15 - 6.5E-3 * hgt + ZEROC; + double ew = 0.5 / 100 * exp(-37.2465 + 0.213166 * tp - 0.000256908 * tp * tp); + double gm = 1 - 0.00266 * cos(2 * lat) - 0.00028 * hgt / 1E3; - /* get pressure and mapping coefficients from gpts */ - GPTVals gptVals = gpt2(globalGPT2Grids, mjd, lat, lon, hgt); + if (globalGPT2GridsReady == false) + { + return 0; + } - pres = gptVals.pressure; - tp = gptVals.temperature + ZEROC; /* celcius to kelvin */ - ew = gptVals.waterVp; + /* use GPT2 model */ - /* get mapping function */ - vmf1(gptVals.hydroCoef, gptVals.wetCoef, mjd, lat, hgt, el, dryMap, wetMap); + /* get pressure and mapping coefficients from gpts */ + GPTVals gptVals = gpt2(globalGPT2Grids, mjd, lat, lon, hgt); - /* zenith wet delay (m) */ - dryZTD = 0.002277 * pres / gm; - wetZTD = 0.002277 * (1255 / tp + 0.05) * ew * (1 / gm); + pres = gptVals.pressure; + tp = gptVals.temperature + ZEROC; /* celcius to kelvin */ + ew = gptVals.waterVp; - var = 0; - double delay = dryMap * dryZTD - + wetMap * wetZTD; + /* get mapping function */ + vmf1(gptVals.hydroCoef, gptVals.wetCoef, mjd, lat, hgt, el, dryMap, wetMap); - return delay; -} + /* zenith wet delay (m) */ + dryZTD = 0.002277 * pres / gm; + wetZTD = 0.002277 * (1255 / tp + 0.05) * ew * (1 / gm); + var = 0; + double delay = dryMap * dryZTD + wetMap * wetZTD; + + return delay; +} diff --git a/src/cpp/trop/tropModels.cpp b/src/cpp/trop/tropModels.cpp index 9fc18f9b7..e46cb20e6 100644 --- a/src/cpp/trop/tropModels.cpp +++ b/src/cpp/trop/tropModels.cpp @@ -1,250 +1,334 @@ - // #pragma GCC optimize ("O0") +#include "trop/tropModels.hpp" #include - -#include "observations.hpp" -#include "coordinates.hpp" -#include "tropModels.hpp" -#include "acsConfig.hpp" -#include "satStat.hpp" -#include "algebra.hpp" -#include "common.hpp" -#include "trace.hpp" -#include "enums.h" +#include "common/acsConfig.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/enums.h" +#include "common/observations.hpp" +#include "common/satStat.hpp" +#include "common/trace.hpp" +#include "orbprop/coordinates.hpp" struct TropMapBasis { - int regionID; /* Atmospheric region ID */ - E_BasisType type; /* parameter type: 0 polnomial, 1 gridpoint */ - int index; /* parameter index */ + int regionID; /* Atmospheric region ID */ + E_BasisType type; /* parameter type: 0 polnomial, 1 gridpoint */ + int index; /* parameter index */ }; -vector tropBasisVec; +vector tropBasisVec; void defineLocalTropBasis() { - tropBasisVec.clear(); - - for (auto& [iatm, atmReg] : nav.ssrAtm.atmosRegionsMap) - { - for (int i = 0; i < atmReg.tropPolySize; i++) - { - TropMapBasis basis; - basis.regionID = iatm; - basis.type = E_BasisType::POLYNOMIAL; - basis.index = i; - - tropBasisVec.push_back(basis); - } - - if ( atmReg.gridType >= 0 - && atmReg.tropGrid) - for (auto& [igrid, latgrid] : atmReg.gridLatDeg) - { - TropMapBasis basis; - basis.regionID = iatm; - basis.type = E_BasisType::GRIDPOINT; - basis.index = igrid; - - tropBasisVec.push_back(basis); - } - } - - acsConfig.ssrOpts.nbasis = tropBasisVec.size(); + tropBasisVec.clear(); + + for (auto& [iatm, atmReg] : nav.ssrAtm.atmosRegionsMap) + { + for (int i = 0; i < atmReg.tropPolySize; i++) + { + TropMapBasis basis; + basis.regionID = iatm; + basis.type = E_BasisType::POLYNOMIAL; + basis.index = i; + + tropBasisVec.push_back(basis); + } + + if (atmReg.gridType >= 0 && atmReg.tropGrid) + for (auto& [igrid, latgrid] : atmReg.gridLatDeg) + { + TropMapBasis basis; + basis.regionID = iatm; + basis.type = E_BasisType::GRIDPOINT; + basis.index = igrid; + + tropBasisVec.push_back(basis); + } + } + + acsConfig.ssrOpts.nbasis = tropBasisVec.size(); } -double tropModelCoef( - int ind, - VectorPos& pos) +double tropModelCoef(int ind, VectorPos& pos) { - if (ind >= tropBasisVec.size()) return 0; - - auto& basis = tropBasisVec[ind]; - - auto& atmReg = nav.ssrAtm.atmosRegionsMap[basis.regionID]; - - double recLatDeg = pos.latDeg(); - double recLonDeg = pos.lonDeg(); - - if (recLatDeg > atmReg.maxLatDeg) return 0; - if (recLatDeg < atmReg.minLatDeg) return 0; - - double midLonDeg = (atmReg.minLonDeg + atmReg.maxLonDeg) / 2; - if ((recLonDeg - midLonDeg) > 180) recLonDeg -= 360; - else if ((recLonDeg - midLonDeg) < -180) recLonDeg += 360; - - if (recLonDeg > atmReg.maxLonDeg) return 0; - if (recLonDeg < atmReg.minLonDeg) return 0; - - double latdiff = recLatDeg - atmReg.gridLatDeg[0]; - double londiff = recLonDeg - atmReg.gridLonDeg[0]; - - switch (basis.type) - { - case E_BasisType::POLYNOMIAL: - { - switch (basis.index) - { - case E_PolyType::CONSTANT: return 1; - case E_PolyType::LAT: return latdiff; - case E_PolyType::LON: return londiff; - case E_PolyType::LAT_LON: return latdiff * londiff; - case E_PolyType::LAT_SQRD: return latdiff * latdiff; - case E_PolyType::LON_SQRD: return londiff * londiff; - default: return 0; - } - } - case E_BasisType::GRIDPOINT: - { - double dlatDeg = fabs(recLatDeg - atmReg.gridLatDeg[basis.index]); - double dlonDeg = fabs(recLonDeg - atmReg.gridLonDeg[basis.index]); - - if (dlatDeg > atmReg.intLatDeg || atmReg.intLatDeg == 0) return 0; - if (dlonDeg > atmReg.intLonDeg || atmReg.intLonDeg == 0) return 0; - - return (1 - dlatDeg / atmReg.intLatDeg) * (1 - dlonDeg / atmReg.intLonDeg); //todo aaron use bilinear interpolation function? - } - default: - { - return 0; - } - } + if (ind >= tropBasisVec.size()) + return 0; + + auto& basis = tropBasisVec[ind]; + + auto& atmReg = nav.ssrAtm.atmosRegionsMap[basis.regionID]; + + double recLatDeg = pos.latDeg(); + double recLonDeg = pos.lonDeg(); + + if (recLatDeg > atmReg.maxLatDeg) + return 0; + if (recLatDeg < atmReg.minLatDeg) + return 0; + + double midLonDeg = (atmReg.minLonDeg + atmReg.maxLonDeg) / 2; + if ((recLonDeg - midLonDeg) > 180) + recLonDeg -= 360; + else if ((recLonDeg - midLonDeg) < -180) + recLonDeg += 360; + + if (recLonDeg > atmReg.maxLonDeg) + return 0; + if (recLonDeg < atmReg.minLonDeg) + return 0; + + double latdiff = recLatDeg - atmReg.gridLatDeg[0]; + double londiff = recLonDeg - atmReg.gridLonDeg[0]; + + switch (basis.type) + { + case E_BasisType::POLYNOMIAL: + { + switch (int_to_enum(basis.index)) + { + case E_PolyType::CONSTANT: + return 1; + case E_PolyType::LAT: + return latdiff; + case E_PolyType::LON: + return londiff; + case E_PolyType::LAT_LON: + return latdiff * londiff; + case E_PolyType::LAT_SQRD: + return latdiff * latdiff; + case E_PolyType::LON_SQRD: + return londiff * londiff; + default: + return 0; + } + } + case E_BasisType::GRIDPOINT: + { + double dlatDeg = fabs(recLatDeg - atmReg.gridLatDeg[basis.index]); + double dlonDeg = fabs(recLonDeg - atmReg.gridLonDeg[basis.index]); + + if (dlatDeg > atmReg.intLatDeg || atmReg.intLatDeg == 0) + return 0; + if (dlonDeg > atmReg.intLonDeg || atmReg.intLonDeg == 0) + return 0; + + return (1 - dlatDeg / atmReg.intLatDeg) * + (1 - dlonDeg / atmReg.intLonDeg + ); // todo aaron use bilinear interpolation function? + } + default: + { + return 0; + } + } } - double mapHerring(double el, double a, double b, double c) { - double sinel = sin(el); - return (1 + a / - (1 + b / - (1 + c))) / (sinel + (a / - (sinel + b / - (sinel + c)))); + double sinel = sin(el); + return (1 + a / (1 + b / (1 + c))) / (sinel + (a / (sinel + b / (sinel + c)))); } /** Returns gradient mapping function m_az(el) -* Valid for 0 < el < 0.9999 * PI/2; returns 0 otherwise -* Ref: https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/97JB01739 -*/ -double gradMapFn( - double el) ///< Elevation (rad) + * Valid for 0 < el < 0.9999 * PI/2; returns 0 otherwise + * Ref: https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/97JB01739 + */ +double gradMapFn(double el) ///< Elevation (rad) { - if ( el < 0 - ||el > 0.9999 * PI/2) - { - return 0; - } - - double c = 0.0031; - return 1 / (sin(el) * tan(el) + c); -} + if (el < 0 || el > 0.9999 * PI / 2) + { + return 0; + } + double c = 0.0031; + return 1 / (sin(el) * tan(el) + c); +} double tropModel( - Trace& trace, - vector models, - GTime time, - VectorPos& pos, - AzEl& azel, - TropStates& tropStates, - TropMapping& dTropDx, - double& var) + Trace& trace, + vector models, + GTime time, + VectorPos& pos, + AzEl& azel, + TropStates& tropStates, + TropMapping& dTropDx, + double& var +) { - double dryZTD; - double wetZTD; - var = -1; - - for (auto& model : models) - { - switch (model) - { - case E_TropModel::STANDARD: tropSAAS(trace, time, pos, azel.el, dryZTD, dTropDx.dryMap, wetZTD, dTropDx.wetMap, var); break; - case E_TropModel::SBAS: tropSBAS(trace, time, pos, azel.el, dryZTD, dTropDx.dryMap, wetZTD, dTropDx.wetMap, var); break; - case E_TropModel::GPT2: tropGPT2(trace, time, pos, azel.el, dryZTD, dTropDx.dryMap, wetZTD, dTropDx.wetMap, var); break; - case E_TropModel::VMF3: tropVMF3(trace, time, pos, azel.el, dryZTD, dTropDx.dryMap, wetZTD, dTropDx.wetMap, var); break; - case E_TropModel::CSSR: tropCSSR(trace, time, pos, azel.el, dryZTD, dTropDx.dryMap, wetZTD, dTropDx.wetMap, var); break; - default: return 0; - } - - if (var < 0) - continue; - - tracepdeex(2, trace,"\nTroposphere Model %s %s %d %d %d", time.to_string().c_str(), model._to_string(), pos.latDeg(), pos.lonDeg(), pos.hgt()); - - break; - } - - //todo aaron var might still be < 0 if everything in models failed - - if (tropStates.zenith == 0) //initialization - { - tropStates.zenith = dryZTD + wetZTD; - } - else - { - wetZTD = tropStates.zenith - dryZTD; - var = 0; - } - - tracepdeex(4,trace," dry: %.4f x %.4f wet: %.4f x %.4f", dTropDx.dryMap, dryZTD, dTropDx.wetMap, wetZTD); - - double gradMap = gradMapFn(azel.el); - dTropDx.northMap = gradMap * cos(azel.az); - dTropDx.eastMap = gradMap * sin(azel.az); - - return dTropDx.dryMap * dryZTD - + dTropDx.wetMap * wetZTD - + dTropDx.northMap * tropStates.grads[0] - + dTropDx.eastMap * tropStates.grads[1]; + double dryZTD; + double wetZTD; + var = -1; + + for (auto& model : models) + { + switch (model) + { + case E_TropModel::STANDARD: + tropSAAS( + trace, + time, + pos, + azel.el, + dryZTD, + dTropDx.dryMap, + wetZTD, + dTropDx.wetMap, + var + ); + break; + case E_TropModel::SBAS: + tropSBAS( + trace, + time, + pos, + azel.el, + dryZTD, + dTropDx.dryMap, + wetZTD, + dTropDx.wetMap, + var + ); + break; + case E_TropModel::GPT2: + tropGPT2( + trace, + time, + pos, + azel.el, + dryZTD, + dTropDx.dryMap, + wetZTD, + dTropDx.wetMap, + var + ); + break; + case E_TropModel::VMF3: + tropVMF3( + trace, + time, + pos, + azel.el, + dryZTD, + dTropDx.dryMap, + wetZTD, + dTropDx.wetMap, + var + ); + break; + case E_TropModel::CSSR: + tropCSSR( + trace, + time, + pos, + azel.el, + dryZTD, + dTropDx.dryMap, + wetZTD, + dTropDx.wetMap, + var + ); + break; + default: + return 0; + } + + if (var < 0) + continue; + + tracepdeex( + 3, + trace, + "\nTroposphere Model %s %s %d %d %d", + time.to_string().c_str(), + enum_to_string(model), + pos.latDeg(), + pos.lonDeg(), + pos.hgt() + ); + + break; + } + + // todo aaron var might still be < 0 if everything in models failed + + if (tropStates.zenith == 0) // initialization + { + tropStates.zenith = dryZTD + wetZTD; + } + else + { + wetZTD = tropStates.zenith - dryZTD; + var = 0; + } + + tracepdeex( + 4, + trace, + " dry: %.4f x %.4f wet: %.4f x %.4f", + dTropDx.dryMap, + dryZTD, + dTropDx.wetMap, + wetZTD + ); + + double gradMap = gradMapFn(azel.el); + dTropDx.northMap = gradMap * cos(azel.az); + dTropDx.eastMap = gradMap * sin(azel.az); + + return dTropDx.dryMap * dryZTD + dTropDx.wetMap * wetZTD + + dTropDx.northMap * tropStates.grads[0] + dTropDx.eastMap * tropStates.grads[1]; } -double tropDryZTD( - Trace& trace, - vector models, - GTime time, - VectorPos& pos) +double tropDryZTD(Trace& trace, vector models, GTime time, VectorPos& pos) { - double dryZTD; - double dryMap; - double wetZTD; - double wetMap; - double var = -1; - for (auto& model : models) - { - switch (model) - { - case E_TropModel::STANDARD: tropSAAS(trace, time, pos, PI/2, dryZTD, dryMap, wetZTD, wetMap, var); break; - case E_TropModel::SBAS: tropSBAS(trace, time, pos, PI/2, dryZTD, dryMap, wetZTD, wetMap, var); break; - case E_TropModel::GPT2: tropGPT2(trace, time, pos, PI/2, dryZTD, dryMap, wetZTD, wetMap, var); break; - case E_TropModel::VMF3: tropVMF3(trace, time, pos, PI/2, dryZTD, dryMap, wetZTD, wetMap, var); break; - default: return 0; - } - - if (var >= 0) - break; - } - - //todo aaron var might still be < 0 if everything in models failed - return dryZTD; + double dryZTD; + double dryMap; + double wetZTD; + double wetMap; + double var = -1; + for (auto& model : models) + { + switch (model) + { + case E_TropModel::STANDARD: + tropSAAS(trace, time, pos, PI / 2, dryZTD, dryMap, wetZTD, wetMap, var); + break; + case E_TropModel::SBAS: + tropSBAS(trace, time, pos, PI / 2, dryZTD, dryMap, wetZTD, wetMap, var); + break; + case E_TropModel::GPT2: + tropGPT2(trace, time, pos, PI / 2, dryZTD, dryMap, wetZTD, wetMap, var); + break; + case E_TropModel::VMF3: + tropVMF3(trace, time, pos, PI / 2, dryZTD, dryMap, wetZTD, wetMap, var); + break; + default: + return 0; + } + + if (var >= 0) + break; + } + + // todo aaron var might still be < 0 if everything in models failed + return dryZTD; } -double heightAdjustWet( - double hgt) +double heightAdjustWet(double hgt) { - double hgtKm = hgt / 1E3; - double temp = 288.15 - 6.5 * hgtKm; - double eScale = exp(0.9636 * hgtKm / (38.4154 - hgtKm)); + double hgtKm = hgt / 1E3; + double temp = 288.15 - 6.5 * hgtKm; + double eScale = exp(0.9636 * hgtKm / (38.4154 - hgtKm)); - return eScale * (1255 / temp + 0.05) / 4.40537; + return eScale * (1255 / temp + 0.05) / 4.40537; } -double heightAdjustDry( - double hgt, - double lat) +double heightAdjustDry(double hgt, double lat) { - double hgtKm = hgt / 1E3; - double latScale = 1 - 0.00266 * cos(2 * lat); + double hgtKm = hgt / 1E3; + double latScale = 1 - 0.00266 * cos(2 * lat); - return pow((1 - 0.0226 * hgtKm), 5.225) * latScale / (latScale - 0.00028 * hgtKm); + return pow((1 - 0.0226 * hgtKm), 5.225) * latScale / (latScale - 0.00028 * hgtKm); } diff --git a/src/cpp/trop/tropModels.hpp b/src/cpp/trop/tropModels.hpp index 6eeca436e..434c67ff0 100644 --- a/src/cpp/trop/tropModels.hpp +++ b/src/cpp/trop/tropModels.hpp @@ -1,133 +1,119 @@ - - #pragma once -#include "eigenIncluder.hpp" -#include "navigation.hpp" -#include "constants.hpp" -#include "common.hpp" -#include "gTime.hpp" -#include "enums.h" - #include +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/enums.h" +#include "common/gTime.hpp" +#include "common/navigation.hpp" using std::string; -#define ERR_TROP 3.0 ///< tropspheric delay std (m) -#define NGPT 2592 ///< grid number +constexpr double ERR_TROP = 3.0; ///< tropspheric delay std (m) +constexpr int NGPT = 2592; ///< grid number struct TropMapping { - double dryMap = 0; - double wetMap = 0; - double northMap = 0; - double eastMap = 0; + double dryMap = 0; + double wetMap = 0; + double northMap = 0; + double eastMap = 0; }; struct TropStates { - double zenith = 0; - double grads[2] = {}; + double zenith = 0; + double grads[2] = {}; }; -double mapHerring( - double el, - double a, - double b, - double c); +double mapHerring(double el, double a, double b, double c); -void readvmf3( - string filepath); +void readvmf3(string filepath); -void readorog( - string filepath); +void readorog(string filepath); -void readgrid( - string filepath); +void readgrid(string filepath); double tropSAAS( - Trace& trace, - GTime time, - VectorPos& pos, - double el, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var); + Trace& trace, + GTime time, + VectorPos& pos, + double el, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +); double tropSBAS( - Trace& trace, - GTime time, - VectorPos& pos, - double el, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var); + Trace& trace, + GTime time, + VectorPos& pos, + double el, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +); double tropVMF3( - Trace& trace, - GTime time, - VectorPos& pos, - double el, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var); + Trace& trace, + GTime time, + VectorPos& pos, + double el, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +); double tropGPT2( - Trace& trace, - GTime time, - VectorPos& pos, - double el, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var); + Trace& trace, + GTime time, + VectorPos& pos, + double el, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +); double tropCSSR( - Trace& trace, - GTime time, - VectorPos& pos, - double elv, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var); - -double gradMapFn( - double el); + Trace& trace, + GTime time, + VectorPos& pos, + double elv, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +); + +double gradMapFn(double el); double tropModel( - Trace& trace, - vector models, - GTime time, - VectorPos& pos, - AzEl& azel, - TropStates& tropStates, - TropMapping& dTropDx, - double& var); - -double tropDryZTD( - Trace& trace, - vector models, - GTime time, - VectorPos& pos); - -double tropModelCoef( - int ind, - VectorPos& pos); + Trace& trace, + vector models, + GTime time, + VectorPos& pos, + AzEl& azel, + TropStates& tropStates, + TropMapping& dTropDx, + double& var +); + +double tropDryZTD(Trace& trace, vector models, GTime time, VectorPos& pos); + +double tropModelCoef(int ind, VectorPos& pos); void defineLocalTropBasis(); -double heightAdjustWet( - double hgt); +double heightAdjustWet(double hgt); -double heightAdjustDry( - double hgt, - double lat); +double heightAdjustDry(double hgt, double lat); diff --git a/src/cpp/trop/tropSAAS.cpp b/src/cpp/trop/tropSAAS.cpp index f6f68a645..0e6839ed2 100644 --- a/src/cpp/trop/tropSAAS.cpp +++ b/src/cpp/trop/tropSAAS.cpp @@ -1,92 +1,100 @@ -#include "tropModels.hpp" - -#define ERR_SAAS 0.3 ///< saastamoinen model error std (m) - -const double coefNMF[][5] = -{ - { 1.2769934E-3, 1.2683230E-3, 1.2465397E-3, 1.2196049E-3, 1.2045996E-3}, - { 2.9153695E-3, 2.9152299E-3, 2.9288445E-3, 2.9022565E-3, 2.9024912E-3}, - { 62.610505E-3, 62.837393E-3, 63.721774E-3, 63.824265E-3, 64.258455E-3}, - - { 0.0000000E-0, 1.2709626E-5, 2.6523662E-5, 3.4000452E-5, 4.1202191E-5}, - { 0.0000000E-0, 2.1414979E-5, 3.0160779E-5, 7.2562722E-5, 11.723375E-5}, - { 0.0000000E-0, 9.0128400E-5, 4.3497037E-5, 84.795348E-5, 170.37206E-5}, - - { 5.8021897E-4, 5.6794847E-4, 5.8118019E-4, 5.9727542E-4, 6.1641693E-4}, - { 1.4275268E-3, 1.5138625E-3, 1.4572752E-3, 1.5007428E-3, 1.7599082E-3}, - { 4.3472961E-2, 4.6729510E-2, 4.3908931E-2, 4.4626982E-2, 5.4736038E-2} -}; - -double interpc(const double coef[], double lat) -{ - int i = (int)(lat / 15); - - if (i < 1) return coef[0]; - else if (i > 4) return coef[4]; - - return coef[i - 1] * (1 - lat / 15 + i) + coef[i] * (lat / 15 - i); -} - -/* troposphere model ----------------------------------------------------------- -* compute tropospheric delay by standard atmosphere (relative humidity of 0.7 -* 15 degrees of temperature at sea level) and saastamoinen model -* Neill Mapping functions ia used for mapping -* args : gtime_t time I time -* double *pos I receiver position {lat,lon,h} (rad,m) -* return : tropospheric delay (m) -*-----------------------------------------------------------------------------*/ -double tropSAAS( - Trace& trace, - GTime time, - VectorPos& pos, - double el, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var) -{ - double lat = pos.latDeg(); - double hgt = pos.hgt(); - - if (hgt < -100 - || hgt > +20000 - || el < 0) - { - dryMap = 0; - wetMap = 0; - dryMap = 0; - wetMap = 0; - var = SQR(ERR_TROP); - return 0; - } - - UYds yds = time; - /* year from doy 28, added half a year for southern latitudes */ - double y = (yds.doy - 28) / 365.25 + (lat < 0 ? 0.5 : 0); - double cosy = cos(2 * PI * y); - lat = fabs(lat); - - double ah[3]; - double aw[3]; - for (int i = 0; i < 3; i++) - { /* year average + seasonal variation */ - ah[i] = interpc(coefNMF[i ], lat) - interpc(coefNMF[i + 3], lat) * cosy; - aw[i] = interpc(coefNMF[i + 6], lat); - } - /* height correction */ - double dm = (1 / sin(el) - mapHerring(el, 2.53E-5, 5.49E-3, 1.14E-3)) * hgt / 1E3; - dryMap = mapHerring(el, ah[0], ah[1], ah[2]) + dm; - wetMap = mapHerring(el, aw[0], aw[1], aw[2]); - - double temp = 15 - 6.5E-3 * hgt + ZEROC; - double pres = 1013.25 * pow(288.15 / temp, -5.255877); - double e = 6.108 * 0.7 * exp((17.15 * temp - 4684) / (temp - 38.45)); - - dryZTD = 0.0022768 * pres / (1 - 0.00266 * cos(2 * pos.lat()) - 0.00028 * hgt / 1E3); - wetZTD = 0.002277 * (1255 / temp + 0.05) * e; - - var = SQR(ERR_SAAS / (sin(el) + 0.1)); - - return dryMap * dryZTD + wetMap * wetZTD; -} +#include "tropModels.hpp" + +constexpr double ERR_SAAS = 0.3; ///< saastamoinen model error std (m) + +const double coefNMF[][5] = { + {1.2769934E-3, 1.2683230E-3, 1.2465397E-3, 1.2196049E-3, 1.2045996E-3}, + {2.9153695E-3, 2.9152299E-3, 2.9288445E-3, 2.9022565E-3, 2.9024912E-3}, + {62.610505E-3, 62.837393E-3, 63.721774E-3, 63.824265E-3, 64.258455E-3}, + + {0.0000000E-0, 1.2709626E-5, 2.6523662E-5, 3.4000452E-5, 4.1202191E-5}, + {0.0000000E-0, 2.1414979E-5, 3.0160779E-5, 7.2562722E-5, 11.723375E-5}, + {0.0000000E-0, 9.0128400E-5, 4.3497037E-5, 84.795348E-5, 170.37206E-5}, + + {5.8021897E-4, 5.6794847E-4, 5.8118019E-4, 5.9727542E-4, 6.1641693E-4}, + {1.4275268E-3, 1.5138625E-3, 1.4572752E-3, 1.5007428E-3, 1.7599082E-3}, + {4.3472961E-2, 4.6729510E-2, 4.3908931E-2, 4.4626982E-2, 5.4736038E-2} +}; + +double interpc(const double coef[], double lat) +{ + int i = (int)(lat / 15); + + if (i < 1) + return coef[0]; + else if (i > 4) + return coef[4]; + + return coef[i - 1] * (1 - lat / 15 + i) + coef[i] * (lat / 15 - i); +} + +/** + * compute tropospheric delay by standard atmosphere (relative humidity of 0.7 + * 15 degrees of temperature at sea level) and saastamoinen model + * Neill Mapping functions is used for mapping + * @note validity range of geodetic height is -1000 to 11000m, and of elevation is 0 to 90 degrees + * @todo model can be extended to 51km using + * https://www.eoas.ubc.ca/books/Practical_Meteorology/prmet102/Ch01-atmos-v102b.pdf (eq 1.16/1.17) + */ +double tropSAAS( + Trace& trace, + GTime time, + VectorPos& pos, + double el, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +) +{ + double lat = pos.latDeg(); + double hgt = pos.hgt(); + + if (hgt < -1000) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": hgt < -1000m, setting it to -1000m"; + hgt = -1000; + } + + if (hgt > +11000) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": hgt > 11000m, setting it to 11000m"; + hgt = 11000; + } + + if (el < 0) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": el < 0, setting it to 1e-6"; + el = 1e-6; + } + + UYds yds = time; + /* year from doy 28, added half a year for southern latitudes */ + double yearFraction = (yds.doy - 28) / 365.25 + (lat < 0 ? 0.5 : 0); + double cosy = cos(2 * PI * yearFraction); + lat = fabs(lat); + + double ah[3]; + double aw[3]; + for (int i = 0; i < 3; i++) + { /* year average + seasonal variation */ + ah[i] = interpc(coefNMF[i], lat) - interpc(coefNMF[i + 3], lat) * cosy; + aw[i] = interpc(coefNMF[i + 6], lat); + } + /* height correction */ + double dm = (1 / sin(el) - mapHerring(el, 2.53E-5, 5.49E-3, 1.14E-3)) * hgt / 1E3; + dryMap = mapHerring(el, ah[0], ah[1], ah[2]) + dm; + wetMap = mapHerring(el, aw[0], aw[1], aw[2]); + + double temp = 15 - 6.5E-3 * hgt + ZEROC; + double pres = 1013.25 * pow(288.15 / temp, -5.255877); + double e = 6.108 * 0.7 * exp((17.15 * temp - 4684) / (temp - 38.45)); + + dryZTD = 0.0022768 * pres / (1 - 0.00266 * cos(2 * pos.lat()) - 0.00028 * hgt / 1E3); + wetZTD = 0.002277 * (1255 / temp + 0.05) * e; + + var = SQR(ERR_SAAS / (sin(el) + 0.1)); + + return dryMap * dryZTD + wetMap * wetZTD; +} diff --git a/src/cpp/trop/tropSBAS.cpp b/src/cpp/trop/tropSBAS.cpp index 1ff1fe139..b94aa6185 100644 --- a/src/cpp/trop/tropSBAS.cpp +++ b/src/cpp/trop/tropSBAS.cpp @@ -1,87 +1,89 @@ -#include "tropModels.hpp" - -/** get meterological parameters -*/ -void getmet( - double lat, - double* met) -{ - const double metprm[][10] = /* lat=15,30,45,60,75 */ - { - {1013.25, 299.65, 26.31, 6.30E-3, 2.77, 0.00, 0.00, 0.00, 0.00E-3, 0.00}, - {1017.25, 294.15, 21.79, 6.05E-3, 3.15, -3.75, 7.00, 8.85, 0.25E-3, 0.33}, - {1015.75, 283.15, 11.66, 5.58E-3, 2.57, -2.25, 11.00, 7.24, 0.32E-3, 0.46}, - {1011.75, 272.15, 6.78, 5.39E-3, 1.81, -1.75, 15.00, 5.36, 0.81E-3, 0.74}, - {1013.00, 263.65, 4.11, 4.53E-3, 1.55, -0.50, 14.50, 3.39, 0.62E-3, 0.30} - }; - - lat = fabs(lat); - - if (lat <= 15) for (int i = 0; i < 10; i++) met[i] = metprm[0][i]; - else if (lat >= 75) for (int i = 0; i < 10; i++) met[i] = metprm[4][i]; - else - { - int j = (int)(lat / 15); - double a = (lat - j * 15) / 15.0; - - for (int i = 0; i < 10; i++) - { - met[i] = (1 - a) * metprm[j - 1][i] + a * metprm[j][i]; - } - } -} - -/* tropospheric delay correction ----------------------------------------------- -* compute sbas tropospheric delay correction (mops model) -*-----------------------------------------------------------------------------*/ -double tropSBAS( - Trace& trace, - GTime time, - VectorPos& pos, - double elev, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var) -{ - const double k1 = 77.604; - const double k2 = 382000; - const double rd = 287.054; - const double gm = 9.784; - const double g = 9.80665; - - if ( pos.hgt() < -100 - ||pos.hgt() > +10000 - ||elev <= 0) - { - dryMap = 0; - wetMap = 0; - dryMap = 0; - wetMap = 0; - var = 0; - return 0; - } - - double met[10]; - getmet(pos.latDeg(), met); - - UYds yds = time; - double c = cos(2 * PI * (yds.doy - (pos.lat() >= 0 ? 28 : 211)) / 365.25); - for (int i = 0; i < 5; i++) - { - met[i] -= met[i + 5] * c; - } - dryZTD = 1E-6 * k1 * rd * met[0] / gm; - wetZTD = 1E-6 * k2 * rd / (gm * (met[4] + 1) - met[3] * rd) * met[2] / met[1]; - - double h = pos.hgt(); - dryZTD *= pow(1 - met[3] * h / met[1], g / (rd * met[3])); - wetZTD *= pow(1 - met[3] * h / met[1], (met[4] + 1) * g / (rd * met[3]) - 1); - - double sinel = sin(elev); - dryMap = 1.001 / sqrt(0.002001 + sinel * sinel); - dryMap = wetMap; - var = SQR(0.12 * dryMap); - return (dryZTD + wetZTD) * dryMap; -} +#include "tropModels.hpp" + +/** get meterological parameters + */ +void getmet(double lat, double* met) +{ + const double metprm[][10] = /* lat=15,30,45,60,75 */ + {{1013.25, 299.65, 26.31, 6.30E-3, 2.77, 0.00, 0.00, 0.00, 0.00E-3, 0.00}, + {1017.25, 294.15, 21.79, 6.05E-3, 3.15, -3.75, 7.00, 8.85, 0.25E-3, 0.33}, + {1015.75, 283.15, 11.66, 5.58E-3, 2.57, -2.25, 11.00, 7.24, 0.32E-3, 0.46}, + {1011.75, 272.15, 6.78, 5.39E-3, 1.81, -1.75, 15.00, 5.36, 0.81E-3, 0.74}, + {1013.00, 263.65, 4.11, 4.53E-3, 1.55, -0.50, 14.50, 3.39, 0.62E-3, 0.30}}; + + lat = fabs(lat); + + if (lat <= 15) + for (int i = 0; i < 10; i++) + met[i] = metprm[0][i]; + else if (lat >= 75) + for (int i = 0; i < 10; i++) + met[i] = metprm[4][i]; + else + { + int j = (int)(lat / 15); + double a = (lat - j * 15) / 15.0; + + for (int i = 0; i < 10; i++) + { + met[i] = (1 - a) * metprm[j - 1][i] + a * metprm[j][i]; + } + } +} + +/* tropospheric delay correction ----------------------------------------------- + * compute sbas tropospheric delay correction (mops model) + *-----------------------------------------------------------------------------*/ +double tropSBAS( + Trace& trace, + GTime time, + VectorPos& pos, + double elev, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +) +{ + const double k1 = 77.604; + const double k2 = 382000; + const double rd = 287.054; + const double gm = 9.784; + const double g = 9.80665; + + if (pos.hgt() < -100 || pos.hgt() > +10000 || elev <= 0) + { + dryMap = 0; + wetMap = 0; + dryMap = 0; + wetMap = 0; + var = 0; + return 0; + } + + double met[10]; + getmet(pos.latDeg(), met); + + UYds yds = time; + double c = cos(2 * PI * (yds.doy - (pos.lat() >= 0 ? 28 : 211)) / 365.25); + for (int i = 0; i < 5; i++) + { + met[i] -= met[i + 5] * c; + } + dryZTD = 1E-6 * k1 * rd * met[0] / gm; + wetZTD = 1E-6 * k2 * rd * met[2] / (gm * (met[4] + 1) - met[3] * rd) / met[1]; + + double h = pos.hgt(); + dryZTD *= pow(1 - met[3] * h / met[1], g / (rd * met[3])); + wetZTD *= pow(1 - met[3] * h / met[1], (met[4] + 1) * g / (rd * met[3]) - 1); + + double sinel = sin(elev); + dryMap = 1.001 / sqrt(0.002001 + sinel * sinel); + double elevDeg = elev * R2D; + if (elevDeg < 4) + dryMap *= 1.0 + 0.015 * SQR(4 - elevDeg); + wetMap = dryMap; + var = SQR(0.12 * dryMap); + return (dryZTD + wetZTD) * dryMap; +} diff --git a/src/cpp/trop/tropVMF3.cpp b/src/cpp/trop/tropVMF3.cpp index aed8f0d4c..ae2b5482a 100644 --- a/src/cpp/trop/tropVMF3.cpp +++ b/src/cpp/trop/tropVMF3.cpp @@ -1,1206 +1,3894 @@ - // #pragma GCC optimize ("O0") #include +#include "trop/tropModels.hpp" using std::ifstream; -#include "tropModels.hpp" - /** vmf3 grid file contents */ struct Vmf3GridPoint { - union - { - double data[10] = {}; - struct - { - double lat; // lat grid (degree) - double lon; // lon grid (degree) - double ah; // hydrostatic mapping coefficient - double aw; // wet mapping coefficient - double zhd; // zenith hydrastatic delay (m) - double zwd; // zenith wet delay - double mfh; // hydrostatic mapping function - double mfw; // wet mapping function - double orog; - double index; // index in the file, for use against orography files - }; - }; - - Vmf3GridPoint operator * (double d) - { - Vmf3GridPoint output = *this; - for (int i = 0; i < 9; i++) //skip index - { - output.data[i] *= d; - } - - return output; - } - - Vmf3GridPoint& operator += (const Vmf3GridPoint& d) - { - for (int i = 0; i < 9; i++) //skip index - { - data[i] += d.data[i]; - } - - return *this; - } + union + { + double data[10] = {}; + struct + { + double lat; // lat grid (degree) + double lon; // lon grid (degree) + double ah; // hydrostatic mapping coefficient + double aw; // wet mapping coefficient + double zhd; // zenith hydrastatic delay (m) + double zwd; // zenith wet delay + double mfh; // hydrostatic mapping function + double mfw; // wet mapping function + double orog; + double index; // index in the file, for use against orography files + }; + }; + + Vmf3GridPoint operator*(double d) + { + Vmf3GridPoint output = *this; + for (int i = 0; i < 9; i++) // skip index + { + output.data[i] *= d; + } + + return output; + } + + Vmf3GridPoint& operator+=(const Vmf3GridPoint& d) + { + for (int i = 0; i < 9; i++) // skip index + { + data[i] += d.data[i]; + } + + return *this; + } }; -struct Vmf3 : map>> /* VMF gridmap [time][lat][lon] plus orography vector */ +struct Vmf3 : map>> /* VMF gridmap [time][lat][lon] + plus orography vector */ { - vector orography; + vector orography; - int gridLength = 0; - int orographyLength = 0; + int gridLength = 0; + int orographyLength = 0; }; -Vmf3 globalVMF3; ///< vmf3 grid info - -const double anm_bh[91][5] = -{ - {0.00271285863109945,-1.39197786008938E-06,1.34955672002719E-06,2.71686279717968E-07,1.56659301773925E-06}, - {9.80476624811974E-06,-5.83922611260673E-05,-2.07307023860417E-05,1.14628726961148E-06,4.93610283608719E-06}, - {-1.03443106534268E-05,-2.05536138785961E-06,2.09692641914244E-06,-1.55491034130965E-08,-1.89706404675801E-07}, - {-3.00353961749658E-05,2.37284447073503E-05,2.02236885378918E-05,1.69276006349609E-06,8.72156681243892E-07}, - {-7.99121077044035E-07,-5.39048313389504E-06,-4.21234502039861E-06,-2.70944149806894E-06,-6.80894455531746E-07}, - {7.51439609883296E-07,3.85509708865520E-07,4.41508016098164E-08,-2.07507808307757E-08,4.95354985050743E-08}, - {2.21790962160087E-05,-5.56986238775212E-05,-1.81287885563308E-05,-4.41076013532589E-06,4.93573223917278E-06}, - {-4.47639989737328E-06,-2.60452893072120E-06,2.56376320011189E-06,4.41600992220479E-07,2.93437730332869E-07}, - {8.14992682244945E-07,2.03945571424434E-07,1.11832498659806E-08,3.25756664234497E-08,3.01029040414968E-08}, - {-7.96927680907488E-08,-3.66953150925865E-08,-6.74742632186619E-09,-1.30315731273651E-08,-2.00748924306947E-09}, - {-2.16138375166934E-05,1.67350317962556E-05,1.93768260076821E-05,1.99595120161850E-06,-2.42463528222014E-06}, - {5.34360283708044E-07,-3.64189022040600E-06,-2.99935375194279E-06,-2.06880962903922E-06,-9.40815692626002E-07}, - {6.80235884441822E-07,1.33023436079845E-07,-1.80349593705226E-08,2.51276252565192E-08,-1.43240592002794E-09}, - {-7.13790897253802E-08,7.81998506267559E-09,1.13826909570178E-09,-5.89629600214654E-09,-4.20760865522804E-09}, - {-5.80109372399116E-09,1.13702284491976E-09,7.29046067602764E-10,-9.10468988754012E-10,-2.58814364808642E-10}, - {1.75558618192965E-05,-2.85579168876063E-05,-1.47442190284602E-05,-6.29300414335248E-06,-5.12204538913460E-07}, - {-1.90788558291310E-06,-1.62144845155361E-06,7.57239241641566E-07,6.93365788711348E-07,6.88855644570695E-07}, - {2.27050351488552E-07,1.03925791277660E-07,-3.31105076632079E-09,2.88065761026675E-08,-8.00256848229136E-09}, - {-2.77028851807614E-08,-5.96251132206930E-09,2.95987495527251E-10,-5.87644249625625E-09,-3.28803981542337E-09}, - {-1.89918479865558E-08,3.54083436578857E-09,8.10617835854935E-10,4.99207055948336E-10,-1.52691648387663E-10}, - {1.04022499586096E-09,-2.36437143845013E-10,-2.25110813484842E-10,-7.39850069252329E-11,7.95929405440911E-11}, - {-3.11579421267630E-05,-3.43576336877494E-06,5.81663608263384E-06,8.31534700351802E-07,4.02619520312154E-06}, - {6.00037066879001E-07,-1.12538760056168E-07,-3.86745332115590E-07,-3.88218746020826E-07,-6.83764967176388E-07}, - {-9.79583981249316E-08,9.14964449851003E-08,4.77779838549237E-09,2.44283811750703E-09,-6.26361079345158E-09}, - {-2.37742207548109E-08,-5.53336301671633E-09,-3.73625445257115E-09,-1.92304189572886E-09,-7.18681390197449E-09}, - {-6.58203463929583E-09,9.28456148541896E-10,2.47218904311077E-10,1.10664919110218E-10,-4.20390976974043E-11}, - {9.45857603373426E-10,-3.29683402990254E-11,-8.15440375865127E-11,-1.21615589356628E-12,-9.70713008848085E-12}, - {1.61377382316176E-10,6.84326027598147E-12,-4.66898885683671E-12,2.31211355085535E-12,2.39195112937346E-12}, - {2.99634365075821E-07,8.14391615472128E-06,6.70458490942443E-06,-9.92542646762000E-07,-3.04078064992750E-06}, - {-6.52697933801393E-07,2.87255329776428E-07,-1.78227609772085E-08,2.65525429849935E-07,8.60650570551813E-08}, - {-1.62727164011710E-07,1.09102479325892E-07,4.97827431850001E-09,7.86649963082937E-11,-6.67193813407656E-09}, - {-2.96370000987760E-09,1.20008401576557E-09,1.75885448022883E-09,-1.74756709684384E-09,3.21963061454248E-09}, - {-9.91101697778560E-10,7.54541713140752E-10,-2.95880967800875E-10,1.81009160501278E-10,8.31547411640954E-11}, - {1.21268051949609E-10,-5.93572774509587E-11,-5.03295034994351E-11,3.05383430975252E-11,3.56280438509939E-11}, - {6.92012970333794E-11,-9.02885345797597E-12,-3.44151832744880E-12,2.03164894681921E-12,-5.44852265137606E-12}, - {5.56731263672800E-12,3.57272150106101E-12,2.25885622368678E-12,-2.44508240047675E-13,-6.83314378535235E-13}, - {3.96883487797254E-06,-4.57100506169608E-06,-3.30208117813256E-06,3.32599719134845E-06,4.26539325549339E-06}, - {1.10123151770973E-06,4.58046760144882E-07,1.86831972581926E-07,-1.60092770735081E-07,-5.58956114867062E-07}, - {-3.40344900506653E-08,2.87649741373047E-08,-1.83929753066251E-08,-9.74179203885847E-09,-2.42064137485043E-09}, - {-6.49731596932566E-09,-3.07048108404447E-09,-2.84380614669848E-09,1.55123146524283E-09,4.53694984588346E-10}, - {5.45175793803325E-10,-3.73287624700125E-10,-1.16293122618336E-10,7.25845618602690E-11,-4.34112440021627E-11}, - {1.89481447552805E-10,3.67431482211078E-12,-1.72180065021194E-11,1.47046319023226E-11,1.31920481414062E-11}, - {2.10125915737167E-12,-3.08420783495975E-12,-4.87748712363020E-12,1.16363599902490E-14,1.26698255558605E-13}, - {-8.07894928696254E-12,9.19344620512607E-13,3.26929173307443E-13,2.00438149416495E-13,-9.57035765212079E-15}, - {1.38737151773284E-12,1.09340178371420E-13,5.15714202449053E-14,-5.92156438588931E-14,-3.29586752336143E-14}, - {6.38137197198254E-06,4.62426300749908E-06,4.42334454191034E-06,1.15374736092349E-06,-2.61859702227253E-06}, - {-2.25320619636149E-07,3.21907705479353E-07,-3.34834530764823E-07,-4.82132753601810E-07,-3.22410936343355E-07}, - {3.48894515496995E-09,3.49951261408458E-08,-6.01128959281142E-09,4.78213900943443E-09,1.46012816168576E-08}, - {-9.66682871952083E-11,3.75806627535317E-09,2.38984004956705E-09,2.07545049877203E-09,1.58573595632766E-09}, - {1.06834370693917E-09,-4.07975055112153E-10,-2.37598937943957E-10,5.89327007480137E-11,1.18891820437634E-10}, - {5.22433722695807E-11,6.02011995016293E-12,-7.80605402956048E-12,1.50873145627341E-11,-1.40550093106311E-12}, - {2.13396242187279E-13,-1.71939313965536E-12,-3.57625378660975E-14,-5.01675184988446E-14,-1.07805487368797E-12}, - {-1.24352330043311E-12,8.26105883301606E-13,4.63606970128517E-13,6.39517888984486E-14,-7.35135439920086E-14}, - {-5.39023859065631E-13,2.54188315588243E-14,1.30933833278664E-14,6.06153473304781E-15,-4.24722717533726E-14}, - {3.12767756884813E-14,-2.29517847871632E-15,2.53117304424948E-16,7.07504914138118E-16,-1.20089065310688E-15}, - {2.08311178819214E-06,-1.22179185044174E-06,-2.98842190131044E-06,3.07310218974299E-06,2.27100346036619E-06}, - {-3.94601643855452E-07,-5.44014825116083E-07,-6.16955333162507E-08,-2.31954821580670E-07,1.14010813005310E-07}, - {6.11067575043044E-08,-3.93240193194272E-08,-1.62979132528933E-08,1.01339204652581E-08,1.97319601566071E-08}, - {2.57770508710055E-09,1.87799543582899E-09,1.95407654714372E-09,1.15276419281270E-09,2.25397005402120E-09}, - {7.16926338026236E-10,-3.65857693313858E-10,-1.54864067050915E-11,6.50770211276549E-11,-7.85160007413546E-12}, - {4.90007693914221E-12,3.31649396536340E-12,4.81664871165640E-13,7.26080745617085E-12,2.30960953372164E-12}, - {9.75489202240545E-13,-1.68967954531421E-13,7.38383391334110E-13,-3.58435515913239E-13,-3.01564710027450E-13}, - {-3.79533601922805E-13,2.76681830946617E-13,1.21480375553803E-13,-1.57729077644850E-14,-8.87664977818700E-14}, - {-3.96462845480288E-14,2.94155690934610E-14,6.78413205760717E-15,-4.12135802787361E-15,-1.46373307795619E-14}, - {-8.64941937408121E-15,-1.91822620970386E-15,-8.01725413560744E-16,5.02941051180784E-16,-1.07572628474344E-15}, - {-4.13816294742758E-15,-7.43602019785880E-17,-5.54248556346072E-17,-4.83999456005158E-17,-1.19622559730466E-16}, - {-8.34852132750364E-07,-7.45794677612056E-06,-6.58132648865533E-06,-1.38608110346732E-06,5.32326534882584E-07}, - {-2.75513802414150E-07,3.64713745106279E-08,-7.12385417940442E-08,-7.86206067228882E-08,2.28048393207161E-08}, - {-4.26696415431918E-08,-4.65599668635087E-09,7.35037936327566E-09,1.17098354115804E-08,1.44594777658035E-08}, - {1.12407689274199E-09,7.62142529563709E-10,-6.72563708415472E-10,-1.18094592485992E-10,-1.17043815733292E-09}, - {1.76612225246125E-10,-1.01188552503192E-10,7.32546072616968E-11,1.79542821801610E-11,-2.23264859965402E-11}, - {-9.35960722512375E-12,1.90894283812231E-12,-6.34792824525760E-13,3.98597963877826E-12,-4.47591409078971E-12}, - {-3.34623858556099E-12,4.56384903915853E-14,2.72561108521416E-13,-3.57942733300468E-15,1.99794810657713E-13}, - {-6.16775522568954E-14,8.25316968328823E-14,7.19845814260518E-14,-2.92415710855106E-14,-5.49570017444031E-15}, - {-8.50728802453217E-15,8.38161600916267E-15,3.43651657459983E-15,-8.19429434115910E-16,-4.08905746461100E-15}, - {4.39042894275548E-15,-3.69440485320477E-16,1.22249256876779E-16,-2.09359444520984E-16,-3.34211740264257E-16}, - {-5.36054548134225E-16,3.29794204041989E-17,2.13564354374585E-17,-1.37838993720865E-18,-1.29188342867753E-17}, - {-3.26421841529845E-17,7.38235405234126E-18,2.49291659676210E-18,8.18252735459593E-19,1.73824952279230E-20}, - {4.67237509268208E-06,1.93611283787239E-06,9.39035455627622E-07,-5.84565118072823E-07,-1.76198705802101E-07}, - {-3.33739157421993E-07,4.12139555299163E-07,1.58754695700856E-07,1.37448753329669E-07,1.04722936936873E-07}, - {6.64200603076386E-09,1.45412222625734E-08,1.82498796118030E-08,2.86633517581614E-09,1.06066984548100E-09}, - {5.25549696746655E-09,-1.33677183394083E-09,7.60804375937931E-11,-1.07918624219037E-10,8.09178898247941E-10}, - {1.89318454110039E-10,9.23092164791765E-11,5.51434573131180E-11,3.86696392289240E-11,-1.15208165047149E-11}, - {-1.02252706006226E-12,-7.25921015411136E-13,-1.98110126887620E-12,-2.18964868282672E-13,-7.18834476685625E-13}, - {-2.69770025318548E-12,-2.17850340796321E-14,4.73040820865871E-13,1.57947421572149E-13,1.86925164972766E-13}, - {1.07831718354771E-13,2.26681841611017E-14,2.56046087047783E-14,-1.14995851659554E-14,-2.27056907624485E-14}, - {6.29825154734712E-15,8.04458225889001E-16,9.53173540411138E-16,1.16892301877735E-15,-1.04324684545047E-15}, - {-5.57345639727027E-16,-2.93949227634932E-16,7.47621406284534E-18,-5.36416885470756E-17,-2.87213280230513E-16}, - {1.73219775047208E-16,2.05017387523061E-17,9.08873886345587E-18,-2.86881547225742E-18,-1.25303645304992E-17}, - {-7.30829109684568E-18,2.03711261415353E-18,7.62162636124024E-19,-7.54847922012517E-19,-8.85105098195030E-19}, - {5.62039968280587E-18,-1.38144206573507E-19,1.68028711767211E-20,1.81223858251981E-19,-8.50245194985878E-20} +Vmf3 globalVMF3; ///< vmf3 grid info + +const double anm_bh[91][5] = { + {0.00271285863109945, + -1.39197786008938E-06, + 1.34955672002719E-06, + 2.71686279717968E-07, + 1.56659301773925E-06}, + {9.80476624811974E-06, + -5.83922611260673E-05, + -2.07307023860417E-05, + 1.14628726961148E-06, + 4.93610283608719E-06}, + {-1.03443106534268E-05, + -2.05536138785961E-06, + 2.09692641914244E-06, + -1.55491034130965E-08, + -1.89706404675801E-07}, + {-3.00353961749658E-05, + 2.37284447073503E-05, + 2.02236885378918E-05, + 1.69276006349609E-06, + 8.72156681243892E-07}, + {-7.99121077044035E-07, + -5.39048313389504E-06, + -4.21234502039861E-06, + -2.70944149806894E-06, + -6.80894455531746E-07}, + {7.51439609883296E-07, + 3.85509708865520E-07, + 4.41508016098164E-08, + -2.07507808307757E-08, + 4.95354985050743E-08}, + {2.21790962160087E-05, + -5.56986238775212E-05, + -1.81287885563308E-05, + -4.41076013532589E-06, + 4.93573223917278E-06}, + {-4.47639989737328E-06, + -2.60452893072120E-06, + 2.56376320011189E-06, + 4.41600992220479E-07, + 2.93437730332869E-07}, + {8.14992682244945E-07, + 2.03945571424434E-07, + 1.11832498659806E-08, + 3.25756664234497E-08, + 3.01029040414968E-08}, + {-7.96927680907488E-08, + -3.66953150925865E-08, + -6.74742632186619E-09, + -1.30315731273651E-08, + -2.00748924306947E-09}, + {-2.16138375166934E-05, + 1.67350317962556E-05, + 1.93768260076821E-05, + 1.99595120161850E-06, + -2.42463528222014E-06}, + {5.34360283708044E-07, + -3.64189022040600E-06, + -2.99935375194279E-06, + -2.06880962903922E-06, + -9.40815692626002E-07}, + {6.80235884441822E-07, + 1.33023436079845E-07, + -1.80349593705226E-08, + 2.51276252565192E-08, + -1.43240592002794E-09}, + {-7.13790897253802E-08, + 7.81998506267559E-09, + 1.13826909570178E-09, + -5.89629600214654E-09, + -4.20760865522804E-09}, + {-5.80109372399116E-09, + 1.13702284491976E-09, + 7.29046067602764E-10, + -9.10468988754012E-10, + -2.58814364808642E-10}, + {1.75558618192965E-05, + -2.85579168876063E-05, + -1.47442190284602E-05, + -6.29300414335248E-06, + -5.12204538913460E-07}, + {-1.90788558291310E-06, + -1.62144845155361E-06, + 7.57239241641566E-07, + 6.93365788711348E-07, + 6.88855644570695E-07}, + {2.27050351488552E-07, + 1.03925791277660E-07, + -3.31105076632079E-09, + 2.88065761026675E-08, + -8.00256848229136E-09}, + {-2.77028851807614E-08, + -5.96251132206930E-09, + 2.95987495527251E-10, + -5.87644249625625E-09, + -3.28803981542337E-09}, + {-1.89918479865558E-08, + 3.54083436578857E-09, + 8.10617835854935E-10, + 4.99207055948336E-10, + -1.52691648387663E-10}, + {1.04022499586096E-09, + -2.36437143845013E-10, + -2.25110813484842E-10, + -7.39850069252329E-11, + 7.95929405440911E-11}, + {-3.11579421267630E-05, + -3.43576336877494E-06, + 5.81663608263384E-06, + 8.31534700351802E-07, + 4.02619520312154E-06}, + {6.00037066879001E-07, + -1.12538760056168E-07, + -3.86745332115590E-07, + -3.88218746020826E-07, + -6.83764967176388E-07}, + {-9.79583981249316E-08, + 9.14964449851003E-08, + 4.77779838549237E-09, + 2.44283811750703E-09, + -6.26361079345158E-09}, + {-2.37742207548109E-08, + -5.53336301671633E-09, + -3.73625445257115E-09, + -1.92304189572886E-09, + -7.18681390197449E-09}, + {-6.58203463929583E-09, + 9.28456148541896E-10, + 2.47218904311077E-10, + 1.10664919110218E-10, + -4.20390976974043E-11}, + {9.45857603373426E-10, + -3.29683402990254E-11, + -8.15440375865127E-11, + -1.21615589356628E-12, + -9.70713008848085E-12}, + {1.61377382316176E-10, + 6.84326027598147E-12, + -4.66898885683671E-12, + 2.31211355085535E-12, + 2.39195112937346E-12}, + {2.99634365075821E-07, + 8.14391615472128E-06, + 6.70458490942443E-06, + -9.92542646762000E-07, + -3.04078064992750E-06}, + {-6.52697933801393E-07, + 2.87255329776428E-07, + -1.78227609772085E-08, + 2.65525429849935E-07, + 8.60650570551813E-08}, + {-1.62727164011710E-07, + 1.09102479325892E-07, + 4.97827431850001E-09, + 7.86649963082937E-11, + -6.67193813407656E-09}, + {-2.96370000987760E-09, + 1.20008401576557E-09, + 1.75885448022883E-09, + -1.74756709684384E-09, + 3.21963061454248E-09}, + {-9.91101697778560E-10, + 7.54541713140752E-10, + -2.95880967800875E-10, + 1.81009160501278E-10, + 8.31547411640954E-11}, + {1.21268051949609E-10, + -5.93572774509587E-11, + -5.03295034994351E-11, + 3.05383430975252E-11, + 3.56280438509939E-11}, + {6.92012970333794E-11, + -9.02885345797597E-12, + -3.44151832744880E-12, + 2.03164894681921E-12, + -5.44852265137606E-12}, + {5.56731263672800E-12, + 3.57272150106101E-12, + 2.25885622368678E-12, + -2.44508240047675E-13, + -6.83314378535235E-13}, + {3.96883487797254E-06, + -4.57100506169608E-06, + -3.30208117813256E-06, + 3.32599719134845E-06, + 4.26539325549339E-06}, + {1.10123151770973E-06, + 4.58046760144882E-07, + 1.86831972581926E-07, + -1.60092770735081E-07, + -5.58956114867062E-07}, + {-3.40344900506653E-08, + 2.87649741373047E-08, + -1.83929753066251E-08, + -9.74179203885847E-09, + -2.42064137485043E-09}, + {-6.49731596932566E-09, + -3.07048108404447E-09, + -2.84380614669848E-09, + 1.55123146524283E-09, + 4.53694984588346E-10}, + {5.45175793803325E-10, + -3.73287624700125E-10, + -1.16293122618336E-10, + 7.25845618602690E-11, + -4.34112440021627E-11}, + {1.89481447552805E-10, + 3.67431482211078E-12, + -1.72180065021194E-11, + 1.47046319023226E-11, + 1.31920481414062E-11}, + {2.10125915737167E-12, + -3.08420783495975E-12, + -4.87748712363020E-12, + 1.16363599902490E-14, + 1.26698255558605E-13}, + {-8.07894928696254E-12, + 9.19344620512607E-13, + 3.26929173307443E-13, + 2.00438149416495E-13, + -9.57035765212079E-15}, + {1.38737151773284E-12, + 1.09340178371420E-13, + 5.15714202449053E-14, + -5.92156438588931E-14, + -3.29586752336143E-14}, + {6.38137197198254E-06, + 4.62426300749908E-06, + 4.42334454191034E-06, + 1.15374736092349E-06, + -2.61859702227253E-06}, + {-2.25320619636149E-07, + 3.21907705479353E-07, + -3.34834530764823E-07, + -4.82132753601810E-07, + -3.22410936343355E-07}, + {3.48894515496995E-09, + 3.49951261408458E-08, + -6.01128959281142E-09, + 4.78213900943443E-09, + 1.46012816168576E-08}, + {-9.66682871952083E-11, + 3.75806627535317E-09, + 2.38984004956705E-09, + 2.07545049877203E-09, + 1.58573595632766E-09}, + {1.06834370693917E-09, + -4.07975055112153E-10, + -2.37598937943957E-10, + 5.89327007480137E-11, + 1.18891820437634E-10}, + {5.22433722695807E-11, + 6.02011995016293E-12, + -7.80605402956048E-12, + 1.50873145627341E-11, + -1.40550093106311E-12}, + {2.13396242187279E-13, + -1.71939313965536E-12, + -3.57625378660975E-14, + -5.01675184988446E-14, + -1.07805487368797E-12}, + {-1.24352330043311E-12, + 8.26105883301606E-13, + 4.63606970128517E-13, + 6.39517888984486E-14, + -7.35135439920086E-14}, + {-5.39023859065631E-13, + 2.54188315588243E-14, + 1.30933833278664E-14, + 6.06153473304781E-15, + -4.24722717533726E-14}, + {3.12767756884813E-14, + -2.29517847871632E-15, + 2.53117304424948E-16, + 7.07504914138118E-16, + -1.20089065310688E-15}, + {2.08311178819214E-06, + -1.22179185044174E-06, + -2.98842190131044E-06, + 3.07310218974299E-06, + 2.27100346036619E-06}, + {-3.94601643855452E-07, + -5.44014825116083E-07, + -6.16955333162507E-08, + -2.31954821580670E-07, + 1.14010813005310E-07}, + {6.11067575043044E-08, + -3.93240193194272E-08, + -1.62979132528933E-08, + 1.01339204652581E-08, + 1.97319601566071E-08}, + {2.57770508710055E-09, + 1.87799543582899E-09, + 1.95407654714372E-09, + 1.15276419281270E-09, + 2.25397005402120E-09}, + {7.16926338026236E-10, + -3.65857693313858E-10, + -1.54864067050915E-11, + 6.50770211276549E-11, + -7.85160007413546E-12}, + {4.90007693914221E-12, + 3.31649396536340E-12, + 4.81664871165640E-13, + 7.26080745617085E-12, + 2.30960953372164E-12}, + {9.75489202240545E-13, + -1.68967954531421E-13, + 7.38383391334110E-13, + -3.58435515913239E-13, + -3.01564710027450E-13}, + {-3.79533601922805E-13, + 2.76681830946617E-13, + 1.21480375553803E-13, + -1.57729077644850E-14, + -8.87664977818700E-14}, + {-3.96462845480288E-14, + 2.94155690934610E-14, + 6.78413205760717E-15, + -4.12135802787361E-15, + -1.46373307795619E-14}, + {-8.64941937408121E-15, + -1.91822620970386E-15, + -8.01725413560744E-16, + 5.02941051180784E-16, + -1.07572628474344E-15}, + {-4.13816294742758E-15, + -7.43602019785880E-17, + -5.54248556346072E-17, + -4.83999456005158E-17, + -1.19622559730466E-16}, + {-8.34852132750364E-07, + -7.45794677612056E-06, + -6.58132648865533E-06, + -1.38608110346732E-06, + 5.32326534882584E-07}, + {-2.75513802414150E-07, + 3.64713745106279E-08, + -7.12385417940442E-08, + -7.86206067228882E-08, + 2.28048393207161E-08}, + {-4.26696415431918E-08, + -4.65599668635087E-09, + 7.35037936327566E-09, + 1.17098354115804E-08, + 1.44594777658035E-08}, + {1.12407689274199E-09, + 7.62142529563709E-10, + -6.72563708415472E-10, + -1.18094592485992E-10, + -1.17043815733292E-09}, + {1.76612225246125E-10, + -1.01188552503192E-10, + 7.32546072616968E-11, + 1.79542821801610E-11, + -2.23264859965402E-11}, + {-9.35960722512375E-12, + 1.90894283812231E-12, + -6.34792824525760E-13, + 3.98597963877826E-12, + -4.47591409078971E-12}, + {-3.34623858556099E-12, + 4.56384903915853E-14, + 2.72561108521416E-13, + -3.57942733300468E-15, + 1.99794810657713E-13}, + {-6.16775522568954E-14, + 8.25316968328823E-14, + 7.19845814260518E-14, + -2.92415710855106E-14, + -5.49570017444031E-15}, + {-8.50728802453217E-15, + 8.38161600916267E-15, + 3.43651657459983E-15, + -8.19429434115910E-16, + -4.08905746461100E-15}, + {4.39042894275548E-15, + -3.69440485320477E-16, + 1.22249256876779E-16, + -2.09359444520984E-16, + -3.34211740264257E-16}, + {-5.36054548134225E-16, + 3.29794204041989E-17, + 2.13564354374585E-17, + -1.37838993720865E-18, + -1.29188342867753E-17}, + {-3.26421841529845E-17, + 7.38235405234126E-18, + 2.49291659676210E-18, + 8.18252735459593E-19, + 1.73824952279230E-20}, + {4.67237509268208E-06, + 1.93611283787239E-06, + 9.39035455627622E-07, + -5.84565118072823E-07, + -1.76198705802101E-07}, + {-3.33739157421993E-07, + 4.12139555299163E-07, + 1.58754695700856E-07, + 1.37448753329669E-07, + 1.04722936936873E-07}, + {6.64200603076386E-09, + 1.45412222625734E-08, + 1.82498796118030E-08, + 2.86633517581614E-09, + 1.06066984548100E-09}, + {5.25549696746655E-09, + -1.33677183394083E-09, + 7.60804375937931E-11, + -1.07918624219037E-10, + 8.09178898247941E-10}, + {1.89318454110039E-10, + 9.23092164791765E-11, + 5.51434573131180E-11, + 3.86696392289240E-11, + -1.15208165047149E-11}, + {-1.02252706006226E-12, + -7.25921015411136E-13, + -1.98110126887620E-12, + -2.18964868282672E-13, + -7.18834476685625E-13}, + {-2.69770025318548E-12, + -2.17850340796321E-14, + 4.73040820865871E-13, + 1.57947421572149E-13, + 1.86925164972766E-13}, + {1.07831718354771E-13, + 2.26681841611017E-14, + 2.56046087047783E-14, + -1.14995851659554E-14, + -2.27056907624485E-14}, + {6.29825154734712E-15, + 8.04458225889001E-16, + 9.53173540411138E-16, + 1.16892301877735E-15, + -1.04324684545047E-15}, + {-5.57345639727027E-16, + -2.93949227634932E-16, + 7.47621406284534E-18, + -5.36416885470756E-17, + -2.87213280230513E-16}, + {1.73219775047208E-16, + 2.05017387523061E-17, + 9.08873886345587E-18, + -2.86881547225742E-18, + -1.25303645304992E-17}, + {-7.30829109684568E-18, + 2.03711261415353E-18, + 7.62162636124024E-19, + -7.54847922012517E-19, + -8.85105098195030E-19}, + {5.62039968280587E-18, + -1.38144206573507E-19, + 1.68028711767211E-20, + 1.81223858251981E-19, + -8.50245194985878E-20} }; -const double anm_bw[91][5]= -{ - {0.00136127467401223,-6.83476317823061E-07,-1.37211986707674E-06,7.02561866200582E-07,-2.16342338010651E-07}, - {-9.53197486400299E-06,6.58703762338336E-06,2.42000663952044E-06,-6.04283463108935E-07,2.02144424676990E-07}, - {-6.76728911259359E-06,6.03830755085583E-07,-8.72568628835897E-08,2.21750344140938E-06,1.05146032931020E-06}, - {-3.21102832397338E-05,-7.88685357568093E-06,-2.55495673641049E-06,-1.99601934456719E-06,-4.62005252198027E-07}, - {-7.84639263523250E-07,3.11624739733849E-06,9.02170019697389E-07,6.37066632506008E-07,-9.44485038780872E-09}, - {2.19476873575507E-06,-2.20580510638233E-07,6.94761415598378E-07,4.80770865279717E-07,-1.34357837196401E-07}, - {2.18469215148328E-05,-1.80674174262038E-06,-1.52754285605060E-06,-3.51212288219241E-07,2.73741237656351E-06}, - {2.85579058479116E-06,1.57201369332361E-07,-2.80599072875081E-07,-4.91267304946072E-07,-2.11648188821805E-07}, - {2.81729255594770E-06,3.02487362536122E-07,-1.64836481475431E-07,-2.11607615408593E-07,-6.47817762225366E-08}, - {1.31809947620223E-07,-1.58289524114549E-07,-7.05580919885505E-08,5.56781440550867E-08,1.23403290710365E-08}, - {-1.29252282695869E-05,-1.07247072037590E-05,-3.31109519638196E-06,2.13776673779736E-06,-1.49519398373391E-07}, - {1.81685152305722E-06,-1.17362204417861E-06,-3.19205277136370E-08,4.09166457255416E-07,1.53286667406152E-07}, - {1.63477723125362E-06,-2.68584775517243E-08,4.94662064805191E-09,-7.09027987928288E-08,4.44353430574937E-08}, - {-2.13090618917978E-07,4.05836983493219E-08,2.94495876336549E-08,-1.75005469063176E-08,-3.03015988647002E-09}, - {-2.16074435298006E-09,9.37631708987675E-09,-2.05996036369828E-08,6.97068002894092E-09,-8.90988987979604E-09}, - {1.38047798906967E-05,2.05528261553901E-05,1.59072148872708E-05,7.34088731264443E-07,1.28226710383580E-06}, - {7.08175753966264E-07,-9.27988276636505E-07,1.60535820026081E-07,-3.27296675122065E-07,-2.20518321170684E-07}, - {1.90932483086199E-07,-7.44215272759193E-08,1.81330673333187E-08,4.37149649043616E-08,4.18884335594172E-08}, - {-5.37009063880924E-08,2.22870057779431E-08,1.73740123037651E-08,-4.45137302235032E-09,9.44721910524571E-09}, - {-6.83406949047909E-08,-1.95046676795923E-10,2.57535903049686E-09,4.82643164083020E-09,3.37657333705158E-09}, - {3.96128688448981E-09,-6.63809403270686E-10,2.44781464212534E-10,5.92280853590699E-11,-4.78502591970721E-10}, - {1.75859399041414E-05,-2.81238050668481E-06,-2.43670534594848E-06,3.58244562699714E-06,-1.76547446732691E-06}, - {-1.06451311473304E-07,1.54336689617184E-06,-2.00690000442673E-07,1.38790047911880E-09,-1.62490619890017E-07}, - {-2.72757421686155E-07,1.71139266205398E-07,-2.55080309401917E-08,-8.40793079489831E-09,-1.01129447760167E-08}, - {2.92966025844079E-08,-2.07556718857313E-08,5.45985315647905E-09,8.76857690274150E-09,1.06785510440474E-08}, - {-1.22059608941331E-08,6.52491630264276E-09,-1.79332492326928E-10,3.75921793745396E-10,-7.06416506254786E-10}, - {1.63224355776652E-09,4.95586028736232E-10,-3.07879011759040E-10,-7.78354087544277E-11,1.43959047067250E-10}, - {3.86319414653663E-10,-2.06467134617933E-10,4.37330971382694E-11,-5.00421056263711E-11,-9.40237773015723E-12}, - {-1.23856142706451E-05,7.61047394008415E-06,-1.99104114578138E-07,6.86177748886858E-07,-1.09466747592827E-07}, - {2.99866062403128E-07,1.87525561397390E-07,4.99374806994715E-08,4.86229763781404E-07,4.46570575517658E-07}, - {-5.05748332368430E-07,1.95523624722285E-08,-9.17535435911345E-08,-2.56671607433547E-08,-7.11896201616653E-08}, - {-2.66062200406494E-08,-5.40470019739274E-09,-2.29718660244954E-09,-3.73328592264404E-09,3.38748313712376E-09}, - {5.30855327954894E-10,5.28851845648032E-10,-2.22278913745418E-10,-5.52628653064771E-11,-9.24825145219684E-10}, - {6.03737227573716E-10,-3.52190673510919E-12,-1.30371720641414E-10,-9.12787239944822E-12,6.42187285537238E-12}, - {1.78081862458539E-10,2.93772078656037E-12,-1.04698379945322E-11,-2.82260024833024E-11,-5.61810459067525E-12}, - {9.35003092299580E-12,-8.23133834521577E-13,5.54878414224198E-13,-3.62943215777181E-13,2.38858933771653E-12}, - {-1.31216096107331E-05,-5.70451670731759E-06,-5.11598683573971E-06,-4.99990779887599E-06,1.27389320221511E-07}, - {-1.23108260369048E-06,5.53093245213587E-07,8.60093183929302E-07,2.65569700925696E-07,1.95485134805575E-07}, - {-2.29647072638049E-07,-5.45266515081825E-08,2.85298129762263E-08,1.98167939680185E-08,5.52227340898335E-09}, - {-2.73844745019857E-08,-4.48345173291362E-10,-1.93967347049382E-09,-1.41508853776629E-09,-1.75456962391145E-09}, - {-2.68863184376108E-11,-2.20546981683293E-09,6.56116990576877E-10,1.27129855674922E-10,-2.32334506413213E-10}, - {1.98303136881156E-10,6.04782006047075E-11,2.91291115431570E-11,6.18098615782757E-11,-3.82682292530379E-11}, - {9.48294455071158E-12,-3.05873596453015E-13,5.31539408055057E-13,-7.31016438665600E-12,-1.19921002209198E-11}, - {-2.25188050845725E-11,-3.91627574966393E-13,-6.80217235976769E-13,5.91033607278405E-13,5.02991534452191E-13}, - {1.29532063896247E-12,1.66337285851564E-13,3.25543028344555E-13,1.89143357962363E-13,3.32288378169726E-13}, - {-2.45864358781728E-06,4.49460524898260E-06,1.03890496648813E-06,-2.73783420376785E-06,7.12695730642593E-07}, - {-9.27805078535168E-07,-4.97733876686731E-07,9.18680298906510E-08,-2.47200617423980E-07,6.16163630140379E-08}, - {-1.39623661883136E-08,-1.12580495666505E-07,2.61821435950379E-08,-2.31875562002885E-08,5.72679835033659E-08}, - {-9.52538983318497E-09,-5.40909215302433E-09,1.88698793952475E-09,-4.08127746406372E-09,1.09534895853812E-10}, - {3.79767457525741E-09,1.11549801373366E-10,-6.45504957274111E-10,3.05477141010356E-10,1.26261210565856E-10}, - {5.08813577945300E-11,1.43250547678637E-11,8.81616572082448E-12,2.58968878880804E-11,3.83421818249954E-11}, - {8.95094368142044E-12,-3.26220304555971E-12,-1.28047847191896E-12,2.67562170258942E-12,2.72195031576670E-12}, - {-6.47181697409757E-12,1.13776457455685E-12,2.84856274334969E-13,-7.63667272085395E-14,-1.34451657758826E-13}, - {-1.25291265888343E-12,8.63500441050317E-14,-1.21307856635548E-13,5.12570529540511E-14,3.32389276976573E-14}, - {3.73573418085813E-14,-5.37808783042784E-16,-4.23430408270850E-16,-4.75110565740493E-15,6.02553212780166E-15}, - {8.95483987262751E-06,-3.90778212666235E-06,-1.12115019808259E-06,1.78678942093383E-06,1.46806344157962E-06}, - {-4.59185232678613E-07,1.09497995905419E-07,1.31663977640045E-07,4.20525791073626E-08,-9.71470741607431E-08}, - {1.63399802579572E-07,1.50909360648645E-08,-1.11480472593347E-08,-1.84000857674573E-08,7.82124614794256E-09}, - {1.22887452385094E-08,-4.06647399822746E-10,-6.49120327585597E-10,8.63651225791194E-10,-2.73440085913102E-09}, - {2.51748630889583E-09,4.79895880425564E-10,-2.44908073860844E-10,2.56735882664876E-10,-1.64815306286912E-10}, - {4.85671381736718E-11,-2.51742732115131E-11,-2.60819437993179E-11,6.12728324086123E-12,2.16833310896138E-11}, - {4.11389702320298E-12,-8.09433180989935E-13,-1.19812498226024E-12,1.46885737888520E-12,3.15807685137836E-12}, - {-1.47614580597013E-12,4.66726413909320E-13,1.72089709006255E-13,1.13854935381418E-13,2.77741161317003E-13}, - {-1.02257724967727E-13,1.10394382923502E-13,-3.14153505370805E-15,2.41103099110106E-14,2.13853053149771E-14}, - {-3.19080885842786E-14,-9.53904307973447E-15,2.74542788156379E-15,2.33797859107844E-15,-2.53192474907304E-15}, - {-5.87702222126367E-15,-1.80133850930249E-15,-3.09793125614454E-16,-1.04197538975295E-16,3.72781664701327E-16}, - {1.86187054729085E-06,8.33098045333428E-06,3.18277735484232E-06,-7.68273797022231E-07,-1.52337222261696E-06}, - {-5.07076646593648E-07,-8.61959553442156E-07,-3.51690005432816E-07,-4.20797082902431E-07,-3.07652993252673E-07}, - {-7.38992472164147E-08,-8.39473083080280E-08,-2.51587083298935E-08,7.30691259725451E-09,-3.19457155958983E-08}, - {-1.99777182012924E-09,-3.21265085916022E-09,-4.84477421865675E-10,-1.82924814205799E-09,-3.46664344655997E-10}, - {-7.05788559634927E-11,1.21840735569025E-10,7.97347726425926E-11,1.08275679614409E-10,-1.17891254809785E-10}, - {1.10299718947774E-11,-3.22958261390263E-11,-1.43535798209229E-11,6.87096504209595E-12,-6.64963212272352E-12}, - {-6.47393639740084E-12,1.03156978325120E-12,-9.20099775082358E-14,-2.40150316641949E-13,1.14008812047857E-12}, - {-1.23957846397250E-13,2.85996703969692E-13,1.91579874982553E-13,5.20597174693064E-14,-4.06741434883370E-14}, - {-2.35479068911236E-14,1.97847338186993E-14,1.58935977518516E-15,-2.32217195254742E-15,-8.48611789490575E-15}, - {1.03992320391626E-14,1.54017082092642E-15,1.05950035082788E-16,-1.17870898461353E-15,-1.10937420707372E-15}, - {-1.09011948374520E-15,-6.04168007633584E-16,-9.10901998157436E-17,1.98379116989461E-16,-1.03715496658498E-16}, - {-1.38171942108278E-16,-6.33037999097522E-17,-1.38777695011470E-17,1.94191397045401E-17,5.70055906754485E-18}, - {1.92989406002085E-06,-3.82662130483128E-06,-4.60189561036048E-07,2.24290587856309E-06,1.40544379451550E-06}, - {6.49033717633394E-08,2.41396114435326E-07,2.73948898223321E-07,1.10633664439332E-07,-3.19555270171075E-08}, - {-2.91988966963297E-08,-6.03828192816571E-09,1.18462386444840E-08,1.32095545004128E-08,-5.06572721528914E-09}, - {7.31079058474148E-09,-8.42775299751834E-10,1.10190810090667E-09,1.96592273424306E-09,-2.13135932785688E-09}, - {7.06656405314388E-11,1.43441125783756E-10,1.46962246686924E-10,7.44592776425197E-11,-3.64331892799173E-11}, - {-2.52393942119372E-11,1.07520964869263E-11,5.84669886072094E-12,6.52029744217103E-12,1.82947123132059E-12}, - {-4.15669940115121E-12,-1.95963254053648E-13,2.16977822834301E-13,-2.84701408462031E-13,4.27194601040231E-13}, - {3.07891105454129E-13,1.91523190672955E-13,1.05367297580989E-13,-5.28136363920236E-14,-3.53364110005917E-14}, - {7.02156663274738E-15,9.52230536780849E-15,-3.41019408682733E-15,-3.59825303352899E-15,-2.62576411636150E-15}, - {-1.75110277413804E-15,5.29265220719483E-16,4.45015980897919E-16,-3.80179856341347E-16,-4.32917763829695E-16}, - {1.16038609651443E-16,-6.69643574373352E-17,2.65667154817303E-17,-9.76010333683956E-17,4.07312981076655E-17}, - {5.72659246346386E-18,1.30357528108671E-18,2.49193258417535E-18,1.76247014075584E-18,7.59614374197688E-19}, - {1.03352170833303E-17,-2.30633516638829E-18,2.84777940620193E-18,-7.72161347944693E-19,6.07028034506380E-19} +const double anm_bw[91][5] = { + {0.00136127467401223, + -6.83476317823061E-07, + -1.37211986707674E-06, + 7.02561866200582E-07, + -2.16342338010651E-07}, + {-9.53197486400299E-06, + 6.58703762338336E-06, + 2.42000663952044E-06, + -6.04283463108935E-07, + 2.02144424676990E-07}, + {-6.76728911259359E-06, + 6.03830755085583E-07, + -8.72568628835897E-08, + 2.21750344140938E-06, + 1.05146032931020E-06}, + {-3.21102832397338E-05, + -7.88685357568093E-06, + -2.55495673641049E-06, + -1.99601934456719E-06, + -4.62005252198027E-07}, + {-7.84639263523250E-07, + 3.11624739733849E-06, + 9.02170019697389E-07, + 6.37066632506008E-07, + -9.44485038780872E-09}, + {2.19476873575507E-06, + -2.20580510638233E-07, + 6.94761415598378E-07, + 4.80770865279717E-07, + -1.34357837196401E-07}, + {2.18469215148328E-05, + -1.80674174262038E-06, + -1.52754285605060E-06, + -3.51212288219241E-07, + 2.73741237656351E-06}, + {2.85579058479116E-06, + 1.57201369332361E-07, + -2.80599072875081E-07, + -4.91267304946072E-07, + -2.11648188821805E-07}, + {2.81729255594770E-06, + 3.02487362536122E-07, + -1.64836481475431E-07, + -2.11607615408593E-07, + -6.47817762225366E-08}, + {1.31809947620223E-07, + -1.58289524114549E-07, + -7.05580919885505E-08, + 5.56781440550867E-08, + 1.23403290710365E-08}, + {-1.29252282695869E-05, + -1.07247072037590E-05, + -3.31109519638196E-06, + 2.13776673779736E-06, + -1.49519398373391E-07}, + {1.81685152305722E-06, + -1.17362204417861E-06, + -3.19205277136370E-08, + 4.09166457255416E-07, + 1.53286667406152E-07}, + {1.63477723125362E-06, + -2.68584775517243E-08, + 4.94662064805191E-09, + -7.09027987928288E-08, + 4.44353430574937E-08}, + {-2.13090618917978E-07, + 4.05836983493219E-08, + 2.94495876336549E-08, + -1.75005469063176E-08, + -3.03015988647002E-09}, + {-2.16074435298006E-09, + 9.37631708987675E-09, + -2.05996036369828E-08, + 6.97068002894092E-09, + -8.90988987979604E-09}, + {1.38047798906967E-05, + 2.05528261553901E-05, + 1.59072148872708E-05, + 7.34088731264443E-07, + 1.28226710383580E-06}, + {7.08175753966264E-07, + -9.27988276636505E-07, + 1.60535820026081E-07, + -3.27296675122065E-07, + -2.20518321170684E-07}, + {1.90932483086199E-07, + -7.44215272759193E-08, + 1.81330673333187E-08, + 4.37149649043616E-08, + 4.18884335594172E-08}, + {-5.37009063880924E-08, + 2.22870057779431E-08, + 1.73740123037651E-08, + -4.45137302235032E-09, + 9.44721910524571E-09}, + {-6.83406949047909E-08, + -1.95046676795923E-10, + 2.57535903049686E-09, + 4.82643164083020E-09, + 3.37657333705158E-09}, + {3.96128688448981E-09, + -6.63809403270686E-10, + 2.44781464212534E-10, + 5.92280853590699E-11, + -4.78502591970721E-10}, + {1.75859399041414E-05, + -2.81238050668481E-06, + -2.43670534594848E-06, + 3.58244562699714E-06, + -1.76547446732691E-06}, + {-1.06451311473304E-07, + 1.54336689617184E-06, + -2.00690000442673E-07, + 1.38790047911880E-09, + -1.62490619890017E-07}, + {-2.72757421686155E-07, + 1.71139266205398E-07, + -2.55080309401917E-08, + -8.40793079489831E-09, + -1.01129447760167E-08}, + {2.92966025844079E-08, + -2.07556718857313E-08, + 5.45985315647905E-09, + 8.76857690274150E-09, + 1.06785510440474E-08}, + {-1.22059608941331E-08, + 6.52491630264276E-09, + -1.79332492326928E-10, + 3.75921793745396E-10, + -7.06416506254786E-10}, + {1.63224355776652E-09, + 4.95586028736232E-10, + -3.07879011759040E-10, + -7.78354087544277E-11, + 1.43959047067250E-10}, + {3.86319414653663E-10, + -2.06467134617933E-10, + 4.37330971382694E-11, + -5.00421056263711E-11, + -9.40237773015723E-12}, + {-1.23856142706451E-05, + 7.61047394008415E-06, + -1.99104114578138E-07, + 6.86177748886858E-07, + -1.09466747592827E-07}, + {2.99866062403128E-07, + 1.87525561397390E-07, + 4.99374806994715E-08, + 4.86229763781404E-07, + 4.46570575517658E-07}, + {-5.05748332368430E-07, + 1.95523624722285E-08, + -9.17535435911345E-08, + -2.56671607433547E-08, + -7.11896201616653E-08}, + {-2.66062200406494E-08, + -5.40470019739274E-09, + -2.29718660244954E-09, + -3.73328592264404E-09, + 3.38748313712376E-09}, + {5.30855327954894E-10, + 5.28851845648032E-10, + -2.22278913745418E-10, + -5.52628653064771E-11, + -9.24825145219684E-10}, + {6.03737227573716E-10, + -3.52190673510919E-12, + -1.30371720641414E-10, + -9.12787239944822E-12, + 6.42187285537238E-12}, + {1.78081862458539E-10, + 2.93772078656037E-12, + -1.04698379945322E-11, + -2.82260024833024E-11, + -5.61810459067525E-12}, + {9.35003092299580E-12, + -8.23133834521577E-13, + 5.54878414224198E-13, + -3.62943215777181E-13, + 2.38858933771653E-12}, + {-1.31216096107331E-05, + -5.70451670731759E-06, + -5.11598683573971E-06, + -4.99990779887599E-06, + 1.27389320221511E-07}, + {-1.23108260369048E-06, + 5.53093245213587E-07, + 8.60093183929302E-07, + 2.65569700925696E-07, + 1.95485134805575E-07}, + {-2.29647072638049E-07, + -5.45266515081825E-08, + 2.85298129762263E-08, + 1.98167939680185E-08, + 5.52227340898335E-09}, + {-2.73844745019857E-08, + -4.48345173291362E-10, + -1.93967347049382E-09, + -1.41508853776629E-09, + -1.75456962391145E-09}, + {-2.68863184376108E-11, + -2.20546981683293E-09, + 6.56116990576877E-10, + 1.27129855674922E-10, + -2.32334506413213E-10}, + {1.98303136881156E-10, + 6.04782006047075E-11, + 2.91291115431570E-11, + 6.18098615782757E-11, + -3.82682292530379E-11}, + {9.48294455071158E-12, + -3.05873596453015E-13, + 5.31539408055057E-13, + -7.31016438665600E-12, + -1.19921002209198E-11}, + {-2.25188050845725E-11, + -3.91627574966393E-13, + -6.80217235976769E-13, + 5.91033607278405E-13, + 5.02991534452191E-13}, + {1.29532063896247E-12, + 1.66337285851564E-13, + 3.25543028344555E-13, + 1.89143357962363E-13, + 3.32288378169726E-13}, + {-2.45864358781728E-06, + 4.49460524898260E-06, + 1.03890496648813E-06, + -2.73783420376785E-06, + 7.12695730642593E-07}, + {-9.27805078535168E-07, + -4.97733876686731E-07, + 9.18680298906510E-08, + -2.47200617423980E-07, + 6.16163630140379E-08}, + {-1.39623661883136E-08, + -1.12580495666505E-07, + 2.61821435950379E-08, + -2.31875562002885E-08, + 5.72679835033659E-08}, + {-9.52538983318497E-09, + -5.40909215302433E-09, + 1.88698793952475E-09, + -4.08127746406372E-09, + 1.09534895853812E-10}, + {3.79767457525741E-09, + 1.11549801373366E-10, + -6.45504957274111E-10, + 3.05477141010356E-10, + 1.26261210565856E-10}, + {5.08813577945300E-11, + 1.43250547678637E-11, + 8.81616572082448E-12, + 2.58968878880804E-11, + 3.83421818249954E-11}, + {8.95094368142044E-12, + -3.26220304555971E-12, + -1.28047847191896E-12, + 2.67562170258942E-12, + 2.72195031576670E-12}, + {-6.47181697409757E-12, + 1.13776457455685E-12, + 2.84856274334969E-13, + -7.63667272085395E-14, + -1.34451657758826E-13}, + {-1.25291265888343E-12, + 8.63500441050317E-14, + -1.21307856635548E-13, + 5.12570529540511E-14, + 3.32389276976573E-14}, + {3.73573418085813E-14, + -5.37808783042784E-16, + -4.23430408270850E-16, + -4.75110565740493E-15, + 6.02553212780166E-15}, + {8.95483987262751E-06, + -3.90778212666235E-06, + -1.12115019808259E-06, + 1.78678942093383E-06, + 1.46806344157962E-06}, + {-4.59185232678613E-07, + 1.09497995905419E-07, + 1.31663977640045E-07, + 4.20525791073626E-08, + -9.71470741607431E-08}, + {1.63399802579572E-07, + 1.50909360648645E-08, + -1.11480472593347E-08, + -1.84000857674573E-08, + 7.82124614794256E-09}, + {1.22887452385094E-08, + -4.06647399822746E-10, + -6.49120327585597E-10, + 8.63651225791194E-10, + -2.73440085913102E-09}, + {2.51748630889583E-09, + 4.79895880425564E-10, + -2.44908073860844E-10, + 2.56735882664876E-10, + -1.64815306286912E-10}, + {4.85671381736718E-11, + -2.51742732115131E-11, + -2.60819437993179E-11, + 6.12728324086123E-12, + 2.16833310896138E-11}, + {4.11389702320298E-12, + -8.09433180989935E-13, + -1.19812498226024E-12, + 1.46885737888520E-12, + 3.15807685137836E-12}, + {-1.47614580597013E-12, + 4.66726413909320E-13, + 1.72089709006255E-13, + 1.13854935381418E-13, + 2.77741161317003E-13}, + {-1.02257724967727E-13, + 1.10394382923502E-13, + -3.14153505370805E-15, + 2.41103099110106E-14, + 2.13853053149771E-14}, + {-3.19080885842786E-14, + -9.53904307973447E-15, + 2.74542788156379E-15, + 2.33797859107844E-15, + -2.53192474907304E-15}, + {-5.87702222126367E-15, + -1.80133850930249E-15, + -3.09793125614454E-16, + -1.04197538975295E-16, + 3.72781664701327E-16}, + {1.86187054729085E-06, + 8.33098045333428E-06, + 3.18277735484232E-06, + -7.68273797022231E-07, + -1.52337222261696E-06}, + {-5.07076646593648E-07, + -8.61959553442156E-07, + -3.51690005432816E-07, + -4.20797082902431E-07, + -3.07652993252673E-07}, + {-7.38992472164147E-08, + -8.39473083080280E-08, + -2.51587083298935E-08, + 7.30691259725451E-09, + -3.19457155958983E-08}, + {-1.99777182012924E-09, + -3.21265085916022E-09, + -4.84477421865675E-10, + -1.82924814205799E-09, + -3.46664344655997E-10}, + {-7.05788559634927E-11, + 1.21840735569025E-10, + 7.97347726425926E-11, + 1.08275679614409E-10, + -1.17891254809785E-10}, + {1.10299718947774E-11, + -3.22958261390263E-11, + -1.43535798209229E-11, + 6.87096504209595E-12, + -6.64963212272352E-12}, + {-6.47393639740084E-12, + 1.03156978325120E-12, + -9.20099775082358E-14, + -2.40150316641949E-13, + 1.14008812047857E-12}, + {-1.23957846397250E-13, + 2.85996703969692E-13, + 1.91579874982553E-13, + 5.20597174693064E-14, + -4.06741434883370E-14}, + {-2.35479068911236E-14, + 1.97847338186993E-14, + 1.58935977518516E-15, + -2.32217195254742E-15, + -8.48611789490575E-15}, + {1.03992320391626E-14, + 1.54017082092642E-15, + 1.05950035082788E-16, + -1.17870898461353E-15, + -1.10937420707372E-15}, + {-1.09011948374520E-15, + -6.04168007633584E-16, + -9.10901998157436E-17, + 1.98379116989461E-16, + -1.03715496658498E-16}, + {-1.38171942108278E-16, + -6.33037999097522E-17, + -1.38777695011470E-17, + 1.94191397045401E-17, + 5.70055906754485E-18}, + {1.92989406002085E-06, + -3.82662130483128E-06, + -4.60189561036048E-07, + 2.24290587856309E-06, + 1.40544379451550E-06}, + {6.49033717633394E-08, + 2.41396114435326E-07, + 2.73948898223321E-07, + 1.10633664439332E-07, + -3.19555270171075E-08}, + {-2.91988966963297E-08, + -6.03828192816571E-09, + 1.18462386444840E-08, + 1.32095545004128E-08, + -5.06572721528914E-09}, + {7.31079058474148E-09, + -8.42775299751834E-10, + 1.10190810090667E-09, + 1.96592273424306E-09, + -2.13135932785688E-09}, + {7.06656405314388E-11, + 1.43441125783756E-10, + 1.46962246686924E-10, + 7.44592776425197E-11, + -3.64331892799173E-11}, + {-2.52393942119372E-11, + 1.07520964869263E-11, + 5.84669886072094E-12, + 6.52029744217103E-12, + 1.82947123132059E-12}, + {-4.15669940115121E-12, + -1.95963254053648E-13, + 2.16977822834301E-13, + -2.84701408462031E-13, + 4.27194601040231E-13}, + {3.07891105454129E-13, + 1.91523190672955E-13, + 1.05367297580989E-13, + -5.28136363920236E-14, + -3.53364110005917E-14}, + {7.02156663274738E-15, + 9.52230536780849E-15, + -3.41019408682733E-15, + -3.59825303352899E-15, + -2.62576411636150E-15}, + {-1.75110277413804E-15, + 5.29265220719483E-16, + 4.45015980897919E-16, + -3.80179856341347E-16, + -4.32917763829695E-16}, + {1.16038609651443E-16, + -6.69643574373352E-17, + 2.65667154817303E-17, + -9.76010333683956E-17, + 4.07312981076655E-17}, + {5.72659246346386E-18, + 1.30357528108671E-18, + 2.49193258417535E-18, + 1.76247014075584E-18, + 7.59614374197688E-19}, + {1.03352170833303E-17, + -2.30633516638829E-18, + 2.84777940620193E-18, + -7.72161347944693E-19, + 6.07028034506380E-19} }; -const double anm_ch[91][5]= -{ - {0.0571481238161787,3.35402081801137E-05,3.15988141788728E-05,-1.34477341887086E-05,-2.61831023577773E-07}, - {5.77367395845715E-05,-0.000669057185209558,-6.51057691648904E-05,-1.61830149147091E-06,8.96771209464758E-05}, - {-8.50773002452907E-05,-4.87106614880272E-05,4.03431160775277E-05,2.54090162741464E-06,-5.59109319864264E-06}, - {0.00150536423187709,0.000611682258892697,0.000369730024614855,-1.95658439780282E-05,-3.46246726553700E-05}, - {-2.32168718433966E-05,-0.000127478686553809,-9.00292451740728E-05,-6.07834315901830E-05,-1.04628419422714E-05}, - {-1.38607250922551E-06,-3.97271603842309E-06,-8.16155320152118E-07,5.73266706046665E-07,2.00366060212696E-07}, - {6.52491559188663E-05,-0.00112224323460183,-0.000344967958304075,-7.67282640947300E-05,0.000107907110551939}, - {-0.000138870461448036,-7.29995695401936E-05,5.35986591445824E-05,9.03804869703890E-06,8.61370129482732E-06}, - {-9.98524443968768E-07,-6.84966792665998E-08,1.47478021860771E-07,1.94857794008064E-06,7.17176852732910E-07}, - {1.27066367911720E-06,1.12113289164288E-06,2.71525688515375E-07,-2.76125723009239E-07,-1.05429690305013E-07}, - {-0.000377264999981652,0.000262691217024294,0.000183639785837590,3.93177048515576E-06,-6.66187081899168E-06}, - {-4.93720951871921E-05,-0.000102820030405771,-5.69904376301748E-05,-3.79603438055116E-05,-3.96726017834930E-06}, - {-2.21881958961135E-06,-1.40207117987894E-06,1.60956630798516E-07,2.06121145135022E-06,6.50944708093149E-07}, - {2.21876332411271E-07,1.92272880430386E-07,-6.44016558013941E-09,-1.40954921332410E-07,-4.26742169137667E-07}, - {-3.51738525149881E-08,2.89616194332516E-08,-3.40343352397886E-08,-2.89763392721812E-08,-6.40980581663785E-10}, - {3.51240856823468E-05,-0.000725895015345786,-0.000322514037108045,-0.000106143759981636,4.08153152459337E-05}, - {-2.36269716929413E-05,-4.20691836557932E-05,1.43926743222922E-05,2.61811210631784E-05,2.09610762194903E-05}, - {-7.91765756673890E-07,1.64556789159745E-06,-9.43930166276555E-07,6.46641738736139E-07,-5.91509547299176E-07}, - {3.92768838766879E-07,-1.98027731703690E-07,-5.41303590057253E-08,-4.21705797874207E-07,-6.06042329660681E-08}, - {-1.56650141024305E-08,7.61808165752027E-08,-1.81900460250934E-08,1.30196216971675E-08,1.08616031342379E-08}, - {-2.80964779829242E-08,-7.25951488826103E-09,-2.59789823306225E-09,-2.79271942407154E-09,4.10558774868586E-09}, - {-0.000638227857648286,-0.000154814045363391,7.78518327501759E-05,-2.95961469342381E-05,1.15965225055757E-06}, - {4.47833146915112E-06,1.33712284237555E-05,3.61048816552123E-06,-2.50717844073547E-06,-1.28100822021734E-05}, - {-2.26958070007455E-06,2.57779960912242E-06,1.08395653197976E-06,1.29403393862805E-07,-1.04854652812567E-06}, - {-3.98954043463392E-07,-2.26931182815454E-07,-1.09169545045028E-07,-1.49509536031939E-07,-3.98376793949903E-07}, - {2.30418911071110E-08,1.23098508481555E-08,-1.71161401463708E-08,2.35829696577657E-09,1.31136164162040E-08}, - {3.69423793101582E-09,3.49231027561927E-10,-1.18581468768647E-09,5.43180735828820E-10,5.43192337651588E-10}, - {-1.38608847117992E-09,-1.86719145546559E-10,-8.13477384765498E-10,2.01919878240491E-10,1.00067892622287E-10}, - {-4.35499078415956E-05,0.000450727967957804,0.000328978494268850,-3.05249478582848E-05,-3.21914834544310E-05}, - {1.24887940973241E-05,1.34275239548403E-05,1.11275518344713E-06,7.46733554562851E-06,-2.12458664760353E-06}, - {9.50250784948476E-07,2.34367372695203E-06,-5.43099244798980E-07,-4.35196904508734E-07,-8.31852234345897E-07}, - {5.91775478636535E-09,-1.48970922508592E-07,2.99840061173840E-08,-1.30595933407792E-07,1.27136765045597E-07}, - {-1.78491083554475E-08,1.76864919393085E-08,-1.96740493482011E-08,1.21096708004261E-08,2.95518703155064E-10}, - {1.75053510088658E-09,-1.31414287871615E-09,-1.44689439791928E-09,1.14682483668460E-09,1.74488616540169E-09}, - {1.08152964586251E-09,-3.85678162063266E-10,-2.77851016629979E-10,3.89890578625590E-11,-2.54627365853495E-10}, - {-1.88340955578221E-10,5.19645384002867E-11,2.14131326027631E-11,1.24027770392728E-11,-9.42818962431967E-12}, - {0.000359777729843898,-0.000111692619996219,-6.87103418744904E-05,0.000115128973879551,7.59796247722486E-05}, - {5.23717968000879E-05,1.32279078116467E-05,-5.72277317139479E-07,-7.56326558610214E-06,-1.95749622214651E-05}, - {1.00109213210139E-06,-2.75515216592735E-07,-1.13393194050846E-06,-4.75049734870663E-07,-3.21499480530932E-07}, - {-2.07013716598890E-07,-7.31392258077707E-08,-3.96445714084160E-08,3.21390452929387E-08,-1.43738764991525E-08}, - {2.03081434931767E-09,-1.35423687136122E-08,-4.47637454261816E-09,2.18409121726643E-09,-3.74845286805217E-09}, - {3.17469255318367E-09,2.44221027314129E-10,-2.46820614760019E-10,7.55851003884434E-10,6.98980592550891E-10}, - {9.89541493531067E-11,-2.78762878057315E-11,-2.10947962916771E-10,3.77882267360636E-11,-1.20009542671532E-12}, - {5.01720575730940E-11,1.66470417102135E-11,-7.50624817938091E-12,9.97880221482238E-12,4.87141864438892E-12}, - {2.53137945301589E-11,1.93030083090772E-12,-1.44708804231290E-12,-1.77837100743423E-12,-8.10068935490951E-13}, - {0.000115735341520738,0.000116910591048350,8.36315620479475E-05,1.61095702669207E-05,-7.53084853489862E-05}, - {-9.76879433427199E-06,9.16968438003335E-06,-8.72755127288830E-06,-1.30077933880053E-05,-9.78841937993320E-06}, - {1.04902782517565E-07,2.14036988364936E-07,-7.19358686652888E-07,1.12529592946332E-07,7.07316352860448E-07}, - {7.63177265285080E-08,1.22781974434290E-07,8.99971272969286E-08,5.63482239352990E-08,4.31054352285547E-08}, - {3.29855763107355E-09,-6.95004336734441E-09,-6.52491370576354E-09,1.97749180391742E-09,3.51941791940498E-09}, - {3.85373745846559E-10,1.65754130924183E-10,-3.31326088103057E-10,5.93256024580436E-10,1.27725220636915E-10}, - {-1.08840956376565E-10,-4.56042860268189E-11,-4.77254322645633E-12,-2.94405398621875E-12,-3.07199979999475E-11}, - {2.07389879095010E-11,1.51186798732451E-11,9.28139802941848E-12,5.92738269687687E-12,9.70337402306505E-13}, - {-2.85879708060306E-12,1.92164314717053E-13,4.02664678967890E-14,5.18246319204277E-13,-7.91438726419423E-13}, - {6.91890667590734E-13,-8.49442290988352E-14,-5.54404947212402E-15,9.71093377538790E-15,-5.33714333415971E-14}, - {-5.06132972789792E-05,-4.28348772058883E-05,-6.90746551020305E-05,8.48380415176836E-05,7.04135614675053E-05}, - {-1.27945598849788E-05,-1.92362865537803E-05,-2.30971771867138E-06,-8.98515975724166E-06,5.25675205004752E-06}, - {-8.71907027470177E-07,-1.02091512861164E-06,-1.69548051683864E-07,4.87239045855761E-07,9.13163249899837E-07}, - {-6.23651943425918E-08,6.98993315829649E-08,5.91597766733390E-08,4.36227124230661E-08,6.45321798431575E-08}, - {-1.46315079552637E-10,-7.85142670184337E-09,1.48788168857903E-09,2.16870499912160E-09,-1.16723047065545E-09}, - {3.31888494450352E-10,1.90931898336457E-10,-3.13671901557599E-11,2.60711798190524E-10,8.45240112207997E-11}, - {1.36645682588537E-11,-5.68830303783976E-12,1.57518923848140E-11,-1.61935794656758E-11,-4.16568077748351E-12}, - {9.44684950971905E-13,7.30313977131995E-12,3.14451447892684E-12,6.49029875639842E-13,-9.66911019905919E-13}, - {-8.13097374090024E-13,5.23351897822186E-13,8.94349188113951E-14,-1.33327759673270E-13,-4.04549450989029E-13}, - {-3.76176467005839E-14,-6.19953702289713E-14,-3.74537190139726E-14,1.71275486301958E-14,-3.81946773167132E-14}, - {-4.81393385544160E-14,3.66084990006325E-15,3.10432030972253E-15,-4.10964475657416E-15,-6.58644244242900E-15}, - {-7.81077363746945E-05,-0.000254773632197303,-0.000214538508009518,-3.80780934346726E-05,1.83495359193990E-05}, - {5.89140224113144E-06,-3.17312632433258E-06,-3.81872516710791E-06,-2.27592226861647E-06,1.57044619888023E-06}, - {-1.44272505088690E-06,-1.10236588903758E-07,2.64336813084693E-07,4.76074163332460E-07,4.28623587694570E-07}, - {3.98889120733904E-08,-1.29638005554027E-08,-4.13668481273828E-08,1.27686793719542E-09,-3.54202962042383E-08}, - {1.60726837551750E-09,-2.70750776726156E-09,2.79387092681070E-09,-3.01419734793998E-10,-1.29101669438296E-10}, - {-2.55708290234943E-10,2.27878015173471E-11,-6.43063443462716E-12,1.26531554846856E-10,-1.65822147437220E-10}, - {-3.35886470557484E-11,-3.51895009091595E-12,5.80698399963198E-12,-2.84881487149207E-12,8.91708061745902E-12}, - {-3.12788523950588E-12,3.35366912964637E-12,2.52236848033838E-12,-8.12801050709184E-13,-2.63510394773892E-13}, - {6.83791881183142E-14,2.41583263270381E-13,8.58807794189356E-14,-5.12528492761045E-14,-1.40961725631276E-13}, - {-1.28585349115321E-14,-2.11049721804969E-14,5.26409596614749E-15,-4.31736582588616E-15,-1.60991602619068E-14}, - {-9.35623261461309E-15,-3.94384886372442E-16,5.04633016896942E-16,-5.40268998456055E-16,-1.07857944298104E-15}, - {8.79756791888023E-16,4.52529935675330E-16,1.36886341163227E-16,-1.12984402980452E-16,6.30354561057224E-18}, - {0.000117829256884757,2.67013591698442E-05,2.57913446775250E-05,-4.40766244878807E-05,-1.60651761172523E-06}, - {-1.87058092029105E-05,1.34371169060024E-05,5.59131416451555E-06,4.50960364635647E-06,2.87612873904633E-06}, - {2.79835536517287E-07,8.93092708148293E-07,8.37294601021795E-07,-1.99029785860896E-08,-8.87240405168977E-08}, - {4.95854313394905E-08,-1.44694570735912E-08,2.51662229339375E-08,-3.87086600452258E-09,2.29741919071270E-08}, - {4.71497840986162E-09,2.47509999454076E-09,1.67323845102824E-09,8.14196768283530E-10,-3.71467396944165E-10}, - {-1.07340743907054E-10,-8.07691657949326E-11,-5.99381660248133E-11,2.33173929639378E-12,-2.26994195544563E-11}, - {-3.83130441984224E-11,-5.82499946138714E-12,1.43286311435124E-11,3.15150503353387E-12,5.97891025146774E-12}, - {-5.64389191072230E-13,9.57258316335954E-13,1.12055192185939E-12,-4.42417706775420E-13,-9.93190361616481E-13}, - {1.78188860269677E-13,7.82582024904950E-14,5.18061650118009E-14,2.13456507353387E-14,-5.26202113779510E-14}, - {-8.18481324740893E-15,-3.71256746886786E-15,4.23508855164371E-16,-2.91292502923102E-15,-1.15454205389350E-14}, - {6.16578691696810E-15,6.74087154080877E-16,5.71628946437034E-16,-2.05251213979975E-16,-7.25999138903781E-16}, - {9.35481959699383E-17,6.23535830498083E-17,3.18076728802060E-18,-2.92353209354587E-17,7.65216088665263E-19}, - {2.34173078531701E-17,-8.30342420281772E-18,-4.33602329912952E-18,1.90226281379981E-18,-7.85507922718903E-19} +const double anm_ch[91][5] = { + {0.0571481238161787, + 3.35402081801137E-05, + 3.15988141788728E-05, + -1.34477341887086E-05, + -2.61831023577773E-07}, + {5.77367395845715E-05, + -0.000669057185209558, + -6.51057691648904E-05, + -1.61830149147091E-06, + 8.96771209464758E-05}, + {-8.50773002452907E-05, + -4.87106614880272E-05, + 4.03431160775277E-05, + 2.54090162741464E-06, + -5.59109319864264E-06}, + {0.00150536423187709, + 0.000611682258892697, + 0.000369730024614855, + -1.95658439780282E-05, + -3.46246726553700E-05}, + {-2.32168718433966E-05, + -0.000127478686553809, + -9.00292451740728E-05, + -6.07834315901830E-05, + -1.04628419422714E-05}, + {-1.38607250922551E-06, + -3.97271603842309E-06, + -8.16155320152118E-07, + 5.73266706046665E-07, + 2.00366060212696E-07}, + {6.52491559188663E-05, + -0.00112224323460183, + -0.000344967958304075, + -7.67282640947300E-05, + 0.000107907110551939}, + {-0.000138870461448036, + -7.29995695401936E-05, + 5.35986591445824E-05, + 9.03804869703890E-06, + 8.61370129482732E-06}, + {-9.98524443968768E-07, + -6.84966792665998E-08, + 1.47478021860771E-07, + 1.94857794008064E-06, + 7.17176852732910E-07}, + {1.27066367911720E-06, + 1.12113289164288E-06, + 2.71525688515375E-07, + -2.76125723009239E-07, + -1.05429690305013E-07}, + {-0.000377264999981652, + 0.000262691217024294, + 0.000183639785837590, + 3.93177048515576E-06, + -6.66187081899168E-06}, + {-4.93720951871921E-05, + -0.000102820030405771, + -5.69904376301748E-05, + -3.79603438055116E-05, + -3.96726017834930E-06}, + {-2.21881958961135E-06, + -1.40207117987894E-06, + 1.60956630798516E-07, + 2.06121145135022E-06, + 6.50944708093149E-07}, + {2.21876332411271E-07, + 1.92272880430386E-07, + -6.44016558013941E-09, + -1.40954921332410E-07, + -4.26742169137667E-07}, + {-3.51738525149881E-08, + 2.89616194332516E-08, + -3.40343352397886E-08, + -2.89763392721812E-08, + -6.40980581663785E-10}, + {3.51240856823468E-05, + -0.000725895015345786, + -0.000322514037108045, + -0.000106143759981636, + 4.08153152459337E-05}, + {-2.36269716929413E-05, + -4.20691836557932E-05, + 1.43926743222922E-05, + 2.61811210631784E-05, + 2.09610762194903E-05}, + {-7.91765756673890E-07, + 1.64556789159745E-06, + -9.43930166276555E-07, + 6.46641738736139E-07, + -5.91509547299176E-07}, + {3.92768838766879E-07, + -1.98027731703690E-07, + -5.41303590057253E-08, + -4.21705797874207E-07, + -6.06042329660681E-08}, + {-1.56650141024305E-08, + 7.61808165752027E-08, + -1.81900460250934E-08, + 1.30196216971675E-08, + 1.08616031342379E-08}, + {-2.80964779829242E-08, + -7.25951488826103E-09, + -2.59789823306225E-09, + -2.79271942407154E-09, + 4.10558774868586E-09}, + {-0.000638227857648286, + -0.000154814045363391, + 7.78518327501759E-05, + -2.95961469342381E-05, + 1.15965225055757E-06}, + {4.47833146915112E-06, + 1.33712284237555E-05, + 3.61048816552123E-06, + -2.50717844073547E-06, + -1.28100822021734E-05}, + {-2.26958070007455E-06, + 2.57779960912242E-06, + 1.08395653197976E-06, + 1.29403393862805E-07, + -1.04854652812567E-06}, + {-3.98954043463392E-07, + -2.26931182815454E-07, + -1.09169545045028E-07, + -1.49509536031939E-07, + -3.98376793949903E-07}, + {2.30418911071110E-08, + 1.23098508481555E-08, + -1.71161401463708E-08, + 2.35829696577657E-09, + 1.31136164162040E-08}, + {3.69423793101582E-09, + 3.49231027561927E-10, + -1.18581468768647E-09, + 5.43180735828820E-10, + 5.43192337651588E-10}, + {-1.38608847117992E-09, + -1.86719145546559E-10, + -8.13477384765498E-10, + 2.01919878240491E-10, + 1.00067892622287E-10}, + {-4.35499078415956E-05, + 0.000450727967957804, + 0.000328978494268850, + -3.05249478582848E-05, + -3.21914834544310E-05}, + {1.24887940973241E-05, + 1.34275239548403E-05, + 1.11275518344713E-06, + 7.46733554562851E-06, + -2.12458664760353E-06}, + {9.50250784948476E-07, + 2.34367372695203E-06, + -5.43099244798980E-07, + -4.35196904508734E-07, + -8.31852234345897E-07}, + {5.91775478636535E-09, + -1.48970922508592E-07, + 2.99840061173840E-08, + -1.30595933407792E-07, + 1.27136765045597E-07}, + {-1.78491083554475E-08, + 1.76864919393085E-08, + -1.96740493482011E-08, + 1.21096708004261E-08, + 2.95518703155064E-10}, + {1.75053510088658E-09, + -1.31414287871615E-09, + -1.44689439791928E-09, + 1.14682483668460E-09, + 1.74488616540169E-09}, + {1.08152964586251E-09, + -3.85678162063266E-10, + -2.77851016629979E-10, + 3.89890578625590E-11, + -2.54627365853495E-10}, + {-1.88340955578221E-10, + 5.19645384002867E-11, + 2.14131326027631E-11, + 1.24027770392728E-11, + -9.42818962431967E-12}, + {0.000359777729843898, + -0.000111692619996219, + -6.87103418744904E-05, + 0.000115128973879551, + 7.59796247722486E-05}, + {5.23717968000879E-05, + 1.32279078116467E-05, + -5.72277317139479E-07, + -7.56326558610214E-06, + -1.95749622214651E-05}, + {1.00109213210139E-06, + -2.75515216592735E-07, + -1.13393194050846E-06, + -4.75049734870663E-07, + -3.21499480530932E-07}, + {-2.07013716598890E-07, + -7.31392258077707E-08, + -3.96445714084160E-08, + 3.21390452929387E-08, + -1.43738764991525E-08}, + {2.03081434931767E-09, + -1.35423687136122E-08, + -4.47637454261816E-09, + 2.18409121726643E-09, + -3.74845286805217E-09}, + {3.17469255318367E-09, + 2.44221027314129E-10, + -2.46820614760019E-10, + 7.55851003884434E-10, + 6.98980592550891E-10}, + {9.89541493531067E-11, + -2.78762878057315E-11, + -2.10947962916771E-10, + 3.77882267360636E-11, + -1.20009542671532E-12}, + {5.01720575730940E-11, + 1.66470417102135E-11, + -7.50624817938091E-12, + 9.97880221482238E-12, + 4.87141864438892E-12}, + {2.53137945301589E-11, + 1.93030083090772E-12, + -1.44708804231290E-12, + -1.77837100743423E-12, + -8.10068935490951E-13}, + {0.000115735341520738, + 0.000116910591048350, + 8.36315620479475E-05, + 1.61095702669207E-05, + -7.53084853489862E-05}, + {-9.76879433427199E-06, + 9.16968438003335E-06, + -8.72755127288830E-06, + -1.30077933880053E-05, + -9.78841937993320E-06}, + {1.04902782517565E-07, + 2.14036988364936E-07, + -7.19358686652888E-07, + 1.12529592946332E-07, + 7.07316352860448E-07}, + {7.63177265285080E-08, + 1.22781974434290E-07, + 8.99971272969286E-08, + 5.63482239352990E-08, + 4.31054352285547E-08}, + {3.29855763107355E-09, + -6.95004336734441E-09, + -6.52491370576354E-09, + 1.97749180391742E-09, + 3.51941791940498E-09}, + {3.85373745846559E-10, + 1.65754130924183E-10, + -3.31326088103057E-10, + 5.93256024580436E-10, + 1.27725220636915E-10}, + {-1.08840956376565E-10, + -4.56042860268189E-11, + -4.77254322645633E-12, + -2.94405398621875E-12, + -3.07199979999475E-11}, + {2.07389879095010E-11, + 1.51186798732451E-11, + 9.28139802941848E-12, + 5.92738269687687E-12, + 9.70337402306505E-13}, + {-2.85879708060306E-12, + 1.92164314717053E-13, + 4.02664678967890E-14, + 5.18246319204277E-13, + -7.91438726419423E-13}, + {6.91890667590734E-13, + -8.49442290988352E-14, + -5.54404947212402E-15, + 9.71093377538790E-15, + -5.33714333415971E-14}, + {-5.06132972789792E-05, + -4.28348772058883E-05, + -6.90746551020305E-05, + 8.48380415176836E-05, + 7.04135614675053E-05}, + {-1.27945598849788E-05, + -1.92362865537803E-05, + -2.30971771867138E-06, + -8.98515975724166E-06, + 5.25675205004752E-06}, + {-8.71907027470177E-07, + -1.02091512861164E-06, + -1.69548051683864E-07, + 4.87239045855761E-07, + 9.13163249899837E-07}, + {-6.23651943425918E-08, + 6.98993315829649E-08, + 5.91597766733390E-08, + 4.36227124230661E-08, + 6.45321798431575E-08}, + {-1.46315079552637E-10, + -7.85142670184337E-09, + 1.48788168857903E-09, + 2.16870499912160E-09, + -1.16723047065545E-09}, + {3.31888494450352E-10, + 1.90931898336457E-10, + -3.13671901557599E-11, + 2.60711798190524E-10, + 8.45240112207997E-11}, + {1.36645682588537E-11, + -5.68830303783976E-12, + 1.57518923848140E-11, + -1.61935794656758E-11, + -4.16568077748351E-12}, + {9.44684950971905E-13, + 7.30313977131995E-12, + 3.14451447892684E-12, + 6.49029875639842E-13, + -9.66911019905919E-13}, + {-8.13097374090024E-13, + 5.23351897822186E-13, + 8.94349188113951E-14, + -1.33327759673270E-13, + -4.04549450989029E-13}, + {-3.76176467005839E-14, + -6.19953702289713E-14, + -3.74537190139726E-14, + 1.71275486301958E-14, + -3.81946773167132E-14}, + {-4.81393385544160E-14, + 3.66084990006325E-15, + 3.10432030972253E-15, + -4.10964475657416E-15, + -6.58644244242900E-15}, + {-7.81077363746945E-05, + -0.000254773632197303, + -0.000214538508009518, + -3.80780934346726E-05, + 1.83495359193990E-05}, + {5.89140224113144E-06, + -3.17312632433258E-06, + -3.81872516710791E-06, + -2.27592226861647E-06, + 1.57044619888023E-06}, + {-1.44272505088690E-06, + -1.10236588903758E-07, + 2.64336813084693E-07, + 4.76074163332460E-07, + 4.28623587694570E-07}, + {3.98889120733904E-08, + -1.29638005554027E-08, + -4.13668481273828E-08, + 1.27686793719542E-09, + -3.54202962042383E-08}, + {1.60726837551750E-09, + -2.70750776726156E-09, + 2.79387092681070E-09, + -3.01419734793998E-10, + -1.29101669438296E-10}, + {-2.55708290234943E-10, + 2.27878015173471E-11, + -6.43063443462716E-12, + 1.26531554846856E-10, + -1.65822147437220E-10}, + {-3.35886470557484E-11, + -3.51895009091595E-12, + 5.80698399963198E-12, + -2.84881487149207E-12, + 8.91708061745902E-12}, + {-3.12788523950588E-12, + 3.35366912964637E-12, + 2.52236848033838E-12, + -8.12801050709184E-13, + -2.63510394773892E-13}, + {6.83791881183142E-14, + 2.41583263270381E-13, + 8.58807794189356E-14, + -5.12528492761045E-14, + -1.40961725631276E-13}, + {-1.28585349115321E-14, + -2.11049721804969E-14, + 5.26409596614749E-15, + -4.31736582588616E-15, + -1.60991602619068E-14}, + {-9.35623261461309E-15, + -3.94384886372442E-16, + 5.04633016896942E-16, + -5.40268998456055E-16, + -1.07857944298104E-15}, + {8.79756791888023E-16, + 4.52529935675330E-16, + 1.36886341163227E-16, + -1.12984402980452E-16, + 6.30354561057224E-18}, + {0.000117829256884757, + 2.67013591698442E-05, + 2.57913446775250E-05, + -4.40766244878807E-05, + -1.60651761172523E-06}, + {-1.87058092029105E-05, + 1.34371169060024E-05, + 5.59131416451555E-06, + 4.50960364635647E-06, + 2.87612873904633E-06}, + {2.79835536517287E-07, + 8.93092708148293E-07, + 8.37294601021795E-07, + -1.99029785860896E-08, + -8.87240405168977E-08}, + {4.95854313394905E-08, + -1.44694570735912E-08, + 2.51662229339375E-08, + -3.87086600452258E-09, + 2.29741919071270E-08}, + {4.71497840986162E-09, + 2.47509999454076E-09, + 1.67323845102824E-09, + 8.14196768283530E-10, + -3.71467396944165E-10}, + {-1.07340743907054E-10, + -8.07691657949326E-11, + -5.99381660248133E-11, + 2.33173929639378E-12, + -2.26994195544563E-11}, + {-3.83130441984224E-11, + -5.82499946138714E-12, + 1.43286311435124E-11, + 3.15150503353387E-12, + 5.97891025146774E-12}, + {-5.64389191072230E-13, + 9.57258316335954E-13, + 1.12055192185939E-12, + -4.42417706775420E-13, + -9.93190361616481E-13}, + {1.78188860269677E-13, + 7.82582024904950E-14, + 5.18061650118009E-14, + 2.13456507353387E-14, + -5.26202113779510E-14}, + {-8.18481324740893E-15, + -3.71256746886786E-15, + 4.23508855164371E-16, + -2.91292502923102E-15, + -1.15454205389350E-14}, + {6.16578691696810E-15, + 6.74087154080877E-16, + 5.71628946437034E-16, + -2.05251213979975E-16, + -7.25999138903781E-16}, + {9.35481959699383E-17, + 6.23535830498083E-17, + 3.18076728802060E-18, + -2.92353209354587E-17, + 7.65216088665263E-19}, + {2.34173078531701E-17, + -8.30342420281772E-18, + -4.33602329912952E-18, + 1.90226281379981E-18, + -7.85507922718903E-19} }; -const double anm_cw[91][5]= -{ - {0.0395329695826997,-0.000131114380761895,-0.000116331009006233,6.23548420410646E-05,5.72641113425116E-05}, - {-0.000441837640880650,0.000701288648654908,0.000338489802858270,3.76700309908602E-05,-8.70889013574699E-06}, - {1.30418530496887E-05,-0.000185046547597376,4.31032103066723E-05,0.000105583334124319,3.23045436993589E-05}, - {3.68918433448519E-05,-0.000219433014681503,3.46768613485000E-06,-9.17185187163528E-05,-3.69243242456081E-05}, - {-6.50227201116778E-06,2.07614874282187E-05,-5.09131314798362E-05,-3.08053225174359E-05,-4.18483655873918E-05}, - {2.67879176459056E-05,-6.89303730743691E-05,2.11046783217168E-06,1.93163912538178E-05,-1.97877143887704E-06}, - {0.000393937595007422,-0.000452948381236406,-0.000136517846073846,0.000138239247989489,0.000133175232977863}, - {5.00214539435002E-05,3.57229726719727E-05,-9.38010547535432E-07,-3.52586798317563E-05,-7.01218677681254E-06}, - {3.91965314099929E-05,1.02236686806489E-05,-1.95710695226022E-05,-5.93904795230695E-06,3.24339769876093E-06}, - {6.68158778290653E-06,-8.10468752307024E-06,-9.91192994096109E-06,-1.89755520007723E-07,-3.26799467595579E-06}, - {0.000314196817753895,-0.000296548447162009,-0.000218410153263575,-1.57318389871000E-05,4.69789570185785E-05}, - {0.000104597721123977,-3.31000119089319E-05,5.60326793626348E-05,4.71895007710715E-05,3.57432326236664E-05}, - {8.95483021572039E-06,1.44019305383365E-05,4.87912790492931E-06,-3.45826387853503E-06,3.23960320438157E-06}, - {-1.35249651009930E-05,-2.49349762695977E-06,-2.51509483521132E-06,-9.14254874104858E-07,-8.57897406100890E-07}, - {-1.68143325235195E-06,1.72073417594235E-06,1.38765993969565E-06,4.09770982137530E-07,-6.60908742097123E-07}, - {-0.000639889366487161,0.00120194042474696,0.000753258598887703,3.87356377414663E-05,1.31231811175345E-05}, - {2.77062763606783E-05,-9.51425270178477E-06,-6.61068056107547E-06,-1.38713669012109E-05,9.84662092961671E-06}, - {-2.69398078539471E-06,6.50860676783123E-06,3.80855926988090E-06,-1.98076068364785E-06,1.17187335666772E-06}, - {-2.63719028151905E-06,5.03149473656743E-07,7.38964893399716E-07,-8.38892485369078E-07,1.30943917775613E-06}, - {-1.56634992245479E-06,-2.97026487417045E-08,5.06602801102463E-08,-4.60436007958792E-08,-1.62536449440997E-07}, - {-2.37493912770935E-07,1.69781593069938E-08,8.35178275224265E-08,-4.83564044549811E-08,-4.96448864199318E-08}, - {0.00134012259587597,-0.000250989369253194,-2.97647945512547E-05,-6.47889968094926E-05,8.41302130716859E-05}, - {-0.000113287184900929,4.78918993866293E-05,-3.14572113583139E-05,-2.10518256626847E-05,-2.03933633847417E-05}, - {-4.97413321312139E-07,3.72599822034753E-06,-3.53221588399266E-06,-1.05232048036416E-06,-2.74821498198519E-06}, - {4.81988542428155E-06,4.21400219782474E-07,1.02814808667637E-06,4.40299068486188E-09,3.37103399036634E-09}, - {1.10140301678818E-08,1.90257670180182E-07,-1.00831353341885E-08,1.44860642389714E-08,-5.29882089987747E-08}, - {6.12420414245775E-08,-4.48953461152996E-09,-1.38837603709003E-08,-2.05533675904779E-08,1.49517908802329E-09}, - {9.17090243673643E-10,-9.24878857867367E-09,-2.30856560363943E-09,-4.36348789716735E-09,-4.45808881183025E-10}, - {-0.000424912699609112,-0.000114365438471564,-0.000403200981827193,4.19949560550194E-05,-3.02068483713739E-05}, - {3.85435472851225E-05,-5.70726887668306E-05,4.96313706308613E-07,1.02395703617082E-05,5.85550000567006E-06}, - {-7.38204470183331E-06,-4.56638770109511E-06,-3.94007992121367E-06,-2.16666812189101E-06,-4.55694264113194E-06}, - {5.89841165408527E-07,1.40862905173449E-08,1.08149086563211E-07,-2.18592601537944E-07,-3.78927431428119E-07}, - {4.85164687450468E-08,8.34273921293655E-08,1.47489605513673E-08,6.01494125001291E-08,6.43812884159484E-09}, - {1.13055580655363E-08,3.50568765400469E-09,-5.09396162501750E-09,-1.83362063152411E-09,-4.11227251553035E-09}, - {3.16454132867156E-09,-1.39634794131087E-09,-7.34085003895929E-10,-7.55541371271796E-10,-1.57568747643705E-10}, - {1.27572900992112E-09,-3.51625955080441E-10,-4.84132020565098E-10,1.52427274930711E-10,1.27466120431317E-10}, - {-0.000481655666236529,-0.000245423313903835,-0.000239499902816719,-0.000157132947351028,5.54583099258017E-05}, - {-1.52987254785589E-05,2.78383892116245E-05,4.32299123991860E-05,1.70981319744327E-05,-1.35090841769225E-06}, - {-8.65400907717798E-06,-6.51882656990376E-06,-2.43810171017369E-07,8.54348785752623E-07,2.98371863248143E-07}, - {-1.68155571776752E-06,-3.53602587563318E-07,-1.00404435881759E-07,-2.14162249012859E-08,-2.42131535531526E-07}, - {-1.08048603277187E-08,-9.78850785763030E-08,-2.32906554437417E-08,2.22003630858805E-08,-2.27230368089683E-09}, - {-5.98864391551041E-09,7.38970926486848E-09,3.61322835311957E-09,3.70037329172919E-09,-3.41121137081362E-09}, - {-7.33113754909726E-10,-9.08374249335220E-11,-1.78204392133739E-10,8.28618491929026E-11,-1.32966817912373E-10}, - {-5.23340481314676E-10,1.36403528233346E-10,-7.04478837151279E-11,-6.83175201536443E-12,-2.86040864071134E-12}, - {3.75347503578356E-11,-1.08518134138781E-11,-2.53583751744508E-12,1.00168232812303E-11,1.74929602713312E-11}, - {-0.000686805336370570,0.000591849814585706,0.000475117378328026,-2.59339398048415E-05,3.74825110514968E-05}, - {3.35231363034093E-05,2.38331521146909E-05,7.43545963794093E-06,-3.41430817541849E-06,7.20180957675353E-06}, - {3.60564374432978E-07,-3.13300039589662E-06,-6.38974746108020E-07,-8.63985524672024E-07,2.43367665208655E-06}, - {-4.09605238516094E-07,-2.51158699554904E-07,-1.29359217235188E-07,-2.27744642483133E-07,7.04065989970205E-08}, - {6.74886341820129E-08,-1.02009407061935E-08,-3.30790296448812E-08,1.64959795655031E-08,1.40641779998855E-08}, - {1.31706886235108E-09,-1.06243701278671E-09,-2.85573799673944E-09,3.72566568681289E-09,2.48402582003925E-09}, - {-3.68427463251097E-11,-1.90028122983781E-10,-3.98586561768697E-11,1.14458831693287E-11,-2.27722300377854E-12}, - {-7.90029729611056E-11,3.81213646526419E-11,4.63303426711788E-11,1.52294835905903E-11,-2.99094751490726E-12}, - {-2.36146602045017E-11,1.03852674709985E-11,-4.47242126307100E-12,5.30884113537806E-12,1.68499023262969E-12}, - {-3.30107358134527E-13,-4.73989085379655E-13,5.17199549822684E-13,2.34951744478255E-13,2.05931351608192E-13}, - {0.000430215687511780,-0.000132831373000014,-3.41830835017045E-05,4.70312161436033E-06,-3.84807179340006E-05}, - {1.66861163032403E-05,-8.10092908523550E-06,8.20658107437905E-06,6.12399025026683E-06,-1.85536495631911E-06}, - {1.53552093641337E-06,2.19486495660361E-06,-1.07253805120137E-06,-4.72141767909137E-07,4.00744581573216E-07}, - {2.56647305130757E-07,-8.07492046592274E-08,-2.05858469296168E-07,1.09784168930599E-07,-7.76823030181225E-08}, - {1.77744008115031E-08,1.64134677817420E-08,4.86163044879020E-09,1.13334251800856E-08,-7.17260621115426E-09}, - {1.61133063219326E-09,-1.85414677057024E-09,-2.13798537812651E-09,1.15255123229679E-09,2.24504700129464E-09}, - {1.23344223096739E-10,-1.20385012169848E-10,-2.18038256346433E-12,3.23033120628279E-11,8.01179568213400E-11}, - {-6.55745274387847E-12,1.22127104697198E-11,5.83805016355883E-12,-8.31201582509817E-12,1.90985373872656E-12}, - {-2.89199983667265E-12,5.05962500506667E-12,1.28092925110279E-12,5.60353813743813E-13,1.76753731968770E-12}, - {-1.61678729774956E-13,-3.92206170988615E-13,-9.04941327579237E-14,1.89847694200763E-13,4.10008676756463E-14}, - {-1.16808369005656E-13,-9.97464591430510E-14,7.46366550245722E-15,2.53398578153179E-14,1.06510689748906E-14}, - {-0.000113716921384790,-0.000131902722651488,-0.000162844886485788,7.90171538739454E-06,-0.000178768066961413}, - {-2.13146535366500E-06,-3.57818705543597E-05,-1.50825855069298E-05,-2.17909259570022E-05,-8.19332236308581E-06}, - {-2.88001138617357E-06,-2.09957465440793E-06,6.81466526687552E-08,3.58308906974448E-07,-4.18502067223724E-07}, - {-1.10761444317605E-07,6.91773860777929E-08,8.17125372450372E-08,-2.16476237959181E-08,7.59221970502074E-08}, - {-9.56994224818941E-09,6.64104921728432E-09,6.33077902928348E-09,2.85721181743727E-09,-6.39666681678123E-09}, - {4.62558627839842E-10,-1.69014863754621E-09,-2.80260429599733E-10,4.27558937623863E-11,-1.66926133269027E-10}, - {-7.23385132663753E-11,5.51961193545280E-11,3.04070791942335E-11,3.23227055919062E-12,8.47312431934829E-11}, - {-1.61189613765486E-11,1.66868155925172E-11,1.05370341694715E-11,-4.41495859079592E-12,-2.24939051401750E-12}, - {-8.72229568056267E-13,1.88613726203286E-12,1.21711137534390E-14,-1.13342372297867E-12,-6.87151975256052E-13}, - {7.99311988544090E-15,4.46150979586709E-14,7.50406779454998E-14,-3.20385428942275E-14,-1.26543636054393E-14}, - {4.80503817699514E-14,-3.35545623603729E-14,-1.18546423610485E-14,4.19419209985980E-15,-1.73525614436880E-14}, - {-1.20464898830163E-15,-8.80752065000456E-16,-1.22214298993313E-15,1.69928513019657E-15,1.93593051311405E-16}, - {1.68528879784841E-05,3.57144412031081E-05,-1.65999910125077E-05,5.40370336805755E-05,0.000118138122851376}, - {-3.28151779115881E-05,1.04231790790798E-05,-2.80761862890640E-06,2.98996152515593E-06,-2.67641158709985E-06}, - {-2.08664816151978E-06,-1.64463884697475E-06,6.79099429284834E-08,7.23955842946495E-07,-6.86378427465657E-07}, - {-2.88205823027255E-09,2.38319699493291E-09,1.14169347509045E-07,8.12981074994402E-08,-1.56957943666988E-07}, - {-7.09711403570189E-09,6.29470515502988E-09,3.50833306577579E-09,8.31289199649054E-09,-2.14221463168338E-09}, - {-8.11910123910038E-10,3.34047829618955E-10,3.70619377446490E-10,3.30426088213373E-10,4.86297305597865E-11}, - {1.98628160424161E-11,-4.98557831380098E-12,-5.90523187802174E-12,-1.27027116925122E-12,1.49982368570355E-11}, - {2.62289263262748E-12,3.91242360693861E-12,6.56035499387192E-12,-1.17412941089401E-12,-9.40878197853394E-13}, - {-3.37805010124487E-13,5.39454874299593E-13,-2.41569839991525E-13,-2.41572016820792E-13,-3.01983673057198E-13}, - {-1.85034053857964E-13,4.31132161871815E-14,4.13497222026824E-15,-4.60075514595980E-14,-1.92454846400146E-14}, - {2.96113888929854E-15,-1.11688534391626E-14,3.76275373238932E-15,-3.72593295948136E-15,1.98205490249604E-16}, - {1.40074667864629E-15,-5.15564234798333E-16,3.56287382196512E-16,5.07242777691587E-16,-2.30405782826134E-17}, - {2.96822530176851E-16,-4.77029898301223E-17,1.12782285532775E-16,1.58443229778573E-18,8.22141904662969E-17} +const double anm_cw[91][5] = { + {0.0395329695826997, + -0.000131114380761895, + -0.000116331009006233, + 6.23548420410646E-05, + 5.72641113425116E-05}, + {-0.000441837640880650, + 0.000701288648654908, + 0.000338489802858270, + 3.76700309908602E-05, + -8.70889013574699E-06}, + {1.30418530496887E-05, + -0.000185046547597376, + 4.31032103066723E-05, + 0.000105583334124319, + 3.23045436993589E-05}, + {3.68918433448519E-05, + -0.000219433014681503, + 3.46768613485000E-06, + -9.17185187163528E-05, + -3.69243242456081E-05}, + {-6.50227201116778E-06, + 2.07614874282187E-05, + -5.09131314798362E-05, + -3.08053225174359E-05, + -4.18483655873918E-05}, + {2.67879176459056E-05, + -6.89303730743691E-05, + 2.11046783217168E-06, + 1.93163912538178E-05, + -1.97877143887704E-06}, + {0.000393937595007422, + -0.000452948381236406, + -0.000136517846073846, + 0.000138239247989489, + 0.000133175232977863}, + {5.00214539435002E-05, + 3.57229726719727E-05, + -9.38010547535432E-07, + -3.52586798317563E-05, + -7.01218677681254E-06}, + {3.91965314099929E-05, + 1.02236686806489E-05, + -1.95710695226022E-05, + -5.93904795230695E-06, + 3.24339769876093E-06}, + {6.68158778290653E-06, + -8.10468752307024E-06, + -9.91192994096109E-06, + -1.89755520007723E-07, + -3.26799467595579E-06}, + {0.000314196817753895, + -0.000296548447162009, + -0.000218410153263575, + -1.57318389871000E-05, + 4.69789570185785E-05}, + {0.000104597721123977, + -3.31000119089319E-05, + 5.60326793626348E-05, + 4.71895007710715E-05, + 3.57432326236664E-05}, + {8.95483021572039E-06, + 1.44019305383365E-05, + 4.87912790492931E-06, + -3.45826387853503E-06, + 3.23960320438157E-06}, + {-1.35249651009930E-05, + -2.49349762695977E-06, + -2.51509483521132E-06, + -9.14254874104858E-07, + -8.57897406100890E-07}, + {-1.68143325235195E-06, + 1.72073417594235E-06, + 1.38765993969565E-06, + 4.09770982137530E-07, + -6.60908742097123E-07}, + {-0.000639889366487161, + 0.00120194042474696, + 0.000753258598887703, + 3.87356377414663E-05, + 1.31231811175345E-05}, + {2.77062763606783E-05, + -9.51425270178477E-06, + -6.61068056107547E-06, + -1.38713669012109E-05, + 9.84662092961671E-06}, + {-2.69398078539471E-06, + 6.50860676783123E-06, + 3.80855926988090E-06, + -1.98076068364785E-06, + 1.17187335666772E-06}, + {-2.63719028151905E-06, + 5.03149473656743E-07, + 7.38964893399716E-07, + -8.38892485369078E-07, + 1.30943917775613E-06}, + {-1.56634992245479E-06, + -2.97026487417045E-08, + 5.06602801102463E-08, + -4.60436007958792E-08, + -1.62536449440997E-07}, + {-2.37493912770935E-07, + 1.69781593069938E-08, + 8.35178275224265E-08, + -4.83564044549811E-08, + -4.96448864199318E-08}, + {0.00134012259587597, + -0.000250989369253194, + -2.97647945512547E-05, + -6.47889968094926E-05, + 8.41302130716859E-05}, + {-0.000113287184900929, + 4.78918993866293E-05, + -3.14572113583139E-05, + -2.10518256626847E-05, + -2.03933633847417E-05}, + {-4.97413321312139E-07, + 3.72599822034753E-06, + -3.53221588399266E-06, + -1.05232048036416E-06, + -2.74821498198519E-06}, + {4.81988542428155E-06, + 4.21400219782474E-07, + 1.02814808667637E-06, + 4.40299068486188E-09, + 3.37103399036634E-09}, + {1.10140301678818E-08, + 1.90257670180182E-07, + -1.00831353341885E-08, + 1.44860642389714E-08, + -5.29882089987747E-08}, + {6.12420414245775E-08, + -4.48953461152996E-09, + -1.38837603709003E-08, + -2.05533675904779E-08, + 1.49517908802329E-09}, + {9.17090243673643E-10, + -9.24878857867367E-09, + -2.30856560363943E-09, + -4.36348789716735E-09, + -4.45808881183025E-10}, + {-0.000424912699609112, + -0.000114365438471564, + -0.000403200981827193, + 4.19949560550194E-05, + -3.02068483713739E-05}, + {3.85435472851225E-05, + -5.70726887668306E-05, + 4.96313706308613E-07, + 1.02395703617082E-05, + 5.85550000567006E-06}, + {-7.38204470183331E-06, + -4.56638770109511E-06, + -3.94007992121367E-06, + -2.16666812189101E-06, + -4.55694264113194E-06}, + {5.89841165408527E-07, + 1.40862905173449E-08, + 1.08149086563211E-07, + -2.18592601537944E-07, + -3.78927431428119E-07}, + {4.85164687450468E-08, + 8.34273921293655E-08, + 1.47489605513673E-08, + 6.01494125001291E-08, + 6.43812884159484E-09}, + {1.13055580655363E-08, + 3.50568765400469E-09, + -5.09396162501750E-09, + -1.83362063152411E-09, + -4.11227251553035E-09}, + {3.16454132867156E-09, + -1.39634794131087E-09, + -7.34085003895929E-10, + -7.55541371271796E-10, + -1.57568747643705E-10}, + {1.27572900992112E-09, + -3.51625955080441E-10, + -4.84132020565098E-10, + 1.52427274930711E-10, + 1.27466120431317E-10}, + {-0.000481655666236529, + -0.000245423313903835, + -0.000239499902816719, + -0.000157132947351028, + 5.54583099258017E-05}, + {-1.52987254785589E-05, + 2.78383892116245E-05, + 4.32299123991860E-05, + 1.70981319744327E-05, + -1.35090841769225E-06}, + {-8.65400907717798E-06, + -6.51882656990376E-06, + -2.43810171017369E-07, + 8.54348785752623E-07, + 2.98371863248143E-07}, + {-1.68155571776752E-06, + -3.53602587563318E-07, + -1.00404435881759E-07, + -2.14162249012859E-08, + -2.42131535531526E-07}, + {-1.08048603277187E-08, + -9.78850785763030E-08, + -2.32906554437417E-08, + 2.22003630858805E-08, + -2.27230368089683E-09}, + {-5.98864391551041E-09, + 7.38970926486848E-09, + 3.61322835311957E-09, + 3.70037329172919E-09, + -3.41121137081362E-09}, + {-7.33113754909726E-10, + -9.08374249335220E-11, + -1.78204392133739E-10, + 8.28618491929026E-11, + -1.32966817912373E-10}, + {-5.23340481314676E-10, + 1.36403528233346E-10, + -7.04478837151279E-11, + -6.83175201536443E-12, + -2.86040864071134E-12}, + {3.75347503578356E-11, + -1.08518134138781E-11, + -2.53583751744508E-12, + 1.00168232812303E-11, + 1.74929602713312E-11}, + {-0.000686805336370570, + 0.000591849814585706, + 0.000475117378328026, + -2.59339398048415E-05, + 3.74825110514968E-05}, + {3.35231363034093E-05, + 2.38331521146909E-05, + 7.43545963794093E-06, + -3.41430817541849E-06, + 7.20180957675353E-06}, + {3.60564374432978E-07, + -3.13300039589662E-06, + -6.38974746108020E-07, + -8.63985524672024E-07, + 2.43367665208655E-06}, + {-4.09605238516094E-07, + -2.51158699554904E-07, + -1.29359217235188E-07, + -2.27744642483133E-07, + 7.04065989970205E-08}, + {6.74886341820129E-08, + -1.02009407061935E-08, + -3.30790296448812E-08, + 1.64959795655031E-08, + 1.40641779998855E-08}, + {1.31706886235108E-09, + -1.06243701278671E-09, + -2.85573799673944E-09, + 3.72566568681289E-09, + 2.48402582003925E-09}, + {-3.68427463251097E-11, + -1.90028122983781E-10, + -3.98586561768697E-11, + 1.14458831693287E-11, + -2.27722300377854E-12}, + {-7.90029729611056E-11, + 3.81213646526419E-11, + 4.63303426711788E-11, + 1.52294835905903E-11, + -2.99094751490726E-12}, + {-2.36146602045017E-11, + 1.03852674709985E-11, + -4.47242126307100E-12, + 5.30884113537806E-12, + 1.68499023262969E-12}, + {-3.30107358134527E-13, + -4.73989085379655E-13, + 5.17199549822684E-13, + 2.34951744478255E-13, + 2.05931351608192E-13}, + {0.000430215687511780, + -0.000132831373000014, + -3.41830835017045E-05, + 4.70312161436033E-06, + -3.84807179340006E-05}, + {1.66861163032403E-05, + -8.10092908523550E-06, + 8.20658107437905E-06, + 6.12399025026683E-06, + -1.85536495631911E-06}, + {1.53552093641337E-06, + 2.19486495660361E-06, + -1.07253805120137E-06, + -4.72141767909137E-07, + 4.00744581573216E-07}, + {2.56647305130757E-07, + -8.07492046592274E-08, + -2.05858469296168E-07, + 1.09784168930599E-07, + -7.76823030181225E-08}, + {1.77744008115031E-08, + 1.64134677817420E-08, + 4.86163044879020E-09, + 1.13334251800856E-08, + -7.17260621115426E-09}, + {1.61133063219326E-09, + -1.85414677057024E-09, + -2.13798537812651E-09, + 1.15255123229679E-09, + 2.24504700129464E-09}, + {1.23344223096739E-10, + -1.20385012169848E-10, + -2.18038256346433E-12, + 3.23033120628279E-11, + 8.01179568213400E-11}, + {-6.55745274387847E-12, + 1.22127104697198E-11, + 5.83805016355883E-12, + -8.31201582509817E-12, + 1.90985373872656E-12}, + {-2.89199983667265E-12, + 5.05962500506667E-12, + 1.28092925110279E-12, + 5.60353813743813E-13, + 1.76753731968770E-12}, + {-1.61678729774956E-13, + -3.92206170988615E-13, + -9.04941327579237E-14, + 1.89847694200763E-13, + 4.10008676756463E-14}, + {-1.16808369005656E-13, + -9.97464591430510E-14, + 7.46366550245722E-15, + 2.53398578153179E-14, + 1.06510689748906E-14}, + {-0.000113716921384790, + -0.000131902722651488, + -0.000162844886485788, + 7.90171538739454E-06, + -0.000178768066961413}, + {-2.13146535366500E-06, + -3.57818705543597E-05, + -1.50825855069298E-05, + -2.17909259570022E-05, + -8.19332236308581E-06}, + {-2.88001138617357E-06, + -2.09957465440793E-06, + 6.81466526687552E-08, + 3.58308906974448E-07, + -4.18502067223724E-07}, + {-1.10761444317605E-07, + 6.91773860777929E-08, + 8.17125372450372E-08, + -2.16476237959181E-08, + 7.59221970502074E-08}, + {-9.56994224818941E-09, + 6.64104921728432E-09, + 6.33077902928348E-09, + 2.85721181743727E-09, + -6.39666681678123E-09}, + {4.62558627839842E-10, + -1.69014863754621E-09, + -2.80260429599733E-10, + 4.27558937623863E-11, + -1.66926133269027E-10}, + {-7.23385132663753E-11, + 5.51961193545280E-11, + 3.04070791942335E-11, + 3.23227055919062E-12, + 8.47312431934829E-11}, + {-1.61189613765486E-11, + 1.66868155925172E-11, + 1.05370341694715E-11, + -4.41495859079592E-12, + -2.24939051401750E-12}, + {-8.72229568056267E-13, + 1.88613726203286E-12, + 1.21711137534390E-14, + -1.13342372297867E-12, + -6.87151975256052E-13}, + {7.99311988544090E-15, + 4.46150979586709E-14, + 7.50406779454998E-14, + -3.20385428942275E-14, + -1.26543636054393E-14}, + {4.80503817699514E-14, + -3.35545623603729E-14, + -1.18546423610485E-14, + 4.19419209985980E-15, + -1.73525614436880E-14}, + {-1.20464898830163E-15, + -8.80752065000456E-16, + -1.22214298993313E-15, + 1.69928513019657E-15, + 1.93593051311405E-16}, + {1.68528879784841E-05, + 3.57144412031081E-05, + -1.65999910125077E-05, + 5.40370336805755E-05, + 0.000118138122851376}, + {-3.28151779115881E-05, + 1.04231790790798E-05, + -2.80761862890640E-06, + 2.98996152515593E-06, + -2.67641158709985E-06}, + {-2.08664816151978E-06, + -1.64463884697475E-06, + 6.79099429284834E-08, + 7.23955842946495E-07, + -6.86378427465657E-07}, + {-2.88205823027255E-09, + 2.38319699493291E-09, + 1.14169347509045E-07, + 8.12981074994402E-08, + -1.56957943666988E-07}, + {-7.09711403570189E-09, + 6.29470515502988E-09, + 3.50833306577579E-09, + 8.31289199649054E-09, + -2.14221463168338E-09}, + {-8.11910123910038E-10, + 3.34047829618955E-10, + 3.70619377446490E-10, + 3.30426088213373E-10, + 4.86297305597865E-11}, + {1.98628160424161E-11, + -4.98557831380098E-12, + -5.90523187802174E-12, + -1.27027116925122E-12, + 1.49982368570355E-11}, + {2.62289263262748E-12, + 3.91242360693861E-12, + 6.56035499387192E-12, + -1.17412941089401E-12, + -9.40878197853394E-13}, + {-3.37805010124487E-13, + 5.39454874299593E-13, + -2.41569839991525E-13, + -2.41572016820792E-13, + -3.01983673057198E-13}, + {-1.85034053857964E-13, + 4.31132161871815E-14, + 4.13497222026824E-15, + -4.60075514595980E-14, + -1.92454846400146E-14}, + {2.96113888929854E-15, + -1.11688534391626E-14, + 3.76275373238932E-15, + -3.72593295948136E-15, + 1.98205490249604E-16}, + {1.40074667864629E-15, + -5.15564234798333E-16, + 3.56287382196512E-16, + 5.07242777691587E-16, + -2.30405782826134E-17}, + {2.96822530176851E-16, + -4.77029898301223E-17, + 1.12782285532775E-16, + 1.58443229778573E-18, + 8.22141904662969E-17} }; -const double bnm_bh[91][5]= -{ - {0,0,0,0,0}, - {0,0,0,0,0}, - {-2.29210587053658E-06,-2.33805004374529E-06,-7.49312880102168E-07,-5.12022747852006E-07,5.88926055066172E-07}, - {0,0,0,0,0}, - {-4.63382754843690E-06,-2.23853015662938E-06,8.14830531656518E-07,1.15453269407116E-06,-4.53555450927571E-07}, - {-6.92432096320778E-07,-2.98734455136141E-07,1.48085153955641E-08,1.37881746148773E-07,-6.92492118460215E-09}, - {0,0,0,0,0}, - {-1.91507979850310E-06,-1.83614825459598E-06,-7.46807436870647E-07,-1.28329122348007E-06,5.04937180063059E-07}, - {-8.07527103916713E-07,2.83997840574570E-08,-6.01890498063025E-08,-2.48339507554546E-08,2.46284627824308E-08}, - {-2.82995069303093E-07,1.38818274596408E-09,3.22731214161408E-09,2.87731153972404E-10,1.53895537278496E-08}, - {0,0,0,0,0}, - {-6.68210270956800E-07,-2.19104833297845E-06,1.30116691657253E-07,4.78445730433450E-07,-4.40344300914051E-07}, - {-2.36946755740436E-07,-1.32730991878204E-07,1.83669593693860E-08,7.90218931983569E-08,-4.70161979232584E-08}, - {1.07746083292179E-07,-4.17088637760330E-09,-1.83296035841109E-09,-5.80243971371211E-09,-2.11682361167439E-09}, - {-5.44712355496109E-08,1.89717032256923E-09,2.27327316287804E-10,7.78400728280038E-10,8.82380487618991E-12}, - {0,0,0,0,0}, - {-5.61707049615673E-08,-1.09066447089585E-06,-2.25742250174119E-07,-8.64367795924377E-07,1.06411275240680E-08}, - {2.41782935157918E-08,-3.65762298303819E-08,-6.93420659586875E-08,-3.97316214341991E-08,-2.08767816486390E-08}, - {6.38293030383436E-08,1.11377936334470E-08,6.91424941454782E-09,1.39887159955004E-09,5.25428749022906E-09}, - {1.09291268489958E-08,1.23935926756516E-10,3.92917259954515E-10,-1.79144682483562E-10,-9.11802874917597E-10}, - {-4.40957607823325E-09,1.45751390560667E-10,1.24641258165301E-10,-6.45810339804674E-11,-8.92894658893326E-12}, - {0,0,0,0,0}, - {1.54754294162102E-08,-1.60154742388847E-06,-4.08425188394881E-07,6.18170290113531E-09,-2.58919765162122E-07}, - {1.37130642286873E-08,-6.67813955828458E-08,-7.01410996605609E-09,3.82732572660461E-08,-2.73381870915135E-08}, - {2.19113155379218E-08,4.11027496396868E-09,6.33816020485226E-09,-1.49242411327524E-09,-6.14224941851705E-10}, - {6.26573021218961E-09,5.17137416480052E-10,-3.49784328298676E-10,1.13578756343208E-10,2.80414613398411E-10}, - {1.65048133258794E-11,1.00047239417239E-10,1.05124654878499E-10,-3.03826002621926E-11,4.57155388334682E-11}, - {6.20221691418381E-11,9.75852610098156E-12,-5.46716005756984E-12,1.31643349569537E-11,3.61618775715470E-12}, - {0,0,0,0,0}, - {-1.03938913012708E-06,-1.78417431315664E-07,2.86040141364439E-07,1.83508599345952E-08,-1.34452220464346E-07}, - {-4.36557481393662E-08,7.49780206868834E-09,-8.62829428674082E-09,5.50577793039009E-09,-9.46897502333254E-09}, - {3.43193738406672E-10,1.13545447306468E-08,1.25242388852214E-09,6.03221501959620E-10,1.57172070361180E-09}, - {-4.73307591021391E-10,1.70855824051391E-10,-2.62470421477037E-11,2.04525835988874E-10,-1.17859695928164E-10}, - {-3.36185995299839E-10,3.19243054562183E-11,1.17589412418126E-10,-1.35478747434514E-12,5.11192214558542E-11}, - {3.19640547592136E-11,2.94297823804643E-12,-1.00651526276990E-11,-1.67028733953153E-12,3.03938833625503E-12}, - {1.68928641118173E-11,-7.90032886682002E-13,-1.40899773539137E-12,7.76937592393354E-13,7.32539820298651E-13}, - {0,0,0,0,0}, - {2.32949756055277E-07,1.46237594908093E-07,-1.07770884952484E-07,1.26824870644476E-07,-2.36345735961108E-08}, - {8.89572676497766E-08,7.24810004121931E-08,2.67583556180119E-08,2.48434796111361E-08,-3.55004782858686E-09}, - {-1.00823909773603E-08,8.84433929029076E-10,-2.55502517594511E-10,-5.48034274059119E-10,-8.50241938494079E-10}, - {1.13259819566467E-09,5.55186945221216E-10,7.63679807785295E-11,-1.70067998092043E-11,1.57081965572493E-10}, - {-2.37748192185353E-10,2.45463764948000E-11,3.23208414802860E-11,-2.72624834520723E-12,8.14449183666500E-12}, - {-1.54977633126025E-11,4.58754903157884E-12,-1.25864665839074E-12,2.44139868157872E-12,-1.82827441958193E-12}, - {3.28285563794513E-12,-1.10072329225465E-12,-7.23470501810935E-13,5.85309745620389E-13,4.11317589687125E-13}, - {4.57596974384170E-13,9.84198128213558E-14,3.34503817702830E-14,7.08431086558307E-15,2.79891177268807E-14}, - {0,0,0,0,0}, - {-3.67820719155580E-07,6.98497901205902E-07,1.83397388750300E-07,2.39730262495372E-07,-2.58441984368194E-07}, - {5.17793954077994E-08,5.54614175977835E-08,1.75026214305232E-09,-2.55518450411346E-09,-6.12272723006537E-09}, - {-7.94292648157198E-09,-1.01709107852895E-09,-1.49251241812310E-09,9.32827213605682E-10,-8.24490722043118E-10}, - {1.36410408475679E-11,2.16390220454971E-10,1.24934806872235E-10,-6.82507825145903E-11,-4.01575177719668E-11}, - {-1.41619917600555E-11,-1.54733230409082E-11,1.36792829351538E-11,1.11157862104733E-12,2.08548465892268E-11}, - {-3.56521723755846E-12,4.47877185884557E-12,-6.34096209274637E-16,-1.13010624512348E-12,-2.82018136861041E-13}, - {2.22758955943441E-12,-4.63876465559380E-13,-5.80688019272507E-13,2.45878690598655E-13,1.49997666808106E-13}, - {-6.26833903786958E-14,2.73416335780807E-14,1.91842340758425E-14,1.67405061129010E-14,-2.45268543953704E-17}, - {1.81972870222228E-14,5.43036245069085E-15,1.92476637107321E-15,8.78498602508626E-17,-1.42581647227657E-15}, - {0,0,0,0,0}, - {9.74322164613392E-07,-5.23101820582724E-07,-2.81997898176227E-07,4.54762451707384E-08,-3.34645078118827E-08}, - {-6.75813194549663E-09,3.49744702199583E-08,-5.09170419895883E-09,5.24359476874755E-09,4.96664262534662E-09}, - {4.53858847892396E-10,-1.49347392165963E-09,-2.00939511362154E-09,9.30987163387955E-10,9.74450200826854E-11}, - {-4.92900885858693E-10,5.34223033225688E-12,1.08501839729368E-10,-6.43526142089173E-11,-3.11063319142619E-11}, - {1.38469246386690E-11,-7.91180584906922E-12,2.26641656746936E-13,4.55251515177956E-12,6.05270575117769E-12}, - {4.02247935664225E-12,1.82776657951829E-12,-1.28348801405445E-13,-2.16257301300350E-13,-5.54363979435025E-14}, - {4.15005914461687E-13,-2.00647573581168E-13,-1.67278251942946E-13,1.30332398257985E-13,1.52742363652434E-13}, - {6.36376500056974E-14,1.65794532815776E-14,-3.80832559052662E-15,-6.40262894005341E-16,2.42577181848072E-15}, - {-5.55273521249151E-15,3.69725182221479E-15,2.02114207545759E-15,-4.50870833392161E-16,9.62950493696677E-17}, - {1.00935904205024E-17,6.54751873609395E-17,-1.09138810997186E-16,-8.62396750098759E-17,-3.82788257844306E-17}, - {0,0,0,0,0}, - {4.21958510903678E-07,-8.30678271007705E-08,-3.47006439555247E-07,-3.36442823712421E-08,9.90739768222027E-08}, - {2.64389033612742E-08,2.65825090066479E-09,-1.28895513428522E-08,-7.07182694980098E-10,7.10907165301180E-09}, - {6.31203524153492E-09,-1.67038260990134E-09,1.33104703539822E-09,8.34376495185149E-10,-2.52478613522612E-10}, - {1.18414896299279E-10,-2.57745052288455E-11,2.88295935685818E-11,-3.27782977418354E-11,-1.05705000036156E-11}, - {-4.20826459055091E-12,-6.97430607432268E-12,-3.90660545970607E-12,-3.90449239948755E-13,-4.60384797517466E-13}, - {-9.47668356558200E-13,6.53305025354881E-13,2.63240185434960E-13,1.40129115015734E-13,3.85788887132074E-14}, - {2.23947810407291E-13,7.35262771548253E-15,-3.83348211931292E-14,4.20376514344176E-14,4.26445836468461E-14}, - {-3.88008154470596E-16,2.28561424667750E-15,-8.73599966653373E-16,2.14321147947665E-15,6.38631825071920E-16}, - {-8.62165565535721E-15,1.79742912149810E-15,1.01541125038661E-15,-7.91027655831866E-17,-4.06505132825230E-16}, - {-2.35355054392189E-16,-6.13997759731013E-17,-2.73490528665965E-17,2.63895177155121E-17,-4.47531057245187E-18}, - {6.01909706823530E-17,5.35520010856833E-18,-2.15530106132531E-18,-2.46778496746231E-18,-7.09947296442799E-19}, - {0,0,0,0,0}, - {-3.75005956318736E-07,-5.39872297906819E-07,-1.19929654883034E-07,4.52771083775007E-08,1.82790552943564E-07}, - {7.82606642505646E-09,-1.68890832383153E-08,-8.45995188378997E-09,1.42958730598502E-09,3.21075754133531E-09}, - {4.28818421913782E-09,-1.07501469928219E-09,8.84086350297418E-10,9.74171228764155E-10,8.59877149602304E-12}, - {1.28983712172521E-10,-6.96375160373676E-11,-2.13481436408896E-11,1.33516375568179E-11,-1.65864626508258E-11}, - {-4.48914384622368E-12,9.68953616831263E-13,-1.61372463422897E-12,-2.09683563440448E-12,-1.90096826314068E-12}, - {-1.12626619779175E-13,3.34903159106509E-14,-1.21721528343657E-13,7.46246339290354E-14,3.68424909859186E-13}, - {5.08294274367790E-14,2.83036159977090E-14,1.48074873486387E-14,-9.59633528834945E-15,-1.26231060951100E-14}, - {-4.01464098583541E-16,1.97047929526674E-15,-5.29967950447497E-16,-3.59120406619931E-16,1.69690933982683E-16}, - {-1.73919209873841E-15,7.52792462841274E-16,3.65589287101147E-16,-7.79247612043812E-17,-8.24599670368999E-17}, - {-4.61555616150128E-17,4.94529746019753E-19,-1.09858157212270E-17,3.95550811124928E-18,3.23972399884100E-18}, - {-2.27040686655766E-17,-3.27855689001215E-18,-3.30649011116861E-19,9.08748546536849E-19,8.92197599890994E-19}, - {5.67241944733762E-18,3.84449400209976E-19,1.77668058015537E-19,2.00432838283455E-20,-2.00801461564767E-19} +const double bnm_bh[91][5] = { + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {-2.29210587053658E-06, + -2.33805004374529E-06, + -7.49312880102168E-07, + -5.12022747852006E-07, + 5.88926055066172E-07}, + {0, 0, 0, 0, 0}, + {-4.63382754843690E-06, + -2.23853015662938E-06, + 8.14830531656518E-07, + 1.15453269407116E-06, + -4.53555450927571E-07}, + {-6.92432096320778E-07, + -2.98734455136141E-07, + 1.48085153955641E-08, + 1.37881746148773E-07, + -6.92492118460215E-09}, + {0, 0, 0, 0, 0}, + {-1.91507979850310E-06, + -1.83614825459598E-06, + -7.46807436870647E-07, + -1.28329122348007E-06, + 5.04937180063059E-07}, + {-8.07527103916713E-07, + 2.83997840574570E-08, + -6.01890498063025E-08, + -2.48339507554546E-08, + 2.46284627824308E-08}, + {-2.82995069303093E-07, + 1.38818274596408E-09, + 3.22731214161408E-09, + 2.87731153972404E-10, + 1.53895537278496E-08}, + {0, 0, 0, 0, 0}, + {-6.68210270956800E-07, + -2.19104833297845E-06, + 1.30116691657253E-07, + 4.78445730433450E-07, + -4.40344300914051E-07}, + {-2.36946755740436E-07, + -1.32730991878204E-07, + 1.83669593693860E-08, + 7.90218931983569E-08, + -4.70161979232584E-08}, + {1.07746083292179E-07, + -4.17088637760330E-09, + -1.83296035841109E-09, + -5.80243971371211E-09, + -2.11682361167439E-09}, + {-5.44712355496109E-08, + 1.89717032256923E-09, + 2.27327316287804E-10, + 7.78400728280038E-10, + 8.82380487618991E-12}, + {0, 0, 0, 0, 0}, + {-5.61707049615673E-08, + -1.09066447089585E-06, + -2.25742250174119E-07, + -8.64367795924377E-07, + 1.06411275240680E-08}, + {2.41782935157918E-08, + -3.65762298303819E-08, + -6.93420659586875E-08, + -3.97316214341991E-08, + -2.08767816486390E-08}, + {6.38293030383436E-08, + 1.11377936334470E-08, + 6.91424941454782E-09, + 1.39887159955004E-09, + 5.25428749022906E-09}, + {1.09291268489958E-08, + 1.23935926756516E-10, + 3.92917259954515E-10, + -1.79144682483562E-10, + -9.11802874917597E-10}, + {-4.40957607823325E-09, + 1.45751390560667E-10, + 1.24641258165301E-10, + -6.45810339804674E-11, + -8.92894658893326E-12}, + {0, 0, 0, 0, 0}, + {1.54754294162102E-08, + -1.60154742388847E-06, + -4.08425188394881E-07, + 6.18170290113531E-09, + -2.58919765162122E-07}, + {1.37130642286873E-08, + -6.67813955828458E-08, + -7.01410996605609E-09, + 3.82732572660461E-08, + -2.73381870915135E-08}, + {2.19113155379218E-08, + 4.11027496396868E-09, + 6.33816020485226E-09, + -1.49242411327524E-09, + -6.14224941851705E-10}, + {6.26573021218961E-09, + 5.17137416480052E-10, + -3.49784328298676E-10, + 1.13578756343208E-10, + 2.80414613398411E-10}, + {1.65048133258794E-11, + 1.00047239417239E-10, + 1.05124654878499E-10, + -3.03826002621926E-11, + 4.57155388334682E-11}, + {6.20221691418381E-11, + 9.75852610098156E-12, + -5.46716005756984E-12, + 1.31643349569537E-11, + 3.61618775715470E-12}, + {0, 0, 0, 0, 0}, + {-1.03938913012708E-06, + -1.78417431315664E-07, + 2.86040141364439E-07, + 1.83508599345952E-08, + -1.34452220464346E-07}, + {-4.36557481393662E-08, + 7.49780206868834E-09, + -8.62829428674082E-09, + 5.50577793039009E-09, + -9.46897502333254E-09}, + {3.43193738406672E-10, + 1.13545447306468E-08, + 1.25242388852214E-09, + 6.03221501959620E-10, + 1.57172070361180E-09}, + {-4.73307591021391E-10, + 1.70855824051391E-10, + -2.62470421477037E-11, + 2.04525835988874E-10, + -1.17859695928164E-10}, + {-3.36185995299839E-10, + 3.19243054562183E-11, + 1.17589412418126E-10, + -1.35478747434514E-12, + 5.11192214558542E-11}, + {3.19640547592136E-11, + 2.94297823804643E-12, + -1.00651526276990E-11, + -1.67028733953153E-12, + 3.03938833625503E-12}, + {1.68928641118173E-11, + -7.90032886682002E-13, + -1.40899773539137E-12, + 7.76937592393354E-13, + 7.32539820298651E-13}, + {0, 0, 0, 0, 0}, + {2.32949756055277E-07, + 1.46237594908093E-07, + -1.07770884952484E-07, + 1.26824870644476E-07, + -2.36345735961108E-08}, + {8.89572676497766E-08, + 7.24810004121931E-08, + 2.67583556180119E-08, + 2.48434796111361E-08, + -3.55004782858686E-09}, + {-1.00823909773603E-08, + 8.84433929029076E-10, + -2.55502517594511E-10, + -5.48034274059119E-10, + -8.50241938494079E-10}, + {1.13259819566467E-09, + 5.55186945221216E-10, + 7.63679807785295E-11, + -1.70067998092043E-11, + 1.57081965572493E-10}, + {-2.37748192185353E-10, + 2.45463764948000E-11, + 3.23208414802860E-11, + -2.72624834520723E-12, + 8.14449183666500E-12}, + {-1.54977633126025E-11, + 4.58754903157884E-12, + -1.25864665839074E-12, + 2.44139868157872E-12, + -1.82827441958193E-12}, + {3.28285563794513E-12, + -1.10072329225465E-12, + -7.23470501810935E-13, + 5.85309745620389E-13, + 4.11317589687125E-13}, + {4.57596974384170E-13, + 9.84198128213558E-14, + 3.34503817702830E-14, + 7.08431086558307E-15, + 2.79891177268807E-14}, + {0, 0, 0, 0, 0}, + {-3.67820719155580E-07, + 6.98497901205902E-07, + 1.83397388750300E-07, + 2.39730262495372E-07, + -2.58441984368194E-07}, + {5.17793954077994E-08, + 5.54614175977835E-08, + 1.75026214305232E-09, + -2.55518450411346E-09, + -6.12272723006537E-09}, + {-7.94292648157198E-09, + -1.01709107852895E-09, + -1.49251241812310E-09, + 9.32827213605682E-10, + -8.24490722043118E-10}, + {1.36410408475679E-11, + 2.16390220454971E-10, + 1.24934806872235E-10, + -6.82507825145903E-11, + -4.01575177719668E-11}, + {-1.41619917600555E-11, + -1.54733230409082E-11, + 1.36792829351538E-11, + 1.11157862104733E-12, + 2.08548465892268E-11}, + {-3.56521723755846E-12, + 4.47877185884557E-12, + -6.34096209274637E-16, + -1.13010624512348E-12, + -2.82018136861041E-13}, + {2.22758955943441E-12, + -4.63876465559380E-13, + -5.80688019272507E-13, + 2.45878690598655E-13, + 1.49997666808106E-13}, + {-6.26833903786958E-14, + 2.73416335780807E-14, + 1.91842340758425E-14, + 1.67405061129010E-14, + -2.45268543953704E-17}, + {1.81972870222228E-14, + 5.43036245069085E-15, + 1.92476637107321E-15, + 8.78498602508626E-17, + -1.42581647227657E-15}, + {0, 0, 0, 0, 0}, + {9.74322164613392E-07, + -5.23101820582724E-07, + -2.81997898176227E-07, + 4.54762451707384E-08, + -3.34645078118827E-08}, + {-6.75813194549663E-09, + 3.49744702199583E-08, + -5.09170419895883E-09, + 5.24359476874755E-09, + 4.96664262534662E-09}, + {4.53858847892396E-10, + -1.49347392165963E-09, + -2.00939511362154E-09, + 9.30987163387955E-10, + 9.74450200826854E-11}, + {-4.92900885858693E-10, + 5.34223033225688E-12, + 1.08501839729368E-10, + -6.43526142089173E-11, + -3.11063319142619E-11}, + {1.38469246386690E-11, + -7.91180584906922E-12, + 2.26641656746936E-13, + 4.55251515177956E-12, + 6.05270575117769E-12}, + {4.02247935664225E-12, + 1.82776657951829E-12, + -1.28348801405445E-13, + -2.16257301300350E-13, + -5.54363979435025E-14}, + {4.15005914461687E-13, + -2.00647573581168E-13, + -1.67278251942946E-13, + 1.30332398257985E-13, + 1.52742363652434E-13}, + {6.36376500056974E-14, + 1.65794532815776E-14, + -3.80832559052662E-15, + -6.40262894005341E-16, + 2.42577181848072E-15}, + {-5.55273521249151E-15, + 3.69725182221479E-15, + 2.02114207545759E-15, + -4.50870833392161E-16, + 9.62950493696677E-17}, + {1.00935904205024E-17, + 6.54751873609395E-17, + -1.09138810997186E-16, + -8.62396750098759E-17, + -3.82788257844306E-17}, + {0, 0, 0, 0, 0}, + {4.21958510903678E-07, + -8.30678271007705E-08, + -3.47006439555247E-07, + -3.36442823712421E-08, + 9.90739768222027E-08}, + {2.64389033612742E-08, + 2.65825090066479E-09, + -1.28895513428522E-08, + -7.07182694980098E-10, + 7.10907165301180E-09}, + {6.31203524153492E-09, + -1.67038260990134E-09, + 1.33104703539822E-09, + 8.34376495185149E-10, + -2.52478613522612E-10}, + {1.18414896299279E-10, + -2.57745052288455E-11, + 2.88295935685818E-11, + -3.27782977418354E-11, + -1.05705000036156E-11}, + {-4.20826459055091E-12, + -6.97430607432268E-12, + -3.90660545970607E-12, + -3.90449239948755E-13, + -4.60384797517466E-13}, + {-9.47668356558200E-13, + 6.53305025354881E-13, + 2.63240185434960E-13, + 1.40129115015734E-13, + 3.85788887132074E-14}, + {2.23947810407291E-13, + 7.35262771548253E-15, + -3.83348211931292E-14, + 4.20376514344176E-14, + 4.26445836468461E-14}, + {-3.88008154470596E-16, + 2.28561424667750E-15, + -8.73599966653373E-16, + 2.14321147947665E-15, + 6.38631825071920E-16}, + {-8.62165565535721E-15, + 1.79742912149810E-15, + 1.01541125038661E-15, + -7.91027655831866E-17, + -4.06505132825230E-16}, + {-2.35355054392189E-16, + -6.13997759731013E-17, + -2.73490528665965E-17, + 2.63895177155121E-17, + -4.47531057245187E-18}, + {6.01909706823530E-17, + 5.35520010856833E-18, + -2.15530106132531E-18, + -2.46778496746231E-18, + -7.09947296442799E-19}, + {0, 0, 0, 0, 0}, + {-3.75005956318736E-07, + -5.39872297906819E-07, + -1.19929654883034E-07, + 4.52771083775007E-08, + 1.82790552943564E-07}, + {7.82606642505646E-09, + -1.68890832383153E-08, + -8.45995188378997E-09, + 1.42958730598502E-09, + 3.21075754133531E-09}, + {4.28818421913782E-09, + -1.07501469928219E-09, + 8.84086350297418E-10, + 9.74171228764155E-10, + 8.59877149602304E-12}, + {1.28983712172521E-10, + -6.96375160373676E-11, + -2.13481436408896E-11, + 1.33516375568179E-11, + -1.65864626508258E-11}, + {-4.48914384622368E-12, + 9.68953616831263E-13, + -1.61372463422897E-12, + -2.09683563440448E-12, + -1.90096826314068E-12}, + {-1.12626619779175E-13, + 3.34903159106509E-14, + -1.21721528343657E-13, + 7.46246339290354E-14, + 3.68424909859186E-13}, + {5.08294274367790E-14, + 2.83036159977090E-14, + 1.48074873486387E-14, + -9.59633528834945E-15, + -1.26231060951100E-14}, + {-4.01464098583541E-16, + 1.97047929526674E-15, + -5.29967950447497E-16, + -3.59120406619931E-16, + 1.69690933982683E-16}, + {-1.73919209873841E-15, + 7.52792462841274E-16, + 3.65589287101147E-16, + -7.79247612043812E-17, + -8.24599670368999E-17}, + {-4.61555616150128E-17, + 4.94529746019753E-19, + -1.09858157212270E-17, + 3.95550811124928E-18, + 3.23972399884100E-18}, + {-2.27040686655766E-17, + -3.27855689001215E-18, + -3.30649011116861E-19, + 9.08748546536849E-19, + 8.92197599890994E-19}, + {5.67241944733762E-18, + 3.84449400209976E-19, + 1.77668058015537E-19, + 2.00432838283455E-20, + -2.00801461564767E-19} }; -const double bnm_bw[91][5]= -{ - {0,0,0,0,0}, - {0,0,0,0,0}, - {-9.56715196386889E-06,-3.68040633020420E-08,1.27846786489883E-07,1.32525487755973E-06,1.53075361125066E-06}, - {0,0,0,0,0}, - {-7.17682617983607E-06,2.89994188119445E-06,-2.97763578173405E-07,8.95742089134942E-07,3.44416325304006E-07}, - {-8.02661132285210E-07,3.66738692077244E-07,-3.02880965723280E-07,3.54144282036103E-07,-1.68873066391463E-07}, - {0,0,0,0,0}, - {-2.89640569283461E-06,-7.83566373343614E-07,-8.36667214682577E-07,-7.41891843549121E-07,-9.23922655636489E-08}, - {-1.06144662284862E-06,1.57709930505924E-07,1.04203025714319E-07,1.20783300488461E-07,-1.38726055821134E-07}, - {-4.16549018672265E-07,-1.35220897698872E-07,-6.40269964829901E-08,1.63258283210837E-08,-2.57958025095959E-08}, - {0,0,0,0,0}, - {3.52324885892419E-06,-2.26705543513814E-07,1.53835589488292E-06,-3.75263061267433E-07,3.69384057396017E-07}, - {-2.06569149157664E-07,-9.36260183227175E-08,-3.55985284353048E-08,-9.13671163891094E-08,6.93156256562600E-09}, - {1.32437594740782E-07,4.44349887272663E-08,-3.38192451721674E-08,-3.97263855781102E-08,-1.93087822995800E-09}, - {-1.29595244818942E-07,-1.40852985547683E-08,1.42587592939760E-09,7.05779876554001E-09,-1.00996269264535E-08}, - {0,0,0,0,0}, - {4.06960756215938E-06,-1.97898540226986E-06,7.21905857553588E-08,-1.19908881538755E-06,-5.67561861536903E-08}, - {6.53369660286999E-08,-2.42818687866392E-07,-1.66203004559493E-08,-2.41512414151897E-08,4.45426333411018E-08}, - {1.44650670663281E-07,8.50666367433859E-09,-4.61165612004307E-09,4.88527987491045E-09,1.06277326713172E-08}, - {1.86770937103513E-08,-6.44197940288930E-10,-7.60456736846174E-09,-9.97186468682689E-10,8.73229752697716E-10}, - {-1.00206566229113E-08,1.33934372663121E-09,1.41691503439220E-09,8.72352590578753E-10,-8.04561626629829E-10}, - {0,0,0,0,0}, - {3.07161843116618E-06,1.82962085656470E-06,1.87728623016069E-07,7.10611617623261E-07,2.26499092250481E-07}, - {4.50766403064905E-08,-1.67752393078256E-07,2.47844723639070E-08,-3.56484348424869E-09,-1.56634836636584E-08}, - {3.77011651881090E-08,-7.23045828480496E-09,5.22995988863761E-09,-1.03740320341306E-09,4.57839777217789E-09}, - {8.09495635883121E-09,-3.01977244420529E-10,-2.30104544933093E-09,3.63658580939428E-10,4.39320811714867E-10}, - {9.37087629961269E-11,1.00780920426635E-09,1.28140539913350E-10,-6.65795285522138E-12,4.71732796198631E-11}, - {-8.88504487069155E-11,-1.63253810435461E-10,7.22669710644299E-11,5.64715132584527E-11,-1.08949308197617E-12}, - {0,0,0,0,0}, - {-2.64054293284174E-07,-2.37611606117256E-06,-1.83671059706264E-06,-3.12199354841993E-07,-1.05598289276114E-07}, - {7.41706968747147E-08,-1.64359098062646E-08,-3.09750224040234E-08,-9.68640079410317E-09,-7.90399057863403E-08}, - {-1.00254376564271E-08,1.12528248631191E-08,-2.67841549174100E-09,-2.69481819323647E-09,1.56550607475331E-09}, - {-2.18568129350729E-09,6.26422056977450E-10,1.95007291427316E-09,3.14226463591125E-10,-3.62000388344482E-10}, - {-9.30451291747549E-10,5.62175549482704E-11,1.01022849902012E-10,5.18675856498499E-11,5.37561696283235E-11}, - {5.33151334468794E-11,1.07571307336725E-10,-1.31714567944652E-11,-4.17524405900018E-11,-2.16737797893502E-12}, - {4.69916869001309E-11,-4.34516364859583E-12,-6.61054225868897E-12,-5.75845818545368E-12,-2.32180293529175E-12}, - {0,0,0,0,0}, - {-3.50305843086926E-06,1.76085131953403E-06,8.16661224478572E-07,4.09111042640801E-07,-9.85414469804995E-08}, - {1.44670876127274E-07,-1.41331228923029E-08,-3.06530152369269E-08,-1.46732098927996E-08,-2.30660839364244E-08}, - {-2.00043052422933E-08,1.72145861031776E-09,2.13714615094209E-09,1.02982676689194E-09,-1.64945224692217E-10}, - {1.23552540016991E-09,1.42028470911613E-09,8.79622616627508E-10,-7.44465600265154E-10,-7.17124672589442E-11}, - {-6.67749524914644E-10,-5.77722874934050E-11,3.40077806879472E-11,4.26176076541840E-11,8.23189659748212E-11}, - {-4.62771648935992E-11,-7.24005305716782E-13,1.18233730497485E-12,5.18156973532267E-12,-1.53329687155297E-12}, - {4.75581699468619E-12,-3.79782291469732E-12,1.33077109836853E-12,-1.02426020107120E-12,3.10385019249130E-13}, - {1.66486090578792E-12,1.08573672403649E-12,1.26268044166279E-13,-1.23509297742757E-13,-1.81842007284038E-13}, - {0,0,0,0,0}, - {9.93870680202303E-08,-1.85264736035628E-06,-5.58942734710854E-07,-5.54183448316270E-07,-3.95581289689398E-08}, - {7.88329069002365E-08,2.04810091451078E-08,3.74588851000076E-09,3.42429296613803E-08,-2.00840228416712E-08}, - {-5.93700447329696E-10,-6.57499436973459E-10,-6.90560448220751E-09,3.56586371051089E-09,7.33310245621566E-11}, - {-6.38101662363634E-11,4.23668020216529E-10,-2.43764895979202E-10,-9.31466610703172E-11,-3.17491457845975E-10}, - {1.50943725382470E-11,-6.11641188685078E-11,-4.37018785685645E-11,-2.32871158949602E-11,4.19757251950526E-11}, - {-1.18165328825853E-11,-9.91299557532438E-13,6.40908678055865E-14,2.41049422936434E-12,-8.20746054454953E-14}, - {6.01892101914838E-12,-8.78487122873450E-13,-1.58887481332294E-12,-3.13556902469604E-13,5.14523727801645E-14}, - {-1.50791729401891E-13,-1.45234807159695E-13,1.65302377570887E-13,-5.77094211651483E-15,9.22218953528393E-14}, - {-1.85618902787381E-14,5.64333811864051E-14,-9.94311377945570E-15,-2.40992156199999E-15,-2.19196760659665E-14}, - {0,0,0,0,0}, - {-8.16252352075899E-08,1.61725487723444E-06,9.55522506715921E-07,4.02436267433511E-07,-2.80682052597712E-07}, - {7.68684790328630E-09,-5.00940723761353E-09,-2.43640127974386E-08,-2.59119930503129E-08,3.35015169182094E-08}, - {7.97903115186673E-09,3.73803883416618E-09,3.27888334636662E-09,1.37481300578804E-09,-1.10677168734482E-10}, - {-1.67853012769912E-09,-1.61405252173139E-10,-1.98841576520056E-10,-1.46591506832192E-11,9.35710487804660E-11}, - {4.08807084343221E-11,-3.74514169689568E-11,-3.03638493323910E-11,-5.02332555734577E-12,-8.03417498408344E-12}, - {6.48922619024579E-12,1.96166891023817E-12,-1.96968755122868E-12,-5.20970156382361E-12,-1.62656885103402E-12}, - {1.28603518902875E-12,-4.88146958435109E-13,-3.37034886991840E-13,1.37393696103000E-14,4.41398325716943E-14}, - {1.48670014793021E-13,4.41636026364555E-14,2.06210477976005E-14,-3.43717583585390E-14,-1.21693704024213E-14}, - {-1.67624180330244E-14,6.59317111144238E-15,2.57238525440646E-15,-3.21568425020512E-17,5.29659568026553E-15}, - {7.85453466393227E-16,6.91252183915939E-16,-1.20540764178454E-15,-3.85803892583301E-16,3.46606994632006E-16}, - {0,0,0,0,0}, - {2.86710087625579E-06,-1.68179842305865E-06,-8.48306772016870E-07,-7.08798062479598E-07,-1.27469453733635E-07}, - {2.11824305734993E-09,2.02274279084379E-08,1.61862253091554E-08,3.25597167111807E-08,3.40868964045822E-09}, - {1.21757111431438E-08,1.68405530472906E-09,1.55379338018638E-09,-3.81467795805531E-10,2.53316405545058E-09}, - {-9.98413758659768E-11,5.38382145421318E-10,3.92629628330704E-10,-1.43067134097778E-10,3.74959329667113E-12}, - {-1.57270407028909E-11,-9.02797202317592E-12,8.45997059887690E-12,4.71474382524218E-12,5.41880986596427E-12}, - {-1.20658618702054E-12,7.12940685593433E-13,1.02148613026937E-12,1.63063852348169E-13,1.74048793197708E-13}, - {3.80559390991789E-13,1.19678271353485E-13,9.72859455604188E-14,5.42642400031729E-14,8.18796710714586E-14}, - {-4.69629218656902E-14,5.59889038686206E-15,2.05363292795059E-15,5.38599403288686E-15,-2.68929559474202E-15}, - {-1.88759348081742E-14,5.20975954705924E-15,-4.43585653096395E-16,5.57436617793556E-16,-3.95922805817677E-16}, - {-9.80871456373282E-16,2.50857658461759E-17,-1.24253000050963E-16,6.00857065211394E-17,3.53799635311500E-18}, - {2.49370713054872E-16,-1.49119714269816E-17,-3.12276052640583E-17,-2.42001662334001E-17,-1.69766504318143E-17}, - {0,0,0,0,0}, - {-1.69222102455713E-06,1.64277906173064E-06,5.28855114364096E-07,4.28159853268650E-07,-1.57362445882665E-07}, - {1.67656782413678E-08,-3.77746114074055E-08,-2.21564555842165E-08,-3.37071806992217E-08,1.47454008739800E-08}, - {1.06080499491408E-08,3.21990403709678E-09,3.87301757435359E-09,2.92241827834347E-10,-1.86619473655742E-11}, - {1.62399669665839E-10,3.51322865845172E-10,2.67086377702958E-11,-1.31596563625491E-10,3.14164569507034E-11}, - {-2.02180016657259E-11,2.03305178342732E-11,6.34969032565839E-12,5.99522296668787E-12,-4.46275273451008E-12}, - {-9.88409290158885E-13,-1.47692750858224E-13,3.14655550730530E-13,-2.41857189187879E-13,4.47727504501486E-13}, - {1.71430777754854E-13,1.73950835042486E-13,5.92323956541558E-14,8.06625710171825E-15,2.33252485755634E-14}, - {-1.74184545690134E-15,-8.18003353124179E-16,-6.62369006497819E-16,4.16303374396147E-15,7.06513748014024E-15}, - {-6.02936238677014E-15,1.89241084885229E-15,1.99097881944270E-17,-6.99974290696640E-16,-2.69504942597709E-17}, - {-4.65632962602379E-16,3.70281995445114E-18,-9.04232973763345E-17,2.20847370761932E-17,7.62909453726566E-17}, - {-6.25921477907943E-17,-2.10532795609842E-17,-1.03808073867183E-17,1.15091380049019E-18,4.66794445408388E-19}, - {9.39427013576903E-18,9.17044662931859E-19,2.04132745117549E-18,-1.72364063154625E-19,-1.18098896532163E-18} +const double bnm_bw[91][5] = { + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {-9.56715196386889E-06, + -3.68040633020420E-08, + 1.27846786489883E-07, + 1.32525487755973E-06, + 1.53075361125066E-06}, + {0, 0, 0, 0, 0}, + {-7.17682617983607E-06, + 2.89994188119445E-06, + -2.97763578173405E-07, + 8.95742089134942E-07, + 3.44416325304006E-07}, + {-8.02661132285210E-07, + 3.66738692077244E-07, + -3.02880965723280E-07, + 3.54144282036103E-07, + -1.68873066391463E-07}, + {0, 0, 0, 0, 0}, + {-2.89640569283461E-06, + -7.83566373343614E-07, + -8.36667214682577E-07, + -7.41891843549121E-07, + -9.23922655636489E-08}, + {-1.06144662284862E-06, + 1.57709930505924E-07, + 1.04203025714319E-07, + 1.20783300488461E-07, + -1.38726055821134E-07}, + {-4.16549018672265E-07, + -1.35220897698872E-07, + -6.40269964829901E-08, + 1.63258283210837E-08, + -2.57958025095959E-08}, + {0, 0, 0, 0, 0}, + {3.52324885892419E-06, + -2.26705543513814E-07, + 1.53835589488292E-06, + -3.75263061267433E-07, + 3.69384057396017E-07}, + {-2.06569149157664E-07, + -9.36260183227175E-08, + -3.55985284353048E-08, + -9.13671163891094E-08, + 6.93156256562600E-09}, + {1.32437594740782E-07, + 4.44349887272663E-08, + -3.38192451721674E-08, + -3.97263855781102E-08, + -1.93087822995800E-09}, + {-1.29595244818942E-07, + -1.40852985547683E-08, + 1.42587592939760E-09, + 7.05779876554001E-09, + -1.00996269264535E-08}, + {0, 0, 0, 0, 0}, + {4.06960756215938E-06, + -1.97898540226986E-06, + 7.21905857553588E-08, + -1.19908881538755E-06, + -5.67561861536903E-08}, + {6.53369660286999E-08, + -2.42818687866392E-07, + -1.66203004559493E-08, + -2.41512414151897E-08, + 4.45426333411018E-08}, + {1.44650670663281E-07, + 8.50666367433859E-09, + -4.61165612004307E-09, + 4.88527987491045E-09, + 1.06277326713172E-08}, + {1.86770937103513E-08, + -6.44197940288930E-10, + -7.60456736846174E-09, + -9.97186468682689E-10, + 8.73229752697716E-10}, + {-1.00206566229113E-08, + 1.33934372663121E-09, + 1.41691503439220E-09, + 8.72352590578753E-10, + -8.04561626629829E-10}, + {0, 0, 0, 0, 0}, + {3.07161843116618E-06, + 1.82962085656470E-06, + 1.87728623016069E-07, + 7.10611617623261E-07, + 2.26499092250481E-07}, + {4.50766403064905E-08, + -1.67752393078256E-07, + 2.47844723639070E-08, + -3.56484348424869E-09, + -1.56634836636584E-08}, + {3.77011651881090E-08, + -7.23045828480496E-09, + 5.22995988863761E-09, + -1.03740320341306E-09, + 4.57839777217789E-09}, + {8.09495635883121E-09, + -3.01977244420529E-10, + -2.30104544933093E-09, + 3.63658580939428E-10, + 4.39320811714867E-10}, + {9.37087629961269E-11, + 1.00780920426635E-09, + 1.28140539913350E-10, + -6.65795285522138E-12, + 4.71732796198631E-11}, + {-8.88504487069155E-11, + -1.63253810435461E-10, + 7.22669710644299E-11, + 5.64715132584527E-11, + -1.08949308197617E-12}, + {0, 0, 0, 0, 0}, + {-2.64054293284174E-07, + -2.37611606117256E-06, + -1.83671059706264E-06, + -3.12199354841993E-07, + -1.05598289276114E-07}, + {7.41706968747147E-08, + -1.64359098062646E-08, + -3.09750224040234E-08, + -9.68640079410317E-09, + -7.90399057863403E-08}, + {-1.00254376564271E-08, + 1.12528248631191E-08, + -2.67841549174100E-09, + -2.69481819323647E-09, + 1.56550607475331E-09}, + {-2.18568129350729E-09, + 6.26422056977450E-10, + 1.95007291427316E-09, + 3.14226463591125E-10, + -3.62000388344482E-10}, + {-9.30451291747549E-10, + 5.62175549482704E-11, + 1.01022849902012E-10, + 5.18675856498499E-11, + 5.37561696283235E-11}, + {5.33151334468794E-11, + 1.07571307336725E-10, + -1.31714567944652E-11, + -4.17524405900018E-11, + -2.16737797893502E-12}, + {4.69916869001309E-11, + -4.34516364859583E-12, + -6.61054225868897E-12, + -5.75845818545368E-12, + -2.32180293529175E-12}, + {0, 0, 0, 0, 0}, + {-3.50305843086926E-06, + 1.76085131953403E-06, + 8.16661224478572E-07, + 4.09111042640801E-07, + -9.85414469804995E-08}, + {1.44670876127274E-07, + -1.41331228923029E-08, + -3.06530152369269E-08, + -1.46732098927996E-08, + -2.30660839364244E-08}, + {-2.00043052422933E-08, + 1.72145861031776E-09, + 2.13714615094209E-09, + 1.02982676689194E-09, + -1.64945224692217E-10}, + {1.23552540016991E-09, + 1.42028470911613E-09, + 8.79622616627508E-10, + -7.44465600265154E-10, + -7.17124672589442E-11}, + {-6.67749524914644E-10, + -5.77722874934050E-11, + 3.40077806879472E-11, + 4.26176076541840E-11, + 8.23189659748212E-11}, + {-4.62771648935992E-11, + -7.24005305716782E-13, + 1.18233730497485E-12, + 5.18156973532267E-12, + -1.53329687155297E-12}, + {4.75581699468619E-12, + -3.79782291469732E-12, + 1.33077109836853E-12, + -1.02426020107120E-12, + 3.10385019249130E-13}, + {1.66486090578792E-12, + 1.08573672403649E-12, + 1.26268044166279E-13, + -1.23509297742757E-13, + -1.81842007284038E-13}, + {0, 0, 0, 0, 0}, + {9.93870680202303E-08, + -1.85264736035628E-06, + -5.58942734710854E-07, + -5.54183448316270E-07, + -3.95581289689398E-08}, + {7.88329069002365E-08, + 2.04810091451078E-08, + 3.74588851000076E-09, + 3.42429296613803E-08, + -2.00840228416712E-08}, + {-5.93700447329696E-10, + -6.57499436973459E-10, + -6.90560448220751E-09, + 3.56586371051089E-09, + 7.33310245621566E-11}, + {-6.38101662363634E-11, + 4.23668020216529E-10, + -2.43764895979202E-10, + -9.31466610703172E-11, + -3.17491457845975E-10}, + {1.50943725382470E-11, + -6.11641188685078E-11, + -4.37018785685645E-11, + -2.32871158949602E-11, + 4.19757251950526E-11}, + {-1.18165328825853E-11, + -9.91299557532438E-13, + 6.40908678055865E-14, + 2.41049422936434E-12, + -8.20746054454953E-14}, + {6.01892101914838E-12, + -8.78487122873450E-13, + -1.58887481332294E-12, + -3.13556902469604E-13, + 5.14523727801645E-14}, + {-1.50791729401891E-13, + -1.45234807159695E-13, + 1.65302377570887E-13, + -5.77094211651483E-15, + 9.22218953528393E-14}, + {-1.85618902787381E-14, + 5.64333811864051E-14, + -9.94311377945570E-15, + -2.40992156199999E-15, + -2.19196760659665E-14}, + {0, 0, 0, 0, 0}, + {-8.16252352075899E-08, + 1.61725487723444E-06, + 9.55522506715921E-07, + 4.02436267433511E-07, + -2.80682052597712E-07}, + {7.68684790328630E-09, + -5.00940723761353E-09, + -2.43640127974386E-08, + -2.59119930503129E-08, + 3.35015169182094E-08}, + {7.97903115186673E-09, + 3.73803883416618E-09, + 3.27888334636662E-09, + 1.37481300578804E-09, + -1.10677168734482E-10}, + {-1.67853012769912E-09, + -1.61405252173139E-10, + -1.98841576520056E-10, + -1.46591506832192E-11, + 9.35710487804660E-11}, + {4.08807084343221E-11, + -3.74514169689568E-11, + -3.03638493323910E-11, + -5.02332555734577E-12, + -8.03417498408344E-12}, + {6.48922619024579E-12, + 1.96166891023817E-12, + -1.96968755122868E-12, + -5.20970156382361E-12, + -1.62656885103402E-12}, + {1.28603518902875E-12, + -4.88146958435109E-13, + -3.37034886991840E-13, + 1.37393696103000E-14, + 4.41398325716943E-14}, + {1.48670014793021E-13, + 4.41636026364555E-14, + 2.06210477976005E-14, + -3.43717583585390E-14, + -1.21693704024213E-14}, + {-1.67624180330244E-14, + 6.59317111144238E-15, + 2.57238525440646E-15, + -3.21568425020512E-17, + 5.29659568026553E-15}, + {7.85453466393227E-16, + 6.91252183915939E-16, + -1.20540764178454E-15, + -3.85803892583301E-16, + 3.46606994632006E-16}, + {0, 0, 0, 0, 0}, + {2.86710087625579E-06, + -1.68179842305865E-06, + -8.48306772016870E-07, + -7.08798062479598E-07, + -1.27469453733635E-07}, + {2.11824305734993E-09, + 2.02274279084379E-08, + 1.61862253091554E-08, + 3.25597167111807E-08, + 3.40868964045822E-09}, + {1.21757111431438E-08, + 1.68405530472906E-09, + 1.55379338018638E-09, + -3.81467795805531E-10, + 2.53316405545058E-09}, + {-9.98413758659768E-11, + 5.38382145421318E-10, + 3.92629628330704E-10, + -1.43067134097778E-10, + 3.74959329667113E-12}, + {-1.57270407028909E-11, + -9.02797202317592E-12, + 8.45997059887690E-12, + 4.71474382524218E-12, + 5.41880986596427E-12}, + {-1.20658618702054E-12, + 7.12940685593433E-13, + 1.02148613026937E-12, + 1.63063852348169E-13, + 1.74048793197708E-13}, + {3.80559390991789E-13, + 1.19678271353485E-13, + 9.72859455604188E-14, + 5.42642400031729E-14, + 8.18796710714586E-14}, + {-4.69629218656902E-14, + 5.59889038686206E-15, + 2.05363292795059E-15, + 5.38599403288686E-15, + -2.68929559474202E-15}, + {-1.88759348081742E-14, + 5.20975954705924E-15, + -4.43585653096395E-16, + 5.57436617793556E-16, + -3.95922805817677E-16}, + {-9.80871456373282E-16, + 2.50857658461759E-17, + -1.24253000050963E-16, + 6.00857065211394E-17, + 3.53799635311500E-18}, + {2.49370713054872E-16, + -1.49119714269816E-17, + -3.12276052640583E-17, + -2.42001662334001E-17, + -1.69766504318143E-17}, + {0, 0, 0, 0, 0}, + {-1.69222102455713E-06, + 1.64277906173064E-06, + 5.28855114364096E-07, + 4.28159853268650E-07, + -1.57362445882665E-07}, + {1.67656782413678E-08, + -3.77746114074055E-08, + -2.21564555842165E-08, + -3.37071806992217E-08, + 1.47454008739800E-08}, + {1.06080499491408E-08, + 3.21990403709678E-09, + 3.87301757435359E-09, + 2.92241827834347E-10, + -1.86619473655742E-11}, + {1.62399669665839E-10, + 3.51322865845172E-10, + 2.67086377702958E-11, + -1.31596563625491E-10, + 3.14164569507034E-11}, + {-2.02180016657259E-11, + 2.03305178342732E-11, + 6.34969032565839E-12, + 5.99522296668787E-12, + -4.46275273451008E-12}, + {-9.88409290158885E-13, + -1.47692750858224E-13, + 3.14655550730530E-13, + -2.41857189187879E-13, + 4.47727504501486E-13}, + {1.71430777754854E-13, + 1.73950835042486E-13, + 5.92323956541558E-14, + 8.06625710171825E-15, + 2.33252485755634E-14}, + {-1.74184545690134E-15, + -8.18003353124179E-16, + -6.62369006497819E-16, + 4.16303374396147E-15, + 7.06513748014024E-15}, + {-6.02936238677014E-15, + 1.89241084885229E-15, + 1.99097881944270E-17, + -6.99974290696640E-16, + -2.69504942597709E-17}, + {-4.65632962602379E-16, + 3.70281995445114E-18, + -9.04232973763345E-17, + 2.20847370761932E-17, + 7.62909453726566E-17}, + {-6.25921477907943E-17, + -2.10532795609842E-17, + -1.03808073867183E-17, + 1.15091380049019E-18, + 4.66794445408388E-19}, + {9.39427013576903E-18, + 9.17044662931859E-19, + 2.04132745117549E-18, + -1.72364063154625E-19, + -1.18098896532163E-18} }; -const double bnm_ch[91][5]= -{ - {0,0,0,0,0}, - {0,0,0,0,0}, - {3.44092035729033E-05,-1.21876825440561E-05,-1.87490665238967E-05,-2.60980336247863E-05,4.31639313264615E-06}, - {0,0,0,0,0}, - {-2.60125613000133E-05,1.70570295762269E-05,3.08331896996832E-05,1.66256596588688E-05,-1.07841055501996E-05}, - {8.74011641844073E-06,-2.25874169896607E-06,6.50985196673747E-07,1.30424765493752E-06,-1.85081244549542E-07}, - {0,0,0,0,0}, - {3.77496505484964E-05,-1.08198973553337E-05,-1.67717574544937E-05,-3.22476096673598E-05,1.12281888201134E-05}, - {-7.68623378647958E-07,-4.01400837153063E-06,-2.16390246700835E-06,-1.76912959937924E-06,-1.12740084951955E-06}, - {-2.37092815818895E-06,-9.52317223759653E-07,-2.22722065579131E-07,-6.25157619772530E-08,1.86582003894639E-08}, - {0,0,0,0,0}, - {-6.10254317785872E-05,-2.51815503068494E-05,2.01046207874667E-05,7.21107723367308E-06,-1.30692058660457E-05}, - {-9.60655417241537E-06,-7.31381721742373E-06,-2.52767927589636E-06,9.09039973214621E-07,-6.76454911344246E-07}, - {-2.25743206384908E-08,2.33058746737575E-07,2.24746779293445E-07,6.78551351968876E-08,1.25076011387284E-07}, - {-2.25744112770133E-07,-1.44429560891636E-07,-2.96810417448652E-08,-5.93858519742856E-08,-2.43210229455420E-08}, - {0,0,0,0,0}, - {7.45721015256308E-06,-3.81396821676410E-05,-1.41086198468687E-05,-2.28514517574713E-05,7.28638705683277E-06}, - {-5.77517778169692E-06,-3.93061211403839E-06,-2.17369763310752E-06,-1.48060935583664E-07,-2.74200485662814E-07}, - {4.52962035878238E-07,9.80990375495214E-07,4.67492045269286E-07,-8.31032252212116E-09,1.69426023427740E-07}, - {7.20536791795515E-10,2.75612253452141E-09,2.47772119382536E-09,4.30621825021233E-09,-2.86498479499428E-08}, - {-2.46253956492716E-08,-3.10300833499669E-09,8.06559148724445E-09,2.98197408430123E-10,6.32503656532846E-09}, - {0,0,0,0,0}, - {-6.01147094179306E-05,-3.16631758509869E-05,4.10038115100010E-06,3.55215057231403E-07,-2.23606515237408E-06}, - {-2.85937516921923E-06,-3.67775706610630E-06,-5.06445540401637E-07,8.21776759711184E-07,-5.98690271725558E-07}, - {7.77122595418965E-07,3.60896376754085E-07,3.88610487893381E-07,-4.39533892679537E-08,-6.26882227849174E-08}, - {1.05759993661891E-07,2.58009912408833E-08,-1.51356049060972E-08,-1.13335813107412E-09,5.37470857850370E-10}, - {7.99831506181984E-09,1.67423735327465E-09,2.94736760548677E-09,-1.56727133704788E-09,8.46186800849124E-10}, - {3.07727104043851E-09,3.93584215798484E-10,3.86721562770643E-11,1.72181091277391E-10,-2.16915737920145E-10}, - {0,0,0,0,0}, - {-1.16335389078126E-05,-1.39864676661484E-05,2.52546278407717E-06,-8.79152625440188E-06,-8.97665132187974E-06}, - {-3.95874550504316E-06,-1.17976262528730E-07,7.03189926369300E-07,3.38907065351535E-07,-3.67714052493558E-07}, - {2.29082449370440E-07,5.72961531093329E-07,4.21969662578894E-08,1.24112958141431E-08,9.56404486571888E-08}, - {1.44631865298671E-09,6.19368473895584E-09,1.67110424041236E-09,2.57979463602951E-09,-6.90806907510366E-09}, - {1.77235802019153E-09,-8.14388846228970E-10,4.50421956523579E-09,5.67452314909707E-10,2.47610443675560E-09}, - {4.85932343880617E-10,2.24864117422804E-10,-2.22534534468511E-10,-7.96395824973477E-11,3.12587399902493E-12}, - {-3.20173937255409E-11,-1.29872402028088E-11,-4.24092901203818E-11,2.66570185704416E-11,-5.25164954403909E-12}, - {0,0,0,0,0}, - {-1.36010179191872E-05,1.77873053642413E-05,4.80988546657119E-06,3.46859608161212E-06,-1.73247520896541E-06}, - {2.00020483116258E-06,2.43393064079673E-06,1.21478843695862E-06,1.95582820041644E-07,-3.11847995109088E-07}, - {-8.13287218979310E-09,1.05206830238665E-08,6.54040136224164E-09,-1.96402660575990E-08,-1.40379796070732E-08}, - {4.01291020310740E-08,2.92634301047947E-08,6.04179709273169E-09,8.61849065020545E-10,5.98065429697245E-09}, - {-1.06149335032911E-09,-4.39748495862323E-10,8.83040310269353E-10,3.49392227277679E-10,8.57722299002622E-10}, - {-1.25049888909390E-11,2.05203288281631E-10,1.37817670505319E-11,6.82057794430145E-11,-9.41515631694254E-11}, - {7.47196022644130E-12,-2.51369898528782E-11,-2.12196687809200E-11,1.55282119505201E-11,9.99224438231805E-12}, - {-7.90534019004874E-13,3.55824506982589E-12,8.00835777767281E-13,8.73460019069655E-13,1.34176126600106E-12}, - {0,0,0,0,0}, - {3.12855262465316E-05,1.31629386003608E-05,2.65598119437581E-06,8.68923340949135E-06,-7.51164082949678E-06}, - {1.56870792650533E-06,1.89227301685370E-06,4.15620385341985E-07,-2.74253787880603E-07,-4.28826210119200E-07}, - {-9.99176994565587E-08,-1.10785129426286E-07,-1.10318125091182E-07,6.22726507350764E-09,-3.39214566386250E-08}, - {1.24872975018433E-08,1.10663206077249E-08,5.40658975901469E-09,-2.79119137105115E-09,-2.47500096192502E-09}, - {1.11518917154060E-10,-4.21965763244849E-10,3.26786005211229E-10,1.93488254914545E-10,7.00774679999972E-10}, - {1.50889220040757E-10,1.03130002661366E-10,-3.09481760816903E-11,-4.47656630703759E-11,-7.36245021803800E-12}, - {-1.91144562110285E-12,-1.11355583995978E-11,-1.76207323352556E-11,8.15289793192265E-12,3.45078925412654E-12}, - {-2.73248710476019E-12,-1.65089342283056E-13,-2.20125355220819E-13,5.32589191504356E-13,5.70008982140874E-13}, - {8.06636928368811E-13,1.30893069976672E-13,9.72079137767479E-14,3.87410156264322E-14,-5.56410013263563E-14}, - {0,0,0,0,0}, - {2.02454485403216E-05,-9.77720471118669E-06,-4.35467548126223E-06,2.19599868869063E-06,-3.26670819043690E-06}, - {-3.21839256310540E-08,8.38760368015005E-07,-5.08058835724060E-07,4.16177282491396E-08,1.53842592762120E-07}, - {-1.57377633165313E-07,-7.86803586842404E-08,-7.40444711426898E-08,3.15259864117954E-08,5.60536231567172E-09}, - {-3.26080428920229E-10,-3.14576780695439E-09,8.46796096612981E-10,-2.59329379174262E-09,-8.01054756588382E-10}, - {-4.58725236153576E-11,-6.87847958546571E-11,8.18226480126754E-12,1.81082075625897E-10,1.74510532938256E-10}, - {7.60233505328792E-11,4.76463939581321E-11,-2.47198455442033E-11,-8.83439688929965E-12,5.93967446277316E-13}, - {-8.92919292558887E-12,-4.38524572312029E-12,-4.02709146060896E-12,4.84344426425295E-12,5.12869042781520E-12}, - {1.91518361809952E-12,3.06846255371817E-13,-2.44830265306345E-13,7.86297493099244E-14,2.72347805801980E-13}, - {9.09936624159538E-14,7.20650818861447E-15,2.45383991578283E-14,-4.79580974186462E-15,3.64604724046944E-14}, - {-4.63611142770709E-14,1.73908246420636E-15,-4.41651410674801E-15,-6.61409045306922E-16,-1.60016049099639E-15}, - {0,0,0,0,0}, - {6.17105245892845E-06,-1.04342983738457E-05,-1.72711741097994E-05,-8.16815967888426E-07,3.42789959967593E-06}, - {-2.44014060833825E-07,2.06991837444652E-07,-3.85805819475679E-07,1.67162359832166E-08,4.15139610402483E-07}, - {8.18199006804020E-08,-3.20013409049159E-08,5.94000906771151E-08,2.24122167188946E-08,-1.33796186160409E-08}, - {7.66269294674338E-11,-6.07862178874828E-10,4.95795757186248E-10,-3.07589245481422E-10,3.44456287710689E-10}, - {-1.84076250254929E-10,-1.30985312312781E-10,-1.52547325533276E-10,-2.51000125929512E-11,-1.93924012590455E-11}, - {-2.93307452197665E-11,2.88627386757582E-11,5.58812021182217E-12,-1.68692874069187E-13,1.80464313900575E-12}, - {-9.59053874473003E-13,6.04803122874761E-13,-9.80015608958536E-13,1.70530372034214E-12,1.70458664160775E-12}, - {2.80169588226043E-13,9.09573148053551E-14,2.16449186617004E-14,1.15550091496353E-13,4.97772796761321E-14}, - {-3.04524400761371E-14,3.42845631349694E-14,2.44230630602064E-14,5.76017546103056E-16,-9.74409465961093E-15}, - {5.98765340844291E-15,-2.63942474859535E-15,-1.80204805804437E-15,-1.84981819321183E-16,-5.85073392163660E-16}, - {-2.37069441910133E-15,2.87429226086856E-16,-1.67055963193389E-16,2.72110684914090E-18,8.46646962667892E-17}, - {0,0,0,0,0}, - {-2.71386164105722E-05,-1.41834938338454E-05,-2.00777928859929E-07,5.94329804681196E-07,8.61856994375586E-06}, - {-3.93656495458664E-08,-6.36432821807576E-07,-2.47887475106438E-07,-2.64906446204966E-08,1.10689794197004E-07}, - {5.25319489188562E-08,9.00866357158695E-09,5.00693379572512E-08,2.47269011056404E-08,-7.27648556194598E-09}, - {1.87207107149043E-09,-1.46428282396138E-09,-2.71812237167257E-10,8.44902265891466E-10,-5.62683870906027E-10}, - {-1.08295119666184E-10,4.75553388543793E-11,-5.49429386495686E-11,-6.60907871731611E-11,-5.97347322824822E-11}, - {-4.95118306815571E-12,5.31083735234970E-13,-1.93679746327378E-12,-1.61770521840510E-12,1.23276727202510E-11}, - {6.68582682909900E-13,7.38288575160449E-13,5.47630483499201E-13,-1.00770258118914E-13,-1.65564928475981E-13}, - {5.80963409268471E-14,6.93474288078737E-14,6.60728092794315E-15,-5.21029056725202E-15,-1.11283532854883E-16}, - {-4.10567742688903E-15,1.62252646805882E-14,1.00774699865989E-14,-2.44793214897877E-16,-1.59283906414563E-15}, - {1.84669506619904E-17,8.28473337813919E-17,-1.53400662078899E-16,-5.01060672199689E-17,-2.20727935766132E-16}, - {2.65355116203636E-16,-3.70233146147684E-17,3.52689394451586E-18,-8.62215942516328E-18,9.26909361974526E-18}, - {9.94266950643135E-17,4.17028699663441E-18,-7.65153491125819E-21,-5.62131270981041E-18,-3.03732817297438E-18} +const double bnm_ch[91][5] = { + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {3.44092035729033E-05, + -1.21876825440561E-05, + -1.87490665238967E-05, + -2.60980336247863E-05, + 4.31639313264615E-06}, + {0, 0, 0, 0, 0}, + {-2.60125613000133E-05, + 1.70570295762269E-05, + 3.08331896996832E-05, + 1.66256596588688E-05, + -1.07841055501996E-05}, + {8.74011641844073E-06, + -2.25874169896607E-06, + 6.50985196673747E-07, + 1.30424765493752E-06, + -1.85081244549542E-07}, + {0, 0, 0, 0, 0}, + {3.77496505484964E-05, + -1.08198973553337E-05, + -1.67717574544937E-05, + -3.22476096673598E-05, + 1.12281888201134E-05}, + {-7.68623378647958E-07, + -4.01400837153063E-06, + -2.16390246700835E-06, + -1.76912959937924E-06, + -1.12740084951955E-06}, + {-2.37092815818895E-06, + -9.52317223759653E-07, + -2.22722065579131E-07, + -6.25157619772530E-08, + 1.86582003894639E-08}, + {0, 0, 0, 0, 0}, + {-6.10254317785872E-05, + -2.51815503068494E-05, + 2.01046207874667E-05, + 7.21107723367308E-06, + -1.30692058660457E-05}, + {-9.60655417241537E-06, + -7.31381721742373E-06, + -2.52767927589636E-06, + 9.09039973214621E-07, + -6.76454911344246E-07}, + {-2.25743206384908E-08, + 2.33058746737575E-07, + 2.24746779293445E-07, + 6.78551351968876E-08, + 1.25076011387284E-07}, + {-2.25744112770133E-07, + -1.44429560891636E-07, + -2.96810417448652E-08, + -5.93858519742856E-08, + -2.43210229455420E-08}, + {0, 0, 0, 0, 0}, + {7.45721015256308E-06, + -3.81396821676410E-05, + -1.41086198468687E-05, + -2.28514517574713E-05, + 7.28638705683277E-06}, + {-5.77517778169692E-06, + -3.93061211403839E-06, + -2.17369763310752E-06, + -1.48060935583664E-07, + -2.74200485662814E-07}, + {4.52962035878238E-07, + 9.80990375495214E-07, + 4.67492045269286E-07, + -8.31032252212116E-09, + 1.69426023427740E-07}, + {7.20536791795515E-10, + 2.75612253452141E-09, + 2.47772119382536E-09, + 4.30621825021233E-09, + -2.86498479499428E-08}, + {-2.46253956492716E-08, + -3.10300833499669E-09, + 8.06559148724445E-09, + 2.98197408430123E-10, + 6.32503656532846E-09}, + {0, 0, 0, 0, 0}, + {-6.01147094179306E-05, + -3.16631758509869E-05, + 4.10038115100010E-06, + 3.55215057231403E-07, + -2.23606515237408E-06}, + {-2.85937516921923E-06, + -3.67775706610630E-06, + -5.06445540401637E-07, + 8.21776759711184E-07, + -5.98690271725558E-07}, + {7.77122595418965E-07, + 3.60896376754085E-07, + 3.88610487893381E-07, + -4.39533892679537E-08, + -6.26882227849174E-08}, + {1.05759993661891E-07, + 2.58009912408833E-08, + -1.51356049060972E-08, + -1.13335813107412E-09, + 5.37470857850370E-10}, + {7.99831506181984E-09, + 1.67423735327465E-09, + 2.94736760548677E-09, + -1.56727133704788E-09, + 8.46186800849124E-10}, + {3.07727104043851E-09, + 3.93584215798484E-10, + 3.86721562770643E-11, + 1.72181091277391E-10, + -2.16915737920145E-10}, + {0, 0, 0, 0, 0}, + {-1.16335389078126E-05, + -1.39864676661484E-05, + 2.52546278407717E-06, + -8.79152625440188E-06, + -8.97665132187974E-06}, + {-3.95874550504316E-06, + -1.17976262528730E-07, + 7.03189926369300E-07, + 3.38907065351535E-07, + -3.67714052493558E-07}, + {2.29082449370440E-07, + 5.72961531093329E-07, + 4.21969662578894E-08, + 1.24112958141431E-08, + 9.56404486571888E-08}, + {1.44631865298671E-09, + 6.19368473895584E-09, + 1.67110424041236E-09, + 2.57979463602951E-09, + -6.90806907510366E-09}, + {1.77235802019153E-09, + -8.14388846228970E-10, + 4.50421956523579E-09, + 5.67452314909707E-10, + 2.47610443675560E-09}, + {4.85932343880617E-10, + 2.24864117422804E-10, + -2.22534534468511E-10, + -7.96395824973477E-11, + 3.12587399902493E-12}, + {-3.20173937255409E-11, + -1.29872402028088E-11, + -4.24092901203818E-11, + 2.66570185704416E-11, + -5.25164954403909E-12}, + {0, 0, 0, 0, 0}, + {-1.36010179191872E-05, + 1.77873053642413E-05, + 4.80988546657119E-06, + 3.46859608161212E-06, + -1.73247520896541E-06}, + {2.00020483116258E-06, + 2.43393064079673E-06, + 1.21478843695862E-06, + 1.95582820041644E-07, + -3.11847995109088E-07}, + {-8.13287218979310E-09, + 1.05206830238665E-08, + 6.54040136224164E-09, + -1.96402660575990E-08, + -1.40379796070732E-08}, + {4.01291020310740E-08, + 2.92634301047947E-08, + 6.04179709273169E-09, + 8.61849065020545E-10, + 5.98065429697245E-09}, + {-1.06149335032911E-09, + -4.39748495862323E-10, + 8.83040310269353E-10, + 3.49392227277679E-10, + 8.57722299002622E-10}, + {-1.25049888909390E-11, + 2.05203288281631E-10, + 1.37817670505319E-11, + 6.82057794430145E-11, + -9.41515631694254E-11}, + {7.47196022644130E-12, + -2.51369898528782E-11, + -2.12196687809200E-11, + 1.55282119505201E-11, + 9.99224438231805E-12}, + {-7.90534019004874E-13, + 3.55824506982589E-12, + 8.00835777767281E-13, + 8.73460019069655E-13, + 1.34176126600106E-12}, + {0, 0, 0, 0, 0}, + {3.12855262465316E-05, + 1.31629386003608E-05, + 2.65598119437581E-06, + 8.68923340949135E-06, + -7.51164082949678E-06}, + {1.56870792650533E-06, + 1.89227301685370E-06, + 4.15620385341985E-07, + -2.74253787880603E-07, + -4.28826210119200E-07}, + {-9.99176994565587E-08, + -1.10785129426286E-07, + -1.10318125091182E-07, + 6.22726507350764E-09, + -3.39214566386250E-08}, + {1.24872975018433E-08, + 1.10663206077249E-08, + 5.40658975901469E-09, + -2.79119137105115E-09, + -2.47500096192502E-09}, + {1.11518917154060E-10, + -4.21965763244849E-10, + 3.26786005211229E-10, + 1.93488254914545E-10, + 7.00774679999972E-10}, + {1.50889220040757E-10, + 1.03130002661366E-10, + -3.09481760816903E-11, + -4.47656630703759E-11, + -7.36245021803800E-12}, + {-1.91144562110285E-12, + -1.11355583995978E-11, + -1.76207323352556E-11, + 8.15289793192265E-12, + 3.45078925412654E-12}, + {-2.73248710476019E-12, + -1.65089342283056E-13, + -2.20125355220819E-13, + 5.32589191504356E-13, + 5.70008982140874E-13}, + {8.06636928368811E-13, + 1.30893069976672E-13, + 9.72079137767479E-14, + 3.87410156264322E-14, + -5.56410013263563E-14}, + {0, 0, 0, 0, 0}, + {2.02454485403216E-05, + -9.77720471118669E-06, + -4.35467548126223E-06, + 2.19599868869063E-06, + -3.26670819043690E-06}, + {-3.21839256310540E-08, + 8.38760368015005E-07, + -5.08058835724060E-07, + 4.16177282491396E-08, + 1.53842592762120E-07}, + {-1.57377633165313E-07, + -7.86803586842404E-08, + -7.40444711426898E-08, + 3.15259864117954E-08, + 5.60536231567172E-09}, + {-3.26080428920229E-10, + -3.14576780695439E-09, + 8.46796096612981E-10, + -2.59329379174262E-09, + -8.01054756588382E-10}, + {-4.58725236153576E-11, + -6.87847958546571E-11, + 8.18226480126754E-12, + 1.81082075625897E-10, + 1.74510532938256E-10}, + {7.60233505328792E-11, + 4.76463939581321E-11, + -2.47198455442033E-11, + -8.83439688929965E-12, + 5.93967446277316E-13}, + {-8.92919292558887E-12, + -4.38524572312029E-12, + -4.02709146060896E-12, + 4.84344426425295E-12, + 5.12869042781520E-12}, + {1.91518361809952E-12, + 3.06846255371817E-13, + -2.44830265306345E-13, + 7.86297493099244E-14, + 2.72347805801980E-13}, + {9.09936624159538E-14, + 7.20650818861447E-15, + 2.45383991578283E-14, + -4.79580974186462E-15, + 3.64604724046944E-14}, + {-4.63611142770709E-14, + 1.73908246420636E-15, + -4.41651410674801E-15, + -6.61409045306922E-16, + -1.60016049099639E-15}, + {0, 0, 0, 0, 0}, + {6.17105245892845E-06, + -1.04342983738457E-05, + -1.72711741097994E-05, + -8.16815967888426E-07, + 3.42789959967593E-06}, + {-2.44014060833825E-07, + 2.06991837444652E-07, + -3.85805819475679E-07, + 1.67162359832166E-08, + 4.15139610402483E-07}, + {8.18199006804020E-08, + -3.20013409049159E-08, + 5.94000906771151E-08, + 2.24122167188946E-08, + -1.33796186160409E-08}, + {7.66269294674338E-11, + -6.07862178874828E-10, + 4.95795757186248E-10, + -3.07589245481422E-10, + 3.44456287710689E-10}, + {-1.84076250254929E-10, + -1.30985312312781E-10, + -1.52547325533276E-10, + -2.51000125929512E-11, + -1.93924012590455E-11}, + {-2.93307452197665E-11, + 2.88627386757582E-11, + 5.58812021182217E-12, + -1.68692874069187E-13, + 1.80464313900575E-12}, + {-9.59053874473003E-13, + 6.04803122874761E-13, + -9.80015608958536E-13, + 1.70530372034214E-12, + 1.70458664160775E-12}, + {2.80169588226043E-13, + 9.09573148053551E-14, + 2.16449186617004E-14, + 1.15550091496353E-13, + 4.97772796761321E-14}, + {-3.04524400761371E-14, + 3.42845631349694E-14, + 2.44230630602064E-14, + 5.76017546103056E-16, + -9.74409465961093E-15}, + {5.98765340844291E-15, + -2.63942474859535E-15, + -1.80204805804437E-15, + -1.84981819321183E-16, + -5.85073392163660E-16}, + {-2.37069441910133E-15, + 2.87429226086856E-16, + -1.67055963193389E-16, + 2.72110684914090E-18, + 8.46646962667892E-17}, + {0, 0, 0, 0, 0}, + {-2.71386164105722E-05, + -1.41834938338454E-05, + -2.00777928859929E-07, + 5.94329804681196E-07, + 8.61856994375586E-06}, + {-3.93656495458664E-08, + -6.36432821807576E-07, + -2.47887475106438E-07, + -2.64906446204966E-08, + 1.10689794197004E-07}, + {5.25319489188562E-08, + 9.00866357158695E-09, + 5.00693379572512E-08, + 2.47269011056404E-08, + -7.27648556194598E-09}, + {1.87207107149043E-09, + -1.46428282396138E-09, + -2.71812237167257E-10, + 8.44902265891466E-10, + -5.62683870906027E-10}, + {-1.08295119666184E-10, + 4.75553388543793E-11, + -5.49429386495686E-11, + -6.60907871731611E-11, + -5.97347322824822E-11}, + {-4.95118306815571E-12, + 5.31083735234970E-13, + -1.93679746327378E-12, + -1.61770521840510E-12, + 1.23276727202510E-11}, + {6.68582682909900E-13, + 7.38288575160449E-13, + 5.47630483499201E-13, + -1.00770258118914E-13, + -1.65564928475981E-13}, + {5.80963409268471E-14, + 6.93474288078737E-14, + 6.60728092794315E-15, + -5.21029056725202E-15, + -1.11283532854883E-16}, + {-4.10567742688903E-15, + 1.62252646805882E-14, + 1.00774699865989E-14, + -2.44793214897877E-16, + -1.59283906414563E-15}, + {1.84669506619904E-17, + 8.28473337813919E-17, + -1.53400662078899E-16, + -5.01060672199689E-17, + -2.20727935766132E-16}, + {2.65355116203636E-16, + -3.70233146147684E-17, + 3.52689394451586E-18, + -8.62215942516328E-18, + 9.26909361974526E-18}, + {9.94266950643135E-17, + 4.17028699663441E-18, + -7.65153491125819E-21, + -5.62131270981041E-18, + -3.03732817297438E-18} }; -const double bnm_cw[91][5]= -{ - {0,0,0,0,0}, - {0,0,0,0,0}, - {-0.000209104872912563,-1.41530274973540E-05,3.00318745764815E-05,-1.82864291318284E-05,-7.62965409959238E-06}, - {0,0,0,0,0}, - {-0.000186336519900275,0.000191256553935638,7.28356195304996E-05,3.59637869639906E-05,-2.53927226167388E-05}, - {0.000108195343799485,-6.97050977217619E-05,-6.68037133871099E-05,2.30387653190503E-05,-1.22735483925784E-05}, - {0,0,0,0,0}, - {0.000119941091277039,-7.70547844186875E-05,-8.15376297964528E-05,1.06005789545203E-05,2.31177232268720E-05}, - {-1.77494760217164E-05,-1.37061385686605E-05,-1.74805936475816E-05,-6.91745900867532E-07,-7.10231790947787E-06}, - {-1.47564103733219E-05,2.08890785485260E-06,3.19876879447867E-06,9.43984664503715E-07,-4.90480527577521E-06}, - {0,0,0,0,0}, - {4.93300138389457E-05,-6.77641298460617E-05,-3.25043347246397E-05,8.33226714911921E-06,8.11499972792905E-06}, - {-2.80449863471272E-05,-1.04367606414606E-05,1.64473584641163E-07,-3.57420965807816E-06,2.95887156564038E-06}, - {1.88835280111533E-06,5.69125761193702E-07,-2.22757382799409E-06,-1.96699131032252E-07,-2.91861219283659E-07}, - {-4.69918971436680E-06,-7.00778948636735E-07,2.97544157334673E-09,3.86100512544410E-07,2.30939653701027E-07}, - {0,0,0,0,0}, - {1.77050610394149E-05,-3.18353071311574E-05,3.04232260950316E-05,-6.26821316488169E-05,-1.75094810002378E-06}, - {9.25605901565775E-06,-8.25179123302247E-06,6.74032752408358E-06,3.22192289084524E-06,6.09414500075259E-06}, - {4.28233825242200E-06,2.10470570087927E-07,-4.75050074985668E-07,-4.89382663470592E-07,8.75232347469207E-07}, - {8.50393520366934E-07,1.58764911467186E-07,-2.16267638321210E-07,-7.43341300487416E-10,1.75131729813230E-07}, - {-2.87064111623119E-07,4.50393893102830E-08,6.63315044416690E-08,7.61199387418853E-08,-6.05694385243652E-09}, - {0,0,0,0,0}, - {-1.95692079507947E-05,5.15486098887851E-05,3.00852761598173E-05,1.21485028343416E-05,-6.72450521493428E-06}, - {5.34496867088158E-06,3.90973451680699E-06,3.70148924718425E-06,5.73731499938212E-08,5.52258220288780E-07}, - {3.39950838185315E-07,-5.63443976772634E-07,4.52082211980595E-07,-2.57094645806243E-07,-6.84885762924729E-08}, - {2.15793276880684E-07,2.05911354090873E-07,1.33747872341142E-08,-2.07997626478952E-08,-3.69812938736019E-08}, - {2.11952749403224E-09,4.04317822544732E-08,2.40972024883650E-09,8.56289126938059E-09,2.31035283490200E-08}, - {-2.08402298813248E-09,-8.50243600879112E-09,2.60895410117768E-09,-6.69156841738591E-10,-5.16280278087006E-09}, - {0,0,0,0,0}, - {0.000124901291436683,-5.70770326719086E-05,-8.44887248105015E-05,-3.11442665354698E-05,-1.12982893252046E-05}, - {-8.38934444233944E-06,1.56860091415414E-06,-1.77704563531825E-06,-5.70219068898717E-08,-4.30377735031244E-06}, - {3.72965318017681E-07,6.98175439446187E-07,1.75760544807919E-08,1.59731284857151E-07,3.62363848767891E-07}, - {-2.32148850787091E-07,-4.21888751852973E-08,8.35926113952108E-08,-2.24572480575674E-08,-6.92114100904503E-08}, - {-2.92635642210745E-09,3.38086229163415E-09,4.72186694662901E-09,-8.32354437305758E-11,4.19673890995627E-09}, - {-1.26452887692900E-09,1.91309690886864E-09,1.54755631983655E-09,-1.09865169400249E-09,1.83645326319994E-10}, - {9.92539437011905E-10,-2.96318203488300E-10,1.17466020823486E-10,-5.00185957995526E-10,-8.54777591408537E-11}, - {0,0,0,0,0}, - {-0.000182885335404854,7.27424724520089E-05,3.05286278023427E-05,2.55324463432562E-05,-6.39859510763234E-06}, - {-5.21449265232557E-06,-6.70572386081398E-06,-3.95473351292738E-06,-6.41023334372861E-07,-3.11616331059009E-06}, - {2.37090789071727E-07,3.58427517014705E-07,2.55709192777007E-07,8.44593804408541E-08,9.27243162355359E-09}, - {7.24370898432057E-08,-7.43945120337710E-09,8.61751911975683E-10,-2.34651212610623E-08,2.94052921681456E-09}, - {-1.22127317934425E-08,-3.89758984276768E-09,4.12890383904924E-11,2.06528068002723E-09,1.73488696972270E-09}, - {-5.44137406907620E-10,-4.81034553189921E-10,-2.56101759039694E-11,3.21880564410154E-10,-2.70195343165250E-11}, - {1.08394225300546E-10,-7.99525492688661E-11,1.73850287030654E-10,-8.06390014426271E-11,-7.63143364291160E-13}, - {-3.41446959267441E-11,2.72675729042792E-11,5.69674704865345E-12,-3.38402998344892E-12,-2.96732381931007E-12}, - {0,0,0,0,0}, - {2.91161315987250E-05,-7.24641166590735E-05,-8.58323519857884E-06,-1.14037444255820E-05,1.32244819451517E-05}, - {1.24266748259826E-06,-4.13127038469802E-06,-8.47496394492885E-07,5.48722958754267E-07,-1.98288551821205E-06}, - {-1.70671245196917E-08,1.36891127083540E-08,-2.80901972249870E-07,-5.45369793946222E-09,-9.58796303763498E-08}, - {1.14115335901746E-08,2.79308166429178E-08,-1.71144803132413E-08,4.86116243565380E-09,-8.13061459952280E-09}, - {-1.19144311035824E-09,-1.28197815211763E-09,-1.22313592972373E-09,6.23116336753674E-10,2.11527825898689E-09}, - {4.94618645030426E-10,-1.01554483531252E-10,-3.58808808952276E-10,1.23499783028794E-10,-1.21017599361833E-10}, - {1.33959569836451E-10,-1.87140898812283E-11,-3.04265350158941E-11,-1.42907553051431E-11,-1.09873858099638E-11}, - {1.30277419203512E-11,-4.95312627777245E-12,2.23070215544358E-12,1.66450226016423E-12,6.26222944728474E-12}, - {-4.40721204874728E-12,2.99575133064885E-12,-1.54917262009097E-12,8.90015664527060E-14,-1.59135267012937E-12}, - {0,0,0,0,0}, - {-4.17667211323160E-05,1.39005215116294E-05,1.46521361817829E-05,3.23485458024416E-05,-8.57936261085263E-06}, - {9.48491026524450E-07,1.67749735481991E-06,6.80159475477603E-07,-1.34558044496631E-06,1.62108231492249E-06}, - {-2.67545753355631E-07,-3.31848493018159E-08,1.05837219557465E-07,1.55587655479400E-07,-2.84996014386667E-08}, - {-5.15113778734878E-08,8.83630725241303E-09,3.36579455982772E-09,-6.22350102096402E-09,5.03959133095369E-09}, - {2.04635880823035E-11,-1.07923589059151E-09,-6.96482137669712E-10,-4.70238500452793E-10,-6.60277903598297E-10}, - {-2.41897168749189E-11,1.33547763615216E-10,-5.13534673658908E-11,-8.32767177662817E-11,5.72614717082428E-11}, - {7.55170562359940E-12,-1.57123461699055E-11,-1.48874069619124E-11,-7.10529462981252E-13,-7.99006335025107E-12}, - {2.41883156738960E-12,2.97346980183361E-12,1.28719977731450E-12,-2.49240876894143E-12,6.71155595793198E-13}, - {4.16995565336914E-13,-1.71584521275288E-13,-7.23064067359978E-14,2.45405880599037E-13,4.43532934905830E-13}, - {3.56937508828997E-14,2.43012511260300E-14,-7.96090778289326E-14,-1.59548529636358E-14,8.99103763000507E-15}, - {0,0,0,0,0}, - {0.000117579258399489,-4.52648448635772E-05,-2.69130037097862E-05,-3.82266335794366E-05,-4.36549257701084E-06}, - {-1.43270371215502E-06,1.21565440183855E-06,8.53701136074284E-07,1.52709810023665E-06,1.22382663462904E-06}, - {3.06089147519664E-07,9.79084123751975E-08,7.96524661441178E-08,4.54770947973458E-08,2.22842369458882E-07}, - {-9.94254707745127E-09,1.43251376378012E-08,1.93911753685160E-08,-6.52214645690987E-09,-1.97114016452408E-09}, - {-9.20751919828404E-10,-9.44312829629076E-10,7.24196738163952E-11,-6.71801072324561E-11,2.33146774065873E-10}, - {-1.43544298956410E-11,1.78464235318769E-10,7.69950023012326E-11,-4.22390057304453E-12,3.05176324574816E-11}, - {-7.88053753973990E-12,-3.20207793051003E-12,1.01527407317625E-12,6.02788185858449E-12,1.14919530900453E-11}, - {-1.21558899266069E-12,5.31300597882986E-13,3.44023865079264E-13,-6.22598216726224E-14,-5.47031650765402E-14}, - {-4.15627948750943E-13,2.77620907292721E-13,-8.99784134364011E-14,1.07254247320864E-13,6.85990080564196E-14}, - {-3.91837863922901E-14,9.74714976816180E-15,6.79982450963903E-15,-2.41420876658572E-15,-2.20889384455344E-15}, - {9.25912068402776E-15,-4.02621719248224E-15,-2.43952036351187E-15,-1.97006876049866E-15,1.03065621527869E-16}, - {0,0,0,0,0}, - {-0.000103762036940193,4.38145356960292E-05,2.43406920349913E-05,7.89103527673736E-06,-1.66841465339160E-05}, - {-1.18428449371744E-06,-1.30188721737259E-06,-1.88013557116650E-06,-1.01342046295303E-06,9.21813037802502E-07}, - {1.51836068712460E-07,1.11362553803933E-07,1.55375052233052E-07,1.94450910788747E-09,-1.73093755828342E-08}, - {-3.77758211813121E-09,1.23323969583610E-08,1.72510045250302E-09,-1.88609789458597E-09,1.28937597985937E-09}, - {-1.07947760393523E-09,5.26051570105365E-10,-3.67657536332496E-11,3.16110123523840E-10,-3.24273198242170E-10}, - {-2.00385649209820E-12,2.54703869682390E-11,4.08563622440851E-12,-4.83350348928636E-11,-3.98153443845079E-13}, - {2.73094467727215E-12,5.08900664114903E-12,-7.66669089075134E-13,2.50015592643012E-12,4.29763262853853E-12}, - {6.53946487537890E-13,-2.24958413781008E-13,6.74638861781238E-15,3.28537647613903E-14,2.54199700290116E-13}, - {-1.09122051193505E-13,8.36362392931501E-14,-3.90750153912300E-14,-5.44915910741950E-14,2.43816947219217E-14}, - {-1.41882561550134E-14,1.00455397812713E-14,2.63347255121581E-15,1.53043256823601E-15,2.49081021428095E-15}, - {-1.17256193152654E-15,1.05648985031971E-16,1.31778372453016E-16,1.44815198666577E-16,-3.72532768618480E-16}, - {2.66203457773766E-16,-7.67224608659658E-17,3.51487351031864E-18,4.10287131339291E-17,-6.72171711728514E-17} +const double bnm_cw[91][5] = { + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {-0.000209104872912563, + -1.41530274973540E-05, + 3.00318745764815E-05, + -1.82864291318284E-05, + -7.62965409959238E-06}, + {0, 0, 0, 0, 0}, + {-0.000186336519900275, + 0.000191256553935638, + 7.28356195304996E-05, + 3.59637869639906E-05, + -2.53927226167388E-05}, + {0.000108195343799485, + -6.97050977217619E-05, + -6.68037133871099E-05, + 2.30387653190503E-05, + -1.22735483925784E-05}, + {0, 0, 0, 0, 0}, + {0.000119941091277039, + -7.70547844186875E-05, + -8.15376297964528E-05, + 1.06005789545203E-05, + 2.31177232268720E-05}, + {-1.77494760217164E-05, + -1.37061385686605E-05, + -1.74805936475816E-05, + -6.91745900867532E-07, + -7.10231790947787E-06}, + {-1.47564103733219E-05, + 2.08890785485260E-06, + 3.19876879447867E-06, + 9.43984664503715E-07, + -4.90480527577521E-06}, + {0, 0, 0, 0, 0}, + {4.93300138389457E-05, + -6.77641298460617E-05, + -3.25043347246397E-05, + 8.33226714911921E-06, + 8.11499972792905E-06}, + {-2.80449863471272E-05, + -1.04367606414606E-05, + 1.64473584641163E-07, + -3.57420965807816E-06, + 2.95887156564038E-06}, + {1.88835280111533E-06, + 5.69125761193702E-07, + -2.22757382799409E-06, + -1.96699131032252E-07, + -2.91861219283659E-07}, + {-4.69918971436680E-06, + -7.00778948636735E-07, + 2.97544157334673E-09, + 3.86100512544410E-07, + 2.30939653701027E-07}, + {0, 0, 0, 0, 0}, + {1.77050610394149E-05, + -3.18353071311574E-05, + 3.04232260950316E-05, + -6.26821316488169E-05, + -1.75094810002378E-06}, + {9.25605901565775E-06, + -8.25179123302247E-06, + 6.74032752408358E-06, + 3.22192289084524E-06, + 6.09414500075259E-06}, + {4.28233825242200E-06, + 2.10470570087927E-07, + -4.75050074985668E-07, + -4.89382663470592E-07, + 8.75232347469207E-07}, + {8.50393520366934E-07, + 1.58764911467186E-07, + -2.16267638321210E-07, + -7.43341300487416E-10, + 1.75131729813230E-07}, + {-2.87064111623119E-07, + 4.50393893102830E-08, + 6.63315044416690E-08, + 7.61199387418853E-08, + -6.05694385243652E-09}, + {0, 0, 0, 0, 0}, + {-1.95692079507947E-05, + 5.15486098887851E-05, + 3.00852761598173E-05, + 1.21485028343416E-05, + -6.72450521493428E-06}, + {5.34496867088158E-06, + 3.90973451680699E-06, + 3.70148924718425E-06, + 5.73731499938212E-08, + 5.52258220288780E-07}, + {3.39950838185315E-07, + -5.63443976772634E-07, + 4.52082211980595E-07, + -2.57094645806243E-07, + -6.84885762924729E-08}, + {2.15793276880684E-07, + 2.05911354090873E-07, + 1.33747872341142E-08, + -2.07997626478952E-08, + -3.69812938736019E-08}, + {2.11952749403224E-09, + 4.04317822544732E-08, + 2.40972024883650E-09, + 8.56289126938059E-09, + 2.31035283490200E-08}, + {-2.08402298813248E-09, + -8.50243600879112E-09, + 2.60895410117768E-09, + -6.69156841738591E-10, + -5.16280278087006E-09}, + {0, 0, 0, 0, 0}, + {0.000124901291436683, + -5.70770326719086E-05, + -8.44887248105015E-05, + -3.11442665354698E-05, + -1.12982893252046E-05}, + {-8.38934444233944E-06, + 1.56860091415414E-06, + -1.77704563531825E-06, + -5.70219068898717E-08, + -4.30377735031244E-06}, + {3.72965318017681E-07, + 6.98175439446187E-07, + 1.75760544807919E-08, + 1.59731284857151E-07, + 3.62363848767891E-07}, + {-2.32148850787091E-07, + -4.21888751852973E-08, + 8.35926113952108E-08, + -2.24572480575674E-08, + -6.92114100904503E-08}, + {-2.92635642210745E-09, + 3.38086229163415E-09, + 4.72186694662901E-09, + -8.32354437305758E-11, + 4.19673890995627E-09}, + {-1.26452887692900E-09, + 1.91309690886864E-09, + 1.54755631983655E-09, + -1.09865169400249E-09, + 1.83645326319994E-10}, + {9.92539437011905E-10, + -2.96318203488300E-10, + 1.17466020823486E-10, + -5.00185957995526E-10, + -8.54777591408537E-11}, + {0, 0, 0, 0, 0}, + {-0.000182885335404854, + 7.27424724520089E-05, + 3.05286278023427E-05, + 2.55324463432562E-05, + -6.39859510763234E-06}, + {-5.21449265232557E-06, + -6.70572386081398E-06, + -3.95473351292738E-06, + -6.41023334372861E-07, + -3.11616331059009E-06}, + {2.37090789071727E-07, + 3.58427517014705E-07, + 2.55709192777007E-07, + 8.44593804408541E-08, + 9.27243162355359E-09}, + {7.24370898432057E-08, + -7.43945120337710E-09, + 8.61751911975683E-10, + -2.34651212610623E-08, + 2.94052921681456E-09}, + {-1.22127317934425E-08, + -3.89758984276768E-09, + 4.12890383904924E-11, + 2.06528068002723E-09, + 1.73488696972270E-09}, + {-5.44137406907620E-10, + -4.81034553189921E-10, + -2.56101759039694E-11, + 3.21880564410154E-10, + -2.70195343165250E-11}, + {1.08394225300546E-10, + -7.99525492688661E-11, + 1.73850287030654E-10, + -8.06390014426271E-11, + -7.63143364291160E-13}, + {-3.41446959267441E-11, + 2.72675729042792E-11, + 5.69674704865345E-12, + -3.38402998344892E-12, + -2.96732381931007E-12}, + {0, 0, 0, 0, 0}, + {2.91161315987250E-05, + -7.24641166590735E-05, + -8.58323519857884E-06, + -1.14037444255820E-05, + 1.32244819451517E-05}, + {1.24266748259826E-06, + -4.13127038469802E-06, + -8.47496394492885E-07, + 5.48722958754267E-07, + -1.98288551821205E-06}, + {-1.70671245196917E-08, + 1.36891127083540E-08, + -2.80901972249870E-07, + -5.45369793946222E-09, + -9.58796303763498E-08}, + {1.14115335901746E-08, + 2.79308166429178E-08, + -1.71144803132413E-08, + 4.86116243565380E-09, + -8.13061459952280E-09}, + {-1.19144311035824E-09, + -1.28197815211763E-09, + -1.22313592972373E-09, + 6.23116336753674E-10, + 2.11527825898689E-09}, + {4.94618645030426E-10, + -1.01554483531252E-10, + -3.58808808952276E-10, + 1.23499783028794E-10, + -1.21017599361833E-10}, + {1.33959569836451E-10, + -1.87140898812283E-11, + -3.04265350158941E-11, + -1.42907553051431E-11, + -1.09873858099638E-11}, + {1.30277419203512E-11, + -4.95312627777245E-12, + 2.23070215544358E-12, + 1.66450226016423E-12, + 6.26222944728474E-12}, + {-4.40721204874728E-12, + 2.99575133064885E-12, + -1.54917262009097E-12, + 8.90015664527060E-14, + -1.59135267012937E-12}, + {0, 0, 0, 0, 0}, + {-4.17667211323160E-05, + 1.39005215116294E-05, + 1.46521361817829E-05, + 3.23485458024416E-05, + -8.57936261085263E-06}, + {9.48491026524450E-07, + 1.67749735481991E-06, + 6.80159475477603E-07, + -1.34558044496631E-06, + 1.62108231492249E-06}, + {-2.67545753355631E-07, + -3.31848493018159E-08, + 1.05837219557465E-07, + 1.55587655479400E-07, + -2.84996014386667E-08}, + {-5.15113778734878E-08, + 8.83630725241303E-09, + 3.36579455982772E-09, + -6.22350102096402E-09, + 5.03959133095369E-09}, + {2.04635880823035E-11, + -1.07923589059151E-09, + -6.96482137669712E-10, + -4.70238500452793E-10, + -6.60277903598297E-10}, + {-2.41897168749189E-11, + 1.33547763615216E-10, + -5.13534673658908E-11, + -8.32767177662817E-11, + 5.72614717082428E-11}, + {7.55170562359940E-12, + -1.57123461699055E-11, + -1.48874069619124E-11, + -7.10529462981252E-13, + -7.99006335025107E-12}, + {2.41883156738960E-12, + 2.97346980183361E-12, + 1.28719977731450E-12, + -2.49240876894143E-12, + 6.71155595793198E-13}, + {4.16995565336914E-13, + -1.71584521275288E-13, + -7.23064067359978E-14, + 2.45405880599037E-13, + 4.43532934905830E-13}, + {3.56937508828997E-14, + 2.43012511260300E-14, + -7.96090778289326E-14, + -1.59548529636358E-14, + 8.99103763000507E-15}, + {0, 0, 0, 0, 0}, + {0.000117579258399489, + -4.52648448635772E-05, + -2.69130037097862E-05, + -3.82266335794366E-05, + -4.36549257701084E-06}, + {-1.43270371215502E-06, + 1.21565440183855E-06, + 8.53701136074284E-07, + 1.52709810023665E-06, + 1.22382663462904E-06}, + {3.06089147519664E-07, + 9.79084123751975E-08, + 7.96524661441178E-08, + 4.54770947973458E-08, + 2.22842369458882E-07}, + {-9.94254707745127E-09, + 1.43251376378012E-08, + 1.93911753685160E-08, + -6.52214645690987E-09, + -1.97114016452408E-09}, + {-9.20751919828404E-10, + -9.44312829629076E-10, + 7.24196738163952E-11, + -6.71801072324561E-11, + 2.33146774065873E-10}, + {-1.43544298956410E-11, + 1.78464235318769E-10, + 7.69950023012326E-11, + -4.22390057304453E-12, + 3.05176324574816E-11}, + {-7.88053753973990E-12, + -3.20207793051003E-12, + 1.01527407317625E-12, + 6.02788185858449E-12, + 1.14919530900453E-11}, + {-1.21558899266069E-12, + 5.31300597882986E-13, + 3.44023865079264E-13, + -6.22598216726224E-14, + -5.47031650765402E-14}, + {-4.15627948750943E-13, + 2.77620907292721E-13, + -8.99784134364011E-14, + 1.07254247320864E-13, + 6.85990080564196E-14}, + {-3.91837863922901E-14, + 9.74714976816180E-15, + 6.79982450963903E-15, + -2.41420876658572E-15, + -2.20889384455344E-15}, + {9.25912068402776E-15, + -4.02621719248224E-15, + -2.43952036351187E-15, + -1.97006876049866E-15, + 1.03065621527869E-16}, + {0, 0, 0, 0, 0}, + {-0.000103762036940193, + 4.38145356960292E-05, + 2.43406920349913E-05, + 7.89103527673736E-06, + -1.66841465339160E-05}, + {-1.18428449371744E-06, + -1.30188721737259E-06, + -1.88013557116650E-06, + -1.01342046295303E-06, + 9.21813037802502E-07}, + {1.51836068712460E-07, + 1.11362553803933E-07, + 1.55375052233052E-07, + 1.94450910788747E-09, + -1.73093755828342E-08}, + {-3.77758211813121E-09, + 1.23323969583610E-08, + 1.72510045250302E-09, + -1.88609789458597E-09, + 1.28937597985937E-09}, + {-1.07947760393523E-09, + 5.26051570105365E-10, + -3.67657536332496E-11, + 3.16110123523840E-10, + -3.24273198242170E-10}, + {-2.00385649209820E-12, + 2.54703869682390E-11, + 4.08563622440851E-12, + -4.83350348928636E-11, + -3.98153443845079E-13}, + {2.73094467727215E-12, + 5.08900664114903E-12, + -7.66669089075134E-13, + 2.50015592643012E-12, + 4.29763262853853E-12}, + {6.53946487537890E-13, + -2.24958413781008E-13, + 6.74638861781238E-15, + 3.28537647613903E-14, + 2.54199700290116E-13}, + {-1.09122051193505E-13, + 8.36362392931501E-14, + -3.90750153912300E-14, + -5.44915910741950E-14, + 2.43816947219217E-14}, + {-1.41882561550134E-14, + 1.00455397812713E-14, + 2.63347255121581E-15, + 1.53043256823601E-15, + 2.49081021428095E-15}, + {-1.17256193152654E-15, + 1.05648985031971E-16, + 1.31778372453016E-16, + 1.44815198666577E-16, + -3.72532768618480E-16}, + {2.66203457773766E-16, + -7.67224608659658E-17, + 3.51487351031864E-18, + 4.10287131339291E-17, + -6.72171711728514E-17} }; - /** read orography file */ -void readorog( - string filepath) ///< filename +void readorog(string filepath) ///< filename { - globalVMF3.orography.clear(); + globalVMF3.orography.clear(); - ifstream filestream(filepath); - if (!filestream) - { - BOOST_LOG_TRIVIAL(error) - << "Error opening orography file" << filepath << "\n"; - return; - } + ifstream filestream(filepath); + if (!filestream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening orography file" << filepath << "\n"; + return; + } - while (filestream) - { - string line; + while (filestream) + { + string line; - getline(filestream, line); + getline(filestream, line); - char* buff = &line[0]; + char* buff = &line[0]; - double val; - int found = sscanf(buff, "%lf", &val); + double val; + int found = sscanf(buff, "%lf", &val); - if (found) - { - globalVMF3.orography.push_back(val); - } - } + if (found) + { + globalVMF3.orography.push_back(val); + } + } - globalVMF3.orographyLength = globalVMF3.orography.size(); + globalVMF3.orographyLength = globalVMF3.orography.size(); - if ( globalVMF3.gridLength != 0 - &&globalVMF3.gridLength != globalVMF3.orographyLength) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Orography and VMF3 file grid dimensions do not match"; - } + if (globalVMF3.gridLength != 0 && globalVMF3.gridLength != globalVMF3.orographyLength) + { + BOOST_LOG_TRIVIAL(error) << "Orography and VMF3 file grid dimensions do not match"; + } } /** read vmf3 grid */ -void readvmf3( - string filepath) ///< vmf3 grid file path +void readvmf3(string filepath) ///< vmf3 grid file path { - ifstream filestream(filepath); - if (!filestream) - { - BOOST_LOG_TRIVIAL(error) - << "Error opening vmf3 file" << filepath << "\n"; - return; - } - -// int found = sscanf(file.c_str(), " -/** update grid for epoch - sprintf(gfile, "%sVMF3_%4d%02d%02d.H%02d", dir, (int)ep1[0], (int)ep1[1], (int)ep1[2], (int)ep1[3]); -*/ - - int index = 0; - GTime time = GTime::noTime(); - - while (filestream) - { - string line; - - getline(filestream, line); - - char* buff = &line[0]; - - if (strchr(buff, '!')) - { - GEpoch epoch; - int found = sscanf(buff, "! Epoch: %lf %lf %lf %lf %lf %lf", - &epoch.year, - &epoch.month, - &epoch.day, - &epoch.hour, - &epoch.min, - &epoch.sec); - if (found == 6) - { - time = epoch; - } - - continue; - } - - if (time == GTime::noTime()) - { - BOOST_LOG_TRIVIAL(error) - << "Failed to get time from vmf file '" << filepath << "'"; - return; - } - - Vmf3GridPoint gridPoint; - - int found = sscanf(buff, "%lf %lf %lf %lf %lf %lf", - &gridPoint.lat, - &gridPoint.lon, - &gridPoint.ah, - &gridPoint.aw, - &gridPoint.zhd, - &gridPoint.zwd); - - gridPoint.index = index; - index++; - - if (found == 6) - { - globalVMF3[time][gridPoint.lat][gridPoint.lon] = gridPoint; - } - } - - globalVMF3.gridLength = index; - - if ( globalVMF3.orographyLength != 0 - &&globalVMF3.orographyLength != globalVMF3.gridLength) - { - BOOST_LOG_TRIVIAL(error) - << "Error: Orography and VMF3 file grid dimensions do not match"; - } + ifstream filestream(filepath); + if (!filestream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening vmf3 file" << filepath << "\n"; + return; + } + + // int found = sscanf(file.c_str(), " + /** update grid for epoch + sprintf(gfile, "%sVMF3_%4d%02d%02d.H%02d", dir, (int)ep1[0], (int)ep1[1], + (int)ep1[2], (int)ep1[3]); + */ + + int index = 0; + GTime time = GTime::noTime(); + + while (filestream) + { + string line; + + getline(filestream, line); + + char* buff = &line[0]; + + if (strchr(buff, '!')) + { + GEpoch epoch; + int found = sscanf( + buff, + "! Epoch: %lf %lf %lf %lf %lf %lf", + &epoch.year, + &epoch.month, + &epoch.day, + &epoch.hour, + &epoch.min, + &epoch.sec + ); + if (found == 6) + { + time = epoch; + } + + continue; + } + + if (time == GTime::noTime()) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get time from vmf file '" << filepath << "'"; + return; + } + + Vmf3GridPoint gridPoint; + + int found = sscanf( + buff, + "%lf %lf %lf %lf %lf %lf", + &gridPoint.lat, + &gridPoint.lon, + &gridPoint.ah, + &gridPoint.aw, + &gridPoint.zhd, + &gridPoint.zwd + ); + + gridPoint.index = index; + index++; + + if (found == 6) + { + globalVMF3[time][gridPoint.lat][gridPoint.lon] = gridPoint; + } + } + + globalVMF3.gridLength = index; + + if (globalVMF3.orographyLength != 0 && globalVMF3.orographyLength != globalVMF3.gridLength) + { + BOOST_LOG_TRIVIAL(error) << "Orography and VMF3 file grid dimensions do not match"; + } } /** legendre polynomials */ void legenpoly( - Vmf3GridPoint& vmf3GP, ///< vmf3 info - const double doy, ///< day of year - const double el, ///< elevation (rad) - const double hgt) ///< height (m) + Vmf3GridPoint& vmf3GP, ///< vmf3 info + const double doy, ///< day of year + const double el, ///< elevation (rad) + const double hgt ///< height (m) +) { - int nmax = 12; - double bhc[5] = {}; - double bwc[5] = {}; - double chc[5] = {}; - double cwc[5] = {}; - double a1 = 2.53e-5; - double b1 = 5.49e-3; - double c1 = 1.14e-3; - double vmat[13][13] = {{}}; - double wmat[13][13] = {{}}; - - /* unit vector */ - double x = sin(PI / 2 - vmf3GP.lat * D2R) * cos(vmf3GP.lon * D2R); - double y = sin(PI / 2 - vmf3GP.lat * D2R) * sin(vmf3GP.lon * D2R); - double z = cos(PI / 2 - vmf3GP.lat * D2R); - - - vmat[0][0] = 1; - wmat[0][0] = 0; - vmat[1][0] = z * vmat[0][0]; - wmat[1][0] = 0; - - for (int j = 1; j < nmax; j++) - { - int n = j + 1; - vmat[j + 1][0] = ((2 * n - 1) * z * vmat[j][0] - j * vmat[j - 1][0]) / n; - wmat[j + 1][0] = 0.0; - } - - for (int j = 0; j < nmax; j++) - { - int m = j + 1; - vmat[j + 1][j + 1] = (2 * m - 1) * (x * vmat[j][j] - y * wmat[j][j]); - wmat[j + 1][j + 1] = (2 * m - 1) * (x * wmat[j][j] + y * vmat[j][j]); - - if (j < nmax - 1) - { - vmat[j + 2][j + 1] = (2 * m + 1) * z * vmat[j + 1][j + 1]; - wmat[j + 2][j + 1] = (2 * m + 1) * z * wmat[j + 1][j + 1]; - } - - for (int k = j + 2; k < nmax; k++) - { - vmat[k + 1][j + 1] = ((2 * (k + 1) - 1) * z * vmat[k][j + 1] - (k + 1 + j) * vmat[k - 1][j + 1]) / (k + 1 - m); - wmat[k + 1][j + 1] = ((2 * (k + 1) - 1) * z * wmat[k][j + 1] - (k + 1 + j) * wmat[k - 1][j + 1]) / (k + 1 - m); - } - } - - /* b) determine coefficients bh, bw, ch, cw */ - int p = 0; - - for (int j = 0; j < 5; j++) - bhc[j] = bwc[j] = chc[j] = cwc[j] = 0; - - for (int j = 0; j <= nmax; j++) - for (int k = 0; k <= j; k++) - { - for (int q = 0; q < 5; q++) - { - bhc[q] += anm_bh[p][q] * vmat[j][k] + bnm_bh[p][q] * wmat[j][k]; - bwc[q] += anm_bw[p][q] * vmat[j][k] + bnm_bw[p][q] * wmat[j][k]; - chc[q] += anm_ch[p][q] * vmat[j][k] + bnm_ch[p][q] * wmat[j][k]; - cwc[q] += anm_cw[p][q] * vmat[j][k] + bnm_cw[p][q] * wmat[j][k]; - } - - p++; - } - - /* adding seasonal amplitudes */ - double bh = bhc[0] - + bhc[1] * cos(doy / 365.25 * 2*PI) - + bhc[2] * sin(doy / 365.25 * 2*PI) - + bhc[3] * cos(doy / 365.25 * 4*PI) - + bhc[4] * sin(doy / 365.25 * 4*PI); - - double bw = bwc[0] - + bwc[1] * cos(doy / 365.25 * 2*PI) - + bwc[2] * sin(doy / 365.25 * 2*PI) - + bwc[3] * cos(doy / 365.25 * 4*PI) - + bwc[4] * sin(doy / 365.25 * 4*PI); - - double ch = chc[0] - + chc[1] * cos(doy / 365.25 * 2*PI) - + chc[2] * sin(doy / 365.25 * 2*PI) - + chc[3] * cos(doy / 365.25 * 4*PI) - + chc[4] * sin(doy / 365.25 * 4*PI); - - double cw = cwc[0] - + cwc[1] * cos(doy / 365.25 * 2*PI) - + cwc[2] * sin(doy / 365.25 * 2*PI) - + cwc[3] * cos(doy / 365.25 * 4*PI) - + cwc[4] * sin(doy / 365.25 * 4*PI); - - - /* using a from the grid and calculate hydro and wet mapping factor */ - double ah = vmf3GP.ah; - double aw = vmf3GP.aw; - - double sinel = sin(el); - - auto continuedFrac = [&](double a, double b, double c) - { - double answer = (1 + a - / (1 + b - / (1 + c))) - - / (sinel + a - / (sinel + b - / (sinel + c))); - - return answer; - }; - - vmf3GP.mfh = continuedFrac(ah, bh, ch); - vmf3GP.mfw = continuedFrac(aw, bw, cw); - - double h1 = 1 / sin(el) - - continuedFrac(a1, b1, c1); - - vmf3GP.mfh += h1 * hgt / 1000; + int nmax = 12; + double bhc[5] = {}; + double bwc[5] = {}; + double chc[5] = {}; + double cwc[5] = {}; + double a1 = 2.53e-5; + double b1 = 5.49e-3; + double c1 = 1.14e-3; + double vmat[13][13] = {{}}; + double wmat[13][13] = {{}}; + + /* unit vector */ + double x = sin(PI / 2 - vmf3GP.lat * D2R) * cos(vmf3GP.lon * D2R); + double y = sin(PI / 2 - vmf3GP.lat * D2R) * sin(vmf3GP.lon * D2R); + double z = cos(PI / 2 - vmf3GP.lat * D2R); + + vmat[0][0] = 1; + wmat[0][0] = 0; + vmat[1][0] = z * vmat[0][0]; + wmat[1][0] = 0; + + for (int j = 1; j < nmax; j++) + { + int n = j + 1; + vmat[j + 1][0] = ((2 * n - 1) * z * vmat[j][0] - j * vmat[j - 1][0]) / n; + wmat[j + 1][0] = 0.0; + } + + for (int j = 0; j < nmax; j++) + { + int m = j + 1; + vmat[j + 1][j + 1] = (2 * m - 1) * (x * vmat[j][j] - y * wmat[j][j]); + wmat[j + 1][j + 1] = (2 * m - 1) * (x * wmat[j][j] + y * vmat[j][j]); + + if (j < nmax - 1) + { + vmat[j + 2][j + 1] = (2 * m + 1) * z * vmat[j + 1][j + 1]; + wmat[j + 2][j + 1] = (2 * m + 1) * z * wmat[j + 1][j + 1]; + } + + for (int k = j + 2; k < nmax; k++) + { + vmat[k + 1][j + 1] = + ((2 * (k + 1) - 1) * z * vmat[k][j + 1] - (k + 1 + j) * vmat[k - 1][j + 1]) / + (k + 1 - m); + wmat[k + 1][j + 1] = + ((2 * (k + 1) - 1) * z * wmat[k][j + 1] - (k + 1 + j) * wmat[k - 1][j + 1]) / + (k + 1 - m); + } + } + + /* b) determine coefficients bh, bw, ch, cw */ + int p = 0; + + for (int j = 0; j < 5; j++) + bhc[j] = bwc[j] = chc[j] = cwc[j] = 0; + + for (int j = 0; j <= nmax; j++) + for (int k = 0; k <= j; k++) + { + for (int q = 0; q < 5; q++) + { + bhc[q] += anm_bh[p][q] * vmat[j][k] + bnm_bh[p][q] * wmat[j][k]; + bwc[q] += anm_bw[p][q] * vmat[j][k] + bnm_bw[p][q] * wmat[j][k]; + chc[q] += anm_ch[p][q] * vmat[j][k] + bnm_ch[p][q] * wmat[j][k]; + cwc[q] += anm_cw[p][q] * vmat[j][k] + bnm_cw[p][q] * wmat[j][k]; + } + + p++; + } + + /* adding seasonal amplitudes */ + double bh = bhc[0] + bhc[1] * cos(doy / 365.25 * 2 * PI) + bhc[2] * sin(doy / 365.25 * 2 * PI) + + bhc[3] * cos(doy / 365.25 * 4 * PI) + bhc[4] * sin(doy / 365.25 * 4 * PI); + + double bw = bwc[0] + bwc[1] * cos(doy / 365.25 * 2 * PI) + bwc[2] * sin(doy / 365.25 * 2 * PI) + + bwc[3] * cos(doy / 365.25 * 4 * PI) + bwc[4] * sin(doy / 365.25 * 4 * PI); + + double ch = chc[0] + chc[1] * cos(doy / 365.25 * 2 * PI) + chc[2] * sin(doy / 365.25 * 2 * PI) + + chc[3] * cos(doy / 365.25 * 4 * PI) + chc[4] * sin(doy / 365.25 * 4 * PI); + + double cw = cwc[0] + cwc[1] * cos(doy / 365.25 * 2 * PI) + cwc[2] * sin(doy / 365.25 * 2 * PI) + + cwc[3] * cos(doy / 365.25 * 4 * PI) + cwc[4] * sin(doy / 365.25 * 4 * PI); + + /* using a from the grid and calculate hydro and wet mapping factor */ + double ah = vmf3GP.ah; + double aw = vmf3GP.aw; + + double sinel = sin(el); + + auto continuedFrac = [&](double a, double b, double c) + { + double answer = (1 + a / (1 + b / (1 + c))) + + / (sinel + a / (sinel + b / (sinel + c))); + + return answer; + }; + + vmf3GP.mfh = continuedFrac(ah, bh, ch); + vmf3GP.mfw = continuedFrac(aw, bw, cw); + + double h1 = 1 / sinel - continuedFrac(a1, b1, c1); + + vmf3GP.mfh += h1 * hgt / 1000; } -template -vector getStraddle( - const map& straddleMap, - INTER inter, - vector& fractions) +template +vector +getStraddle(const map& straddleMap, INTER inter, vector& fractions) { - vector outputs; - - auto it = straddleMap.lower_bound(inter); - if (it == straddleMap.end()) - { - auto lastIt = straddleMap.rbegin(); - - auto& [dummy, last] = *lastIt; - outputs.push_back(&last); fractions.push_back(1); - outputs.push_back(&last); fractions.push_back(0); - } - else if (it == straddleMap.begin()) - { - auto& [dummy, first] = *it; - outputs.push_back(&first); fractions.push_back(0); - outputs.push_back(&first); fractions.push_back(1); - } - else - { - auto& [inter2, out2] = *it; - it--; - auto& [inter1, out1] = *it; - - outputs.push_back(&out2); fractions.push_back((inter - inter1) / (inter2 - inter1)); - outputs.push_back(&out1); fractions.push_back((inter - inter1) / (inter1 - inter2) + 1); - } - - return outputs; + vector outputs; + + auto it = straddleMap.lower_bound(inter); + if (it == straddleMap.end()) + { + auto lastIt = straddleMap.rbegin(); + + auto& [dummy, last] = *lastIt; + outputs.push_back(&last); + fractions.push_back(1); + outputs.push_back(&last); + fractions.push_back(0); + } + else if (it == straddleMap.begin()) + { + auto& [dummy, first] = *it; + outputs.push_back(&first); + fractions.push_back(0); + outputs.push_back(&first); + fractions.push_back(1); + } + else + { + auto& [inter2, out2] = *it; + it--; + auto& [inter1, out1] = *it; + + outputs.push_back(&out2); + fractions.push_back((inter - inter1) / (inter2 - inter1)); + outputs.push_back(&out1); + fractions.push_back((inter - inter1) / (inter1 - inter2) + 1); + } + + return outputs; } /** vmf3 */ double tropVMF3( - Trace& trace, - GTime time, - VectorPos& pos, - double el, - double& dryZTD, - double& dryMap, - double& wetZTD, - double& wetMap, - double& var) + Trace& trace, + GTime time, + VectorPos& pos, + double el, + double& dryZTD, + double& dryMap, + double& wetZTD, + double& wetMap, + double& var +) { - var = -1; - if (globalVMF3.empty()) - { - return 0; - } - - double latd = pos.latDeg(); - double lond = pos.lonDeg(); - double hgt = pos.hgt(); - - if (lond < 0) - lond += 360; - - vector fractionsa; - auto a = getStraddle(globalVMF3, time, fractionsa); - - Vmf3GridPoint timePoint; - for (int i = 0; i < 2; i++) - { - vector fractionsb; - auto& A = *a[i]; - auto b = getStraddle(A, latd, fractionsb); - - Vmf3GridPoint latPoint; - for (int i = 0; i < 2; i++) - { - vector fractionsc; - auto& B = *b[i]; - auto c = getStraddle(B, lond, fractionsc); - - Vmf3GridPoint lonPoint; - for (int i = 0; i < 2; i++) - { - Vmf3GridPoint vmf3GP = *c[i]; - { - vmf3GP.orog = globalVMF3.orography[vmf3GP.index]; - } - - lonPoint += vmf3GP * fractionsc[i]; - } - latPoint += lonPoint * fractionsb[i]; - } - timePoint += latPoint * fractionsa[i]; - } - - Vmf3GridPoint& vmf3GP = timePoint; - { - /* (a) zhd */ - - // const double REFRACTIVITY = 0.0022768; //cancelled - - const double LAPSE_RATE = 0.0000226; - - double lapseRateDelta = pow((1 - LAPSE_RATE * (hgt - vmf3GP.orog)), 5.225); - - auto davisDelayDenom = [&](double height) { return 1 - 0.00266 * cos(2 * pos.lat()) - 0.28 * 1e-6 * height; }; - - vmf3GP.zhd = lapseRateDelta * vmf3GP.zhd * davisDelayDenom(vmf3GP.orog) - / davisDelayDenom(hgt); - - /* (b) zwd */ - double scaler = exp(-(hgt - vmf3GP.orog) / 2000); - vmf3GP.zwd *= scaler; - - UYds yds = time; - - double doy = yds.doy - + yds.sod / S_IN_DAY; - - /* legendre polynomials */ - legenpoly(vmf3GP, doy, el, hgt); - } - - dryZTD = vmf3GP.zhd; - wetZTD = vmf3GP.zwd; - dryMap = vmf3GP.mfh; - wetMap = vmf3GP.mfw; - - var = 0; - double delay = dryMap * dryZTD - + wetMap * wetZTD; - return delay; + var = -1; + if (globalVMF3.empty()) + { + return 0; + } + + double latd = pos.latDeg(); + double lond = pos.lonDeg(); + double hgt = pos.hgt(); + + if (lond < 0) + lond += 360; + + if (el < 0) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": el < 0, setting it to 1e-6"; + el = 1e-6; + } + + vector fractionsa; + auto a = getStraddle(globalVMF3, time, fractionsa); + + Vmf3GridPoint timePoint; + for (int i = 0; i < 2; i++) + { + vector fractionsb; + auto& A = *a[i]; + auto b = getStraddle(A, latd, fractionsb); + + Vmf3GridPoint latPoint; + for (int i = 0; i < 2; i++) + { + vector fractionsc; + auto& B = *b[i]; + auto c = getStraddle(B, lond, fractionsc); + + Vmf3GridPoint lonPoint; + for (int i = 0; i < 2; i++) + { + Vmf3GridPoint vmf3GP = *c[i]; + { + vmf3GP.orog = globalVMF3.orography[vmf3GP.index]; + } + + lonPoint += vmf3GP * fractionsc[i]; + } + latPoint += lonPoint * fractionsb[i]; + } + timePoint += latPoint * fractionsa[i]; + } + + Vmf3GridPoint& vmf3GP = timePoint; + { + /* (a) zhd */ + + // const double REFRACTIVITY = 0.0022768; //cancelled + + const double LAPSE_RATE = 0.0000226; + + double lapseRateDelta = pow((1 - LAPSE_RATE * (hgt - vmf3GP.orog)), 5.225); + + auto davisDelayDenom = [&](double height) + { return 1 - 0.00266 * cos(2 * pos.lat()) - 0.28 * 1e-6 * height; }; + + vmf3GP.zhd = + lapseRateDelta * vmf3GP.zhd * davisDelayDenom(vmf3GP.orog) / davisDelayDenom(hgt); + + /* (b) zwd */ + double scaler = exp(-(hgt - vmf3GP.orog) / 2000); + vmf3GP.zwd *= scaler; + + UYds yds = time; + + double doy = yds.doy + yds.sod / S_IN_DAY; + + /* legendre polynomials */ + legenpoly(vmf3GP, doy, el, hgt); + } + + dryZTD = vmf3GP.zhd; + wetZTD = vmf3GP.zwd; + dryMap = vmf3GP.mfh; + wetMap = vmf3GP.mfw; + + var = 0; + double delay = dryMap * dryZTD + wetMap * wetZTD; + return delay; } diff --git a/src/testing/enums_magic.h b/src/testing/enums_magic.h new file mode 100644 index 000000000..f0cfe7f1e --- /dev/null +++ b/src/testing/enums_magic.h @@ -0,0 +1,1248 @@ +/** + * @file enums_magic.h + * @brief Converted enums.h file using magic_enum instead of BETTER_ENUM + * + * This is a demonstration conversion of the original enums.h file from + * src/cpp/common/enums.h to use standard C++ enum class with magic_enum. + * + * Original file: src/cpp/common/enums.h + * Conversion date: November 10, 2025 + * + * Key differences from BETTER_ENUM version: + * 1. Uses standard C++ enum class instead of BETTER_ENUM macro + * 2. Requires magic_enum.hpp header for enum reflection + * 3. All enum operations use magic_enum free functions instead of member functions + * 4. Requires setting MAGIC_ENUM_RANGE for enums with large values (>127) + * + * Note: This is placed in testing/ directory as a demonstration. + * To use in production, you would need to: + * - Update all code calling BETTER_ENUM member functions + * - Change _to_string() to magic_enum::enum_name() + * - Change _from_string_nocase() to magic_enum::enum_cast(..., case_insensitive) + * - Change _to_integral() to magic_enum::enum_integer() + * - Change _from_integral() to magic_enum::enum_cast() + */ + +#pragma once + +#include + +// Configure magic_enum range to support large enum values (RTCM message types >1000) +#define MAGIC_ENUM_RANGE_MIN -200 +#define MAGIC_ENUM_RANGE_MAX 10000 +#include + +// Keep the traditional C enums as-is (these aren't using BETTER_ENUM anyway) +typedef enum +{ + NONE, + + /* Base carrier frequencies */ + F1 = 1, // 1575.42 MHz: GPS L1, GAL E1, BDS B1, QZS L1, SBS L1, + F2 = 2, // 1227.60 MHz: GPS L2, QZS L2, + F5 = 5, // 1176.45 MHz: GPS L5, GAL E5A, BDS B2A, QZS L5, SBS L5 + F6 = 6, // 1278.75 MHz: GAL E6, QZS L6, + F7 = 7, // 1207.14 MHz: GAL E5B, BDS B2B + F8 = 8, // 1191.795 MHz: GAL E5, BDS B2, + G1 = 11, // ~1602 MHz: GLO G1, + G2 = 12, // ~1246 MHz: GLO G2, + G3 = 13, // 1202.025 MHz: GLO G3, + G4 = 14, // 1600.995 MHz GLO G1A + G6 = 16, // 1248.08 MHz: GLO G2A, + B1 = 21, // 1561.098 MHz: BDS B2-1, + B3 = 23, // 1268.52 MHz: BDS B3 + I9 = 39, // 2492.028 MHz: IRN S9 + NUM_FTYPES, +} E_FType; + +typedef enum +{ + CODE, + PHAS, + NUM_MEAS_TYPES +} E_MeasType; + +typedef enum +{ + SVH_OK, + SVH_UNHEALTHY = -1 // implicitly used in rtcm +} E_Svh; + +// Converted BETTER_ENUM to enum class +enum class E_Solution : short int +{ + NONE, + SINGLE, + SINGLE_X, + PPP +}; + +enum class E_Radio : short int +{ + TRANSMITTER, + RECEIVER +}; + +enum class E_Sys : short int +{ + NONE, + GPS, + GAL, + GLO, + QZS, + SBS, + BDS, + LEO, + SUPPORTED, + IRN, + IMS, + COMB +}; + +enum class E_Block : short int +{ + UNKNOWN, + GPS_I, + GPS_II, + GPS_IIA, + GPS_IIR_A, + GPS_IIR_B, + GPS_IIR_M, + GPS_IIF, + GPS_IIIA, + GLO_M, + GLO, + GLO_K1A, + GLO_K1B, + GLO_K2, + GLO_MP, // GLO-M+ + GAL_0A, + GAL_0B, + GAL_1, + GAL_2, + BDS_2M, + BDS_2G, + BDS_2I, + BDS_3SI_SECM, + BDS_3SM_CAST, + BDS_3SI_CAST, + BDS_3SM_SECM, + BDS_3M_CAST, + BDS_3M_SECM_A, + BDS_3G, + BDS_3I, + BDS_3M_SECM_B, + QZS_1, + QZS_2I, + QZS_2G, + QZS_2A, + IRS_1I, + IRS_1G, + IRS_2G +}; + +enum class E_OffsetType : short int +{ + UNSPECIFIED, + APC, + COM +}; + +enum class E_TrigType : short int +{ + NONE, + COS, + SIN +}; + +enum class E_EmpAxis : short int +{ + NONE, + D, + Y, + B, + R, + T, + N, + P, + Q +}; + +enum class KF : short int +{ + NONE, + ONE, + ALL, + + REC_POS, + REC_VEL, + REC_POS_RATE = REC_VEL, + REC_ACC, + + STRAIN_RATE, + + POS, + VEL, + ACC, + + HEADING, + + ORIENTATION, + + REF_SYS_BIAS, + + BEGIN_CLOCK_STATES, + REC_CLOCK, + REC_SYS_BIAS = REC_CLOCK, + REC_CLOCK_RATE, + REC_SYS_BIAS_RATE = REC_CLOCK_RATE, + REC_CLOCK_RATE_GM, + REC_SYS_BIAS_RATE_GM = REC_CLOCK_RATE_GM, + + SAT_CLOCK, + SAT_CLOCK_RATE, + SAT_CLOCK_RATE_GM, + END_CLOCK_STATES, + + TROP, + TROP_GRAD, + TROP_MODEL, + + IONOSPHERIC, + IONO_STEC, + REC_PCO_X, + REC_PCO_Y, + REC_PCO_Z, + SAT_PCO_X, + SAT_PCO_Y, + SAT_PCO_Z, + + REC_PCV, + + ANT_DELTA, + + EOP, + EOP_RATE, + + CALC, + + SLR_REC_RANGE_BIAS, + SLR_REC_TIME_BIAS, + + XFORM_XLATE, + XFORM_RTATE, + XFORM_SCALE, + XFORM_DELAY, + + XFORM_XLATE_RATE, + XFORM_RTATE_RATE, + XFORM_SCALE_RATE, + XFORM_DELAY_RATE, + + AMBIGUITY, + CODE_BIAS, + PHASE_BIAS, + + Z_AMB, + + REFERENCE, + + BEGIN_MEAS_STATES, + CODE_MEAS, + PHAS_MEAS, + LASER_MEAS, + PSEUDO_MEAS, + ORBIT_MEAS, + FILTER_MEAS, + END_MEAS_STATES, + + BEGIN_ORBIT_STATES, + ORBIT, + CR, + CD, + EMP_D_0, + EMP_D_1, + EMP_D_2, + EMP_D_3, + EMP_D_4, + + EMP_Y_0, + EMP_Y_1, + EMP_Y_2, + EMP_Y_3, + EMP_Y_4, + + EMP_B_0, + EMP_B_1, + EMP_B_2, + EMP_B_3, + EMP_B_4, + + EMP_R_0, + EMP_R_1, + EMP_R_2, + EMP_R_3, + EMP_R_4, + + EMP_T_0, + EMP_T_1, + EMP_T_2, + EMP_T_3, + EMP_T_4, + + EMP_N_0, + EMP_N_1, + EMP_N_2, + EMP_N_3, + EMP_N_4, + + EMP_P_0, + EMP_P_1, + EMP_P_2, + EMP_P_3, + EMP_P_4, + + EMP_Q_0, + EMP_Q_1, + EMP_Q_2, + EMP_Q_3, + EMP_Q_4, + END_ORBIT_STATES, + + BEGIN_INERTIAL_STATES, + GYRO_BIAS, + GYRO_SCALE, + ACCL_BIAS, + ACCL_SCALE, + IMU_OFFSET, + END_INERTIAL_STATES, + + RANGE +}; + +enum class E_StateComponent : short int +{ + NONE, + + X, + Y, + Z, + VX, + VY, + VZ, + + E, + N, + U, + + XP, + YP, + UT1_UTC, + + W, + QX, + QY, + QZ +}; + +enum class KEPLER : short int +{ + LX, + LY, + LZ, + EU, + EV, + M +}; + +enum class E_PolyType : short int +{ + CONSTANT, + LAT, + LON, + LAT_LON, + LAT_SQRD, + LON_SQRD +}; + +enum class E_BasisType : short int +{ + POLYNOMIAL, + GRIDPOINT +}; + +enum class E_Relativity : short int +{ + OFF, + ON +}; + +enum class E_FilterStage : int +{ + LSQ, + PREFIT, + POSTFIT +}; + +enum class E_ChiSqMode : int +{ + INNOVATION, + MEASUREMENT, + STATE +}; + +enum class E_TropModel : int +{ + STANDARD, + SBAS, + VMF3, + GPT2, + CSSR +}; + +enum class E_NoiseModel : int +{ + UNIFORM, + ELEVATION_DEPENDENT +}; + +enum class E_IonoModel : int +{ + NONE, + MEAS_OUT, + BSPLINE, + SPHERICAL_CAPS, + SPHERICAL_HARMONICS, + LOCAL +}; + +enum class E_IonoMode : int +{ + OFF, ///< ionosphere option: correction off + BROADCAST, ///< ionosphere option: broadcast model + SBAS, ///< ionosphere option: SBAS model + IONO_FREE_LINEAR_COMBO, ///< ionosphere option: L1/L2 or L1/L5 iono-free LC + ESTIMATE, ///< ionosphere option: estimation + TOTAL_ELECTRON_CONTENT, ///< ionosphere option: IONEX TEC model + QZS, ///< ionosphere option: QZSS broadcast model + LEX, ///< ionosphere option: QZSS LEX ionospehre + STEC ///< ionosphere option: SLANT TEC model +}; + +enum class E_IonoMapFn : int +{ + SLM, ///< single layer model mapping function + MSLM, ///< modified single layer model mapping function + MLM, ///< multiple layer model mapping function + KLOBUCHAR ///< Klobuchar mapping function +}; + +enum class E_IonoFrame : int +{ + EARTH_FIXED, ///< Earth-fixed reference frame + SUN_FIXED ///< Sun-fixed reference frame +}; + +enum class E_Period : int +{ + SECOND = 1, + MINUTE = 60, + HOUR = 60 * 60, + DAY = 60 * 60 * 24, + WEEK = 60 * 60 * 24 * 7, + YEAR = 60 * 60 * 24 * 365, + + SECONDS = SECOND, + MINUTES = MINUTE, + HOURS = HOUR, + DAYS = DAY, + WEEKS = WEEK, + YEARS = YEAR, + SEC = SECOND, + MIN = MINUTE, + HR = HOUR, + DY = DAY, + WK = WEEK, + YR = YEAR, + SECS = SECOND, + MINS = MINUTE, + HRS = HOUR, + DYS = DAY, + WKS = WEEK, + YRS = YEAR, + SQRT_SEC = SECOND, + SQRT_MIN = MINUTE, + SQRT_HR = HOUR, + SQRT_DY = DAY, + SQRT_WK = WEEK, + SQRT_YR = YEAR, + SQRT_SECS = SECOND, + SQRT_MINS = MINUTE, + SQRT_HRS = HOUR, + SQRT_DYS = DAY, + SQRT_WKS = WEEK, + SQRT_YRS = YEAR, + SQRT_SECOND = SECOND, + SQRT_MINUTE = MINUTE, + SQRT_HOUR = HOUR, + SQRT_DAY = DAY, + SQRT_WEEK = WEEK, + SQRT_YEAR = YEAR, + SQRT_SECONDS = SECOND, + SQRT_MINUTES = MINUTE, + SQRT_HOURS = HOUR, + SQRT_DAYS = DAY, + SQRT_WEEKS = WEEK, + SQRT_YEARS = YEAR +}; + +enum class E_TimeSys : int +{ + NONE, ///< NONE for unknown + GPST, ///< GPS Time + GLONASST, ///< GLONASS Time + GST, ///< Galileo System Time + BDT, ///< BeiDou Time + QZSST, ///< QZSS Time + TAI, ///< International Atomic Time + UTC, ///< Universal Coordinated Time + UT1, ///< Universal Time corrected for polar motion + TT ///< Terrestrial Time +}; + +enum class E_PosFrame : int +{ + NONE, + XYZ, + NED, + RTN +}; + +enum class E_ObxFrame : short int +{ + OTHER, + ECEF, + ECI, + BCRS +}; + +enum class E_FilterMode : int +{ + LSQ, + KALMAN +}; + +enum class E_Inverter : int +{ + NONE, + INV, + LLT, + LDLT, + COLPIVHQR, + BDCSVD, + JACOBISVD, + FULLPIVLU, + FIRST_UNSUPPORTED = FULLPIVLU, + FULLPIVHQR +}; + +enum class E_MongoType : int +{ + NONE, + STATES, + STATES_AVAILABLE, + RESIDUALS, + TRACE, + LIST +}; + +enum class E_ObsDesc : int +{ + C, // Code / Pseudorange + L, // Phase + D, // Doppler + S, // Raw signal strength (carrier to noise ratio) + X // Receiver channel numbers +}; + +enum class E_ObsCode : int +{ + NONE = 0, ///< none or unknown + L1C = 1, ///< L1C/A,G1C/A,E1C (GPS,GLO,GAL,QZS,SBS) + L1P = 2, ///< L1P,G1P (GPS,GLO) + L1W = 3, ///< L1 Z-track (GPS) + L1Y = 4, ///< L1Y (GPS) + L1M = 5, ///< L1M (GPS) + L1N = 6, ///< L1codeless (GPS) + L1S = 7, ///< L1C(D) (GPS,QZS) + L1L = 8, ///< L1C(P) (GPS,QZS) + L1E = 9, ///< L1C/B (QZS) + L1A = 10, ///< E1A (GAL) + L1B = 11, ///< E1B (GAL) + L1X = 12, ///< E1B+C,L1C(D+P) (GAL,QZS) + L1Z = 13, ///< E1A+B+C,L1-SAIF (GAL,QZS) + L2C = 14, ///< L2C/A,G1C/A (GPS,GLO) + L2D = 15, ///< L2 L1C/A-(P2-P1) (GPS) + L2S = 16, ///< L2C(M) (GPS,QZS) + L2L = 17, ///< L2C(L) (GPS,QZS) + L2X = 18, ///< L2C(M+L),B1-2I+Q (GPS,QZS,BDS) + L2P = 19, ///< L2P,G2P (GPS,GLO) + L2W = 20, ///< L2 Z-track (GPS) + L2Y = 21, ///< L2Y (GPS) + L2M = 22, ///< L2M (GPS) + L2N = 23, ///< L2codeless (GPS) + L5I = 24, ///< L5/E5aI (GPS,GAL,QZS,SBS) + L5Q = 25, ///< L5/E5aQ (GPS,GAL,QZS,SBS) + L5X = 26, ///< L5/E5aI+Q (GPS,GAL,QZS,SBS) + L7I = 27, ///< E5bI,B2aI (GAL,BDS) + L7Q = 28, ///< E5bQ,B2aQ (GAL,BDS) + L7X = 29, ///< E5bI+Q,B2aI+Q (GAL,BDS) + L6A = 30, ///< E6A, L2OCd (GAL,GLO) + L6B = 31, ///< E6B, L2OCp (GAL,GLO) + L6C = 32, ///< E6C, L2OCd+L2OCp (GAL,GLO) + L6X = 33, ///< E6B+C,LEXS+L,B3I+Q (GAL,QZS,BDS) + L6Z = 34, ///< E6A+B+C (GAL) + L6S = 35, ///< L6S (QZS) + L6L = 36, ///< L6L (QZS) + L8I = 37, ///< E5(a+b)I (GAL) + L8Q = 38, ///< E5(a+b)Q (GAL) + L8X = 39, ///< E5(a+b)I+Q (GAL, BDS) + L2I = 40, ///< B1-2I (BDS) + L2Q = 41, ///< B1-2Q (BDS) + L6I = 42, ///< B3I (BDS) + L6Q = 43, ///< B3Q (BDS) + L3I = 44, ///< G3I (GLO) + L3Q = 45, ///< G3Q (GLO) + L3X = 46, ///< G3I+Q (GLO) + L1I = 47, ///< B1I (BDS) + L1Q = 48, ///< B1Q (BDS) + L4A = 49, ///< L1OCd (GLO) + L4B = 50, ///< L1OCp (GLO) + L4X = 51, ///< L1OCd+L1OCp (GLO) + L6E = 52, ///< L6E (QZS) + L1D = 53, ///< B1D (BDS) + L5D = 54, ///< B2aD (BDS) + L5P = 55, ///< B2aP (BDS) + L9A = 57, ///< S9 A SPS (IRN) + L9B = 58, ///< S9 B RS(D) (IRN) + L9C = 59, ///< S9 C RS(P) (IRN) + L9X = 60, ///< S9 B+C (IRN) + L5A = 61, ///< L5 A SPS (IRN) + L5B = 62, ///< L5 B RS(D) (IRN) + L5C = 63, ///< L5 C RS(P) (IRN) + L5Z = 64, ///< L5 B+C (IRN) + L6D = 65, ///< L6 Data (BDS) + L6P = 66, ///< L6 Pilot (BDS) + L7D = 67, ///< L7 Data (BDS) + L7P = 68, ///< L7 Pilot (BDS) + L7Z = 69, ///< L7 Data+Pilot (BDS) + L8D = 70, ///< L8 Data (BDS) + L8P = 71, ///< L8 Pilot (BDS) + AUTO = 9001 +}; + +enum class E_ObsCode2 : int +{ + NONE, + P1, + P2, + C1, + C2, + C3, + C4, + C5, + C6, + C7, + C8, + L1, + L2, + L3, + L4, + L5, + L6, + L7, + L8, + LA +}; + +enum class E_ARmode : short int +{ + OFF, + ROUND, + ITER_RND, + BOOTST, + LAMBDA, + LAMBDA_ALT, + LAMBDA_AL2, + LAMBDA_BIE +}; + +enum class E_NavRecType : short int +{ + NONE, ///< NONE for unknown */ + EPH, ///< Ephemerides data including orbit, clock, biases, accuracy and status parameters */ + STO, ///< System Time and UTC proxy offset parameters */ + EOP, ///< Earth Orientation Parameters */ + ION ///< Global/Regional ionospheric model parameters */ +}; + +enum class E_NavMsgType : short int +{ + NONE, ///< NONE for unknown + LNAV, ///< GPS/QZSS/NavIC Legacy Navigation Messages + FDMA, ///< GLONASS Legacy FDMA Navigation Message + FNAV, ///< Galileo Free Navigation Message + INAV, ///< Galileo Integrity Navigation Message + IFNV, ///< Galileo INAV or FNAV Navigation Message + D1, ///< BeiDou-2/3 MEO/IGSO Navigation Message + D2, ///< BeiDou-2/3 GEO Navigation Message + D1D2, ///< BeiDou-2/3 MEO/IGSO and GEO Navigation Message + SBAS, ///< SBAS Navigation Message + CNAV, ///< GPS/QZSS CNAV Navigation Message + CNV1, ///< BeiDou-3 CNAV-1 Navigation Message + CNV2, ///< GPS/QZSS CNAV-2 Navigation Message BeiDou-3 CNAV-2 Navigation Message + CNV3, ///< BeiDou-3 CNAV-3 Navigation Message + CNVX ///< GPS/QZSS CNAV or CNAV-2 Navigation Message BeiDou-3 CNAV-1, CNAV-2 or CNAV-3 Navigation Message +}; + +enum class E_SatType : short int +{ + NONE, + GEO, + IGSO, + MEO +}; + +enum class E_StoCode : short int +{ + NONE, + GPUT, + GLUT, + GLGP, + GAUT, + GAGP, + GPGA = GAGP, // From RINEX 3.04 the GPGA label is replaced by GAGP + GAGL, + BDUT, + BDGP, + BDGL, + BDGA, + QZUT, + QZGP, + QZGL, + QZGA, + QZBD, + IRUT, + IRGP, + IRGL, + IRGA, + IRBD, + IRQZ, + SBUT, + SBGP, + SBGL, + SBGA, + SBBD, + SBQZ, + SBIR +}; + +enum class E_UtcId : short int +{ + NONE, + UTC_USNO, + UTC_SU, + UTCGAL, + UTC_NTSC, + UTC_NICT, + UTC_NPLI, + UTCIRN, + UTC_OP, + UTC_NIST +}; + +enum class E_SbasId : short int +{ + NONE, + WAAS, + EGNOS, + MSAS, + GAGAN, + SDCM, + BDSBAS, + KASS, + A_SBAS, + SPAN +}; + +enum class RtcmMessageType : uint16_t +{ + NONE = 0, + + GPS_EPHEMERIS = 1019, + + GLO_EPHEMERIS = 1020, + + BDS_EPHEMERIS = 1042, + + QZS_EPHEMERIS = 1044, + + GAL_FNAV_EPHEMERIS = 1045, + GAL_INAV_EPHEMERIS = 1046, + + GPS_SSR_ORB_CORR = 1057, + GPS_SSR_CLK_CORR = 1058, + GPS_SSR_CODE_BIAS = 1059, + GPS_SSR_COMB_CORR = 1060, + GPS_SSR_URA = 1061, + GPS_SSR_HR_CLK_CORR = 1062, + GPS_SSR_PHASE_BIAS = 1265, + + GLO_SSR_ORB_CORR = 1063, + GLO_SSR_CLK_CORR = 1064, + GLO_SSR_CODE_BIAS = 1065, + GLO_SSR_COMB_CORR = 1066, + GLO_SSR_URA = 1067, + GLO_SSR_HR_CLK_CORR = 1068, + GLO_SSR_PHASE_BIAS = 1266, + + MSM4_GPS = 1074, + MSM5_GPS = 1075, + MSM6_GPS = 1076, + MSM7_GPS = 1077, + + MSM4_GLONASS = 1084, + MSM5_GLONASS = 1085, + MSM6_GLONASS = 1086, + MSM7_GLONASS = 1087, + + MSM4_GALILEO = 1094, + MSM5_GALILEO = 1095, + MSM6_GALILEO = 1096, + MSM7_GALILEO = 1097, + + MSM4_QZSS = 1114, + MSM5_QZSS = 1115, + MSM6_QZSS = 1116, + MSM7_QZSS = 1117, + + MSM4_BEIDOU = 1124, + MSM5_BEIDOU = 1125, + MSM6_BEIDOU = 1126, + MSM7_BEIDOU = 1127, + + GAL_SSR_ORB_CORR = 1240, + GAL_SSR_CLK_CORR = 1241, + GAL_SSR_CODE_BIAS = 1242, + GAL_SSR_COMB_CORR = 1243, + GAL_SSR_URA = 1244, + GAL_SSR_HR_CLK_CORR = 1245, + GAL_SSR_PHASE_BIAS = 1267, + + QZS_SSR_ORB_CORR = 1246, + QZS_SSR_CLK_CORR = 1247, + QZS_SSR_CODE_BIAS = 1248, + QZS_SSR_COMB_CORR = 1249, + QZS_SSR_URA = 1250, + QZS_SSR_HR_CLK_CORR = 1251, + QZS_SSR_PHASE_BIAS = 1268, + + SBS_SSR_ORB_CORR = 1252, + SBS_SSR_CLK_CORR = 1253, + SBS_SSR_CODE_BIAS = 1254, + SBS_SSR_COMB_CORR = 1255, + SBS_SSR_URA = 1256, + SBS_SSR_HR_CLK_CORR = 1257, + SBS_SSR_PHASE_BIAS = 1269, + + BDS_SSR_ORB_CORR = 1258, + BDS_SSR_CLK_CORR = 1259, + BDS_SSR_CODE_BIAS = 1260, + BDS_SSR_COMB_CORR = 1261, + BDS_SSR_URA = 1262, + BDS_SSR_HR_CLK_CORR = 1263, + BDS_SSR_PHASE_BIAS = 1270, + + COMPACT_SSR = 4073, + IGS_SSR = 4076, + CUSTOM = 4082 +}; + +enum class CompactSSRSubtype : unsigned short +{ + NONE = 0, + MSK = 1, + ORB = 2, + CLK = 3, + COD = 4, + PHS = 5, + BIA = 6, + URA = 7, + TEC = 8, + GRD = 9, + SRV = 10, + CMB = 11, + ATM = 12 +}; + +enum class IgsSSRSubtype : unsigned short +{ + NONE = 0, + + GROUP_ORB = 1, + GROUP_CLK = 2, + GROUP_CMB = 3, + GROUP_HRC = 4, + GROUP_COD = 5, + GROUP_PHS = 6, + GROUP_URA = 7, + GROUP_ION = 8, + + GPS_OFFSET = 20, + GPS_ORB = 21, + GPS_CLK = 22, + GPS_CMB = 23, + GPS_HRC = 24, + GPS_COD = 25, + GPS_PHS = 26, + GPS_URA = 27, + + GLO_OFFSET = 40, + GLO_ORB = 41, + GLO_CLK = 42, + GLO_CMB = 43, + GLO_HRC = 44, + GLO_COD = 45, + GLO_PHS = 46, + GLO_URA = 47, + + GAL_OFFSET = 60, + GAL_ORB = 61, + GAL_CLK = 62, + GAL_CMB = 63, + GAL_HRC = 64, + GAL_COD = 65, + GAL_PHS = 66, + GAL_URA = 67, + + QZS_OFFSET = 80, + QZS_ORB = 81, + QZS_CLK = 82, + QZS_CMB = 83, + QZS_HRC = 84, + QZS_COD = 85, + QZS_PHS = 86, + QZS_URA = 87, + + BDS_OFFSET = 100, + BDS_ORB = 101, + BDS_CLK = 102, + BDS_CMB = 103, + BDS_HRC = 104, + BDS_COD = 105, + BDS_PHS = 106, + BDS_URA = 107, + + SBS_OFFSET = 120, + SBS_ORB = 121, + SBS_CLK = 122, + SBS_CMB = 123, + SBS_HRC = 124, + SBS_COD = 125, + SBS_PHS = 126, + SBS_URA = 127, + + IONVTEC = 201 +}; + +enum class E_Source : short int +{ + NONE, + SPP, + CONFIG, + PRECISE, + SSR, + KALMAN, + BROADCAST, + NOMINAL, + MODEL, + PSEUDO, + REMOTE +}; + +enum class E_OrbexRecord : short int +{ + PCS, + VCS, + CPC, + CVC, + POS, + VEL, + CLK, + CRT, + ATT +}; + +enum class E_RTCMSubmessage : short int +{ + TIMESTAMP = 1 +}; + +enum class E_CrdEpochEvent : int +{ + REC_RX = 0, // ground receive time (at SRP) (two-way) + SAT_BN = 1, // spacecraft bounce time (two-way) + REC_TX = 2, // ground transmit time (at SRP) (two-way) + SAT_RX = 3, // spacecraft receive time (one-way) + SAT_TX = 4, // spacecraft transmit time (one-way) + REC_TX_SAT_RX = 5, // ground transmit time (at SRP) and spacecraft receive time (one-way) + SAT_TX_REC_RX = 6, // spacecraft transmit time and ground receive time (at SRP) (one-way) + NONE = 7 +}; + +enum class E_ObsWaitCode : short int +{ + OK, + EARLY_DATA, + NO_DATA_WAIT, + NO_DATA_EVER +}; + +enum class E_SRPModel : int +{ + NONE, + CANNONBALL, + BOXWING +}; + +enum class E_TidesModel : short int +{ + ELASTIC, + ANELASTIC +}; + +enum class E_ThirdBody : short int +{ + MERCURY = 1, + VENUS = 2, + EARTH = 3, + MARS = 4, + JUPITER = 5, + SATURN = 6, + URANUS = 7, + NEPTUNE = 8, + PLUTO = 9, + MOON = 10, + SUN = 11 +}; // from jpl, do not modify + +enum class E_SigWarning : short int +{ + SIG_OUTG = 1, // Minor (one signal) outage + LOW_ELEV = 2, // Low elevation + CYC_SLIP = 3, // Cycle slip + MAJ_OUTG = 4, // Major (whole satellite/receiver) outage + USR_DISC = 5 // User defined +}; + +enum class E_SlrRangeType : short int +{ + TX_ONLY = 0, // no ranges (i.e., transmit time only) + ONE_WAY = 1, // one-way ranging + TWO_WAY = 2, // two-way ranging + RX_ONLY = 3, // receive times only + MIXED = 3 // mixed +}; + +enum class E_UBXClass : short int +{ + NAV = 0x01, + RXM = 0x02, + INF = 0x04, + ACK = 0x05, + CFG = 0x06, + MON = 0x0A, + AID = 0x0B, + TIM = 0x0D, + ESF = 0x10 +}; + +enum class E_RXMId : short int +{ + SFRBX = 0x13, + MEASX = 0x14, + RAWX = 0x15 +}; + +enum class E_ESFId : short int +{ + MEAS = 0x02 +}; + +enum class E_MEASDataType : short int +{ + NONE = 0, + GYRO_Z = 5, + WHEEL_FL = 6, + WHEEL_FR = 7, + WHEEL_RL = 8, + WHEEL_RR = 9, + SPEED_TICK = 10, + SPEED = 11, + GYRO_TEMP = 12, + GYRO_Y = 13, + GYRO_X = 14, + ACCL_X = 16, + ACCL_Y = 17, + ACCL_Z = 18 +}; + +enum class E_Month : short int +{ + NONE, + JAN, + FEB, + MAR, + ARP, + MAY, + JUN, + JUL, + AUG, + SEP, + OCT, + NOV, + DEC +}; + +enum class E_FilePos : short int +{ + COORD, + CURR_TIME, + TOTAL_TIME, + PCDH, + NUM_SAMPLES, + FOOTER +}; + +enum class E_SSROutTiming : short int +{ + GPS_TIME, + LATEST_CLOCK_ESTIMATE +}; + +enum class E_Component : short int +{ + NONE, + X, + P, + DX, + PREFIT, + POSTFIT, + VARIANCE, + OBSERVED, + HEADING, + RANGE, + REC_CLOCK, + SAT_CLOCK, + SAGNAC, + RELATIVITY1, + RELATIVITY2, + REC_ANTENNA_DELTA, + REC_PCO, + SAT_PCO, + REC_PCV, + SAT_PCV, + TIDES_SOLID, + TIDES_OTL, + TIDES_ATL, + TIDES_SPOLE, + TIDES_OPOLE, + TROPOSPHERE, + IONOSPHERIC_COMPONENT, + IONOSPHERIC_COMPONENT1, + IONOSPHERIC_COMPONENT2, + IONOSPHERIC_MODEL, + PHASE_WIND_UP, + PHASE_AMBIGUITY, + REC_RANGE_BIAS, + REC_TIME_BIAS, + REC_PHASE_BIAS, + SAT_PHASE_BIAS, + REC_CODE_BIAS, + SAT_CODE_BIAS, + TROPOSPHERE_MODEL, + EOP, + SAT_REFLECTOR_DELTA, + NET_RESIDUAL, + CENTRAL_FORCE, + ALBEDO_CANNONBALL, + ALBEDO_BOXWING, + INDIRECT_J2, + DRAG, + EMPIRICAL, + GENERAL_RELATIVITY, + EGM, + SRP_CANNONBALL, + SRP_BOXWING, + ANTENNA_THRUST, + PLANETARY_PERTURBATION +}; + +// Keep as regular enum (not converted - already not using BETTER_ENUM) +enum E_ReturnType +{ + UNSUPPORTED, + OK, + WAIT, + GOT_OBS, + BAD_LENGTH +}; + +enum class E_LoadingType : short int +{ + NONE, + OCEAN, + ATMOSPHERIC +}; + +enum class E_TidalConstituent : short int +{ + M2, + S2, + N2, + K2, + S1, + K1, + O1, + P1, + Q1, + MF, + MM, + SSA +}; + +enum class E_TidalComponent : short int +{ + EAST, + WEST, + NORTH, + SOUTH, + UP, + DOWN +}; + +enum class E_Mongo : short int +{ + NONE, + PRIMARY, + SECONDARY, + BOTH +}; + +enum class E_Mincon : short int +{ + PSEUDO_OBS, + WEIGHT_MATRIX, + VARIANCE_INVERSE, + COVARIANCE_INVERSE +}; diff --git a/src/testing/test_magic_enum.cpp b/src/testing/test_magic_enum.cpp new file mode 100644 index 000000000..054cd8629 --- /dev/null +++ b/src/testing/test_magic_enum.cpp @@ -0,0 +1,336 @@ +/** + * @file test_magic_enum.cpp + * @brief Test file demonstrating magic_enum library usage with enums_magic.h + * + * This file demonstrates the three basic functions requested: + * 1. For loop through all enums + * 2. Conversion from string to enum + * 3. Conversion from enum to string + * + * Now uses the actual converted enums from enums_magic.h + * + * Compile with: g++ -std=c++17 -o test_magic_enum test_magic_enum.cpp -I. + */ + +#include "enums_magic.h" +#include +#include +#include + +/** + * @brief Test 1: Loop through all enum values + * + * Demonstrates: magic_enum::enum_values() + * This is equivalent to BETTER_ENUM's _values() method + */ +void test_enum_iteration() +{ + std::cout << "========================================" << std::endl; + std::cout << "Test 1: For loop through all enums" << std::endl; + std::cout << "========================================\n" << std::endl; + + // Test with E_Solution enum + std::cout << "E_Solution values:" << std::endl; + for (auto value : magic_enum::enum_values()) + { + std::cout << " - " << magic_enum::enum_name(value) + << " = " << magic_enum::enum_integer(value) << std::endl; + } + + // Verify the count + auto count = magic_enum::enum_count(); + std::cout << "Total: " << count << " values\n" << std::endl; + + // Test with E_Sys enum + std::cout << "E_Sys values:" << std::endl; + for (auto value : magic_enum::enum_values()) + { + std::cout << " - " << magic_enum::enum_name(value) + << " = " << magic_enum::enum_integer(value) << std::endl; + } + std::cout << "Total: " << magic_enum::enum_count() << " values\n" << std::endl; + + // Test with E_TropModel enum (troposphere model) + std::cout << "E_TropModel values:" << std::endl; + for (auto value : magic_enum::enum_values()) + { + std::cout << " - " << magic_enum::enum_name(value) << std::endl; + } + std::cout << "Total: " << magic_enum::enum_count() << " values\n" << std::endl; +} + +/** + * @brief Test 2: Convert from string to enum + * + * Demonstrates: magic_enum::enum_cast(string) + * This is equivalent to BETTER_ENUM's _from_string() and _from_string_nocase() + */ +void test_string_to_enum() +{ + std::cout << "========================================" << std::endl; + std::cout << "Test 2: Conversion from string to enum" << std::endl; + std::cout << "========================================\n" << std::endl; + + // Test E_Solution + std::cout << "E_Solution conversions:" << std::endl; + + auto ppp = magic_enum::enum_cast("PPP"); + if (ppp.has_value()) + { + std::cout << " 'PPP' -> " << magic_enum::enum_name(ppp.value()) + << " (value: " << magic_enum::enum_integer(ppp.value()) << ")" << std::endl; + assert(ppp.value() == E_Solution::PPP); + } + + auto single = magic_enum::enum_cast("SINGLE"); + if (single.has_value()) + { + std::cout << " 'SINGLE' -> " << magic_enum::enum_name(single.value()) << std::endl; + assert(single.value() == E_Solution::SINGLE); + } + + // Test case-insensitive conversion (like _from_string_nocase) + std::cout << "\nCase-insensitive conversions:" << std::endl; + auto gps = magic_enum::enum_cast("gps", magic_enum::case_insensitive); + if (gps.has_value()) + { + std::cout << " 'gps' (lowercase) -> " << magic_enum::enum_name(gps.value()) << std::endl; + assert(gps.value() == E_Sys::GPS); + } + + auto galileo = magic_enum::enum_cast("GaLiLeO", magic_enum::case_insensitive); + if (galileo.has_value()) + { + std::cout << " 'GaLiLeO' (mixed case) -> " << magic_enum::enum_name(galileo.value()) << std::endl; + assert(galileo.value() == E_Sys::GAL); + } + + // Test invalid string + std::cout << "\nInvalid string test:" << std::endl; + auto invalid = magic_enum::enum_cast("INVALID_MODE"); + if (!invalid.has_value()) + { + std::cout << " 'INVALID_MODE' -> (no match, as expected)" << std::endl; + } + std::cout << std::endl; +} + +/** + * @brief Test 3: Convert from enum to string + * + * Demonstrates: magic_enum::enum_name(value) + * This is equivalent to BETTER_ENUM's _to_string() method + */ +void test_enum_to_string() +{ + std::cout << "========================================" << std::endl; + std::cout << "Test 3: Conversion from enum to string" << std::endl; + std::cout << "========================================\n" << std::endl; + + // Test E_Solution + std::cout << "E_Solution conversions:" << std::endl; + E_Solution sol1 = E_Solution::PPP; + std::cout << " E_Solution::PPP -> '" << magic_enum::enum_name(sol1) << "'" << std::endl; + assert(magic_enum::enum_name(sol1) == "PPP"); + + E_Solution sol2 = E_Solution::SINGLE_X; + std::cout << " E_Solution::SINGLE_X -> '" << magic_enum::enum_name(sol2) << "'" << std::endl; + assert(magic_enum::enum_name(sol2) == "SINGLE_X"); + + // Test E_Sys + std::cout << "\nE_Sys conversions:" << std::endl; + E_Sys sys1 = E_Sys::GPS; + std::cout << " E_Sys::GPS -> '" << magic_enum::enum_name(sys1) << "'" << std::endl; + assert(magic_enum::enum_name(sys1) == "GPS"); + + E_Sys sys2 = E_Sys::GAL; + std::cout << " E_Sys::GAL -> '" << magic_enum::enum_name(sys2) << "'" << std::endl; + assert(magic_enum::enum_name(sys2) == "GAL"); + + // Test KF (Kalman Filter state) + std::cout << "\nKF conversions:" << std::endl; + KF kf1 = KF::ONE; + std::cout << " KF::ONE -> '" << magic_enum::enum_name(kf1) << "'" << std::endl; + assert(magic_enum::enum_name(kf1) == "ONE"); + + KF kf2 = KF::REC_POS; + std::cout << " KF::REC_POS -> '" << magic_enum::enum_name(kf2) << "'" << std::endl; + assert(magic_enum::enum_name(kf2) == "REC_POS"); + + std::cout << std::endl; +} + +/** + * @brief Test 4: Integer to enum and enum to integer conversions + * + * Demonstrates: magic_enum::enum_cast(int) and magic_enum::enum_integer(value) + * This is equivalent to BETTER_ENUM's _from_integral() and _to_integral() + */ +void test_integer_conversions() +{ + std::cout << "========================================" << std::endl; + std::cout << "Test 4: Integer <-> Enum conversions" << std::endl; + std::cout << "========================================\n" << std::endl; + + // Test enum to integer + std::cout << "Enum to Integer:" << std::endl; + E_ObsCode code = E_ObsCode::L1C; + auto code_int = magic_enum::enum_integer(code); + std::cout << " E_ObsCode::L1C -> " << code_int << std::endl; + assert(code_int == 1); + + // Test integer to enum + std::cout << "\nInteger to Enum:" << std::endl; + auto code_from_int = magic_enum::enum_cast(25); + if (code_from_int.has_value()) + { + std::cout << " 25 -> " << magic_enum::enum_name(code_from_int.value()) << std::endl; + assert(code_from_int.value() == E_ObsCode::L5Q); + } + + // Test with RTCM message types (large values) + std::cout << "\nRTCM Message Types (large values):" << std::endl; + RtcmMessageType msg = RtcmMessageType::GPS_EPHEMERIS; + auto msg_int = magic_enum::enum_integer(msg); + std::cout << " RtcmMessageType::GPS_EPHEMERIS -> " << msg_int << std::endl; + assert(msg_int == 1019); + + auto msg_from_int = magic_enum::enum_cast(1042); + if (msg_from_int.has_value()) + { + std::cout << " 1042 -> " << magic_enum::enum_name(msg_from_int.value()) << std::endl; + assert(msg_from_int.value() == RtcmMessageType::BDS_EPHEMERIS); + } + + std::cout << std::endl; +} + +/** + * @brief Test 5: Validation checks + * + * Demonstrates: magic_enum::enum_contains(value) + * Useful for checking if an integer is a valid enum value + */ +void test_validation() +{ + std::cout << "========================================" << std::endl; + std::cout << "Test 5: Enum validation" << std::endl; + std::cout << "========================================\n" << std::endl; + + // Test valid values + std::cout << "Valid values:" << std::endl; + bool valid1 = magic_enum::enum_contains(1); + std::cout << " Is 1 a valid E_FilterMode? " << (valid1 ? "YES" : "NO") << std::endl; + assert(valid1); + + // Test invalid values + std::cout << "\nInvalid values:" << std::endl; + bool valid2 = magic_enum::enum_contains(999); + std::cout << " Is 999 a valid E_FilterMode? " << (valid2 ? "YES" : "NO") << std::endl; + assert(!valid2); + + // Test with RTCM message type + bool valid3 = magic_enum::enum_contains(1019); + std::cout << " Is 1019 a valid RtcmMessageType? " << (valid3 ? "YES" : "NO") << std::endl; + assert(valid3); + + std::cout << std::endl; +} + +/** + * @brief Test 6: Working with enum ranges + * + * Demonstrates filtering enum values between BEGIN_CLOCK_STATES and END_CLOCK_STATES + * This is useful for processing subsets of large enums + */ +void test_enum_ranges() +{ + std::cout << "========================================" << std::endl; + std::cout << "Test 6: Enum ranges (KF clock states)" << std::endl; + std::cout << "========================================\n" << std::endl; + + // Get the boundary values + auto begin_value = magic_enum::enum_integer(KF::BEGIN_CLOCK_STATES); + auto end_value = magic_enum::enum_integer(KF::END_CLOCK_STATES); + + std::cout << "Clock states range:" << std::endl; + std::cout << " BEGIN_CLOCK_STATES = " << begin_value << std::endl; + std::cout << " END_CLOCK_STATES = " << end_value << std::endl; + std::cout << std::endl; + + // Iterate through all KF values and filter those within the range + std::cout << "Clock-related states (between BEGIN and END):" << std::endl; + int count = 0; + for (auto value : magic_enum::enum_values()) + { + auto int_val = magic_enum::enum_integer(value); + + // Check if this value is within the clock states range + if (int_val > begin_value && int_val < end_value) + { + std::cout << " - " << magic_enum::enum_name(value) + << " = " << int_val << std::endl; + count++; + } + } + std::cout << "\nTotal clock states found: " << count << std::endl; + + // Test specific clock state conversions + std::cout << "\nSpecific clock state tests:" << std::endl; + + KF rec_clock = KF::REC_CLOCK; + std::cout << " KF::REC_CLOCK -> " << magic_enum::enum_name(rec_clock) << std::endl; + assert(magic_enum::enum_name(rec_clock) == "REC_CLOCK"); + + // Note: REC_SYS_BIAS is an alias for REC_CLOCK + KF rec_sys_bias = KF::REC_SYS_BIAS; + std::cout << " KF::REC_SYS_BIAS -> " << magic_enum::enum_name(rec_sys_bias) << std::endl; + std::cout << " (Note: REC_SYS_BIAS = REC_CLOCK, so name shows as " + << magic_enum::enum_name(rec_sys_bias) << ")" << std::endl; + + KF sat_clock = KF::SAT_CLOCK; + std::cout << " KF::SAT_CLOCK -> " << magic_enum::enum_name(sat_clock) << std::endl; + assert(magic_enum::enum_name(sat_clock) == "SAT_CLOCK"); + + // Check if a value is a clock state + std::cout << "\nChecking if values are clock states:" << std::endl; + + auto rec_pos_int = magic_enum::enum_integer(KF::REC_POS); + bool is_rec_pos_clock = (rec_pos_int > begin_value && rec_pos_int < end_value); + std::cout << " Is REC_POS a clock state? " << (is_rec_pos_clock ? "YES" : "NO") << std::endl; + assert(!is_rec_pos_clock); + + auto rec_clock_int = magic_enum::enum_integer(KF::REC_CLOCK); + bool is_rec_clock_clock = (rec_clock_int > begin_value && rec_clock_int < end_value); + std::cout << " Is REC_CLOCK a clock state? " << (is_rec_clock_clock ? "YES" : "NO") << std::endl; + assert(is_rec_clock_clock); + + auto trop_int = magic_enum::enum_integer(KF::TROP); + bool is_trop_clock = (trop_int > begin_value && trop_int < end_value); + std::cout << " Is TROP a clock state? " << (is_trop_clock ? "YES" : "NO") << std::endl; + assert(!is_trop_clock); + + std::cout << std::endl; +} + +int main() +{ + std::cout << "\n╔════════════════════════════════════════╗" << std::endl; + std::cout << "║ Magic Enum Test with enums_magic.h ║" << std::endl; + std::cout << "╚════════════════════════════════════════╝\n" << std::endl; + + // Run all tests + test_enum_iteration(); + test_string_to_enum(); + test_enum_to_string(); + test_integer_conversions(); + test_validation(); + test_enum_ranges(); + + std::cout << "╔════════════════════════════════════════╗" << std::endl; + std::cout << "║ All tests passed! ✓ ║" << std::endl; + std::cout << "║ enums_magic.h works correctly ║" << std::endl; + std::cout << "╚════════════════════════════════════════╝" << std::endl; + + return 0; +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..4b93f504a --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "name": "ginan", + "version-string": "dev", + "description": "GNSS Analysis Software", + "dependencies": [ + "boost-log", + "boost-date-time", + "boost-system", + "boost-thread", + "boost-program-options", + "boost-serialization", + "boost-timer", + "boost-bimap", + "boost-format", + "boost-iostreams", + "boost-assign", + "boost-math", + "boost-odeint", + "boost-json", + "eigen3", + { + "name": "openblas", + "features": ["threads"] + }, + { + "name": "lapack", + "platform": "!windows" + }, + { + "name": "clapack", + "platform": "windows" + }, + "yaml-cpp", + "openssl", + "zstd", + { + "name": "mongo-cxx-driver", + "$comment": "Optional: MongoDB support - can be disabled with ENABLE_MONGODB=OFF", + "platform": "!windows" + } + ], + "builtin-baseline": "4c5ae6b55f3e3e39d291679f89822f496cf190ee" +}
    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 literal 0 HcmV?d00001 diff --git a/scripts/GinanUI/main.py b/scripts/GinanUI/main.py new file mode 100644 index 000000000..74543cb4a --- /dev/null +++ b/scripts/GinanUI/main.py @@ -0,0 +1,23 @@ +import os +import sys +import logging + +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QApplication +from scripts.GinanUI.app.main_window import MainWindow + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) + +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' + + app = QApplication(sys.argv) + app.setWindowIcon(QIcon("app/resources/ginan-logo.png")) + window = MainWindow() + window.setWindowIcon(QIcon("app/resources/ginan-logo.png")) + window.show() + sys.exit(app.exec()) diff --git a/scripts/GinanUI/requirements.txt b/scripts/GinanUI/requirements.txt new file mode 100644 index 000000000..c3bacb8bd --- /dev/null +++ b/scripts/GinanUI/requirements.txt @@ -0,0 +1,10 @@ +ruamel.yaml~=0.18.15 +pandas~=2.3.3 +PySide6~=6.10.0 +plotly~=6.3.1 +numpy~=2.3.3 +statsmodels~=0.14.5 +requests~=2.32.5 +hatanaka~=2.8.1 +unlzw3~=0.2.3 +beautifulsoup4~=4.14.2 \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..7492ff621 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,232 @@ +# Ginan Scripts + +This directory contains a number of useful scripts that facilitate: + + - running Ginan via: + - a graphical user interface (under `scripts/GinanUI`) + - shell scripts for installing Ginan natively (under `scripts/installation`) + - scripts that handle downloading necessary input files (`auto_download_PPP.py`) + - plotting Ginan output files, including + - POS files (`plot_pos.py`) + - ZTD files (`plotting/ztd_plot.py`) + - exploring and debugging Ginan and it's Kalman filter via: + - The Ginan Exploratory Data Analysis (EDA) tool (`scripts/GinanEDA`) + +Each sub-directory listed above contains it's own README, which provides further details on running the various functionalities. +The rest of this README will cover the files located on the `scripts` directory, namely: +1. `auto_download_PPP.py` +2. `plot_pos.py` +3. `plot_trace_res.py ` +4. `s3_filehandler.py` + +## _**Recommended:**_ +Before continuing, it is highly recommended that you create a python virtual environment if you have not already done so as suggested on the root README file: +```bash +# Create virtual environment +python3 -m venv ginan-env +source ginan-env/bin/activate +``` +The above line will the virtual environment in your current working directory. Once the above is complete, you will have the virtual environment in your current working directory. + +You can then install all python dependencies via a `pip` command: +```bash +# Install Python dependencies +pip3 install -r requirements.txt +``` +## 1. auto_download_PPP +The `auto_download_PPP.py` script makes it easier to download the necessary high precision products and model files necessary for processing RINEX data in Ginan to produce PPP results. + +Based on a few details provided by the user via arguments in the command-line interface (CLI), the script fetches the appropriate files for a given date or date range. These files includes products such as: + + - precise orbits (`.SP3`) + - broadcast orbits (`BRDC.RNX`) + - precise clocks (`.CLK`) + - Earth rotation parameters (`.ERP` or IERS IAU2000 file) + - CORS station positions and metadata (`.SNX`), + - satellite metadata (`.SNX`) + - code biases (`.BIA`) + +The product files are mostly obtained from the NASA archive known as the Crustal Dynamics Data Information System (CDDIS). This is one of NASA's Distributed Active Archive Centers (DAACs). + +To use and download from this archive, you will need to create an Earthdata Login account and provide your username and password in a `.netrc` file. This outlined below in Section 1.2. + +It also includes the various model files needed: + + - planetary ephemerides (JPL Development Ephemeris `DE436.1950.2050`), + - atmospheric loading, + - geopotential (Earth Gravitational Model `EGM2008`), + - geomagnetic reference field (International Geomagnetic Reference Field `IGRF14`) + - ocean loading, + - ocean pole tide coefficients, + - ocean tide potential (Finite Element Solution 2014b `FES2014b`), + - troposphere (Global Pressure and Temperature model `GPT2.5`) + +These are needed for running PPP. + +### 1.1 Earthdata Login Credentials - CDDIS Downloads +To download product files from the Crustal Dynamics Data Information System (CDDIS) web archive you will need an Earthdata Login account credentials saved to your machine. + +#### 1.1.1 Create New Earthdata Login Account (if you don't have one): +You can create a new Earthdata account at the following website: +https://urs.earthdata.nasa.gov/users/new + +#### 1.1.2 Save Credentials to Your Machine + +Once you have your username and password, these must be saved in a `.netrc` file on your home directory. Depending on your operating system, this can be achieved in different ways: + +##### Unix / Linux / MacOS: +This can be done in a terminal window via: +```bash +echo "machine urs.earthdata.nasa.gov login your_username password your_password" >> ~/.netrc +``` +Make sure to set appropriately restrictive file permissions as well (read / write by the current user only): +```bash +chmod 0600 ~/.netrc +``` + +##### Windows: +1. Open Notepad or any plain-text editor. + +2. Enter the `.netrc` format shown above, replace the placeholders with your actual login and password. + +3. Save the file as `_netrc` (with an underscore instead of a period) in your home directory. + +4. Set file permissions: + - Right-click _netrc file and choose Properties + - Go to the Security tab → Click Edit + - Remove access for all other users except your own account + - Click Apply to save the changes. + +The above Earthdata credential instructions are adapted from the following website:
    +https://nsidc.org/data/user-resources/help-center/creating-netrc-file-earthdata-login + +### 1.2 Test "auto_download_PPP" in Virtual Enviroment +Once you have your credentials set up, you are ready to automatically download all necessary product and model files via python. Before we do this though, we will test that the script is working as expected. + +First, make sure you have your virtual environment activated in your current terminal. Following the way we recommended to create the environment above, this would look like: +```bash +# Activate virtual environment - ginan-env +source ginan-env/bin/activate +``` + +Next, test that the `auto_download_PPP` script functions correctly: +```bash +# Test auto_download_PPP script: +python auto_download_PPP.py --help +``` +This will display the help page with detailed information on all possible arugments into the function itself. + +### 1.3 Example Run of "auto_download_PPP" +With your virtual environment active, you can now download the product files needed for a PPP run in Ginan. + +We will use the `igs-station` preset to download RINEX files for two IGS stations for two days in 2024 together with all the product and model files needed to run this in Ginan. + +```bash +# Example run of auto_download_PPP: +python auto_download_PPP.py --target-dir /data/temp/products --rinex-data-dir /data/temp/data --station-list ALIC,HOB2 --start-datetime 2024-01-06_00:00:00 --end-datetime 2024-01-07_23:59:30 --preset=igs-station --dont-replace +``` +Each of the arguments used above are described below: + +- `--target-dir`: sets the directory where product files are downloaded into (with model files going into `target-dir/tables`). In this case `target-dir=/data/temp/products` and model files end up in `/data/temp/products/tables` +- `--rinex-data-dir`: sets the directory where observational RINEX files are downloaded into (for `ALIC` and `HOB2` in this case) +- `--station-list`: this list of stations to download RINEX files for +- `--start-datetime`: the start date and time to download files from +- `--end-datetime`: the end date and time to download files to +- `--preset=igs-station`: tells the script to download all necessary products to run this in PPP mode in Ginan: `SP3`,`CLK`, `BRDC`, `SNX`, `BIA`, satellite metadata `SNX`, IERS Earth orientation data, plus all necessary model files in `target-dir/tables` + +If you are aware of which files you need to download, you can use the default `preset` mode of `manual` and just choose the various files needed based on their flags. For detailed info on all possible flags, run: +```bash +python auto_download_PPP.py --help +``` + +## 2. plot_pos + +The `plot_pos.py` script is used to visualise the contents of a Ginan `.POS` format file. + +Output plots are plotly `.html` files that can be displayed in a web browser + +```bash +Usage: +plot_pos.py [-h] --input-files INPUT_FILES [INPUT_FILES ...] [--start-datetime START_DATETIME] [--end-datetime END_DATETIME] [--horz-smoothing HORZ_SMOOTHING] [--vert-smoothing VERT_SMOOTHING] [--colour-sigma] [--max-sigma MAX_SIGMA] [--elevation] [--demean] [--map] [--heatmap] [--sigma-threshold SIGMA_THRESHOLD SIGMA_THRESHOLD SIGMA_THRESHOLD] [--down-sample DOWN_SAMPLE] [--save-prefix [SAVE_PREFIX]] +``` +Plots positional data and uncertainties with optional smoothing and color coding. + +### Optional arguments: + + - `-h`, `--help` show this help message and exit + - `--input-files` INPUT_FILES ...: One or more input .POS files for plotting (**required**) + - `--start-datetime` START_DATETIME: Start datetime in the format YYYY-MM-DDTHH:MM:SS, optional timezone + - `--end-datetime` END_DATETIME: End datetime in the format YYYY-MM-DDTHH:MM:SS, optional timezone + - `--horz-smoothing` HORZ_SMOOTHING: Fraction of the data used for horizontal LOWESS smoothing (optional). + - `--vert-smoothing` VERT_SMOOTHING: Fraction of the data used for vertical (Up) LOWESS smoothing (optional). + - `--colour-sigma`: Colourize the timeseries using the standard deviation (sigma) values (optional). + - `--max-sigma` MAX_SIGMA: Set a maximum sigma threshold for the sigma colour scale (optional). + - `--elevation`: Plot Elevation values inplace of dU wrt the reference coord (optional). + - `--demean`: Remove the mean values from all time series before plotting (optional). + - `--map`: Create a geographic map view from the Longitude & Latitude estiamtes (optional). + - `--heatmap`: Create a 2D heatmap view of E/N coodrinates wrt the reference position (optional). + - `--sigma-threshold` THRESHOLDS: Thresholds for sE, sN, and sU to filter data. + - `--down-sample` DOWN_SAMPLE: Interval in seconds for down-sampling data. + - `--save-prefix` [SAVE_PREFIX]: Prefix for saving HTML figures, e.g., ./output/fig + +### Examples + +- Plot a Ginan output .POS file: + +```bash +python plot_pos.py --input-files ALIC00AUS_R_20191990000_01D_30S_MO.rnx.POS +``` + +- Plot a Ginan output .POS file, using colours to represent uncertainties and a heatmep of horizontal positions: + +```bash +python plot_pos.py --input-files ALIC00AUS_R_20191990000_01D_30S_MO.rnx.POS --colour-sigma --heatmap --elevation +``` + +## 3. plot_trace_res + +The `plot_trace_res.py` script is used to visualise the contents of a Ginan Network `.TRACE` format file. + +Extracts and plots GNSS code and phase residuals by receiver and/or satellite with optional markers for large-errors, state errors. + +Output plots are plotly `.html` files that can be displayed in a web browser + +```bash +Usage: +plot_trace_res.py [-h] --files FILES [FILES ...] [--residual {prefit,postfit}] [--receivers RECEIVERS [--sat SAT] [--label-regex LABEL_REGEX] [--max-abs MAX_ABS] [--start START] [--end END] [--decimate DECIMATE] [--split-per-sat | --split-per-recv] [--out-dir OUT_DIR] [--basename BASENAME] [--webgl] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--out-prefix OUT_PREFIX] [--mark-large-errors] [--hover-unified] [--plot-normalised-res] [--show-stats-table] [--stats-matrix] [--stats-matrix-weighted] [--annotate-stats-matrix] [--mark-amb-resets] [--ambiguity-counts] [--ambiguity-totals] [--amb-totals-orient {h,v}] [--amb-totals-topn AMB_TOTALS_TOPN] [--use-forward-residuals] +``` + +Optional arguments: + +- `-h`, `--help` show this help message and exit +- `--files` FILES [FILES ...]: One or more TRACE files (space or , sep ), e.g. 'A.trace B.trace,C.trace' (wildcards allowed eg. *.TRACE) +- `--residual` {prefit,postfit}: Plot prefit or postfit residuals (default: postfit) +- `--receivers` RECEIVERS: One or more receiver names (space or , separated), e.g. 'ABMF,CHUR ALGO' +- `--sat` SAT, -s SAT: Filter by satellite ID +- `--label-regex` LABEL_REGEX: Regex to filter labels +- `--max-abs` MAX_ABS: Max residual to plot +- `--start` START: Start datetime or time-only +- `--end` END: End datetime (exclusive) +- `--decimate` DECIMATE: +- `--split-per-sat` : +- `--split-per-recv` : +- `--out-dir` OUT_DIR: Output directory for HTML files; defaults to CWD. +- `--basename` BASENAME: Base filename prefix for outputs (no extension). +- `--webgl`: Use webgl graphic acceleration +- `--log-level` {DEBUG,INFO,WARNING,ERROR,CRITICAL}: Logging verbosity +- `--out-prefix` OUT_PREFIX: unique prefix to add to output filenames +- `--mark-large-errors`: Mark LARGE STATE/MEAS ERROR events on plots. +- `--hover-unified`: Use unified hover tooltips across all traces (default: closest point hover). +- `--plot-normalised-res`: Also generate plots of normalised residuals (residual / sigma). +- `--show-stats-table`: Add a Mean / Std / RMS table per (sat × signal) at the bottom of each plot. +- `--stats-matrix`: Generate receiver×satellite heatmaps (Mean/Std/RMS) aggregated across signals. +- `--stats-matrix-weighted`: Use sigma-weighted statistics in the heatmaps (weights 1/σ²). +- `--annotate-stats-matrix`: Write the numeric value (mean/std/rms) into each stats heatmap cell. Hover still shows full details. +- `--mark-amb-resets`: Overlay PHASE ambiguity reset events (PREPROC=green, REJECT=blue) on PHASE per-receiver plots. +- `--ambiguity-counts`: Plot cumulative counts of ambiguity reset reasons and unique satellite resets over time. +- `--ambiguity-totals`: Bar chart of total ambiguity reset reasons (diagnostic view of detection methods). +- `--amb-totals-orient`: {h,v} Orientation for totals bar charts: 'h' (horizontal, default) or 'v' (vertical). +- `--amb-totals-topn AMB_TOTALS_TOPN`: Show only the top N receivers/satellites by total resets (to avoid clutter). +- `--use-forward-residuals`: Use residuals from forward (non-smoothed) files instead of smoothed files (default: use smoothed for more accurate residuals). + +## 4. s3_filehandler diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/auto_download_PPP.py b/scripts/auto_download_PPP.py index aafa3a910..42c928d4c 100644 --- a/scripts/auto_download_PPP.py +++ b/scripts/auto_download_PPP.py @@ -5,23 +5,28 @@ import random import ftplib import logging -import tarfile import requests import numpy as np from time import sleep from pathlib import Path -from typing import Tuple, Union -from copy import deepcopy +from typing import Tuple from urllib.parse import urlparse from contextlib import contextmanager -from datetime import datetime, timedelta, date +from datetime import datetime, timedelta -from gn_functions import ( - GPSDate, - gpswkD2dt, +from gnssanalysis.gn_datetime import GPSDate +from gnssanalysis.gn_download import ( + download_product_from_cddis, decompress_file, - download_url, + generate_content_type, + generate_sampling_rate, + generate_product_filename, + download_file_from_cddis, + check_whether_to_download, + attempt_url_download, + long_filename_cddis_cutoff, ) +from gnssanalysis.gn_utils import configure_logging, ensure_folders API_URL = "https://data.gnss.ga.gov.au/api" @@ -36,429 +41,6 @@ def ftp_tls(url: str, **kwargs) -> None: ftps.quit() -def configure_logging(verbose: bool) -> None: - if verbose: - logging_level = logging.DEBUG - else: - logging_level = logging.INFO - logging.basicConfig(format="%(asctime)s [%(funcName)s] %(levelname)s: %(message)s") - logging.getLogger().setLevel(logging_level) - - -def ensure_folders(paths: list) -> None: - """ - Ensures the list of folders exist in the file system. - """ - for path in paths: - if not isinstance(path, Path): - path = Path(path) - if not path.is_dir(): - path.mkdir(parents=True) - - -def generate_nominal_span(start_epoch: datetime, end_epoch: datetime) -> str: - """ - Generate the 3 character LEN for IGS filename based on the start and end epochs passed in - """ - span = (end_epoch - start_epoch).total_seconds() - if span % 86400 == 0.0: - unit = "D" - span = int(span // 86400) - elif span % 3600 == 0.0: - unit = "H" - span = int(span // 3600) - elif span % 60 == 0.0: - unit = "M" - span = int(span // 60) - else: - raise NotImplementedError - - return f"{span:02}{unit}" - - -def generate_long_filename( - analysis_center: str, # AAA - content_type: str, # CNT - format_type: str, # FMT - start_epoch: datetime, - end_epoch: datetime = None, - timespan: timedelta = None, - solution_type: str = "", # TTT - sampling_rate: str = "15M", # SMP - version: str = "0", # V - project: str = "EXP", # PPP, e.g. EXP, OPS -) -> str: - """ - Function to generate filename with IGS Long Product Filename convention (v1.0) as outlined in - http://acc.igs.org/repro3/Long_Product_Filenames_v1.0.pdf - - AAAVPPPTTT_YYYYDDDHHMM_LEN_SMP_CNT.FMT[.gz] - """ - initial_epoch = start_epoch.strftime("%Y%j%H%M") - if end_epoch == None: - end_epoch = start_epoch + timespan - timespan_str = generate_nominal_span(start_epoch, end_epoch) - - result = ( - f"{analysis_center}{version}{project}" - f"{solution_type}_" - f"{initial_epoch}_{timespan_str}_{sampling_rate}_" - f"{content_type}.{format_type}" - ) - return result - - -def generate_content_type(file_ext: str, analysis_center: str) -> str: - """ - IGS files following the long filename convention require a content specifier - Given the file extension, generate the content specifier - """ - file_ext = file_ext.upper() - file_ext_dict = { - "ERP": "ERP", - "SP3": "ORB", - "CLK": "CLK", - "OBX": "ATT", - "TRO": "TRO", - "SNX": "CRD", - "BIA": {"ESA": "BIA", None: "OSB"}, - } - content_type = file_ext_dict.get(file_ext) - # If result is still dictionary, use analysis_center to determine content_type - if isinstance(content_type, dict): - content_type = content_type.get(analysis_center, content_type.get(None)) - return content_type - - -def generate_sampling_rate(file_ext: str, analysis_center: str, solution_type: str) -> str: - """ - IGS files following the long filename convention require a content specifier - Given the file extension, generate the content specifier - """ - file_ext = file_ext.upper() - sampling_rates = { - "ERP": { - ("COD"): {"FIN": "12H", "RAP": "01D", "ERP": "01D"}, - (): "01D", - }, - "BIA": "01D", - "SP3": { - ("COD", "GFZ", "GRG", "IAC", "JAX", "MIT", "WUM"): "05M", - ("ESA"): {"FIN": "05M", "RAP": "15M", None: "15M"}, - (): "15M", - }, - "CLK": { - ("EMR", "MIT", "SHA", "USN"): "05M", - ("ESA", "GFZ", "GRG", "IGS"): {"FIN": "30S", "RAP": "05M", None: "30S"}, # DZ: IGS FIN has 30S CLK - (): "30S", - }, - "OBX": {"GRG": "05M", None: "30S"}, - "TRO": {"JPL": "30S", None: "01H"}, - "SNX": "01D", - } - if file_ext in sampling_rates: - file_rates = sampling_rates[file_ext] - if isinstance(file_rates, dict): - center_rates_found = False - for key in file_rates: - if analysis_center in key: - center_rates = file_rates.get(key, file_rates.get(())) - center_rates_found = True - break - # else: - # return file_rates.get(()) - if not center_rates_found: # DZ: bug fix - return file_rates.get(()) - if isinstance(center_rates, dict): - return center_rates.get(solution_type, center_rates.get(None)) - else: - return center_rates - else: - return file_rates - else: - return "01D" - - -def generate_product_filename( - reference_start: datetime, - file_ext: str, - shift: int = 0, - long_filename: bool = False, - AC: str = "IGS", - timespan: timedelta = timedelta(days=1), - solution_type: str = "ULT", - sampling_rate: str = "15M", - version: str = "0", - project: str = "OPS", - content_type: str = None, -) -> Tuple[str, GPSDate, datetime]: - """ - Generate filename, GPSDate obj from datetime - Optionally, move reference_start forward by "shift" hours - """ - reference_start += timedelta(hours=shift) - if type(reference_start == date): - gps_date = GPSDate(str(reference_start)) - else: - gps_date = GPSDate(str(reference_start.date())) - - if long_filename: - if content_type == None: - content_type = generate_content_type(file_ext, analysis_center=AC) - product_filename = ( - generate_long_filename( - analysis_center=AC, - content_type=content_type, - format_type=file_ext, - start_epoch=reference_start, - timespan=timespan, - solution_type=solution_type, - sampling_rate=sampling_rate, - version=version, - project=project, - ) - + ".gz" - ) - else: - if file_ext.lower() == "snx": - product_filename = f"igs{gps_date.yr[2:]}P{gps_date.gpswk}.snx.Z" - else: - hour = f"{reference_start.hour:02}" - product_filename = f"igu{gps_date.gpswkD}_{hour}.{file_ext}.Z" - return product_filename, gps_date, reference_start - - -def download_file_from_cddis( - filename: str, - ftp_folder: str, - output_folder: Path, - max_retries: int = 3, - decompress: bool = True, - if_file_present: str = "prompt_user", - note_filetype: str = None, -) -> None: - with ftp_tls("gdc.cddis.eosdis.nasa.gov") as ftps: - ftps.cwd(ftp_folder) - retries = 0 - download_done = False - while not download_done and retries <= max_retries: - try: - download_filepath = attempt_ftps_download( - download_dir=output_folder, - ftps=ftps, - filename=filename, - type_of_file=note_filetype, - if_file_present=if_file_present, - ) - if decompress and download_filepath: - download_filepath = decompress_file( - input_filepath=download_filepath, delete_after_decompression=True - ) - download_done = True - if download_filepath: - logging.info(f"Downloaded {download_filepath.name}") - except ftplib.all_errors as e: - retries += 1 - if retries > max_retries: - logging.info(f"Failed to download {filename} and reached maximum retry count ({max_retries}).") - if (output_folder / filename).is_file(): - (output_folder / filename).unlink() - raise e - - logging.debug(f"Received an error ({e}) while try to download {filename}, retrying({retries}).") - # Add some backoff time (exponential random as it appears to be contention based?) - sleep(random.uniform(0.0, 2.0**retries)) - return download_filepath - - -def download_product_from_cddis( - download_dir: Path, - start_epoch: datetime, - end_epoch: datetime, - file_ext: str, - limit: int = None, - long_filename: bool = False, - analysis_center: str = "IGS", - solution_type: str = "ULT", - sampling_rate: str = "15M", - project_type: str = "OPS", - timespan: timedelta = timedelta(days=2), - if_file_present: str = "prompt_user", -) -> None: - """ - Download the file/s from CDDIS based on start and end epoch, to the - provided the download directory (download_dir) - """ - # DZ: Download the correct IGS FIN ERP files - if file_ext == "ERP" and analysis_center == "IGS" and solution_type == "FIN": # get the correct start_epoch - start_epoch = GPSDate(str(start_epoch)) - start_epoch = gpswkD2dt(f"{start_epoch.gpswk}0") - timespan = timedelta(days=7) - # Details for debugging purposes: - logging.debug("Attempting CDDIS Product download/s") - logging.debug(f"Start Epoch - {start_epoch}") - logging.debug(f"End Epoch - {end_epoch}") - - reference_start = deepcopy(start_epoch) - product_filename, gps_date, reference_start = generate_product_filename( - reference_start, - file_ext, - long_filename=long_filename, - AC=analysis_center, - timespan=timespan, - solution_type=solution_type, - sampling_rate=sampling_rate, - project=project_type, - ) - logging.debug( - f"Generated filename: {product_filename}, with GPS Date: {gps_date.gpswkD} and reference: {reference_start}" - ) - with ftp_tls("gdc.cddis.eosdis.nasa.gov") as ftps: - try: - ftps.cwd(f"gnss/products/{gps_date.gpswk}") - except ftplib.all_errors as e: - logging.info(f"{reference_start} too recent") - logging.info(f"ftp_lib error: {e}") - product_filename, gps_date, reference_start = generate_product_filename( - reference_start, - file_ext, - shift=-6, - long_filename=long_filename, - AC=analysis_center, - timespan=timespan, - solution_type=solution_type, - sampling_rate=sampling_rate, - project=project_type, - ) - ftps.cwd(f"gnss/products/{gps_date.gpswk}") - - all_files = ftps.nlst() - if not (product_filename in all_files): - logging.info(f"{product_filename} not in gnss/products/{gps_date.gpswk} - too recent") - raise FileNotFoundError - - # reference_start will be changed in the first run through while loop below - reference_start -= timedelta(hours=24) - count = 0 - remain = end_epoch - reference_start - while remain.total_seconds() > timespan.total_seconds(): - if count == limit: - remain = timedelta(days=0) - else: - product_filename, gps_date, reference_start = generate_product_filename( - reference_start, - file_ext, - shift=24, # Shift at the start of the loop - speeds up total download time - long_filename=long_filename, - AC=analysis_center, - timespan=timespan, - solution_type=solution_type, - sampling_rate=sampling_rate, - project=project_type, - ) - download_filepath = check_whether_to_download( - filename=product_filename, download_dir=download_dir, if_file_present=if_file_present - ) - if download_filepath: - download_file_from_cddis( - filename=product_filename, - ftp_folder=f"gnss/products/{gps_date.gpswk}", - output_folder=download_dir, - if_file_present=if_file_present, - note_filetype=file_ext, - ) - count += 1 - remain = end_epoch - reference_start - - -def check_whether_to_download( - filename: str, download_dir: Path, if_file_present: str = "prompt_user" -) -> Union[Path, None]: - """ - Determine whether to download given file (filename) to the desired location (download_dir) based on whether it is - already present and what action to take if it is (if_file_present). Output is the Path obj to the file to be - downloaded (output_path) or None if file is not to be downloaded (already present and skipped) - """ - # Flag to determine whether to download: - download = None - # Create Path obj to where file will be - if original file is compressed, check for decompressed file - uncompressed_filename = generate_uncompressed_filename(filename) # Returns original filename if not compressed - output_path = download_dir / uncompressed_filename - # Check if file already exists - if so, then re-download or not based on if_file_present value - if output_path.is_file(): - if if_file_present == "prompt_user": - replace = click.confirm( - f"File {output_path} already present; download and replace? - Answer:", default=None - ) - if replace: - logging.info(f"Option chosen: Replace. Re-downloading {output_path.name} to {download_dir}") - download = True - else: - logging.info(f"Option chosen: Don't Replace. Leaving {output_path.name} as is in {download_dir}") - download = False - elif if_file_present == "dont_replace": - logging.info(f"File {output_path} already present; Flag --dont-replace is on ... skipping download ...") - download = False - elif if_file_present == "replace": - logging.info( - f"File {output_path} already present; Flag --replace is on ... re-downloading to {download_dir}" - ) - download = True - else: - download = True - - if download == False: - return None - elif download == True: - if uncompressed_filename == filename: # Existing Path obj is already the one we need to download - return output_path - else: - return download_dir / filename # Path to compressed file to download - elif download == None: - logging.error(f"Invalid internal flag value for if_file_present: '{if_file_present}'") - - -def attempt_ftps_download( - download_dir: Path, - ftps: ftplib.FTP_TLS, - filename: str, - type_of_file: str = None, - if_file_present: str = "prompt_user", -) -> Path: - """ - Attempt download of file (filename) given the ftps client object (ftps) to chosen location (download_dir) - """ - logging.info(f"Attempting FTPS Download of {type_of_file} file - {filename} to {download_dir}") - download_filepath = check_whether_to_download( - filename=filename, download_dir=download_dir, if_file_present=if_file_present - ) - if download_filepath: - logging.debug(f"Downloading {filename}") - with open(download_filepath, "wb") as local_file: - ftps.retrbinary(f"RETR {filename}", local_file.write) - - return download_filepath - - -def attempt_url_download( - download_dir: Path, url: str, filename: str = None, type_of_file: str = None, if_file_present: str = "prompt_user" -) -> Path: - """ - Attempt download of file given URL (url) to chosen location (download_dir) - """ - # If the filename is not provided, use the filename from the URL - if not filename: - filename = url[url.rfind("/") + 1 :] - logging.info(f"Attempting URL Download of {type_of_file} file - {filename} to {download_dir}") - # Use the check_whether_to_download function to determine whether to download the file - download_filepath = check_whether_to_download( - filename=filename, download_dir=download_dir, if_file_present=if_file_present - ) - if download_filepath: - download_url(url, download_filepath) - return download_filepath - - def download_atx(download_dir: Path, long_filename: bool = False, if_file_present: str = "prompt_user") -> None: """ Download the ATX file necessary for running the PEA provided the download directory (download_dir) @@ -475,28 +57,6 @@ def download_atx(download_dir: Path, long_filename: bool = False, if_file_presen ) -def generate_uncompressed_filename(filename: str) -> str: - """ - Name of uncompressed filename given the [assumed compressed] filename (filename). - If not one of the recognized compression format types, return original filename - - """ - if filename.endswith(".tar.gz") or filename.endswith(".tar"): - with tarfile.open(filename, "r") as tar: - # Get name of file inside tar.gz file (assuming only one file) - return tar.getmembers()[0].name - elif filename.endswith(".crx.gz"): - return filename[:-6] + "rnx" - elif filename.endswith(".gz"): - return filename[:-3] - elif filename.endswith(".Z"): - return filename[:-2] - elif filename.endswith(".bz2"): - return filename[:-4] - else: - logging.debug(f"{filename} not compressed - extension not a recognized compression format") - return filename - - def download_atmosphere_loading_model(download_dir: Path, if_file_present: str = "prompt_user") -> Path: """ Download the Atmospheric loading BLQ file necessary for running the PPP example @@ -561,19 +121,19 @@ def download_brdc( reference_dt += timedelta(days=1) -def download_geomagnetic_model(download_dir: Path, model: str = "igrf13", if_file_present: str = "prompt_user") -> Path: +def download_geomagnetic_model(download_dir: Path, model: str = "igrf14", if_file_present: str = "prompt_user") -> Path: """ Download the International Geomagnetic Reference Field model file necessary for running the PPP example provided the download directory (download_dir) - Default: IGRF13 coefficients + Default: IGRF14 coefficients """ - if model == "igrf13": + if model == "igrf14": ensure_folders([download_dir]) download_filepath = attempt_url_download( download_dir=download_dir, - url="https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/igrf13coeffs.txt.gz", - filename="igrf13coeffs.txt.gz", - type_of_file="Geomagnetic Field coefficients - IGRF13", + url="https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/products/tables/igrf14coeffs.txt.gz", + filename="igrf14coeffs.txt.gz", + type_of_file="Geomagnetic Field coefficients - IGRF14", if_file_present=if_file_present, ) else: @@ -880,7 +440,7 @@ def search_for_most_recent_file( reference_start=pointer_date.as_datetime, file_ext=file_type, long_filename=long_filename, - AC=analysis_center, + analysis_center=analysis_center, timespan=timespan, solution_type=solution_type, sampling_rate=sampling_rate, @@ -900,7 +460,7 @@ def search_for_most_recent_file( reference_start=pointer_date.as_datetime, file_ext=file_type, long_filename=long_filename, - AC=analysis_center, + analysis_center=analysis_center, timespan=timespan, solution_type=solution_type, sampling_rate=sampling_rate, @@ -996,18 +556,6 @@ def download_files_from_gnss_data( logging.info(f"Not downloaded / missing: {list(missing_stations)}") -def long_filename_cddis_cutoff(epoch: datetime) -> bool: - """ - Simple function that determines whether long filenames should be expected on the CDDIS server - """ - # Long filename cut-off: - long_filename_cutoff = datetime(2022, 11, 27) - if epoch >= long_filename_cutoff: - return True - else: - return False - - def most_recent_6_hour(): """ Returns a datetime object set to the most recent hour divisible by 6: (0000, 0600, 1200 or 1800) @@ -1195,11 +743,15 @@ def auto_download( timespan=timedelta(days=1), if_file_present=if_file_present, ) - except FileNotFoundError: - logging.info(f"Received an error ({FileNotFoundError}) while try to download - date too recent.") + except ftplib.all_errors as e: + logging.info(f"Received an error ({e}) while try to download - date too recent.") logging.info(f"Downloading most recent SNX file available.") download_most_recent_cddis_file( - download_dir=target_dir, pointer_date=start_gpsdate, file_type="SNX", long_filename=long_filename + download_dir=target_dir, + pointer_date=start_gpsdate, + file_type="SNX", + long_filename=long_filename, + if_file_present=if_file_present, ) if sp3: download_product_from_cddis( @@ -1303,6 +855,11 @@ def auto_download( help="Provide comma-separated list of IGS stations to download - daily observation RNX files", type=str, ) +@click.option( + "--station-list-file", + help="Read file of newline separated list of IGS stations to download - takes precedence over option --station-list", + type=click.File("r"), +) @click.option("--start-datetime", help="Start of date-time period to download files for", type=str) @click.option("--end-datetime", help="End of date-time period to download files for", type=str) @click.option("--replace", help=" Re-download all files already present in target-dir", default=False, is_flag=True) @@ -1328,10 +885,10 @@ def auto_download( @click.option("--gpt2", help="Flag to Download GPT 2.5 file", default=False, is_flag=True) @click.option("--rinex-data-dir", help="Directory to Download RINEX data file/s. Default: target-dir", type=Path) @click.option("--trop-dir", help="Directory to Download troposphere model file/s. Default: target-dir", type=Path) -@click.option("--model-dir", help="Directory to Download static model files. Default: product-dir / tables", type=Path) +@click.option("--model-dir", help="Directory to Download static model files. Default: target-dir / tables", type=Path) @click.option( "--solution-type", - help="The solution type of products to download from CDDIS. 'RAP': rapid, or 'ULT': ultra-rapid. Default: RAP", + help="The solution type of products to download from CDDIS. 'FIN': final, or 'RAP': rapid, or 'ULT': ultra-rapid. Default: RAP", default="RAP", type=str, ) @@ -1372,6 +929,7 @@ def auto_download_main( target_dir, preset, station_list, + station_list_file, start_datetime, end_datetime, replace, @@ -1413,6 +971,9 @@ def auto_download_main( station_list = None if not station_list == None: station_list = station_list.split(",") + if station_list_file: + content = station_list_file.read() + station_list = content.split("\n") auto_download( target_dir, preset, diff --git a/scripts/auto_generate_yaml.py b/scripts/deprecated_scripts/auto_generate_yaml.py similarity index 100% rename from scripts/auto_generate_yaml.py rename to scripts/deprecated_scripts/auto_generate_yaml.py diff --git a/scripts/auto_run_PPP.py b/scripts/deprecated_scripts/auto_run_PPP.py similarity index 100% rename from scripts/auto_run_PPP.py rename to scripts/deprecated_scripts/auto_run_PPP.py diff --git a/scripts/compareGinanJson.py b/scripts/deprecated_scripts/compareGinanJson.py similarity index 100% rename from scripts/compareGinanJson.py rename to scripts/deprecated_scripts/compareGinanJson.py diff --git a/scripts/createAppimage.sh b/scripts/deprecated_scripts/createAppimage.sh similarity index 60% rename from scripts/createAppimage.sh rename to scripts/deprecated_scripts/createAppimage.sh index 3ed3bc970..6c623bbbf 100755 --- a/scripts/createAppimage.sh +++ b/scripts/deprecated_scripts/createAppimage.sh @@ -1,9 +1,9 @@ #!/bin/bash -mkdir linuxDeploy +mkdir -p linuxDeploy cd linuxDeploy -wget https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20230713-1/linuxdeploy-x86_64.AppImage +curl -L -o linuxdeploy-x86_64.AppImage https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20230713-1/linuxdeploy-x86_64.AppImage apt update -y apt install -y file diff --git a/scripts/download_slr_data.py b/scripts/deprecated_scripts/download_slr_data.py similarity index 100% rename from scripts/download_slr_data.py rename to scripts/deprecated_scripts/download_slr_data.py diff --git a/scripts/qzss_ohi_merge.py b/scripts/deprecated_scripts/qzss_ohi_merge.py similarity index 100% rename from scripts/qzss_ohi_merge.py rename to scripts/deprecated_scripts/qzss_ohi_merge.py diff --git a/scripts/download_archives.py b/scripts/download_archives.py deleted file mode 100644 index 0f350339f..000000000 --- a/scripts/download_archives.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Downloads auxiliary example_input_data files to/from example_input_data dir""" -import logging -import hashlib -import concurrent.futures -import base64 -import os - -from pathlib import Path -from typing import Union as _Union -import tarfile - -import boto3 -from botocore import UNSIGNED -from botocore.config import Config - -import click as _click - -logging.basicConfig(level=logging.INFO, format="%(message)s") -logger = logging.getLogger(__name__) - - -def compute_checksum(path2file: str) -> str: - """Computes checksum of a file given its path - - :param str path2file: path to the file - :return str: checksum value - """ - logger.debug(f'Computing checksum of "{path2file}"') - with open(path2file, "rb") as file: - filehash = hashlib.md5() - for data in iter(lambda: file.read(8 * 1024 * 1024), b""): - filehash.update(data) - checksum = base64.b64encode(filehash.digest()).decode() - logger.debug(f'Got "{checksum}"') - return checksum - - -def create_s3_client(profile_name: str = None, access_key: str = None, secret_key: str = None) -> boto3.client: - """ - create_s3_client creates a boto3 client for s3 - if profile_name is provided, it will use the credentials from the profile - if access_key and secret_key are provided, it will use those credentials - otherwise it will create an anonymous client - - :param str profile_name: _description_, defaults to None - :param str access_key: _description_, defaults to None - :param str secret_key: _description_, defaults to None - :return boto3.client: _description_ - """ - if profile_name: - session = boto3.Session(profile_name=profile_name) - logger.debug("setting up s3 client with profile %s", profile_name) - elif access_key and secret_key: - session = boto3.Session(aws_access_key_id=access_key, aws_secret_access_key=secret_key) - logger.debug("setting up s3 client with access key and secret key") - else: - session = boto3.Session() - logger.debug("setting up s3 client with no credentials") - s3 = session.client("s3", config=Config(signature_version=UNSIGNED, max_pool_connections=50)) - return s3 - - -def read_tags_from_file(file_path): - """ - read_tags_from_file _summary_ - - :param _type_ file_path: _description_ - :return _type_: _description_ - """ - dictionary = {} - with open(file_path, "r", encoding="utf-8") as file: - for line in file: - if "=" in line: - key, value = line.strip().split("=") - value = value.strip('"') # Remove quotes from the value - dictionary[key] = value - return dictionary - - -def get_list_from_tag(s3client, bucket, dictdata, target, dirs, list_to_download): - """ - get_list_from_tag - Function to get the list of files to download from the tag, - for each element in the dictionary dictdata, it will need to download the file at the following address - https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/solutions//index.json - concatenate them all in a dictionary. - :param _type_ dictdata: _description_ - """ - ex_type_dict = {"PEA": [0, 1, 4], "POD": [2], "PEAPOD": [3], "OTHER": [5]} - ex_type_dict["ALL"] = list(set([item for sublist in ex_type_dict.values() for item in sublist])) - - logger.info(f" to download {dirs}") - for key, tag in dictdata.items(): - logger.debug(f"Looking for data in {key} tagged {tag}") - request = s3client.list_objects(Bucket=bucket, Prefix=f"{target}/solutions/{tag}/") - try: - for data in request["Contents"]: - name = data["Key"].split("/")[-1].split(".")[0] - if (len(dirs) == 0 or name in dirs) and int(name[2]) in ex_type_dict[key]: - logger.debug(f"Found {data['Key']} in {key}") - list_to_download.append(data["Key"]) - except KeyError: - logger.warning(f"{request['Prefix']} on bucket {request['Name']} not found") - - -def check_checksum(s3client, bucket, file, download_file): - logger.info(f"Checking checksum of {file}") - response = s3client.head_object(Bucket=bucket, Key=file) - if compute_checksum(download_file) != response["Metadata"]["md5checksum"]: - raise Exception(f"Checksum failed for {file}") - logger.info(" -> Checksum OK") - - -def download_file(s3client, bucket, file, checksum_check=False): - logger.info(f"Downloading {file} from {bucket}") - filename = file.split("/")[-1] - s3client.download_file(bucket, file, filename) - if checksum_check: - try: - check_checksum(s3client, bucket, file, filename) - except Exception as excep: - os.remove(filename) - raise excep - return filename - - -def extract(filename, path): - logger.info(f" -> Extracting {filename} to {path}") - with tarfile.open(filename, "r:bz2") as tar: - for member in tar.getmembers(): - if not member.name.startswith("/") and ".." not in member.name: - tar.extract(member, path) - else: - logging.warning(f"Skipping dangerous member: {member.name}") - os.remove(filename) - logger.info(f" -> Extracted {filename} to {path}") - - -def process_dwl_file(s3client: boto3.client, bucket, file, path, skip_extract): - """ - download_file - Function to download the file from the bucket and extract it to the path - :param _type_ file: _description_ - """ - try: - filename = download_file(s3client, bucket, file, checksum_check=True) - if not skip_extract: - extract(filename, path) - else: - logger.info(f" -> Skipping extraction of {file} to {path}") - except Exception as e: - raise e - - -def process_dwl_files_concurrently( - s3client: boto3.client, bucket: str, files: list, path: Path, skip_extract: bool -) -> None: - """ - Download files concurrently using a ThreadPoolExecutor. - """ - with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: - futures = [executor.submit(process_dwl_file, s3client, bucket, file, path, skip_extract) for file in files] - - for future in concurrent.futures.as_completed(futures): - try: - future.result() # Ensure any exceptions are raised - except Exception as e: - logger.warning(f"Failed to download file: {str(e)}") - - -def generate_list_of_files(dirs, bucket, target, solutions, data, products, loading, s3, tag_dict) -> list: - list_to_download = [] - if products: - list_to_download.append(f"{target}/products.tar.bz2") - if data: - list_to_download.append(f"{target}/data.tar.bz2") - if loading: - list_to_download.append(f"{target}/loading.tar.bz2") - if solutions: - get_list_from_tag(s3, bucket, tag_dict, target, dirs, list_to_download) - return list_to_download - - -def generate_tag_dict(tag: str, tags_file_path: str) -> dict: - """ - generate_tag_dict _summary_ - - :param _type_ tag: _description_ - :param _type_ tags_file_path: _description_ - :return _type_: _description_ - """ - tag_dict = {} - if not tag: - logger.info(f"reading tags from {tags_file_path}") - tag_dict = read_tags_from_file(tags_file_path) - else: - tag_dict["ALL"] = tag - logger.info(f"using the provided {tag_dict} tag") - return tag_dict - - -# function to download_example_data, command line argument viw click p for product, l for loading, s for solution, followed by uknown number of arguments -@_click.command() -@_click.argument("dirs", nargs=-1, type=str, default=None) -@_click.option("--bucket", default="peanpod", help="s3 bucket name to push and pull from") -@_click.option("--target", default="aux", help="s3 target name (dir) within the selected bucket") -@_click.option( - "--path", - type=Path, - default="inputData", - help="custom path to inputData dir, a dir that stores products/data etc, default is inputData", -) -@_click.option("--tagpath", type=Path, default="docker/tags") -@_click.option("--tag", type=str, default=None) -@_click.option( - "--skip_extract", - is_flag=True, - show_default=True, - default=False, - help=( - """Skips extraction of the on-disk tar file if checksum test is OK and the - destination dir exists. This is done to save time in the pipeline as - there is no need to overwrite the files.""" - ), -) -@_click.option("-s", "--solutions", is_flag=True, help="download solutions") -@_click.option("-d", "--data", is_flag=True, help="download data") -@_click.option("-p", "--products", is_flag=True, help="download products") -@_click.option("-l", "--loading", is_flag=True, help="download loadings") -@_click.option("--profile", default=None, help="aws profile name") -@_click.option("-v", "--verbose", is_flag=True, help="verbose output") -def download_example_data( - dirs: str, - bucket: str, - target: str, - path: Path, - tagpath: Path, - tag: str, - skip_extract: bool, - solutions: bool, - data: bool, - products: bool, - loading: bool, - profile: str, - verbose: bool, -): - """Downloads auxiliary example_input_data files to/from example_input_data dir""" - logger.setLevel(logging.DEBUG if verbose else logging.INFO) - # todo later plug the possible for the keys. (not needed for download) - s3 = create_s3_client(profile_name=profile, access_key=None, secret_key=None) - tag_dict = generate_tag_dict(tag, tagpath.resolve()) - logger.info(f"list of tags {tag_dict}") - list_to_download = generate_list_of_files(dirs, bucket, target, solutions, data, products, loading, s3, tag_dict) - logger.info("list of files to download") - for file in list_to_download: - logger.info(f" - {file}") - process_dwl_files_concurrently(s3, bucket, list_to_download, path.resolve(), skip_extract) - - -if __name__ == "__main__": - download_example_data() diff --git a/scripts/download_example_input_data.py b/scripts/download_example_input_data.py deleted file mode 100755 index 30674f242..000000000 --- a/scripts/download_example_input_data.py +++ /dev/null @@ -1,322 +0,0 @@ -#!/usr/bin/env python3 - -"""Downloads/Uploads auxiliary example_input_data files to/from example_input_data dir""" -import logging as _logging -from pathlib import Path as _Path -from shutil import copy as _copy -from shutil import rmtree as _rmtree -from typing import Union as _Union - -import click as _click -import gnssanalysis as ga - -EX_GLOB_DICT = { - "ex02": ["*.TRACE"], - "ex11": ["*.TRACE", "*.snx", "*.*_smoothed"], - "ex12": ["*.TRACE", "*.snx"], - "ex13": ["*.TRACE", "*.snx"], - "ex14": ["*.TRACE", "*.snx"], - "ex16": ["*Network*.TRACE", "*.*I", "*.stec", "*.snx", "*.BIA", "*.INX*"], - "ex17": ["*Network*.TRACE", "*.snx", "*.clk*"], - "ex21": ["pod*.out", "*.sp3"], - "ex22g": ["pod*.out", "*.sp3"], - "ex22r": ["pod*.out", "*.sp3"], - "ex22e": ["pod*.out", "*.sp3"], - "ex22c": ["pod*.out", "*.sp3"], - "ex22j": ["pod*.out", "*.sp3"], - "ex23": ["pod*.out", "*.sp3"], - "ex24": ["pod*.out", "*.sp3"], - "ex25": ["pod*.out", "*.sp3"], - "ex26": ["pod*.out", "*.sp3"], - "ex31": [ - "pod_fit/pod*.out", - "pod_fit/*.sp3", - "pea/*etwork*.TRACE*", # Network starts with lower case for some reason - "pea/*.snx", - "pea/*.erp", - "pea/*clk*", - "pod_ic/pod*.out", - "pod_ic/*.sp3", - ], - "ex41": ["*.TRACE", "*.snx", "*.*_smoothed"], - "ex42": ["*.TRACE", "*.snx"], - "ex43": ["*.TRACE", "*.snx"], - "ex43a": ["*.TRACE", "*.snx"], - "ex44": ["*.TRACE", "*.snx"], - "ex48": ["*.TRACE", "*.snx"], - "ex51": [ - "*blq", - ], - "ex52": [ - "*blq", - ], -} - - -def insert_tag(name: str, tag: str) -> str: - """inserts tag name right before the filename: - insert_tag('ex11','some_tag') -> 'some_tag/ex11' - insert_tag('solutions/ex11','some_tag') -> 'solutions/some_tag/ex11' - """ - name_split = name.split("/") - name_split.insert(-1, tag) - return "/".join(name_split) - - -def get_example_type(name: str) -> _Union[str, bool]: - """ - Checks if input string is a type of example dir, e.g. ex22g, - returns string type of the example test. - ex13 (name[2]==1) is 'PEA' and ex21 (name[2]==2) is 'POD' - """ - ex_type_dict = {"0": "PEA", "1": "PEA", "2": "POD", "3": "PEAPOD", "4": "PEA", "5": "OTHER"} - if name.startswith("ex"): - try: - idx = name[2] - return ex_type_dict[idx] - except KeyError: - raise ValueError(f"Example code '{idx}' could not be matched to a type") - return False - - -def update_solutions_dict(example_input_data_dir: _Path, directory: str, ex_glob_dict: dict, tag: str = ""): - """ """ - if get_example_type(directory): # room for five-symbol name - ex22g - example_dir = example_input_data_dir / directory - ref_sol_dir = example_input_data_dir / "solutions" / tag / directory - if example_dir.exists() and ref_sol_dir.exists(): - _rmtree(ref_sol_dir) - _logging.info( - f"removing {ref_sol_dir} and its content" - ) # if actual solution exists -> clean respective reference solution before copying - l = len(example_dir.as_posix()) - - if directory not in ex_glob_dict.keys(): - raise ValueError(f"{directory} not in EX_GLOB_DICT dictionary") - - paths_list = [] - for expr in ex_glob_dict[directory]: - paths = list(example_dir.glob(expr)) - if paths == []: - _logging.warning(msg=f"no files were found using the {expr} rule") - paths_list.append(paths) - for path in paths: - dst = ref_sol_dir / (path.as_posix()[l + 1 :]) - dst.parent.mkdir(parents=True, exist_ok=True) - _logging.info(f"Copying {path} -> {_copy(src=path,dst=dst)}") - if not any(paths_list): - raise ValueError( - f"No files found in '{example_dir}' according to {directory} directory rules from EX_GLOB_DICT: {ex_glob_dict[directory]}" - ) - - -def upload_example_input_data_tar( - example_output_data_path: _Union[_Path, str], - bucket: str, - target: str, - dirs: _Union[list, tuple] = ("products", "data", "loading", "solutions"), - compression: str = "bz2", - tag: str = "", - show_progress: bool = False, - push_no_tar: bool = False, -): - __doc__ = ( - "tars selected aux dirs from ginan/inputData and compares their checksums with" - " the ones from s3 bucket. If checksums different - upload, else log info" - " message and do nothing. Default paths are [bucket] s3://peanpod/aux/ ->" - " [html] https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/" - ) - base_url = f"https://{bucket}.s3.ap-southeast-2.amazonaws.com/{target}" - for directory in dirs: - # update tarname with tag - example_type = get_example_type(directory) - if example_type: - directory = "solutions/" + (f"{tag}/" if tag != "" else "") + directory - tarname = directory + ".tar." + compression - destpath_targz = example_output_data_path / tarname - dest_url = base_url + "/" + tarname - if not push_no_tar: - ga.gn_io.common.tar_compress( - srcpath=example_output_data_path / directory, - destpath=destpath_targz, - reset_info=example_type, # reset timestamp etc for examples only - compression=compression, - ) # overwrite only if push_no_tar is False (default) - md5_checksum_aws = ga.gn_download.request_metadata(dest_url) - md5_checksum = ga.gn_io.common.compute_checksum(destpath_targz) - if md5_checksum_aws != md5_checksum: - _logging.info(f'checksums different -> uploading "{tarname}"') - ga.gn_download.upload_with_chunksize_and_meta( - local_file_path=destpath_targz, - metadata={"md5checksum": md5_checksum}, - public_read=True, - bucket_name=bucket, - object_key=target + "/" + tarname, - verbose=show_progress, - ) - else: - _logging.info(f"checksums the same -> skipping upload") - _logging.info(f"------------------------------") - - -def download_example_input_data_tar( - example_input_data_path: _Union[_Path, str], - bucket: str, - target: str, - dirs: _Union[list, tuple] = ("products", "data", "solutions", "loading"), - compression: str = "bz2", - tag: str = "", - skip_extract: bool = False, - tags_file_path: _Union[str, None] = None, # fall back on tags file if no tag was provided -): - __doc__ = ( - "Downloads compressed selected tarballs from ap-southeast-2 s3 bucket, untars" - " them to ginan/inputData dir. If tarball is available locally then checksums" - " are compared at first. If the same - nothing is downloaded, local tarball" - " gets uncompressed" - ) - - base_url = f"https://{bucket}.s3.ap-southeast-2.amazonaws.com/{target}" - if tag == "": - if tags_file_path is None: - raise ValueError("tag not provided and tags_file_path not provided") - _logging.info(f"reading tags from {tags_file_path}") - tag = ga.gn_download.get_vars_from_file(tags_file_path) - else: - _logging.info(f"using the provided {tag} tag") - - for directory in dirs: - example_type = get_example_type(directory) - - if example_type: - dir_url = f"solutions/{(f'{tag[example_type]}/{directory}' if isinstance(tag,dict) else f'{tag}/{directory}')}.tar.{compression}" - directory = f"solutions/{directory}.tar.{compression}" - else: - directory = dir_url = f"{directory}.tar.{compression}" - - destpath_targz = example_input_data_path / directory - dest_url = base_url + "/" + dir_url - md5_checksum_aws = ga.gn_download.request_metadata(dest_url) - destpath = example_input_data_path / directory - if not destpath_targz.exists(): - _logging.info(msg=f"{directory} not found on disk ['{md5_checksum_aws}'].") - destpath_targz.parent.mkdir(parents=True, exist_ok=True) - ga.gn_download.download_url(url=dest_url, destfile=destpath_targz) - else: - _logging.info(msg=f"{directory} found on disk. Validating...") - md5_checksum = ga.gn_io.common.compute_checksum(destpath_targz) - if md5_checksum_aws != md5_checksum: - _logging.info(f'checksums different -> downloading "{dir_url}"') - ga.gn_download.download_url(url=dest_url, destfile=destpath_targz) - else: - _logging.info(f"checksums the same -> skipping download") - - if skip_extract and destpath.exists(): - _logging.info( - "skipping extraction step as '--skip_extract' provided, checksums the same and destination directory exists" - ) - else: - try: - ga.gn_io.common.tar_extract(srcpath=destpath_targz, destpath=destpath) - except Exception as e: - _logging.error(f"could not extract {destpath_targz} to {destpath} due to {e}") - - -@_click.command() -@_click.argument("dirs", nargs=-1, type=str) -@_click.option("--bucket", default="peanpod", help="s3 bucket name to push and pull from") -@_click.option("--target", default="aux", help="s3 target name (dir) within the selected bucket") -@_click.option( - "--path", - default=None, - help="custom path to inputData dir, a dir that stores products/data etc, default is ginan/inputData", -) -@_click.option("--tag", type=str, default="") -@_click.option( - "--skip_extract", - is_flag=True, - show_default=True, - default=False, - help=( - """Skips extraction of the on-disk tar file if checksum test is OK and the - destination dir exists. This is done to save time in the pipeline as - there is no need to overwrite the files.""" - ), -) -@_click.option("-s", "--solutions", is_flag=True, help="download/upload solutions") -@_click.option("-d", "--data", is_flag=True, help="download/upload data") -@_click.option("-p", "--products", is_flag=True, help="download/upload products") -@_click.option("-l", "--loading", is_flag=True, help="download/upload loadings") -@_click.option("--push", is_flag=True, help="tar dirs and push them to aws with checksum metadata and public read") -@_click.option("--push_no_tar", is_flag=True, help="push tar archive which is present on disk") -def download_example_input_data(dirs, bucket, target, path, tag, skip_extract, solutions, data, products, loading, push, push_no_tar): - """Downloads 'products', 'data', and 'solutions' tarballs from s3 bucket and - extracts the content into inputData dir. The list of tarballs can be - changed with the combination of [-p/-d/-s] options. Similar tarballs - upload functionality is available - can be activated with '--push' key - To configure the utility for --push functionality it is enough to create - ~/.aws/credentials file containing - [default] / aws_access_key_id=ACCESS_KEY / - aws_secret_access_key=SECRET_KEY""" - _logging.getLogger().setLevel(_logging.INFO) - script_path = _Path(__file__).resolve().parent - if path is None: - example_input_data_path = (script_path.parent / "inputData").resolve() - _logging.info(f"default input path relative to script location selected: {example_input_data_path}") - else: - example_input_data_path = _Path(path) - _logging.info(f"custom input path selected: {example_input_data_path}") - - if path is None: - example_output_data_path = _Path("./") - _logging.info(f"default output path relative to script location selected: {example_output_data_path}") - else: - example_output_data_path = _Path(path) - _logging.info(f"custom output path selected: {example_output_data_path}") - - if not dirs: - if products: - dirs += ("products",) - if loading: - dirs += ("loading",) - if data: - dirs += ("data",) - if solutions: - dirs += tuple(EX_GLOB_DICT.keys()) - if not dirs: # if nothing has been selected - dirs = ("products", "data") + tuple(EX_GLOB_DICT.keys()) - - _logging.info(f"{dirs} selected") - - if push or push_no_tar: - # copy over the required files if exist - if solutions/blah -> rm blah, copy from ../blah to solutions/blah - _logging.info(msg="updating solutions") - [ - update_solutions_dict(example_input_data_dir=example_output_data_path, directory=directory, ex_glob_dict=EX_GLOB_DICT, tag=tag) - for directory in dirs - ] - upload_example_input_data_tar( - dirs=dirs, - compression="bz2", - tag=tag, - example_output_data_path=example_output_data_path, - bucket=bucket, - target=target, - show_progress=False, - push_no_tar=push_no_tar, - ) - else: - download_example_input_data_tar( - dirs=dirs, - compression="bz2", - tag=tag, - example_input_data_path=example_input_data_path, - skip_extract=skip_extract, - bucket=bucket, - target=target, - tags_file_path=(script_path.parent / "docker" / "tags").as_posix(), - ) - - -if __name__ == "__main__": - download_example_input_data() diff --git a/scripts/formatting/fix_doxygen.py b/scripts/formatting/fix_doxygen.py new file mode 100644 index 000000000..45fa606bd --- /dev/null +++ b/scripts/formatting/fix_doxygen.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +import re +import sys +import os + +def fix_doxygen_comments(content): + """ + Fix Doxygen comments that appear after function parameter closing parenthesis + by moving them to the end of the last parameter line. + """ + + # Pattern to match function declarations with trailing Doxygen comments + # This matches the last parameter line followed by closing paren and comment + pattern = r'(\n\s*)([^)\n]+)(\s*\n\s*\)\s*)(///< [^\n\r]*)' + + def replace_func(match): + indent = match.group(1) + last_param = match.group(2) + closing_section = match.group(3) + comment = match.group(4).strip() + + # Add comment to end of last parameter line + return f'{indent}{last_param} {comment}{closing_section}' + + # Apply the replacement + result = re.sub(pattern, replace_func, content) + + return result + +def process_file(filepath): + """Process a single file to fix Doxygen comments.""" + try: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + original_content = content + fixed_content = fix_doxygen_comments(content) + + if fixed_content != original_content: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(fixed_content) + print(f"Fixed: {filepath}") + return True + else: + print(f"No changes needed: {filepath}") + return False + + except Exception as e: + print(f"Error processing {filepath}: {e}") + return False + +def main(): + if len(sys.argv) < 2: + print("Usage: python fix_doxygen.py ") + sys.exit(1) + + target = sys.argv[1] + + if os.path.isfile(target): + # Process single file + process_file(target) + elif os.path.isdir(target): + # Process all .cpp and .hpp files in directory recursively + files_processed = 0 + files_changed = 0 + + for root, dirs, files in os.walk(target): + for file in files: + if file.endswith(('.cpp', '.hpp', '.h', '.cc', '.cxx')): + filepath = os.path.join(root, file) + files_processed += 1 + if process_file(filepath): + files_changed += 1 + + print(f"\nSummary: {files_changed} files changed out of {files_processed} processed") + else: + print(f"Error: {target} is not a valid file or directory") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/scripts/formatting/reorganise_include.py b/scripts/formatting/reorganise_include.py new file mode 100644 index 000000000..e96f27ae6 --- /dev/null +++ b/scripts/formatting/reorganise_include.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 + +import re +import sys +import os + +def reorganize_includes_and_using(content): + """ + Reorganize C++ file to have: includes → namespace aliases → using statements. + Handles template aliases correctly. + """ + + # Split content into lines for easier processing + lines = content.split('\n') + + # Lists to collect different types of lines + header_comments = [] + includes = [] + namespace_aliases = [] + using_statements = [] + other_lines = [] + + # Track what section we're in + found_first_include = False + found_first_namespace = False + found_first_using = False + collecting_includes_and_using = True + + i = 0 + while i < len(lines): + line = lines[i].strip() + + # Skip empty lines when collecting + if not line and collecting_includes_and_using: + i += 1 + continue + + # Check for include statements + if line.startswith('#include'): + if not found_first_include: + found_first_include = True + includes.append(lines[i]) # Keep original formatting + i += 1 + continue + + # Check for namespace aliases (namespace alias = existing::namespace) + if line.startswith('namespace ') and '=' in line: + if not found_first_namespace: + found_first_namespace = True + namespace_aliases.append(lines[i]) # Keep original formatting + i += 1 + continue + + # Check for using statements (but not template aliases) + if line.startswith('using '): + # Check if this is a template alias by looking for 'template' on same or previous lines + is_template_alias = False + + # Check current line for template + if 'template' in line: + is_template_alias = True + + # Check if previous non-empty line contains template + j = i - 1 + while j >= 0 and not lines[j].strip(): + j -= 1 + if j >= 0 and 'template' in lines[j]: + is_template_alias = True + + if not is_template_alias: + if not found_first_using: + found_first_using = True + using_statements.append(lines[i]) # Keep original formatting + i += 1 + continue + + # If we haven't found includes/namespace/using yet, it's probably header comments + if not found_first_include and not found_first_namespace and not found_first_using: + header_comments.append(lines[i]) + i += 1 + continue + + # If we've found includes/namespace/using but this line is neither, we're done collecting + if found_first_include or found_first_namespace or found_first_using: + collecting_includes_and_using = False + + # Everything else goes to other_lines + other_lines.append(lines[i]) + i += 1 + + # Reconstruct the file + result_lines = [] + + # Add header comments + result_lines.extend(header_comments) + + # Add a blank line if we have header comments and includes + if header_comments and includes: + result_lines.append('') + + # Add all includes + result_lines.extend(includes) + + # Add a blank line between includes and namespace aliases + if includes and namespace_aliases: + result_lines.append('') + + # Add all namespace aliases + result_lines.extend(namespace_aliases) + + # Add a blank line between namespace aliases and using statements + if namespace_aliases and using_statements: + result_lines.append('') + # Add a blank line between includes and using statements when no namespace aliases + elif includes and using_statements and not namespace_aliases: + result_lines.append('') + # Add all using statements + result_lines.extend(using_statements) + + # Add a blank line between using statements and other code + if using_statements and other_lines: + result_lines.append('') + + # Add the rest of the code + result_lines.extend(other_lines) + + return '\n'.join(result_lines) + +def process_file(filepath): + """Process a single file to reorganize includes and using statements.""" + try: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + original_content = content + reorganized_content = reorganize_includes_and_using(content) + + if reorganized_content != original_content: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(reorganized_content) + print(f"Reorganized: {filepath}") + return True + else: + print(f"No changes needed: {filepath}") + return False + + except Exception as e: + print(f"Error processing {filepath}: {e}") + return False + +def main(): + if len(sys.argv) < 2: + print("Usage: python reorganize_includes.py ") + sys.exit(1) + + target = sys.argv[1] + + if os.path.isfile(target): + # Process single file + process_file(target) + elif os.path.isdir(target): + # Process all .cpp and .hpp files in directory recursively + files_processed = 0 + files_changed = 0 + + for root, dirs, files in os.walk(target): + for file in files: + if file.endswith(('.cpp', '.hpp', '.h', '.cc', '.cxx')): + filepath = os.path.join(root, file) + files_processed += 1 + if process_file(filepath): + files_changed += 1 + + print(f"\nSummary: {files_changed} files changed out of {files_processed} processed") + else: + print(f"Error: {target} is not a valid file or directory") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/gn/templates/auto_template.yaml b/scripts/gn/templates/auto_template.yaml index 30d8adae0..ebc9f906a 100644 --- a/scripts/gn/templates/auto_template.yaml +++ b/scripts/gn/templates/auto_template.yaml @@ -50,7 +50,7 @@ outputs: output_config: true sinex: {filename: .SNX, output: true} - + # log: # output: false # directory: ./logs/ @@ -88,14 +88,14 @@ receiver_options: troposphere: # Tropospheric modelling accounts for delays due to refraction of light in water vapour enable: true models: [ gpt2 ] # List of models to use for troposphere [standard,sbas,vmf3,gpt2,cssr] - tides: + tides: atl: true # Enable atmospheric tide loading enable: true # Enable modelling of tidal disaplacements opole: true # Enable ocean pole tides otl: true # Enable ocean tide loading solid: true # Enable solid Earth tides spole: true # Enable solid Earth pole tides - + # ALIC: # receiver_type: "LEICA GR25" # (string) @@ -168,12 +168,13 @@ processing_options: mw_process_noise: 0 # Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips slip_threshold: 0.05 # Value used to determine when a slip has occurred preprocess_all_data: true - + spp: # always_reinitialise: false # Reset SPP state to zero to avoid potential for lock-in of bad states max_lsq_iterations: 12 # Maximum number of iterations of least squares allowed for convergence outlier_screening: - raim: true # Enable Receiver Autonomous Integrity Monitoring + raim: + enable: true # Enable Receiver Autonomous Integrity Monitoring max_gdop: 30 # Maximum dilution of precision before error is flagged ppp_filter: @@ -199,7 +200,7 @@ processing_options: use_gf_combo: false # Combine 'uncombined' measurements to simulate a geometry-free solution use_if_combo: false # Combine 'uncombined' measurements to simulate an ionosphere-free solution - chunking: + chunking: by_receiver: false # Split large filter and measurement matrices blockwise by receiver ID to improve processing speed by_satellite: false # Split large filter and measurement matrices blockwise by satellite ID to improve processing speed size: 0 @@ -207,7 +208,6 @@ processing_options: rts: # Rauch-Tung-Striebel (RTS) backwards smoothing enable: true lag: -1 - 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 model_error_handling: diff --git a/scripts/installation/apple.md b/scripts/installation/apple.md index 5aa7da0ed..b9e035fce 100644 --- a/scripts/installation/apple.md +++ b/scripts/installation/apple.md @@ -1,9 +1,52 @@ # Installation procedure on Apple -Tested on Macbook Pro (Intel) with Somona OSX. +Tested on Macbook Pro (Intel) with Somona OSX and Macbook Pro (ARM64) with Sonoma OSX -After installation of brew, install the following packages using brew +## Install Ginan dependencies + +After installation of homebrew, install the following packages using brew ```bash brew install boost cmake eigen netcdf-cxx netcdf mongo-c-driver mongo-cxx-driver openblas openssl@3 yaml-cpp libomp +``` +*** + +Follow the instructions here to install the MongoDB application: +https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/` + +## Install gnssanalysis python module + +``` +pip3 install gnssanalysis +``` + +## Download Ginan from Github + +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 + +cd ginan +cd src +mkdir build +cd build +cmake -DCMAKE_TOOLCHAIN_FILE=compile_mac_arm64.cmake .. +make -j4 pea + +cd ../.. +./bin/pea --help +``` + +## Download Demo data and products + +Then download all of the example data using the python script provided (requires `gnssanalysis`): + +``` +cd inputData +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 480537c04..60baeafdc 100644 --- a/scripts/installation/generic.md +++ b/scripts/installation/generic.md @@ -8,17 +8,14 @@ If instead you wish to build Ginan from source, there are several software depen * BLAS and LAPACK linear algebra libraries. We use and recommend [OpenBlas](https://www.openblas.net/) as this contains both libraries required * CMAKE > 3.0 * YAML > 0.6 -* Boost >= 1.73 (tested on 1.73). On Ubuntu 22.04 which uses gcc-11, you need Boost >= 1.74.0 +* Boost >= 1.74 * MongoDB -* Mongo_C >= 1.71.1 -* Mongo_cxx >= 3.6.0 +* Mongo_C >= 1.71.1 (automatically installed with mongo-cxx-driver) +* Mongo_cxx >= 3.9.0 * Eigen3 > 3.4 * netCDF4 * Python >= 3.7 -If using gcc verion 11 or about, the minimum version of libraries are: -* Boost >= 1.74.0 -* Mongo_cxx = 3.7.0 *** ## Installing dependencies (Example with Ubuntu) @@ -37,25 +34,11 @@ sudo apt install -y git gobjc gobjc++ gfortran libopenblas-dev openssl curl net- sudo -H pip3 install wheel pandas boto3 unlzw tdqm scipy gnssanalysis ``` -Ginan requires at least version 9 of both gcc and g++, so make sure to update the gcc/g++ alternatives prior to compilation: -(this is not required on Ubuntu 22.04) - -``` -sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y - -sudo apt update - -sudo apt install -y gcc-9 g++-9 - -sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 51 - -sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 51 -``` *** ## Building additional dependencies -Depending on the user's installation choice: install PEA-only, POD-only or all software packages, a set of additional dependencies that need to be built may change. Below, we explain building all the additional dependencies: +Depending on the user's installation choice: install PEA-only, or all software packages, a set of additional dependencies that need to be built may change. Below, we explain building all the additional dependencies: Note that many `make` commands here have the option `-j 2` applied, this will enable parallel compilation and may speed up installation time. The number of threads can be increased by changing the number, such as `-j 8`, but be aware that each new thread may require up to 2GB of memory. @@ -90,16 +73,16 @@ rm -rf yaml-cpp ### Boost (PEA) PEA relies on a number of the utilities provided by [boost](https://www.boost.org/), such as their time and logging libraries. -NB for compilation using gcc-11, you need to change this to boost_1_74_0 + ``` cd $dir/tmp -wget -c https://boostorg.jfrog.io/artifactory/main/release/1.73.0/source/boost_1_73_0.tar.gz +wget -c https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.gz -tar -xf boost_1_73_0.tar.gz +tar -xf boost_1_83_0.tar.gz -cd boost_1_73_0/ +cd boost_1_83_0/ ./bootstrap.sh @@ -107,7 +90,7 @@ sudo ./b2 -j2 install cd $dir/tmp -sudo rm -rf boost_1_73_0 boost_1_73_0.tar.gz +sudo rm -rf boost_1_83_0 boost_1_83_0.tar.gz ``` ### Eigen3 (PEA) @@ -137,49 +120,28 @@ rm -rf eigen ### Mongo_cxx_driver (PEA) -Needed for json formatting and other self-descriptive markup. - -``` -cd $dir/tmp -# NB for compilation using gcc-11, you need to change this to 1.21.2 +Needed for connection to the MongoDB database, which is used for realtime plotting and statistics in `GinanEDA`. -wget https://github.com/mongodb/mongo-c-driver/releases/download/1.17.1/mongo-c-driver-1.17.1.tar.gz - -tar -xf mongo-c-driver-1.17.1.tar.gz - -cd mongo-c-driver-1.17.1/ - -mkdir cmake-build - -cd cmake-build/ - -cmake -DENABLE_AUTOMATIC_INIT_AND_CLEANUP=OFF -DENABLE_EXAMPLES=OFF ../ - -cmake --build . -- -j 2 - -sudo cmake --build . --target install -- -j 2 +Note: for later version of mongo-cxx-driver install (3.9.0 and above) it also the mongo-c driver, so it is not needed to install it separately. +``` cd $dir/tmp -NB for compilation using gcc-11, you need to change this to 3.7.0 - -curl -OL https://github.com/mongodb/mongo-cxx-driver/releases/download/r3.6.0/mongo-cxx-driver-r3.6.0.tar.gz - -tar -xf mongo-cxx-driver-r3.6.0.tar.gz +curl -OL https://github.com/mongodb/mongo-cxx-driver/releases/download/r3.11.0/mongo-cxx-driver-r3.11.0.tar.gz -cd mongo-cxx-driver-r3.6.0/build +tar -xf mongo-cxx-driver-r3.11.0.tar.gz -cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DENABLE_EXAMPLES=OFF ../ +cd mongo-cxx-driver-r3.11.0/build -sudo cmake --build . --target EP_mnmlstc_core -- -j 2 +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -cmake --build . -- -j 2 +make -j 1 -sudo cmake --build . --target install +sudo make install cd $dir/tmp -sudo rm -rf mongo-c-driver-1.17.1 mongo-c-driver-1.17.1.tar.gz mongo-cxx-driver-r3.6.0 mongo-cxx-driver-r3.6.0.tar.gz +sudo rm -rf mongo-cxx-driver-r3.11.0 mongo-cxx-driver-r3.11.0.tar.gz ``` ### MongoDB (PEA, optional) diff --git a/scripts/installation/ubuntu20.sh b/scripts/installation/ubuntu20.sh index b423d9f89..ad7b67a66 100755 --- a/scripts/installation/ubuntu20.sh +++ b/scripts/installation/ubuntu20.sh @@ -26,7 +26,7 @@ if [[ "$ubuntu_version" != "20.04" && "$ubuntu_version" != "18.04" ]]; then fi # MongoDB library version numbers -mongo_cxx_driver_version="r3.6.7" +mongo_cxx_driver_version="r3.11.0" echo "Updating package repositories..." $sudo_cmd apt update -y @@ -79,25 +79,17 @@ $sudo_cmd make -j2 install cd /tmp -echo "Downloading, extracting, building, and installing Mongo drivers..." -cd /tmp -wget https://github.com/mongodb/mongo-c-driver/releases/download/1.17.1/mongo-c-driver-1.17.1.tar.gz -tar -xf mongo-c-driver-1.17.1.tar.gz -cd mongo-c-driver-1.17.1/ -mkdir cmake-build -cd cmake-build/ -cmake -DENABLE_AUTOMATIC_INIT_AND_CLEANUP=OFF -DENABLE_EXAMPLES=OFF ../ -cmake --build . -- -j 2 -$sudo_cmd cmake --build . --target install -- -j 2 -cd /tmp -curl -OL https://github.com/mongodb/mongo-cxx-driver/releases/download/r3.6.0/mongo-cxx-driver-r3.6.0.tar.gz -tar -xf mongo-cxx-driver-r3.6.0.tar.gz -cd mongo-cxx-driver-r3.6.0/build -cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DENABLE_EXAMPLES=OFF ../ -sudo cmake --build . --target EP_mnmlstc_core -- -j 2 -$sudo_cmd cmake --build . -- -j 2 -$sudo_cmd cmake --build . --target install -cd /tmp +echo "Downloading and extracting mongo-cxx-driver version $mongo_cxx_driver_version..." +wget --no-check-certificate https://github.com/mongodb/mongo-cxx-driver/releases/download/$mongo_cxx_driver_version/mongo-cxx-driver-$mongo_cxx_driver_version.tar.gz +tar -xzf mongo-cxx-driver-$mongo_cxx_driver_version.tar.gz +rm mongo-cxx-driver-$mongo_cxx_driver_version.tar.gz + +echo "Building and installing mongo-cxx-driver..." +mkdir -p mongo-cxx-driver-$mongo_cxx_driver_version/build +cd mongo-cxx-driver-$mongo_cxx_driver_version/build +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local +$sudo_cmd make -j 1 +$sudo_cmd make install echo "Downloading, extracting, building, and installing MongoDB ..." cd /tmp diff --git a/scripts/installation/ubuntu22.sh b/scripts/installation/ubuntu22.sh index dd4d176c5..302170099 100755 --- a/scripts/installation/ubuntu22.sh +++ b/scripts/installation/ubuntu22.sh @@ -26,7 +26,7 @@ if [[ "$ubuntu_version" != "22.04" ]]; then fi # MongoDB library version numbers -mongo_cxx_driver_version="r3.6.7" +mongo_cxx_driver_version="r3.11.0" echo "Updating package repositories..." $sudo_cmd apt update -y diff --git a/scripts/installation/ubuntu24.sh b/scripts/installation/ubuntu24.sh new file mode 100755 index 000000000..c5753669e --- /dev/null +++ b/scripts/installation/ubuntu24.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +set -e # Exit immediately if any command fails + +# Check if sudo is available +sudo_cmd="sudo" +if ! command -v sudo >/dev/null 2>&1; then + sudo_cmd="" +fi + +# Check if the system is Ubuntu +if [[ ! -f /etc/os-release ]] || ! grep -iq "ubuntu" /etc/os-release; then + echo "This script is designed for Ubuntu. Please run it on an Ubuntu system." + exit 1 +fi + +# Check if the system is Ubuntu 22.04 +ubuntu_version=$(grep -oP '(?<=^VERSION_ID=")\d{2}\.\d{2}(?="$)' /etc/os-release) +if [[ "$ubuntu_version" != "24.04" ]]; then + echo "This script is designed for Ubuntu 24.04. Do you want to continue? (y/n)" + read -r response + if [[ ! $response =~ ^[Yy]$ ]]; then + echo "Script execution aborted." + exit 0 + fi +fi + +# MongoDB library version numbers +mongo_cxx_driver_version="r3.11.0" + +echo "Updating package repositories..." +$sudo_cmd apt update -y + +echo "Installing dependencies..." +$sudo_cmd apt-get install --no-install-recommends --yes \ + libgomp1 \ + gcc \ + g++ \ + gdb \ + gfortran \ + openssl \ + curl \ + net-tools \ + wget \ + openssh-server \ + apt-transport-https \ + ca-certificates \ + libopenblas0 \ + libnetcdf-c++4-1 \ + gzip \ + gnupg2 \ + git \ + cmake \ + make \ + libssl-dev \ + libboost-all-dev \ + libeigen3-dev \ + libyaml-cpp-dev \ + libnetcdf-dev \ + libnetcdf-c++4-dev \ + libzstd-dev \ + libssl-dev \ + libncurses5-dev \ + libopenblas-dev \ + python3-pip + + +echo "Creating build directory..." +mkdir -p /tmp/build +cd /tmp/build + +echo "Downloading and extracting mongo-cxx-driver version $mongo_cxx_driver_version..." +wget --no-check-certificate https://github.com/mongodb/mongo-cxx-driver/releases/download/$mongo_cxx_driver_version/mongo-cxx-driver-$mongo_cxx_driver_version.tar.gz +tar -xzf mongo-cxx-driver-$mongo_cxx_driver_version.tar.gz +rm mongo-cxx-driver-$mongo_cxx_driver_version.tar.gz + +echo "Building and installing mongo-cxx-driver..." +mkdir -p mongo-cxx-driver-$mongo_cxx_driver_version/build +cd mongo-cxx-driver-$mongo_cxx_driver_version/build +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local +$sudo_cmd make -j 1 +$sudo_cmd make install + +echo "Installation of Mongodb" +curl -fsSL https://pgp.mongodb.com/server-7.0.asc | $sudo_cmd gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor +echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | $sudo_cmd tee /etc/apt/sources.list.d/mongodb-org-7.0.list +$sudo_cmd apt update +$sudo_cmd apt-get install -y mongodb-org + +$sudo_cmd echo "/usr/local/lib" > /etc/ld.so.conf.d/usr-local-lib.conf +$sudo_cmd chown root:root /etc/ld.so.conf.d/usr-local-lib.conf +$sudo_cmd chmod 644 /etc/ld.so.conf.d/usr-local-lib.conf +$sudo_cmd ldconfig + +echo "Installation completed" diff --git a/scripts/plot_pos.py b/scripts/plot_pos.py index 4ae77b862..bd28f1b47 100644 --- a/scripts/plot_pos.py +++ b/scripts/plot_pos.py @@ -1,3 +1,5 @@ +from pathlib import Path +import os import pandas as pd from datetime import datetime import plotly.graph_objects as go @@ -6,6 +8,16 @@ import argparse def parse_pos_format(file_path): + """ + Parse a .POS file into a pandas DataFrame. + + Arguments: + file_path (str): Path to a .POS file with recordings following the expected format. + + Returns: + pandas.DataFrame: Table with columns such as 'Time', 'Latitude', 'Longitude', + 'Elevation', 'dN', 'dE', 'dU', 'sN', 'sE', 'sU', 'sElevation', 'Rne', 'Rnu', 'Reu', 'soln'. + """ data = [] try: with open(file_path, 'r') as file: @@ -36,11 +48,23 @@ def parse_pos_format(file_path): } data.append(record) except Exception as e: - print(f"Error parsing file {file_path}: {e}") + print(f"Error parsing file {file_path}: {e}") return pd.DataFrame(data) # Function to parse the datetime with optional timezone def parse_datetime(datetime_str): + """ + Parse a datetime string with or without timezone into a naive datetime. + + Arguments: + datetime_str (str): Datetime string, e.g., 'YYYY-MM-DDTHH:MM:SS' or 'YYYY-MM-DDTHH:MM:SS±HHMM'. + + Returns: + datetime: Timezone-naive datetime object. If timezone was present, it is stripped. + + Raises: + ValueError: If datetime string doesn't match expected formats. + """ # Attempt to parse datetime with and without timezone for fmt in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S"): try: @@ -52,9 +76,18 @@ def parse_datetime(datetime_str): except ValueError as e: print(f"ValueError: {e}") continue - raise ValueError(f"datetime {datetime_str} does not match expected formats.") + raise ValueError(f"datetime {datetime_str} does not match expected formats.") def remove_weighted_mean(data): + """ + Remove weighted mean from each component series in-place and return the DataFrame. + + Arguments: + data (pandas.DataFrame): Data with components ('dN','dE','dU','Elevation') and their sigmas ('sN','sE','sU','sElevation'). + + Returns: + pandas.DataFrame: The same DataFrame with each component demeaned by its weighted mean. + """ sigma_keys = {'dN': 'sN', 'dE': 'sE', 'dU': 'sU', 'Elevation': 'sElevation'} # Assume sElevation exists for component in ['dN', 'dE', 'dU', 'Elevation']: sigma_key = sigma_keys[component] @@ -63,12 +96,23 @@ def remove_weighted_mean(data): data[component] -= weighted_mean # Demean the series return data -def apply_smoothing(data): +def apply_smoothing(data, horz_smoothing=None, vert_smoothing=None): + """ + Apply LOWESS smoothing to horizontal and / or vertical components. + + Arguments: + data (pandas.DataFrame): Input data with time column 'Time' and components. + horz_smoothing (float or None): Fraction for LOWESS on dN/dE (0..1), or None to skip. + vert_smoothing (float or None): Fraction for LOWESS on dU/Elevation (0..1), or None to skip. + + Returns: + pandas.DataFrame: DataFrame with additional 'Smoothed_*' columns when smoothing is applied. + """ for component in ['dN', 'dE', 'dU', 'Elevation']: - if args.horz_smoothing and (component == 'dN' or component == 'dE'): - data[f'Smoothed_{component}'] = lowess(data[component], data['Time'], frac=args.horz_smoothing, return_sorted=False) - if args.vert_smoothing and (component == 'dU' or component == 'Elevation'): - data[f'Smoothed_{component}'] = lowess(data[component], data['Time'], frac=args.vert_smoothing, return_sorted=False) + if horz_smoothing and (component == 'dN' or component == 'dE'): + data[f'Smoothed_{component}'] = lowess(data[component], data['Time'], frac=horz_smoothing, return_sorted=False) + if vert_smoothing and (component == 'dU' or component == 'Elevation'): + data[f'Smoothed_{component}'] = lowess(data[component], data['Time'], frac=vert_smoothing, return_sorted=False) return data def compute_statistics(data): @@ -95,395 +139,521 @@ def compute_statistics(data): return data, stats +def create_plots(all_data, input_files, component_stats, args, show_plots=True): + """ + Create interactive HTML plots for POS analysis. + + Arguments: + all_data (pandas.DataFrame): Measurement table with columns such as 'Time', 'dN', 'dE', 'dU' or 'Elevation', and their sigmas. + input_files (list): One or more .POS filepaths + component_stats (dict): Statistics returned by compute_statistics(). + args (object): Arguments used to call plot_pos like colour_sigma, max_sigma, elevation, map, heatmap, save_prefix. + show_plots (bool): If True, display plots in browser (CLI mode), if False, only save to files (program mode). + + Returns: + None: Writes HTML files when args.save_prefix is provided. + """ + input_root = Path(input_files[0]).stem + + # Start plotting + ## Fig1 + # Determine max sigma and color scale settings for Fig1 + title_text = f"Time Series Analysis: {', '.join(input_files)}
    " + color_scale = 'Jet' if args.colour_sigma else None # Only set color scale if --colour_sigma is active + max_sigma_data = np.max([all_data['sN'].max(), all_data['sE'].max(), all_data['sU'].max()]) + min_sigma_data = np.min([all_data['sN'].min(), all_data['sE'].min(), all_data['sU'].min()]) + cmax = min(args.max_sigma, max_sigma_data) if args.max_sigma is not None else max_sigma_data + # cmin = min_sigma_data + cmin = 0.0 + + # Setting up the plot + fig1 = go.Figure() + components = ['dN', 'dE', 'Elevation'] if args.elevation else ['dN', 'dE', 'dU'] + component_colors = { + 'dN': 'red', + 'dE': 'green', + 'dU': 'blue', + 'Elevation': 'orange' + } + + for component in components: + # Correctly map the component to its sigma key + if component == 'Elevation': + sigma_key = 'sU' # Assuming sigma for Elevation is stored in 'sU' + else: + sigma_key = f's{component[-1].upper()}' + + print('Plotting: ', sigma_key) # To check if the correct sigma key is being used + + # Add the primary and smoothed series data + if args.colour_sigma: + # When using --colour_sigma, use the sigma value for coloring + fig1.add_trace(go.Scatter( + x=all_data['Time'], y=all_data[component], + mode='lines+markers', + marker=dict(size=5, color=all_data[sigma_key], coloraxis="coloraxis"), + name=component, + hoverinfo='text+x+y', + text=f'{component} Sigma: ' + all_data[sigma_key].astype(str) + )) -# Setup and parse arguments -parser = argparse.ArgumentParser(description="Plot positional data with optional smoothing and color coding.") -parser.add_argument('--start-datetime', type=str, - help="Start datetime in the format YYYY-MM-DDTHH:MM:SS, optional timezone") -parser.add_argument('--end-datetime', type=str, - help="End datetime in the format YYYY-MM-DDTHH:MM:SS, optional timezone") -parser.add_argument('--horz_smoothing', type=float, default=None, - help='Fraction of the data used for horizontal (East and North) LOWESS smoothing (optional).') -parser.add_argument('--vert_smoothing', type=float, default=None, - help='Fraction of the data used for vertical (Up) LOWESS smoothing (optional).') -parser.add_argument('--colour_sigma', action='store_true', - help='Colourize the timeseries using the standard deviation (sigma) values (optional).') -parser.add_argument('--max_sigma', type=float, default=None, - help='Set a maximum sigma threshold for the sigma colour scale (optional).') -parser.add_argument('--elevation', action='store_true', - help='Plot Elevation values inplace of dU wrt the reference coord (optional).') -parser.add_argument('--demean', action='store_true', - help='Remove the mean values from all time series before plotting (optional).') -parser.add_argument('--map', action='store_true', - help='Create a geographic map view from the Longitude & Latitude estiamtes (optional).') -parser.add_argument('--heatmap', action='store_true', - help='Create a 2D heatmap view of E & N coodrinates wrt the reference position (optional).') -parser.add_argument('--sigma_threshold', nargs=3, type=float, - help="Thresholds for sE, sN, and sU to filter data.") -parser.add_argument('--down_sample', type=int, - help="Interval in seconds for down-sampling data.") -parser.add_argument('--save', action='store_true', - help='Save requested plots as .html format files (optional).') -parser.add_argument('files', nargs='+') -args = parser.parse_args() - -# Parse the start and end datetime if provided -start_datetime = parse_datetime(args.start_datetime) if args.start_datetime else None -end_datetime = parse_datetime(args.end_datetime) if args.end_datetime else None - -# Load and process data -all_data = pd.DataFrame() -for file_path in args.files: - file_data = parse_pos_format(file_path) - all_data = pd.concat([all_data, file_data], ignore_index=True) - -all_data['Time'] = pd.to_datetime(all_data['Time'], format="%Y-%m-%dT%H:%M:%S.%f") - -# Apply time windowing -if start_datetime: - all_data = all_data[all_data['Time'] >= start_datetime] -if end_datetime: - all_data = all_data[all_data['Time'] <= end_datetime] - -# Apply threshold filtering if sigma_threshold is provided -if args.sigma_threshold: - se_threshold, sn_threshold, su_threshold = args.sigma_threshold - mask = (all_data['sE'] <= se_threshold) & (all_data['sN'] <= sn_threshold) & (all_data['sU'] <= su_threshold) & (all_data['sElevation'] <= su_threshold) - all_data = all_data[mask] - -# Down-sample the data if requested -if args.down_sample: - # Ensure the 'Time' column is datetime for proper indexing - all_data['Time'] = pd.to_datetime(all_data['Time']) - all_data.set_index('Time', inplace=True) - # Resample and take the first available data point in each bin - all_data = all_data.resample(f'{args.down_sample}s').first().dropna().reset_index() - -# Demean, smooth, and compute statistics -if args.demean: - all_data = remove_weighted_mean(all_data) -all_data = apply_smoothing(all_data) -all_data, component_stats = compute_statistics(all_data) - -# Start plotting -# Determine max sigma and color scale settings for Fig1 -title_text = f"Time Series Analysis: {', '.join(args.files)}
    " -color_scale = 'Jet' if args.colour_sigma else None # Only set color scale if --colour_sigma is active -max_sigma_data = np.max([all_data['sN'].max(), all_data['sE'].max(), all_data['sU'].max()]) -min_sigma_data = np.min([all_data['sN'].min(), all_data['sE'].min(), all_data['sU'].min()]) -cmax = min(args.max_sigma, max_sigma_data) if args.max_sigma is not None else max_sigma_data -#cmin = min_sigma_data -cmin = 0.0 - -# Setting up the plot -fig1 = go.Figure() -components = ['dN', 'dE', 'Elevation'] if args.elevation else ['dN', 'dE', 'dU'] - -for component in components: - # Correctly map the component to its sigma key - if component == 'Elevation': - sigma_key = 'sU' # Assuming sigma for Elevation is stored in 'sU' - else: - sigma_key = f's{component[-1].upper()}' - - print('Plotting: ', sigma_key) # To check if the correct sigma key is being used - - # Add the primary and smoothed series data - if args.colour_sigma: - # When using --colour_sigma, use the sigma value for coloring - fig1.add_trace(go.Scatter( - x=all_data['Time'], y=all_data[component], - mode='lines+markers', - marker=dict(size=5, color=all_data[sigma_key], coloraxis="coloraxis"), - name=component, - hoverinfo='text+x+y', - text=f'{component} Sigma: ' + all_data[sigma_key].astype(str) - )) - - else: - # When not using --colour_sigma, add error bars using the sigma values + else: + # When not using --colour_sigma, add error bars using the sigma values + fig1.add_trace(go.Scatter( + x=all_data['Time'], y=all_data[component], + mode='markers', + name=component, + error_y=dict( + type='data', # Represent error in data coordinates + array=all_data[sigma_key], # Positive error + arrayminus=all_data[sigma_key], # Negative error + visible=True, # Make error bars visible + color='gray' # Color of error bars + ), + marker=dict(size=5, color=component_colors[component]), + line=dict(color=component_colors[component]), + hoverinfo='text+x+y', + text=f'{component} Sigma: ' + all_data[sigma_key].astype(str) + )) + + if f'Smoothed_{component}' in all_data: + fig1.add_trace(go.Scatter( + x=all_data['Time'], y=all_data[f'Smoothed_{component}'], + mode='lines', + name=f'Smoothed {component}', + line=dict(color='rgba(0,0,255,0.5)') + )) + + # Add statistical lines and shaded areas for standard deviation fig1.add_trace(go.Scatter( - x=all_data['Time'], y=all_data[component], - mode='markers', - name=component, - error_y=dict( - type='data', # Represent error in data coordinates - array=all_data[sigma_key], # Positive error - arrayminus=all_data[sigma_key], # Negative error - visible=True, # Make error bars visible - color='gray' # Color of error bars - ), - marker=dict(size=5, color='blue'), - line=dict(color='blue'), - hoverinfo='text+x+y', - text=f'{component} Sigma: ' + all_data[sigma_key].astype(str) + x=all_data['Time'], y=all_data[f'{component}_weighted_mean'], + mode='lines', + name=f'{component} Weighted Mean', + line=dict(color=component_colors[component]) )) - if f'Smoothed_{component}' in all_data: fig1.add_trace(go.Scatter( - x=all_data['Time'], y=all_data[f'Smoothed_{component}'], - mode='lines', - name=f'Smoothed {component}', - line=dict(color='rgba(0,0,255,0.5)') + x=all_data['Time'].tolist() + all_data['Time'].tolist()[::-1], + y=all_data[f'{component}_std_dev_upper'].tolist() + all_data[f'{component}_std_dev_lower'].tolist()[::-1], + fill='toself', + fillcolor='rgba(68, 68, 255, 0.2)', + line=dict(color='rgba(255,255,255,0)'), + name=f'{component} CI: 2 Sigma (95%)' )) - # Add statistical lines and shaded areas for standard deviation - fig1.add_trace(go.Scatter( - x=all_data['Time'], y=all_data[f'{component}_weighted_mean'], - mode='lines', - name=f'{component} Weighted Mean', - line=dict(color='red') - )) + stats = component_stats[component] + title_text += f"{component}: Weighted Mean = {stats['weighted_mean']:.3f}, Std Dev = {stats['std_dev']:.3f}, RMS = {stats['rms']:.3f}
    " + + fig1.update_layout( + title=title_text, + xaxis_title='Time', + yaxis_title='Measurement Value', + xaxis=dict( + rangeslider=dict(visible=True), + fixedrange=False, + type='date' + ), + yaxis=dict( + fixedrange=False + ), + coloraxis=dict( + colorscale=color_scale, + cmin=cmin, + cmax=cmax, + colorbar=dict( + title='Sigma Value', + x=0.5, # Center the color bar on the x-axis + y=-0.5, # Position the color bar below the x-axis + xanchor='center', # Anchor the color bar at its center for x positioning + yanchor='bottom', # Anchor the color bar from its bottom edge for y positioning + len=0.5, # Length of the color bar (75% of the width of the plot area) + thickness=10, # Thickness of the color bar + orientation='h' # Horizontal orientation + ), + ) if args.colour_sigma else {}, + showlegend=True, + margin=dict(t=150) + ) - fig1.add_trace(go.Scatter( - x=all_data['Time'].tolist() + all_data['Time'].tolist()[::-1], - y=all_data[f'{component}_std_dev_upper'].tolist() + all_data[f'{component}_std_dev_lower'].tolist()[::-1], - fill='toself', - fillcolor='rgba(68, 68, 255, 0.2)', - line=dict(color='rgba(255,255,255,0)'), - name=f'{component} CI: 2 Sigma (95%)' - )) + if show_plots: + fig1.show() + + if args.save_prefix is not None: + output_path = os.path.join(os.path.dirname(args.save_prefix), f"{input_root}_fig1.html") + os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) + fig1.write_html(output_path) + + ## Fig2 + # Build the title with file names and statistics for Fig2 + title_text = f"dN vs dE Analysis: {', '.join(input_files)}
    " + for component in ['dN', 'dE']: + stats = component_stats[component] + title_text += f"{component}: Weighted Mean = {stats['weighted_mean']:.3f}, Std Dev = {stats['std_dev']:.3f}, RMS = {stats['rms']:.3f}
    " + + # Conditional sigma calculations and setup + composite_uncertainty = np.sqrt(all_data['sN'] ** 2 + all_data['sE'] ** 2) + all_data['composite_uncertainty'] = composite_uncertainty + max_sigma_data = composite_uncertainty.max() + if args.colour_sigma: + cmax = min(args.max_sigma, max_sigma_data) if args.max_sigma is not None else max_sigma_data + cmin = composite_uncertainty.min() + # cmin = 0.0 + color_scale = 'Jet' # Define the color scale here within the condition + else: + cmin = None # No cmax needed for static colors + cmax = None # No cmax needed for static colors + color_scale = None # No color scale needed for static colors - stats = component_stats[component] - title_text += f"{component}: Weighted Mean = {stats['weighted_mean']:.3f}, Std Dev = {stats['std_dev']:.3f}, RMS = {stats['rms']:.3f}
    " - -fig1.update_layout( - title=title_text, - xaxis_title='Time', - yaxis_title='Measurement Value', - xaxis=dict( - rangeslider=dict(visible=True), - fixedrange=False, - type='date' - ), - yaxis=dict( - fixedrange=False - ), - coloraxis=dict( - colorscale=color_scale, - cmin=cmin, - cmax=cmax, - colorbar=dict( - title='Sigma Value', - x=0.5, # Center the color bar on the x-axis - y=-0.5, # Position the color bar below the x-axis - xanchor='center', # Anchor the color bar at its center for x positioning - yanchor='bottom', # Anchor the color bar from its bottom edge for y positioning - len=0.5, # Length of the color bar (75% of the width of the plot area) - thickness=10, # Thickness of the color bar - orientation='h' # Horizontal orientation - ), - ) if args.colour_sigma else {}, - showlegend=True, - margin=dict(t=150) -) -fig1.show() -if args.save: - fig1.write_html("fig1.html") - -# Build the title with file names and statistics for Fig2 -title_text = f"dN vs dE Analysis: {', '.join(args.files)}
    " -for component in ['dN', 'dE']: - stats = component_stats[component] - title_text += f"{component}: Weighted Mean = {stats['weighted_mean']:.3f}, Std Dev = {stats['std_dev']:.3f}, RMS = {stats['rms']:.3f}
    " - -# Conditional sigma calculations and setup -composite_uncertainty = np.sqrt(all_data['sN']**2 + all_data['sE']**2) -all_data['composite_uncertainty'] = composite_uncertainty -max_sigma_data = composite_uncertainty.max() -if args.colour_sigma: - cmax = min(args.max_sigma, max_sigma_data) if args.max_sigma is not None else max_sigma_data - cmin = composite_uncertainty.min() - #cmin = 0.0 - color_scale = 'Jet' # Define the color scale here within the condition -else: - cmin = None # No cmax needed for static colors - cmax = None # No cmax needed for static colors - color_scale = None # No color scale needed for static colors - -# Plot configuration -fig2 = go.Figure() -fig2.add_trace(go.Scatter( - x=all_data['dE'], y=all_data['dN'], - mode='markers', - marker=dict( - size=5, - color=all_data['composite_uncertainty'] if args.colour_sigma else 'blue', # Conditional coloring - coloraxis="coloraxis" if args.colour_sigma else None # Use color axis only if color sigma is set - ), - name='dE vs dN', - text=[f"{time} Sigma dNdE: {unc:.4f}" for time, unc in zip(all_data['Time'], all_data['composite_uncertainty'])], - hoverinfo='text+x+y' -)) - -# Add smoothed data if available -if 'Smoothed_dN' in all_data.columns and 'Smoothed_dE' in all_data.columns: + # Plot configuration + fig2 = go.Figure() fig2.add_trace(go.Scatter( - x=all_data['Smoothed_dE'], y=all_data['Smoothed_dN'], + x=all_data['dE'], y=all_data['dN'], mode='markers', marker=dict( size=5, - color='red' + color=all_data['composite_uncertainty'] if args.colour_sigma else 'blue', # Conditional coloring + coloraxis="coloraxis" if args.colour_sigma else None # Use color axis only if color sigma is set ), - name='Smoothed' + name='dE vs dN', + text=[f"{time} Sigma dNdE: {unc:.4f}" for time, unc in + zip(all_data['Time'], all_data['composite_uncertainty'])], + hoverinfo='text+x+y' )) -# Layout update with conditional color axis settings -fig2.update_layout( - title=title_text, - xaxis_title='dE (meters)', - yaxis_title='dN (meters)', - xaxis=dict(scaleanchor="y", scaleratio=1), - yaxis=dict(scaleanchor="x", scaleratio=1), - coloraxis=dict( - colorscale=color_scale, - cmin=cmin, - cmax=cmax, - colorbar=dict( - title='Sigma Value', - x=0.5, y=-0.15, # Adjusted for visibility - xanchor='center', yanchor='bottom', - len=0.75, thickness=20, orientation='h' - ) - ) if args.colour_sigma else None, # Apply color axis settings only if needed - showlegend=True -) -fig2.show() -if args.save: - fig2.write_html("fig2.html") - -if args.map: - # Plotly plotting using mapbox open-street-map - - # Adjust the zoom level dynamically based on the spread of the latitude and longitude - def adjust_zoom(latitudes, longitudes): - lat_range = np.ptp(latitudes) # Peak to peak (range) of latitudes - lon_range = np.ptp(longitudes) # Peak to peak (range) of longitudes - if max(lat_range, lon_range) < 0.02: - return 13 # City level zoom - elif max(lat_range, lon_range) < 0.1: - return 10 # Regional level zoom - elif max(lat_range, lon_range) < 1: - return 7 # Country level zoom - else: - return 5 # Continental level zoom - - zoom_level = adjust_zoom(all_data['Latitude'], all_data['Longitude']) - - fig3 = go.Figure(go.Scattermapbox( - lat=all_data['Latitude'], - lon=all_data['Longitude'], - mode='markers+lines', - marker=dict(size=5, color='blue') - )) - - fig3.update_layout( - mapbox=dict( - style="open-street-map", - center=go.layout.mapbox.Center( - lat=all_data['Latitude'].mean(), - lon=all_data['Longitude'].mean() + # Add smoothed data if available + if 'Smoothed_dN' in all_data.columns and 'Smoothed_dE' in all_data.columns: + fig2.add_trace(go.Scatter( + x=all_data['Smoothed_dE'], y=all_data['Smoothed_dN'], + mode='markers', + marker=dict( + size=5, + color='red' ), - zoom=zoom_level - ), - title='Geographic Plot of Latitude and Longitude', - showlegend=False - ) - - fig3.show() - if args.save: - fig2.write_html("fig3.html") - -if args.heatmap: - # Plotly plotting dN vs dE heatmap - fig4 = go.Figure() - fig4.add_trace(go.Histogram2dContour( - x = all_data['dE'], - y = all_data['dN'], - colorscale = 'Jet', - reversescale = False, - xaxis = 'x', - yaxis = 'y' - )) - fig4.add_trace(go.Scatter( - x = all_data['dE'], - y = all_data['dN'], - xaxis = 'x', - yaxis = 'y', - mode = 'markers', - marker = dict( - color = 'rgba(0,0,0,0.3)', - size = 3 - ) - )) - fig4.add_trace(go.Scatter( - x = all_data['dE_weighted_mean'], - y = all_data['dN_weighted_mean'], - xaxis = 'x', - yaxis = 'y', - mode = 'markers', - marker = dict( - color="white", - size = 15, - line_color='black', - symbol='x-dot', - line_width=2 - ), - hoverinfo='text+x+y', - text='Weighted Mean (dE, dN)' - )) - fig4.add_trace(go.Histogram( - y = all_data['dN'], - xaxis = 'x2', - marker = dict( - color = 'rgba(0,0,0,1)' - ) - )) - fig4.add_trace(go.Histogram( - x = all_data['dE'], - yaxis = 'y2', - marker = dict( - color = 'rgba(0,0,0,1)' - ) - )) + name='Smoothed' + )) - fig4.update_layout( - autosize = False, - xaxis = dict( - zeroline = False, - domain = [0,0.85], - showgrid = False - ), - yaxis = dict( - zeroline = False, - domain = [0,0.85], - showgrid = False - ), - xaxis2 = dict( - zeroline = False, - domain = [0.85,1], - showgrid = False - ), - yaxis2 = dict( - zeroline = False, - domain = [0.85,1], - showgrid = False - ), - title=title_text, - xaxis_title='dE (meters)', - yaxis_title='dN (meters)', - height = 800, - width = 800, - bargap = 0, - hovermode = 'closest', - showlegend = False + # Layout update with conditional color axis settings + fig2.update_layout( + title=title_text, + xaxis_title='dE (meters)', + yaxis_title='dN (meters)', + xaxis=dict(scaleanchor="y", scaleratio=1), + yaxis=dict(scaleanchor="x", scaleratio=1), + coloraxis=dict( + colorscale=color_scale, + cmin=cmin, + cmax=cmax, + colorbar=dict( + title='Sigma Value', + x=0.5, y=-0.15, # Adjusted for visibility + xanchor='center', yanchor='bottom', + len=0.75, thickness=20, orientation='h' + ) + ) if args.colour_sigma else None, # Apply color axis settings only if needed + showlegend=True ) - fig4.show() - if args.save: - fig4.write_html("fig4.html") - + if show_plots: + fig2.show() + + if args.save_prefix is not None: + output_path = os.path.join(os.path.dirname(args.save_prefix), f"{input_root}_fig2.html") + os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) + fig2.write_html(output_path) + + ## Fig3 + if getattr(args, 'map', False) or getattr(args, 'map_view', False): + # Plotly plotting using mapbox open-street-map + + # Adjust the zoom level dynamically based on the spread of the latitude and longitude + def adjust_zoom(latitudes, longitudes): + lat_range = np.ptp(latitudes) # Peak to peak (range) of latitudes + lon_range = np.ptp(longitudes) # Peak to peak (range) of longitudes + if max(lat_range, lon_range) < 0.02: + return 13 # City level zoom + elif max(lat_range, lon_range) < 0.1: + return 10 # Regional level zoom + elif max(lat_range, lon_range) < 1: + return 7 # Country level zoom + else: + return 5 # Continental level zoom + + zoom_level = adjust_zoom(all_data['Latitude'], all_data['Longitude']) + + fig3 = go.Figure(go.Scattermapbox( + lat=all_data['Latitude'], + lon=all_data['Longitude'], + mode='markers+lines', + marker=dict(size=5, color='blue') + )) + fig3.update_layout( + mapbox=dict( + style="open-street-map", + center=go.layout.mapbox.Center( + lat=all_data['Latitude'].mean(), + lon=all_data['Longitude'].mean() + ), + zoom=zoom_level + ), + title='Geographic Plot of Latitude and Longitude', + showlegend=False + ) + if show_plots: + fig3.show() + + if args.save_prefix is not None: + output_path = os.path.join(os.path.dirname(args.save_prefix), f"{input_root}_fig3.html") + os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) + fig3.write_html(output_path) + + ## Fig4 + if args.heatmap: + # Plotly plotting dN vs dE heatmap + fig4 = go.Figure() + fig4.add_trace(go.Histogram2dContour( + x=all_data['dE'], + y=all_data['dN'], + colorscale='Jet', + reversescale=False, + xaxis='x', + yaxis='y' + )) + fig4.add_trace(go.Scatter( + x=all_data['dE'], + y=all_data['dN'], + xaxis='x', + yaxis='y', + mode='markers', + marker=dict( + color='rgba(0,0,0,0.3)', + size=3 + ) + )) + fig4.add_trace(go.Scatter( + x=all_data['dE_weighted_mean'], + y=all_data['dN_weighted_mean'], + xaxis='x', + yaxis='y', + mode='markers', + marker=dict( + color="white", + size=15, + line_color='black', + symbol='x-dot', + line_width=2 + ), + hoverinfo='text+x+y', + text='Weighted Mean (dE, dN)' + )) + fig4.add_trace(go.Histogram( + y=all_data['dN'], + xaxis='x2', + marker=dict( + color='rgba(0,0,0,1)' + ) + )) + fig4.add_trace(go.Histogram( + x=all_data['dE'], + yaxis='y2', + marker=dict( + color='rgba(0,0,0,1)' + ) + )) + fig4.update_layout( + autosize=False, + xaxis=dict( + zeroline=False, + domain=[0, 0.85], + showgrid=False + ), + yaxis=dict( + zeroline=False, + domain=[0, 0.85], + showgrid=False + ), + xaxis2=dict( + zeroline=False, + domain=[0.85, 1], + showgrid=False + ), + yaxis2=dict( + zeroline=False, + domain=[0.85, 1], + showgrid=False + ), + title=title_text, + xaxis_title='dE (meters)', + yaxis_title='dN (meters)', + height=800, + width=800, + bargap=0, + hovermode='closest', + showlegend=False + ) + if show_plots: + fig4.show() + + if args.save_prefix is not None: + output_path = os.path.join(os.path.dirname(args.save_prefix), f"{input_root}_fig4.html") + os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) + fig4.write_html(output_path) + +def _process_and_plot(input_files, args, show_plots=False): + """ + Internal helper to process POS data and generates plots. Shared function between the CLI and program UI call modes. + + Arguments: + input_files (list): One or more input .POS file paths. + args (object): Arguments for plot_pos call. + show_plots (bool): If True, open plots in browser, or if False, only save files for UI to access. + + Returns: + list: Paths to generated HTML files when args.save_prefix is provided, empty list otherwise. + """ + # Parse the start and end datetime if provided + start_datetime = parse_datetime(args.start_datetime) if args.start_datetime else None + end_datetime = parse_datetime(args.end_datetime) if args.end_datetime else None + + # Load and process data + all_data = pd.DataFrame() + for file_path in input_files: + file_data = parse_pos_format(file_path) + all_data = pd.concat([all_data, file_data], ignore_index=True) + + all_data['Time'] = pd.to_datetime(all_data['Time'], format="%Y-%m-%dT%H:%M:%S.%f") + + # Apply time windowing + if start_datetime: + all_data = all_data[all_data['Time'] >= start_datetime] + if end_datetime: + all_data = all_data[all_data['Time'] <= end_datetime] + + # Apply threshold filtering if sigma_threshold is provided + if args.sigma_threshold: + se_threshold, sn_threshold, su_threshold = args.sigma_threshold + mask = (all_data['sE'] <= se_threshold) & (all_data['sN'] <= sn_threshold) & ( + all_data['sU'] <= su_threshold) & (all_data['sElevation'] <= su_threshold) + all_data = all_data[mask] + + # Down-sample the data if requested + if args.down_sample: + # Ensure the 'Time' column is datetime for proper indexing + all_data['Time'] = pd.to_datetime(all_data['Time']) + all_data.set_index('Time', inplace=True) + # Resample and take the first available data point in each bin + all_data = all_data.resample(f'{args.down_sample}s').first().dropna().reset_index() + + # Demean, smooth, and compute statistics + if args.demean: + all_data = remove_weighted_mean(all_data) + all_data = apply_smoothing(all_data, args.horz_smoothing, args.vert_smoothing) + all_data, component_stats = compute_statistics(all_data) + + # Generate plots + create_plots(all_data, input_files, component_stats, args, show_plots = show_plots) + + # Return list of generated files + if args.save_prefix: + input_root = Path(input_files[0]).stem + generated_files = [] + generated_files.append(os.path.join(os.path.dirname(args.save_prefix), f"{input_root}_fig1.html")) + generated_files.append(os.path.join(os.path.dirname(args.save_prefix), f"{input_root}_fig2.html")) + # Access map and / or heatmap + if getattr(args, 'map', False) or getattr(args, 'map_view', False): + generated_files.append(os.path.join(os.path.dirname(args.save_prefix), f"{input_root}_fig3.html")) + if args.heatmap: + generated_files.append(os.path.join(os.path.dirname(args.save_prefix), f"{input_root}_fig4.html")) + return generated_files + + return [] + +def plot_pos_files(input_files, start_datetime=None, end_datetime=None, + horz_smoothing=None, vert_smoothing=None, colour_sigma=False, + max_sigma=None, elevation=False, demean=False, map_view=False, + heatmap=False, sigma_threshold=None, down_sample=None, + save_prefix=None): + """ + Generate the interactive figures from one or more POS files (the programmatic call for Ginan-UI to use). + + This function provides a programmatic way of generating visualisations + + Arguments: + input_files (list): One or more input .POS file paths. + start_datetime (str, optional): Start time (e.g., 'YYYY-MM-DDTHH:MM:SS'). + end_datetime (str, optional): End time (e.g., 'YYYY-MM-DDTHH:MM:SS'). + horz_smoothing (float, optional): LOWESS fraction for horizontal components (dN / dE). + vert_smoothing (float, optional): LOWESS fraction for vertical component (dU or Elevation). + colour_sigma (bool): If True, colour markers by sigma; otherwise show error bars. + max_sigma (float, optional): Upper cap for sigma colour scale when colour_sigma is True. + elevation (bool): If True, use 'Elevation' instead of 'dU' for vertical plotting. + demean (bool): If True, remove weighted mean from each series before plotting. + map_view (bool): If True, generate a geographic map (fig3). + heatmap (bool): If True, generate a 2D dE–dN density view (fig4). + sigma_threshold (tuple, optional): (sE, sN, sU) for filtering rows. + down_sample (int, optional): Resampling interval in seconds. + save_prefix (str, optional): If provided, write HTML files next to this prefix. + + Returns: + list: Paths to generated HTML files when save_prefix is provided; empty list otherwise. + """ + class Args: + def __init__(self): + self.input_files = input_files + self.start_datetime = start_datetime + self.end_datetime = end_datetime + self.horz_smoothing = horz_smoothing + self.vert_smoothing = vert_smoothing + self.colour_sigma = colour_sigma + self.max_sigma = max_sigma + self.elevation = elevation + self.demean = demean + self.map = map_view + self.heatmap = heatmap + self.sigma_threshold = sigma_threshold + self.down_sample = down_sample + self.save_prefix = save_prefix + + args = Args() + + # "show_plots = False" flags to remain in UI (don't open web browser) + return _process_and_plot(input_files, args, show_plots = False) + +# CLI Entry +if __name__ == "__main__": + # Setup and parse arguments + parser = argparse.ArgumentParser(description="Plot positional data with optional smoothing and color coding.") + parser.add_argument('--input-files', nargs='+', required=True, help='One or more input .POS files') + parser.add_argument('--start-datetime', type=str, + help="Start datetime in the format YYYY-MM-DDTHH:MM:SS, optional timezone") + parser.add_argument('--end-datetime', type=str, + help="End datetime in the format YYYY-MM-DDTHH:MM:SS, optional timezone") + parser.add_argument('--horz-smoothing', type=float, default=None, + help='Fraction of the data used for horizontal (East and North) LOWESS smoothing (optional).') + parser.add_argument('--vert-smoothing', type=float, default=None, + help='Fraction of the data used for vertical (Up) LOWESS smoothing (optional).') + parser.add_argument('--colour-sigma', action='store_true', + help='Colourize the timeseries using the standard deviation (sigma) values (optional).') + parser.add_argument('--max-sigma', type=float, default=None, + help='Set a maximum sigma threshold for the sigma colour scale (optional).') + parser.add_argument('--elevation', action='store_true', + help='Plot Elevation values inplace of dU wrt the reference coord (optional).') + parser.add_argument('--demean', action='store_true', + help='Remove the mean values from all time series before plotting (optional).') + parser.add_argument('--map', action='store_true', + help='Create a geographic map view from the Longitude & Latitude estiamtes (optional).') + parser.add_argument('--heatmap', action='store_true', + help='Create a 2D heatmap view of E & N coodrinates wrt the reference position (optional).') + parser.add_argument('--sigma-threshold', nargs=3, type=float, + help="Thresholds for sE, sN, and sU to filter data.") + parser.add_argument('--down-sample', type=int, + help="Interval in seconds for down-sampling data.") + parser.add_argument('--save-prefix', nargs='?', const='plot', default=None, + help='Prefix for saving HTML figures, e.g., ./output/fig') + args = parser.parse_args() + + # "show_plots = True" flags to open the HTML file in web browser + _process_and_plot(args.input_files, args, show_plots = True) diff --git a/scripts/plot_trace_res.py b/scripts/plot_trace_res.py new file mode 100644 index 000000000..c6648c45e --- /dev/null +++ b/scripts/plot_trace_res.py @@ -0,0 +1,2811 @@ +from __future__ import annotations +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +GNSS TRACE residual plotter with optional large-error and ambiguity reset markers. + +Features +--------- +- Parse residual lines beginning with '%' +- Parse 'LARGE STATE ERROR' and 'LARGE MEAS ERROR' lines (if --mark-large-errors) +- Keep only the highest iteration per observation +- Create separate CODE / PHASE HTML plots, optionally split per satellite or receiver +- Overlay LARGE MEAS (black ▲) and LARGE STATE (orange dashed) markers with concise tooltips +- Overlay ambiguity resets from the preprocessor (green), from the filter (blue) +- Create CODE & PHASE weighted and unweighted residual heatmaps of mean/std_dev/rms. +- Create cumulative and total ambiguity reset plots +""" + +import os, glob +import re +import argparse +from pathlib import Path +from typing import Iterable, Optional, List, Dict, Tuple +from datetime import datetime + + +def ensure_parent(p) -> None: + """Create parent directory for a path if it doesn't exist.""" + from pathlib import Path as _P + try: + _P(p).parent.mkdir(parents=True, exist_ok=True) + except Exception: + pass + + +def _sanitize_filename_piece(s: str) -> str: + """Sanitize string for filenames using underscores.""" + return re.sub(r"[^A-Za-z0-9._-]+", "_", str(s)) + + +def build_out_path( + base: str, + variant_suffix: str, + short: str, + *, + split: str | None = None, # "recv" | "sat" | None + key: str | None = None, # station or satellite ID + tag: str | None = None, # e.g., "residual", "h"/"v" for totals + ext: str = "html", +) -> str: + """ + Build consistent output names like: + _[_][_recv|_sat]_[]. + """ + parts = [f"{base}{variant_suffix}", short] + if tag: + parts.append(tag) + if split in ("recv", "sat"): + parts.append(split) + if key: + parts.append(_sanitize_filename_piece(key)) + return "_".join(parts) + f".{ext}" +def slugify(text: str) -> str: + """Return a safe slug for filenames: lowercase, alnum-plus-dashes.""" + import re + t = re.sub(r"[^A-Za-z0-9]+", "-", text).strip("-").lower() + return re.sub(r"-{2,}", "-", t) or "out" + +import logging +logger = logging.getLogger("plot_trace_res") + + +def _setup_logging(level: str = "INFO") -> None: + """Configure root logger for the script.""" + lvl = getattr(logging, level.upper(), logging.INFO) + logging.basicConfig( + level=lvl, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + logger.setLevel(lvl) + +from typing import Any, Dict, Iterable, Iterator, List, Literal, Optional, Sequence, Tuple + +import pandas as pd +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import plotly.io as pio +pio.templates.default = None + +import numpy as np +from collections import defaultdict + +# -------- Parsing -------- + +FLOAT = r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?" + +# AJC - added -? to iter group to allow parsing -1 from smoothed TRACE file +LINE_RE = re.compile( + rf""" + ^%\s+ + (?P-?\d+)\s+ + (?P\d{{4}}-\d{{2}}-\d{{2}})\s+ # e.g. 2025-10-05 + (?P
    ' + ) + for item, fname in items + ) + + return f'

    {title}

      \n{lis}\n
    ' + + code_sec = section("code", "CODE") + phase_sec = section("phase", "PHASE") + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + html = f""" + + + +{html_escape(base_title)} — residual plots index ({item_title}s) + + + + +
    +

    {html_escape(base_title)} — residual plots index ({html_escape(item_title)}s)

    +
    Generated {html_escape(now)}
    +
    +
    Run info
    + {meta_rows}
    +
    +
    + +
    + + + +
    + +{code_sec} +{phase_sec} + +
    Tip: The search box accepts regular expressions.
    + + + + +""" + ensure_parent(index_path) + index_path.write_text(html, encoding="utf-8") + + +def build_recv_sat_stats(df: pd.DataFrame, yfield: str, weighted: bool) -> Dict[str, pd.DataFrame]: + """ + Super-fast aggregator using NumPy bincount. Returns dict of DataFrames: 'mean','std','rms'. + Includes margins: 'ALL_SAT' (left column), 'ALL_RECV' (top row), with grand cell at top-left. + """ + if df is None or df.empty: + return {"mean": pd.DataFrame(), "std": pd.DataFrame(), "rms": pd.DataFrame()} + + work = df[[ "recv","sat", yfield, "sigma" ]].copy() + y = pd.to_numeric(work[yfield], errors="coerce").to_numpy() + sig = pd.to_numeric(work["sigma"], errors="coerce").to_numpy() + + # masks: keep finite (and positive w when weighted) + if weighted: + w = 1.0 / (sig ** 2) + m = np.isfinite(y) & np.isfinite(w) & (w > 0) + else: + w = None + m = np.isfinite(y) + if not m.any(): + return {"mean": pd.DataFrame(), "std": pd.DataFrame(), "rms": pd.DataFrame()} + + y = y[m] + recv_vals = work.loc[m, "recv"].astype(str).to_numpy() + sat_vals = work.loc[m, "sat"].astype(str).to_numpy() + w = (w[m] if weighted else None) + + # Factorize to integer codes (preserves first-seen order) + recv_codes, recv_uniques = pd.factorize(recv_vals, sort=False) + sat_codes, sat_uniques = pd.factorize(sat_vals, sort=False) + R = recv_uniques.size + S = sat_uniques.size + + # Flat index r*S + s + flat = recv_codes * S + sat_codes + + # Unweighted tallies + if not weighted: + cnt = np.bincount(flat, minlength=R*S).astype(float) + sumy = np.bincount(flat, weights=y, minlength=R*S) + sumy2 = np.bincount(flat, weights=y*y, minlength=R*S) + + cnt2D = cnt.reshape(R, S) + sum2D = sumy.reshape(R, S) + sum22D = sumy2.reshape(R, S) + + # Mean + with np.errstate(invalid="ignore", divide="ignore"): + mean = np.where(cnt2D > 0, sum2D / cnt2D, np.nan) + # Sample variance (ddof=1) where cnt>1 + var = np.where(cnt2D > 1, (sum22D - (sum2D*sum2D)/cnt2D) / (cnt2D - 1.0), np.nan) + std = np.sqrt(var) + rms = np.where(cnt2D > 0, np.sqrt(sum22D / cnt2D), np.nan) + + # Margins (per-receiver = across columns, per-sat = across rows) + cnt_r = cnt2D.sum(axis=1) + sum_r = sum2D.sum(axis=1) + sum2_r = sum22D.sum(axis=1) + + cnt_s = cnt2D.sum(axis=0) + sum_s = sum2D.sum(axis=0) + sum2_s = sum22D.sum(axis=0) + + # per-receiver margins + mean_r = np.where(cnt_r > 0, sum_r / cnt_r, np.nan) + var_r = np.where(cnt_r > 1, (sum2_r - (sum_r*sum_r)/cnt_r) / (cnt_r - 1.0), np.nan) + std_r = np.sqrt(var_r) + rms_r = np.where(cnt_r > 0, np.sqrt(sum2_r / cnt_r), np.nan) + + # per-sat margins + mean_s = np.where(cnt_s > 0, sum_s / cnt_s, np.nan) + var_s = np.where(cnt_s > 1, (sum2_s - (sum_s*sum_s)/cnt_s) / (cnt_s - 1.0), np.nan) + std_s = np.sqrt(var_s) + rms_s = np.where(cnt_s > 0, np.sqrt(sum2_s / cnt_s), np.nan) + + # grand + cnt_all = cnt2D.sum() + sum_all = sum2D.sum() + sum2_all = sum22D.sum() + mean_all = (sum_all / cnt_all) if cnt_all > 0 else np.nan + var_all = ((sum2_all - (sum_all*sum_all)/cnt_all) / (cnt_all - 1.0)) if cnt_all > 1 else np.nan + std_all = np.sqrt(var_all) if np.isfinite(var_all) else np.nan + rms_all = np.sqrt(sum2_all / cnt_all) if cnt_all > 0 else np.nan + + else: + # Weighted tallies + wsum = np.bincount(flat, weights=w, minlength=R*S) + wysum = np.bincount(flat, weights=w*y, minlength=R*S) + wy2sum = np.bincount(flat, weights=w*y*y, minlength=R*S) + + wsum2D = wsum.reshape(R, S) + wysum2D = wysum.reshape(R, S) + wy22D = wy2sum.reshape(R, S) + + with np.errstate(invalid="ignore", divide="ignore"): + mean = np.where(wsum2D > 0, wysum2D / wsum2D, np.nan) + var = np.where(wsum2D > 0, (wy22D / wsum2D) - mean*mean, np.nan) # your previous definition + std = np.sqrt(var) + rms = np.where(wsum2D > 0, np.sqrt(wy22D / wsum2D), np.nan) + + # Margins + wsum_r = wsum2D.sum(axis=1) + wysum_r = wysum2D.sum(axis=1) + wy2_r = wy22D.sum(axis=1) + + wsum_s = wsum2D.sum(axis=0) + wysum_s = wysum2D.sum(axis=0) + wy2_s = wy22D.sum(axis=0) + + mean_r = np.where(wsum_r > 0, wysum_r / wsum_r, np.nan) + var_r = np.where(wsum_r > 0, (wy2_r / wsum_r) - mean_r*mean_r, np.nan) + std_r = np.sqrt(var_r) + rms_r = np.where(wsum_r > 0, np.sqrt(wy2_r / wsum_r), np.nan) + + mean_s = np.where(wsum_s > 0, wysum_s / wsum_s, np.nan) + var_s = np.where(wsum_s > 0, (wy2_s / wsum_s) - mean_s*mean_s, np.nan) + std_s = np.sqrt(var_s) + rms_s = np.where(wsum_s > 0, np.sqrt(wy2_s / wsum_s), np.nan) + + wsum_all = wsum2D.sum() + wysum_all = wysum2D.sum() + wy2_all = wy22D.sum() + mean_all = (wysum_all / wsum_all) if wsum_all > 0 else np.nan + var_all = ((wy2_all / wsum_all) - mean_all*mean_all) if wsum_all > 0 else np.nan + std_all = np.sqrt(var_all) if np.isfinite(var_all) else np.nan + rms_all = np.sqrt(wy2_all / wsum_all) if wsum_all > 0 else np.nan + + # Build matrices with margins at top/left + def _mk_df(mcore: np.ndarray, row_margin: np.ndarray, col_margin: np.ndarray, grand: float) -> pd.DataFrame: + # Assemble with ALL_RECV as top row, ALL_SAT as left col + # rows: ALL_RECV + recv_uniques + # cols: ALL_SAT + sat_uniques (sorted with your GNSS sorter) + sats_sorted = sorted(list(sat_uniques), key=_sat_sort_key) + # map sat_uniques -> order indices + sat_order = np.array([np.where(sat_uniques == s)[0][0] for s in sats_sorted], dtype=int) + m_sorted = mcore[:, sat_order] + + # left column (per-recv margin) + col_left = row_margin.reshape(-1, 1) + body_with_left = np.concatenate([col_left, m_sorted], axis=1) + + # top row (per-sat margin) + top_row = np.concatenate([[grand], col_margin[sat_order]], axis=0).reshape(1, -1) + + full = np.concatenate([top_row, body_with_left], axis=0) + + rows = ["ALL_RECV"] + [str(r) for r in recv_uniques.tolist()] + cols = ["ALL_SAT"] + [str(s) for s in sats_sorted] + return pd.DataFrame(full, index=rows, columns=cols) + + mean_df = _mk_df(mean, mean_r, mean_s, mean_all) + std_df = _mk_df(std, std_r, std_s, std_all) + rms_df = _mk_df(rms, rms_r, rms_s, rms_all) + + return {"mean": mean_df, "std": std_df, "rms": rms_df} + + +def _counts_to_customdata(counts_sorted: Optional[pd.DataFrame], z_shape: tuple): + """Convert a counts DataFrame into numeric customdata with shape (rows, cols, 1).""" + if counts_sorted is None or counts_sorted.empty: + return None + try: + cd = counts_sorted.to_numpy(dtype=float) # (rows, cols) + cd = np.nan_to_num(cd, nan=0.0, posinf=0.0, neginf=0.0) + if cd.shape != z_shape: + logger.warning("Counts shape %s != Z shape %s; omitting customdata.", cd.shape, z_shape) + return None + return cd[..., np.newaxis] # -> (rows, cols, 1) + except Exception as e: + logger.warning("Failed to build numeric customdata: %s", e) + return None + + +def write_heatmap_html( + df_mat: pd.DataFrame, + title: str, + out_html: str, + colorscale: str = "RdBu", + reversescale: bool = True, + zmid: Optional[float] = 0.0, + zmin: Optional[float] = None, + zmax: Optional[float] = None, + # Annotation knobs: + annotate: str = "none", # "none" | "auto" | "all" | "margins" (bool True->"all", False->"none") + precision: int = 3, + max_annot_cells: int = 2500, + # Hover counts matrix (aligned to df_mat's index/columns) + counts: Optional[pd.DataFrame] = None, + # sorting controls + row_sort: str = "alpha", # "alpha" | "none" + sat_sort: str = "alpha", # "alpha" | "natural" | "none" + # html writing + include_plotlyjs: str = "inline", # "inline" (default) or "cdn" +) -> str: + """ + Render a heatmap with optional annotations, sorted rows/columns, and robust hover counts. + + Sorting: + - Rows: 'ALL_RECV' is kept at the top if present; remaining rows sorted by `row_sort`. + - Columns: 'ALL_SAT' is kept at the left if present; remaining columns sorted by `sat_sort`. + - `sat_sort='natural'` uses `_sat_sort_key('G12') -> ('G', 12)` if available. + + Notes: + - `counts` (receiver×satellite sample counts) are shown in hover via `customdata[0]`. + - HTML is written with Plotly JS **inline** by default to avoid CDN caching issues. + """ + fig = go.Figure() + if df_mat is None or df_mat.empty: + fig.update_layout(title=title, template="plotly_white") + ensure_parent(out_html) + fig.write_html(out_html, include_plotlyjs=include_plotlyjs, validate=False, config={"scrollZoom": True}) + return out_html + + # --- Build target row/col orders (preserve margins, sort the rest) --- + rows = list(df_mat.index) + cols = list(df_mat.columns) + + # Rows: keep ALL_RECV first if present + if rows and rows[0] == "ALL_RECV": + row_header, row_body = ["ALL_RECV"], rows[1:] + else: + row_header = [r for r in rows if r == "ALL_RECV"] + row_body = [r for r in rows if r != "ALL_RECV"] + + if row_sort == "alpha": + row_body_sorted = sorted(row_body, key=lambda s: str(s).upper()) + else: + row_body_sorted = row_body + rows_order = row_header + row_body_sorted + + # Columns: keep ALL_SAT first if present + if cols and cols[0] == "ALL_SAT": + col_header, col_body = ["ALL_SAT"], cols[1:] + else: + col_header = [c for c in cols if c == "ALL_SAT"] + col_body = [c for c in cols if c != "ALL_SAT"] + + if sat_sort == "alpha": + col_body_sorted = sorted(col_body, key=lambda s: str(s).upper()) + elif sat_sort == "natural" and "_sat_sort_key" in globals(): + col_body_sorted = sorted(col_body, key=_sat_sort_key) + else: + col_body_sorted = col_body + cols_order = col_header + col_body_sorted + + # --- Reindex matrices to this order (keeps alignment with hover counts) --- + mat_sorted = df_mat.reindex(index=rows_order, columns=cols_order) + counts_sorted = counts.reindex(index=rows_order, columns=cols_order) if (counts is not None and not counts.empty) else None + + # --- Extract arrays --- + Z = mat_sorted.values + X = list(mat_sorted.columns) + Y = list(mat_sorted.index) + + # --- Decide annotation mode --- + if isinstance(annotate, bool): + annotate = "all" if annotate else "none" + + if annotate == "auto": + do_text, margins_only = (Z.size <= max_annot_cells), False + elif annotate == "all": + do_text, margins_only = True, False + elif annotate == "margins": + do_text, margins_only = True, True + else: + do_text, margins_only = False, False + + # --- Build text labels ONLY if annotating --- + text = None + if do_text: + text = np.full(Z.shape, "", dtype=object) + if margins_only: + if Z.shape[0] > 0: + top = Z[0, :] + text[0, :] = np.where(np.isfinite(top), np.round(top, precision).astype(str), "") + if Z.shape[1] > 0: + left = Z[:, 0] + text[1:, 0] = np.where(np.isfinite(left[1:]), np.round(left[1:], precision).astype(str), "") + else: + text = np.where(np.isfinite(Z), np.round(Z, precision).astype(str), "") + + # --- Prepare customdata and hovertemplate --- + customdata = _counts_to_customdata(counts_sorted, Z.shape) + if customdata is not None: + hovertemplate = ( + "Receiver=%{y}
    Satellite=%{x}" + f"
    Value=%{{z:.{precision}f}}" + "
    Count=%{customdata[0]:.0f}" + "" + ) + else: + hovertemplate = ( + "Receiver=%{y}
    Satellite=%{x}" + f"
    Value=%{{z:.{precision}f}}" + "" + ) + + # --- Heatmap trace --- + hm_kwargs = dict( + z=Z, x=X, y=Y, + colorscale=colorscale, + reversescale=reversescale, + hoverongaps=False, + hovertemplate=hovertemplate, + ) + if customdata is not None: + hm_kwargs["customdata"] = customdata + if zmid is not None: hm_kwargs["zmid"] = zmid + if zmin is not None: hm_kwargs["zmin"] = zmin + if zmax is not None: hm_kwargs["zmax"] = zmax + if do_text: + hm_kwargs["text"] = text + hm_kwargs["texttemplate"] = "%{text}" + hm_kwargs["textfont"] = {"size": 11} + + fig.add_trace(go.Heatmap(**hm_kwargs)) + + # Axes + fig.update_yaxes( + autorange="reversed", + categoryorder="array", + categoryarray=Y, + side="top", + mirror=True, + ticks="outside", + showline=True, + ) + fig.update_xaxes( + side="top", + mirror=True, + ticks="outside", + showline=True, + ) + + fig.update_layout( + title=title, + xaxis_title="Satellite", + yaxis_title="Receiver", + template="plotly_white", + uniformtext_minsize=8, + uniformtext_mode="hide", + margin=dict(l=20, r=20, t=20, b=20), + ) + + # Interactions + fig.update_layout(dragmode="zoom") + fig.update_xaxes(fixedrange=False) + fig.update_yaxes(fixedrange=False) + + # Write HTML (inline Plotly by default) + ensure_parent(out_html) + fig.write_html(out_html, include_plotlyjs=include_plotlyjs, validate=False, config={"scrollZoom": True}) + logger.info("Wrote: %s", out_html) + return out_html + + +def prepare_ambiguity_reasons(df_amb: pd.DataFrame) -> pd.DataFrame: + """ + Process ambiguity reset data for counting by reason. + + - Explodes comma-separated reasons into individual rows + - Deduplicates by (datetime, sat, reason) to count each reason once per satellite per epoch + + Returns: + DataFrame with one row per unique (datetime, sat, reason) combination + """ + if df_amb is None or df_amb.empty: + return pd.DataFrame() + + df = df_amb.copy() + df["datetime"] = pd.to_datetime(df["datetime"], errors="coerce") + + # Explode comma-separated reasons into individual rows + df = df.assign(reason=df["reasons"].fillna("").str.split(",")) + df = df.explode("reason") + df["reason"] = df["reason"].str.strip() + df = df[df["reason"] != ""] + + if df.empty: + return df + + # Deduplicate: count each unique (datetime, sat, recv, reason) only once + # (same reason on multiple signals for same sat at same time at same receiver = count once) + df = df.drop_duplicates(subset=["datetime", "sat", "recv", "reason"]) + + return df + + +def prepare_ambiguity_events(df_amb: pd.DataFrame) -> pd.DataFrame: + """ + Process ambiguity reset data for counting satellite reset events. + + - Deduplicates by (datetime, sat, action) to count each reset event once + - Each satellite reset = one event, regardless of how many signals or reasons + + Returns: + DataFrame with one row per unique (datetime, sat, action) combination + """ + if df_amb is None or df_amb.empty: + return pd.DataFrame() + + df = df_amb.copy() + df["datetime"] = pd.to_datetime(df["datetime"], errors="coerce") + df = df.dropna(subset=["datetime"]) + + if df.empty: + return df + + # Deduplicate: count each satellite reset once per epoch per receiver + # (same sat at same time with multiple signals/reasons at same receiver = one reset event) + df = df.drop_duplicates(subset=["datetime", "sat", "recv", "action"]) + + return df + + +def plot_ambiguity_reason_counts( + df_amb: pd.DataFrame, + split: str = "combined", + *, + base: str, + variant_suffix: str, +) -> List[str]: + """ + Generate cumulative ambiguity-reset reason count plots (diagnostic view). + Shows how often each detection method (GF, MW, LLI, SCDIA) triggers. + Also shows unique satellite resets for comparison. + """ + outputs = [] + if df_amb is None or df_amb.empty: + logger.warning("No ambiguity-reset data to plot.") + return outputs + + # Process and deduplicate ambiguity reasons + df_reasons = prepare_ambiguity_reasons(df_amb) + if df_reasons.empty: + logger.warning("No ambiguity-reset reasons found after processing.") + return outputs + + df_reasons = df_reasons.sort_values("datetime") + + # Also prepare unique satellite reset events + df_events = prepare_ambiguity_events(df_amb) + df_events = df_events.sort_values("datetime") if not df_events.empty else df_events + + # Choose grouping key + if split == "recv": + groups = df_reasons.groupby("recv", dropna=False) + event_groups = df_events.groupby("recv", dropna=False) if not df_events.empty else [] + elif split == "sat": + groups = df_reasons.groupby("sat", dropna=False) + event_groups = df_events.groupby("sat", dropna=False) if not df_events.empty else [] + else: + groups = [("ALL", df_reasons)] + event_groups = [("ALL", df_events)] if not df_events.empty else [] + + # Convert event_groups to dict for easy lookup + event_dict = {k: v for k, v in event_groups} + + for key, g in groups: + if g.empty: + continue + + counts = ( + g.groupby(["datetime", "reason"], sort=True) + .size() + .reset_index(name="count") + .sort_values("datetime") + ) + counts["cumcount"] = counts.groupby("reason")["count"].cumsum() + pivot = counts.pivot(index="datetime", columns="reason", values="cumcount").ffill().fillna(0) + + fig = go.Figure() + for reason in pivot.columns: + fig.add_trace(go.Scatter(x=pivot.index, y=pivot[reason], mode="lines", name=reason)) + + # Add unique resets trace if available + if key in event_dict and not event_dict[key].empty: + g_events = event_dict[key] + event_counts = ( + g_events.groupby("datetime", sort=True) + .size() + .reset_index(name="count") + .sort_values("datetime") + ) + event_counts["cumcount"] = event_counts["count"].cumsum() + + fig.add_trace(go.Scatter( + x=event_counts["datetime"], + y=event_counts["cumcount"], + mode="lines", + name="Unique Resets", + line=dict(dash="dash", width=2.5), + )) + + title_suffix = { + "recv": f"Receiver {key}", + "sat": f"Satellite {key}", + "combined": "All receivers/satellites", + }.get(split, "All") + + fig.update_layout( + title=f"Cumulative Ambiguity Resets — {title_suffix}
    Each reason counted once per epoch-satellite across all signals. 'Unique Resets' shows total satellites affected.", + xaxis_title="Time", + yaxis_title="Cumulative Count", + template="plotly_white", + legend_title="Metric", + hovermode="x unified", + ) + + safe_key = _sanitize_filename_piece(str(key)) + out_html = build_out_path(base, variant_suffix, "ambiguity_counts", split=split, key=safe_key) + ensure_parent(out_html) + fig.write_html(out_html, include_plotlyjs="cdn", validate=False, config={"scrollZoom": True}) + logger.info(f"Wrote: {out_html}") + outputs.append(out_html) + + return outputs + + +def plot_ambiguity_reason_totals( + df_amb: pd.DataFrame, + split: str = "combined", + orientation: str = "h", + top_n: int = None, + *, + base: str, + variant_suffix: str, +) -> List[str]: + """ + Write a stacked bar chart of total ambiguity reset reasons (diagnostic view). + Shows total counts of each detection method (GF, MW, LLI, SCDIA) per receiver/satellite. + """ + outputs = [] + if df_amb is None or df_amb.empty: + logger.warning("No ambiguity-reset data to plot.") + return outputs + + # Process and deduplicate ambiguity reasons + df = prepare_ambiguity_reasons(df_amb) + if df.empty: + logger.info("No ambiguity-reset reasons found after processing.") + return outputs + + key_name = {"recv": "recv", "sat": "sat"}.get(split, None) + + if key_name is None: + gdf = df.groupby("reason", sort=True).size().rename("count").reset_index() + gdf["group"] = "ALL" # AJC - Add group column for pivot + pivot = gdf.pivot_table(index="group", + columns="reason", values="count", fill_value=0) + else: + pivot = ( + df.groupby([key_name, "reason"], sort=True) + .size() + .rename("count") + .reset_index() + .pivot(index=key_name, columns="reason", values="count") + .fillna(0) + ) + + reason_totals = pivot.sum(axis=0).sort_values(ascending=False) + pivot = pivot.reindex(columns=reason_totals.index) + + # Compute unique reset counts per group (sat/recv/combined) + df_events = prepare_ambiguity_events(df_amb) + if not df_events.empty and key_name is not None: + unique_resets = df_events.groupby(key_name).size().reindex(pivot.index, fill_value=0) + elif not df_events.empty: + # Combined case: count all unique resets + unique_resets = pd.Series([len(df_events)], index=pivot.index) + else: + unique_resets = pd.Series([0] * len(pivot), index=pivot.index) + + if top_n is not None and top_n > 0 and pivot.shape[0] > top_n: + group_totals = pivot.sum(axis=1).sort_values(ascending=False) + keep = group_totals.index[:top_n] + pivot = pivot.loc[keep] + unique_resets = unique_resets.loc[keep] + + fig = go.Figure() + groups = pivot.index.astype(str).tolist() + is_h = (orientation == "h") + x_title = "Total resets" if is_h else (key_name or "All") + y_title = (key_name or "All") if is_h else "Total resets" + + for reason in pivot.columns: + vals = pivot[reason].to_numpy() + if is_h: + fig.add_trace(go.Bar(y=groups, x=vals, orientation="h", name=reason)) + else: + fig.add_trace(go.Bar(x=groups, y=vals, name=reason)) + + # Add unique reset markers (star) on each bar at their actual value + unique_reset_vals = unique_resets.values + + if is_h: + # Horizontal bars: place star markers at unique reset count position + fig.add_trace(go.Scatter( + x=unique_reset_vals, + y=groups, + mode='markers', + marker=dict(symbol='star', size=12, color='gold', line=dict(width=0)), + showlegend=True, + name='Unique Resets', + hovertemplate='Unique Resets: %{x}', + )) + else: + # Vertical bars: place star markers at unique reset count position + fig.add_trace(go.Scatter( + x=groups, + y=unique_reset_vals, + mode='markers', + marker=dict(symbol='star', size=12, color='gold', line=dict(width=0)), + showlegend=True, + name='Unique Resets', + hovertemplate='Unique Resets: %{y}', + )) + + split_title = {"recv": "by Receiver", "sat": "by Satellite", "combined": "All"}[split] + fig.update_layout( + title=f"Total Ambiguity Resets {split_title} (stacked by reason)
    Each reason counted once per epoch-satellite across all signals. Any signal triggering a reason resets all ambiguities (ionosphere no longer constrained). ⭐ = unique satellite resets.", + barmode="stack", + template="plotly_white", + legend_title="Reason", + xaxis_title=x_title, + yaxis_title=y_title, + hovermode="closest", + margin=dict(l=80, r=20, t=60, b=60), + ) + + if is_h and pivot.shape[0] > 1: + fig.update_yaxes(categoryorder="array", categoryarray=groups[::-1]) + + out_html = build_out_path(base, variant_suffix, "ambiguity_totals", split=split, tag=orientation) + ensure_parent(out_html) + fig.write_html(out_html, include_plotlyjs="cdn", validate=False, config={"scrollZoom": True}) + logger.info(f"Wrote: {out_html}") + outputs.append(out_html) + return outputs + + +def add_ambiguity_markers_combined( + fig: go.Figure, + df_amb: pd.DataFrame, + *, + y_anchor: Optional[float] = None, + y_span: Optional[Tuple[float, float]] = None, # (y_lo, y_hi) for the v-line span + line_width: float = 0.5, + marker_size: int = 10, + line_color_preproc: str = "rgba(0,128,0,0.85)", # green + line_color_reject: str = "rgba(65,105,225,0.85)", # royal blue + split_by_reason: bool = True, + split_context: Optional[str] = None, # "recv" or "sat" to indicate grouping + add_trace_fn = None, # Function to add traces (handles subplot layout) +) -> None: + """ + Add ambiguity-reset overlays (PREPROC/REJECT) that toggle with each satellite's legend item. + """ + if add_trace_fn is None: + add_trace_fn = fig.add_trace + if df_amb is None or df_amb.empty: + return + + amb = df_amb.copy() + amb["datetime"] = pd.to_datetime(amb["datetime"], errors="coerce") + amb = amb.dropna(subset=["datetime"]) + if "reasons" not in amb.columns: + amb["reasons"] = "" + + if amb.empty: + return + + # If no explicit y_anchor given, place markers near the top of the line span + if y_anchor is None and y_span and isinstance(y_span, tuple) and len(y_span) == 2: + y_anchor = y_span[1] + + def _add_sat_vline(ts, y_lo, y_hi, sat, color="rgba(120,120,120,0.55)"): + add_trace_fn( + go.Scatter( + x=[ts, ts], + y=[y_lo, y_hi], + mode="lines", + line=dict(color=color, width=line_width, dash="dash"), + hoverinfo="skip", + showlegend=False, + legendgroup=str(sat), + name="", + ) + ) + + for ts, g_ts in amb.groupby("datetime", sort=True): + y_lo, y_hi = (None, None) + if y_span and isinstance(y_span, tuple) and len(y_span) == 2: + y_lo, y_hi = y_span + + # For recv context, aggregate all satellites at this timestamp into one marker + # For sat context, create separate markers per satellite + # For combined/default, aggregate all at timestamp + if split_context == "recv" or split_context is None: + # Add vertical lines for each satellite at this timestamp + for sat in g_ts["sat"].unique() if "sat" in g_ts.columns else []: + _add_sat_vline(ts, y_lo, y_hi, sat) + + # Create markers aggregated across all satellites + if split_by_reason: + if "action" in g_ts.columns: + action_key = g_ts["action"].where(g_ts["action"].notna(), "").astype(str).str.upper() + else: + action_key = pd.Series([""], index=g_ts.index) + for action_val, g_act in g_ts.groupby(action_key, sort=False): + mcolor = line_color_preproc if action_val == "PREPROC" else line_color_reject + + # Build hierarchical hover text + if split_context == "recv": + # Group by Sat, then show Sig | Reasons for each + lines = [] + for sat_key, sat_grp in g_act.groupby("sat", sort=False): + lines.append(f"Sat= {sat_key}") + for _, row in sat_grp.iterrows(): + lines.append(f"Sig= {row.get('sig','')} | Reasons= {row.get('reasons','(none)')}") + rows_html = lines + hover_recv = g_act.iloc[0].get('recv', '') + hover = ( + "Phase Ambiguity Reset
    " + f"Time: {ts}
    " + f"Action: {action_val}
    " + f"Recv= {hover_recv}
    " + + "
    ".join(rows_html) + ) + else: + # Combined/default: show both Sat and Recv for each signal + rows_html = [ + f"Sat={r.get('sat','')} | Recv={r.get('recv','')} | Sig={r.get('sig','')} | Reasons={r.get('reasons','(none)')}" + for _, r in g_act.iterrows() + ] + hover = ( + "Phase Ambiguity Reset
    " + f"Time: {ts}
    " + f"Action: {action_val}
    " + + "
    ".join(rows_html) + ) + # Use first satellite for legendgroup (allows toggling visibility) + first_sat = g_act.iloc[0].get("sat", "") if not g_act.empty else "" + add_trace_fn( + go.Scatter( + x=[ts], + y=[y_anchor], + mode="markers", + marker=dict(color=mcolor, size=marker_size, symbol="triangle-down"), + hovertemplate=hover + "", + showlegend=False, + legendgroup=str(first_sat), + name="", + ) + ) + else: + # One combined marker at this timestamp (mixed actions possible) + actions = set(g_ts["action"].str.upper() if "action" in g_ts.columns else [""]) + + # Build hierarchical hover text + if split_context == "recv": + # Group by Action, then Sat, then show Sig | Reasons + lines = [] + for act_key, act_grp in g_ts.groupby(g_ts["action"].str.upper() if "action" in g_ts.columns else "", sort=False): + if act_key: + lines.append(f"Action: {act_key}") + for sat_key, sat_grp in act_grp.groupby("sat", sort=False): + lines.append(f"Sat= {sat_key}") + for _, row in sat_grp.iterrows(): + lines.append(f"Sig= {row.get('sig','')} | Reasons= {row.get('reasons','(none)')}") + hover_recv = g_ts.iloc[0].get('recv', '') if not g_ts.empty else '' + hover = f"Phase Ambiguity Reset
    Time: {ts}
    Recv= {hover_recv}
    " + "
    ".join(lines) + else: + # Combined/default + lines = [] + for _, r in g_ts.iterrows(): + act = str(r.get("action", "")).upper() + lines.append( + f"{act}: Sat={r.get('sat','')} | Recv={r.get('recv','')} | Sig={r.get('sig','')} | Reasons={r.get('reasons','(none)')}" + ) + hover = f"Phase Ambiguity Reset
    Time: {ts}
    " + "
    ".join(lines) + + if actions == {"PREPROC"}: + mcolor = line_color_preproc + elif actions == {"REJECT"}: + mcolor = line_color_reject + else: + mcolor = "rgba(90,90,90,0.8)" # mixed/unknown + first_sat = g_ts.iloc[0].get("sat", "") if not g_ts.empty else "" + add_trace_fn( + go.Scatter( + x=[ts], + y=[y_anchor], + mode="markers", + marker=dict(color=mcolor, size=marker_size, symbol="triangle-down"), + hovertemplate=hover + "", + showlegend=False, + legendgroup=str(first_sat), + name="", + ) + ) + else: + # split_context == "sat": Aggregate all receivers at this timestamp for the satellite + # Add vertical lines for the satellite(s) at this timestamp + for sat in g_ts["sat"].unique() if "sat" in g_ts.columns else []: + _add_sat_vline(ts, y_lo, y_hi, sat) + + # Create markers aggregated across all receivers for this satellite + if split_by_reason: + if "action" in g_ts.columns: + action_key = g_ts["action"].where(g_ts["action"].notna(), "").astype(str).str.upper() + else: + action_key = pd.Series([""], index=g_ts.index) + for action_val, g_act in g_ts.groupby(action_key, sort=False): + mcolor = line_color_preproc if action_val == "PREPROC" else line_color_reject + + # Group by Recv, then show Sig | Reasons for each + lines = [] + for recv_key, recv_grp in g_act.groupby("recv", sort=False): + lines.append(f"Recv= {recv_key}") + for _, row in recv_grp.iterrows(): + lines.append(f"Sig= {row.get('sig','')} | Reasons= {row.get('reasons','(none)')}") + rows_html = lines + hover_sat = g_act.iloc[0].get('sat', '') if not g_act.empty else '' + hover = ( + "Phase Ambiguity Reset
    " + f"Time: {ts}
    " + f"Action: {action_val}
    " + f"Sat= {hover_sat}
    " + + "
    ".join(rows_html) + ) + # Use first satellite for legendgroup + first_sat = g_act.iloc[0].get("sat", "") if not g_act.empty else "" + add_trace_fn( + go.Scatter( + x=[ts], + y=[y_anchor], + mode="markers", + marker=dict(color=mcolor, size=marker_size, symbol="triangle-down"), + hovertemplate=hover + "", + showlegend=False, + legendgroup=str(first_sat), + name="", + ) + ) + else: + # One combined marker at this timestamp (mixed actions possible) + actions = set(g_ts["action"].str.upper() if "action" in g_ts.columns else [""]) + + # Group by Action, then Recv, then show Sig | Reasons + lines = [] + for act_key, act_grp in g_ts.groupby(g_ts["action"].str.upper() if "action" in g_ts.columns else "", sort=False): + if act_key: + lines.append(f"Action: {act_key}") + for recv_key, recv_grp in act_grp.groupby("recv", sort=False): + lines.append(f"Recv= {recv_key}") + for _, row in recv_grp.iterrows(): + lines.append(f"Sig= {row.get('sig','')} | Reasons= {row.get('reasons','(none)')}") + hover_sat = g_ts.iloc[0].get('sat', '') if not g_ts.empty else '' + hover = f"Phase Ambiguity Reset
    Time: {ts}
    Sat= {hover_sat}
    " + "
    ".join(lines) + + if actions == {"PREPROC"}: + mcolor = line_color_preproc + elif actions == {"REJECT"}: + mcolor = line_color_reject + else: + mcolor = "rgba(90,90,90,0.8)" # mixed/unknown + first_sat = g_ts.iloc[0].get("sat", "") if not g_ts.empty else "" + add_trace_fn( + go.Scatter( + x=[ts], + y=[y_anchor], + mode="markers", + marker=dict(color=mcolor, size=marker_size, symbol="triangle-down"), + hovertemplate=hover + "", + showlegend=False, + legendgroup=str(first_sat), + name="", + ) + ) + +# -------- CLI / Main -------- + +def pair_forward_smoothed_files(in_paths: List[Path], use_forward_residuals: bool): + """ + Separate and pair forward and smoothed TRACE files. + + Returns: + residual_paths: List[Path] - files to use for residuals + forward_paths: List[Path] - files to use for ambiguity resets and large errors + warnings: List[str] - warning messages about missing files + """ + forward_files = [] + smoothed_files = [] + + # Separate files into forward and smoothed + for p in in_paths: + if "_smoothed" in p.stem: + smoothed_files.append(p) + else: + forward_files.append(p) + + warnings = [] + + # Build pairing maps: base_name -> (forward_path, smoothed_path) + pairs = {} + + # Add forward files to pairs + for fwd in forward_files: + base = fwd.stem + if base not in pairs: + pairs[base] = {"forward": fwd, "smoothed": None} + + # Add smoothed files to pairs + for smo in smoothed_files: + # Remove "_smoothed" suffix to get base name + base = smo.stem.replace("_smoothed", "") + if base in pairs: + pairs[base]["smoothed"] = smo + else: + # Smoothed file without matching forward + pairs[base] = {"forward": None, "smoothed": smo} + warnings.append( + f"Found smoothed file but no matching forward file: {smo.name}\n" + f" Ambiguity reset and large error data will not be available for this file" + ) + + # Determine which files to use for what + if use_forward_residuals: + # Use forward for everything + residual_paths = forward_files.copy() + forward_paths = forward_files.copy() + if smoothed_files: + logger.info("Using residuals from FORWARD files (--use-forward-residuals specified)") + else: + # Default: use smoothed for residuals if available, otherwise forward + residual_paths = [] + forward_paths = [] + + has_smoothed = False + has_forward = False + + for base, files in pairs.items(): + fwd = files["forward"] + smo = files["smoothed"] + + if smo is not None: + residual_paths.append(smo) + has_smoothed = True + elif fwd is not None: + residual_paths.append(fwd) + warnings.append( + f"No smoothed file found for: {fwd.name}\n" + f" Using forward file for residuals. For more accurate residuals, include *_smoothed.TRACE files" + ) + + if fwd is not None: + forward_paths.append(fwd) + has_forward = True + + if has_smoothed: + logger.info("Using residuals from SMOOTHED files (more accurate)") + if has_forward: + logger.info("Using ambiguity resets and large errors from FORWARD files") + + if not has_smoothed and forward_files: + warnings.append( + "No smoothed files found - using residuals from forward files\n" + " For more accurate residuals, include *_smoothed.TRACE files in your pattern" + ) + + # Log file pairing summary + if forward_files and smoothed_files: + logger.info(f"Found {len(forward_files)} forward file(s) and {len(smoothed_files)} smoothed file(s)") + for base, files in pairs.items(): + fwd = files["forward"] + smo = files["smoothed"] + if fwd and smo: + logger.debug(f"Paired: {fwd.name} (forward) with {smo.name} (smoothed)") + + return residual_paths, forward_paths, warnings + + +def main(): + p = argparse.ArgumentParser(description="Extract and plot GNSS residuals with optional large-error markers.") + p.add_argument("--files", required=True, nargs="+", help="One or more TRACE files (space and/or comma separated), e.g. 'A.trace B.trace,C.trace' (wildcards allowed eg. *.TRACE)") + p.add_argument("--residual", choices=["prefit", "postfit"], default="postfit") + p.add_argument("--receivers", help="One or more receiver names (comma or space separated), e.g. 'ABMF,CHUR ALGO' ") + p.add_argument("--sat", "-s", action="append", help="Filter by satellite ID") + p.add_argument("--label-regex", help="Regex to filter labels") + p.add_argument("--max-abs", type=float, default=None) + p.add_argument("--start", help="Start datetime or time-only") + p.add_argument("--end", help="End datetime (exclusive)") + p.add_argument("--decimate", type=int, default=1) + split_group = p.add_mutually_exclusive_group() + split_group.add_argument("--split-per-sat", action="store_true") + split_group.add_argument("--split-per-recv", action="store_true") + p.add_argument("--out-dir", help="Output directory for HTML files; defaults to CWD.") + p.add_argument("--basename", help="Base filename prefix for outputs (no extension).") + p.add_argument("--webgl", action="store_true") + p.add_argument("--log-level", default="INFO", choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL"], help="Logging verbosity.") + p.add_argument("--out-prefix", default=None) + p.add_argument("--mark-large-errors", action="store_true", help="Mark LARGE STATE/MEAS ERROR events on plots.") + p.add_argument("--hover-unified", action="store_true", help="Use unified hover tooltips across all traces (default: closest point hover).") + p.add_argument("--plot-normalised-res", action="store_true", help="Also generate plots of normalised residuals (residual / sigma).") + p.add_argument("--plot-normalized-res", action="store_true", help=argparse.SUPPRESS) + p.add_argument("--show-stats-table", action="store_true", help="Add a Mean / Std / RMS table per (sat × signal) at the bottom of each plot.") + p.add_argument("--stats-matrix", action="store_true", help="Generate receiver×satellite heatmaps (Mean/Std/RMS) aggregated across signals.") + p.add_argument("--stats-matrix-weighted", action="store_true", help="Use sigma-weighted statistics in the heatmaps (weights 1/σ²).") + p.add_argument("--annotate-stats-matrix", action="store_true", help="Write the numeric value (mean/std/rms) into each stats heatmap cell. Hover still shows full details.") + p.add_argument("--mark-amb-resets", action="store_true", help="Overlay PHASE ambiguity reset events (PREPROC=green, REJECT=blue) on PHASE per-receiver plots.") + + # Ambiguity reset plots (includes both reasons and unique satellite resets) + p.add_argument("--ambiguity-counts", action="store_true", help="Plot cumulative counts of ambiguity reset reasons and unique satellite resets over time.") + p.add_argument("--ambiguity-totals", action="store_true", help="Bar chart of total ambiguity reset reasons (diagnostic view of detection methods).") + + p.add_argument("--amb-totals-orient", choices=["h", "v"], default="h", help="Orientation for totals bar charts: 'h' (horizontal, default) or 'v' (vertical).") + p.add_argument("--amb-totals-topn", type=int, default=None, help="Show only the top-N receivers/satellites by total resets (to avoid clutter).") + p.add_argument("--use-forward-residuals", action="store_true", help="Use residuals from forward (non-smoothed) files instead of smoothed files (default: use smoothed for more accurate residuals).") + + args = p.parse_args() + _setup_logging(args.log_level) + args.plot_normalised_res = args.plot_normalised_res or args.plot_normalized_res + + import glob, os, re + from pathlib import Path + from typing import List + + # --- Expand wildcards and handle comma/space separated patterns --- + # args.files is a list because of nargs="+". Each element itself may contain commas. + patterns: List[str] = [] + for tok in args.files: + # split any "A.trace,B.trace" into ["A.trace","B.trace"] + parts = [s for s in re.split(r"[,\s]+", str(tok)) if s] + patterns.extend(parts) + + expanded_files: List[str] = [] + for pattern in patterns: + # Expand ~ and wildcards; keep deterministic order + matches = sorted(glob.glob(os.path.expanduser(pattern))) + if not matches: + print(f"⚠️ No files matched pattern: {pattern}") + expanded_files.extend(matches) + + # Replace the original with the expanded list (still strings at this point) + args.files = expanded_files + + if not args.files: + p.error("No input files found after wildcard expansion.") + + # --- Normalize into Path objects, validate existence, and de-duplicate (preserve order) --- + in_paths: List[Path] = [] + seen = set() + for f in args.files: + pth = Path(f) + if not pth.exists(): + raise SystemExit(f"File not found: {pth}") + key = str(pth.resolve()) + if key not in seen: + seen.add(key) + in_paths.append(pth) + + if not in_paths: + raise SystemExit("No input files provided to --files.") + + # de-duplicate, preserve order + seen = set() + in_paths = [p for p in in_paths if not (str(p.resolve()) in seen or seen.add(str(p.resolve())))] + + if not in_paths: + raise SystemExit("No input files provided to --files.") + + # --- Pair forward and smoothed files --- + residual_paths, forward_paths, file_warnings = pair_forward_smoothed_files( + in_paths, args.use_forward_residuals + ) + + # Display any file pairing warnings + for warn in file_warnings: + logger.warning(warn) + + # For output filenames, use only forward files (or unique base names) to avoid duplication + # This prevents names like "file_file_smoothed" when both forward and smoothed exist + output_basename_paths = forward_paths if forward_paths else residual_paths + + # --- Normalize receiver filters from --receivers into an ordered, deduplicated list --- + if args.receivers: + recv_list = [tok.strip() for tok in re.split(r"[,\s]+", args.receivers) if tok.strip()] + # dedupe case-insensitively, preserve order + seen = set() + recv_list = [r for r in recv_list if not (r.upper() in seen or seen.add(r.upper()))] + else: + recv_list = None + + # AJC --- Generator to iterate through all files without loading into memory --- + def iter_all_lines(paths): + """Generator that yields lines from all input files.""" + for pth in paths: + with pth.open("r", encoding="utf-8", errors="ignore") as fh: + yield from fh + + # Parse files using generator (makes multiple passes to reduce memory usage) + # Use residual_paths for residuals (smoothed if available), forward_paths for other data + + # If we have both smoothed and forward files, merge them: + # - Use postfit from smoothed (more accurate residuals) + # - Use sigma from forward (has measurement weights) + if residual_paths != forward_paths: + logger.info("Merging smoothed and forward files: postfit from smoothed, sigma from forward") + df_smoothed = parse_trace_lines(iter_all_lines(residual_paths)) + df_forward = parse_trace_lines(iter_all_lines(forward_paths)) + + # Normalize string columns for robust matching + for col in ["meas", "sat", "recv", "sig"]: + if col in df_smoothed.columns: + df_smoothed[col] = df_smoothed[col].astype(str).str.strip() + if col in df_forward.columns: + df_forward[col] = df_forward[col].astype(str).str.strip() + + # Merge on matching keys + merge_keys = ["datetime", "meas", "sat", "recv", "sig"] + df = df_forward.merge( + df_smoothed[merge_keys + ["postfit"]], + on=merge_keys, + how="left", + suffixes=("_fwd", "_smo") + ) + + # Use smoothed postfit where available, otherwise fall back to forward + if "postfit_smo" in df.columns: + n_smoothed = df["postfit_smo"].notna().sum() + n_forward = df["postfit_smo"].isna().sum() + df["postfit"] = df["postfit_smo"].fillna(df["postfit_fwd"]) + + # Recalculate postfit_ratio if it exists (postfit_ratio = postfit / sigma) + if "postfit_ratio" in df.columns and "sigma" in df.columns: + # Use smoothed postfit with forward sigma + df["postfit_ratio"] = df["postfit"] / df["sigma"].replace(0, float('nan')) + + df = df.drop(columns=["postfit_fwd", "postfit_smo"]) + logger.info(f"Merged: {len(df)} measurements total ({n_smoothed} from smoothed, {n_forward} forward-only)") + else: + logger.warning("Merge did not produce postfit_smo column - using forward values only") + else: + df = parse_trace_lines(iter_all_lines(residual_paths)) + #df = parse_trace_lines_fast(iter_all_lines(residual_paths)) + + df_large = parse_large_errors(iter_all_lines(forward_paths)) if args.mark_large_errors else pd.DataFrame() + + # --- Apply CLI filters to residuals --- + df = filter_df(df, receivers=recv_list, sats=args.sat, label_regex=args.label_regex) + df = keep_last_iteration(df) + + # --- Time window filtering --- + start_dt = _parse_dt_like(args.start, df["datetime"]) if args.start else None + end_dt = _parse_dt_like(args.end, df["datetime"]) if args.end else None + if start_dt is not None: + df = df[df["datetime"] >= start_dt] + if end_dt is not None: + df = df[df["datetime"] < end_dt] + + # --- Apply same filters/time window to large-error events --- + if args.mark_large_errors and not df_large.empty: + df_large = filter_large_errors(df_large, receivers=recv_list, sats=args.sat, start_dt=start_dt, end_dt=end_dt) + + # Parse ambiguity resets if needed by any ambiguity feature (from forward files only) + need_amb = bool( + args.mark_amb_resets or + args.ambiguity_counts or args.ambiguity_totals + ) + df_amb = parse_ambiguity_resets(iter_all_lines(forward_paths)) if need_amb else pd.DataFrame() + + # --- Schema normalize & assert (Part 1) --- + REQUIRED_AMB_COLS = ["datetime","sat","recv","action","sig","reasons"] + + if not need_amb: + # Ensure empty-but-well-formed frame so downstream never breaks + df_amb = pd.DataFrame(columns=REQUIRED_AMB_COLS) + + else: + if df_amb is None or df_amb.empty: + # Still provide the required columns for consistency + df_amb = pd.DataFrame(columns=REQUIRED_AMB_COLS) + else: + # Ensure string col names + df_amb = df_amb.rename(columns=str).copy() + + # Drop duplicate columns by name (keep first occurrence) + if df_amb.columns.duplicated().any(): + # Optional: log duplicates for visibility + dup_names = df_amb.columns[df_amb.columns.duplicated()].tolist() + logger.warning("df_amb had duplicate columns, dropping dups: %s", dup_names) + df_amb = df_amb.loc[:, ~df_amb.columns.duplicated(keep="first")] + + # Assert required columns are present + missing = set(REQUIRED_AMB_COLS) - set(df_amb.columns) + if missing: + raise ValueError(f"df_amb missing required columns: {sorted(missing)}; have={list(df_amb.columns)}") + + # Coerce dtypes (lightweight, no extra normalization logic) + df_amb["datetime"] = pd.to_datetime(df_amb["datetime"], errors="coerce") + for c in ("sat","recv","action","sig","reasons"): + df_amb[c] = df_amb[c].astype(str) + + # --- Now apply the same CLI/time-window filters --- + if need_amb and not df_amb.empty: + df_amb = filter_ambiguity_resets( + df_amb, + receivers=recv_list, + sats=args.sat, + start_dt=start_dt, + end_dt=end_dt, + ) + + # after building df_amb (parse_ambiguity_resets + filter_ambiguity_resets) + dups = df_amb.columns[df_amb.columns.duplicated()].tolist() + if dups: + raise RuntimeError(f"df_amb has duplicate columns: {dups}") + + if "action" not in df_amb.columns: + raise RuntimeError(f"df_amb missing 'action' column; cols={list(df_amb.columns)}") + + # Y-field & outlier trim + yfield = args.residual + if args.max_abs is not None: + df = df[df[yfield].abs() <= args.max_abs] + + # Compute normalised residual columns + sigma = pd.to_numeric(df["sigma"], errors="coerce") + sigma_nz = sigma.mask(sigma == 0) # -> NaN where zero to avoid divide-by-zero + df["norm_prefit"] = pd.to_numeric(df["prefit"], errors="coerce") / sigma_nz + df["norm_postfit"] = pd.to_numeric(df["postfit"], errors="coerce") / sigma_nz + + # Keep a non-decimated copy for precise y lookups of MEAS error markers + df_lookup = df.copy() + + # Decimate for plotting + df = decimate_per_pair(df, args.decimate) + + # Output filenames root - A.trace + B.sum -> base_root = "A_B"; final base = "A_B_postfit" + # Use output_basename_paths to avoid duplicating forward+smoothed in filename + + # Smart filename generation to avoid "File name too long" errors + if len(output_basename_paths) == 1: + # Single file: use the name + base_root = output_basename_paths[0].stem + else: + # Multiple files: find common prefix or use generic name with count + stems = [p.stem for p in output_basename_paths] + + # Try to find common prefix (e.g., "network-" for network files) + if len(stems) > 0: + # Find longest common prefix + prefix = stems[0] + for stem in stems[1:]: + while stem[:len(prefix)] != prefix and prefix: + prefix = prefix[:-1] + + # Use common prefix if meaningful (at least 5 chars), otherwise use first file + if len(prefix) >= 5: + # Remove trailing dashes/underscores + prefix = prefix.rstrip('-_') + base_root = f"{prefix}_{len(stems)}_files" + else: + base_root = f"{stems[0]}_{len(stems)}_files" + else: + base_root = "network" + + chosen_base = args.basename if args.basename else f"{base_root}_{args.residual}" + chosen_base = slugify(chosen_base) + out_dir = Path(args.out_dir).expanduser().resolve() if args.out_dir else Path.cwd() + out_dir.mkdir(parents=True, exist_ok=True) + + # --- canonical naming roots --- + base_root = slugify(args.basename) if getattr(args, "basename", None) else "output" + base = str((out_dir / base_root)) + variant_suffix = "" + if getattr(args, "stats_matrix_weighted", False): + variant_suffix = "_w" + try: + logger.debug("base=%s variant_suffix=%s", base, variant_suffix) + except Exception: + pass + base = str(out_dir / chosen_base) + + # Graphics acceleration + use_webgl = args.webgl + + # Build which variants we will plot (raw + optional normalised) + plot_variants = [("raw", yfield, "", "residual")] + if args.plot_normalised_res: + plot_variants.append(("norm", f"norm_{yfield}", "_norm", "normalised residual (res/σ)")) + + any_outputs_global = False + + for variant_tag, variant_yfield, variant_suffix, ytitle in plot_variants: + + lookup_cache = build_lookup_cache(df_lookup, variant_yfield) + + meas_map: Dict[str, List[Tuple[str, str]]] = {"code": [], "phase": []} + outputs_variant: List[str] = [] + + for meas_name, short in [("CODE_MEAS", "code"), ("PHAS_MEAS", "phase")]: + df_m = df[df["meas"] == meas_name] + if df_m.empty: + continue + + is_phase = (meas_name == "PHAS_MEAS") + + if args.split_per_sat: + for sat in sorted(df_m["sat"].unique(), key=_sat_sort_key): + df_ms = df_m[df_m["sat"] == sat] + if df_ms.empty: + continue + fig = make_plot( + df_ms, variant_yfield, + title=f"{sat} {short.upper()} {ytitle}", + use_webgl=use_webgl, + df_large=df_large, + context={"sat": sat, "meas_type": meas_name}, + hover_unified=args.hover_unified, + df_lookup=df_lookup, + lookup_cache=lookup_cache, + show_stats=args.show_stats_table, + df_amb=(df_amb if (args.mark_amb_resets and is_phase) else pd.DataFrame()), + ) + out_html = build_out_path( + base, variant_suffix, short, + split="sat", + key=sat, + tag="residual", + ext="html", + ) + ensure_parent(out_html) + fig.write_html(out_html, include_plotlyjs="cdn", validate=False, config={"scrollZoom": True}) + logger.info("Wrote: %s", out_html) + outputs_variant.append(out_html) + meas_map[short].append((sat, out_html)) + + elif args.split_per_recv: + for recv in sorted(df_m["recv"].unique(), key=_recv_sort_key): + df_mr = df_m[df_m["recv"] == recv] + if df_mr.empty: + continue + + # right before the make_plot() call + is_phase = (meas_name == "PHAS_MEAS") # make this exact comparison + df_amb_for_plot = df_amb if (args.mark_amb_resets and is_phase) else pd.DataFrame() + + fig = make_plot( + df=df_mr, + residual_field=variant_yfield, + title=f"{recv} {short.upper()} {ytitle}", + use_webgl=use_webgl, + df_large=df_large, + context={"recv": recv, "meas_type": meas_name}, + hover_unified=args.hover_unified, + df_lookup=df_lookup, + lookup_cache=lookup_cache, + show_stats=args.show_stats_table, + df_amb=df_amb_for_plot, # <- pass the guarded DF + ) + out_html = build_out_path( + base, variant_suffix, short, + split="recv", + key=recv, + tag="residual", + ext="html", + ) + ensure_parent(out_html) + fig.write_html(out_html, include_plotlyjs="cdn", validate=False, config={"scrollZoom": True}) + logger.info("Wrote: %s", out_html) + outputs_variant.append(out_html) + meas_map[short].append((recv, out_html)) + + else: + fig = make_plot( + df_m, variant_yfield, + title=f"GNSS {short.upper()} {ytitle}", + use_webgl=use_webgl, + df_large=df_large, + context={"meas_type": meas_name}, + hover_unified=args.hover_unified, + df_lookup=df_lookup, + lookup_cache=lookup_cache, + show_stats=args.show_stats_table, + df_amb=(df_amb if (args.mark_amb_resets and is_phase) else pd.DataFrame()), + ) + + out_html = build_out_path(base, variant_suffix, short, tag="residual", ext="html") + ensure_parent(out_html) + fig.write_html(out_html, include_plotlyjs="cdn", validate=False, config={"scrollZoom": True}) + outputs_variant.append(out_html) + + # Build an index page for this variant if split mode was used + if (args.split_per_sat or args.split_per_recv) and (meas_map["code"] or meas_map["phase"]): + mode_tag = "sat" if args.split_per_sat else "recv" + index_path = Path(f"{base}{variant_suffix}_index_{mode_tag}.html") + # Build trace file info showing which files were used for what + residual_file_names = ", ".join(p.name for p in residual_paths) + if residual_paths != forward_paths: + trace_files_info = f"Residuals: {residual_file_names} | Amb/Errors: {', '.join(p.name for p in forward_paths)}" + else: + trace_files_info = residual_file_names + + meta = { + "Trace files": trace_files_info, + "Residual type": variant_yfield, + "Split mode": "per-sat" if args.split_per_sat else "per-recv", + "Receiver filter(s)": ", ".join(recv_list) if recv_list else "", + "Sat filter(s)": ", ".join(args.sat) if args.sat else "", + "Label regex": args.label_regex or "", + "Start": start_dt.isoformat() if start_dt is not None else "", + "End (exclusive)": end_dt.isoformat() if end_dt is not None else "", + "Decimate (N)": str(args.decimate), + "WebGL": "yes" if use_webgl else "no", + "Hover mode": "unified" if args.hover_unified else "closest", + "Large errors": "on" if args.mark_large_errors else "off", + "Variant": variant_tag, + } + write_index_html( + index_path, + base_title=Path(base + variant_suffix).name, + meas_map=meas_map, + meta=meta, + item_kind="sat" if args.split_per_sat else "recv", + ) + logger.info(f"Wrote: {index_path}") + + # Print this variant’s outputs (once), then proceed + if outputs_variant: + any_outputs_global = True + logger.info("Wrote %d plot files for variant '%s'.", len(outputs_variant), variant_tag) + + # ---- Receiver × Satellite heatmaps (weighted OR unweighted, not both) ---- + if args.stats_matrix or args.stats_matrix_weighted: + # choose data source for stats (full-res recommended) + mat_source = df_lookup.copy() if df_lookup is not None else df.copy() + + # Single mode: weighted if --stats-matrix-weighted, else unweighted + weighted = bool(args.stats_matrix_weighted) + + annotate_mode = "all" if getattr(args, "annotate_stats_matrix", False) else "none" + + # loop per measurement type so CODE and PHASE get separate heatmaps + for meas_name, dtype in [("CODE_MEAS", "code"), ("PHAS_MEAS", "phase")]: + ms = mat_source[mat_source["meas"] == meas_name].copy() + if ms.empty: + logger.info("No samples for %s; skipping stats-matrix.", dtype) + continue + + stats_mats = build_recv_sat_stats(ms, variant_yfield, weighted=weighted) # {"mean","std","rms"} + + # ---- Build counts matrix aligned to stats_mats ---- + yv = pd.to_numeric(ms[variant_yfield], errors="coerce") + counts = ( + ms.loc[yv.notna()] + .groupby(["recv", "sat"], sort=False) + .size() + .unstack(fill_value=0) + ) + + # Add ALL_SAT (per receiver) as first column + counts["ALL_SAT"] = counts.sum(axis=1) + + # Add ALL_RECV (per satellite) as first row + all_recv_row = pd.DataFrame([counts.sum(axis=0)], index=["ALL_RECV"]) + counts_full = pd.concat([all_recv_row, counts], axis=0) + + # Add top-left grand total + counts_full.loc["ALL_RECV", "ALL_SAT"] = int(counts.values.sum()) + + # Ensure ordering matches stats matrices (ALL_RECV top, ALL_SAT left) + def _align_counts(mat: pd.DataFrame) -> pd.DataFrame: + if mat is None or mat.empty: + return counts_full + return counts_full.reindex(index=mat.index, columns=mat.columns) + + # ---------- Titles ---------- + wtxt = "weighted" if weighted else "unweighted" + titles = { + "mean": f"{dtype.upper()} — Receiver × Satellite — Mean ({wtxt}) — {ytitle}", + "std": f"{dtype.upper()} — Receiver × Satellite — Std Dev ({wtxt}) — {ytitle}", + "rms": f"{dtype.upper()} — Receiver × Satellite — RMS ({wtxt}) — {ytitle}", + } + + # ---------- Mean (diverging, centered at 0.0) ---------- + mat = stats_mats.get("mean") + if mat is not None and not mat.empty: + out_html = build_out_path(base, variant_suffix, f"stats_matrix_mean_{dtype}") + ensure_parent(out_html) + write_heatmap_html( + mat, titles["mean"], out_html, + colorscale="RdBu", reversescale=True, + zmid=0.0, zmin=None, zmax=None, + counts=_align_counts(mat).astype(float), + row_sort="alpha", + sat_sort="natural", + annotate=annotate_mode, + ) + logger.info("Wrote: %s", out_html) + else: + logger.info("Mean matrix empty for %s; skipping.", dtype) + + # ---------- Std & RMS (sequential from 0) ---------- + for metric in ("std", "rms"): + mat = stats_mats.get(metric) + if mat is None or mat.empty: + logger.info("%s matrix empty for %s; skipping.", metric.upper(), dtype) + continue + + short = f"stats_matrix_{metric}_{dtype}" + + # HTML + out_html = build_out_path(base, variant_suffix, short) + ensure_parent(out_html) + zmax = float(np.nanmax(mat.values)) if mat.size else None + write_heatmap_html( + mat, titles[metric], out_html, + colorscale="Blues", reversescale=False, + zmid=None, zmin=0.0, zmax=zmax, + counts=_align_counts(mat).astype(float), + row_sort="alpha", + sat_sort="natural", + annotate=annotate_mode, + ) + logger.info("Wrote: %s", out_html) + + # CSV + out_csv = build_out_path(base, variant_suffix, short, ext="csv") + ensure_parent(out_csv) + mat.to_csv(out_csv) + logger.info("Wrote: %s", out_csv) + + # Shared prefix (same as other outputs) + prefix = base + + # Ambiguity reset plots (reasons + unique resets) + if args.ambiguity_counts: + if args.split_per_recv: + plot_ambiguity_reason_counts(df_amb, split="recv", base=base, variant_suffix=variant_suffix) + elif args.split_per_sat: + plot_ambiguity_reason_counts(df_amb, split="sat", base=base, variant_suffix=variant_suffix) + else: + plot_ambiguity_reason_counts(df_amb, split="combined", base=base, variant_suffix=variant_suffix) + + if args.ambiguity_totals: + if args.split_per_recv: + plot_ambiguity_reason_totals(df_amb, split="recv", + orientation=args.amb_totals_orient, + top_n=args.amb_totals_topn, + base=base, variant_suffix=variant_suffix) + elif args.split_per_sat: + plot_ambiguity_reason_totals(df_amb, split="sat", + orientation=args.amb_totals_orient, + top_n=args.amb_totals_topn, + base=base, variant_suffix=variant_suffix) + else: + plot_ambiguity_reason_totals(df_amb, split="combined", + orientation=args.amb_totals_orient, + top_n=args.amb_totals_topn, + base=base, variant_suffix=variant_suffix) + + if not any_outputs_global: + logger.warning("No residuals matched your filters.") + +if __name__ == "__main__": + main() diff --git a/scripts/plotting/obs_code_plot.py b/scripts/plotting/obs_code_plot.py index 209b28126..eacf04a28 100644 --- a/scripts/plotting/obs_code_plot.py +++ b/scripts/plotting/obs_code_plot.py @@ -211,7 +211,7 @@ def plot_code_vs_time(time_data, code_data, station, year, doy, sat_nums, start_ # Create and save figure for each obs. code # If ALL option is chosen for obs_codes AND sat_nums, plot all: - if (codes == 'ALL') & (sat_nums == 'ALL'): + if (codes == "ALL") and (sat_nums == "ALL"): # Get all satellites from Rinex file: sat_list = [x for x in obs.sv.values if 'G' in x] sat_nums = ','.join(sat_list) @@ -231,7 +231,7 @@ def plot_code_vs_time(time_data, code_data, station, year, doy, sat_nums, start_ ) # If ALL codes but not all satellites (SV): - elif (codes == 'ALL') & (sat_nums != 'ALL'): + elif (codes == "ALL") and (sat_nums != "ALL"): # Run through all observables (data variables) in Rinex file for code in list(obs.data_vars.keys()): plot_code_vs_time( @@ -248,7 +248,7 @@ def plot_code_vs_time(time_data, code_data, station, year, doy, sat_nums, start_ ) # If ALL satellites (SV) but not all codes: - elif (codes != 'ALL') & (sat_nums == 'ALL'): + elif (codes != "ALL") and (sat_nums == "ALL"): # Get all satellites from Rinex file: sat_list = [x for x in obs.sv.values if 'G' in x] sat_nums = ','.join(sat_list) diff --git a/scripts/plotting/ztd_plot.py b/scripts/plotting/ztd_plot.py index 340c89b8b..5b6fb59d1 100644 --- a/scripts/plotting/ztd_plot.py +++ b/scripts/plotting/ztd_plot.py @@ -49,7 +49,7 @@ def bernese_timeseries(time_series_ztd_file): result = pd.read_csv( time_series_ztd_file, - delim_whitespace=True, + sep="\\s+", # delim_whitespace is deprecated header=None, names=names, ) @@ -170,7 +170,7 @@ def plot_station_2(station_code, year, doy_pair, output_folder, smooth_input_fol & (df_pea["datetime"] <= (datetime(year, 1, 1) + timedelta(days=end_doy))) ] df_pea["epoch"] = list(range(len(df_pea))) - # df_bern = pd.read_csv(f'{ROOT}/bernese_time_series/{station_code}_time_series.ztd',delim_whitespace=True,header=None) + # df_bern = pd.read_csv(f'{ROOT}/bernese_time_series/{station_code}_time_series.ztd',sep="\\s+",header=None) bucket_name = "jamiacstest" s3_client = boto3.client("s3") diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 25c999fb4..52832393e 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -13,12 +13,10 @@ pandas>=1.1.0 #MongoDash required version matplotlib>=3.5.2 ruamel.yaml>=0.17.21 boto3 -# ipython==7.5.0 -# notebook>=5.3 -# ipywidgets>=7.2 -pymongo>=3.7.2 +pymongo>=3.7.2 pytest>=7.1.2 scipy>=1.7.3 statsmodels>=0.13.2 hatanaka>=2.8.1 -gnssanalysis>=0.0.28 +gnssanalysis>=0.0.57 +beautifulsoup4 diff --git a/scripts/ssrMonitoring/README.md b/scripts/ssrMonitoring/README.md index 279084a75..3118cffe7 100644 --- a/scripts/ssrMonitoring/README.md +++ b/scripts/ssrMonitoring/README.md @@ -32,9 +32,12 @@ aws configure This script is master script that calls following individual scripts to automatically download required real-time products and start PEA instances to record and decode SSR streams, compare their orbits and clocks against IGS rapid products, and upload output files to an AWS S3 bucket. ```bash -python auto_record_ssr_streams.py --job-dir /path/to/job/folder/ --product-dir /path/to/ginan/products/ --template-config /path/to/ginan/debugConfigs/record_ssr_stream.yaml --ssr-streams 'SSRA00BKG0, SSRA00GFZ0, SSRA00WHU0, SSRA02IGS0, SSRA03IGS0' --aws-profile aws-credentials-profile --s3-bucket target-s3-bucket --s3-root-dir target/s3/prefix --cull-file-types '.rtcm, .rnx, .json' +python auto_record_ssr_streams.py --job-dir /path/to/job/folder/ --product-dir /path/to/ginan/products/ --pea-dir /data/acs/ginan/bin/ --template-config /path/to/ginan/debugConfigs/record_ssr_stream.yaml --ntrip-cred-file-path ntrip_cred.json --ssr-streams 'SSRA00BKG0, SSRA00GFZ0, SSRA00WHU0, SSRA02IGS0, SSRA03IGS0' --aws-profile aws-credentials-profile --s3-bucket target-s3-bucket --s3-root-dir target/s3/prefix --cull-file-types '.rtcm, .rnx, .json' ``` +The NTRIP credential file 'ntrip_cred.json' should be of a JSON format as below: +{"username": "your_ntrip_username", "password": "your_ntrip_password"} + ## Use of individual scripts # `kill_pids.py` @@ -58,7 +61,7 @@ python download_rt_products.py --product-dir /path/to/ginan/products/ --interval This script is to start a PEA instance to record and decode a SSR stream in real-time. The PEA instance will run infinitely once it is started. ```bash -python record_ssr_stream.py --template-config /path/to/ginan/debugConfigs/record_ssr_stream.yaml --job-dir /path/to/job/folder/ --product-dir /path/to/ginan/products/ --ssr-mountpoint SSRA00BKG0 --rotation-period 86400 --interval 1 +python record_ssr_stream.py --pea-dir /data/acs/ginan/bin/ --template-config /path/to/ginan/debugConfigs/record_ssr_stream.yaml --job-dir /path/to/job/folder/ --product-dir /path/to/ginan/products/ --ntrip-username ntrip-username --ntrip-password ntrip-password --ssr-mountpoint SSRA00BKG0 --rotation-period 86400 --interval 1 ``` # `analyse_orbit_clock.py` @@ -67,7 +70,7 @@ This script is to compare orbits and clocks in SP3 and CLK files against corresp Post processing mode: ```bash -python analyse_orbit_clock.py --job-dir /path/to/job/folder/ --ref-dir /path/to/ginan/products/ --start-yrdoy 2024204 --end-yrdoy 2024215 --session-len 1 --clk-norm-types 'epoch, daily' --rel-output-dir gnssanalysis +python analyse_orbit_clock.py --job-dir /path/to/job/folder/ --ref-dir /path/to/ginan/products/ --sub-jobs 'sub-job-1, sub-job-2' --start-yrdoy 2024204 --end-yrdoy 2024215 --session-len 1 --clk-norm-types 'epoch, daily' --rel-output-dir gnssanalysis ``` Real-time mode: diff --git a/scripts/ssrMonitoring/analyse_orbit_clock.py b/scripts/ssrMonitoring/analyse_orbit_clock.py index af2528d17..6cc676f49 100644 --- a/scripts/ssrMonitoring/analyse_orbit_clock.py +++ b/scripts/ssrMonitoring/analyse_orbit_clock.py @@ -10,308 +10,490 @@ It can run in post-processing (finite) mode or real-time (infinite) mode. """ -import os -import sys +import itertools +import logging import time +from datetime import date, datetime, timedelta +from pathlib import Path +from typing import Any, Union + import click -import logging import matplotlib -import matplotlib.font_manager +import matplotlib.dates as mdates +import matplotlib.pyplot as plt import numpy as np +import numpy.typing as npt import pandas as pd -import gnssanalysis as ga -import matplotlib.pyplot as plt -import matplotlib.dates as mdates -from pathlib import Path from matplotlib.figure import Figure -from datetime import date, datetime, timedelta -parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, parent_dir) +import gnssanalysis as ga + -from auto_download_PPP import generate_sampling_rate +sys_meta = { + "G": {"name": "GPS", "max_sats": 32}, + "R": {"name": "GLO", "max_sats": 27}, + "E": {"name": "GAL", "max_sats": 36}, + "C": {"name": "BDS", "max_sats": 62}, +} -def set_up_matplotlib( - font: str, - colormap: str, -) -> None: +def str_to_list(input: str) -> list[str]: """ - Set up font and colormap for matplotlib + Split a comma separated string to a list of sub strings - :param str font: font name - :param str colormap: colormap name - :return None + :param str input: String to split + :return list[str]: A list of sub strings from the input string """ - try: - matplotlib.rcParams["font.family"] = font - except: - logging.exception( - f"Preferred font '{font}' for plotting not available, using default font {matplotlib.rcParams['font.family']}" - ) - logging.debug( - f"Available fonts: {', '.join([ft.name for ft in matplotlib.font_manager.fontManager.ttflist])}" + if input: + output = input.replace(" ", "").split(",") + else: + output = [] + + return output + + +def filter_svs_list(svs: list[str], dataframe: pd.DataFrame) -> list[str]: + """ + Filter out the unavailable satellites from the given satellite list + + :param list[str] svs: Satellite list to filter + :param pd.DataFrame dataframe: The Pandas DataFrame containing orbit/clock differences or statistics + :return list[str]: A new satellite list containing only available satellites + """ + svs_new = sorted(list(dataframe.index.get_level_values("Satellite").intersection(svs))) + if not svs_new: + logging.error("No data found for given satellites") + return svs_new + + +def download_ref_products( + ref_dir: Path, + ref_prefix: str, + product_type: str, + start_epoch: datetime, + end_epoch: datetime, + sampling_rate: dict = {}, + if_file_present: str = "dont_replace", +) -> list[Path]: + """ + Download reference products for orbits and clocks comparison + + :param Path ref_dir: Directory to download reference products to + :param str ref_prefix: Prefix of reference products, e.g. IGS0OPSRAP + :param str product_type: Product type to download, SP3 or CLK + :param datetime start_epoch: Start of epoch time + :param datetime end_epoch: End of epoch time + :param dict sampling_rate: Dictionary of sampling rates for SP3 and CLK products in seconds, + will be determined and saved based on the prefix if not specified + :return list[Path]: Return list of paths of downloaded files + """ + logging.info("Downloading reference products ...") + + analysis_center = ref_prefix[0:3] + version = ref_prefix[3] + project_type = ref_prefix[4:7] + solution_type = ref_prefix[7:10] + + days = 2 if solution_type == "ULT" else 1 + + if product_type not in sampling_rate.keys() or not sampling_rate[product_type]: + sampling_rate[product_type] = ga.gn_download.generate_sampling_rate( + file_ext=product_type, analysis_center=analysis_center, solution_type=solution_type ) + download_filepaths = [] try: - matplotlib.rcParams["image.cmap"] = colormap - except: - logging.exception( - f"Preferred colormap '{colormap}' for plotting not available, using default colormap {matplotlib.rcParams['image.cmap']}" + download_filepaths = ga.gn_download.download_product_from_cddis( + download_dir=ref_dir, + start_epoch=start_epoch, + end_epoch=(end_epoch + timedelta(days - 1)), + file_ext=product_type, + long_filename=True, + analysis_center=analysis_center, + version=version, + project_type=project_type, + solution_type=solution_type, + timespan=timedelta(days), + sampling_rate=sampling_rate[product_type], + if_file_present=if_file_present, ) - logging.debug(f"Available colormaps: {', '.join(plt.colormaps())}") + logging.info("Reference products downloaded\n") + except Exception as error: + logging.error(f"Fail to download products: {error}\n") + return download_filepaths -def sp3_diff( - ref_sp3_file: Path, - job_sp3_file: Path, -) -> pd.DataFrame: + +def search_input_file_case_insensitively(input_path: Path) -> Union[Path, None]: """ - Compare two SP3 files to calculate orbit and clock differences. The orbit differences will be represented - in both X/Y/Z ECEF frame and R/A/C orbit frame, and the clock differences will NOT be normalised. + Search an input file case-insensitively - :param Path ref_sp3_file: Path of the reference SP3 file - :param Path job_sp3_file: Path of the testing SP3 file - :return pd.DataFrame: The Pandas DataFrame containing orbit and clock differences + :param Path input_path: Path of the input file to search + :return Union[Path, None]: None if the input file does not exist otherwise the real path which matches the input file case-insensitively """ - tic = time.time() - ref_sp3_df = ga.gn_io.sp3.read_sp3(str(ref_sp3_file)) - job_sp3_df = ga.gn_io.sp3.read_sp3(str(job_sp3_file)) - toc = time.time() - logging.debug(f"Took {toc-tic} seconds to read SP3 files") - - # Select rows with matching indices and calculate ECEF differences - common_indices = ref_sp3_df.index.intersection( - job_sp3_df.index - ) # get common indices - diff_EST_df = ( - job_sp3_df.loc[common_indices, "EST"] - ref_sp3_df.loc[common_indices, "EST"] - ) + matched_input_path = None - # The index is in J2000 seconds but python datetimes are a little easier to read - diff_EST_df.index = pd.MultiIndex.from_tuples( - ( - (idx[0] + ga.gn_const.J2000_ORIGIN, idx[1]) - for idx in diff_EST_df.index.values - ) - ) + for file in input_path.parent.iterdir(): + if file.is_file() and file.name.lower() == input_path.name.lower(): + matched_input_path = file + break + + return matched_input_path - # Rename the indices - diff_EST_df.index = diff_EST_df.index.set_names(["Epoch", "Satellite"]) - # Extract clocks and change the units from ms to ns - diff_CLK_df = diff_EST_df["CLK"].to_frame(name="CLK") * 1e3 +def generate_input_paths( + job_dir: Path, + ref_dir: Path, + sub_job_name: str, + ref_prefix: str, + yrdoys: list[str], + sampling_rate: dict, + product_type: str, + orb_ref_frame: str = "ECF", +) -> list[list[Path]]: + """ + Generate a list of input path pairs of test file (file to analyse) and reference product + + :param Path job_dir: Directory where data is processed + :param Path ref_dir: Directory where reference products are placed + :param str sub_job_name: Sub job to process, which should be the name of the parent folder (and the prefix) of test files to analyse + :param str ref_prefix: Prefix of reference products, e.g. IGS0OPSRAP + :param list[str] yrdoys: A list of day-of-year strings to analyse data for in the format of 'YYYYDDD' + :param dict sampling_rate: Dictionary of sampling rates for SP3 and CLK products in seconds + :param str product_type: Type of files/products to analyse, i.e. 'SP3' or 'CLK' + :return list[list[str]]: A list of input path pairs (list) of test file and reference product + """ + input_paths = [] + + file_ext = product_type + if product_type == "SP3" and orb_ref_frame == "ECI": + file_ext = file_ext + "i" + content_type = "ORB" if product_type == "SP3" else "CLK" if product_type == "CLK" else None + if not content_type: + return input_paths + solution_type = ref_prefix[7:10] + timespan = "02D" if solution_type == "ULT" else "01D" + + for yrdoy in yrdoys: + job_file_path = job_dir / sub_job_name / f"{sub_job_name}_{yrdoy}0000_{content_type}.{file_ext}" + ref_file_path = ( + ref_dir / f"{ref_prefix}_{yrdoy}0000_{timespan}_{sampling_rate[product_type]}_{content_type}.{file_ext}" + ) - # Drop the clocks and then change the units from km to m - diff_XYZ_df = diff_EST_df.drop(columns=["CLK"]) * 1e3 + if not ref_file_path.exists() and product_type == "SP3" and solution_type == "ULT": + date = datetime.strptime(yrdoy, "%Y%j") + earlist = date - timedelta(hours=42) # earliest possible ultra rapid file containing overlapping data - # RAC difference - diff_RAC_df = ga.gn_io.sp3.diff_sp3_rac( - ref_sp3_df, job_sp3_df, hlm_mode=None - ) # TODO: hlm_mode + while date > earlist: + logging.warning( + f"'{ref_file_path.name}' not found, attempting an earlier ultra-rapid product for its predicted part" + ) - # Change the units from km to m (read_sp3 and diff_sp3_rac will result in a dataframe in sp3 units (km)) - diff_RAC_df = diff_RAC_df * 1e3 + date -= timedelta(hours=6) + download_ref_products(ref_dir, ref_prefix, product_type, date, date + timedelta(days=1), sampling_rate) - # The index is in J2000 seconds but python datetimes are a little easier to read - diff_RAC_df.index = pd.MultiIndex.from_tuples( - ( - (idx[0] + ga.gn_const.J2000_ORIGIN, idx[1]) - for idx in diff_RAC_df.index.values - ) - ) + yrdoy_hr = date.strftime("%Y%j%H") + ref_file_path = ( + ref_dir + / f"{ref_prefix}_{yrdoy_hr}00_{timespan}_{sampling_rate[product_type]}_{content_type}.{file_ext}" + ) - # Name the indices - diff_RAC_df.index = diff_RAC_df.index.set_names(["Epoch", "Satellite"]) + if ref_file_path.exists(): + break - # Drop the not-particularly needed 'EST_RAC' multi-index level - diff_RAC_df.columns = diff_RAC_df.columns.droplevel(0) + job_file_path = search_input_file_case_insensitively(job_file_path) + ref_file_path = search_input_file_case_insensitively(ref_file_path) - diff_df = diff_XYZ_df.join(diff_RAC_df) - diff_df["3D-Total"] = diff_XYZ_df.pow(2).sum(axis=1, min_count=3).pow(0.5) - diff_df["Clock"] = diff_CLK_df + if job_file_path and ref_file_path: + input_paths.append([ref_file_path, job_file_path]) - return diff_df + return input_paths -def clk_diff( - ref_clk_file: Path, - job_clk_file: Path, - norm_types: list[str], +def compare_sp3_files( + input_paths: list[list[Path]], + output_path: Path, + svs: Union[dict, list[str]], + orb_ref_frame: str, + orb_hlm_mode: str, + epochwise_hlm: bool, + clk_norm_types: list[str], ) -> pd.DataFrame: """ - Compare two CLK files to calculate clock differences with common mode removed (if specified) - based on the chosen normalisations. + Compare SP3 file pairs and to calculate orbit and clock differences. The orbit differences will be represented + in both X/Y/Z ECEF frame and R/A/C orbit frame, and the clock differences will NOT be normalised. - :param Path ref_clk_file: Path of the reference CLK file - :param Path job_clk_file: Path of the testing CLK file - :param norm_types list[str]: Normalizations to apply. Available options include 'epoch', 'daily', 'sv', - any satellite PRN, or any combination of them, defaults to None - :return pd.DataFrame: The Pandas DataFrame containing clock differences + :param list[list[Path]] input_paths: A list of input path pairs of test file and reference product + :param Path output_path: Path of the output file to write results to in CSV format + :param Union[dict, list[str]] svs: List(s) of satellites to process + :param str orb_ref_frame: Reference frame of input orbits. Should either be 'ECF' or 'ECI' + :param str orb_hlm_mode: Helmert transformation to apply to orbits. Can be None, 'ECF', or 'ECI' + :param bool epochwise_hlm: Epochwise Helmert transformation + :param list[str] clk_norm_types: Normalizations to apply to clocks. Available options include + 'epoch', 'daily', 'sv', any satellite PRN, or any combination of them + :return pd.DataFrame: The Pandas DataFrame containing orbit and clock differences """ - tic = time.time() - ref_clk_df = ga.gn_io.clk.read_clk(ref_clk_file) - job_clk_df = ga.gn_io.clk.read_clk(job_clk_file) - toc = time.time() - logging.debug(f"Took {toc-tic} seconds to read CLK files") + svs = list(itertools.chain.from_iterable(svs.values())) - diff_CLK_df = ga.gn_diffaux.compare_clk( - job_clk_df, ref_clk_df, norm_types=norm_types - ) - diff_CLK_df = diff_CLK_df.stack() # compare_clk() returns unstacked dataframe + sp3_diff_df = pd.DataFrame() - # Change the units from s to ns (read_clk and compare_clk will result in a dataframe in clk units (s)) - diff_CLK_df = diff_CLK_df.to_frame(name="Clock") * 1e9 + for input in input_paths: + ref_sp3_file = input[0] + job_sp3_file = input[1] - # The index is in J2000 seconds but python datetimes are a little easier to read - diff_CLK_df.index = pd.MultiIndex.from_tuples( - ( - (idx[0] + ga.gn_const.J2000_ORIGIN, idx[1]) - for idx in diff_CLK_df.index.values - ) - ) + logging.info(f"-- '{job_sp3_file}' vs '{ref_sp3_file}'") - # Name the indices - diff_CLK_df.index = diff_CLK_df.index.set_names(["Epoch", "Satellite"]) + diff_df = pd.DataFrame() + try: + diff_df = ga.gn_diffaux.sp3_difference( + ref_sp3_file, job_sp3_file, svs, orb_ref_frame, orb_hlm_mode, epochwise_hlm, clk_norm_types + ) + except Exception as error: + logging.error(f"Error with sp3_difference(): {error}") + continue + + sp3_diff_df = pd.concat([sp3_diff_df, diff_df]) - return diff_CLK_df + sp3_diff_df.to_csv(output_path) + return sp3_diff_df -def sp3_stats( - res: pd.DataFrame, + +def compare_clk_files( + input_paths: list[list[Path]], output_path: Path, svs: Union[dict, list[str]], clk_norm_types: list[str] ) -> pd.DataFrame: """ - Compute statistics of SP3 differences in a Pandas DataFrame. + Compare CLK file pairs and to calculate clock differences with common mode removed (if specified) + based on the chosen normalisations. - :param pd.DataFrame res: The Pandas DataFrame containing SP3 differences - :return pd.DataFrame: The Pandas DataFrame containing statistics of SP3 differences + :param list[list[Path]] input_paths: A list of input path pairs of test file and reference product + :param Path output_path: Path of the output file to write results to in CSV format + :param Union[dict, list[str]] svs: List(s) of satellites to process + :param list[str] clk_norm_types: Normalizations to apply to clocks. Available options include + 'epoch', 'daily', 'sv', any satellite PRN, or any combination of them + :return pd.DataFrame: The Pandas DataFrame containing clock differences """ - df = res[["Radial", "Along-track", "Cross-track", "3D-Total", "Clock"]] - stats = df.describe(percentiles=[0.25, 0.50, 0.75, 0.90, 0.95]) - stats.loc["rms"] = df.pow(2).mean().pow(0.5) + svs = list(itertools.chain.from_iterable(svs.values())) - stats.index = pd.MultiIndex.from_tuples( - (("All", idx) for idx in stats.index.values) - ) - stats.index = stats.index.set_names(["Satellite", "Stats"]) + clk_diff_df = pd.DataFrame() - sat_index = res.index.get_level_values("Satellite") - svs = sorted(list(sat_index.unique())) + for input in input_paths: + ref_clk_file = input[0] + job_clk_file = input[1] - for sat in svs: - res_sat = res[sat_index == sat] + logging.info(f"-- '{job_clk_file}' vs '{ref_clk_file}'") - df = res_sat[["Radial", "Along-track", "Cross-track", "3D-Total", "Clock"]] - stats_sat = df.describe(percentiles=[0.25, 0.50, 0.75, 0.90, 0.95]) - stats_sat.loc["rms"] = df.pow(2).mean().pow(0.5) + diff_df = pd.DataFrame() + try: + diff_df = ga.gn_diffaux.clk_difference(ref_clk_file, job_clk_file, svs, clk_norm_types) + except Exception as error: + logging.error(f"Error with clk_difference(): {error}") + continue - stats_sat.index = pd.MultiIndex.from_tuples( - ((sat, idx) for idx in stats_sat.index.values) - ) - stats_sat.index = stats_sat.index.set_names(["Satellite", "Stats"]) + clk_diff_df = pd.concat([clk_diff_df, diff_df]) - stats = pd.concat([stats, stats_sat]) + clk_diff_df.to_csv(output_path) - return stats + return clk_diff_df -def clk_stats( - res: pd.DataFrame, -) -> pd.DataFrame: +def compute_stats(diff_df: pd.DataFrame, output_path: Path) -> pd.DataFrame: """ - Compute statistics of CLK differences in a Pandas DataFrame. + Compute statistics of orbit and/or clock differences, namely 'count', 'mean', 'std', 'rms', + 'min', '5%', '10%', '50%', '75%', '90%', '95%' percentiles, and 'max'. - :param pd.DataFrame res: The Pandas DataFrame containing CLK differences - :return pd.DataFrame: The Pandas DataFrame containing statistics of CLK differences + :param pd.DataFrame diff_df: The Pandas DataFrame containing orbit and/or clock differences + :param Path output_path: Path of the output file to write results to in CSV format + :return pd.DataFrame: The Pandas DataFrame containing statistics of orbit and/or clock differences """ - stats = res.describe(percentiles=[0.25, 0.50, 0.75, 0.90, 0.95]) - stats.loc["rms"] = res.pow(2).mean().pow(0.5) + stats_df = ga.gn_diffaux.difference_statistics(diff_df) + stats_df.to_csv(output_path) - stats.index = pd.MultiIndex.from_tuples( - (("All", idx) for idx in stats.index.values) - ) - stats.index = stats.index.set_names(["Satellite", "Stats"]) + return stats_df - sat_index = res.index.get_level_values("Satellite") - svs = sorted(list(sat_index.unique())) - for sat in svs: - res_sat = res[sat_index == sat] +def set_up_matplotlib(font: str, colormap: str) -> None: + """ + Set up font and colormap for matplotlib - stats_sat = res_sat.describe(percentiles=[0.25, 0.50, 0.75, 0.90, 0.95]) - stats_sat.loc["rms"] = res_sat.pow(2).mean().pow(0.5) + :param str font: font name + :param str colormap: colormap name + :return None + """ + available_fonts = matplotlib.font_manager.get_font_names() + if font in available_fonts: + matplotlib.rcParams["font.family"] = font + else: + logging.warning(f"Font ['{font}'] not found, falling back to {matplotlib.rcParams['font.family']}") + logging.warning(f"Available fonts: {available_fonts}") - stats_sat.index = pd.MultiIndex.from_tuples( - ((sat, idx) for idx in stats_sat.index.values) - ) - stats_sat.index = stats_sat.index.set_names(["Satellite", "Stats"]) + available_cmaps = plt.colormaps() + if colormap in available_cmaps: + matplotlib.rcParams["image.cmap"] = colormap + else: + logging.warning(f"Colormap '{colormap}' not found, falling back to '{matplotlib.rcParams['image.cmap']}'") + logging.warning(f"Available colormaps: {plt.colormaps()}") - stats = pd.concat([stats, stats_sat]) - return stats +def configure_diff_plot( + axes: Any, + start_epoch: datetime, + end_epoch: datetime, + nominal_ymin: float, + nominal_ymax: float, + fix_ylim: bool, + xlabel: str, + ylabel: str, + title: str, +) -> None: + """ + Configure the orbit/clock difference plot + + :param Any axes: The axes object to configure + :param datetime start_epoch: Start of epoch time (lower limit of x-axis) + :param datetime end_epoch: End of epoch time (upper limit of x-axis) + :param float nominal_ymin: Nominal lower limit of y-axis + :param float nominal_ymax: Nominal upper limit of y-axis + :param bool fix_ylim: Fix Y-axis limit to nominal values. + If False, the Y-axis limits will be automatically set when any data points exceed the nominal range. + :param str xlabel: Label of x-axis + :param str ylabel: Label of y-axis + :param str title: Title of the axes + :return None + """ + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + formatter.formats = ["%Y", "%m", "%j", "%H:%M", "%H:%M", "%S.%f"] + formatter.zero_formats = ["", "%Y", "%Y", "%Y-%j", "%H:%M", "%H:%M"] + formatter.offset_formats = ["", "", "", "", "%Y-%j", "%Y-%j"] + + axes.grid(True) + ymin, ymax = axes.get_ylim() + if (ymin >= nominal_ymin and ymax <= nominal_ymax) or fix_ylim: + ymin = nominal_ymin + ymax = nominal_ymax + axes.set_ylim(ymin, ymax) + axes.set_ylabel(ylabel) + axes.set_xlim(start_epoch, end_epoch) + axes.xaxis.set_major_locator(locator) + axes.xaxis.set_major_formatter(formatter) + axes.set_xlabel(xlabel) + axes.set_title(title) + + +def configure_rms_plot( + axes: Any, + xdata: npt.ArrayLike, + nominal_ymin: float, + nominal_ymax: float, + fix_ylim: bool, + xticklabels: list[str], + xlabel: str, + ylabel: str, + annotation: str, + title: str, +) -> None: + """ + Configure the orbit/clock RMS plot + + :param Any axes: The axes object to configure + :param npt.ArrayLike xdata: Data array for the x-axis + :param float nominal_ymin: Nominal lower limit of y-axis + :param float nominal_ymax: Nominal upper limit of y-axis + :param bool fix_ylim: Fix Y-axis limit to nominal values. + If False, the Y-axis limits will be automatically set when any data points exceed the nominal range. + :param list[str] xticklabels: Tick labels of x-axis + :param str xlabel: Label of x-axis + :param str ylabel: Label of y-axis + :param str annotation: Annotation to add on the axes + :param str title: Title of the axes + :return None + """ + axes.grid(True) + ymin, ymax = axes.get_ylim() + ymin = nominal_ymin if fix_ylim else min(nominal_ymin, ymin) + ymax = nominal_ymax if fix_ylim else max(nominal_ymax, ymax) + axes.set_ylim([ymin, ymax]) + axes.set_ylabel(ylabel) + axes.set_xlim([xdata[0] - 1, xdata[-1] + 1]) + axes.set_xticks(xdata) + axes.set_xticklabels(xticklabels, ha="center", rotation=90) + axes.set_xlabel(xlabel) + axes.set_title(title) + axes.legend(loc="upper right") + axes.text(xdata[0] - 0.5, 0.95 * ymax, annotation, ha="left", va="top") def plot_orb_diff( - res: pd.DataFrame, + diff_df: pd.DataFrame, svs: list[str], title: str, start_epoch: datetime, end_epoch: datetime, -) -> Figure: + nominal_ylim: float, + fix_ylim: bool, +) -> Union[Figure, None]: """ Plot orbit differences from a Pandas DataFrame. - :param pd.DataFrame res: The Pandas DataFrame containing orbit differences + :param pd.DataFrame diff_df: The Pandas DataFrame containing orbit differences :param list[str] svs: Satellite list to plot differences for :param str title: Figure title :param datetime start_epoch: Start of epoch time :param datetime end_epoch: End of epoch time + :param float nominal_ylim: Nominal Y-axis plotting limit (used as ±limit) in metres. + :param bool fix_ylim: Fix Y-axis limit to nominal value. + If False, the Y-axis limit will be automatically set when any data points exceed the nominal range. :return Figure: The Figure object for the plot """ - dim_desc = { - "R": "Radial", - "T": "Along-track", - "N": "Cross-track", - "3D": "3D-Total", - } - # Filter data with given satellite list and unstack the dataframe to push the satellite PRNs # from the index to columns as it's easier to plot - unstacked_diff_df = res[res.index.get_level_values("Satellite").isin(svs)].unstack() - svs = sorted(list(unstacked_diff_df.columns.get_level_values("Satellite").unique())) + svs = filter_svs_list(svs, diff_df) if not svs: - logging.error("No data found for given satellites") return None + unstacked_diff_df = diff_df[diff_df.index.get_level_values("Satellite").isin(svs)].unstack() + # Plot epoch-by-epoch orbit comparison results fig, ax = plt.subplots(nrows=3, dpi=300.0, figsize=(12, 6)) - locator = mdates.AutoDateLocator() - formatter = mdates.ConciseDateFormatter(locator) - formatter.formats = ["%Y", "%m", "%Y-%j", "%H:%M", "%H:%M", "%S.%f"] - formatter.zero_formats = ["", "%Y", "%Y-%j", "%Y-%j", "%H:%M", "%H:%M:%S"] - formatter.offset_formats = ["", "", "", "", "%Y-%j", "%Y-%j"] - - for i, dim in enumerate(["R", "T", "N"]): # choose dimension to plot - ax[i].plot( - unstacked_diff_df[dim_desc[dim]].index.values, - unstacked_diff_df[dim_desc[dim]].values, - ) + for i, dim in enumerate(["Radial", "Along-track", "Cross-track"]): + ax[i].plot(unstacked_diff_df[dim].index.values, unstacked_diff_df[dim].values) + + configure_diff_plot( + axes=ax[0], + start_epoch=start_epoch, + end_epoch=end_epoch, + nominal_ymin=-abs(nominal_ylim), + nominal_ymax=+abs(nominal_ylim), + fix_ylim=fix_ylim, + xlabel="", + ylabel="Radial [m]", + title=title, + ) + configure_diff_plot( + axes=ax[1], + start_epoch=start_epoch, + end_epoch=end_epoch, + nominal_ymin=-abs(nominal_ylim), + nominal_ymax=+abs(nominal_ylim), + fix_ylim=fix_ylim, + xlabel="", + ylabel="Along-track [m]", + title="", + ) + configure_diff_plot( + axes=ax[2], + start_epoch=start_epoch, + end_epoch=end_epoch, + nominal_ymin=-abs(nominal_ylim), + nominal_ymax=+abs(nominal_ylim), + fix_ylim=fix_ylim, + xlabel="Time", + ylabel="Cross-track [m]", + title="", + ) - ax[i].grid(True) - ymin, ymax = ax[i].get_ylim() - if ymin >= -0.25 and ymax <= 0.25: - ymin = -0.25 - ymax = 0.25 - ax[i].set_ylim(ymin, ymax) - ax[i].set_ylabel(f"{dim_desc[dim]} [m]") - ax[i].set_xlim(start_epoch, end_epoch) - ax[i].set_xticklabels([]) - - ax[-1].xaxis.set_major_locator(locator) - ax[-1].xaxis.set_major_formatter(formatter) - ax[-1].set_xlabel("Time") - ax[0].set_title(title) plt.legend(svs, ncol=8, loc="upper center", bbox_to_anchor=(0.5, -0.3)) plt.close() @@ -319,56 +501,52 @@ def plot_orb_diff( def plot_clk_diff( - res: pd.DataFrame, + diff_df: pd.DataFrame, svs: list[str], title: str, start_epoch: datetime, end_epoch: datetime, -) -> Figure: + nominal_ylim: float, + fix_ylim: bool, +) -> Union[Figure, None]: """ Plot clock differences from a Pandas DataFrame. - :param pd.DataFrame res: The Pandas DataFrame containing clock differences + :param pd.DataFrame diff_df: The Pandas DataFrame containing clock differences :param list[str] svs: Satellite list to plot differences for :param str title: Figure title :param datetime start_epoch: Start of epoch time :param datetime end_epoch: End of epoch time + :param float nominal_ylim: Nominal Y-axis plotting limit (used as ±limit) in nanoseconds. + :param bool fix_ylim: Fix Y-axis limit to nominal value. + If False, the Y-axis limit will be automatically set when any data points exceed the nominal range. :return Figure: The Figure object for the plot """ # Filter data with given satellite list and unstack the dataframe to push the satellite PRNs # from the index to columns as it's easier to plot - unstacked_diff_df = res[res.index.get_level_values("Satellite").isin(svs)].unstack() - svs = sorted(list(unstacked_diff_df.columns.get_level_values("Satellite").unique())) + svs = filter_svs_list(svs, diff_df) if not svs: - logging.error("No data found for given satellites") return None + unstacked_diff_df = diff_df[diff_df.index.get_level_values("Satellite").isin(svs)].unstack() + # Plot epoch-by-epoch clock comparison results fig, ax = plt.subplots(dpi=300.0, figsize=(12, 3)) - locator = mdates.AutoDateLocator() - formatter = mdates.ConciseDateFormatter(locator) - formatter.formats = ["%Y", "%m", "%Y-%j", "%H:%M", "%H:%M", "%S.%f"] - formatter.zero_formats = ["", "%Y", "%Y-%j", "%Y-%j", "%H:%M", "%H:%M:%S"] - formatter.offset_formats = ["", "", "", "", "%Y-%j", "%Y-%j"] - - ax.plot( - unstacked_diff_df["Clock"].index.values, - unstacked_diff_df["Clock"].values, + ax.plot(unstacked_diff_df["Clock"].index.values, unstacked_diff_df["Clock"].values) + + configure_diff_plot( + axes=ax, + start_epoch=start_epoch, + end_epoch=end_epoch, + nominal_ymin=-abs(nominal_ylim), + nominal_ymax=+abs(nominal_ylim), + fix_ylim=fix_ylim, + xlabel="Time", + ylabel="Clock [ns]", + title=title, ) - ax.grid(True) - ymin, ymax = ax.get_ylim() - if ymin >= -0.40 and ymax <= 0.40: - ymin = -0.40 - ymax = 0.40 - ax.set_ylim(ymin, ymax) - ax.set_ylabel(f"Clock [ns]") - ax.set_xlim(start_epoch, end_epoch) - ax.xaxis.set_major_locator(locator) - ax.xaxis.set_major_formatter(formatter) - ax.set_xlabel("Time") - ax.set_title(title) plt.legend(svs, ncol=8, loc="upper center", bbox_to_anchor=(0.5, -0.2)) plt.close() @@ -376,41 +554,30 @@ def plot_clk_diff( def plot_orb_rms( - stats: pd.DataFrame, - svs: list[str], - title: str, -) -> Figure: + stats_df: pd.DataFrame, svs: list[str], title: str, nominal_ylim: float, fix_ylim: bool +) -> Union[Figure, None]: """ Plot orbit RMS errors from a Pandas DataFrame. - :param pd.DataFrame stats: The Pandas DataFrame containing orbit statistics + :param pd.DataFrame stats_df: The Pandas DataFrame containing orbit statistics :param list[str] svs: Satellite list to plot orbit RMS errors for :param str title: Figure title + :param float nominal_ylim: Nominal Y-axis plotting limit (used as 0 to +limit) in metres. + :param bool fix_ylim: Fix Y-axis limit to nominal value. + If False, the Y-axis limit will be automatically set when any data points exceed the nominal range. :return Figure: The Figure object for the plot """ - dim_desc = { - "R": "Radial", - "T": "Along-track", - "N": "Cross-track", - "3D": "3D-Total", - } - - svs = sorted(list(stats.index.get_level_values("Satellite").intersection(svs))) + svs = filter_svs_list(svs, stats_df) if not svs: - logging.error("No data found for given satellites") return None # Extract data for Radial, Along-track, and Cross-track RMS rms = {} for sat in svs: - rms[sat] = { - dim_desc[dim]: stats.loc[(sat, "rms"), dim_desc[dim]] - for dim in ["R", "T", "N"] - } + rms[sat] = {dim: stats_df.loc[(sat, "rms"), dim] for dim in ["Radial", "Along-track", "Cross-track"]} rms_all = { - dim_desc[dim]: round(stats.loc[("All", "rms"), dim_desc[dim]], 3) - for dim in ["R", "T", "N"] - } + dim: round(stats_df.loc[("All", "rms"), dim], 3) for dim in ["Radial", "Along-track", "Cross-track"] + } # TODO Eugene: 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)) @@ -418,61 +585,51 @@ def plot_orb_rms( bar_width = 0.2 x = np.arange(len(svs)) - for i, dim in enumerate(["R", "T", "N"]): # choose dimension to plot + for i, dim in enumerate(["Radial", "Along-track", "Cross-track"]): pos = x + (i - 1) * bar_width - ax.bar( - pos, - [rms[sat][dim_desc[dim]] for sat in svs], - bar_width, - label=dim_desc[dim], - ) + ax.bar(pos, [rms[sat][dim] for sat in svs], bar_width, label=dim) + + configure_rms_plot( + axes=ax, + xdata=x, + nominal_ymin=0, + nominal_ymax=abs(nominal_ylim), + fix_ylim=fix_ylim, + xticklabels=svs, + xlabel="Satellite", + ylabel="RMS [m]", + annotation=f'All satellites: R={rms_all["Radial"]}, A={rms_all["Along-track"]}, C={rms_all["Cross-track"]} m', + title=title, + ) - ax.grid(True) - ymin, ymax = ax.get_ylim() - ymax = max(0.15, ymax) - ax.set_ylim([ymin, ymax]) - ax.set_ylabel("RMS [m]") - ax.set_xlim([x[0] - 1, x[-1] + 1]) - ax.set_xticks(x) - ax.set_xticklabels(svs, ha="center", rotation=90) - ax.set_xlabel("Satellite") - ax.set_title(title) - ax.legend(loc="upper right") - ax.text( - x[0] - 0.5, - 0.95 * ymax, - f'All satellites: R={rms_all["Radial"]}, A={rms_all["Along-track"]}, C={rms_all["Cross-track"]} m', - ha="left", - va="top", - ) # Eugene: change to mean RMS? plt.close() return fig def plot_clk_rms( - stats: pd.DataFrame, - svs: list[str], - title: str, -) -> Figure: + stats_df: pd.DataFrame, svs: list[str], title: str, nominal_ylim: float, fix_ylim: bool +) -> Union[Figure, None]: """ Plot clock RMS errors from a Pandas DataFrame. - :param pd.DataFrame stats: The Pandas DataFrame containing clock statistics + :param pd.DataFrame stats_df: The Pandas DataFrame containing clock statistics :param list[str] svs: Satellite list to plot clock RMS errors for :param str title: Figure title + :param float nominal_ylim: Nominal Y-axis plotting limit (used as 0 to +limit) in nanoseconds. + :param bool fix_ylim: Fix Y-axis limit to nominal value. + If False, the Y-axis limit will be automatically set when any data points exceed the nominal range. :return Figure: The Figure object for the plot """ - svs = sorted(list(stats.index.get_level_values("Satellite").intersection(svs))) + svs = filter_svs_list(svs, stats_df) if not svs: - logging.error("No data found for given satellites") return None # Extract data for Clock rms = {} for sat in svs: - rms[sat] = {"Clock": stats.loc[(sat, "rms"), "Clock"]} - rms_all = {"Clock": round(stats.loc[("All", "rms"), "Clock"], 3)} + 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 # Plot Clock RMS of each satellite fig, ax = plt.subplots(dpi=300.0, figsize=(12, 3)) @@ -482,62 +639,165 @@ def plot_clk_rms( ax.bar(x, [rms[sat]["Clock"] for sat in svs], bar_width, label="Clock") - ax.grid(True) - ymin, ymax = ax.get_ylim() - ymax = max(0.20, ymax) - ax.set_ylim([ymin, ymax]) - ax.set_ylabel("RMS [ns]") - ax.set_xlim([x[0] - 1, x[-1] + 1]) - ax.set_xticks(x) - ax.set_xticklabels(svs, ha="center", rotation=90) - ax.set_xlabel("Satellite") - ax.set_title(title) - ax.legend(loc="upper right") - ax.text( - x[0] - 0.5, - 0.95 * ymax, - f'All satellites: {rms_all["Clock"]} ns', - ha="left", - va="top", - ) # Eugene: change to mean RMS? + configure_rms_plot( + axes=ax, + xdata=x, + nominal_ymin=0, + nominal_ymax=abs(nominal_ylim), + fix_ylim=fix_ylim, + xticklabels=svs, + xlabel="Satellite", + ylabel="RMS [ns]", + annotation=f'All satellites: {rms_all["Clock"]} ns', + title=title, + ) + plt.close() return fig +def plot_orbits( + diff_df: pd.DataFrame, + stats_df: pd.DataFrame, + sys_name: str, + sys_svs: list[str], + start_epoch: datetime, + end_epoch: datetime, + nominal_ylim: float, + fix_ylim: bool, + diff_plot_title: str, + rms_plot_title: str, + diff_plot_path: Path, + rms_plot_path: Path, +) -> None: + """ + Plot orbit differences and RMS errors. + + :param pd.DataFrame diff_df: The Pandas DataFrame containing orbit differences + :param pd.DataFrame stats_df: The Pandas DataFrame containing orbit statistics + :param str sys_name: Name of the satellite system to plot orbits + :param list[str] sys_svs: Satellite list of the satellite system to plot orbits + :param datetime start_epoch: Start of epoch time + :param datetime end_epoch: End of epoch time + :param float nominal_ylim: Nominal Y-axis plotting limit in metres. + Used as ±limit for orbit differences, and 0 to +limit for RMS errors. + :param bool fix_ylim: Fix Y-axis limits of orbit plots to nominal value. + If False, the Y-axis limits will be automatically set when any data points exceed the nominal ranges. + :param str diff_plot_title: Figure title of orbit differences + :param str rms_plot_title: Figure title of orbit RMS errors + :param Path diff_plot_path: Path to save the orbit difference plot + :param Path rms_plot_path: Path to save the orbit RMS plot + :return None + """ + # Plot epoch-by-epoch orbit differences + logging.info(f"-- Orbit differences - {sys_name}") + + fig = plot_orb_diff(diff_df, sys_svs, diff_plot_title, start_epoch, end_epoch, nominal_ylim, fix_ylim) + if fig: + fig.savefig(diff_plot_path, bbox_inches="tight") + + # Plot orbit RMS errors during the whole session + logging.info(f"-- Orbit RMS - {sys_name}") + + fig = plot_orb_rms(stats_df, sys_svs, rms_plot_title, nominal_ylim, fix_ylim) + if fig: + fig.savefig(rms_plot_path, bbox_inches="tight") + + +def plot_clocks( + diff_df: pd.DataFrame, + stats_df: pd.DataFrame, + sys_name: str, + sys_svs: list[str], + start_epoch: datetime, + end_epoch: datetime, + nominal_ylim: float, + fix_ylim: bool, + diff_plot_title: str, + rms_plot_title: str, + diff_plot_path: Path, + rms_plot_path: Path, +) -> None: + """ + Plot clock differences and RMS errors. + + :param pd.DataFrame diff_df: The Pandas DataFrame containing clock differences + :param pd.DataFrame stats_df: The Pandas DataFrame containing clock statistics + :param str sys_name: Name of the satellite system to plot clocks + :param list[str] sys_svs: Satellite list of the satellite system to plot clocks + :param datetime start_epoch: Start of epoch time + :param datetime end_epoch: End of epoch time + :param float nominal_ylim: Nominal Y-axis plotting limit in nanoseconds. + Used as ±limit for clock differences, and 0 to +limit for RMS errors. + :param bool fix_ylim: Fix Y-axis limits of clock plots to nominal value. + If False, the Y-axis limits will be automatically set when any data points exceed the nominal ranges. + :param str diff_plot_title: Figure title of clock differences + :param str rms_plot_title: Figure title of clock RMS errors + :param Path diff_plot_path: Path to save the clock difference plot + :param Path rms_plot_path: Path to save the clock RMS plot + :return None + """ + # Plot epoch-by-epoch clock differences + logging.info(f"-- Clock differences - {sys_name}") + + fig = plot_clk_diff(diff_df, sys_svs, diff_plot_title, start_epoch, end_epoch, nominal_ylim, fix_ylim) + if fig: + fig.savefig(diff_plot_path, bbox_inches="tight") + + # Plot clock RMS errors during the whole session + logging.info(f"-- Clock RMS - {sys_name}") + + fig = plot_clk_rms(stats_df, sys_svs, rms_plot_title, nominal_ylim, fix_ylim) + if fig: + fig.savefig(rms_plot_path, bbox_inches="tight") + + def analyse_orbit_clock( job_dir: Path, ref_dir: Path, + sub_job_list: list[str], ref_prefix: str, + file_types: list[str], start_date: datetime, session_len: int, sat_sys: str, excluded_svs: list[str], + orb_ref_frame: str, + orb_hlm_mode: str, + epochwise_hlm: bool, clk_norm_types: list[str], rel_output_dir: Path, + nominal_orb_ylim: float, + nominal_clk_ylim: float, + fix_ylim: bool, ) -> None: """ Compare orbits and clocks against reference products and plot differences and RMS errors. :param Path job_dir: Directory where data is processed :param Path ref_dir: Directory where reference products are placed - :param str ref_prefix: Prefix of reference products, e.g. IGS0OPSRAP - :param datetime start_date: Start of epoch time + :param list[str] sub_job_list: Sub jobs (which should be the names of sub folders in the job directory) to analyse. + All sub jobs will be processed when empty + :param str ref_prefix: Prefix of reference products, e.g. 'IGS0OPSRAP' + :param list[str] file_types: File types to analyse, 'SP3' and/or 'CLK' + :param datetime start_date: Start of date period (datetime) to analyse data :param int session_len: Length of each analysis session :param str sat_sys: Satellite systems of orbits and clocks to analyse :param list[str] excluded_svs: Satellites to exclude - :param list[str] clk_norm_types: Normalizations to apply to clock differences. Available options include - 'epoch', 'daily', 'sv', any satellite PRN, or any combination of them, defaults to None + :param str orb_hlm_mode: Helmert transformation to apply to orbits. Can be None, 'ECF', or 'ECI' + :param bool epochwise_hlm: Epochwise Helmert transformation + :param list[str] clk_norm_types: Normalizations to apply to clocks. Available options include + 'epoch', 'daily', 'sv', any satellite PRN, or any combination of them :param Path rel_output_dir: Relative path of output directory under each sub job directory + :param float nominal_orb_ylim: Nominal Y-axis plotting limit in metres. + Used as ±limit for orbit differences, and 0 to +limit for RMS errors. + :param float nominal_clk_ylim: Nominal Y-axis plotting limit in nanoseconds. + Used as ±limit for clock differences, and 0 to +limit for RMS errors. + :param bool fix_ylim: Fix Y-axis limits of orbit and clock plots to nominal values. + If False, the Y-axis limits will be automatically set when any data points exceed the nominal ranges. :return None """ - sys_meta = { - "G": {"name": "GPS", "max_sats": 32}, - "R": {"name": "GLO", "max_sats": 27}, - "E": {"name": "GAL", "max_sats": 36}, - "C": {"name": "BDS", "max_sats": 62}, - } - # Set matplotlib font and colormap set_up_matplotlib("DejaVu Sans", "jet") @@ -550,334 +810,288 @@ def analyse_orbit_clock( svs = {} for sys in sat_sys: svs[sys] = [ - f"{sys}%02d" % i - for i in range(1, sys_meta[sys]["max_sats"] + 1) - if f"{sys}%02d" % i not in excluded_svs + f"{sys}%02d" % i for i in range(1, sys_meta[sys]["max_sats"] + 1) if f"{sys}%02d" % i not in excluded_svs ] - # Download reference sp3 and clock files - logging.info("Downloading reference products ...") + hlm = orb_hlm_mode if orb_hlm_mode else "none" + norm = "_".join(clk_norm_types) if clk_norm_types else "none" + # Download reference sp3 and clock files start_epoch = dates[0] end_epoch = dates[-1] + timedelta(days=1) sampling_rate = {} - for product in ["SP3", "CLK"]: - analysis_center = ref_prefix[0:3] - project_type = ref_prefix[4:7] - solution_type = ref_prefix[7:10] - sampling_rate[product] = generate_sampling_rate( - file_ext=product, - analysis_center=analysis_center, - solution_type=solution_type, - ) + for file_type in file_types: + download_ref_products(ref_dir, ref_prefix, file_type, start_epoch, end_epoch, sampling_rate) - ga.gn_download.download_product_from_cddis( - download_dir=ref_dir, - start_epoch=start_epoch, - end_epoch=end_epoch, - file_ext=product, - long_filename=True, - analysis_center=analysis_center, - project_type=project_type, - solution_type=solution_type, - timespan=timedelta(days=1), - sampling_rate=sampling_rate[product], - if_file_present="dont_replace", - ) + # TODO: call Ginan to convert ECEF SP3 files to ECI SP3 files - logging.info("Reference products downloaded\n") + if not sub_job_list: + sub_job_list = [ + f.name for f in job_dir.iterdir() if f.is_dir() + ] # get all sub folders when sub_job_list is empty - ssr_list = [ - f.name for f in job_dir.iterdir() if f.is_dir() and f.name[:3] == "SSR" - ] # we assume that sub folders are named with SSR mountpoints - for ssr_mountpoint in ssr_list: + for sub_job_name in sub_job_list: # Set up output directory - output_dir = job_dir / ssr_mountpoint / rel_output_dir + output_dir = job_dir / sub_job_name / rel_output_dir ga.gn_utils.ensure_folders([output_dir]) - # Compare sp3 using gnssanalysis - logging.info("Comparing SP3 files ...") + if "SP3" in file_types: + # Compare sp3 using gnssanalysis + logging.info("Comparing SP3 files ...") - # Get input file paths - input_paths = [] - for yrdoy in yrdoys: - input_paths.append( - [ - ref_dir - / f"{ref_prefix}_{yrdoy}0000_01D_{sampling_rate['SP3']}_ORB.SP3", - job_dir / ssr_mountpoint / f"{ssr_mountpoint}_{yrdoy}0000_ORB.sp3", - ] + input_paths = generate_input_paths( + job_dir, ref_dir, sub_job_name, ref_prefix, yrdoys, sampling_rate, "SP3", orb_ref_frame ) - res_sp3 = pd.DataFrame() - - for input in input_paths: - ref_sp3_file = input[0] - job_sp3_file = input[1] - - logging.info(f"-- {job_sp3_file} vs {ref_sp3_file}") - - diff_df = pd.DataFrame() - try: - diff_df = sp3_diff(ref_sp3_file, job_sp3_file) - except Exception as error: - logging.exception(f"Error with sp3_diff(): {error}") - continue - - res_sp3 = pd.concat([res_sp3, diff_df]) - - if not res_sp3.empty: - res_sp3.to_csv(output_dir / f"diff_{start_yrdoy}_{session_len}D_SP3.csv") - - # Compute statistics - logging.info("Computing statistics of SP3 differences ...") - - stats_sp3 = sp3_stats(res_sp3) - stats_sp3.to_csv(output_dir / f"stats_{start_yrdoy}_{session_len}D_SP3.csv") - - # Plot sp3 comparison results - logging.info("Plotting SP3 comparison results ...") - - for sys in sat_sys: - sys_name = sys_meta[sys]["name"] - sys_svs = svs[sys] - - # Plot epoch-by-epoch orbit differences - logging.info(f"-- Orbit differences - {sys_name}") - - title = f"Satellite Orbit Comparison (gnssanalysis): {ssr_mountpoint} vs {ref_prefix}" - fig = plot_orb_diff(res_sp3, sys_svs, title, start_epoch, end_epoch) - if fig: - fig.savefig( - output_dir / f"diff_{start_yrdoy}_{session_len}D_ORB_{sys}.png", - bbox_inches="tight", - ) - - # Plot epoch-by-epoch clock differences - logging.info(f"-- Clock differences - {sys_name}") - - title = f"Satellite Clock Comparison (gnssanalysis): {ssr_mountpoint} vs {ref_prefix}" - fig = plot_clk_diff(res_sp3, sys_svs, title, start_epoch, end_epoch) - if fig: - fig.savefig( - output_dir - / f"diff_{start_yrdoy}_{session_len}D_CLK_{sys}_none.png", - bbox_inches="tight", - ) - - # Plot orbit RMS during the whole session - logging.info(f"-- Orbit RMS - {sys_name}") + sp3_diff = compare_sp3_files( + input_paths, + output_dir / f"diff_{start_yrdoy}_{session_len}D_SP3_{hlm}_{norm}.csv", + svs, + orb_ref_frame, + orb_hlm_mode, + epochwise_hlm, + clk_norm_types, + ) - title = f"Satellite Orbit Comparison (gnssanalysis): {ssr_mountpoint} vs {ref_prefix}" - fig = plot_orb_rms(stats_sp3, sys_svs, title) - if fig: - fig.savefig( - output_dir / f"rms_{start_yrdoy}_{session_len}D_ORB_{sys}.png", - bbox_inches="tight", + if not sp3_diff.empty: + # Compute statistics + logging.info("Computing statistics of SP3 differences ...") + + sp3_stats = compute_stats( + sp3_diff, output_dir / f"stats_{start_yrdoy}_{session_len}D_SP3_{hlm}_{norm}.csv" + ) + + # Plot sp3 comparison results + logging.info("Plotting SP3 comparison results ...") + + for sys in sat_sys: + sys_name = sys_meta[sys]["name"] + sys_svs = svs[sys] + + title = f"Satellite Orbit Comparison (gnssanalysis): {sub_job_name} vs {ref_prefix}" + diff_plot_path = output_dir / f"diff_{start_yrdoy}_{session_len}D_ORB_{sys}_{hlm}.png" + rms_plot_path = output_dir / f"rms_{start_yrdoy}_{session_len}D_ORB_{sys}_{hlm}.png" + plot_orbits( + sp3_diff, + sp3_stats, + sys_name, + sys_svs, + start_epoch, + end_epoch, + nominal_orb_ylim, + fix_ylim, + title, + title, + diff_plot_path, + rms_plot_path, ) - # Plot clock RMS during the whole session - logging.info(f"-- Clock RMS - {sys_name}") - - title = f"Satellite Clock Comparison (gnssanalysis): {ssr_mountpoint} vs {ref_prefix}" - fig = plot_clk_rms(stats_sp3, sys_svs, title) - if fig: - fig.savefig( - output_dir - / f"rms_{start_yrdoy}_{session_len}D_CLK_{sys}_none.png", - bbox_inches="tight", + title = f"Satellite Clock Comparison (gnssanalysis): {sub_job_name} vs {ref_prefix}" + diff_plot_path = output_dir / f"diff_{start_yrdoy}_{session_len}D_CLK_{sys}_{norm}.png" + rms_plot_path = output_dir / f"rms_{start_yrdoy}_{session_len}D_CLK_{sys}_{norm}.png" + plot_clocks( + sp3_diff, + sp3_stats, + sys_name, + sys_svs, + start_epoch, + end_epoch, + nominal_clk_ylim, + fix_ylim, + title, + title, + diff_plot_path, + rms_plot_path, ) - # Compare clocks using gnssanalysis - logging.info("Comparing CLK files ...") + if "CLK" in file_types: + # Compare clocks using gnssanalysis + logging.info("Comparing CLK files ...") - # Get input file paths - input_paths = [] - for yrdoy in yrdoys: - input_paths.append( - [ - ref_dir - / f"{ref_prefix}_{yrdoy}0000_01D_{sampling_rate['CLK']}_CLK.CLK", - job_dir / ssr_mountpoint / f"{ssr_mountpoint}_{yrdoy}0000_CLK.clk", - ] - ) - - res_clk = pd.DataFrame() - - for input in input_paths: - ref_clk_file = input[0] - job_clk_file = input[1] - - logging.info(f"-- {job_clk_file} vs {ref_clk_file}") - - diff_df = pd.DataFrame() - try: - diff_df = clk_diff(ref_clk_file, job_clk_file, clk_norm_types) - except Exception as error: - logging.exception(f"Error with clk_diff(): {error}") - continue - - res_clk = pd.concat([res_clk, diff_df]) - - if not res_clk.empty: - clk_norm_types = [ - item if isinstance(item, str) else "_".join(item) - for item in clk_norm_types - ] - norm = "_".join(clk_norm_types) if clk_norm_types else "none" + input_paths = generate_input_paths(job_dir, ref_dir, sub_job_name, ref_prefix, yrdoys, sampling_rate, "CLK") - res_clk.to_csv( - output_dir / f"diff_{start_yrdoy}_{session_len}D_CLK_{norm}.csv" + clk_diff = compare_clk_files( + input_paths, output_dir / f"diff_{start_yrdoy}_{session_len}D_CLK_{norm}.csv", svs, clk_norm_types ) - # Compute statistics - logging.info("Computing statistics of CLK differences ...") - - stats_clk = clk_stats(res_clk) - stats_clk.to_csv( - output_dir / f"stats_{start_yrdoy}_{session_len}D_CLK_{norm}.csv" - ) - - # Plot clock comparison results - logging.info("Plotting CLK comparison results ...") - - for sys in sat_sys: - sys_name = sys_meta[sys]["name"] - sys_svs = svs[sys] - - # Plot epoch-by-epoch clock differences - logging.info(f"-- Clock differences - {sys_name}") - - title = f"Satellite Clock Comparison (gnssanalysis): {ssr_mountpoint} vs {ref_prefix}" - fig = plot_clk_diff(res_clk, sys_svs, title, start_epoch, end_epoch) - if fig: - fig.savefig( - output_dir - / f"diff_{start_yrdoy}_{session_len}D_CLK_{sys}_{norm}.png", - bbox_inches="tight", - ) - - # Plot clock RMS during the whole session - logging.info(f"-- Clock RMS - {sys_name}") - - title = f"Satellite Clock Comparison (gnssanalysis): {ssr_mountpoint} vs {ref_prefix}" - fig = plot_clk_rms(stats_clk, sys_svs, title) - if fig: - fig.savefig( - output_dir - / f"rms_{start_yrdoy}_{session_len}D_CLK_{sys}_{norm}.png", - bbox_inches="tight", + if not clk_diff.empty: + # Compute statistics + logging.info("Computing statistics of CLK differences ...") + + clk_stats = compute_stats(clk_diff, output_dir / f"stats_{start_yrdoy}_{session_len}D_CLK_{norm}.csv") + + # Plot clock comparison results + logging.info("Plotting CLK comparison results ...") + + for sys in sat_sys: + sys_name = sys_meta[sys]["name"] + sys_svs = svs[sys] + + title = f"Satellite Clock Comparison (gnssanalysis): {sub_job_name} vs {ref_prefix}" + diff_plot_path = output_dir / f"diff_{start_yrdoy}_{session_len}D_CLK_{sys}_{norm}.png" + rms_plot_path = output_dir / f"rms_{start_yrdoy}_{session_len}D_CLK_{sys}_{norm}.png" + plot_clocks( + clk_diff, + clk_stats, + sys_name, + sys_svs, + start_epoch, + end_epoch, + nominal_clk_ylim, + fix_ylim, + title, + title, + diff_plot_path, + rms_plot_path, ) logging.info("Orbit and clock comparison done\n") @click.command() +@click.option("--job-dir", required=True, help="Directory where data is processed", type=Path) +@click.option("--ref-dir", required=True, help="Directory where reference products are placed", type=Path) @click.option( - "--job-dir", required=True, help="Directory where data is processed", type=Path + "--sub-jobs", + help="Sub jobs (which should be the names of sub folders in the job directory) to analyse. All sub jobs will be processed if not specified. Default: None", + default=None, + type=str, ) @click.option( - "--ref-dir", + "--ref-prefix", required=True, - help="Directory where reference products are placed", - type=Path, + help="Prefix of reference products. Default: 'IGS0OPSRAP'", + default="IGS0OPSRAP", + type=str, ) @click.option( - "--ref-prefix", + "--file-types", required=True, - help="Prefix of reference products, e.g. IGS0OPSRAP", - default="IGS0OPSRAP", + help="File types to analyse, 'SP3' and/or 'CLK'. Default: 'SP3, CLK'", + default="SP3, CLK", type=str, ) @click.option( "--start-yrdoy", - help="Start of date period (day-of-year) to analyse data for in the format of YYYYDDD. Current day will be used if not set", + help="Start of date period (day-of-year) to analyse data for in the format of 'YYYYDDD'. Current day will be used if not set. Default: None", default=None, type=int, ) @click.option( "--end-yrdoy", - help="End of date period (day-of-year, inclusive) to analyse data for in the format of YYYYDDD. Do not set for real-time mode", + help="End of date period (day-of-year, inclusive) to analyse data for in the format of 'YYYYDDD'. Do not set for real-time mode. Default: None", default=None, type=int, ) @click.option( "--session-len", required=True, - help="Length of a session for each rotation period in days. If not set, no ratation (i.e. only one session) will be applied between start- and end-yrdoy", + help="Length of a session for each rotation period in days. If 0 or not set, no rotation (i.e. only one session) will be applied between start- and end-yrdoy. Default: 0", default=0, type=int, ) @click.option( "--align-to-gps-week", - help="Align rotation period to GPS week for weekly solutions. Only effective when session length is 7 days (weekly)", + help="Align rotation period to GPS week for weekly solutions. Only effective when session length is 7 days (weekly). Default: False", default=False, is_flag=True, ) @click.option( "--sat-sys", required=True, - help="Satellite system to analyse, G, R, E, C, or any combination without space", + help="Satellite system to analyse, 'G', 'R', 'E', 'C', or any combination without space, e.g. 'GREC'. Default: 'G'", default="G", type=str, ) +@click.option("--exclude", help="PRNs of satellites to exclude, e.g. 'G01, G03'. Default: None", default=None, type=str) +@click.option( + "--orb-ref-frame", + help="Reference frame of input orbits, either 'ECF' or 'ECI'. Default: 'ECF'", + default="ECF", + type=str, +) @click.option( - "--exclude", - help="PRNs of satellites to exclude, e.g. 'G01, G03'", + "--orb-hlm-mode", + help="Helmert transformation to apply to orbits, 'ECF', 'ECI', or 'none'. Default: None", default=None, type=str, ) +@click.option( + "--epochwise-hlm", + help="Apply Helmert transformation to orbits epochwisely. Default: False", + default=False, + is_flag=True, +) @click.option( "--clk-norm-types", - help="Normalisations to apply for clock files, e.g. 'sv', 'G01', 'epoch, daily'", + help="Normalisations to apply to clocks, e.g. 'none', 'sv', 'G01', 'epoch, daily'. Default: None", default=None, type=str, ) @click.option( "--rel-output-dir", required=True, - help="Relative path of output directory under each sub job directory", + help="Relative path of output directory under each sub job directory. Default: 'gnssanalysis'", default="gnssanalysis", type=Path, ) +@click.option( + "--nominal-orb-ylim", + help="Nominal Y-axis plotting limit in metres. Used as ±limit for orbit differences, and 0 to +limit for RMS errors. The Y-axis limits will be automatically set when any data points exceed the nominal ranges if '--fix-ylim' is not specified. Default: 0.5", + default=0.5, + type=float, +) +@click.option( + "--nominal-clk-ylim", + help="Nominal Y-axis plotting limit in nanoseconds. Used as ±limit for clock differences, and 0 to +limit for RMS errors. The Y-axis limits will be automatically set when any data points exceed the nominal ranges if '--fix-ylim' is not specified. Default: 1.5", + default=1.5, + type=float, +) +@click.option( + "--fix-ylim", + help="Fix Y-axis limits of orbit and clock plots to nominal values. If not specified, the Y-axis limits will be automatically set when any data points exceed the nominal ranges. Default: False", + default=False, + is_flag=True, +) @click.option("--verbose", is_flag=True) def analyse_orbit_clock_main( job_dir, ref_dir, + sub_jobs, ref_prefix, + file_types, start_yrdoy, end_yrdoy, session_len, align_to_gps_week, sat_sys, exclude, + orb_ref_frame, + orb_hlm_mode, + epochwise_hlm, clk_norm_types, rel_output_dir, + nominal_orb_ylim, + nominal_clk_ylim, + fix_ylim, verbose, ): ga.gn_utils.configure_logging(verbose) - excluded_svs = [] - if exclude: - excluded_svs = exclude.replace(" ", "").split(",") - - if clk_norm_types: - clk_norm_types = clk_norm_types.replace("none", "") - clk_norm_types = clk_norm_types.replace(" ", "").split(",") - else: - clk_norm_types = [] + sub_jobs = str_to_list(sub_jobs) + file_types = str_to_list(file_types.upper()) + excluded_svs = str_to_list(exclude) + if orb_hlm_mode == "none": + orb_hlm_mode = None + clk_norm_types = str_to_list(clk_norm_types.replace("none", "")) if clk_norm_types is not None else [] # Post-processing mode if end_yrdoy is not None: - logging.info( - "Orbit and clock analysis in post-processing mode as '--end-yrdoy' is set" - ) + logging.info("Orbit and clock analysis in post-processing mode as '--end-yrdoy' is set") if start_yrdoy is None: - logging.error( - "'--start-yrdoy' must be specified for post-processing mode ('--end-yrdoy' is set)" - ) + logging.error("'--start-yrdoy' must be specified for post-processing mode ('--end-yrdoy' is set)") return elif start_yrdoy > end_yrdoy: logging.error("Start date must be no later than end date") @@ -898,27 +1112,31 @@ def analyse_orbit_clock_main( analyse_orbit_clock( job_dir, ref_dir, + sub_jobs, ref_prefix, + file_types, start_date, session_len, sat_sys, excluded_svs, + orb_ref_frame, + orb_hlm_mode, + epochwise_hlm, clk_norm_types, rel_output_dir, + nominal_orb_ylim, + nominal_clk_ylim, + fix_ylim, ) start_date += timedelta(days=session_len) # Real-time mode else: - logging.info( - "Orbit and clock analysis in real-time mode as '--end-yrdoy' is not set" - ) + logging.info("Orbit and clock analysis in real-time mode as '--end-yrdoy' is not set") if session_len <= 0: - logging.error( - "'--session-len' must be specified for real-time mode ('--end-yrdoy' is not set)" - ) + logging.error("'--session-len' must be specified for real-time mode ('--end-yrdoy' is not set)") return if start_yrdoy is None: @@ -931,21 +1149,15 @@ def analyse_orbit_clock_main( start_date.isoweekday() % 7 ) # round down start date to nearest Sunday (start of a GPS week) - latency = ( - 2 * 86400 - ) # allow 2 days of latency for reference products to be available + latency = 2 * 86400 # allow 2 days of latency for reference products to be available while True: next_start_date = start_date + timedelta(days=session_len) end_date = next_start_date - timedelta(seconds=1) now = time.time() seconds = next_start_date.timestamp() - now + latency - logging.debug( - f"Start time of current session: {start_date}, end time of current session: {end_date}" - ) - logging.debug( - f"{seconds} seconds to wait (for reference products to be available) to start analysis" - ) + logging.debug(f"Start time of current session: {start_date}; end time of current session: {end_date}") + logging.debug(f"{seconds} seconds to wait (for reference products to be available) to start analysis") if seconds > 0: time.sleep(seconds) @@ -953,13 +1165,21 @@ def analyse_orbit_clock_main( analyse_orbit_clock( job_dir, ref_dir, + sub_jobs, ref_prefix, + file_types, start_date, session_len, sat_sys, excluded_svs, + orb_ref_frame, + orb_hlm_mode, + epochwise_hlm, clk_norm_types, rel_output_dir, + nominal_orb_ylim, + nominal_clk_ylim, + fix_ylim, ) start_date = next_start_date diff --git a/scripts/ssrMonitoring/auto_record_ssr_streams.py b/scripts/ssrMonitoring/auto_record_ssr_streams.py index 8e604560a..6ae3d6f4d 100644 --- a/scripts/ssrMonitoring/auto_record_ssr_streams.py +++ b/scripts/ssrMonitoring/auto_record_ssr_streams.py @@ -14,34 +14,35 @@ - Restart the same job using this master script (this will result in new running processes) """ -import json -import click import logging import subprocess -import gnssanalysis as ga -from pathlib import Path from datetime import datetime -from kill_pids import kill_pids +from pathlib import Path + +import click + +import gnssanalysis as ga +from kill_pids import kill_pids, save_pids +from analyse_orbit_clock import str_to_list @click.command() +@click.option("--job-dir", required=True, help="Directory where data is processed", type=Path) +@click.option("--product-dir", required=True, help="Directory where product files are placed", type=Path) +@click.option("--pea-dir", required=True, help="Directory where the PEA executable is placed", type=Path) +@click.option("--template-config", required=True, help="Path of template YAML config for PEA instances", type=Path) +@click.option("--ntrip-username", help="Username of NTRIP account", type=str) +@click.option("--ntrip-password", help="Password of NTRIP account", type=str) @click.option( - "--job-dir", required=True, help="Directory where data is processed", type=Path -) -@click.option( - "--product-dir", - required=True, - help="Directory where product files are placed", + "--ntrip-cred-file-path", + help='Path of NTRIP credential JSON file where username and password of NTRIP account is saved, with the format of \'{"username": "your_ntrip_username", "password": "your_ntrip_password"}\'. Required if --ntrip-username and --ntrip-password options are not specified', type=Path, ) @click.option( - "--template-config", + "--ssr-streams", required=True, - help="Path of template YAML config for PEA instances", - type=Path, -) -@click.option( - "--ssr-streams", required=True, help="SSR streams to monitor quality of", type=str + help="SSR streams to monitor quality of, provided with a comma separated string, e.g. 'SSRA00BKG0, SSRA02IGS0, SSRA03IGS0'", + type=str, ) @click.option( "--rotation-days", @@ -50,6 +51,13 @@ default=1, type=int, ) +@click.option( + "--ref-prefix", + required=True, + help="Prefix of reference products. Default: 'IGS0OPSRAP'", + default="IGS0OPSRAP", + type=str, +) @click.option( "--analysis-session-len", required=True, @@ -59,65 +67,65 @@ ) @click.option( "--align-to-gps-week", - help="Align gnssanalysis session to GPS week for weekly solutions. Only effective when session length is 7 days (weekly)", + help="Align gnssanalysis session to GPS week for weekly solutions. Only effective when session length is 7 days (weekly). Default: False", default=False, is_flag=True, ) @click.option( "--sat-sys", required=True, - help="Satellite system to analyse, G, R, E, C, or any combination without space", + help="Satellite system to analyse, 'G', 'R', 'E', 'C', or any combination without space. Default: 'G'", default="G", type=str, ) +@click.option( + "--orb-hlm-mode", + help="Helmert transformation to apply for orbit analysis, 'ECF', 'ECI', or 'none'. Default: 'none'", + default="none", + type=str, +) @click.option( "--clk-norm-types", - help="Normalisations to apply for clock files for clock analysis, e.g. 'none', 'sv', 'G01', 'epoch', 'daily', or any combination. Default: 'epoch, daily'", + help="Normalisations to apply for clock analysis, e.g. 'none', 'sv', 'G01', 'epoch', 'daily', or any combination. Default: 'epoch, daily'", default="epoch, daily", type=str, ) @click.option( "--min-upload-latency", - required=True, help="Minimum number of hours the files haven't been modified for before uploading to S3. Default: 36 (1.5 days)", default=36, type=int, ) @click.option( "--cull-file-types", - help="File types to cull for saving local space, e.g. '.rtcm, .rnx, .json'", + help="File types to cull for saving local space, e.g. '.rtcm, .rnx, .json'. Default: None", default=None, type=str, ) @click.option( "--aws-profile", - required=True, - help="Profile of credentials for target S3 bucket in AWS credentials file '~/.aws/credentials', 'default' will be used when no profile is specified", + help="Profile of credentials for target S3 bucket in AWS credentials file '~/.aws/credentials', 'default' will be used when no profile is specified. Default: 'default'", default="default", type=str, ) -@click.option( - "--s3-bucket", - required=True, - help="S3 bucket for uploading results", - type=str, -) -@click.option( - "--s3-root-dir", - required=True, - help="Root directory on S3 bucket for saving results", - type=Path, -) +@click.option("--s3-bucket", help="S3 bucket for uploading results", default=None, type=str) +@click.option("--s3-root-dir", help="Root directory on S3 bucket for saving results", default=None, type=Path) @click.option("--verbose", is_flag=True) def auto_record_ssr_streams_main( job_dir, product_dir, + pea_dir, template_config, + ntrip_username, + ntrip_password, + ntrip_cred_file_path, ssr_streams, rotation_days, + ref_prefix, analysis_session_len, align_to_gps_week, sat_sys, + orb_hlm_mode, clk_norm_types, min_upload_latency, cull_file_types, @@ -128,18 +136,24 @@ def auto_record_ssr_streams_main( ): ga.gn_utils.ensure_folders([job_dir]) - ssr_list = [] - if ssr_streams: - ssr_list = ssr_streams.replace(" ", "").split(",") + if ntrip_username is None: + ntrip_username = "" + if ntrip_password is None: + ntrip_password = "" + if ntrip_cred_file_path is None: + ntrip_cred_file_path = "" + if cull_file_types is None: + cull_file_types = "" + if s3_root_dir is None: + s3_root_dir = job_dir.name + + ssr_list = str_to_list(ssr_streams) rotation_period = str(rotation_days * 86400) analysis_session_len = str(analysis_session_len) align_to_gps_week_arg = ["--align-to-gps-week"] if align_to_gps_week else [] upload_time_threshold = str(min_upload_latency * 3600) - if cull_file_types is None: - cull_file_types = "" - ga.gn_utils.configure_logging(verbose) verbose_arg = ["--verbose"] if verbose else [] @@ -165,9 +179,7 @@ def auto_record_ssr_streams_main( ] + verbose_arg proc_list = proc_list + [ subprocess.Popen( - cmd, - stdout=(job_dir / f"download_rt_products_{timestamp}.log").open("w"), - stderr=subprocess.STDOUT, + cmd, stdout=(job_dir / f"download_rt_products_{timestamp}.log").open("w"), stderr=subprocess.STDOUT ) ] @@ -176,12 +188,20 @@ def auto_record_ssr_streams_main( [ "python", "record_ssr_stream.py", + "--pea-dir", + str(pea_dir), "--template-config", str(template_config), "--job-dir", str(job_dir), "--product-dir", str(product_dir), + "--ntrip-username", + ntrip_username, + "--ntrip-password", + ntrip_password, + "--ntrip-cred-file-path", + str(ntrip_cred_file_path), "--ssr-mountpoint", ssr, "--rotation-period", @@ -202,10 +222,14 @@ def auto_record_ssr_streams_main( str(job_dir), "--ref-dir", str(product_dir), + "--ref-prefix", + ref_prefix, "--session-len", analysis_session_len, "--sat-sys", sat_sys, + "--orb-hlm-mode", + orb_hlm_mode, "--clk-norm-types", clk_norm_types, ] @@ -214,46 +238,42 @@ def auto_record_ssr_streams_main( ) proc_list = proc_list + [ subprocess.Popen( - cmd, - stdout=(job_dir / f"analyse_orbit_clock_{timestamp}.log").open("w"), - stderr=subprocess.STDOUT, + cmd, stdout=(job_dir / f"analyse_orbit_clock_{timestamp}.log").open("w"), stderr=subprocess.STDOUT ) ] - # Command for uploading recordings - cmd = [ - "nohup", - "python", - "upload_recordings.py", - "--job-dir", - str(job_dir), - "--aws-profile", - aws_profile, - "--s3-bucket", - s3_bucket, - "--s3-root-dir", - str(s3_root_dir), - "--time-threshold", - upload_time_threshold, - "--interval", - rotation_period, - "--cull-file-types", - cull_file_types, - ] + verbose_arg - proc_list = proc_list + [ - subprocess.Popen( - cmd, - stdout=(job_dir / f"upload_recordings_{timestamp}.log").open("w"), - stderr=subprocess.STDOUT, - ) - ] + if not s3_bucket: + logging.info(f"'--s3-bucket' not specified. Outputs will be saved to '{job_dir}' only") + else: + # Command for uploading recordings + cmd = [ + "nohup", + "python", + "upload_recordings.py", + "--job-dir", + str(job_dir), + "--aws-profile", + aws_profile, + "--s3-bucket", + s3_bucket, + "--s3-root-dir", + str(s3_root_dir), + "--time-threshold", + upload_time_threshold, + "--interval", + rotation_period, + "--cull-file-types", + cull_file_types, + ] + verbose_arg + proc_list = proc_list + [ + subprocess.Popen( + cmd, stdout=(job_dir / f"upload_recordings_{timestamp}.log").open("w"), stderr=subprocess.STDOUT + ) + ] # Save PIDs of started commands to file to kill later - with pid_file_path.open("a") as pid_file: - for proc in proc_list: - json.dump({"PID": proc.pid, "command": " ".join(proc.args)}, pid_file) - pid_file.write("\n") - logging.debug(f"PIDs of started commands saved to file {pid_file_path}") + pid_list = [{"PID": proc.pid, "command": " ".join(proc.args)} for proc in proc_list] + save_pids(pid_file_path, pid_list) # Wait above commands to complete for proc in proc_list: diff --git a/scripts/ssrMonitoring/download_rt_products.py b/scripts/ssrMonitoring/download_rt_products.py index 3b7d90957..6c96575eb 100644 --- a/scripts/ssrMonitoring/download_rt_products.py +++ b/scripts/ssrMonitoring/download_rt_products.py @@ -8,18 +8,18 @@ Other files can be added in the future. It will run infinitely and attempt download periodically once it is started. """ +import logging import time +from pathlib import Path + import click -import logging + import gnssanalysis as ga -from pathlib import Path -def download_rt_products( - product_dir: Path, -) -> None: +def download_rt_products(product_dir: Path) -> None: """ - Download latest product files, including igs20.atx, igs_satellite_metadata.snx, and yaw files + Download latest product files, including 'igs20.atx', 'igs_satellite_metadata.snx', and yaw files for SSR streams recording. :param Path product_dir: Directory where downloaded product files to place @@ -30,9 +30,7 @@ def download_rt_products( model_dir = product_dir / "tables" # Download required products - ga.gn_download.download_atx( - download_dir=product_dir, long_filename=True, if_file_present="replace" - ) + ga.gn_download.download_atx(download_dir=product_dir, if_file_present="replace") ga.gn_download.download_satellite_metadata_snx(download_dir=product_dir, if_file_present="replace") ga.gn_download.download_yaw_files(download_dir=model_dir, if_file_present="replace") @@ -40,28 +38,19 @@ def download_rt_products( @click.command() -@click.option( - "--product-dir", - required=True, - help="Directory where product files are placed", - type=Path, -) +@click.option("--product-dir", required=True, help="Directory where product files are placed", type=Path) @click.option( "--interval", required=True, - help="Time interval to check and download products in seconds", + help="Time interval to check and download products in seconds. Default: 86400", default=86400, type=int, ) @click.option("--verbose", is_flag=True) -def download_rt_products_main( - product_dir, - interval, - verbose, -): +def download_rt_products_main(product_dir, interval, verbose): ga.gn_utils.configure_logging(verbose) - # Check every seconds if local files are outdated, and if so, download products + # Update/Download products every seconds while True: download_rt_products(product_dir) diff --git a/scripts/ssrMonitoring/kill_pids.py b/scripts/ssrMonitoring/kill_pids.py index 35cd4faa0..f7b9a37fa 100644 --- a/scripts/ssrMonitoring/kill_pids.py +++ b/scripts/ssrMonitoring/kill_pids.py @@ -6,28 +6,49 @@ recorded in a JSON file named 'pid.json' in the job directory. """ -import os import json -import click -import signal import logging -import gnssanalysis as ga +import os +import signal from pathlib import Path +import click + +import gnssanalysis as ga + -def kill_pids( - pid_file_path: Path, -) -> None: +def save_pids(pid_file_path: Path, proc_list: dict, overwrite: bool = False) -> None: """ - Kill running processes by their PIDs logged in a json file. + Save PIDs and corresponding commmands to a JSON file. - :param Path pid_file_path: Path of the json file where PIDs are logged + :param Path pid_file_path: Path of the JSON file to log PIDs to + :param dict proc_list: A dictionary containing the PIDs and commands of the processes to log + :param bool overwrite: Overwrite existing PID file, defaults to False + :return None + """ + mode = "w" if overwrite else "a" + + try: + with pid_file_path.open(mode) as pid_file: + for proc in proc_list: + json.dump(proc, pid_file) + pid_file.write("\n") + logging.info(f"PIDs of running commands saved to file '{pid_file_path}'") + except Exception as error: + logging.error(f"Could not write PID file: {error}") + + +def kill_pids(pid_file_path: Path) -> None: + """ + Kill running processes by their PIDs logged in a JSON file. + + :param Path pid_file_path: Path of the JSON file where PIDs are logged :return None """ logging.info("Killing running processes ...") if not pid_file_path.exists(): - logging.warning(f"PID file {pid_file_path} not found") + logging.warning(f"PID file '{pid_file_path}' not found") return # Kill running commands with PIDs listed in the PID file @@ -42,35 +63,24 @@ def kill_pids( os.kill(pid, signal.SIGTERM) logging.info(f"Process {pid}: '{arg}' killed") except ProcessLookupError as error: - logging.debug( - f"Process {pid}: '{arg}' not exists, may have terminated: {error}" - ) + logging.debug(f"Process {pid}: '{arg}' not exists, may have terminated: {error}") except Exception as error: procs_not_killed.append(proc) - logging.exception(f"Could not kill {pid}: '{arg}': {error}") + logging.warning(f"Could not kill {pid}: '{arg}': {error}") # Overwrite the PID file with PIDs not killed - with pid_file_path.open("w") as pid_file: - logging.debug(f"Updating {pid_file_path} with PIDs not killed") - for proc in procs_not_killed: - json.dump(proc, pid_file) - pid_file.write("\n") + if procs_not_killed: + logging.info(f"Updating '{pid_file_path}' with PIDs not killed") + + save_pids(pid_file_path, procs_not_killed, overwrite=True) logging.info("Processes killed\n") @click.command() -@click.option( - "--job-dir", - required=True, - help="Directory where PID file (pid.json) is saved", - type=Path, -) +@click.option("--job-dir", required=True, help="Directory where PID file (pid.json) is saved", type=Path) @click.option("--verbose", is_flag=True) -def kill_pids_main( - job_dir, - verbose, -): +def kill_pids_main(job_dir, verbose): ga.gn_utils.configure_logging(verbose) pid_file_path = job_dir / "pid.json" diff --git a/scripts/ssrMonitoring/record_ssr_stream.py b/scripts/ssrMonitoring/record_ssr_stream.py index d4323824d..824e4f897 100644 --- a/scripts/ssrMonitoring/record_ssr_stream.py +++ b/scripts/ssrMonitoring/record_ssr_stream.py @@ -6,19 +6,25 @@ """ import json -import click import logging import subprocess -import gnssanalysis as ga -import ruamel.yaml as yaml -from typing import Any -from pathlib import Path +import time from datetime import datetime +from pathlib import Path +from typing import Any + +import click +from ruamel.yaml import YAML + +import gnssanalysis as ga +from kill_pids import save_pids + +yaml = YAML(typ="safe", pure=True) +yaml.default_flow_style = False -def load_yaml( - yaml_path: Path, -) -> Any: + +def load_yaml(yaml_path: Path) -> Any: """ Load a YAML file to a dictionary-like object. @@ -26,14 +32,11 @@ def load_yaml( :return Any: YAML config options as a dictionary-like object """ with yaml_path.open("r", encoding="utf-8") as stream: - yaml_dict = yaml.safe_load(stream) + yaml_dict = yaml.load(stream) return yaml_dict -def write_yaml( - yaml_dict: Any, - yaml_path: Path, -) -> None: +def write_yaml(yaml_dict: Any, yaml_path: Path) -> None: """ Write config options saved in a dictionary-like object to a YAML file. @@ -42,64 +45,146 @@ def write_yaml( :return None """ with yaml_path.open("w", encoding="utf-8") as outfile: - yaml.safe_dump(yaml_dict, outfile, default_flow_style=False) + yaml.dump(yaml_dict, outfile) + + +def update_config( + template_config: Path, + config_path: Path, + ntrip_username: str, + ntrip_password: str, + output_dir: Path, + product_dir: Path, + bcep_mountpoint: str, + ssr_mountpoint: str, + rotation_period: int, + output_json: bool, + interval: int, + max_epochs: int = 0, +) -> None: + """ + Update config options from a template YAML file so that the PEA can record and decode a SSR stream + based on the input options. The updated config options will be saved to a new YAML file. + + :param str ntrip_username: Username of NTRIP account + :param str ntrip_password: Password of NTRIP account + :param Path template_config: Path of template YAML config + :param Path config_path: Path of the YAML file to write config options to + :param Path output_dir: Directory to save the PEA outputs + :param Path product_dir: Directory where product files are placed + :param str bcep_mountpoint: Mountpoint of broadcast ephemeris stream + :param str ssr_mountpoint: Mountpoint of the SSR stream to record + :param int rotation_period: Recording length of each output file in seconds + :param bool output_json: Log decoded messages into JSON files + :param int interval: Epoch interval of data processing in seconds + :param int max_epochs: Maximum number of epochs to process, defaults to 0 (infinite) + :return None + """ + logging.info(f"Writing config '{config_path}'") + + # Load config + config = load_yaml(template_config) + + if interval <= 0: + interval = config["processing_options"]["epoch_control"]["epoch_interval"] + # if not bcep_mountpoint: + # bcep_mountpoint = config["inputs"]["satellite_data"]["rtcm_inputs"]["rtcm_inputs"][0] + + # Inputs + config["inputs"]["inputs_root"] = str(product_dir) + config["inputs"]["satellite_data"]["rtcm_inputs"]["rtcm_inputs"] = [bcep_mountpoint, ssr_mountpoint] + config["inputs"]["satellite_data"]["rtcm_inputs"]["ssr_antenna_offset"] = ( + "APC" if ssr_mountpoint[:4] == "SSRA" else "COM" if ssr_mountpoint[:4] == "SSRC" else "UNSPECIFIED" + ) + + # Outputs + config["outputs"]["metadata"]["config_description"] = ssr_mountpoint + config["outputs"]["metadata"]["user"] = ntrip_username + config["outputs"]["metadata"]["pass"] = ntrip_password + config["outputs"]["outputs_root"] = str(output_dir) + config["outputs"]["output_rotation"]["period"] = rotation_period + config["outputs"]["decoded_rtcm"]["output"] = output_json + config["outputs"]["rinex_nav"]["filename"] = f"{bcep_mountpoint}__NAV.rnx" + + # Processing options + config["processing_options"]["epoch_control"]["epoch_interval"] = interval + config["processing_options"]["epoch_control"]["max_epochs"] = max_epochs + + # Write config + write_yaml(config, config_path) + logging.info(f"Config written to '{config_path}'") + + +def record_ssr_stream( + pea_dir: Path, config_path: Path, output_dir: Path, pid_file_path: Path, ssr_mountpoint: str +) -> None: + """Record and decode a SSR stream in real-time with the PEA + + :param Path pea_dir: Directory where the PEA executable is placed + :param Path config_path: Path of the YAML file to write config options to + :param Path output_dir: Directory to save the PEA outputs + :param Path pid_file_path: Path of the JSON file to log PIDs to + :param str ssr_mountpoint: Mountpoint of the SSR stream to record + :return None + """ + logging.info(f"Running PEA for '{ssr_mountpoint}' ...") + + now = datetime.now() + yrdoy = now.strftime("%Y%j") + timestamp = f"{yrdoy}{now.hour:02d}{now.minute:02d}" + + # Process + pea_path = pea_dir / "pea" + proc = subprocess.Popen( + ["nohup", str(pea_path), "--config", str(config_path)], + stdout=(output_dir / f"record_ssr_stream_{timestamp}.log").open("w"), + stderr=subprocess.STDOUT, + ) + + # Save PID of started PEA instance to file to kill later + pid_list = [{"PID": proc.pid, "command": " ".join(proc.args)}] + save_pids(pid_file_path, pid_list) + + proc.wait() + + logging.info("PEA completed\n") @click.command() +@click.option("--pea-dir", required=True, help="Directory where the PEA executable is placed", type=Path) +@click.option("--template-config", required=True, help="Path of template YAML config", type=Path) +@click.option("--job-dir", required=True, help="Directory where data is processed", type=Path) +@click.option("--product-dir", required=True, help="Directory where product files are placed", type=Path) +@click.option("--ntrip-username", help="Username of NTRIP account", type=str) +@click.option("--ntrip-password", help="Password of NTRIP account", type=str) @click.option( - "--template-config", required=True, help="Path of template YAML config", type=Path -) -@click.option( - "--job-dir", required=True, help="Directory where data is processed", type=Path -) -@click.option( - "--product-dir", - required=True, - help="Directory where product files are placed", + "--ntrip-cred-file-path", + help='Path of NTRIP credential JSON file where username and password of NTRIP account is saved, with the format of \'{"username": "your_ntrip_username", "password": "your_ntrip_password"}\'. Required if --ntrip-username and --ntrip-password options are not specified', type=Path, ) @click.option( "--bcep-mountpoint", required=True, - help="Mountpoint of broadcast ephemeris stream", + help="Mountpoint of broadcast ephemeris stream. Default: 'BCEP00BKG0'", default="BCEP00BKG0", type=str, ) +@click.option("--ssr-mountpoint", required=True, help="Mountpoint of the SSR stream to record", type=str) +@click.option("--output-json", help="Log decoded messages into JSON files. Default: True", default=True, is_flag=True) @click.option( - "--ssr-mountpoint", - required=True, - help="Mountpoint of SSR stream to record", - type=str, -) -@click.option( - "--output-json", - help="Log decoded messages into json files", - default=True, - is_flag=True, -) -@click.option( - "--rotation-period", - help="Recording length of each output file in seconds", - default=86400, - type=int, -) -@click.option( - "--interval", - help="Epoch interval of data processing in seconds", - default=1, - type=int, -) -@click.option( - "--max-epochs", - help="Maximum number of epochs to process, default is infinite", - default=0, - type=int, + "--rotation-period", help="Recording length of each output file in seconds. Default: 86400", default=86400, type=int ) +@click.option("--interval", help="Epoch interval of data processing in seconds. Default: 0", default=0, type=int) +@click.option("--max-epochs", help="Maximum number of epochs to process. Default: 0 (infinite)", default=0, type=int) @click.option("--verbose", is_flag=True) def record_ssr_stream_main( + pea_dir, template_config, job_dir, product_dir, + ntrip_username, + ntrip_password, + ntrip_cred_file_path, bcep_mountpoint, ssr_mountpoint, output_json, @@ -110,70 +195,73 @@ def record_ssr_stream_main( ): ga.gn_utils.configure_logging(verbose) - now = datetime.now() - yrdoy = now.strftime("%Y%j") - timestamp = f"{yrdoy}{now.hour:02d}{now.minute:02d}" + # Prepare output directory + output_dir = job_dir / ssr_mountpoint + ga.gn_utils.ensure_folders([output_dir]) - # Prepare job folder and config - sub_job_dir = job_dir / ssr_mountpoint - ga.gn_utils.ensure_folders([sub_job_dir]) + if not (ntrip_username and ntrip_password): + if ntrip_cred_file_path.is_dir(): + logging.error( + "NTRIP credentials not provided, please either specify NTRIP username and password" + "or the JSON file containing NTRIP credentials in following format:\n" + '{"username": "your_ntrip_username", "password": "your_ntrip_password"}' + ) + return + elif not ntrip_cred_file_path.exists(): + logging.error(f"NTRIP credential file '{ntrip_cred_file_path}' not found") + return + else: + with ntrip_cred_file_path.open("r") as ntrip_cred_file: + for line in ntrip_cred_file: + cred = json.loads(line) + ntrip_username = cred["username"] + ntrip_password = cred["password"] + if ntrip_username and ntrip_password: + break - config_path = sub_job_dir / f"record_ssr_stream_{ssr_mountpoint}.yaml" - - logging.info(f"Writing config {config_path}") - - # Load config - config = load_yaml(template_config) - - # Inputs - config["inputs"]["inputs_root"] = str(product_dir) - config["inputs"]["satellite_data"]["rtcm_inputs"]["rtcm_inputs"] = [ + # Prepare config + config_path = output_dir / f"record_ssr_stream_{ssr_mountpoint}.yaml" + update_config( + template_config, + config_path, + ntrip_username, + ntrip_password, + output_dir, + product_dir, bcep_mountpoint, ssr_mountpoint, - ] - config["inputs"]["satellite_data"]["rtcm_inputs"]["ssr_antenna_offset"] = ( - "APC" - if ssr_mountpoint[:4] == "SSRA" - else "COM" if ssr_mountpoint[:4] == "SSRC" else "UNSPECIFIED" + rotation_period, + output_json, + interval, + max_epochs, ) - # Outputs - config["outputs"]["metadata"]["config_description"] = ssr_mountpoint - config["outputs"]["outputs_root"] = str(sub_job_dir) - config["outputs"]["output_rotation"]["period"] = rotation_period - config["outputs"]["decoded_rtcm"]["output"] = output_json - config["outputs"]["rinex_nav"][ - "filename" - ] = f"{bcep_mountpoint}__NAV.rnx" + # Check if input files are available before processing + config = load_yaml(config_path) - # Processing options - config["processing_options"]["epoch_control"]["epoch_interval"] = interval - config["processing_options"]["epoch_control"]["wait_next_epoch"] = interval - config["processing_options"]["epoch_control"]["max_epochs"] = max_epochs + input_files = [] + input_files = input_files + config["inputs"]["atx_files"] + input_files = input_files + config["inputs"]["snx_files"] - # Write config - write_yaml(config, config_path) - logging.info(f"Config written to {config_path}") + while True: + inputs_ready = True - logging.info(f"Running PEA for {ssr_mountpoint} ...") + for file in input_files: + path = product_dir / file + if not path.exists(): + inputs_ready = False + logging.warning( + f"Input file '{path}' not found. Waiting 30 seconds for it to be downloaded or manually provided ..." + ) - # Process - proc = subprocess.Popen( - ["nohup", "/data/acs2/ginan/bin/pea", "--config", str(config_path)], - stdout=(sub_job_dir / f"record_ssr_stream_{timestamp}.log").open("w"), - stderr=subprocess.STDOUT, - ) + if inputs_ready: + break + else: + time.sleep(30) - # Save PID of started PEA instance to file to kill later + # Run the PEA to record SSR stream pid_file_path = job_dir / "pid.json" - with pid_file_path.open("a") as pid_file: - json.dump({"PID": proc.pid, "command": " ".join(proc.args)}, pid_file) - pid_file.write("\n") - logging.debug(f"PID of started PEA instance saved to file {pid_file_path}") - - proc.wait() - - logging.info("PEA completed\n") + record_ssr_stream(pea_dir, config_path, output_dir, pid_file_path, ssr_mountpoint) if __name__ == "__main__": diff --git a/scripts/ssrMonitoring/upload_recordings.py b/scripts/ssrMonitoring/upload_recordings.py index 4389c69d1..bf258ac7a 100644 --- a/scripts/ssrMonitoring/upload_recordings.py +++ b/scripts/ssrMonitoring/upload_recordings.py @@ -1,25 +1,26 @@ #!/usr/bin/env python3 """ -Script to upload outputs and log files in the job folder to an AWS S3 bucket. +Script to upload outputs and log files in the job directory to an AWS S3 bucket. It will run infinitely and attempt upload periodically once it is started. """ +import logging import os +import re import time +from datetime import datetime +from pathlib import Path +from typing import Any, Tuple + import boto3 import click -import logging + import gnssanalysis as ga -from typing import Tuple, Any -from pathlib import Path -from datetime import datetime +from analyse_orbit_clock import str_to_list -def file_ready_to_upload( - local_path: Path, - time_threshold: int, -) -> Tuple[bool, int]: +def file_ready_to_upload(local_path: Path, time_threshold: int) -> Tuple[bool, int]: """ Check if a local file is ready to upload to S3 bucket, i.e. has not been modified for the specified time period. @@ -47,10 +48,7 @@ def file_ready_to_upload( def file_up_to_date_s3( - s3_client: Any, - s3_bucket: str, - dest_path: Path, - timestamp: int, + s3_client: Any, s3_bucket: str, dest_path: Path, timestamp: int, no_logging: bool = False ) -> bool: """ Check if a file exists on S3 bucket and up-to-date by comparing last modified time against the specified timestamp. @@ -59,33 +57,35 @@ def file_up_to_date_s3( :param str s3_bucket: S3 bucket for uploading the file :param Path dest_path: Path of the destination file to save on the S3 bucket :param int timestamp: Timestamp to compare with + :param bool no_logging: Do not log :return bool: True if the file is up-to-date and False otherwise """ - object = s3_client.list_objects_v2( - Bucket=s3_bucket, Prefix=str(dest_path), MaxKeys=1 - ).get("Contents", []) + object = s3_client.list_objects_v2(Bucket=s3_bucket, Prefix=str(dest_path), MaxKeys=1).get("Contents", []) if object: modified_time_remote = object[0]["LastModified"].timestamp() if modified_time_remote > timestamp: - logging.info( - f"Remote file is up-to-date, last modified: {datetime.fromtimestamp(modified_time_remote)}, ommitting upload" + ( + logging.info( + f"Remote file is up-to-date, last modified: {datetime.fromtimestamp(modified_time_remote)}, ommitting upload" + ) + if not no_logging + else None ) return True else: - logging.warning( - f"Remote file is outdated, last modified: {datetime.fromtimestamp(modified_time_remote)}, will be overwritten" + ( + logging.warning( + f"Remote file is outdated, last modified: {datetime.fromtimestamp(modified_time_remote)}, will be overwritten" + ) + if not no_logging + else None ) return False -def upload_file_to_s3( - s3_client: Any, - s3_bucket: str, - local_path: Path, - dest_path: Path, -) -> bool: +def upload_file_to_s3(s3_client: Any, s3_bucket: str, local_path: Path, dest_path: Path) -> bool: """ Upload a local file to target S3 bucket. @@ -100,37 +100,22 @@ def upload_file_to_s3( logging.info("Successfully uploaded") return True except boto3.exceptions.S3UploadFailedError as error: - logging.exception(f"Unable to upload: {error}") + logging.error(f"Unable to upload: {error}") return False -def cull_local_files( - s3_client: Any, - s3_bucket: str, - dest_path: Path, - local_path: Path, - cull_file_types: list[str], - excluded_file: Path, -) -> None: - """ +def cull_local_file(local_path: Path, cull_file_types: list[str], excluded_files: str) -> None: + r""" Delete old local files to save space. - :param Any s3_client: Object for target S3 client - :param str s3_bucket: S3 bucket for uploading the file - :param Path dest_path: Path of the destination file to save on the S3 bucket :param Path local_path: Path of the local file to upload :param list[str] cull_file_types: File types to cull for saving local space, e.g. '.rtcm, .rnx, .json' - :param Path excluded_file: File to exclude from culling + :param str excluded_files: Files to exclude from culls based on regex, e.g. 'a.*txt', 'a\\.txt|b\\.out', '[a-z]\\.txt' :return None """ - if local_path == excluded_file: + if re.fullmatch(excluded_files, local_path.name): return - # Confirm that the file exists on s3 before deleting # todo Eugene: move out? - object = s3_client.list_objects_v2( - Bucket=s3_bucket, Prefix=str(dest_path), MaxKeys=1 - ).get("Contents", []) - if object and local_path.suffix in cull_file_types: logging.info("Deleting the local file") local_path.unlink() @@ -142,9 +127,10 @@ def upload_recordings( s3_root_dir: Path, time_threshold: int, cull_file_types: list[str], + exclude_culls: str, aws_profile: str = "default", ) -> None: - """ + r""" Upload SSR recordings and orbit and clock analysis outputs. :param Path job_dir: Directory where recordings and analysis outputs are saved @@ -152,8 +138,9 @@ def upload_recordings( :param Path s3_root_dir: Root directory on S3 bucket for saving results :param int time_threshold: Number of seconds the files haven't been modified for before uploading to S3 :param list[str] cull_file_types: File types to cull for saving local space, e.g. '.rtcm, .rnx, .json' + :param str exclude_culls: Files to exclude from culls based on regex, e.g. 'a.*txt', 'a\\.txt|b\\.out', '[a-z]\\.txt' :param str aws_profile: Profile of credentials for target S3 bucket in AWS credentials file '~/.aws/credentials', - default to 'default' + defaults to 'default' :return None """ logging.info("Uploading recording results ...") @@ -161,6 +148,9 @@ def upload_recordings( # S3 client s3_client = boto3.Session(profile_name=aws_profile).client("s3") + if not s3_root_dir: + s3_root_dir = job_dir.name + # Upload recordings to S3 bucket and cull old files if needed num = 0 for path, subdirs, files in os.walk( @@ -171,87 +161,67 @@ def upload_recordings( s3_dir = path.replace(str(job_dir), str(s3_root_dir)) dest_path = Path(s3_dir) / file - logging.info(f"{local_path} --> s3://{s3_bucket}/{dest_path}") + logging.info(f"'{local_path}' --> 's3://{s3_bucket}/{dest_path}'") # Check last modified time of the local file - ready, modified_time_local = file_ready_to_upload( - local_path, time_threshold - ) - if ( - not ready - ): # only upload files that haven't been modified within the time threshold + ready, modified_time_local = file_ready_to_upload(local_path, time_threshold) + if not ready: # only upload files that haven't been modified within the time threshold continue # Upload the file to s3 bucket if not exists or outdated on s3 - if not file_up_to_date_s3( - s3_client, s3_bucket, dest_path, modified_time_local - ): - uploaded = upload_file_to_s3( - s3_client, s3_bucket, local_path, dest_path - ) + if not file_up_to_date_s3(s3_client, s3_bucket, dest_path, modified_time_local): + uploaded = upload_file_to_s3(s3_client, s3_bucket, local_path, dest_path) if uploaded: num = num + 1 # Cull old recordings - excluded_file = job_dir / "pid.json" # do not delete pid.json - cull_local_files( - s3_client, - s3_bucket, - dest_path, - local_path, - cull_file_types, - excluded_file, - ) + if file_up_to_date_s3( + s3_client, s3_bucket, dest_path, modified_time_local, no_logging=True + ): # confirm that the file exists on s3 before deleting + cull_local_file(local_path, cull_file_types, exclude_culls) logging.info(f"{num} file(s) uploaded\n") @click.command() -@click.option( - "--job-dir", - required=True, - help="Directory under which files to upload", - type=Path, -) +@click.option("--job-dir", required=True, help="Directory under which files to upload", type=Path) @click.option( "--aws-profile", required=True, - help="Profile of credentials for target S3 bucket in AWS credentials file '~/.aws/credentials', 'default' will be used when no profile is specified", + help="Profile of credentials for target S3 bucket in AWS credentials file '~/.aws/credentials', 'default' will be used when no profile is specified. Default: 'default'", default="default", type=str, ) +@click.option("--s3-bucket", required=True, help="S3 bucket for uploading results", type=str) @click.option( - "--s3-bucket", - required=True, - help="S3 bucket for uploading results", - type=str, -) -@click.option( - "--s3-root-dir", - required=True, - help="Root directory on S3 bucket for saving results", - type=Path, + "--s3-root-dir", help="Root directory on S3 bucket for saving results. Default: None", default=None, type=Path ) @click.option( "--time-threshold", required=True, - help="Number of seconds the files haven't been modified for before uploading to S3", + help="Number of seconds the files haven't been modified for before uploading to S3. Default: 86400", default=86400, type=int, ) @click.option( "--interval", required=True, - help="Interval to check and upload recordings in seconds", + help="Interval to check and upload recordings in seconds. Default: 86400", default=86400, type=int, ) @click.option( "--cull-file-types", - help="File types to cull for saving local space, e.g. '.rtcm, .rnx, .json'", + help="File types to cull for saving local space, e.g. '.rtcm, .rnx, .json'. Default: None", default=None, type=str, ) +@click.option( + "--exclude-culls", + help=r"Files to exclude from culls based on regex, e.g. 'a.*txt', 'a\.txt|b\.out', '[a-z]\.txt'. Default: 'pid\.json'", + default=r"pid\.json", + type=str, +) @click.option("--verbose", is_flag=True) def upload_recordings_main( job_dir, @@ -261,25 +231,16 @@ def upload_recordings_main( time_threshold, interval, cull_file_types, + exclude_culls, verbose, ): ga.gn_utils.configure_logging(verbose) - if cull_file_types: - cull_file_types = cull_file_types.replace(" ", "").split(",") - else: - cull_file_types = [] + cull_file_types = str_to_list(cull_file_types) # Run the scheduled tasks indefinitely while True: - upload_recordings( - job_dir, - s3_bucket, - s3_root_dir, - time_threshold, - cull_file_types, - aws_profile, - ) + upload_recordings(job_dir, s3_bucket, s3_root_dir, time_threshold, cull_file_types, exclude_culls, aws_profile) now = time.time() seconds = interval - now % interval diff --git a/src/Architecture/architectureDocs.hpp b/src/Architecture/architectureDocs.hpp index 1ed2eb2ef..da5c28bf6 100644 --- a/src/Architecture/architectureDocs.hpp +++ b/src/Architecture/architectureDocs.hpp @@ -16,3 +16,8 @@ if (doDocs) ref(); extern bool doDocs; + +// Stub for Mongo_Database__() when MongoDB is not enabled +#ifndef ENABLE_MONGODB +inline void Mongo_Database__() {} +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f3bfb3ddc..0a180ee2e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,21 +1,33 @@ -cmake_minimum_required(VERSION 2.8...3.22) +cmake_minimum_required(VERSION 3.22) cmake_policy(SET CMP0063 NEW) -list(APPEND CMAKE_PREFIX_PATH "/usr/local/opt/openblas") project(ginan) +# Set C++ standard before any other configuration +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(CheckPIESupported) check_pie_supported() -set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/../../lib") -set (CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/../../lib") -set (CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/../../bin") +# 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_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VERBOSE_MAKEFILE OFF) -if (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") - message(STATUS "Using Clang complient compiler flags") +# Platform-specific compiler flags +if(WIN32) + # Windows-specific flags + message(STATUS "Using Windows compiler flags") + # Note: WIN32_WINNT and LEAN_AND_MEAN are set in toolchain file + # Disable some warnings on Windows + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-pragmas") +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + message(STATUS "Using Clang compliant compiler flags") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-shift-overflow") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-string-concatenation") @@ -27,7 +39,6 @@ else () endif () - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a") # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb3") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpermissive") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer") @@ -53,8 +64,12 @@ endif () if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g") +endif () - option(BUILD_DOC "BUILD_DOCUMENTATION" OFF) +option(BUILD_DOC "BUILD_DOCUMENTATION" OFF) if (CMAKE_BUILD_TYPE STREQUAL "Debug") option(ENABLE_PARALLELISATION "ENABLE_PARALLELISATION" OFF) @@ -92,7 +107,21 @@ endif() if(ENABLE_PARALLELISATION) message(STATUS "Setting parallelisation on") - find_package(OpenMP REQUIRED) + # For cross-compilation, OpenMP tests may fail but the library works + if(CMAKE_CROSSCOMPILING) + set(OpenMP_C_FLAGS "-fopenmp -static-libgcc") + set(OpenMP_CXX_FLAGS "-fopenmp -static-libgcc") + set(OpenMP_C_LIB_NAMES "gomp") + set(OpenMP_CXX_LIB_NAMES "gomp") + set(OpenMP_gomp_LIBRARY "gomp") + find_package(OpenMP) + # For Windows cross-compile, link everything statically + if(WIN32) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") + endif() + else() + find_package(OpenMP REQUIRED) + endif() set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") else() @@ -159,20 +188,57 @@ endif() set(YAML_CPP_USE_STATIC_LIBS ON) find_package(YAML_CPP 0.6.2 REQUIRED) +# OpenSSL - use MODULE mode for vcpkg compatibility (wrapper handles it) find_package(OpenSSL REQUIRED) #set(Boost_NO_SYSTEM_PATHS ON) set(Boost_USE_STATIC_LIBS ON) -find_package(Boost 1.73.0 REQUIRED COMPONENTS log log_setup date_time system thread program_options serialization timer stacktrace_addr2line) +find_package(Boost 1.75.0 REQUIRED COMPONENTS log log_setup date_time system thread program_options serialization timer json) -find_package(Eigen3 3.3.0) +# Try CONFIG mode first (for vcpkg), fall back to module mode (for brew/system) +find_package(Eigen3 3.3.0 CONFIG QUIET) +if(NOT Eigen3_FOUND) + find_package(Eigen3 3.3.0) +endif() include_directories(${EIGEN3_INCLUDE_DIRS}) -find_package(mongocxx REQUIRED) -find_package(bsoncxx REQUIRED) +# MongoDB is optional - platforms like MinGW/Windows may not support it +# Users can force it off by setting ENABLE_MONGODB=OFF +# On Windows, MongoDB is disabled by default +if(WIN32) + option(ENABLE_MONGODB "Enable MongoDB support" OFF) +else() + option(ENABLE_MONGODB "Enable MongoDB support" ON) +endif() + +if(ENABLE_MONGODB) + # Try CONFIG mode first (for vcpkg), fall back to module mode (for brew/system) + find_package(mongocxx CONFIG QUIET) + if(mongocxx_FOUND) + find_package(bsoncxx CONFIG QUIET) + if(bsoncxx_FOUND) + set(ENABLE_MONGODB ON) + message(STATUS "MongoDB support enabled") + else() + message(STATUS "MongoDB bsoncxx not found - building without MongoDB support") + set(ENABLE_MONGODB OFF) + endif() + else() + message(STATUS "MongoDB mongocxx not found - building without MongoDB support") + set(ENABLE_MONGODB OFF) + endif() +else() + message(STATUS "MongoDB support disabled by user") +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() -set (NETCDF_CXX "YES") -find_package(NetCDF REQUIRED) # message(STATUS "...NETCDF >>>>>> ${NETCDF_LIBRARIES} ${NETCDF_INCLUDES}" ) # message(STATUS "...NETCDF_C++ >>>>>> ${NETCDF_LIBRARIES_CXX} ${NETCDF_INCLUDES_CXX}" ) @@ -181,15 +247,20 @@ find_package(NetCDF REQUIRED) # set(OPENBLAS_USE_STATIC_LIBS ON) # set(BLA_VENDOR open) -find_package(BLAS) -if(BLAS_FOUND) - set(LAPACK_LIBRARIES "") - message(STATUS "Found BLAS library: " ${BLA_VENDOR}) +find_package(BLAS REQUIRED) + +# Try to find LAPACK - OpenBLAS includes LAPACK so it may be found there +find_package(LAPACK QUIET) + +if(LAPACK_FOUND) + message(STATUS "Found BLAS library: ${BLAS_LIBRARIES}") + message(STATUS "Found LAPACK library: ${LAPACK_LIBRARIES}") else() - set (BLA_VENDOR "") - find_package(LAPACK REQUIRED) - find_package(BLAS REQUIRED) - message(STATUS "Found LAPACK and BLAS") + # 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}) endif() if (YAML_CPP_LIB) @@ -222,73 +293,25 @@ else() message(STATUS "Mongocxx was not found") endif() - message(STATUS "Found C++ compiler: " ${CMAKE_CXX_COMPILER_ID} " " ${CMAKE_CXX_COMPILER_VERSION}) - - -IF(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/../.git) - FIND_PACKAGE(Git) - IF(GIT_FOUND) - - EXECUTE_PROCESS( - COMMAND ${GIT_EXECUTABLE} log --pretty=format:'%H' -n 1 - OUTPUT_VARIABLE GINAN_COMMIT_HASH - ERROR_QUIET - OUTPUT_STRIP_TRAILING_WHITESPACE - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - EXECUTE_PROCESS( - COMMAND bash -c "git diff --quiet --exit-code || echo -dirty " - OUTPUT_VARIABLE GINAN_COMMIT_DIFF - OUTPUT_STRIP_TRAILING_WHITESPACE - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - EXECUTE_PROCESS( - COMMAND ${GIT_EXECUTABLE} describe --exact-match --tags - OUTPUT_VARIABLE GINAN_COMMIT_TAG ERROR_QUIET - OUTPUT_STRIP_TRAILING_WHITESPACE - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - EXECUTE_PROCESS( - COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD - OUTPUT_VARIABLE GINAN_BRANCH_NAME - OUTPUT_STRIP_TRAILING_WHITESPACE - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - string(FIND "${GINAN_BRANCH_NAME}" "/" SLASH_POSITION) - if(SLASH_POSITION GREATER -1) - string(SUBSTRING "${GINAN_BRANCH_NAME}" ${SLASH_POSITION} -1 GINAN_BRANCH_NAME) - string(SUBSTRING "${GINAN_BRANCH_NAME}" 1 -1 GINAN_BRANCH_NAME) # Remove the leading '/' - endif() - EXECUTE_PROCESS( - COMMAND ${GIT_EXECUTABLE} log -1 --format=%cd --date=local - OUTPUT_VARIABLE "GINAN_COMMIT_DATE" - OUTPUT_STRIP_TRAILING_WHITESPACE - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - - if (GINAN_COMMIT_TAG STREQUAL "") - set(GINAN_COMMIT_TAG "untagged") - endif() - string(REGEX REPLACE "'" "" GINAN_COMMIT_HASH "${GINAN_COMMIT_HASH}") - - SET(GINAN_COMMIT_VERSION "${GINAN_COMMIT_TAG}-${GINAN_COMMIT_HASH}${GINAN_COMMIT_DIFF}") - MESSAGE(STATUS "Git branch tag: ${GINAN_COMMIT_VERSION}") - MESSAGE(STATUS "Git branch: ${GINAN_BRANCH_NAME}") - ELSE(GIT_FOUND) - SET(GINAN_COMMIT_VERSION "N/A") - SET(GINAN_COMMIT_DIFF "N/A") - SET(GINAN_COMMIT_TAG "N/A") - SET(GINAN_BRANCH_NAME "N/A") - MESSAGE(STATUS "Git not found: ${GINAN_COMMIT_VERSION}") - ENDIF(GIT_FOUND) -ELSE() - MESSAGE( STATUS "Git not found in ${CMAKE_CURRENT_SOURCE_DIR}/..") -ENDIF() - - -link_directories(/usr/lib64 ${CMAKE_BINARY_DIR}/../../lib) - -CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/cpp/pea/peaCommitVersion.h.in ${CMAKE_CURRENT_SOURCE_DIR}/cpp/pea/peaCommitVersion.h @ONLY) + + +message(STATUS "Found C++ compiler: " ${CMAKE_CXX_COMPILER_ID} " " ${CMAKE_CXX_COMPILER_VERSION}) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/cpp/pea/peaLibVersion.h.in ${CMAKE_CURRENT_SOURCE_DIR}/cpp/pea/peaLibVersion.h @ONLY) + +include(GitVersion) + +# Ensure the file with the commit version is generated before compiling +add_custom_target(updateGit ALL + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/GitVersion.cmake + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Updating git version" +) + +# Make sure it regenerates every time the project is built + +link_directories(/usr/lib64 ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + +# CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/cpp/pea/peaCommitVersion.h.in ${CMAKE_CURRENT_SOURCE_DIR}/cpp/pea/peaCommitVersion.h @ONLY) add_subdirectory(cpp) diff --git a/src/CMakePresets.json b/src/CMakePresets.json new file mode 100644 index 000000000..fe3ebd9b9 --- /dev/null +++ b/src/CMakePresets.json @@ -0,0 +1,302 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 21, + "patch": 0 + }, + "configurePresets": [ + { + "name": "vcpkg-base", + "hidden": true, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "linux-base", + "hidden": true, + "inherits": "vcpkg-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-linux", + "VCPKG_INSTALLED_DIR": "${sourceDir}/../vcpkg_installed" + } + }, + { + "name": "windows-base", + "hidden": true, + "inherits": "vcpkg-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-windows" + } + }, + { + "name": "macos-base", + "hidden": true, + "inherits": "vcpkg-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos-arm64", + "hidden": true, + "inherits": "macos-base", + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "arm64-osx", + "CMAKE_OSX_ARCHITECTURES": "arm64", + "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/toolchain/clang_mac_arm64.cmake", + "VCPKG_INSTALLED_DIR": "${sourceDir}/../vcpkg_installed" + } + }, + { + "name": "macos-x64", + "hidden": true, + "inherits": "macos-base", + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-osx", + "CMAKE_OSX_ARCHITECTURES": "x86_64", + "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/toolchain/clang_mac_x64.cmake", + "VCPKG_INSTALLED_DIR": "${sourceDir}/../vcpkg_installed" + } + }, + { + "name": "mingw-cross-base", + "hidden": true, + "inherits": "vcpkg-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Windows", + "CMAKE_C_COMPILER": "x86_64-w64-mingw32-gcc", + "CMAKE_CXX_COMPILER": "x86_64-w64-mingw32-g++", + "CMAKE_RC_COMPILER": "x86_64-w64-mingw32-windres", + "CMAKE_FIND_ROOT_PATH_MODE_PROGRAM": "NEVER", + "CMAKE_FIND_ROOT_PATH_MODE_LIBRARY": "ONLY", + "CMAKE_FIND_ROOT_PATH_MODE_INCLUDE": "ONLY", + "VCPKG_TARGET_TRIPLET": "x64-mingw-static", + "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/toolchain/mingw64.cmake", + "VCPKG_INSTALLED_DIR": "${sourceDir}/../vcpkg_installed", + "ENABLE_MONGODB": "OFF" + } + }, + { + "name": "mingw-native-base", + "hidden": true, + "inherits": "vcpkg-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++", + "VCPKG_TARGET_TRIPLET": "x64-mingw-static", + "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/toolchain/mingw64.cmake", + "VCPKG_INSTALLED_DIR": "${sourceDir}/../vcpkg_installed", + "ENABLE_MONGODB": "OFF" + } + }, + { + "name": "release", + "displayName": "Release", + "description": "Release build using vcpkg", + "binaryDir": "${sourceDir}/build/linux-Release", + "inherits": ["linux-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "debug", + "displayName": "Debug", + "description": "Debug build using vcpkg", + "binaryDir": "${sourceDir}/build/linux-Debug", + "inherits": ["linux-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-release", + "displayName": "Windows Release", + "description": "Release build for Windows", + "binaryDir": "${sourceDir}/build/windows-Release", + "inherits": ["windows-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_EXE_LINKER_FLAGS": "-static" + } + }, + { + "name": "windows-debug", + "displayName": "Windows Debug", + "description": "Debug build for Windows", + "binaryDir": "${sourceDir}/build/windows-Debug", + "inherits": ["windows-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXE_LINKER_FLAGS": "-static" + } + }, + { + "name": "macos-arm64-release", + "displayName": "macOS ARM64 Release", + "description": "Release build for macOS ARM64", + "binaryDir": "${sourceDir}/build/mac-arm64-Release", + "inherits": ["macos-arm64"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "macos-arm64-debug", + "displayName": "macOS ARM64 Debug", + "description": "Debug build for macOS ARM64", + "binaryDir": "${sourceDir}/build/mac-arm64-Debug", + "inherits": ["macos-arm64"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "macos-x64-release", + "displayName": "macOS x64 Release", + "description": "Release build for macOS x64", + "binaryDir": "${sourceDir}/build/mac-x64-Release", + "inherits": ["macos-x64"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "macos-x64-debug", + "displayName": "macOS x64 Debug", + "description": "Debug build for macOS x64", + "binaryDir": "${sourceDir}/build/mac-x64-Debug", + "inherits": ["macos-x64"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-cross-release", + "displayName": "Windows Cross-compile Release", + "description": "Cross-compile Release build for Windows using MinGW-w64", + "binaryDir": "${sourceDir}/build/windows-cross-Release", + "inherits": ["mingw-cross-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "windows-cross-debug", + "displayName": "Windows Cross-compile Debug", + "description": "Cross-compile Debug build for Windows using MinGW-w64", + "binaryDir": "${sourceDir}/build/windows-cross-Debug", + "inherits": ["mingw-cross-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-mingw-release", + "displayName": "Windows MinGW Native Release", + "description": "Native Windows build using MinGW-w64", + "binaryDir": "${sourceDir}/build/windows-mingw-Release", + "inherits": ["mingw-native-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "windows-mingw-debug", + "displayName": "Windows MinGW Native Debug", + "description": "Native Windows Debug build using MinGW-w64", + "binaryDir": "${sourceDir}/build/windows-mingw-Debug", + "inherits": ["mingw-native-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { + "name": "release", + "configurePreset": "release", + "displayName": "Release Build" + }, + { + "name": "debug", + "configurePreset": "debug", + "displayName": "Debug Build" + }, + { + "name": "windows-release", + "configurePreset": "windows-release", + "displayName": "Windows Release Build" + }, + { + "name": "windows-debug", + "configurePreset": "windows-debug", + "displayName": "Windows Debug Build" + }, + { + "name": "macos-arm64-release", + "configurePreset": "macos-arm64-release", + "displayName": "macOS ARM64 Release Build" + }, + { + "name": "macos-arm64-debug", + "configurePreset": "macos-arm64-debug", + "displayName": "macOS ARM64 Debug Build" + }, + { + "name": "macos-x64-release", + "configurePreset": "macos-x64-release", + "displayName": "macOS x64 Release Build" + }, + { + "name": "macos-x64-debug", + "configurePreset": "macos-x64-debug", + "displayName": "macOS x64 Debug Build" + }, + { + "name": "windows-cross-release", + "configurePreset": "windows-cross-release", + "displayName": "Windows Cross-compile Release Build" + }, + { + "name": "windows-cross-debug", + "configurePreset": "windows-cross-debug", + "displayName": "Windows Cross-compile Debug Build" + }, + { + "name": "windows-mingw-release", + "configurePreset": "windows-mingw-release", + "displayName": "Windows MinGW Native Release Build" + }, + { + "name": "windows-mingw-debug", + "configurePreset": "windows-mingw-debug", + "displayName": "Windows MinGW Native Debug Build" + } + ] +} diff --git a/src/cmake/GitVersion.cmake b/src/cmake/GitVersion.cmake new file mode 100644 index 000000000..4bf067706 --- /dev/null +++ b/src/cmake/GitVersion.cmake @@ -0,0 +1,57 @@ +find_package(Git QUIET) +if (GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} log --pretty=format:'%h' -n 1 + OUTPUT_VARIABLE GINAN_COMMIT_HASH + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + execute_process( + COMMAND bash -c "git diff --quiet --exit-code || echo -dirty " + OUTPUT_VARIABLE GINAN_COMMIT_DIFF + OUTPUT_STRIP_TRAILING_WHITESPACE + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + execute_process( + COMMAND ${GIT_EXECUTABLE} describe --exact-match --tags + OUTPUT_VARIABLE GINAN_COMMIT_TAG ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD + OUTPUT_VARIABLE GINAN_BRANCH_NAME + OUTPUT_STRIP_TRAILING_WHITESPACE + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + string(FIND "${GINAN_BRANCH_NAME}" "/" SLASH_POSITION) + if(SLASH_POSITION GREATER -1) + string(SUBSTRING "${GINAN_BRANCH_NAME}" ${SLASH_POSITION} -1 GINAN_BRANCH_NAME) + string(SUBSTRING "${GINAN_BRANCH_NAME}" 1 -1 GINAN_BRANCH_NAME) # Remove the leading '/' + endif() + execute_process( + COMMAND ${GIT_EXECUTABLE} log -1 --format=%cd --date=iso + OUTPUT_VARIABLE "GINAN_COMMIT_DATE" + OUTPUT_STRIP_TRAILING_WHITESPACE + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + + if (GINAN_COMMIT_TAG STREQUAL "") + set(GINAN_COMMIT_TAG "untagged") + endif() + string(REGEX REPLACE "'" "" GINAN_COMMIT_HASH "${GINAN_COMMIT_HASH}") +else() + message(WARNING "Git not found, version will not be updated") + SET(GINAN_COMMIT_VERSION "N/A") + SET(GINAN_COMMIT_DIFF "N/A") + SET(GINAN_COMMIT_TAG "N/A") + SET(GINAN_BRANCH_NAME "N/A") +endif() + + +SET(GINAN_COMMIT_VERSION "${GINAN_COMMIT_TAG}-${GINAN_COMMIT_HASH}${GINAN_COMMIT_DIFF}") +MESSAGE(STATUS "Git branch tag: ${GINAN_COMMIT_VERSION}") +MESSAGE(STATUS "Git branch: ${GINAN_BRANCH_NAME}") + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/cpp/pea/peaCommitVersion.h.in ${CMAKE_CURRENT_SOURCE_DIR}/cpp/pea/peaCommitVersion.h @ONLY) diff --git a/src/cmake/toolchain/clang_linux_x64.cmake b/src/cmake/toolchain/clang_linux_x64.cmake new file mode 100644 index 000000000..0d0de760a --- /dev/null +++ b/src/cmake/toolchain/clang_linux_x64.cmake @@ -0,0 +1,6 @@ +set(OpenMP_C_FLAGS "-fopenmp") +set(OpenMP_CXX_FLAGS "-fopenmp") +set(OpenMP_EXE_LINKER_FLAGS "-fopenmp") +set(OpenMP_C_LIB_NAMES "omp") +set(OpenMP_CXX_LIB_NAMES "omp") +set(OpenMP_omp_LIBRARY "omp") \ No newline at end of file diff --git a/src/compile_mac_arm64.cmake b/src/cmake/toolchain/clang_mac_arm64.cmake similarity index 100% rename from src/compile_mac_arm64.cmake rename to src/cmake/toolchain/clang_mac_arm64.cmake diff --git a/src/cmake/toolchain/clang_mac_x64.cmake b/src/cmake/toolchain/clang_mac_x64.cmake new file mode 100644 index 000000000..5db6bece5 --- /dev/null +++ b/src/cmake/toolchain/clang_mac_x64.cmake @@ -0,0 +1,7 @@ +set(OpenMP_CXX_FLAGS "-Xclang -fopenmp") +set(OpenMP_CXX_INCLUDE_DIR "/usr/local/opt/libomp/include") +set(OpenMP_CXX_LIB_NAMES "libomp") +set(OpenMP_C_FLAGS "-Xclang -fopenmp") +set(OpenMP_C_INCLUDE_DIR "/usr/local/opt/libomp/include") +set(OpenMP_C_LIB_NAMES "libomp") +set(OpenMP_libomp_LIBRARY "/usr/local/opt/libomp/lib/libomp.dylib") diff --git a/src/cmake/toolchain/gcc_mac_arm64.cmake b/src/cmake/toolchain/gcc_mac_arm64.cmake new file mode 100644 index 000000000..a46996b77 --- /dev/null +++ b/src/cmake/toolchain/gcc_mac_arm64.cmake @@ -0,0 +1,7 @@ +set(OpenMP_CXX_FLAGS "-fopenmp") +set(OpenMP_CXX_INCLUDE_DIR "/opt/homebrew/opt/libomp/include") +set(OpenMP_CXX_LIB_NAMES "libomp") +set(OpenMP_C_FLAGS "-fopenmp") +set(OpenMP_C_INCLUDE_DIR "/opt/homebrew/opt/libomp/include") +set(OpenMP_C_LIB_NAMES "libomp") +set(OpenMP_libomp_LIBRARY "/opt/homebrew/opt/libomp/lib/libomp.dylib") diff --git a/src/cmake/toolchain/mingw64.cmake b/src/cmake/toolchain/mingw64.cmake new file mode 100644 index 000000000..2301bd9dd --- /dev/null +++ b/src/cmake/toolchain/mingw64.cmake @@ -0,0 +1,57 @@ +# CMake toolchain file for cross-compiling to Windows from Linux using MinGW-w64 + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR AMD64) + +# Disable vcpkg applocal.ps1 which requires PowerShell (not available during cross-compile) +set(VCPKG_APPLOCAL_DEPS OFF CACHE BOOL "Disable vcpkg applocal" FORCE) + +# Specify the cross compiler +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) +set(CMAKE_AR x86_64-w64-mingw32-ar) +set(CMAKE_RANLIB x86_64-w64-mingw32-ranlib) + +# Where to search for target environment +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) + +# Adjust the default behavior of the FIND_XXX() commands: +# search programs in the host environment +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + +# search headers and libraries in the target environment +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +# Allow packages from vcpkg to be found +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) + +# Ensure static linking for MinGW +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libgcc -static-libstdc++") + +# Windows-specific definitions +# Use Windows 8 (0x0602) for Boost atomic operations compatibility +add_definitions(-D_WIN32_WINNT=0x0602) +add_definitions(-DWIN32_LEAN_AND_MEAN) + +# Enable large file support (>2GB files) for MinGW +add_definitions(-D_FILE_OFFSET_BITS=64) +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") + + # 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") + + # 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)") +endif() diff --git a/src/cpp/3rdparty/enum.h b/src/cpp/3rdparty/enum.h deleted file mode 100755 index 2dac25c88..000000000 --- a/src/cpp/3rdparty/enum.h +++ /dev/null @@ -1,1184 +0,0 @@ -// This file is part of Better Enums, released under the BSD 2-clause license. -// See doc/LICENSE for details, or visit http://github.com/aantron/better-enums. - -#pragma once - -#ifndef BETTER_ENUMS_ENUM_H -#define BETTER_ENUMS_ENUM_H - - - -#include -#include -#include -#include - - - -// Feature detection. - -#ifdef __GNUC__ -# ifdef __clang__ -# if __has_feature(cxx_constexpr) -# define BETTER_ENUMS_HAVE_CONSTEXPR -# endif -# if !defined(__EXCEPTIONS) || !__has_feature(cxx_exceptions) -# define BETTER_ENUMS_NO_EXCEPTIONS -# endif -# else -# if defined(__GXX_EXPERIMENTAL_CXX0X__) || __cplusplus >= 201103L -# if (__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ >= 6)) -# define BETTER_ENUMS_HAVE_CONSTEXPR -# endif -# endif -# ifndef __EXCEPTIONS -# define BETTER_ENUMS_NO_EXCEPTIONS -# endif -# endif -#endif - -#ifdef _MSC_VER -# ifndef _CPPUNWIND -# define BETTER_ENUMS_NO_EXCEPTIONS -# endif -# if _MSC_VER < 1600 -# define BETTER_ENUMS_VC2008_WORKAROUNDS -# endif -#endif - -#ifdef BETTER_ENUMS_CONSTEXPR -# define BETTER_ENUMS_HAVE_CONSTEXPR -#endif - -#ifdef BETTER_ENUMS_NO_CONSTEXPR -# ifdef BETTER_ENUMS_HAVE_CONSTEXPR -# undef BETTER_ENUMS_HAVE_CONSTEXPR -# endif -#endif - -// GCC (and maybe clang) can be made to warn about using 0 or NULL when nullptr -// is available, so Better Enums tries to use nullptr. This passage uses -// availability of constexpr as a proxy for availability of nullptr, i.e. it -// assumes that nullptr is available when compiling on the right versions of gcc -// and clang with the right -std flag. This is actually slightly wrong, because -// nullptr is also available in Visual C++, but constexpr isn't. This -// imprecision doesn't matter, however, because VC++ doesn't have the warnings -// that make using nullptr necessary. -#ifdef BETTER_ENUMS_HAVE_CONSTEXPR -# define BETTER_ENUMS_CONSTEXPR_ constexpr -# define BETTER_ENUMS_NULLPTR nullptr -#else -# define BETTER_ENUMS_CONSTEXPR_ -# define BETTER_ENUMS_NULLPTR NULL -#endif - -#ifndef BETTER_ENUMS_NO_EXCEPTIONS -# define BETTER_ENUMS_IF_EXCEPTIONS(x) x -#else -# define BETTER_ENUMS_IF_EXCEPTIONS(x) -#endif - -#ifdef __GNUC__ -# define BETTER_ENUMS_UNUSED(x) x __attribute__((__unused__)) -#else -# define BETTER_ENUMS_UNUSED(x) x -#endif - - - -// Higher-order preprocessor macros. - -#ifdef BETTER_ENUMS_MACRO_FILE -# include BETTER_ENUMS_MACRO_FILE -#else - -#define BETTER_ENUMS_PP_MAP(macro, data, ...) \ - BETTER_ENUMS_ID( \ - BETTER_ENUMS_APPLY( \ - BETTER_ENUMS_PP_MAP_VAR_COUNT, \ - BETTER_ENUMS_PP_COUNT(__VA_ARGS__)) \ - (macro, data, __VA_ARGS__)) - -#define BETTER_ENUMS_PP_MAP_VAR_COUNT(count) BETTER_ENUMS_M ## count - -#define BETTER_ENUMS_APPLY(macro, ...) BETTER_ENUMS_ID(macro(__VA_ARGS__)) - -#define BETTER_ENUMS_ID(x) x - -#define BETTER_ENUMS_M1(m, d, x) m(d,0,x) -#define BETTER_ENUMS_M2(m,d,x,...) m(d,1,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M1(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M3(m,d,x,...) m(d,2,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M2(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M4(m,d,x,...) m(d,3,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M3(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M5(m,d,x,...) m(d,4,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M4(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M6(m,d,x,...) m(d,5,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M5(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M7(m,d,x,...) m(d,6,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M6(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M8(m,d,x,...) m(d,7,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M7(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M9(m,d,x,...) m(d,8,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M8(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M10(m,d,x,...) m(d,9,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M9(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M11(m,d,x,...) m(d,10,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M10(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M12(m,d,x,...) m(d,11,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M11(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M13(m,d,x,...) m(d,12,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M12(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M14(m,d,x,...) m(d,13,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M13(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M15(m,d,x,...) m(d,14,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M14(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M16(m,d,x,...) m(d,15,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M15(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M17(m,d,x,...) m(d,16,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M16(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M18(m,d,x,...) m(d,17,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M17(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M19(m,d,x,...) m(d,18,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M18(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M20(m,d,x,...) m(d,19,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M19(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M21(m,d,x,...) m(d,20,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M20(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M22(m,d,x,...) m(d,21,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M21(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M23(m,d,x,...) m(d,22,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M22(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M24(m,d,x,...) m(d,23,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M23(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M25(m,d,x,...) m(d,24,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M24(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M26(m,d,x,...) m(d,25,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M25(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M27(m,d,x,...) m(d,26,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M26(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M28(m,d,x,...) m(d,27,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M27(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M29(m,d,x,...) m(d,28,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M28(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M30(m,d,x,...) m(d,29,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M29(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M31(m,d,x,...) m(d,30,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M30(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M32(m,d,x,...) m(d,31,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M31(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M33(m,d,x,...) m(d,32,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M32(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M34(m,d,x,...) m(d,33,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M33(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M35(m,d,x,...) m(d,34,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M34(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M36(m,d,x,...) m(d,35,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M35(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M37(m,d,x,...) m(d,36,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M36(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M38(m,d,x,...) m(d,37,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M37(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M39(m,d,x,...) m(d,38,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M38(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M40(m,d,x,...) m(d,39,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M39(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M41(m,d,x,...) m(d,40,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M40(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M42(m,d,x,...) m(d,41,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M41(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M43(m,d,x,...) m(d,42,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M42(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M44(m,d,x,...) m(d,43,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M43(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M45(m,d,x,...) m(d,44,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M44(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M46(m,d,x,...) m(d,45,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M45(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M47(m,d,x,...) m(d,46,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M46(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M48(m,d,x,...) m(d,47,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M47(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M49(m,d,x,...) m(d,48,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M48(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M50(m,d,x,...) m(d,49,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M49(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M51(m,d,x,...) m(d,50,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M50(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M52(m,d,x,...) m(d,51,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M51(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M53(m,d,x,...) m(d,52,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M52(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M54(m,d,x,...) m(d,53,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M53(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M55(m,d,x,...) m(d,54,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M54(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M56(m,d,x,...) m(d,55,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M55(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M57(m,d,x,...) m(d,56,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M56(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M58(m,d,x,...) m(d,57,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M57(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M59(m,d,x,...) m(d,58,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M58(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M60(m,d,x,...) m(d,59,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M59(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M61(m,d,x,...) m(d,60,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M60(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M62(m,d,x,...) m(d,61,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M61(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M63(m,d,x,...) m(d,62,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M62(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M64(m,d,x,...) m(d,63,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M63(m,d,__VA_ARGS__)) - -#define BETTER_ENUMS_PP_COUNT_IMPL(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, \ - _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, \ - _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, \ - _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, \ - _56, _57, _58, _59, _60, _61, _62, _63, _64, count, ...) count - -#define BETTER_ENUMS_PP_COUNT(...) \ - BETTER_ENUMS_ID(BETTER_ENUMS_PP_COUNT_IMPL(__VA_ARGS__, 64, 63, 62, 61, 60,\ - 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42,\ - 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24,\ - 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, \ - 4, 3, 2, 1)) - -#define BETTER_ENUMS_ITERATE(X, f, l) X(f, l, 0) X(f, l, 1) X(f, l, 2) \ - X(f, l, 3) X(f, l, 4) X(f, l, 5) X(f, l, 6) X(f, l, 7) X(f, l, 8) \ - X(f, l, 9) X(f, l, 10) X(f, l, 11) X(f, l, 12) X(f, l, 13) X(f, l, 14) \ - X(f, l, 15) X(f, l, 16) X(f, l, 17) X(f, l, 18) X(f, l, 19) X(f, l, 20) \ - X(f, l, 21) X(f, l, 22) X(f, l, 23) - -#endif // #ifdef BETTER_ENUMS_MACRO_FILE else case - - - -namespace better_enums { - - -// Optional type. - -template -BETTER_ENUMS_CONSTEXPR_ inline T _default() -{ - return static_cast(0); -} - -template <> -BETTER_ENUMS_CONSTEXPR_ inline const char* _default() -{ - return BETTER_ENUMS_NULLPTR; -} - -template <> -BETTER_ENUMS_CONSTEXPR_ inline std::size_t _default() -{ - return 0; -} - -template -struct optional { - BETTER_ENUMS_CONSTEXPR_ optional() : - _valid(false), _value(_default()) { } - - BETTER_ENUMS_CONSTEXPR_ optional(T v) : _valid(true), _value(v) { } - - BETTER_ENUMS_CONSTEXPR_ const T& operator *() const { return _value; } - BETTER_ENUMS_CONSTEXPR_ const T* operator ->() const { return &_value; } - - BETTER_ENUMS_CONSTEXPR_ operator bool() const { return _valid; } - - BETTER_ENUMS_CONSTEXPR_ const T& value() const { return _value; } - - private: - bool _valid; - T _value; -}; - -template -BETTER_ENUMS_CONSTEXPR_ static optional -_map_index(const Element *array, optional index) -{ - return index ? static_cast(array[*index]) : optional(); -} - -#ifdef BETTER_ENUMS_VC2008_WORKAROUNDS - -#define BETTER_ENUMS_OR_THROW \ - if (!maybe) \ - throw std::runtime_error(message); \ - \ - return *maybe; - -#else - -#define BETTER_ENUMS_OR_THROW \ - return maybe ? *maybe : throw std::runtime_error(message); - -#endif - -BETTER_ENUMS_IF_EXCEPTIONS( -template -BETTER_ENUMS_CONSTEXPR_ static T _or_throw(optional maybe, - const char *message) -{ - BETTER_ENUMS_OR_THROW -} -) - -template -BETTER_ENUMS_CONSTEXPR_ static T* _or_null(optional maybe) -{ - return maybe ? *maybe : BETTER_ENUMS_NULLPTR; -} - - - -// Functional sequencing. This is essentially a comma operator wrapped in a -// constexpr function. g++ 4.7 doesn't "accept" integral constants in the second -// position for the comma operator, and emits an external symbol, which then -// causes a linking error. - -template -BETTER_ENUMS_CONSTEXPR_ U -continue_with(T BETTER_ENUMS_UNUSED(ignored), U value) { return value; } - - - -// Values array declaration helper. - -template -struct _eat_assign { - explicit BETTER_ENUMS_CONSTEXPR_ _eat_assign(EnumType value) : _value(value) - { } - - template - BETTER_ENUMS_CONSTEXPR_ const _eat_assign& - operator =(Any BETTER_ENUMS_UNUSED(dummy)) const { return *this; } - - BETTER_ENUMS_CONSTEXPR_ operator EnumType () const { return _value; } - - private: - EnumType _value; -}; - - - -// Iterables. - -template -struct _Iterable { - typedef const Element* iterator; - - BETTER_ENUMS_CONSTEXPR_ iterator begin() const { return iterator(_array); } - BETTER_ENUMS_CONSTEXPR_ iterator end() const - { return iterator(_array + _size); } - BETTER_ENUMS_CONSTEXPR_ std::size_t size() const { return _size; } - BETTER_ENUMS_CONSTEXPR_ const Element& operator [](std::size_t index) const - { return _array[index]; } - - BETTER_ENUMS_CONSTEXPR_ _Iterable(const Element *array, std::size_t s) : - _array(array), _size(s) { } - - private: - const Element * const _array; - const std::size_t _size; -}; - - - -// String routines. - -BETTER_ENUMS_CONSTEXPR_ static const char *_name_enders = "= \t\n"; - -BETTER_ENUMS_CONSTEXPR_ inline bool _ends_name(char c, std::size_t index = 0) -{ - return - c == _name_enders[index] ? true : - _name_enders[index] == '\0' ? false : - _ends_name(c, index + 1); -} - -BETTER_ENUMS_CONSTEXPR_ inline bool _has_initializer(const char *s, - std::size_t index = 0) -{ - return - s[index] == '\0' ? false : - s[index] == '=' ? true : - _has_initializer(s, index + 1); -} - -BETTER_ENUMS_CONSTEXPR_ inline std::size_t -_constant_length(const char *s, std::size_t index = 0) -{ - return _ends_name(s[index]) ? index : _constant_length(s, index + 1); -} - -BETTER_ENUMS_CONSTEXPR_ inline char -_select(const char *from, std::size_t from_length, std::size_t index) -{ - return index >= from_length ? '\0' : from[index]; -} - -BETTER_ENUMS_CONSTEXPR_ inline char _to_lower_ascii(char c) -{ - return c >= 0x41 && c <= 0x5A ? static_cast(c + 0x20) : c; -} - -BETTER_ENUMS_CONSTEXPR_ inline bool _names_match(const char *stringizedName, - const char *referenceName, - std::size_t index = 0) -{ - return - _ends_name(stringizedName[index]) ? referenceName[index] == '\0' : - referenceName[index] == '\0' ? false : - stringizedName[index] != referenceName[index] ? false : - _names_match(stringizedName, referenceName, index + 1); -} - -BETTER_ENUMS_CONSTEXPR_ inline bool -_names_match_nocase(const char *stringizedName, const char *referenceName, - std::size_t index = 0) -{ - return - _ends_name(stringizedName[index]) ? referenceName[index] == '\0' : - referenceName[index] == '\0' ? false : - _to_lower_ascii(stringizedName[index]) != - _to_lower_ascii(referenceName[index]) ? false : - _names_match_nocase(stringizedName, referenceName, index + 1); -} - -inline void _trim_names(const char * const *raw_names, - const char **trimmed_names, - char *storage, std::size_t count) -{ - std::size_t offset = 0; - - for (std::size_t index = 0; index < count; ++index) { - trimmed_names[index] = storage + offset; - - std::size_t trimmed_length = - std::strcspn(raw_names[index], _name_enders); - storage[offset + trimmed_length] = '\0'; - - std::size_t raw_length = std::strlen(raw_names[index]); - offset += raw_length + 1; - } -} - - - -// Eager initialization. -template -struct _initialize_at_program_start { - _initialize_at_program_start() { Enum::initialize(); } -}; - -} // namespace better_enums - - - -// Array generation macros. - -#define BETTER_ENUMS_EAT_ASSIGN_SINGLE(EnumType, index, expression) \ - ((::better_enums::_eat_assign)EnumType::expression), - -#define BETTER_ENUMS_EAT_ASSIGN(EnumType, ...) \ - BETTER_ENUMS_ID( \ - BETTER_ENUMS_PP_MAP( \ - BETTER_ENUMS_EAT_ASSIGN_SINGLE, EnumType, __VA_ARGS__)) - - - -#ifdef BETTER_ENUMS_HAVE_CONSTEXPR - - - -#define BETTER_ENUMS_SELECT_SINGLE_CHARACTER(from, from_length, index) \ - ::better_enums::_select(from, from_length, index), - -#define BETTER_ENUMS_SELECT_CHARACTERS(from, from_length) \ - BETTER_ENUMS_ITERATE( \ - BETTER_ENUMS_SELECT_SINGLE_CHARACTER, from, from_length) - - - -#define BETTER_ENUMS_TRIM_SINGLE_STRING(ignored, index, expression) \ -constexpr std::size_t _length_ ## index = \ - ::better_enums::_constant_length(#expression); \ -constexpr const char _trimmed_ ## index [] = \ - { BETTER_ENUMS_SELECT_CHARACTERS(#expression, _length_ ## index) }; \ -constexpr const char *_final_ ## index = \ - ::better_enums::_has_initializer(#expression) ? \ - _trimmed_ ## index : #expression; - -#define BETTER_ENUMS_TRIM_STRINGS(...) \ - BETTER_ENUMS_ID( \ - BETTER_ENUMS_PP_MAP( \ - BETTER_ENUMS_TRIM_SINGLE_STRING, ignored, __VA_ARGS__)) - - - -#define BETTER_ENUMS_REFER_TO_SINGLE_STRING(ignored, index, expression) \ - _final_ ## index, - -#define BETTER_ENUMS_REFER_TO_STRINGS(...) \ - BETTER_ENUMS_ID( \ - BETTER_ENUMS_PP_MAP( \ - BETTER_ENUMS_REFER_TO_SINGLE_STRING, ignored, __VA_ARGS__)) - - - -#endif // #ifdef BETTER_ENUMS_HAVE_CONSTEXPR - - - -#define BETTER_ENUMS_STRINGIZE_SINGLE(ignored, index, expression) #expression, - -#define BETTER_ENUMS_STRINGIZE(...) \ - BETTER_ENUMS_ID( \ - BETTER_ENUMS_PP_MAP( \ - BETTER_ENUMS_STRINGIZE_SINGLE, ignored, __VA_ARGS__)) - -#define BETTER_ENUMS_RESERVE_STORAGE_SINGLE(ignored, index, expression) \ - #expression "," - -#define BETTER_ENUMS_RESERVE_STORAGE(...) \ - BETTER_ENUMS_ID( \ - BETTER_ENUMS_PP_MAP( \ - BETTER_ENUMS_RESERVE_STORAGE_SINGLE, ignored, __VA_ARGS__)) - - - -// The enums proper. - -#define BETTER_ENUMS_NS(EnumType) better_enums::_data_ ## EnumType - -#ifdef BETTER_ENUMS_VC2008_WORKAROUNDS - -#define BETTER_ENUMS_COPY_CONSTRUCTOR(Enum) \ - BETTER_ENUMS_CONSTEXPR_ Enum(const Enum &other) : \ - _value(other._value) { } - -#else - -#define BETTER_ENUMS_COPY_CONSTRUCTOR(Enum) - -#endif - -#define BETTER_ENUMS_TYPE(SetUnderlyingType, SwitchType, GenerateSwitchType, \ - GenerateStrings, ToStringConstexpr, \ - DeclareInitialize, DefineInitialize, CallInitialize, \ - Enum, Underlying, ...) \ - \ -namespace better_enums { \ -namespace _data_ ## Enum { \ - \ -BETTER_ENUMS_ID(GenerateSwitchType(Underlying, __VA_ARGS__)) \ - \ -} \ -} \ - \ -class Enum { \ - private: \ - typedef ::better_enums::optional _optional; \ - typedef ::better_enums::optional _optional_index; \ - \ - public: \ - typedef Underlying _integral; \ - \ - enum _enumerated SetUnderlyingType(Underlying) { __VA_ARGS__ }; \ - \ - BETTER_ENUMS_CONSTEXPR_ Enum(_enumerated value) : _value(value) { } \ - \ - BETTER_ENUMS_COPY_CONSTRUCTOR(Enum) \ - \ - BETTER_ENUMS_CONSTEXPR_ operator SwitchType(Enum)() const \ - { \ - return SwitchType(Enum)(_value); \ - } \ - \ - BETTER_ENUMS_CONSTEXPR_ _integral _to_integral() const; \ - BETTER_ENUMS_IF_EXCEPTIONS( \ - BETTER_ENUMS_CONSTEXPR_ static Enum _from_integral(_integral value); \ - ) \ - BETTER_ENUMS_CONSTEXPR_ static Enum \ - _from_integral_unchecked(_integral value); \ - BETTER_ENUMS_CONSTEXPR_ static _optional \ - _from_integral_nothrow(_integral value); \ - \ - ToStringConstexpr const char* _to_string() const; \ - BETTER_ENUMS_IF_EXCEPTIONS( \ - BETTER_ENUMS_CONSTEXPR_ static Enum _from_string(const char *name); \ - ) \ - BETTER_ENUMS_CONSTEXPR_ static _optional \ - _from_string_nothrow(const char *name); \ - \ - BETTER_ENUMS_IF_EXCEPTIONS( \ - BETTER_ENUMS_CONSTEXPR_ static Enum _from_string_nocase(const char *name); \ - ) \ - BETTER_ENUMS_CONSTEXPR_ static _optional \ - _from_string_nocase_nothrow(const char *name); \ - \ - BETTER_ENUMS_CONSTEXPR_ static bool _is_valid(_integral value); \ - BETTER_ENUMS_CONSTEXPR_ static bool _is_valid(const char *name); \ - BETTER_ENUMS_CONSTEXPR_ static bool _is_valid_nocase(const char *name); \ - \ - typedef ::better_enums::_Iterable _value_iterable; \ - typedef ::better_enums::_Iterable _name_iterable; \ - \ - typedef _value_iterable::iterator _value_iterator; \ - typedef _name_iterable::iterator _name_iterator; \ - \ - BETTER_ENUMS_CONSTEXPR_ static const std::size_t _size_constant = \ - BETTER_ENUMS_ID(BETTER_ENUMS_PP_COUNT(__VA_ARGS__)); \ - BETTER_ENUMS_CONSTEXPR_ static std::size_t _size() \ - { return _size_constant; } \ - \ - BETTER_ENUMS_CONSTEXPR_ static const char* _name(); \ - BETTER_ENUMS_CONSTEXPR_ static _value_iterable _values(); \ - ToStringConstexpr static _name_iterable _names(); \ - \ - _integral _value; \ - \ - BETTER_ENUMS_DEFAULT_CONSTRUCTOR(Enum) \ - \ - private: \ - explicit BETTER_ENUMS_CONSTEXPR_ Enum(const _integral &value) : \ - _value(value) { } \ - \ - DeclareInitialize \ - \ - BETTER_ENUMS_CONSTEXPR_ static _optional_index \ - _from_value_loop(_integral value, std::size_t index = 0); \ - BETTER_ENUMS_CONSTEXPR_ static _optional_index \ - _from_string_loop(const char *name, std::size_t index = 0); \ - BETTER_ENUMS_CONSTEXPR_ static _optional_index \ - _from_string_nocase_loop(const char *name, std::size_t index = 0); \ - \ - friend struct ::better_enums::_initialize_at_program_start; \ -}; \ - \ -namespace better_enums { \ -namespace _data_ ## Enum { \ - \ -static ::better_enums::_initialize_at_program_start \ - _force_initialization; \ - \ -enum _PutNamesInThisScopeAlso { __VA_ARGS__ }; \ - \ -BETTER_ENUMS_CONSTEXPR_ const Enum _value_array[] = \ - { BETTER_ENUMS_ID(BETTER_ENUMS_EAT_ASSIGN(Enum, __VA_ARGS__)) }; \ - \ -BETTER_ENUMS_ID(GenerateStrings(Enum, __VA_ARGS__)) \ - \ -} \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline const Enum \ -operator +(Enum::_enumerated enumerated) \ -{ \ - return static_cast(enumerated); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum::_optional_index \ -Enum::_from_value_loop(Enum::_integral value, std::size_t index) \ -{ \ - return \ - index == _size() ? \ - _optional_index() : \ - BETTER_ENUMS_NS(Enum)::_value_array[index]._value == value ? \ - _optional_index(index) : \ - _from_value_loop(value, index + 1); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum::_optional_index \ -Enum::_from_string_loop(const char *name, std::size_t index) \ -{ \ - return \ - index == _size() ? _optional_index() : \ - ::better_enums::_names_match( \ - BETTER_ENUMS_NS(Enum)::_raw_names()[index], name) ? \ - _optional_index(index) : \ - _from_string_loop(name, index + 1); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum::_optional_index \ -Enum::_from_string_nocase_loop(const char *name, std::size_t index) \ -{ \ - return \ - index == _size() ? _optional_index() : \ - ::better_enums::_names_match_nocase( \ - BETTER_ENUMS_NS(Enum)::_raw_names()[index], name) ? \ - _optional_index(index) : \ - _from_string_nocase_loop(name, index + 1); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum::_integral Enum::_to_integral() const \ -{ \ - return _integral(_value); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum \ -Enum::_from_integral_unchecked(_integral value) \ -{ \ - return static_cast<_enumerated>(value); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum::_optional \ -Enum::_from_integral_nothrow(_integral value) \ -{ \ - return \ - ::better_enums::_map_index(BETTER_ENUMS_NS(Enum)::_value_array, \ - _from_value_loop(value)); \ -} \ - \ -BETTER_ENUMS_IF_EXCEPTIONS( \ -BETTER_ENUMS_CONSTEXPR_ inline Enum Enum::_from_integral(_integral value) \ -{ \ - return \ - ::better_enums::_or_throw(_from_integral_nothrow(value), \ - #Enum "::_from_integral: invalid argument"); \ -} \ -) \ - \ -ToStringConstexpr inline const char* Enum::_to_string() const \ -{ \ - return \ - ::better_enums::_or_null( \ - ::better_enums::_map_index( \ - BETTER_ENUMS_NS(Enum)::_name_array(), \ - _from_value_loop(CallInitialize(_value)))); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum::_optional \ -Enum::_from_string_nothrow(const char *name) \ -{ \ - return \ - ::better_enums::_map_index( \ - BETTER_ENUMS_NS(Enum)::_value_array, _from_string_loop(name)); \ -} \ - \ -BETTER_ENUMS_IF_EXCEPTIONS( \ -BETTER_ENUMS_CONSTEXPR_ inline Enum Enum::_from_string(const char *name) \ -{ \ - return \ - ::better_enums::_or_throw(_from_string_nothrow(name), \ - #Enum "::_from_string: invalid argument"); \ -} \ -) \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum::_optional \ -Enum::_from_string_nocase_nothrow(const char *name) \ -{ \ - return \ - ::better_enums::_map_index(BETTER_ENUMS_NS(Enum)::_value_array, \ - _from_string_nocase_loop(name)); \ -} \ - \ -BETTER_ENUMS_IF_EXCEPTIONS( \ -BETTER_ENUMS_CONSTEXPR_ inline Enum Enum::_from_string_nocase(const char *name)\ -{ \ - return \ - ::better_enums::_or_throw( \ - _from_string_nocase_nothrow(name), \ - #Enum "::_from_string_nocase: invalid argument"); \ -} \ -) \ - \ -BETTER_ENUMS_CONSTEXPR_ inline bool Enum::_is_valid(_integral value) \ -{ \ - return _from_value_loop(value); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline bool Enum::_is_valid(const char *name) \ -{ \ - return _from_string_loop(name); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline bool Enum::_is_valid_nocase(const char *name) \ -{ \ - return _from_string_nocase_loop(name); \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline const char* Enum::_name() \ -{ \ - return #Enum; \ -} \ - \ -BETTER_ENUMS_CONSTEXPR_ inline Enum::_value_iterable Enum::_values() \ -{ \ - return _value_iterable(BETTER_ENUMS_NS(Enum)::_value_array, _size()); \ -} \ - \ -ToStringConstexpr inline Enum::_name_iterable Enum::_names() \ -{ \ - return \ - _name_iterable(BETTER_ENUMS_NS(Enum)::_name_array(), \ - CallInitialize(_size())); \ -} \ - \ -DefineInitialize(Enum) \ - \ -BETTER_ENUMS_CONSTEXPR_ inline bool operator ==(const Enum &a, const Enum &b) \ - { return a._to_integral() == b._to_integral(); } \ -BETTER_ENUMS_CONSTEXPR_ inline bool operator !=(const Enum &a, const Enum &b) \ - { return a._to_integral() != b._to_integral(); } \ -BETTER_ENUMS_CONSTEXPR_ inline bool operator <(const Enum &a, const Enum &b) \ - { return a._to_integral() < b._to_integral(); } \ -BETTER_ENUMS_CONSTEXPR_ inline bool operator <=(const Enum &a, const Enum &b) \ - { return a._to_integral() <= b._to_integral(); } \ -BETTER_ENUMS_CONSTEXPR_ inline bool operator >(const Enum &a, const Enum &b) \ - { return a._to_integral() > b._to_integral(); } \ -BETTER_ENUMS_CONSTEXPR_ inline bool operator >=(const Enum &a, const Enum &b) \ - { return a._to_integral() >= b._to_integral(); } - - - -// Enum feature options. - -// C++98, C++11 -#define BETTER_ENUMS_CXX98_UNDERLYING_TYPE(Underlying) - -// C++11 -#define BETTER_ENUMS_CXX11_UNDERLYING_TYPE(Underlying) \ - : Underlying - -// C++98, C++11 -#define BETTER_ENUMS_REGULAR_ENUM_SWITCH_TYPE(Type) \ - _enumerated - -// C++11 -#define BETTER_ENUMS_ENUM_CLASS_SWITCH_TYPE(Type) \ - BETTER_ENUMS_NS(Type)::_EnumClassForSwitchStatements - -// C++98, C++11 -#define BETTER_ENUMS_REGULAR_ENUM_SWITCH_TYPE_GENERATE(Underlying, ...) - -// C++11 -#define BETTER_ENUMS_ENUM_CLASS_SWITCH_TYPE_GENERATE(Underlying, ...) \ - enum class _EnumClassForSwitchStatements : Underlying { __VA_ARGS__ }; - -// C++98 -#define BETTER_ENUMS_CXX98_TRIM_STRINGS_ARRAYS(Enum, ...) \ - inline const char** _raw_names() \ - { \ - static const char *value[] = \ - { BETTER_ENUMS_ID(BETTER_ENUMS_STRINGIZE(__VA_ARGS__)) }; \ - return value; \ - } \ - \ - inline char* _name_storage() \ - { \ - static char storage[] = \ - BETTER_ENUMS_ID(BETTER_ENUMS_RESERVE_STORAGE(__VA_ARGS__)); \ - return storage; \ - } \ - \ - inline const char** _name_array() \ - { \ - static const char *value[Enum::_size_constant]; \ - return value; \ - } \ - \ - inline bool& _initialized() \ - { \ - static bool value = false; \ - return value; \ - } - -// C++11 fast version -#define BETTER_ENUMS_CXX11_PARTIAL_CONSTEXPR_TRIM_STRINGS_ARRAYS(Enum, ...) \ - constexpr const char *_the_raw_names[] = \ - { BETTER_ENUMS_ID(BETTER_ENUMS_STRINGIZE(__VA_ARGS__)) }; \ - \ - constexpr const char * const * _raw_names() \ - { \ - return _the_raw_names; \ - } \ - \ - inline char* _name_storage() \ - { \ - static char storage[] = \ - BETTER_ENUMS_ID(BETTER_ENUMS_RESERVE_STORAGE(__VA_ARGS__)); \ - return storage; \ - } \ - \ - inline const char** _name_array() \ - { \ - static const char *value[Enum::_size_constant]; \ - return value; \ - } \ - \ - inline bool& _initialized() \ - { \ - static bool value = false; \ - return value; \ - } - -// C++11 slow all-constexpr version -#define BETTER_ENUMS_CXX11_FULL_CONSTEXPR_TRIM_STRINGS_ARRAYS(Enum, ...) \ - BETTER_ENUMS_ID(BETTER_ENUMS_TRIM_STRINGS(__VA_ARGS__)) \ - \ - constexpr const char * const _the_name_array[] = \ - { BETTER_ENUMS_ID(BETTER_ENUMS_REFER_TO_STRINGS(__VA_ARGS__)) }; \ - \ - constexpr const char * const * _name_array() \ - { \ - return _the_name_array; \ - } \ - \ - constexpr const char * const * _raw_names() \ - { \ - return _the_name_array; \ - } - -// C++98, C++11 fast version -#define BETTER_ENUMS_NO_CONSTEXPR_TO_STRING_KEYWORD - -// C++11 slow all-constexpr version -#define BETTER_ENUMS_CONSTEXPR_TO_STRING_KEYWORD \ - constexpr - -// C++98, C++11 fast version -#define BETTER_ENUMS_DO_DECLARE_INITIALIZE \ - static int initialize(); - -// C++11 slow all-constexpr version -#define BETTER_ENUMS_DECLARE_EMPTY_INITIALIZE \ - static int initialize() { return 0; } - -// C++98, C++11 fast version -#define BETTER_ENUMS_DO_DEFINE_INITIALIZE(Enum) \ - inline int Enum::initialize() \ - { \ - if (BETTER_ENUMS_NS(Enum)::_initialized()) \ - return 0; \ - \ - ::better_enums::_trim_names(BETTER_ENUMS_NS(Enum)::_raw_names(), \ - BETTER_ENUMS_NS(Enum)::_name_array(), \ - BETTER_ENUMS_NS(Enum)::_name_storage(), \ - _size()); \ - \ - BETTER_ENUMS_NS(Enum)::_initialized() = true; \ - \ - return 0; \ - } - -// C++11 slow all-constexpr version -#define BETTER_ENUMS_DO_NOT_DEFINE_INITIALIZE(Enum) - -// C++98, C++11 fast version -#define BETTER_ENUMS_DO_CALL_INITIALIZE(value) \ - ::better_enums::continue_with(initialize(), value) - -// C++11 slow all-constexpr version -#define BETTER_ENUMS_DO_NOT_CALL_INITIALIZE(value) \ - value - - - -// User feature selection. - -#ifdef BETTER_ENUMS_STRICT_CONVERSION -# define BETTER_ENUMS_DEFAULT_SWITCH_TYPE \ - BETTER_ENUMS_ENUM_CLASS_SWITCH_TYPE -# define BETTER_ENUMS_DEFAULT_SWITCH_TYPE_GENERATE \ - BETTER_ENUMS_ENUM_CLASS_SWITCH_TYPE_GENERATE -#else -# define BETTER_ENUMS_DEFAULT_SWITCH_TYPE \ - BETTER_ENUMS_REGULAR_ENUM_SWITCH_TYPE -# define BETTER_ENUMS_DEFAULT_SWITCH_TYPE_GENERATE \ - BETTER_ENUMS_REGULAR_ENUM_SWITCH_TYPE_GENERATE -#endif - - - -#ifndef BETTER_ENUMS_DEFAULT_CONSTRUCTOR -# define BETTER_ENUMS_DEFAULT_CONSTRUCTOR(Enum) \ - private: \ - Enum() : _value(0) { } -#endif - - - -#ifdef BETTER_ENUMS_HAVE_CONSTEXPR - -#ifdef BETTER_ENUMS_CONSTEXPR_TO_STRING -# define BETTER_ENUMS_DEFAULT_TRIM_STRINGS_ARRAYS \ - BETTER_ENUMS_CXX11_FULL_CONSTEXPR_TRIM_STRINGS_ARRAYS -# define BETTER_ENUMS_DEFAULT_TO_STRING_KEYWORD \ - BETTER_ENUMS_CONSTEXPR_TO_STRING_KEYWORD -# define BETTER_ENUMS_DEFAULT_DECLARE_INITIALIZE \ - BETTER_ENUMS_DECLARE_EMPTY_INITIALIZE -# define BETTER_ENUMS_DEFAULT_DEFINE_INITIALIZE \ - BETTER_ENUMS_DO_NOT_DEFINE_INITIALIZE -# define BETTER_ENUMS_DEFAULT_CALL_INITIALIZE \ - BETTER_ENUMS_DO_NOT_CALL_INITIALIZE -#else -# define BETTER_ENUMS_DEFAULT_TRIM_STRINGS_ARRAYS \ - BETTER_ENUMS_CXX11_PARTIAL_CONSTEXPR_TRIM_STRINGS_ARRAYS -# define BETTER_ENUMS_DEFAULT_TO_STRING_KEYWORD \ - BETTER_ENUMS_NO_CONSTEXPR_TO_STRING_KEYWORD -# define BETTER_ENUMS_DEFAULT_DECLARE_INITIALIZE \ - BETTER_ENUMS_DO_DECLARE_INITIALIZE -# define BETTER_ENUMS_DEFAULT_DEFINE_INITIALIZE \ - BETTER_ENUMS_DO_DEFINE_INITIALIZE -# define BETTER_ENUMS_DEFAULT_CALL_INITIALIZE \ - BETTER_ENUMS_DO_CALL_INITIALIZE -#endif - - - -// Top-level macros. - -#define BETTER_ENUM(Enum, Underlying, ...) \ - BETTER_ENUMS_ID(BETTER_ENUMS_TYPE( \ - BETTER_ENUMS_CXX11_UNDERLYING_TYPE, \ - BETTER_ENUMS_DEFAULT_SWITCH_TYPE, \ - BETTER_ENUMS_DEFAULT_SWITCH_TYPE_GENERATE, \ - BETTER_ENUMS_DEFAULT_TRIM_STRINGS_ARRAYS, \ - BETTER_ENUMS_DEFAULT_TO_STRING_KEYWORD, \ - BETTER_ENUMS_DEFAULT_DECLARE_INITIALIZE, \ - BETTER_ENUMS_DEFAULT_DEFINE_INITIALIZE, \ - BETTER_ENUMS_DEFAULT_CALL_INITIALIZE, \ - Enum, Underlying, __VA_ARGS__)) - -#define SLOW_ENUM(Enum, Underlying, ...) \ - BETTER_ENUMS_ID(BETTER_ENUMS_TYPE( \ - BETTER_ENUMS_CXX11_UNDERLYING_TYPE, \ - BETTER_ENUMS_DEFAULT_SWITCH_TYPE, \ - BETTER_ENUMS_DEFAULT_SWITCH_TYPE_GENERATE, \ - BETTER_ENUMS_CXX11_FULL_CONSTEXPR_TRIM_STRINGS_ARRAYS, \ - BETTER_ENUMS_CONSTEXPR_TO_STRING_KEYWORD, \ - BETTER_ENUMS_DECLARE_EMPTY_INITIALIZE, \ - BETTER_ENUMS_DO_NOT_DEFINE_INITIALIZE, \ - BETTER_ENUMS_DO_NOT_CALL_INITIALIZE, \ - Enum, Underlying, __VA_ARGS__)) - -#else - -#define BETTER_ENUM(Enum, Underlying, ...) \ - BETTER_ENUMS_ID(BETTER_ENUMS_TYPE( \ - BETTER_ENUMS_CXX98_UNDERLYING_TYPE, \ - BETTER_ENUMS_DEFAULT_SWITCH_TYPE, \ - BETTER_ENUMS_DEFAULT_SWITCH_TYPE_GENERATE, \ - BETTER_ENUMS_CXX98_TRIM_STRINGS_ARRAYS, \ - BETTER_ENUMS_NO_CONSTEXPR_TO_STRING_KEYWORD, \ - BETTER_ENUMS_DO_DECLARE_INITIALIZE, \ - BETTER_ENUMS_DO_DEFINE_INITIALIZE, \ - BETTER_ENUMS_DO_CALL_INITIALIZE, \ - Enum, Underlying, __VA_ARGS__)) - -#endif - - - -namespace better_enums { - -// Maps. - -template -struct map_compare { - BETTER_ENUMS_CONSTEXPR_ static bool less(const T& a, const T& b) - { return a < b; } -}; - -template <> -struct map_compare { - BETTER_ENUMS_CONSTEXPR_ static bool less(const char *a, const char *b) - { return less_loop(a, b); } - - private: - BETTER_ENUMS_CONSTEXPR_ static bool - less_loop(const char *a, const char *b, size_t index = 0) - { - return - a[index] != b[index] ? a[index] < b[index] : - a[index] == '\0' ? false : - less_loop(a, b, index + 1); - } -}; - -template > -struct map { - typedef T (*function)(Enum); - - BETTER_ENUMS_CONSTEXPR_ explicit map(function f) : _f(f) { } - - BETTER_ENUMS_CONSTEXPR_ T from_enum(Enum value) const { return _f(value); } - BETTER_ENUMS_CONSTEXPR_ T operator [](Enum value) const - { return _f(value); } - - BETTER_ENUMS_CONSTEXPR_ Enum to_enum(T value) const - { - return - _or_throw(to_enum_nothrow(value), "map::to_enum: invalid argument"); - } - - BETTER_ENUMS_CONSTEXPR_ optional - to_enum_nothrow(T value, size_t index = 0) const - { - return - index >= Enum::_size() ? optional() : - Compare::less(_f(Enum::_values()[index]), value) || - Compare::less(value, _f(Enum::_values()[index])) ? - to_enum_nothrow(value, index + 1) : - Enum::_values()[index]; - } - - private: - const function _f; -}; - -template -BETTER_ENUMS_CONSTEXPR_ map make_map(T (*f)(Enum)) -{ - return map(f); -} - - - -// Stream I/O operators. - -// This template is used as a sort of enable_if for SFINAE. It should be -// possible to use std::enable_if, however is not available in -// C++98. Non-char streams are currently not supported. -template -struct only_if_enum { typedef T type; }; - -} - -template -inline typename better_enums::only_if_enum, - typename Enum::_enumerated>::type& -operator <<(std::basic_ostream& stream, const Enum &value) -{ - return stream << value._to_string(); -} - -template -inline typename better_enums::only_if_enum, - typename Enum::_enumerated>::type& -operator >>(std::basic_istream& stream, Enum &value) -{ - std::basic_string buffer; - - stream >> buffer; - better_enums::optional converted = - Enum::_from_string_nothrow(buffer.c_str()); - - if (converted) - value = *converted; - else - stream.setstate(std::basic_istream::failbit); - - return stream; -} - - - -#endif // #ifndef BETTER_ENUMS_ENUM_H diff --git a/src/cpp/3rdparty/enum_macros.h b/src/cpp/3rdparty/enum_macros.h deleted file mode 100644 index 5d14a1165..000000000 --- a/src/cpp/3rdparty/enum_macros.h +++ /dev/null @@ -1,581 +0,0 @@ -// This file was automatically generated by make_macros.py - -#pragma once - -#ifndef BETTER_ENUMS_MACRO_FILE_H -#define BETTER_ENUMS_MACRO_FILE_H - -#define BETTER_ENUMS_PP_MAP(macro, data, ...) \ - BETTER_ENUMS_ID( \ - BETTER_ENUMS_APPLY( \ - BETTER_ENUMS_PP_MAP_VAR_COUNT, \ - BETTER_ENUMS_PP_COUNT(__VA_ARGS__)) \ - (macro, data, __VA_ARGS__)) - -#define BETTER_ENUMS_PP_MAP_VAR_COUNT(count) BETTER_ENUMS_M ## count - -#define BETTER_ENUMS_APPLY(macro, ...) BETTER_ENUMS_ID(macro(__VA_ARGS__)) - -#define BETTER_ENUMS_ID(x) x - -#define BETTER_ENUMS_M1(m, d, x) m(d,0,x) -#define BETTER_ENUMS_M2(m,d,x,...) m(d,1,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M1(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M3(m,d,x,...) m(d,2,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M2(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M4(m,d,x,...) m(d,3,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M3(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M5(m,d,x,...) m(d,4,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M4(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M6(m,d,x,...) m(d,5,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M5(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M7(m,d,x,...) m(d,6,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M6(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M8(m,d,x,...) m(d,7,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M7(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M9(m,d,x,...) m(d,8,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M8(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M10(m,d,x,...) m(d,9,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M9(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M11(m,d,x,...) m(d,10,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M10(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M12(m,d,x,...) m(d,11,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M11(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M13(m,d,x,...) m(d,12,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M12(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M14(m,d,x,...) m(d,13,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M13(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M15(m,d,x,...) m(d,14,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M14(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M16(m,d,x,...) m(d,15,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M15(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M17(m,d,x,...) m(d,16,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M16(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M18(m,d,x,...) m(d,17,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M17(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M19(m,d,x,...) m(d,18,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M18(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M20(m,d,x,...) m(d,19,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M19(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M21(m,d,x,...) m(d,20,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M20(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M22(m,d,x,...) m(d,21,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M21(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M23(m,d,x,...) m(d,22,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M22(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M24(m,d,x,...) m(d,23,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M23(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M25(m,d,x,...) m(d,24,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M24(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M26(m,d,x,...) m(d,25,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M25(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M27(m,d,x,...) m(d,26,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M26(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M28(m,d,x,...) m(d,27,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M27(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M29(m,d,x,...) m(d,28,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M28(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M30(m,d,x,...) m(d,29,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M29(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M31(m,d,x,...) m(d,30,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M30(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M32(m,d,x,...) m(d,31,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M31(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M33(m,d,x,...) m(d,32,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M32(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M34(m,d,x,...) m(d,33,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M33(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M35(m,d,x,...) m(d,34,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M34(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M36(m,d,x,...) m(d,35,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M35(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M37(m,d,x,...) m(d,36,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M36(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M38(m,d,x,...) m(d,37,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M37(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M39(m,d,x,...) m(d,38,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M38(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M40(m,d,x,...) m(d,39,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M39(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M41(m,d,x,...) m(d,40,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M40(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M42(m,d,x,...) m(d,41,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M41(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M43(m,d,x,...) m(d,42,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M42(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M44(m,d,x,...) m(d,43,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M43(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M45(m,d,x,...) m(d,44,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M44(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M46(m,d,x,...) m(d,45,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M45(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M47(m,d,x,...) m(d,46,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M46(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M48(m,d,x,...) m(d,47,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M47(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M49(m,d,x,...) m(d,48,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M48(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M50(m,d,x,...) m(d,49,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M49(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M51(m,d,x,...) m(d,50,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M50(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M52(m,d,x,...) m(d,51,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M51(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M53(m,d,x,...) m(d,52,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M52(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M54(m,d,x,...) m(d,53,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M53(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M55(m,d,x,...) m(d,54,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M54(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M56(m,d,x,...) m(d,55,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M55(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M57(m,d,x,...) m(d,56,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M56(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M58(m,d,x,...) m(d,57,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M57(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M59(m,d,x,...) m(d,58,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M58(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M60(m,d,x,...) m(d,59,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M59(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M61(m,d,x,...) m(d,60,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M60(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M62(m,d,x,...) m(d,61,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M61(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M63(m,d,x,...) m(d,62,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M62(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M64(m,d,x,...) m(d,63,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M63(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M65(m,d,x,...) m(d,64,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M64(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M66(m,d,x,...) m(d,65,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M65(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M67(m,d,x,...) m(d,66,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M66(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M68(m,d,x,...) m(d,67,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M67(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M69(m,d,x,...) m(d,68,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M68(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M70(m,d,x,...) m(d,69,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M69(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M71(m,d,x,...) m(d,70,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M70(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M72(m,d,x,...) m(d,71,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M71(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M73(m,d,x,...) m(d,72,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M72(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M74(m,d,x,...) m(d,73,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M73(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M75(m,d,x,...) m(d,74,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M74(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M76(m,d,x,...) m(d,75,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M75(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M77(m,d,x,...) m(d,76,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M76(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M78(m,d,x,...) m(d,77,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M77(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M79(m,d,x,...) m(d,78,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M78(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M80(m,d,x,...) m(d,79,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M79(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M81(m,d,x,...) m(d,80,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M80(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M82(m,d,x,...) m(d,81,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M81(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M83(m,d,x,...) m(d,82,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M82(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M84(m,d,x,...) m(d,83,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M83(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M85(m,d,x,...) m(d,84,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M84(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M86(m,d,x,...) m(d,85,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M85(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M87(m,d,x,...) m(d,86,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M86(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M88(m,d,x,...) m(d,87,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M87(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M89(m,d,x,...) m(d,88,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M88(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M90(m,d,x,...) m(d,89,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M89(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M91(m,d,x,...) m(d,90,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M90(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M92(m,d,x,...) m(d,91,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M91(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M93(m,d,x,...) m(d,92,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M92(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M94(m,d,x,...) m(d,93,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M93(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M95(m,d,x,...) m(d,94,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M94(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M96(m,d,x,...) m(d,95,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M95(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M97(m,d,x,...) m(d,96,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M96(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M98(m,d,x,...) m(d,97,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M97(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M99(m,d,x,...) m(d,98,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M98(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M100(m,d,x,...) m(d,99,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M99(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M101(m,d,x,...) m(d,100,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M100(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M102(m,d,x,...) m(d,101,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M101(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M103(m,d,x,...) m(d,102,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M102(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M104(m,d,x,...) m(d,103,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M103(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M105(m,d,x,...) m(d,104,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M104(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M106(m,d,x,...) m(d,105,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M105(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M107(m,d,x,...) m(d,106,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M106(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M108(m,d,x,...) m(d,107,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M107(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M109(m,d,x,...) m(d,108,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M108(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M110(m,d,x,...) m(d,109,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M109(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M111(m,d,x,...) m(d,110,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M110(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M112(m,d,x,...) m(d,111,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M111(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M113(m,d,x,...) m(d,112,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M112(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M114(m,d,x,...) m(d,113,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M113(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M115(m,d,x,...) m(d,114,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M114(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M116(m,d,x,...) m(d,115,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M115(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M117(m,d,x,...) m(d,116,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M116(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M118(m,d,x,...) m(d,117,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M117(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M119(m,d,x,...) m(d,118,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M118(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M120(m,d,x,...) m(d,119,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M119(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M121(m,d,x,...) m(d,120,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M120(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M122(m,d,x,...) m(d,121,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M121(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M123(m,d,x,...) m(d,122,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M122(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M124(m,d,x,...) m(d,123,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M123(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M125(m,d,x,...) m(d,124,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M124(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M126(m,d,x,...) m(d,125,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M125(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M127(m,d,x,...) m(d,126,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M126(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M128(m,d,x,...) m(d,127,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M127(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M129(m,d,x,...) m(d,128,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M128(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M130(m,d,x,...) m(d,129,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M129(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M131(m,d,x,...) m(d,130,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M130(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M132(m,d,x,...) m(d,131,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M131(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M133(m,d,x,...) m(d,132,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M132(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M134(m,d,x,...) m(d,133,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M133(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M135(m,d,x,...) m(d,134,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M134(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M136(m,d,x,...) m(d,135,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M135(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M137(m,d,x,...) m(d,136,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M136(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M138(m,d,x,...) m(d,137,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M137(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M139(m,d,x,...) m(d,138,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M138(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M140(m,d,x,...) m(d,139,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M139(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M141(m,d,x,...) m(d,140,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M140(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M142(m,d,x,...) m(d,141,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M141(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M143(m,d,x,...) m(d,142,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M142(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M144(m,d,x,...) m(d,143,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M143(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M145(m,d,x,...) m(d,144,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M144(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M146(m,d,x,...) m(d,145,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M145(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M147(m,d,x,...) m(d,146,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M146(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M148(m,d,x,...) m(d,147,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M147(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M149(m,d,x,...) m(d,148,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M148(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M150(m,d,x,...) m(d,149,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M149(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M151(m,d,x,...) m(d,150,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M150(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M152(m,d,x,...) m(d,151,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M151(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M153(m,d,x,...) m(d,152,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M152(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M154(m,d,x,...) m(d,153,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M153(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M155(m,d,x,...) m(d,154,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M154(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M156(m,d,x,...) m(d,155,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M155(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M157(m,d,x,...) m(d,156,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M156(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M158(m,d,x,...) m(d,157,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M157(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M159(m,d,x,...) m(d,158,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M158(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M160(m,d,x,...) m(d,159,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M159(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M161(m,d,x,...) m(d,160,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M160(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M162(m,d,x,...) m(d,161,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M161(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M163(m,d,x,...) m(d,162,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M162(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M164(m,d,x,...) m(d,163,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M163(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M165(m,d,x,...) m(d,164,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M164(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M166(m,d,x,...) m(d,165,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M165(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M167(m,d,x,...) m(d,166,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M166(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M168(m,d,x,...) m(d,167,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M167(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M169(m,d,x,...) m(d,168,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M168(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M170(m,d,x,...) m(d,169,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M169(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M171(m,d,x,...) m(d,170,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M170(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M172(m,d,x,...) m(d,171,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M171(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M173(m,d,x,...) m(d,172,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M172(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M174(m,d,x,...) m(d,173,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M173(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M175(m,d,x,...) m(d,174,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M174(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M176(m,d,x,...) m(d,175,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M175(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M177(m,d,x,...) m(d,176,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M176(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M178(m,d,x,...) m(d,177,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M177(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M179(m,d,x,...) m(d,178,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M178(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M180(m,d,x,...) m(d,179,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M179(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M181(m,d,x,...) m(d,180,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M180(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M182(m,d,x,...) m(d,181,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M181(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M183(m,d,x,...) m(d,182,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M182(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M184(m,d,x,...) m(d,183,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M183(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M185(m,d,x,...) m(d,184,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M184(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M186(m,d,x,...) m(d,185,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M185(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M187(m,d,x,...) m(d,186,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M186(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M188(m,d,x,...) m(d,187,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M187(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M189(m,d,x,...) m(d,188,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M188(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M190(m,d,x,...) m(d,189,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M189(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M191(m,d,x,...) m(d,190,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M190(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M192(m,d,x,...) m(d,191,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M191(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M193(m,d,x,...) m(d,192,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M192(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M194(m,d,x,...) m(d,193,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M193(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M195(m,d,x,...) m(d,194,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M194(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M196(m,d,x,...) m(d,195,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M195(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M197(m,d,x,...) m(d,196,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M196(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M198(m,d,x,...) m(d,197,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M197(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M199(m,d,x,...) m(d,198,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M198(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M200(m,d,x,...) m(d,199,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M199(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M201(m,d,x,...) m(d,200,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M200(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M202(m,d,x,...) m(d,201,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M201(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M203(m,d,x,...) m(d,202,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M202(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M204(m,d,x,...) m(d,203,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M203(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M205(m,d,x,...) m(d,204,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M204(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M206(m,d,x,...) m(d,205,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M205(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M207(m,d,x,...) m(d,206,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M206(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M208(m,d,x,...) m(d,207,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M207(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M209(m,d,x,...) m(d,208,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M208(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M210(m,d,x,...) m(d,209,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M209(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M211(m,d,x,...) m(d,210,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M210(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M212(m,d,x,...) m(d,211,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M211(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M213(m,d,x,...) m(d,212,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M212(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M214(m,d,x,...) m(d,213,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M213(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M215(m,d,x,...) m(d,214,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M214(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M216(m,d,x,...) m(d,215,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M215(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M217(m,d,x,...) m(d,216,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M216(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M218(m,d,x,...) m(d,217,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M217(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M219(m,d,x,...) m(d,218,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M218(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M220(m,d,x,...) m(d,219,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M219(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M221(m,d,x,...) m(d,220,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M220(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M222(m,d,x,...) m(d,221,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M221(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M223(m,d,x,...) m(d,222,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M222(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M224(m,d,x,...) m(d,223,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M223(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M225(m,d,x,...) m(d,224,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M224(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M226(m,d,x,...) m(d,225,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M225(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M227(m,d,x,...) m(d,226,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M226(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M228(m,d,x,...) m(d,227,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M227(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M229(m,d,x,...) m(d,228,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M228(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M230(m,d,x,...) m(d,229,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M229(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M231(m,d,x,...) m(d,230,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M230(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M232(m,d,x,...) m(d,231,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M231(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M233(m,d,x,...) m(d,232,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M232(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M234(m,d,x,...) m(d,233,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M233(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M235(m,d,x,...) m(d,234,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M234(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M236(m,d,x,...) m(d,235,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M235(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M237(m,d,x,...) m(d,236,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M236(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M238(m,d,x,...) m(d,237,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M237(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M239(m,d,x,...) m(d,238,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M238(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M240(m,d,x,...) m(d,239,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M239(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M241(m,d,x,...) m(d,240,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M240(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M242(m,d,x,...) m(d,241,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M241(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M243(m,d,x,...) m(d,242,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M242(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M244(m,d,x,...) m(d,243,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M243(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M245(m,d,x,...) m(d,244,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M244(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M246(m,d,x,...) m(d,245,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M245(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M247(m,d,x,...) m(d,246,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M246(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M248(m,d,x,...) m(d,247,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M247(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M249(m,d,x,...) m(d,248,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M248(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M250(m,d,x,...) m(d,249,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M249(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M251(m,d,x,...) m(d,250,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M250(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M252(m,d,x,...) m(d,251,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M251(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M253(m,d,x,...) m(d,252,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M252(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M254(m,d,x,...) m(d,253,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M253(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M255(m,d,x,...) m(d,254,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M254(m,d,__VA_ARGS__)) -#define BETTER_ENUMS_M256(m,d,x,...) m(d,255,x) \ - BETTER_ENUMS_ID(BETTER_ENUMS_M255(m,d,__VA_ARGS__)) - -#define BETTER_ENUMS_PP_COUNT_IMPL(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, \ - _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, \ - _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, \ - _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, \ - _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, \ - _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, \ - _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98, _99, _100,\ - _101, _102, _103, _104, _105, _106, _107, _108, _109, _110, _111, _112, \ - _113, _114, _115, _116, _117, _118, _119, _120, _121, _122, _123, _124, \ - _125, _126, _127, _128, _129, _130, _131, _132, _133, _134, _135, _136, \ - _137, _138, _139, _140, _141, _142, _143, _144, _145, _146, _147, _148, \ - _149, _150, _151, _152, _153, _154, _155, _156, _157, _158, _159, _160, \ - _161, _162, _163, _164, _165, _166, _167, _168, _169, _170, _171, _172, \ - _173, _174, _175, _176, _177, _178, _179, _180, _181, _182, _183, _184, \ - _185, _186, _187, _188, _189, _190, _191, _192, _193, _194, _195, _196, \ - _197, _198, _199, _200, _201, _202, _203, _204, _205, _206, _207, _208, \ - _209, _210, _211, _212, _213, _214, _215, _216, _217, _218, _219, _220, \ - _221, _222, _223, _224, _225, _226, _227, _228, _229, _230, _231, _232, \ - _233, _234, _235, _236, _237, _238, _239, _240, _241, _242, _243, _244, \ - _245, _246, _247, _248, _249, _250, _251, _252, _253, _254, _255, _256, \ - count, ...) count - -#define BETTER_ENUMS_PP_COUNT(...) \ - BETTER_ENUMS_ID(BETTER_ENUMS_PP_COUNT_IMPL(__VA_ARGS__, 256, 255, 254, 253,\ - 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, \ - 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, \ - 224, 223, 222, 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, 211, \ - 210, 209, 208, 207, 206, 205, 204, 203, 202, 201, 200, 199, 198, 197, \ - 196, 195, 194, 193, 192, 191, 190, 189, 188, 187, 186, 185, 184, 183, \ - 182, 181, 180, 179, 178, 177, 176, 175, 174, 173, 172, 171, 170, 169, \ - 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, \ - 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 143, 142, 141, \ - 140, 139, 138, 137, 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, \ - 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, \ - 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, \ - 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81,\ - 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63,\ - 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45,\ - 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27,\ - 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, \ - 8, 7, 6, 5, 4, 3, 2, 1)) - -#define BETTER_ENUMS_ITERATE(X, f, l) X(f, l, 0) X(f, l, 1) X(f, l, 2) \ - X(f, l, 3) X(f, l, 4) X(f, l, 5) X(f, l, 6) X(f, l, 7) X(f, l, 8) \ - X(f, l, 9) X(f, l, 10) X(f, l, 11) X(f, l, 12) X(f, l, 13) X(f, l, 14) \ - X(f, l, 15) X(f, l, 16) X(f, l, 17) X(f, l, 18) X(f, l, 19) X(f, l, 20) \ - X(f, l, 21) X(f, l, 22) X(f, l, 23) - -#endif // #ifndef BETTER_ENUMS_MACRO_FILE_H diff --git a/src/cpp/3rdparty/iers2010/ch9/fcul_a.cpp b/src/cpp/3rdparty/iers2010/ch9/fcul_a.cpp index 6cdadf436..01b1cab4f 100644 --- a/src/cpp/3rdparty/iers2010/ch9/fcul_a.cpp +++ b/src/cpp/3rdparty/iers2010/ch9/fcul_a.cpp @@ -1,4 +1,4 @@ -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" #ifdef USE_EXTERNAL_CONSTS #include "iersc.hpp" #endif diff --git a/src/cpp/3rdparty/iers2010/ch9/fcul_zd_hpa.cpp b/src/cpp/3rdparty/iers2010/ch9/fcul_zd_hpa.cpp index 79ec24860..e9cec372b 100644 --- a/src/cpp/3rdparty/iers2010/ch9/fcul_zd_hpa.cpp +++ b/src/cpp/3rdparty/iers2010/ch9/fcul_zd_hpa.cpp @@ -1,4 +1,4 @@ -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" #ifdef USE_EXTERNAL_CONSTS #include "iersc.hpp" #endif diff --git a/src/cpp/3rdparty/iers2010/dehanttideinel/dehanttide_all.cpp b/src/cpp/3rdparty/iers2010/dehanttideinel/dehanttide_all.cpp index cda08a5b9..3a9e23f7b 100644 --- a/src/cpp/3rdparty/iers2010/dehanttideinel/dehanttide_all.cpp +++ b/src/cpp/3rdparty/iers2010/dehanttideinel/dehanttide_all.cpp @@ -1,5 +1,5 @@ -#include "constants.hpp" -#include "iers2010.hpp" +#include "common/constants.hpp" +#include "3rdparty/iers2010/iers2010.hpp" // #include namespace { diff --git a/src/cpp/3rdparty/iers2010/hardisp/admint.cpp b/src/cpp/3rdparty/iers2010/hardisp/admint.cpp index a74fdd369..97618d942 100644 --- a/src/cpp/3rdparty/iers2010/hardisp/admint.cpp +++ b/src/cpp/3rdparty/iers2010/hardisp/admint.cpp @@ -1,5 +1,5 @@ // #include "hardisp.hpp" -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" #ifdef DEBUG #include #endif diff --git a/src/cpp/3rdparty/iers2010/hardisp/eval.cpp b/src/cpp/3rdparty/iers2010/hardisp/eval.cpp index 62f833e64..b7a6c6ef2 100644 --- a/src/cpp/3rdparty/iers2010/hardisp/eval.cpp +++ b/src/cpp/3rdparty/iers2010/hardisp/eval.cpp @@ -1,5 +1,5 @@ // #include "hardisp.hpp" -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" #include #ifdef DEBUG #include diff --git a/src/cpp/3rdparty/iers2010/hardisp/hardisp_impl.cpp b/src/cpp/3rdparty/iers2010/hardisp/hardisp_impl.cpp index c9ef2614a..1b9a60fa7 100644 --- a/src/cpp/3rdparty/iers2010/hardisp/hardisp_impl.cpp +++ b/src/cpp/3rdparty/iers2010/hardisp/hardisp_impl.cpp @@ -1,5 +1,5 @@ // #include "hardisp.hpp" -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" #include #include //#include diff --git a/src/cpp/3rdparty/iers2010/hardisp/recurs.cpp b/src/cpp/3rdparty/iers2010/hardisp/recurs.cpp index 0832fe206..89dd2f9fa 100644 --- a/src/cpp/3rdparty/iers2010/hardisp/recurs.cpp +++ b/src/cpp/3rdparty/iers2010/hardisp/recurs.cpp @@ -1,5 +1,5 @@ // #include "hardisp.hpp" -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" /// @details The purpose of the function is to perform sine and cosine recursion /// to fill in data x, of length n, for nf sines and cosines with diff --git a/src/cpp/3rdparty/iers2010/hardisp/shells.cpp b/src/cpp/3rdparty/iers2010/hardisp/shells.cpp index 599a396c0..b5d3d75b6 100644 --- a/src/cpp/3rdparty/iers2010/hardisp/shells.cpp +++ b/src/cpp/3rdparty/iers2010/hardisp/shells.cpp @@ -1,5 +1,5 @@ // #include "hardisp.hpp" -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" #include #include diff --git a/src/cpp/3rdparty/iers2010/hardisp/spline.cpp b/src/cpp/3rdparty/iers2010/hardisp/spline.cpp index 1d2597b0c..a35174a56 100644 --- a/src/cpp/3rdparty/iers2010/hardisp/spline.cpp +++ b/src/cpp/3rdparty/iers2010/hardisp/spline.cpp @@ -1,5 +1,5 @@ // #include "hardisp.hpp" -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" inline double q(double u1, double x1, double u2, double x2) noexcept { return (u1 / (x1 * x1) - u2 / (x2 * x2)) / (1e0 / x1 - 1e0 / x2); diff --git a/src/cpp/3rdparty/iers2010/hardisp/tdfrph.cpp b/src/cpp/3rdparty/iers2010/hardisp/tdfrph.cpp index d6e6432bc..4e047ffa4 100644 --- a/src/cpp/3rdparty/iers2010/hardisp/tdfrph.cpp +++ b/src/cpp/3rdparty/iers2010/hardisp/tdfrph.cpp @@ -1,5 +1,5 @@ // #include "hardisp.hpp" -#include "iers2010.hpp" +#include "3rdparty/iers2010/iers2010.hpp" /// @details This subroutine returns the frequency and phase of a tidal /// constituent when its Doodson number is given as input. diff --git a/src/cpp/3rdparty/iers2010/iers2010.hpp b/src/cpp/3rdparty/iers2010/iers2010.hpp index a72b8ede3..821850f95 100644 --- a/src/cpp/3rdparty/iers2010/iers2010.hpp +++ b/src/cpp/3rdparty/iers2010/iers2010.hpp @@ -1,8 +1,8 @@ #pragma once -#include "eigenIncluder.hpp" -#include "gTime.hpp" +#include "common/eigenIncluder.hpp" +#include "common/gTime.hpp" namespace iers2010 { diff --git a/src/cpp/3rdparty/magic_enum.hpp b/src/cpp/3rdparty/magic_enum.hpp new file mode 100644 index 000000000..0fdc19ec3 --- /dev/null +++ b/src/cpp/3rdparty/magic_enum.hpp @@ -0,0 +1,1508 @@ +// __ __ _ ______ _____ +// | \/ | (_) | ____| / ____|_ _ +// | \ / | __ _ __ _ _ ___ | |__ _ __ _ _ _ __ ___ | | _| |_ _| |_ +// | |\/| |/ _` |/ _` | |/ __| | __| | '_ \| | | | '_ ` _ \ | | |_ _|_ _| +// | | | | (_| | (_| | | (__ | |____| | | | |_| | | | | | | | |____|_| |_| +// |_| |_|\__,_|\__, |_|\___| |______|_| |_|\__,_|_| |_| |_| \_____| +// __/ | https://github.com/Neargye/magic_enum +// |___/ version 0.9.6 +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2019 - 2024 Daniil Goncharov . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef NEARGYE_MAGIC_ENUM_HPP +#define NEARGYE_MAGIC_ENUM_HPP + +#define MAGIC_ENUM_VERSION_MAJOR 0 +#define MAGIC_ENUM_VERSION_MINOR 9 +#define MAGIC_ENUM_VERSION_PATCH 6 + +#ifndef MAGIC_ENUM_USE_STD_MODULE +#include +#include +#include +#include +#include +#include +#include +#endif + +#if defined(MAGIC_ENUM_CONFIG_FILE) +# include MAGIC_ENUM_CONFIG_FILE +#endif + +#ifndef MAGIC_ENUM_USE_STD_MODULE +#if !defined(MAGIC_ENUM_USING_ALIAS_OPTIONAL) +# include +#endif +#if !defined(MAGIC_ENUM_USING_ALIAS_STRING) +# include +#endif +#if !defined(MAGIC_ENUM_USING_ALIAS_STRING_VIEW) +# include +#endif +#endif + +#if defined(MAGIC_ENUM_NO_ASSERT) +# define MAGIC_ENUM_ASSERT(...) static_cast(0) +#elif !defined(MAGIC_ENUM_ASSERT) +# include +# define MAGIC_ENUM_ASSERT(...) assert((__VA_ARGS__)) +#endif + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunknown-warning-option" +# pragma clang diagnostic ignored "-Wenum-constexpr-conversion" +# pragma clang diagnostic ignored "-Wuseless-cast" // suppresses 'static_cast('\0')' for char_type = char (common on Linux). +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wmaybe-uninitialized" // May be used uninitialized 'return {};'. +# pragma GCC diagnostic ignored "-Wuseless-cast" // suppresses 'static_cast('\0')' for char_type = char (common on Linux). +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 26495) // Variable 'static_str::chars_' is uninitialized. +# pragma warning(disable : 28020) // Arithmetic overflow: Using operator '-' on a 4 byte value and then casting the result to a 8 byte value. +# pragma warning(disable : 26451) // The expression '0<=_Param_(1)&&_Param_(1)<=1-1' is not true at this call. +# pragma warning(disable : 4514) // Unreferenced inline function has been removed. +#endif + +// Checks magic_enum compiler compatibility. +#if defined(__clang__) && __clang_major__ >= 5 || defined(__GNUC__) && __GNUC__ >= 9 || defined(_MSC_VER) && _MSC_VER >= 1910 || defined(__RESHARPER__) +# undef MAGIC_ENUM_SUPPORTED +# define MAGIC_ENUM_SUPPORTED 1 +#endif + +// Checks magic_enum compiler aliases compatibility. +#if defined(__clang__) && __clang_major__ >= 5 || defined(__GNUC__) && __GNUC__ >= 9 || defined(_MSC_VER) && _MSC_VER >= 1920 +# undef MAGIC_ENUM_SUPPORTED_ALIASES +# define MAGIC_ENUM_SUPPORTED_ALIASES 1 +#endif + +// Enum value must be greater or equals than MAGIC_ENUM_RANGE_MIN. By default MAGIC_ENUM_RANGE_MIN = -128. +// If need another min range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MIN. +#if !defined(MAGIC_ENUM_RANGE_MIN) +# define MAGIC_ENUM_RANGE_MIN -128 +#endif + +// Enum value must be less or equals than MAGIC_ENUM_RANGE_MAX. By default MAGIC_ENUM_RANGE_MAX = 128. +// If need another max range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MAX. +#if !defined(MAGIC_ENUM_RANGE_MAX) +# define MAGIC_ENUM_RANGE_MAX 127 +#endif + +// Improve ReSharper C++ intellisense performance with builtins, avoiding unnecessary template instantiations. +#if defined(__RESHARPER__) +# undef MAGIC_ENUM_GET_ENUM_NAME_BUILTIN +# undef MAGIC_ENUM_GET_TYPE_NAME_BUILTIN +# if __RESHARPER__ >= 20230100 +# define MAGIC_ENUM_GET_ENUM_NAME_BUILTIN(V) __rscpp_enumerator_name(V) +# define MAGIC_ENUM_GET_TYPE_NAME_BUILTIN(T) __rscpp_type_name() +# else +# define MAGIC_ENUM_GET_ENUM_NAME_BUILTIN(V) nullptr +# define MAGIC_ENUM_GET_TYPE_NAME_BUILTIN(T) nullptr +# endif +#endif + +namespace magic_enum { + +// If need another optional type, define the macro MAGIC_ENUM_USING_ALIAS_OPTIONAL. +#if defined(MAGIC_ENUM_USING_ALIAS_OPTIONAL) +MAGIC_ENUM_USING_ALIAS_OPTIONAL +#else +using std::optional; +#endif + +// If need another string_view type, define the macro MAGIC_ENUM_USING_ALIAS_STRING_VIEW. +#if defined(MAGIC_ENUM_USING_ALIAS_STRING_VIEW) +MAGIC_ENUM_USING_ALIAS_STRING_VIEW +#else +using std::string_view; +#endif + +// If need another string type, define the macro MAGIC_ENUM_USING_ALIAS_STRING. +#if defined(MAGIC_ENUM_USING_ALIAS_STRING) +MAGIC_ENUM_USING_ALIAS_STRING +#else +using std::string; +#endif + +using char_type = string_view::value_type; +static_assert(std::is_same_v, "magic_enum::customize requires same string_view::value_type and string::value_type"); +static_assert([] { + if constexpr (std::is_same_v) { + constexpr const char c[] = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789|"; + constexpr const wchar_t wc[] = L"abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789|"; + static_assert(std::size(c) == std::size(wc), "magic_enum::customize identifier characters are multichars in wchar_t."); + + for (std::size_t i = 0; i < std::size(c); ++i) { + if (c[i] != wc[i]) { + return false; + } + } + } + return true; +} (), "magic_enum::customize wchar_t is not compatible with ASCII."); + +namespace customize { + +// Enum value must be in range [MAGIC_ENUM_RANGE_MIN, MAGIC_ENUM_RANGE_MAX]. By default MAGIC_ENUM_RANGE_MIN = -128, MAGIC_ENUM_RANGE_MAX = 128. +// If need another range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MIN and MAGIC_ENUM_RANGE_MAX. +// If need another range for specific enum type, add specialization enum_range for necessary enum type. +template +struct enum_range { + static constexpr int min = MAGIC_ENUM_RANGE_MIN; + static constexpr int max = MAGIC_ENUM_RANGE_MAX; +}; + +static_assert(MAGIC_ENUM_RANGE_MAX > MAGIC_ENUM_RANGE_MIN, "MAGIC_ENUM_RANGE_MAX must be greater than MAGIC_ENUM_RANGE_MIN."); + +namespace detail { + +enum class customize_tag { + default_tag, + invalid_tag, + custom_tag +}; + +} // namespace magic_enum::customize::detail + +class customize_t : public std::pair { + public: + constexpr customize_t(string_view srt) : std::pair{detail::customize_tag::custom_tag, srt} {} + constexpr customize_t(const char_type* srt) : customize_t{string_view{srt}} {} + constexpr customize_t(detail::customize_tag tag) : std::pair{tag, string_view{}} { + MAGIC_ENUM_ASSERT(tag != detail::customize_tag::custom_tag); + } +}; + +// Default customize. +inline constexpr auto default_tag = customize_t{detail::customize_tag::default_tag}; +// Invalid customize. +inline constexpr auto invalid_tag = customize_t{detail::customize_tag::invalid_tag}; + +// If need custom names for enum, add specialization enum_name for necessary enum type. +template +constexpr customize_t enum_name(E) noexcept { + return default_tag; +} + +// If need custom type name for enum, add specialization enum_type_name for necessary enum type. +template +constexpr customize_t enum_type_name() noexcept { + return default_tag; +} + +} // namespace magic_enum::customize + +namespace detail { + +template +struct supported +#if defined(MAGIC_ENUM_SUPPORTED) && MAGIC_ENUM_SUPPORTED || defined(MAGIC_ENUM_NO_CHECK_SUPPORT) + : std::true_type {}; +#else + : std::false_type {}; +#endif + +template , std::enable_if_t, int> = 0> +using enum_constant = std::integral_constant; + +template +inline constexpr bool always_false_v = false; + +template +struct has_is_flags : std::false_type {}; + +template +struct has_is_flags::is_flags)>> : std::bool_constant::is_flags)>>> {}; + +template +struct range_min : std::integral_constant {}; + +template +struct range_min::min)>> : std::integral_constant::min), customize::enum_range::min> {}; + +template +struct range_max : std::integral_constant {}; + +template +struct range_max::max)>> : std::integral_constant::max), customize::enum_range::max> {}; + +struct str_view { + const char* str_ = nullptr; + std::size_t size_ = 0; +}; + +template +class static_str { + public: + constexpr explicit static_str(str_view str) noexcept : static_str{str.str_, std::make_integer_sequence{}} { + MAGIC_ENUM_ASSERT(str.size_ == N); + } + + constexpr explicit static_str(string_view str) noexcept : static_str{str.data(), std::make_integer_sequence{}} { + MAGIC_ENUM_ASSERT(str.size() == N); + } + + constexpr const char_type* data() const noexcept { return chars_; } + + constexpr std::uint16_t size() const noexcept { return N; } + + constexpr operator string_view() const noexcept { return {data(), size()}; } + + private: + template + constexpr static_str(const char* str, std::integer_sequence) noexcept : chars_{static_cast(str[I])..., static_cast('\0')} {} + + template + constexpr static_str(string_view str, std::integer_sequence) noexcept : chars_{str[I]..., static_cast('\0')} {} + + char_type chars_[static_cast(N) + 1]; +}; + +template <> +class static_str<0> { + public: + constexpr explicit static_str() = default; + + constexpr explicit static_str(str_view) noexcept {} + + constexpr explicit static_str(string_view) noexcept {} + + constexpr const char_type* data() const noexcept { return nullptr; } + + constexpr std::uint16_t size() const noexcept { return 0; } + + constexpr operator string_view() const noexcept { return {}; } +}; + +template > +class case_insensitive { + static constexpr char_type to_lower(char_type c) noexcept { + return (c >= static_cast('A') && c <= static_cast('Z')) ? static_cast(c + (static_cast('a') - static_cast('A'))) : c; + } + + public: + template + constexpr auto operator()(L lhs, R rhs) const noexcept -> std::enable_if_t, char_type> && std::is_same_v, char_type>, bool> { + return Op{}(to_lower(lhs), to_lower(rhs)); + } +}; + +constexpr std::size_t find(string_view str, char_type c) noexcept { +#if defined(__clang__) && __clang_major__ < 9 && defined(__GLIBCXX__) || defined(_MSC_VER) && _MSC_VER < 1920 && !defined(__clang__) +// https://stackoverflow.com/questions/56484834/constexpr-stdstring-viewfind-last-of-doesnt-work-on-clang-8-with-libstdc +// https://developercommunity.visualstudio.com/content/problem/360432/vs20178-regression-c-failed-in-test.html + constexpr bool workaround = true; +#else + constexpr bool workaround = false; +#endif + + if constexpr (workaround) { + for (std::size_t i = 0; i < str.size(); ++i) { + if (str[i] == c) { + return i; + } + } + + return string_view::npos; + } else { + return str.find(c); + } +} + +template +constexpr bool is_default_predicate() noexcept { + return std::is_same_v, std::equal_to> || + std::is_same_v, std::equal_to<>>; +} + +template +constexpr bool is_nothrow_invocable() { + return is_default_predicate() || + std::is_nothrow_invocable_r_v; +} + +template +constexpr bool cmp_equal(string_view lhs, string_view rhs, [[maybe_unused]] BinaryPredicate&& p) noexcept(is_nothrow_invocable()) { +#if defined(_MSC_VER) && _MSC_VER < 1920 && !defined(__clang__) + // https://developercommunity.visualstudio.com/content/problem/360432/vs20178-regression-c-failed-in-test.html + // https://developercommunity.visualstudio.com/content/problem/232218/c-constexpr-string-view.html + constexpr bool workaround = true; +#else + constexpr bool workaround = false; +#endif + + if constexpr (!is_default_predicate() || workaround) { + if (lhs.size() != rhs.size()) { + return false; + } + + const auto size = lhs.size(); + for (std::size_t i = 0; i < size; ++i) { + if (!p(lhs[i], rhs[i])) { + return false; + } + } + + return true; + } else { + return lhs == rhs; + } +} + +template +constexpr bool cmp_less(L lhs, R rhs) noexcept { + static_assert(std::is_integral_v && std::is_integral_v, "magic_enum::detail::cmp_less requires integral type."); + + if constexpr (std::is_signed_v == std::is_signed_v) { + // If same signedness (both signed or both unsigned). + return lhs < rhs; + } else if constexpr (std::is_same_v) { // bool special case + return static_cast(lhs) < rhs; + } else if constexpr (std::is_same_v) { // bool special case + return lhs < static_cast(rhs); + } else if constexpr (std::is_signed_v) { + // If 'right' is negative, then result is 'false', otherwise cast & compare. + return rhs > 0 && lhs < static_cast>(rhs); + } else { + // If 'left' is negative, then result is 'true', otherwise cast & compare. + return lhs < 0 || static_cast>(lhs) < rhs; + } +} + +template +constexpr I log2(I value) noexcept { + static_assert(std::is_integral_v, "magic_enum::detail::log2 requires integral type."); + + if constexpr (std::is_same_v) { // bool special case + return MAGIC_ENUM_ASSERT(false), value; + } else { + auto ret = I{0}; + for (; value > I{1}; value >>= I{1}, ++ret) {} + + return ret; + } +} + +#if defined(__cpp_lib_array_constexpr) && __cpp_lib_array_constexpr >= 201603L +# define MAGIC_ENUM_ARRAY_CONSTEXPR 1 +#else +template +constexpr std::array, N> to_array(T (&a)[N], std::index_sequence) noexcept { + return {{a[I]...}}; +} +#endif + +template +inline constexpr bool is_enum_v = std::is_enum_v && std::is_same_v>; + +template +constexpr auto n() noexcept { + static_assert(is_enum_v, "magic_enum::detail::n requires enum type."); + + if constexpr (supported::value) { +#if defined(MAGIC_ENUM_GET_TYPE_NAME_BUILTIN) + constexpr auto name_ptr = MAGIC_ENUM_GET_TYPE_NAME_BUILTIN(E); + constexpr auto name = name_ptr ? str_view{name_ptr, std::char_traits::length(name_ptr)} : str_view{}; +#elif defined(__clang__) + str_view name; + if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) { + static_assert(always_false_v, "magic_enum::detail::n requires __PRETTY_FUNCTION__."); + return str_view{}; + } else { + name.size_ = sizeof(__PRETTY_FUNCTION__) - 36; + name.str_ = __PRETTY_FUNCTION__ + 34; + } +#elif defined(__GNUC__) + auto name = str_view{__PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 1}; + if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) { + static_assert(always_false_v, "magic_enum::detail::n requires __PRETTY_FUNCTION__."); + return str_view{}; + } else if (name.str_[name.size_ - 1] == ']') { + name.size_ -= 50; + name.str_ += 49; + } else { + name.size_ -= 40; + name.str_ += 37; + } +#elif defined(_MSC_VER) + // CLI/C++ workaround (see https://github.com/Neargye/magic_enum/issues/284). + str_view name; + name.str_ = __FUNCSIG__; + name.str_ += 40; + name.size_ += sizeof(__FUNCSIG__) - 57; +#else + auto name = str_view{}; +#endif + std::size_t p = 0; + for (std::size_t i = name.size_; i > 0; --i) { + if (name.str_[i] == ':') { + p = i + 1; + break; + } + } + if (p > 0) { + name.size_ -= p; + name.str_ += p; + } + return name; + } else { + return str_view{}; // Unsupported compiler or Invalid customize. + } +} + +template +constexpr auto type_name() noexcept { + [[maybe_unused]] constexpr auto custom = customize::enum_type_name(); + static_assert(std::is_same_v, customize::customize_t>, "magic_enum::customize requires customize_t type."); + if constexpr (custom.first == customize::detail::customize_tag::custom_tag) { + constexpr auto name = custom.second; + static_assert(!name.empty(), "magic_enum::customize requires not empty string."); + return static_str{name}; + } else if constexpr (custom.first == customize::detail::customize_tag::invalid_tag) { + return static_str<0>{}; + } else if constexpr (custom.first == customize::detail::customize_tag::default_tag) { + constexpr auto name = n(); + return static_str{name}; + } else { + static_assert(always_false_v, "magic_enum::customize invalid."); + } +} + +template +inline constexpr auto type_name_v = type_name(); + +template +constexpr auto n() noexcept { + static_assert(is_enum_v, "magic_enum::detail::n requires enum type."); + + if constexpr (supported::value) { +#if defined(MAGIC_ENUM_GET_ENUM_NAME_BUILTIN) + constexpr auto name_ptr = MAGIC_ENUM_GET_ENUM_NAME_BUILTIN(V); + auto name = name_ptr ? str_view{name_ptr, std::char_traits::length(name_ptr)} : str_view{}; +#elif defined(__clang__) + str_view name; + if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) { + static_assert(always_false_v, "magic_enum::detail::n requires __PRETTY_FUNCTION__."); + return str_view{}; + } else { + name.size_ = sizeof(__PRETTY_FUNCTION__) - 36; + name.str_ = __PRETTY_FUNCTION__ + 34; + } + if (name.size_ > 22 && name.str_[0] == '(' && name.str_[1] == 'a' && name.str_[10] == ' ' && name.str_[22] == ':') { + name.size_ -= 23; + name.str_ += 23; + } + if (name.str_[0] == '(' || name.str_[0] == '-' || (name.str_[0] >= '0' && name.str_[0] <= '9')) { + name = str_view{}; + } +#elif defined(__GNUC__) + auto name = str_view{__PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 1}; + if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) { + static_assert(always_false_v, "magic_enum::detail::n requires __PRETTY_FUNCTION__."); + return str_view{}; + } else if (name.str_[name.size_ - 1] == ']') { + name.size_ -= 55; + name.str_ += 54; + } else { + name.size_ -= 40; + name.str_ += 37; + } + if (name.str_[0] == '(') { + name = str_view{}; + } +#elif defined(_MSC_VER) + str_view name; + if ((__FUNCSIG__[5] == '_' && __FUNCSIG__[35] != '(') || (__FUNCSIG__[5] == 'c' && __FUNCSIG__[41] != '(')) { + // CLI/C++ workaround (see https://github.com/Neargye/magic_enum/issues/284). + name.str_ = __FUNCSIG__; + name.str_ += 35; + name.size_ = sizeof(__FUNCSIG__) - 52; + } +#else + auto name = str_view{}; +#endif + std::size_t p = 0; + for (std::size_t i = name.size_; i > 0; --i) { + if (name.str_[i] == ':') { + p = i + 1; + break; + } + } + if (p > 0) { + name.size_ -= p; + name.str_ += p; + } + return name; + } else { + return str_view{}; // Unsupported compiler or Invalid customize. + } +} + +#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER < 1920 +# define MAGIC_ENUM_VS_2017_WORKAROUND 1 +#endif + +#if defined(MAGIC_ENUM_VS_2017_WORKAROUND) +template +constexpr auto n() noexcept { + static_assert(is_enum_v, "magic_enum::detail::n requires enum type."); + +# if defined(MAGIC_ENUM_GET_ENUM_NAME_BUILTIN) + constexpr auto name_ptr = MAGIC_ENUM_GET_ENUM_NAME_BUILTIN(V); + auto name = name_ptr ? str_view{name_ptr, std::char_traits::length(name_ptr)} : str_view{}; +# else + // CLI/C++ workaround (see https://github.com/Neargye/magic_enum/issues/284). + str_view name; + name.str_ = __FUNCSIG__; + name.size_ = sizeof(__FUNCSIG__) - 17; + std::size_t p = 0; + for (std::size_t i = name.size_; i > 0; --i) { + if (name.str_[i] == ',' || name.str_[i] == ':') { + p = i + 1; + break; + } + } + if (p > 0) { + name.size_ -= p; + name.str_ += p; + } + if (name.str_[0] == '(' || name.str_[0] == '-' || (name.str_[0] >= '0' && name.str_[0] <= '9')) { + name = str_view{}; + } + return name; +# endif +} +#endif + +template +constexpr auto enum_name() noexcept { + [[maybe_unused]] constexpr auto custom = customize::enum_name(V); + static_assert(std::is_same_v, customize::customize_t>, "magic_enum::customize requires customize_t type."); + if constexpr (custom.first == customize::detail::customize_tag::custom_tag) { + constexpr auto name = custom.second; + static_assert(!name.empty(), "magic_enum::customize requires not empty string."); + return static_str{name}; + } else if constexpr (custom.first == customize::detail::customize_tag::invalid_tag) { + return static_str<0>{}; + } else if constexpr (custom.first == customize::detail::customize_tag::default_tag) { +#if defined(MAGIC_ENUM_VS_2017_WORKAROUND) + constexpr auto name = n(); +#else + constexpr auto name = n(); +#endif + return static_str{name}; + } else { + static_assert(always_false_v, "magic_enum::customize invalid."); + } +} + +template +inline constexpr auto enum_name_v = enum_name(); + +template +constexpr bool is_valid() noexcept { +#if defined(__clang__) && __clang_major__ >= 16 + // https://reviews.llvm.org/D130058, https://reviews.llvm.org/D131307 + constexpr E v = __builtin_bit_cast(E, V); +#else + constexpr E v = static_cast(V); +#endif + [[maybe_unused]] constexpr auto custom = customize::enum_name(v); + static_assert(std::is_same_v, customize::customize_t>, "magic_enum::customize requires customize_t type."); + if constexpr (custom.first == customize::detail::customize_tag::custom_tag) { + constexpr auto name = custom.second; + static_assert(!name.empty(), "magic_enum::customize requires not empty string."); + return name.size() != 0; + } else if constexpr (custom.first == customize::detail::customize_tag::default_tag) { +#if defined(MAGIC_ENUM_VS_2017_WORKAROUND) + return n().size_ != 0; +#else + return n().size_ != 0; +#endif + } else { + return false; + } +} + +enum class enum_subtype { + common, + flags +}; + +template > +constexpr U ualue(std::size_t i) noexcept { + if constexpr (std::is_same_v) { // bool special case + static_assert(O == 0, "magic_enum::detail::ualue requires valid offset."); + + return static_cast(i); + } else if constexpr (S == enum_subtype::flags) { + return static_cast(U{1} << static_cast(static_cast(i) + O)); + } else { + return static_cast(static_cast(i) + O); + } +} + +template > +constexpr E value(std::size_t i) noexcept { + return static_cast(ualue(i)); +} + +template > +constexpr int reflected_min() noexcept { + if constexpr (S == enum_subtype::flags) { + return 0; + } else { + constexpr auto lhs = range_min::value; + constexpr auto rhs = (std::numeric_limits::min)(); + + if constexpr (cmp_less(rhs, lhs)) { + return lhs; + } else { + return rhs; + } + } +} + +template > +constexpr int reflected_max() noexcept { + if constexpr (S == enum_subtype::flags) { + return std::numeric_limits::digits - 1; + } else { + constexpr auto lhs = range_max::value; + constexpr auto rhs = (std::numeric_limits::max)(); + + if constexpr (cmp_less(lhs, rhs)) { + return lhs; + } else { + return rhs; + } + } +} + +#define MAGIC_ENUM_FOR_EACH_256(T) \ + T( 0)T( 1)T( 2)T( 3)T( 4)T( 5)T( 6)T( 7)T( 8)T( 9)T( 10)T( 11)T( 12)T( 13)T( 14)T( 15)T( 16)T( 17)T( 18)T( 19)T( 20)T( 21)T( 22)T( 23)T( 24)T( 25)T( 26)T( 27)T( 28)T( 29)T( 30)T( 31) \ + T( 32)T( 33)T( 34)T( 35)T( 36)T( 37)T( 38)T( 39)T( 40)T( 41)T( 42)T( 43)T( 44)T( 45)T( 46)T( 47)T( 48)T( 49)T( 50)T( 51)T( 52)T( 53)T( 54)T( 55)T( 56)T( 57)T( 58)T( 59)T( 60)T( 61)T( 62)T( 63) \ + T( 64)T( 65)T( 66)T( 67)T( 68)T( 69)T( 70)T( 71)T( 72)T( 73)T( 74)T( 75)T( 76)T( 77)T( 78)T( 79)T( 80)T( 81)T( 82)T( 83)T( 84)T( 85)T( 86)T( 87)T( 88)T( 89)T( 90)T( 91)T( 92)T( 93)T( 94)T( 95) \ + T( 96)T( 97)T( 98)T( 99)T(100)T(101)T(102)T(103)T(104)T(105)T(106)T(107)T(108)T(109)T(110)T(111)T(112)T(113)T(114)T(115)T(116)T(117)T(118)T(119)T(120)T(121)T(122)T(123)T(124)T(125)T(126)T(127) \ + T(128)T(129)T(130)T(131)T(132)T(133)T(134)T(135)T(136)T(137)T(138)T(139)T(140)T(141)T(142)T(143)T(144)T(145)T(146)T(147)T(148)T(149)T(150)T(151)T(152)T(153)T(154)T(155)T(156)T(157)T(158)T(159) \ + T(160)T(161)T(162)T(163)T(164)T(165)T(166)T(167)T(168)T(169)T(170)T(171)T(172)T(173)T(174)T(175)T(176)T(177)T(178)T(179)T(180)T(181)T(182)T(183)T(184)T(185)T(186)T(187)T(188)T(189)T(190)T(191) \ + T(192)T(193)T(194)T(195)T(196)T(197)T(198)T(199)T(200)T(201)T(202)T(203)T(204)T(205)T(206)T(207)T(208)T(209)T(210)T(211)T(212)T(213)T(214)T(215)T(216)T(217)T(218)T(219)T(220)T(221)T(222)T(223) \ + T(224)T(225)T(226)T(227)T(228)T(229)T(230)T(231)T(232)T(233)T(234)T(235)T(236)T(237)T(238)T(239)T(240)T(241)T(242)T(243)T(244)T(245)T(246)T(247)T(248)T(249)T(250)T(251)T(252)T(253)T(254)T(255) + +template +constexpr void valid_count(bool* valid, std::size_t& count) noexcept { +#define MAGIC_ENUM_V(O) \ + if constexpr ((I + O) < Size) { \ + if constexpr (is_valid(I + O)>()) { \ + valid[I + O] = true; \ + ++count; \ + } \ + } + + MAGIC_ENUM_FOR_EACH_256(MAGIC_ENUM_V) + + if constexpr ((I + 256) < Size) { + valid_count(valid, count); + } +#undef MAGIC_ENUM_V +} + +template +struct valid_count_t { + std::size_t count = 0; + bool valid[N] = {}; +}; + +template +constexpr auto valid_count() noexcept { + valid_count_t vc; + valid_count(vc.valid, vc.count); + return vc; +} + +template +constexpr auto values() noexcept { + constexpr auto vc = valid_count(); + + if constexpr (vc.count > 0) { +#if defined(MAGIC_ENUM_ARRAY_CONSTEXPR) + std::array values = {}; +#else + E values[vc.count] = {}; +#endif + for (std::size_t i = 0, v = 0; v < vc.count; ++i) { + if (vc.valid[i]) { + values[v++] = value(i); + } + } +#if defined(MAGIC_ENUM_ARRAY_CONSTEXPR) + return values; +#else + return to_array(values, std::make_index_sequence{}); +#endif + } else { + return std::array{}; + } +} + +template > +constexpr auto values() noexcept { + constexpr auto min = reflected_min(); + constexpr auto max = reflected_max(); + constexpr auto range_size = max - min + 1; + static_assert(range_size > 0, "magic_enum::enum_range requires valid size."); + + return values(); +} + +template > +constexpr enum_subtype subtype(std::true_type) noexcept { + if constexpr (std::is_same_v) { // bool special case + return enum_subtype::common; + } else if constexpr (has_is_flags::value) { + return customize::enum_range::is_flags ? enum_subtype::flags : enum_subtype::common; + } else { +#if defined(MAGIC_ENUM_AUTO_IS_FLAGS) + constexpr auto flags_values = values(); + constexpr auto default_values = values(); + if (flags_values.size() == 0 || default_values.size() > flags_values.size()) { + return enum_subtype::common; + } + for (std::size_t i = 0; i < default_values.size(); ++i) { + const auto v = static_cast(default_values[i]); + if (v != 0 && (v & (v - 1)) != 0) { + return enum_subtype::common; + } + } + return enum_subtype::flags; +#else + return enum_subtype::common; +#endif + } +} + +template +constexpr enum_subtype subtype(std::false_type) noexcept { + // For non-enum type return default common subtype. + return enum_subtype::common; +} + +template > +inline constexpr auto subtype_v = subtype(std::is_enum{}); + +template +inline constexpr auto values_v = values(); + +template > +using values_t = decltype((values_v)); + +template +inline constexpr auto count_v = values_v.size(); + +template > +inline constexpr auto min_v = (count_v > 0) ? static_cast(values_v.front()) : U{0}; + +template > +inline constexpr auto max_v = (count_v > 0) ? static_cast(values_v.back()) : U{0}; + +template +constexpr auto names(std::index_sequence) noexcept { + constexpr auto names = std::array{{enum_name_v[I]>...}}; + return names; +} + +template +inline constexpr auto names_v = names(std::make_index_sequence>{}); + +template > +using names_t = decltype((names_v)); + +template +constexpr auto entries(std::index_sequence) noexcept { + constexpr auto entries = std::array, sizeof...(I)>{{{values_v[I], enum_name_v[I]>}...}}; + return entries; +} + +template +inline constexpr auto entries_v = entries(std::make_index_sequence>{}); + +template > +using entries_t = decltype((entries_v)); + +template > +constexpr bool is_sparse() noexcept { + if constexpr (count_v == 0) { + return false; + } else if constexpr (std::is_same_v) { // bool special case + return false; + } else { + constexpr auto max = (S == enum_subtype::flags) ? log2(max_v) : max_v; + constexpr auto min = (S == enum_subtype::flags) ? log2(min_v) : min_v; + constexpr auto range_size = max - min + 1; + + return range_size != count_v; + } +} + +template > +inline constexpr bool is_sparse_v = is_sparse(); + +template +struct is_reflected +#if defined(MAGIC_ENUM_NO_CHECK_REFLECTED_ENUM) + : std::true_type {}; +#else + : std::bool_constant && (count_v != 0)> {}; +#endif + +template +inline constexpr bool is_reflected_v = is_reflected, S>{}; + +template +struct enable_if_enum {}; + +template +struct enable_if_enum { + using type = R; + static_assert(supported::value, "magic_enum unsupported compiler (https://github.com/Neargye/magic_enum#compiler-compatibility)."); +}; + +template , typename D = std::decay_t> +using enable_if_t = typename enable_if_enum && std::is_invocable_r_v, R>::type; + +template >, int> = 0> +using enum_concept = T; + +template > +struct is_scoped_enum : std::false_type {}; + +template +struct is_scoped_enum : std::bool_constant>> {}; + +template > +struct is_unscoped_enum : std::false_type {}; + +template +struct is_unscoped_enum : std::bool_constant>> {}; + +template >> +struct underlying_type {}; + +template +struct underlying_type : std::underlying_type> {}; + +#if defined(MAGIC_ENUM_ENABLE_HASH) || defined(MAGIC_ENUM_ENABLE_HASH_SWITCH) + +template +struct constexpr_hash_t; + +template +struct constexpr_hash_t>> { + constexpr auto operator()(Value value) const noexcept { + using U = typename underlying_type::type; + if constexpr (std::is_same_v) { // bool special case + return static_cast(value); + } else { + return static_cast(value); + } + } + using secondary_hash = constexpr_hash_t; +}; + +template +struct constexpr_hash_t>> { + static constexpr std::uint32_t crc_table[256] { + 0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L, 0x706af48fL, 0xe963a535L, 0x9e6495a3L, + 0x0edb8832L, 0x79dcb8a4L, 0xe0d5e91eL, 0x97d2d988L, 0x09b64c2bL, 0x7eb17cbdL, 0xe7b82d07L, 0x90bf1d91L, + 0x1db71064L, 0x6ab020f2L, 0xf3b97148L, 0x84be41deL, 0x1adad47dL, 0x6ddde4ebL, 0xf4d4b551L, 0x83d385c7L, + 0x136c9856L, 0x646ba8c0L, 0xfd62f97aL, 0x8a65c9ecL, 0x14015c4fL, 0x63066cd9L, 0xfa0f3d63L, 0x8d080df5L, + 0x3b6e20c8L, 0x4c69105eL, 0xd56041e4L, 0xa2677172L, 0x3c03e4d1L, 0x4b04d447L, 0xd20d85fdL, 0xa50ab56bL, + 0x35b5a8faL, 0x42b2986cL, 0xdbbbc9d6L, 0xacbcf940L, 0x32d86ce3L, 0x45df5c75L, 0xdcd60dcfL, 0xabd13d59L, + 0x26d930acL, 0x51de003aL, 0xc8d75180L, 0xbfd06116L, 0x21b4f4b5L, 0x56b3c423L, 0xcfba9599L, 0xb8bda50fL, + 0x2802b89eL, 0x5f058808L, 0xc60cd9b2L, 0xb10be924L, 0x2f6f7c87L, 0x58684c11L, 0xc1611dabL, 0xb6662d3dL, + 0x76dc4190L, 0x01db7106L, 0x98d220bcL, 0xefd5102aL, 0x71b18589L, 0x06b6b51fL, 0x9fbfe4a5L, 0xe8b8d433L, + 0x7807c9a2L, 0x0f00f934L, 0x9609a88eL, 0xe10e9818L, 0x7f6a0dbbL, 0x086d3d2dL, 0x91646c97L, 0xe6635c01L, + 0x6b6b51f4L, 0x1c6c6162L, 0x856530d8L, 0xf262004eL, 0x6c0695edL, 0x1b01a57bL, 0x8208f4c1L, 0xf50fc457L, + 0x65b0d9c6L, 0x12b7e950L, 0x8bbeb8eaL, 0xfcb9887cL, 0x62dd1ddfL, 0x15da2d49L, 0x8cd37cf3L, 0xfbd44c65L, + 0x4db26158L, 0x3ab551ceL, 0xa3bc0074L, 0xd4bb30e2L, 0x4adfa541L, 0x3dd895d7L, 0xa4d1c46dL, 0xd3d6f4fbL, + 0x4369e96aL, 0x346ed9fcL, 0xad678846L, 0xda60b8d0L, 0x44042d73L, 0x33031de5L, 0xaa0a4c5fL, 0xdd0d7cc9L, + 0x5005713cL, 0x270241aaL, 0xbe0b1010L, 0xc90c2086L, 0x5768b525L, 0x206f85b3L, 0xb966d409L, 0xce61e49fL, + 0x5edef90eL, 0x29d9c998L, 0xb0d09822L, 0xc7d7a8b4L, 0x59b33d17L, 0x2eb40d81L, 0xb7bd5c3bL, 0xc0ba6cadL, + 0xedb88320L, 0x9abfb3b6L, 0x03b6e20cL, 0x74b1d29aL, 0xead54739L, 0x9dd277afL, 0x04db2615L, 0x73dc1683L, + 0xe3630b12L, 0x94643b84L, 0x0d6d6a3eL, 0x7a6a5aa8L, 0xe40ecf0bL, 0x9309ff9dL, 0x0a00ae27L, 0x7d079eb1L, + 0xf00f9344L, 0x8708a3d2L, 0x1e01f268L, 0x6906c2feL, 0xf762575dL, 0x806567cbL, 0x196c3671L, 0x6e6b06e7L, + 0xfed41b76L, 0x89d32be0L, 0x10da7a5aL, 0x67dd4accL, 0xf9b9df6fL, 0x8ebeeff9L, 0x17b7be43L, 0x60b08ed5L, + 0xd6d6a3e8L, 0xa1d1937eL, 0x38d8c2c4L, 0x4fdff252L, 0xd1bb67f1L, 0xa6bc5767L, 0x3fb506ddL, 0x48b2364bL, + 0xd80d2bdaL, 0xaf0a1b4cL, 0x36034af6L, 0x41047a60L, 0xdf60efc3L, 0xa867df55L, 0x316e8eefL, 0x4669be79L, + 0xcb61b38cL, 0xbc66831aL, 0x256fd2a0L, 0x5268e236L, 0xcc0c7795L, 0xbb0b4703L, 0x220216b9L, 0x5505262fL, + 0xc5ba3bbeL, 0xb2bd0b28L, 0x2bb45a92L, 0x5cb36a04L, 0xc2d7ffa7L, 0xb5d0cf31L, 0x2cd99e8bL, 0x5bdeae1dL, + 0x9b64c2b0L, 0xec63f226L, 0x756aa39cL, 0x026d930aL, 0x9c0906a9L, 0xeb0e363fL, 0x72076785L, 0x05005713L, + 0x95bf4a82L, 0xe2b87a14L, 0x7bb12baeL, 0x0cb61b38L, 0x92d28e9bL, 0xe5d5be0dL, 0x7cdcefb7L, 0x0bdbdf21L, + 0x86d3d2d4L, 0xf1d4e242L, 0x68ddb3f8L, 0x1fda836eL, 0x81be16cdL, 0xf6b9265bL, 0x6fb077e1L, 0x18b74777L, + 0x88085ae6L, 0xff0f6a70L, 0x66063bcaL, 0x11010b5cL, 0x8f659effL, 0xf862ae69L, 0x616bffd3L, 0x166ccf45L, + 0xa00ae278L, 0xd70dd2eeL, 0x4e048354L, 0x3903b3c2L, 0xa7672661L, 0xd06016f7L, 0x4969474dL, 0x3e6e77dbL, + 0xaed16a4aL, 0xd9d65adcL, 0x40df0b66L, 0x37d83bf0L, 0xa9bcae53L, 0xdebb9ec5L, 0x47b2cf7fL, 0x30b5ffe9L, + 0xbdbdf21cL, 0xcabac28aL, 0x53b39330L, 0x24b4a3a6L, 0xbad03605L, 0xcdd70693L, 0x54de5729L, 0x23d967bfL, + 0xb3667a2eL, 0xc4614ab8L, 0x5d681b02L, 0x2a6f2b94L, 0xb40bbe37L, 0xc30c8ea1L, 0x5a05df1bL, 0x2d02ef8dL + }; + constexpr std::uint32_t operator()(string_view value) const noexcept { + auto crc = static_cast(0xffffffffL); + for (const auto c : value) { + crc = (crc >> 8) ^ crc_table[(crc ^ static_cast(c)) & 0xff]; + } + return crc ^ 0xffffffffL; + } + + struct secondary_hash { + constexpr std::uint32_t operator()(string_view value) const noexcept { + auto acc = static_cast(2166136261ULL); + for (const auto c : value) { + acc = ((acc ^ static_cast(c)) * static_cast(16777619ULL)) & (std::numeric_limits::max)(); + } + return static_cast(acc); + } + }; +}; + +template +inline constexpr Hash hash_v{}; + +template +constexpr auto calculate_cases(std::size_t Page) noexcept { + constexpr std::array values = *GlobValues; + constexpr std::size_t size = values.size(); + + using switch_t = std::invoke_result_t; + static_assert(std::is_integral_v && !std::is_same_v); + const std::size_t values_to = (std::min)(static_cast(256), size - Page); + + std::array result{}; + auto fill = result.begin(); + { + auto first = values.begin() + static_cast(Page); + auto last = values.begin() + static_cast(Page + values_to); + while (first != last) { + *fill++ = hash_v(*first++); + } + } + + // dead cases, try to avoid case collisions + for (switch_t last_value = result[values_to - 1]; fill != result.end() && last_value != (std::numeric_limits::max)(); *fill++ = ++last_value) { + } + + { + auto it = result.begin(); + auto last_value = (std::numeric_limits::min)(); + for (; fill != result.end(); *fill++ = last_value++) { + while (last_value == *it) { + ++last_value, ++it; + } + } + } + + return result; +} + +template +constexpr R invoke_r(F&& f, Args&&... args) noexcept(std::is_nothrow_invocable_r_v) { + if constexpr (std::is_void_v) { + std::forward(f)(std::forward(args)...); + } else { + return static_cast(std::forward(f)(std::forward(args)...)); + } +} + +enum class case_call_t { + index, + value +}; + +template +inline constexpr auto default_result_type_lambda = []() noexcept(std::is_nothrow_default_constructible_v) { return T{}; }; + +template <> +inline constexpr auto default_result_type_lambda = []() noexcept {}; + +template +constexpr bool has_duplicate() noexcept { + using value_t = std::decay_t; + using hash_value_t = std::invoke_result_t; + std::arraysize()> hashes{}; + std::size_t size = 0; + for (auto elem : *Arr) { + hashes[size] = hash_v(elem); + for (auto i = size++; i > 0; --i) { + if (hashes[i] < hashes[i - 1]) { + auto tmp = hashes[i]; + hashes[i] = hashes[i - 1]; + hashes[i - 1] = tmp; + } else if (hashes[i] == hashes[i - 1]) { + return false; + } else { + break; + } + } + } + return true; +} + +#define MAGIC_ENUM_CASE(val) \ + case cases[val]: \ + if constexpr ((val) + Page < size) { \ + if (!pred(values[val + Page], searched)) { \ + break; \ + } \ + if constexpr (CallValue == case_call_t::index) { \ + if constexpr (std::is_invocable_r_v>) { \ + return detail::invoke_r(std::forward(lambda), std::integral_constant{}); \ + } else if constexpr (std::is_invocable_v>) { \ + MAGIC_ENUM_ASSERT(false && "magic_enum::detail::constexpr_switch wrong result type."); \ + } \ + } else if constexpr (CallValue == case_call_t::value) { \ + if constexpr (std::is_invocable_r_v>) { \ + return detail::invoke_r(std::forward(lambda), enum_constant{}); \ + } else if constexpr (std::is_invocable_r_v>) { \ + MAGIC_ENUM_ASSERT(false && "magic_enum::detail::constexpr_switch wrong result type."); \ + } \ + } \ + break; \ + } else [[fallthrough]]; + +template ::value_type>, + typename BinaryPredicate = std::equal_to<>, + typename Lambda, + typename ResultGetterType> +constexpr decltype(auto) constexpr_switch( + Lambda&& lambda, + typename std::decay_t::value_type searched, + ResultGetterType&& def, + BinaryPredicate&& pred = {}) { + using result_t = std::invoke_result_t; + using hash_t = std::conditional_t(), Hash, typename Hash::secondary_hash>; + static_assert(has_duplicate(), "magic_enum::detail::constexpr_switch duplicated hash found, please report it: https://github.com/Neargye/magic_enum/issues."); + constexpr std::array values = *GlobValues; + constexpr std::size_t size = values.size(); + constexpr std::array cases = calculate_cases(Page); + + switch (hash_v(searched)) { + MAGIC_ENUM_FOR_EACH_256(MAGIC_ENUM_CASE) + default: + if constexpr (size > 256 + Page) { + return constexpr_switch(std::forward(lambda), searched, std::forward(def)); + } + break; + } + return def(); +} + +#undef MAGIC_ENUM_CASE + +#endif + +} // namespace magic_enum::detail + +// Checks is magic_enum supported compiler. +inline constexpr bool is_magic_enum_supported = detail::supported::value; + +template +using Enum = detail::enum_concept; + +// Checks whether T is an Unscoped enumeration type. +// Provides the member constant value which is equal to true, if T is an [Unscoped enumeration](https://en.cppreference.com/w/cpp/language/enum#Unscoped_enumeration) type. Otherwise, value is equal to false. +template +struct is_unscoped_enum : detail::is_unscoped_enum {}; + +template +inline constexpr bool is_unscoped_enum_v = is_unscoped_enum::value; + +// Checks whether T is an Scoped enumeration type. +// Provides the member constant value which is equal to true, if T is an [Scoped enumeration](https://en.cppreference.com/w/cpp/language/enum#Scoped_enumerations) type. Otherwise, value is equal to false. +template +struct is_scoped_enum : detail::is_scoped_enum {}; + +template +inline constexpr bool is_scoped_enum_v = is_scoped_enum::value; + +// If T is a complete enumeration type, provides a member typedef type that names the underlying type of T. +// Otherwise, if T is not an enumeration type, there is no member type. Otherwise (T is an incomplete enumeration type), the program is ill-formed. +template +struct underlying_type : detail::underlying_type {}; + +template +using underlying_type_t = typename underlying_type::type; + +template +using enum_constant = detail::enum_constant; + +// Returns type name of enum. +template +[[nodiscard]] constexpr auto enum_type_name() noexcept -> detail::enable_if_t { + constexpr string_view name = detail::type_name_v>; + static_assert(!name.empty(), "magic_enum::enum_type_name enum type does not have a name."); + + return name; +} + +// Returns number of enum values. +template > +[[nodiscard]] constexpr auto enum_count() noexcept -> detail::enable_if_t { + return detail::count_v, S>; +} + +// Returns enum value at specified index. +// No bounds checking is performed: the behavior is undefined if index >= number of enum values. +template > +[[nodiscard]] constexpr auto enum_value(std::size_t index) noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + if constexpr (detail::is_sparse_v) { + return MAGIC_ENUM_ASSERT(index < detail::count_v), detail::values_v[index]; + } else { + constexpr auto min = (S == detail::enum_subtype::flags) ? detail::log2(detail::min_v) : detail::min_v; + + return MAGIC_ENUM_ASSERT(index < detail::count_v), detail::value(index); + } +} + +// Returns enum value at specified index. +template > +[[nodiscard]] constexpr auto enum_value() noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + static_assert(I < detail::count_v, "magic_enum::enum_value out of range."); + + return enum_value(I); +} + +// Returns std::array with enum values, sorted by enum value. +template > +[[nodiscard]] constexpr auto enum_values() noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return detail::values_v; +} + +// Returns integer value from enum value. +template +[[nodiscard]] constexpr auto enum_integer(E value) noexcept -> detail::enable_if_t> { + return static_cast>(value); +} + +// Returns underlying value from enum value. +template +[[nodiscard]] constexpr auto enum_underlying(E value) noexcept -> detail::enable_if_t> { + return static_cast>(value); +} + +// Obtains index in enum values from enum value. +// Returns optional with index. +template > +[[nodiscard]] constexpr auto enum_index(E value) noexcept -> detail::enable_if_t> { + using D = std::decay_t; + using U = underlying_type_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + if constexpr (detail::is_sparse_v || (S == detail::enum_subtype::flags)) { +#if defined(MAGIC_ENUM_ENABLE_HASH) + return detail::constexpr_switch<&detail::values_v, detail::case_call_t::index>( + [](std::size_t i) { return optional{i}; }, + value, + detail::default_result_type_lambda>); +#else + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (enum_value(i) == value) { + return i; + } + } + return {}; // Invalid value or out of range. +#endif + } else { + const auto v = static_cast(value); + if (v >= detail::min_v && v <= detail::max_v) { + return static_cast(v - detail::min_v); + } + return {}; // Invalid value or out of range. + } +} + +// Obtains index in enum values from enum value. +// Returns optional with index. +template +[[nodiscard]] constexpr auto enum_index(E value) noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return enum_index(value); +} + +// Obtains index in enum values from static storage enum variable. +template >> +[[nodiscard]] constexpr auto enum_index() noexcept -> detail::enable_if_t {\ + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + constexpr auto index = enum_index(V); + static_assert(index, "magic_enum::enum_index enum value does not have a index."); + + return *index; +} + +// Returns name from static storage enum variable. +// This version is much lighter on the compile times and is not restricted to the enum_range limitation. +template +[[nodiscard]] constexpr auto enum_name() noexcept -> detail::enable_if_t { + constexpr string_view name = detail::enum_name_v, V>; + static_assert(!name.empty(), "magic_enum::enum_name enum value does not have a name."); + + return name; +} + +// Returns name from enum value. +// If enum value does not have name or value out of range, returns empty string. +template > +[[nodiscard]] constexpr auto enum_name(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + if (const auto i = enum_index(value)) { + return detail::names_v[*i]; + } + return {}; +} + +// Returns name from enum value. +// If enum value does not have name or value out of range, returns empty string. +template +[[nodiscard]] constexpr auto enum_name(E value) -> detail::enable_if_t { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return enum_name(value); +} + +// Returns std::array with names, sorted by enum value. +template > +[[nodiscard]] constexpr auto enum_names() noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return detail::names_v; +} + +// Returns std::array with pairs (value, name), sorted by enum value. +template > +[[nodiscard]] constexpr auto enum_entries() noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return detail::entries_v; +} + +// Allows you to write magic_enum::enum_cast("bar", magic_enum::case_insensitive); +inline constexpr auto case_insensitive = detail::case_insensitive<>{}; + +// Obtains enum value from integer value. +// Returns optional with enum value. +template > +[[nodiscard]] constexpr auto enum_cast(underlying_type_t value) noexcept -> detail::enable_if_t>> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + if constexpr (detail::is_sparse_v || (S == detail::enum_subtype::flags)) { +#if defined(MAGIC_ENUM_ENABLE_HASH) + return detail::constexpr_switch<&detail::values_v, detail::case_call_t::value>( + [](D v) { return optional{v}; }, + static_cast(value), + detail::default_result_type_lambda>); +#else + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (value == static_cast>(enum_value(i))) { + return static_cast(value); + } + } + return {}; // Invalid value or out of range. +#endif + } else { + if (value >= detail::min_v && value <= detail::max_v) { + return static_cast(value); + } + return {}; // Invalid value or out of range. + } +} + +// Obtains enum value from name. +// Returns optional with enum value. +template , typename BinaryPredicate = std::equal_to<>> +[[nodiscard]] constexpr auto enum_cast(string_view value, [[maybe_unused]] BinaryPredicate p = {}) noexcept(detail::is_nothrow_invocable()) -> detail::enable_if_t>, BinaryPredicate> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + +#if defined(MAGIC_ENUM_ENABLE_HASH) + if constexpr (detail::is_default_predicate()) { + return detail::constexpr_switch<&detail::names_v, detail::case_call_t::index>( + [](std::size_t i) { return optional{detail::values_v[i]}; }, + value, + detail::default_result_type_lambda>, + [&p](string_view lhs, string_view rhs) { return detail::cmp_equal(lhs, rhs, p); }); + } +#endif + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (detail::cmp_equal(value, detail::names_v[i], p)) { + return enum_value(i); + } + } + return {}; // Invalid value or out of range. +} + +// Checks whether enum contains value with such value. +template > +[[nodiscard]] constexpr auto enum_contains(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + using U = underlying_type_t; + + return static_cast(enum_cast(static_cast(value))); +} + +// Checks whether enum contains value with such value. +template +[[nodiscard]] constexpr auto enum_contains(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + using U = underlying_type_t; + + return static_cast(enum_cast(static_cast(value))); +} + +// Checks whether enum contains value with such integer value. +template > +[[nodiscard]] constexpr auto enum_contains(underlying_type_t value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + + return static_cast(enum_cast(value)); +} + +// Checks whether enum contains enumerator with such name. +template , typename BinaryPredicate = std::equal_to<>> +[[nodiscard]] constexpr auto enum_contains(string_view value, BinaryPredicate p = {}) noexcept(detail::is_nothrow_invocable()) -> detail::enable_if_t { + using D = std::decay_t; + + return static_cast(enum_cast(value, std::move(p))); +} + +// Returns true if the enum integer value is in the range of values that can be reflected. +template > +[[nodiscard]] constexpr auto enum_reflected(underlying_type_t value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + + if constexpr (detail::is_reflected_v) { + constexpr auto min = detail::reflected_min(); + constexpr auto max = detail::reflected_max(); + return value >= min && value <= max; + } else { + return false; + } +} + +// Returns true if the enum value is in the range of values that can be reflected. +template > +[[nodiscard]] constexpr auto enum_reflected(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + + return enum_reflected(static_cast>(value)); +} + +// Returns true if the enum value is in the range of values that can be reflected. +template +[[nodiscard]] constexpr auto enum_reflected(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + + return enum_reflected(value); +} + +template +inline constexpr auto as_flags = AsFlags ? detail::enum_subtype::flags : detail::enum_subtype::common; + +template +inline constexpr auto as_common = AsFlags ? detail::enum_subtype::common : detail::enum_subtype::flags; + +namespace bitwise_operators { + +template = 0> +constexpr E operator~(E rhs) noexcept { + return static_cast(~static_cast>(rhs)); +} + +template = 0> +constexpr E operator|(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) | static_cast>(rhs)); +} + +template = 0> +constexpr E operator&(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) & static_cast>(rhs)); +} + +template = 0> +constexpr E operator^(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) ^ static_cast>(rhs)); +} + +template = 0> +constexpr E& operator|=(E& lhs, E rhs) noexcept { + return lhs = (lhs | rhs); +} + +template = 0> +constexpr E& operator&=(E& lhs, E rhs) noexcept { + return lhs = (lhs & rhs); +} + +template = 0> +constexpr E& operator^=(E& lhs, E rhs) noexcept { + return lhs = (lhs ^ rhs); +} + +} // namespace magic_enum::bitwise_operators + +} // namespace magic_enum + +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +#undef MAGIC_ENUM_GET_ENUM_NAME_BUILTIN +#undef MAGIC_ENUM_GET_TYPE_NAME_BUILTIN +#undef MAGIC_ENUM_VS_2017_WORKAROUND +#undef MAGIC_ENUM_ARRAY_CONSTEXPR +#undef MAGIC_ENUM_FOR_EACH_256 + +#endif // NEARGYE_MAGIC_ENUM_HPP diff --git a/src/cpp/3rdparty/nrlmsise/nrlmsise-00.cpp b/src/cpp/3rdparty/nrlmsise/nrlmsise-00.cpp new file mode 100644 index 000000000..5663c71aa --- /dev/null +++ b/src/cpp/3rdparty/nrlmsise/nrlmsise-00.cpp @@ -0,0 +1,1459 @@ +/* -------------------------------------------------------------------- */ +/* --------- N R L M S I S E - 0 0 M O D E L 2 0 0 1 ---------- */ +/* -------------------------------------------------------------------- */ + +/* This file is part of the NRLMSISE-00 C source code package - release + * 20041227 + * + * The NRLMSISE-00 model was developed by Mike Picone, Alan Hedin, and + * Doug Drob. They also wrote a NRLMSISE-00 distribution package in + * FORTRAN which is available at + * http://uap-www.nrl.navy.mil/models_web/msis/msis_home.htm + * + * Dominik Brodowski implemented and maintains this C version. You can + * reach him at mail@brodo.de. See the file "DOCUMENTATION" for details, + * and check http://www.brodo.de/english/pub/nrlmsise/index.html for + * updated releases of this package. + */ + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------ INCLUDES --------------------------- */ +/* ------------------------------------------------------------------- */ + +#include "nrlmsise-00.h" /* header for nrlmsise-00.h */ +#include /* maths functions */ +#include /* for error messages. TBD: remove this */ +#include /* for malloc/free */ + + + +/* ------------------------------------------------------------------- */ +/* ------------------------- SHARED VARIABLES ------------------------ */ +/* ------------------------------------------------------------------- */ + +/* PARMB */ +static double gsurf; +static double re; + +/* GTS3C */ +static double dd; + +/* DMIX */ +static double dm04, dm16, dm28, dm32, dm40, dm01, dm14; + +/* MESO7 */ +static double meso_tn1[5]; +static double meso_tn2[4]; +static double meso_tn3[5]; +static double meso_tgn1[2]; +static double meso_tgn2[2]; +static double meso_tgn3[2]; + +/* POWER7 */ +extern double pt[150]; +extern double pd[9][150]; +extern double ps[150]; +extern double pdl[2][25]; +extern double ptl[4][100]; +extern double pma[10][100]; +extern double sam[100]; + +/* LOWER7 */ +extern double ptm[10]; +extern double pdm[8][10]; +extern double pavgm[10]; + +/* LPOLY */ +static double dfa; +static double plg[4][9]; +static double ctloc, stloc; +static double c2tloc, s2tloc; +static double s3tloc, c3tloc; +static double apdf, apt[4]; + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------ TSELEC ----------------------------- */ +/* ------------------------------------------------------------------- */ + +void tselec(struct nrlmsise_flags *flags) { + int i; + for (i=0;i<24;i++) { + if (i!=9) { + if (flags->switches[i]==1) + flags->sw[i]=1; + else + flags->sw[i]=0; + if (flags->switches[i]>0) + flags->swc[i]=1; + else + flags->swc[i]=0; + } else { + flags->sw[i]=flags->switches[i]; + flags->swc[i]=flags->switches[i]; + } + } +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------ GLATF ------------------------------ */ +/* ------------------------------------------------------------------- */ + +void glatf(double lat, double *gv, double *reff) { + double dgtr = 1.74533E-2; + double c2; + c2 = cos(2.0*dgtr*lat); + *gv = 980.616 * (1.0 - 0.0026373 * c2); + *reff = 2.0 * (*gv) / (3.085462E-6 + 2.27E-9 * c2) * 1.0E-5; +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------ CCOR ------------------------------- */ +/* ------------------------------------------------------------------- */ + +double ccor(double alt, double r, double h1, double zh) { +/* CHEMISTRY/DISSOCIATION CORRECTION FOR MSIS MODELS + * ALT - altitude + * R - target ratio + * H1 - transition scale length + * ZH - altitude of 1/2 R + */ + double e; + double ex; + e = (alt - zh) / h1; + if (e>70) + return exp(0); + if (e<-70) + return exp(r); + ex = exp(e); + e = r / (1.0 + ex); + return exp(e); +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------ CCOR ------------------------------- */ +/* ------------------------------------------------------------------- */ + +double ccor2(double alt, double r, double h1, double zh, double h2) { +/* CHEMISTRY/DISSOCIATION CORRECTION FOR MSIS MODELS + * ALT - altitude + * R - target ratio + * H1 - transition scale length + * ZH - altitude of 1/2 R + * H2 - transition scale length #2 ? + */ + double e1, e2; + double ex1, ex2; + double ccor2v; + e1 = (alt - zh) / h1; + e2 = (alt - zh) / h2; + if ((e1 > 70) || (e2 > 70)) + return exp(0); + if ((e1 < -70) && (e2 < -70)) + return exp(r); + ex1 = exp(e1); + ex2 = exp(e2); + ccor2v = r / (1.0 + 0.5 * (ex1 + ex2)); + return exp(ccor2v); +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- SCALH ----------------------------- */ +/* ------------------------------------------------------------------- */ + +double scalh(double alt, double xm, double temp) { + double g; + double rgas=831.4; + g = gsurf / (pow((1.0 + alt/re),2.0)); + g = rgas * temp / (g * xm); + return g; +} + + + +/* ------------------------------------------------------------------- */ +/* -------------------------------- DNET ----------------------------- */ +/* ------------------------------------------------------------------- */ + +double dnet (double dd, double dm, double zhm, double xmm, double xm) { +/* TURBOPAUSE CORRECTION FOR MSIS MODELS + * Root mean density + * DD - diffusive density + * DM - full mixed density + * ZHM - transition scale length + * XMM - full mixed molecular weight + * XM - species molecular weight + * DNET - combined density + */ + double a; + double ylog; + a = zhm / (xmm-xm); + if (!((dm>0) && (dd>0))) { + printf("dnet log error %e %e %e\n",dm,dd,xm); + if ((dd==0) && (dm==0)) + dd=1; + if (dm==0) + return dd; + if (dd==0) + return dm; + } + ylog = a * log(dm/dd); + if (ylog<-10) + return dd; + if (ylog>10) + return dm; + a = dd*pow((1.0 + exp(ylog)),(1.0/a)); + return a; +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- SPLINI ---------------------------- */ +/* ------------------------------------------------------------------- */ + +void splini (double *xa, double *ya, double *y2a, int n, double x, double *y) { +/* INTEGRATE CUBIC SPLINE FUNCTION FROM XA(1) TO X + * XA,YA: ARRAYS OF TABULATED FUNCTION IN ASCENDING ORDER BY X + * Y2A: ARRAY OF SECOND DERIVATIVES + * N: SIZE OF ARRAYS XA,YA,Y2A + * X: ABSCISSA ENDPOINT FOR INTEGRATION + * Y: OUTPUT VALUE + */ + double yi=0; + int klo=0; + int khi=1; + double xx, h, a, b, a2, b2; + while ((x>xa[klo]) && (khi1) { + k=(khi+klo)/2; + if (xa[k]>x) + khi=k; + else + klo=k; + } + h = xa[khi] - xa[klo]; + if (h==0.0) + printf("bad XA input to splint"); + a = (xa[khi] - x)/h; + b = (x - xa[klo])/h; + yi = a * ya[klo] + b * ya[khi] + ((a*a*a - a) * y2a[klo] + (b*b*b - b) * y2a[khi]) * h * h/6.0; + *y = yi; +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- SPLINE ---------------------------- */ +/* ------------------------------------------------------------------- */ + +void spline (double *x, double *y, int n, double yp1, double ypn, double *y2) { +/* CALCULATE 2ND DERIVATIVES OF CUBIC SPLINE INTERP FUNCTION + * ADAPTED FROM NUMERICAL RECIPES BY PRESS ET AL + * X,Y: ARRAYS OF TABULATED FUNCTION IN ASCENDING ORDER BY X + * N: SIZE OF ARRAYS X,Y + * YP1,YPN: SPECIFIED DERIVATIVES AT X[0] AND X[N-1]; VALUES + * >= 1E30 SIGNAL SIGNAL SECOND DERIVATIVE ZERO + * Y2: OUTPUT ARRAY OF SECOND DERIVATIVES + */ + double *u; + double sig, p, qn, un; + int i, k; + u=(double*)malloc(sizeof(double)*(unsigned int)n); + if (u==NULL) { + printf("Out Of Memory in spline - ERROR"); + return; + } + if (yp1>0.99E30) { + y2[0]=0; + u[0]=0; + } else { + y2[0]=-0.5; + u[0]=(3.0/(x[1]-x[0]))*((y[1]-y[0])/(x[1]-x[0])-yp1); + } + for (i=1;i<(n-1);i++) { + sig = (x[i]-x[i-1])/(x[i+1] - x[i-1]); + p = sig * y2[i-1] + 2.0; + y2[i] = (sig - 1.0) / p; + u[i] = (6.0 * ((y[i+1] - y[i])/(x[i+1] - x[i]) -(y[i] - y[i-1]) / (x[i] - x[i-1]))/(x[i+1] - x[i-1]) - sig * u[i-1])/p; + } + if (ypn>0.99E30) { + qn = 0; + un = 0; + } else { + qn = 0.5; + un = (3.0 / (x[n-1] - x[n-2])) * (ypn - (y[n-1] - y[n-2])/(x[n-1] - x[n-2])); + } + y2[n-1] = (un - qn * u[n-2]) / (qn * y2[n-2] + 1.0); + for (k=n-2;k>=0;k--) + y2[k] = y2[k] * y2[k+1] + u[k]; + + free(u); +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- DENSM ----------------------------- */ +/* ------------------------------------------------------------------- */ + +__inline_double zeta(double zz, double zl) { + return ((zz-zl)*(re+zl)/(re+zz)); +} + +double densm (double alt, double d0, double xm, double *tz, int mn3, double *zn3, double *tn3, double *tgn3, int mn2, double *zn2, double *tn2, double *tgn2) { +/* Calculate Temperature and Density Profiles for lower atmos. */ + double xs[10], ys[10], y2out[10]; + double rgas = 831.4; + double z, z1, z2, t1, t2, zg, zgdif; + double yd1, yd2; + double x, y, yi; + double expl, gamm, glb; + double densm_tmp; + int mn; + int k; + densm_tmp=d0; + if (alt>zn2[0]) { + if (xm==0.0) + return *tz; + else + return d0; + } + + /* STRATOSPHERE/MESOSPHERE TEMPERATURE */ + if (alt>zn2[mn2-1]) + z=alt; + else + z=zn2[mn2-1]; + mn=mn2; + z1=zn2[0]; + z2=zn2[mn-1]; + t1=tn2[0]; + t2=tn2[mn-1]; + zg = zeta(z, z1); + zgdif = zeta(z2, z1); + + /* set up spline nodes */ + for (k=0;k50.0) + expl=50.0; + + /* Density at altitude */ + densm_tmp = densm_tmp * (t1 / *tz) * exp(-expl); + } + + if (alt>zn3[0]) { + if (xm==0.0) + return *tz; + else + return densm_tmp; + } + + /* troposhere / stratosphere temperature */ + z = alt; + mn = mn3; + z1=zn3[0]; + z2=zn3[mn-1]; + t1=tn3[0]; + t2=tn3[mn-1]; + zg=zeta(z,z1); + zgdif=zeta(z2,z1); + + /* set up spline nodes */ + for (k=0;k50.0) + expl=50.0; + + /* Density at altitude */ + densm_tmp = densm_tmp * (t1 / *tz) * exp(-expl); + } + if (xm==0.0) + return *tz; + else + return densm_tmp; +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- DENSU ----------------------------- */ +/* ------------------------------------------------------------------- */ + +double densu (double alt, double dlb, double tinf, double tlb, double xm, double alpha, double *tz, double zlb, double s2, int mn1, double *zn1, double *tn1, double *tgn1) { +/* Calculate Temperature and Density Profiles for MSIS models + * New lower thermo polynomial + */ + double yd2, yd1, x=0, y; + double rgas=831.4; + double densu_temp=1.0; + double za, z, zg2, tt, ta; + double dta, z1=0, z2, t1=0, t2, zg, zgdif=0; + int mn=0; + int k; + double glb; + double expl; + double yi; + double densa; + double gamma, gamm; + double xs[5], ys[5], y2out[5]; + /* joining altitudes of Bates and spline */ + za=zn1[0]; + if (alt>za) + z=alt; + else + z=za; + + /* geopotential altitude difference from ZLB */ + zg2 = zeta(z, zlb); + + /* Bates temperature */ + tt = tinf - (tinf - tlb) * exp(-s2*zg2); + ta = tt; + *tz = tt; + densu_temp = *tz; + + if (altzn1[mn1-1]) + z=alt; + else + z=zn1[mn1-1]; + mn=mn1; + z1=zn1[0]; + z2=zn1[mn-1]; + t1=tn1[0]; + t2=tn1[mn-1]; + /* geopotental difference from z1 */ + zg = zeta (z, z1); + zgdif = zeta(z2, z1); + /* set up spline nodes */ + for (k=0;k50.0) + expl=50.0; + if (tt<=0) + expl=50.0; + + /* density at altitude */ + densa = dlb * pow((tlb/tt),((1.0+alpha+gamma))) * expl; + densu_temp=densa; + if (alt>=za) + return densu_temp; + + /* calculate density below za */ + glb = gsurf / pow((1.0 + z1/re),2.0); + gamm = xm * glb * zgdif / rgas; + + /* integrate spline temperatures */ + splini (xs, ys, y2out, mn, x, &yi); + expl = gamm * yi; + if (expl>50.0) + expl=50.0; + if (*tz<=0) + expl=50.0; + + /* density at altitude */ + densu_temp = densu_temp * pow ((t1 / *tz),(1.0 + alpha)) * exp(-expl); + return densu_temp; +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- GLOBE7 ---------------------------- */ +/* ------------------------------------------------------------------- */ + +/* 3hr Magnetic activity functions */ +/* Eq. A24d */ +__inline_double g0(double a, double *p) { + return (a - 4.0 + (p[25] - 1.0) * (a - 4.0 + (exp(-sqrt(p[24]*p[24]) * (a - 4.0)) - 1.0) / sqrt(p[24]*p[24]))); +} + +/* Eq. A24c */ +__inline_double sumex(double ex) { + return (1.0 + (1.0 - pow(ex,19.0)) / (1.0 - ex) * pow(ex,0.5)); +} + +/* Eq. A24a */ +__inline_double sg0(double ex, double *p, double *ap) { + return (g0(ap[1],p) + (g0(ap[2],p)*ex + g0(ap[3],p)*ex*ex + \ + g0(ap[4],p)*pow(ex,3.0) + (g0(ap[5],p)*pow(ex,4.0) + \ + g0(ap[6],p)*pow(ex,12.0))*(1.0-pow(ex,8.0))/(1.0-ex)))/sumex(ex); +} + +double globe7(double *p, struct nrlmsise_input *input, struct nrlmsise_flags *flags) { +/* CALCULATE G(L) FUNCTION + * Upper Thermosphere Parameters */ + double t[15]; + int i,j; + double apd; + double tloc; + double c, s, c2, c4, s2; + double sr = 7.2722E-5; + double dgtr = 1.74533E-2; + double dr = 1.72142E-2; + double hr = 0.2618; + double cd32, cd18, cd14, cd39; + double df; + double f1, f2; + double tinf; + struct ap_array *ap; + + tloc=input->lst; + for (j=0;j<14;j++) + t[j]=0; + + /* calculate legendre polynomials */ + c = sin(input->g_lat * dgtr); + s = cos(input->g_lat * dgtr); + c2 = c*c; + c4 = c2*c2; + s2 = s*s; + + plg[0][1] = c; + plg[0][2] = 0.5*(3.0*c2 -1.0); + plg[0][3] = 0.5*(5.0*c*c2-3.0*c); + plg[0][4] = (35.0*c4 - 30.0*c2 + 3.0)/8.0; + plg[0][5] = (63.0*c2*c2*c - 70.0*c2*c + 15.0*c)/8.0; + plg[0][6] = (11.0*c*plg[0][5] - 5.0*plg[0][4])/6.0; +/* plg[0][7] = (13.0*c*plg[0][6] - 6.0*plg[0][5])/7.0; */ + plg[1][1] = s; + plg[1][2] = 3.0*c*s; + plg[1][3] = 1.5*(5.0*c2-1.0)*s; + plg[1][4] = 2.5*(7.0*c2*c-3.0*c)*s; + plg[1][5] = 1.875*(21.0*c4 - 14.0*c2 +1.0)*s; + plg[1][6] = (11.0*c*plg[1][5]-6.0*plg[1][4])/5.0; +/* plg[1][7] = (13.0*c*plg[1][6]-7.0*plg[1][5])/6.0; */ +/* plg[1][8] = (15.0*c*plg[1][7]-8.0*plg[1][6])/7.0; */ + plg[2][2] = 3.0*s2; + plg[2][3] = 15.0*s2*c; + plg[2][4] = 7.5*(7.0*c2 -1.0)*s2; + plg[2][5] = 3.0*c*plg[2][4]-2.0*plg[2][3]; + plg[2][6] =(11.0*c*plg[2][5]-7.0*plg[2][4])/4.0; + plg[2][7] =(13.0*c*plg[2][6]-8.0*plg[2][5])/5.0; + plg[3][3] = 15.0*s2*s; + plg[3][4] = 105.0*s2*s*c; + plg[3][5] =(9.0*c*plg[3][4]-7.*plg[3][3])/2.0; + plg[3][6] =(11.0*c*plg[3][5]-8.*plg[3][4])/3.0; + + if (!(((flags->sw[7]==0)&&(flags->sw[8]==0))&&(flags->sw[14]==0))) { + stloc = sin(hr*tloc); + ctloc = cos(hr*tloc); + s2tloc = sin(2.0*hr*tloc); + c2tloc = cos(2.0*hr*tloc); + s3tloc = sin(3.0*hr*tloc); + c3tloc = cos(3.0*hr*tloc); + } + + cd32 = cos(dr*(input->doy-p[31])); + cd18 = cos(2.0*dr*(input->doy-p[17])); + cd14 = cos(dr*(input->doy-p[13])); + cd39 = cos(2.0*dr*(input->doy-p[38])); + + /* F10.7 EFFECT */ + df = input->f107 - input->f107A; + dfa = input->f107A - 150.0; + t[0] = p[19]*df*(1.0+p[59]*dfa) + p[20]*df*df + p[21]*dfa + p[29]*pow(dfa,2.0); + f1 = 1.0 + (p[47]*dfa +p[19]*df+p[20]*df*df)*flags->swc[1]; + f2 = 1.0 + (p[49]*dfa+p[19]*df+p[20]*df*df)*flags->swc[1]; + + /* TIME INDEPENDENT */ + t[1] = (p[1]*plg[0][2]+ p[2]*plg[0][4]+p[22]*plg[0][6]) + \ + (p[14]*plg[0][2])*dfa*flags->swc[1] +p[26]*plg[0][1]; + + /* SYMMETRICAL ANNUAL */ + t[2] = p[18]*cd32; + + /* SYMMETRICAL SEMIANNUAL */ + t[3] = (p[15]+p[16]*plg[0][2])*cd18; + + /* ASYMMETRICAL ANNUAL */ + t[4] = f1*(p[9]*plg[0][1]+p[10]*plg[0][3])*cd14; + + /* ASYMMETRICAL SEMIANNUAL */ + t[5] = p[37]*plg[0][1]*cd39; + + /* DIURNAL */ + if (flags->sw[7]) { + double t71, t72; + t71 = (p[11]*plg[1][2])*cd14*flags->swc[5]; + t72 = (p[12]*plg[1][2])*cd14*flags->swc[5]; + t[6] = f2*((p[3]*plg[1][1] + p[4]*plg[1][3] + p[27]*plg[1][5] + t71) * \ + ctloc + (p[6]*plg[1][1] + p[7]*plg[1][3] + p[28]*plg[1][5] \ + + t72)*stloc); +} + + /* SEMIDIURNAL */ + if (flags->sw[8]) { + double t81, t82; + t81 = (p[23]*plg[2][3]+p[35]*plg[2][5])*cd14*flags->swc[5]; + t82 = (p[33]*plg[2][3]+p[36]*plg[2][5])*cd14*flags->swc[5]; + t[7] = f2*((p[5]*plg[2][2]+ p[41]*plg[2][4] + t81)*c2tloc +(p[8]*plg[2][2] + p[42]*plg[2][4] + t82)*s2tloc); + } + + /* TERDIURNAL */ + if (flags->sw[14]) { + t[13] = f2 * ((p[39]*plg[3][3]+(p[93]*plg[3][4]+p[46]*plg[3][6])*cd14*flags->swc[5])* s3tloc +(p[40]*plg[3][3]+(p[94]*plg[3][4]+p[48]*plg[3][6])*cd14*flags->swc[5])* c3tloc); +} + + /* magnetic activity based on daily ap */ + if (flags->sw[9]==-1) { + ap = input->ap_a; + if (p[51]!=0) { + double exp1; + exp1 = exp(-10800.0*sqrt(p[51]*p[51])/(1.0+p[138]*(45.0-sqrt(input->g_lat*input->g_lat)))); + if (exp1>0.99999) + exp1=0.99999; + if (p[24]<1.0E-4) + p[24]=1.0E-4; + apt[0]=sg0(exp1,p,ap->a); + /* apt[1]=sg2(exp1,p,ap->a); + apt[2]=sg0(exp2,p,ap->a); + apt[3]=sg2(exp2,p,ap->a); + */ + if (flags->sw[9]) { + t[8] = apt[0]*(p[50]+p[96]*plg[0][2]+p[54]*plg[0][4]+ \ + (p[125]*plg[0][1]+p[126]*plg[0][3]+p[127]*plg[0][5])*cd14*flags->swc[5]+ \ + (p[128]*plg[1][1]+p[129]*plg[1][3]+p[130]*plg[1][5])*flags->swc[7]* \ + cos(hr*(tloc-p[131]))); + } + } + } else { + double p44, p45; + apd=input->ap-4.0; + p44=p[43]; + p45=p[44]; + if (p44<0) + p44 = 1.0E-5; + apdf = apd + (p45-1.0)*(apd + (exp(-p44 * apd) - 1.0)/p44); + if (flags->sw[9]) { + t[8]=apdf*(p[32]+p[45]*plg[0][2]+p[34]*plg[0][4]+ \ + (p[100]*plg[0][1]+p[101]*plg[0][3]+p[102]*plg[0][5])*cd14*flags->swc[5]+ + (p[121]*plg[1][1]+p[122]*plg[1][3]+p[123]*plg[1][5])*flags->swc[7]* + cos(hr*(tloc-p[124]))); + } + } + + if ((flags->sw[10])&&(input->g_long>-1000.0)) { + + /* longitudinal */ + if (flags->sw[11]) { + t[10] = (1.0 + p[80]*dfa*flags->swc[1])* \ + ((p[64]*plg[1][2]+p[65]*plg[1][4]+p[66]*plg[1][6]\ + +p[103]*plg[1][1]+p[104]*plg[1][3]+p[105]*plg[1][5]\ + +flags->swc[5]*(p[109]*plg[1][1]+p[110]*plg[1][3]+p[111]*plg[1][5])*cd14)* \ + cos(dgtr*input->g_long) \ + +(p[90]*plg[1][2]+p[91]*plg[1][4]+p[92]*plg[1][6]\ + +p[106]*plg[1][1]+p[107]*plg[1][3]+p[108]*plg[1][5]\ + +flags->swc[5]*(p[112]*plg[1][1]+p[113]*plg[1][3]+p[114]*plg[1][5])*cd14)* \ + sin(dgtr*input->g_long)); + } + + /* ut and mixed ut, longitude */ + if (flags->sw[12]){ + t[11]=(1.0+p[95]*plg[0][1])*(1.0+p[81]*dfa*flags->swc[1])*\ + (1.0+p[119]*plg[0][1]*flags->swc[5]*cd14)*\ + ((p[68]*plg[0][1]+p[69]*plg[0][3]+p[70]*plg[0][5])*\ + cos(sr*(input->sec-p[71]))); + t[11]+=flags->swc[11]*\ + (p[76]*plg[2][3]+p[77]*plg[2][5]+p[78]*plg[2][7])*\ + cos(sr*(input->sec-p[79])+2.0*dgtr*input->g_long)*(1.0+p[137]*dfa*flags->swc[1]); + } + + /* ut, longitude magnetic activity */ + if (flags->sw[13]) { + if (flags->sw[9]==-1) { + if (p[51]) { + t[12]=apt[0]*flags->swc[11]*(1.+p[132]*plg[0][1])*\ + ((p[52]*plg[1][2]+p[98]*plg[1][4]+p[67]*plg[1][6])*\ + cos(dgtr*(input->g_long-p[97])))\ + +apt[0]*flags->swc[11]*flags->swc[5]*\ + (p[133]*plg[1][1]+p[134]*plg[1][3]+p[135]*plg[1][5])*\ + cd14*cos(dgtr*(input->g_long-p[136])) \ + +apt[0]*flags->swc[12]* \ + (p[55]*plg[0][1]+p[56]*plg[0][3]+p[57]*plg[0][5])*\ + cos(sr*(input->sec-p[58])); + } + } else { + t[12] = apdf*flags->swc[11]*(1.0+p[120]*plg[0][1])*\ + ((p[60]*plg[1][2]+p[61]*plg[1][4]+p[62]*plg[1][6])*\ + cos(dgtr*(input->g_long-p[63])))\ + +apdf*flags->swc[11]*flags->swc[5]* \ + (p[115]*plg[1][1]+p[116]*plg[1][3]+p[117]*plg[1][5])* \ + cd14*cos(dgtr*(input->g_long-p[118])) \ + + apdf*flags->swc[12]* \ + (p[83]*plg[0][1]+p[84]*plg[0][3]+p[85]*plg[0][5])* \ + cos(sr*(input->sec-p[75])); + } + } + } + + /* parms not used: 82, 89, 99, 139-149 */ + tinf = p[30]; + for (i=0;i<14;i++) + tinf = tinf + fabs(flags->sw[i+1])*t[i]; + return tinf; +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- GLOB7S ---------------------------- */ +/* ------------------------------------------------------------------- */ + +double glob7s(double *p, struct nrlmsise_input *input, struct nrlmsise_flags *flags) { +/* VERSION OF GLOBE FOR LOWER ATMOSPHERE 10/26/99 + */ + double pset=2.0; + double t[14]; + double tt; + double cd32, cd18, cd14, cd39; + int i,j; + double dr=1.72142E-2; + double dgtr=1.74533E-2; + /* confirm parameter set */ + if (p[99]==0) + p[99]=pset; + if (p[99]!=pset) { + printf("Wrong parameter set for glob7s\n"); + return -1; + } + for (j=0;j<14;j++) + t[j]=0.0; + cd32 = cos(dr*(input->doy-p[31])); + cd18 = cos(2.0*dr*(input->doy-p[17])); + cd14 = cos(dr*(input->doy-p[13])); + cd39 = cos(2.0*dr*(input->doy-p[38])); + + /* F10.7 */ + t[0] = p[21]*dfa; + + /* time independent */ + t[1]=p[1]*plg[0][2] + p[2]*plg[0][4] + p[22]*plg[0][6] + p[26]*plg[0][1] + p[14]*plg[0][3] + p[59]*plg[0][5]; + + /* SYMMETRICAL ANNUAL */ + t[2]=(p[18]+p[47]*plg[0][2]+p[29]*plg[0][4])*cd32; + + /* SYMMETRICAL SEMIANNUAL */ + t[3]=(p[15]+p[16]*plg[0][2]+p[30]*plg[0][4])*cd18; + + /* ASYMMETRICAL ANNUAL */ + t[4]=(p[9]*plg[0][1]+p[10]*plg[0][3]+p[20]*plg[0][5])*cd14; + + /* ASYMMETRICAL SEMIANNUAL */ + t[5]=(p[37]*plg[0][1])*cd39; + + /* DIURNAL */ + if (flags->sw[7]) { + double t71, t72; + t71 = p[11]*plg[1][2]*cd14*flags->swc[5]; + t72 = p[12]*plg[1][2]*cd14*flags->swc[5]; + t[6] = ((p[3]*plg[1][1] + p[4]*plg[1][3] + t71) * ctloc + (p[6]*plg[1][1] + p[7]*plg[1][3] + t72) * stloc) ; + } + + /* SEMIDIURNAL */ + if (flags->sw[8]) { + double t81, t82; + t81 = (p[23]*plg[2][3]+p[35]*plg[2][5])*cd14*flags->swc[5]; + t82 = (p[33]*plg[2][3]+p[36]*plg[2][5])*cd14*flags->swc[5]; + t[7] = ((p[5]*plg[2][2] + p[41]*plg[2][4] + t81) * c2tloc + (p[8]*plg[2][2] + p[42]*plg[2][4] + t82) * s2tloc); + } + + /* TERDIURNAL */ + if (flags->sw[14]) { + t[13] = p[39] * plg[3][3] * s3tloc + p[40] * plg[3][3] * c3tloc; + } + + /* MAGNETIC ACTIVITY */ + if (flags->sw[9]) { + if (flags->sw[9]==1) + t[8] = apdf * (p[32] + p[45] * plg[0][2] * flags->swc[2]); + if (flags->sw[9]==-1) + t[8]=(p[50]*apt[0] + p[96]*plg[0][2] * apt[0]*flags->swc[2]); + } + + /* LONGITUDINAL */ + if (!((flags->sw[10]==0) || (flags->sw[11]==0) || (input->g_long<=-1000.0))) { + t[10] = (1.0 + plg[0][1]*(p[80]*flags->swc[5]*cos(dr*(input->doy-p[81]))\ + +p[85]*flags->swc[6]*cos(2.0*dr*(input->doy-p[86])))\ + +p[83]*flags->swc[3]*cos(dr*(input->doy-p[84]))\ + +p[87]*flags->swc[4]*cos(2.0*dr*(input->doy-p[88])))\ + *((p[64]*plg[1][2]+p[65]*plg[1][4]+p[66]*plg[1][6]\ + +p[74]*plg[1][1]+p[75]*plg[1][3]+p[76]*plg[1][5]\ + )*cos(dgtr*input->g_long)\ + +(p[90]*plg[1][2]+p[91]*plg[1][4]+p[92]*plg[1][6]\ + +p[77]*plg[1][1]+p[78]*plg[1][3]+p[79]*plg[1][5]\ + )*sin(dgtr*input->g_long)); + } + tt=0; + for (i=0;i<14;i++) + tt+=fabs(flags->sw[i+1])*t[i]; + return tt; +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- GTD7 ------------------------------ */ +/* ------------------------------------------------------------------- */ + +void gtd7(struct nrlmsise_input *input, struct nrlmsise_flags *flags, struct nrlmsise_output *output) { + double xlat; + double xmm; + int mn3 = 5; + double zn3[5]={32.5,20.0,15.0,10.0,0.0}; + int mn2 = 4; + double zn2[4]={72.5,55.0,45.0,32.5}; + double altt; + double zmix=62.5; + double tmp; + double dm28m; + double tz; + double dmc; + double dmr; + double dz28; + struct nrlmsise_output soutput; + int i; + + tselec(flags); + + /* Latitude variation of gravity (none for sw[2]=0) */ + xlat=input->g_lat; + if (flags->sw[2]==0) + xlat=45.0; + glatf(xlat, &gsurf, &re); + + xmm = pdm[2][4]; + + /* THERMOSPHERE / MESOSPHERE (above zn2[0]) */ + if (input->alt>zn2[0]) + altt=input->alt; + else + altt=zn2[0]; + + tmp=input->alt; + input->alt=altt; + gts7(input, flags, &soutput); + altt=input->alt; + input->alt=tmp; + if (flags->sw[0]) /* metric adjustment */ + dm28m=dm28*1.0E6; + else + dm28m=dm28; + output->t[0]=soutput.t[0]; + output->t[1]=soutput.t[1]; + if (input->alt>=zn2[0]) { + for (i=0;i<9;i++) + output->d[i]=soutput.d[i]; + return; + } + +/* LOWER MESOSPHERE/UPPER STRATOSPHERE (between zn3[0] and zn2[0]) + * Temperature at nodes and gradients at end nodes + * Inverse temperature a linear function of spherical harmonics + */ + meso_tgn2[0]=meso_tgn1[1]; + meso_tn2[0]=meso_tn1[4]; + meso_tn2[1]=pma[0][0]*pavgm[0]/(1.0-flags->sw[20]*glob7s(pma[0], input, flags)); + meso_tn2[2]=pma[1][0]*pavgm[1]/(1.0-flags->sw[20]*glob7s(pma[1], input, flags)); + meso_tn2[3]=pma[2][0]*pavgm[2]/(1.0-flags->sw[20]*flags->sw[22]*glob7s(pma[2], input, flags)); + meso_tgn2[1]=pavgm[8]*pma[9][0]*(1.0+flags->sw[20]*flags->sw[22]*glob7s(pma[9], input, flags))*meso_tn2[3]*meso_tn2[3]/(pow((pma[2][0]*pavgm[2]),2.0)); + meso_tn3[0]=meso_tn2[3]; + + if (input->alt<=zn3[0]) { +/* LOWER STRATOSPHERE AND TROPOSPHERE (below zn3[0]) + * Temperature at nodes and gradients at end nodes + * Inverse temperature a linear function of spherical harmonics + */ + meso_tgn3[0]=meso_tgn2[1]; + meso_tn3[1]=pma[3][0]*pavgm[3]/(1.0-flags->sw[22]*glob7s(pma[3], input, flags)); + meso_tn3[2]=pma[4][0]*pavgm[4]/(1.0-flags->sw[22]*glob7s(pma[4], input, flags)); + meso_tn3[3]=pma[5][0]*pavgm[5]/(1.0-flags->sw[22]*glob7s(pma[5], input, flags)); + meso_tn3[4]=pma[6][0]*pavgm[6]/(1.0-flags->sw[22]*glob7s(pma[6], input, flags)); + meso_tgn3[1]=pma[7][0]*pavgm[7]*(1.0+flags->sw[22]*glob7s(pma[7], input, flags)) *meso_tn3[4]*meso_tn3[4]/(pow((pma[6][0]*pavgm[6]),2.0)); + } + + /* LINEAR TRANSITION TO FULL MIXING BELOW zn2[0] */ + + dmc=0; + if (input->alt>zmix) + dmc = 1.0 - (zn2[0]-input->alt)/(zn2[0] - zmix); + dz28=soutput.d[2]; + + /**** N2 density ****/ + dmr=soutput.d[2] / dm28m - 1.0; + output->d[2]=densm(input->alt,dm28m,xmm, &tz, mn3, zn3, meso_tn3, meso_tgn3, mn2, zn2, meso_tn2, meso_tgn2); + output->d[2]=output->d[2] * (1.0 + dmr*dmc); + + /**** HE density ****/ + dmr = soutput.d[0] / (dz28 * pdm[0][1]) - 1.0; + output->d[0] = output->d[2] * pdm[0][1] * (1.0 + dmr*dmc); + + /**** O density ****/ + output->d[1] = 0; + output->d[8] = 0; + + /**** O2 density ****/ + dmr = soutput.d[3] / (dz28 * pdm[3][1]) - 1.0; + output->d[3] = output->d[2] * pdm[3][1] * (1.0 + dmr*dmc); + + /**** AR density ***/ + dmr = soutput.d[4] / (dz28 * pdm[4][1]) - 1.0; + output->d[4] = output->d[2] * pdm[4][1] * (1.0 + dmr*dmc); + + /**** Hydrogen density ****/ + output->d[6] = 0; + + /**** Atomic nitrogen density ****/ + output->d[7] = 0; + + /**** Total mass density */ + output->d[5] = 1.66E-24 * (4.0 * output->d[0] + 16.0 * output->d[1] + 28.0 * output->d[2] + 32.0 * output->d[3] + 40.0 * output->d[4] + output->d[6] + 14.0 * output->d[7]); + + if (flags->sw[0]) + output->d[5]=output->d[5]/1000; + + /**** temperature at altitude ****/ + dd = densm(input->alt, 1.0, 0, &tz, mn3, zn3, meso_tn3, meso_tgn3, mn2, zn2, meso_tn2, meso_tgn2); + output->t[1]=tz; + +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- GTD7D ----------------------------- */ +/* ------------------------------------------------------------------- */ + +void gtd7d(struct nrlmsise_input *input, struct nrlmsise_flags *flags, struct nrlmsise_output *output) { + gtd7(input, flags, output); + output->d[5] = 1.66E-24 * (4.0 * output->d[0] + 16.0 * output->d[1] + 28.0 * output->d[2] + 32.0 * output->d[3] + 40.0 * output->d[4] + output->d[6] + 14.0 * output->d[7] + 16.0 * output->d[8]); + if (flags->sw[0]) + output->d[5]=output->d[5]/1000; +} + + + +/* ------------------------------------------------------------------- */ +/* -------------------------------- GHP7 ----------------------------- */ +/* ------------------------------------------------------------------- */ + +void ghp7(struct nrlmsise_input *input, struct nrlmsise_flags *flags, struct nrlmsise_output *output, double press) { + double bm = 1.3806E-19; + double rgas = 831.4; + double test = 0.00043; + double ltest = 12; + double pl, p; + double zi; + double z; + double cl, cl2; + double ca, cd; + double xn, xm, diff; + double g, sh; + int l; + pl = log10(press); + if (pl >= -5.0) { + if (pl>2.5) + zi = 18.06 * (3.00 - pl); + else if ((pl>0.075) && (pl<=2.5)) + zi = 14.98 * (3.08 - pl); + else if ((pl>-1) && (pl<=0.075)) + zi = 17.80 * (2.72 - pl); + else if ((pl>-2) && (pl<=-1)) + zi = 14.28 * (3.64 - pl); + else if ((pl>-4) && (pl<=-2)) + zi = 12.72 * (4.32 -pl); + else + zi = 25.3 * (0.11 - pl); + cl = input->g_lat/90.0; + cl2 = cl*cl; + if (input->doy<182) + cd = (1.0 - (double) input->doy) / 91.25; + else + cd = ((double) input->doy) / 91.25 - 3.0; + ca = 0; + if ((pl > -1.11) && (pl<=-0.23)) + ca = 1.0; + if (pl > -0.23) + ca = (2.79 - pl) / (2.79 + 0.23); + if ((pl <= -1.11) && (pl>-3)) + ca = (-2.93 - pl)/(-2.93 + 1.11); + z = zi - 4.87 * cl * cd * ca - 1.64 * cl2 * ca + 0.31 * ca * cl; + } else + z = 22.0 * pow((pl + 4.0),2.0) + 110.0; + + /* iteration loop */ + l = 0; + do { + l++; + input->alt = z; + gtd7(input, flags, output); + z = input->alt; + xn = output->d[0] + output->d[1] + output->d[2] + output->d[3] + output->d[4] + output->d[6] + output->d[7]; + p = bm * xn * output->t[1]; + if (flags->sw[0]) + p = p*1.0E-6; + diff = pl - log10(p); + if (sqrt(diff*diff)d[5] / xn / 1.66E-24; + if (flags->sw[0]) + xm = xm * 1.0E3; + g = gsurf / (pow((1.0 + z/re),2.0)); + sh = rgas * output->t[1] / (xm * g); + + /* new altitude estimate using scale height */ + if (l < 6) + z = z - sh * diff * 2.302; + else + z = z - sh * diff; + } while (1==1); +} + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- GTS7 ------------------------------ */ +/* ------------------------------------------------------------------- */ + +void gts7(struct nrlmsise_input *input, struct nrlmsise_flags *flags, struct nrlmsise_output *output) { +/* Thermospheric portion of NRLMSISE-00 + * See GTD7 for more extensive comments + * alt > 72.5 km! + */ + double za; + int i, j; + double ddum, z; + double zn1[5] = {120.0, 110.0, 100.0, 90.0, 72.5}; + double tinf; + int mn1 = 5; + double g0; + double tlb; + double s; + double db01, db04, db14, db16, db28, db32, db40; + double zh28, zh04, zh16, zh32, zh40, zh01, zh14; + double zhm28, zhm04, zhm16, zhm32, zhm40, zhm01, zhm14; + double xmd; + double b28, b04, b16, b32, b40, b01, b14; + double tz; + double g28, g4, g16, g32, g40, g1, g14; + double zhf, xmm; + double zc04, zc16, zc32, zc40, zc01, zc14; + double hc04, hc16, hc32, hc40, hc01, hc14; + double hcc16, hcc32, hcc01, hcc14; + double zcc16, zcc32, zcc01, zcc14; + double rc16, rc32, rc01, rc14; + double rl; + double g16h, db16h, tho, zsht, zmho, zsho; + double dgtr=1.74533E-2; + double dr=1.72142E-2; + double alpha[9]={-0.38, 0.0, 0.0, 0.0, 0.17, 0.0, -0.38, 0.0, 0.0}; + double altl[8]={200.0, 300.0, 160.0, 250.0, 240.0, 450.0, 320.0, 450.0}; + double dd; + double hc216, hcc232; + za = pdl[1][15]; + zn1[0] = za; + for (j=0;j<9;j++) + output->d[j]=0; + + /* TINF VARIATIONS NOT IMPORTANT BELOW ZA OR ZN1(1) */ + if (input->alt>zn1[0]) + tinf = ptm[0]*pt[0] * \ + (1.0+flags->sw[16]*globe7(pt,input,flags)); + else + tinf = ptm[0]*pt[0]; + output->t[0]=tinf; + + /* GRADIENT VARIATIONS NOT IMPORTANT BELOW ZN1(5) */ + if (input->alt>zn1[4]) + g0 = ptm[3]*ps[0] * \ + (1.0+flags->sw[19]*globe7(ps,input,flags)); + else + g0 = ptm[3]*ps[0]; + tlb = ptm[1] * (1.0 + flags->sw[17]*globe7(pd[3],input,flags))*pd[3][0]; + s = g0 / (tinf - tlb); + +/* Lower thermosphere temp variations not significant for + * density above 300 km */ + if (input->alt<300.0) { + meso_tn1[1]=ptm[6]*ptl[0][0]/(1.0-flags->sw[18]*glob7s(ptl[0], input, flags)); + meso_tn1[2]=ptm[2]*ptl[1][0]/(1.0-flags->sw[18]*glob7s(ptl[1], input, flags)); + meso_tn1[3]=ptm[7]*ptl[2][0]/(1.0-flags->sw[18]*glob7s(ptl[2], input, flags)); + meso_tn1[4]=ptm[4]*ptl[3][0]/(1.0-flags->sw[18]*flags->sw[20]*glob7s(ptl[3], input, flags)); + meso_tgn1[1]=ptm[8]*pma[8][0]*(1.0+flags->sw[18]*flags->sw[20]*glob7s(pma[8], input, flags))*meso_tn1[4]*meso_tn1[4]/(pow((ptm[4]*ptl[3][0]),2.0)); + } else { + meso_tn1[1]=ptm[6]*ptl[0][0]; + meso_tn1[2]=ptm[2]*ptl[1][0]; + meso_tn1[3]=ptm[7]*ptl[2][0]; + meso_tn1[4]=ptm[4]*ptl[3][0]; + meso_tgn1[1]=ptm[8]*pma[8][0]*meso_tn1[4]*meso_tn1[4]/(pow((ptm[4]*ptl[3][0]),2.0)); + } + + /* N2 variation factor at Zlb */ + g28=flags->sw[21]*globe7(pd[2], input, flags); + + /* VARIATION OF TURBOPAUSE HEIGHT */ + zhf=pdl[1][24]*(1.0+flags->sw[5]*pdl[0][24]*sin(dgtr*input->g_lat)*cos(dr*(input->doy-pt[13]))); + output->t[0]=tinf; + xmm = pdm[2][4]; + z = input->alt; + + + /**** N2 DENSITY ****/ + + /* Diffusive density at Zlb */ + db28 = pdm[2][0]*exp(g28)*pd[2][0]; + /* Diffusive density at Alt */ + output->d[2]=densu(z,db28,tinf,tlb,28.0,alpha[2],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + dd=output->d[2]; + /* Turbopause */ + zh28=pdm[2][2]*zhf; + zhm28=pdm[2][3]*pdl[1][5]; + xmd=28.0-xmm; + /* Mixed density at Zlb */ + b28=densu(zh28,db28,tinf,tlb,xmd,(alpha[2]-1.0),&tz,ptm[5],s,mn1, zn1,meso_tn1,meso_tgn1); + if ((flags->sw[15])&&(z<=altl[2])) { + /* Mixed density at Alt */ + dm28=densu(z,b28,tinf,tlb,xmm,alpha[2],&tz,ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + /* Net density at Alt */ + output->d[2]=dnet(output->d[2],dm28,zhm28,xmm,28.0); + } + + + /**** HE DENSITY ****/ + + /* Density variation factor at Zlb */ + g4 = flags->sw[21]*globe7(pd[0], input, flags); + /* Diffusive density at Zlb */ + db04 = pdm[0][0]*exp(g4)*pd[0][0]; + /* Diffusive density at Alt */ + output->d[0]=densu(z,db04,tinf,tlb, 4.,alpha[0],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + dd=output->d[0]; + if ((flags->sw[15]) && (zt[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + /* Mixed density at Alt */ + dm04=densu(z,b04,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + zhm04=zhm28; + /* Net density at Alt */ + output->d[0]=dnet(output->d[0],dm04,zhm04,xmm,4.); + /* Correction to specified mixing ratio at ground */ + rl=log(b28*pdm[0][1]/b04); + zc04=pdm[0][4]*pdl[1][0]; + hc04=pdm[0][5]*pdl[1][1]; + /* Net density corrected at Alt */ + output->d[0]=output->d[0]*ccor(z,rl,hc04,zc04); + } + + + /**** O DENSITY ****/ + + /* Density variation factor at Zlb */ + g16= flags->sw[21]*globe7(pd[1],input,flags); + /* Diffusive density at Zlb */ + db16 = pdm[1][0]*exp(g16)*pd[1][0]; + /* Diffusive density at Alt */ + output->d[1]=densu(z,db16,tinf,tlb, 16.,alpha[1],&output->t[1],ptm[5],s,mn1, zn1,meso_tn1,meso_tgn1); + dd=output->d[1]; + if ((flags->sw[15]) && (z<=altl[1])) { + /* Turbopause */ + zh16=pdm[1][2]; + /* Mixed density at Zlb */ + b16=densu(zh16,db16,tinf,tlb,16.0-xmm,(alpha[1]-1.0), &output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + /* Mixed density at Alt */ + dm16=densu(z,b16,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + zhm16=zhm28; + /* Net density at Alt */ + output->d[1]=dnet(output->d[1],dm16,zhm16,xmm,16.); + rl=pdm[1][1]*pdl[1][16]*(1.0+flags->sw[1]*pdl[0][23]*(input->f107A-150.0)); + hc16=pdm[1][5]*pdl[1][3]; + zc16=pdm[1][4]*pdl[1][2]; + hc216=pdm[1][5]*pdl[1][4]; + output->d[1]=output->d[1]*ccor2(z,rl,hc16,zc16,hc216); + /* Chemistry correction */ + hcc16=pdm[1][7]*pdl[1][13]; + zcc16=pdm[1][6]*pdl[1][12]; + rc16=pdm[1][3]*pdl[1][14]; + /* Net density corrected at Alt */ + output->d[1]=output->d[1]*ccor(z,rc16,hcc16,zcc16); + } + + + /**** O2 DENSITY ****/ + + /* Density variation factor at Zlb */ + g32= flags->sw[21]*globe7(pd[4], input, flags); + /* Diffusive density at Zlb */ + db32 = pdm[3][0]*exp(g32)*pd[4][0]; + /* Diffusive density at Alt */ + output->d[3]=densu(z,db32,tinf,tlb, 32.,alpha[3],&output->t[1],ptm[5],s,mn1, zn1,meso_tn1,meso_tgn1); + dd=output->d[3]; + if (flags->sw[15]) { + if (z<=altl[3]) { + /* Turbopause */ + zh32=pdm[3][2]; + /* Mixed density at Zlb */ + b32=densu(zh32,db32,tinf,tlb,32.-xmm,alpha[3]-1., &output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + /* Mixed density at Alt */ + dm32=densu(z,b32,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + zhm32=zhm28; + /* Net density at Alt */ + output->d[3]=dnet(output->d[3],dm32,zhm32,xmm,32.); + /* Correction to specified mixing ratio at ground */ + rl=log(b28*pdm[3][1]/b32); + hc32=pdm[3][5]*pdl[1][7]; + zc32=pdm[3][4]*pdl[1][6]; + output->d[3]=output->d[3]*ccor(z,rl,hc32,zc32); + } + /* Correction for general departure from diffusive equilibrium above Zlb */ + hcc32=pdm[3][7]*pdl[1][22]; + hcc232=pdm[3][7]*pdl[0][22]; + zcc32=pdm[3][6]*pdl[1][21]; + rc32=pdm[3][3]*pdl[1][23]*(1.+flags->sw[1]*pdl[0][23]*(input->f107A-150.)); + /* Net density corrected at Alt */ + output->d[3]=output->d[3]*ccor2(z,rc32,hcc32,zcc32,hcc232); + } + + + /**** AR DENSITY ****/ + + /* Density variation factor at Zlb */ + g40= flags->sw[21]*globe7(pd[5],input,flags); + /* Diffusive density at Zlb */ + db40 = pdm[4][0]*exp(g40)*pd[5][0]; + /* Diffusive density at Alt */ + output->d[4]=densu(z,db40,tinf,tlb, 40.,alpha[4],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + dd=output->d[4]; + if ((flags->sw[15]) && (z<=altl[4])) { + /* Turbopause */ + zh40=pdm[4][2]; + /* Mixed density at Zlb */ + b40=densu(zh40,db40,tinf,tlb,40.-xmm,alpha[4]-1.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + /* Mixed density at Alt */ + dm40=densu(z,b40,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + zhm40=zhm28; + /* Net density at Alt */ + output->d[4]=dnet(output->d[4],dm40,zhm40,xmm,40.); + /* Correction to specified mixing ratio at ground */ + rl=log(b28*pdm[4][1]/b40); + hc40=pdm[4][5]*pdl[1][9]; + zc40=pdm[4][4]*pdl[1][8]; + /* Net density corrected at Alt */ + output->d[4]=output->d[4]*ccor(z,rl,hc40,zc40); + } + + + /**** HYDROGEN DENSITY ****/ + + /* Density variation factor at Zlb */ + g1 = flags->sw[21]*globe7(pd[6], input, flags); + /* Diffusive density at Zlb */ + db01 = pdm[5][0]*exp(g1)*pd[6][0]; + /* Diffusive density at Alt */ + output->d[6]=densu(z,db01,tinf,tlb,1.,alpha[6],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + dd=output->d[6]; + if ((flags->sw[15]) && (z<=altl[6])) { + /* Turbopause */ + zh01=pdm[5][2]; + /* Mixed density at Zlb */ + b01=densu(zh01,db01,tinf,tlb,1.-xmm,alpha[6]-1., &output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + /* Mixed density at Alt */ + dm01=densu(z,b01,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + zhm01=zhm28; + /* Net density at Alt */ + output->d[6]=dnet(output->d[6],dm01,zhm01,xmm,1.); + /* Correction to specified mixing ratio at ground */ + rl=log(b28*pdm[5][1]*sqrt(pdl[1][17]*pdl[1][17])/b01); + hc01=pdm[5][5]*pdl[1][11]; + zc01=pdm[5][4]*pdl[1][10]; + output->d[6]=output->d[6]*ccor(z,rl,hc01,zc01); + /* Chemistry correction */ + hcc01=pdm[5][7]*pdl[1][19]; + zcc01=pdm[5][6]*pdl[1][18]; + rc01=pdm[5][3]*pdl[1][20]; + /* Net density corrected at Alt */ + output->d[6]=output->d[6]*ccor(z,rc01,hcc01,zcc01); +} + + + /**** ATOMIC NITROGEN DENSITY ****/ + + /* Density variation factor at Zlb */ + g14 = flags->sw[21]*globe7(pd[7],input,flags); + /* Diffusive density at Zlb */ + db14 = pdm[6][0]*exp(g14)*pd[7][0]; + /* Diffusive density at Alt */ + output->d[7]=densu(z,db14,tinf,tlb,14.,alpha[7],&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + dd=output->d[7]; + if ((flags->sw[15]) && (z<=altl[7])) { + /* Turbopause */ + zh14=pdm[6][2]; + /* Mixed density at Zlb */ + b14=densu(zh14,db14,tinf,tlb,14.-xmm,alpha[7]-1., &output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + /* Mixed density at Alt */ + dm14=densu(z,b14,tinf,tlb,xmm,0.,&output->t[1],ptm[5],s,mn1,zn1,meso_tn1,meso_tgn1); + zhm14=zhm28; + /* Net density at Alt */ + output->d[7]=dnet(output->d[7],dm14,zhm14,xmm,14.); + /* Correction to specified mixing ratio at ground */ + rl=log(b28*pdm[6][1]*sqrt(pdl[0][2]*pdl[0][2])/b14); + hc14=pdm[6][5]*pdl[0][1]; + zc14=pdm[6][4]*pdl[0][0]; + output->d[7]=output->d[7]*ccor(z,rl,hc14,zc14); + /* Chemistry correction */ + hcc14=pdm[6][7]*pdl[0][4]; + zcc14=pdm[6][6]*pdl[0][3]; + rc14=pdm[6][3]*pdl[0][5]; + /* Net density corrected at Alt */ + output->d[7]=output->d[7]*ccor(z,rc14,hcc14,zcc14); + } + + + /**** Anomalous OXYGEN DENSITY ****/ + + g16h = flags->sw[21]*globe7(pd[8],input,flags); + db16h = pdm[7][0]*exp(g16h)*pd[8][0]; + tho = pdm[7][9]*pdl[0][6]; + dd=densu(z,db16h,tho,tho,16.,alpha[8],&output->t[1],ptm[5],s,mn1, zn1,meso_tn1,meso_tgn1); + zsht=pdm[7][5]; + zmho=pdm[7][4]; + zsho=scalh(zmho,16.0,tho); + output->d[8]=dd*exp(-zsht/zsho*(exp(-(z-zmho)/zsht)-1.)); + + + /* total mass density */ + output->d[5] = 1.66E-24*(4.0*output->d[0]+16.0*output->d[1]+28.0*output->d[2]+32.0*output->d[3]+40.0*output->d[4]+ output->d[6]+14.0*output->d[7]); + + + /* temperature */ + z = sqrt(input->alt*input->alt); + ddum = densu(z,1.0, tinf, tlb, 0.0, 0.0, &output->t[1], ptm[5], s, mn1, zn1, meso_tn1, meso_tgn1); + (void) ddum; /* silence gcc */ + if (flags->sw[0]) { + for(i=0;i<9;i++) + output->d[i]=output->d[i]*1.0E6; + output->d[5]=output->d[5]/1000; + } +} diff --git a/src/cpp/3rdparty/nrlmsise/nrlmsise-00.h b/src/cpp/3rdparty/nrlmsise/nrlmsise-00.h new file mode 100644 index 000000000..f1b8b4f9b --- /dev/null +++ b/src/cpp/3rdparty/nrlmsise/nrlmsise-00.h @@ -0,0 +1,224 @@ +#pragma once + +/* -------------------------------------------------------------------- */ +/* --------- N R L M S I S E - 0 0 M O D E L 2 0 0 1 ---------- */ +/* -------------------------------------------------------------------- */ + +/* This file is part of the NRLMSISE-00 C source code package - release + * 20041227 + * + * The NRLMSISE-00 model was developed by Mike Picone, Alan Hedin, and + * Doug Drob. They also wrote a NRLMSISE-00 distribution package in + * FORTRAN which is available at + * http://uap-www.nrl.navy.mil/models_web/msis/msis_home.htm + * + * Dominik Brodowski implemented and maintains this C version. You can + * reach him at mail@brodo.de. See the file "DOCUMENTATION" for details, + * and check http://www.brodo.de/english/pub/nrlmsise/index.html for + * updated releases of this package. + */ + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------- INPUT ----------------------------- */ +/* ------------------------------------------------------------------- */ + +struct nrlmsise_flags { + int switches[24]; + double sw[24]; + double swc[24]; +}; +/* + * Switches: to turn on and off particular variations use these switches. + * 0 is off, 1 is on, and 2 is main effects off but cross terms on. + * + * Standard values are 0 for switch 0 and 1 for switches 1 to 23. The + * array "switches" needs to be set accordingly by the calling program. + * The arrays sw and swc are set internally. + * + * switches[i]: + * i - explanation + * ----------------- + * 0 - output in meters and kilograms instead of centimeters and grams + * 1 - F10.7 effect on mean + * 2 - time independent + * 3 - symmetrical annual + * 4 - symmetrical semiannual + * 5 - asymmetrical annual + * 6 - asymmetrical semiannual + * 7 - diurnal + * 8 - semidiurnal + * 9 - daily ap [when this is set to -1 (!) the pointer + * ap_a in struct nrlmsise_input must + * point to a struct ap_array] + * 10 - all UT/long effects + * 11 - longitudinal + * 12 - UT and mixed UT/long + * 13 - mixed AP/UT/LONG + * 14 - terdiurnal + * 15 - departures from diffusive equilibrium + * 16 - all TINF var + * 17 - all TLB var + * 18 - all TN1 var + * 19 - all S var + * 20 - all TN2 var + * 21 - all NLB var + * 22 - all TN3 var + * 23 - turbo scale height var + */ + +struct ap_array { + double a[7]; +}; +/* Array containing the following magnetic values: + * 0 : daily AP + * 1 : 3 hr AP index for current time + * 2 : 3 hr AP index for 3 hrs before current time + * 3 : 3 hr AP index for 6 hrs before current time + * 4 : 3 hr AP index for 9 hrs before current time + * 5 : Average of eight 3 hr AP indicies from 12 to 33 hrs + * prior to current time + * 6 : Average of eight 3 hr AP indicies from 36 to 57 hrs + * prior to current time + */ + + +struct nrlmsise_input { + int year; /* year, currently ignored */ + int doy; /* day of year */ + double sec; /* seconds in day (UT) */ + double alt; /* altitude in kilometers */ + double g_lat; /* geodetic latitude */ + double g_long; /* geodetic longitude */ + double lst; /* local apparent solar time (hours), see note below */ + double f107A; /* 81 day average of F10.7 flux (centered on doy) */ + double f107; /* daily F10.7 flux for previous day */ + double ap; /* magnetic index(daily) */ + struct ap_array *ap_a; /* see above */ +}; +/* + * NOTES ON INPUT VARIABLES: + * UT, Local Time, and Longitude are used independently in the + * model and are not of equal importance for every situation. + * For the most physically realistic calculation these three + * variables should be consistent (lst=sec/3600 + g_long/15). + * The Equation of Time departures from the above formula + * for apparent local time can be included if available but + * are of minor importance. + * + * f107 and f107A values used to generate the model correspond + * to the 10.7 cm radio flux at the actual distance of the Earth + * from the Sun rather than the radio flux at 1 AU. The following + * site provides both classes of values: + * ftp://ftp.ngdc.noaa.gov/STP/SOLAR_DATA/SOLAR_RADIO/FLUX/ + * + * f107, f107A, and ap effects are neither large nor well + * established below 80 km and these parameters should be set to + * 150., 150., and 4. respectively. + */ + + + +/* ------------------------------------------------------------------- */ +/* ------------------------------ OUTPUT ----------------------------- */ +/* ------------------------------------------------------------------- */ + +struct nrlmsise_output { + double d[9]; /* densities */ + double t[2]; /* temperatures */ +}; +/* + * OUTPUT VARIABLES: + * d[0] - HE NUMBER DENSITY(CM-3) + * d[1] - O NUMBER DENSITY(CM-3) + * d[2] - N2 NUMBER DENSITY(CM-3) + * d[3] - O2 NUMBER DENSITY(CM-3) + * d[4] - AR NUMBER DENSITY(CM-3) + * d[5] - TOTAL MASS DENSITY(GM/CM3) [includes d[8] in td7d] + * d[6] - H NUMBER DENSITY(CM-3) + * d[7] - N NUMBER DENSITY(CM-3) + * d[8] - Anomalous oxygen NUMBER DENSITY(CM-3) + * t[0] - EXOSPHERIC TEMPERATURE + * t[1] - TEMPERATURE AT ALT + * + * + * O, H, and N are set to zero below 72.5 km + * + * t[0], Exospheric temperature, is set to global average for + * altitudes below 120 km. The 120 km gradient is left at global + * average value for altitudes below 72 km. + * + * d[5], TOTAL MASS DENSITY, is NOT the same for subroutines GTD7 + * and GTD7D + * + * SUBROUTINE GTD7 -- d[5] is the sum of the mass densities of the + * species labeled by indices 0-4 and 6-7 in output variable d. + * This includes He, O, N2, O2, Ar, H, and N but does NOT include + * anomalous oxygen (species index 8). + * + * SUBROUTINE GTD7D -- d[5] is the "effective total mass density + * for drag" and is the sum of the mass densities of all species + * in this model, INCLUDING anomalous oxygen. + */ + + + +/* ------------------------------------------------------------------- */ +/* --------------------------- PROTOTYPES ---------------------------- */ +/* ------------------------------------------------------------------- */ + +/* GTD7 */ +/* Neutral Atmosphere Empircial Model from the surface to lower + * exosphere. + */ +void gtd7 (struct nrlmsise_input *input, \ + struct nrlmsise_flags *flags, \ + struct nrlmsise_output *output); + + +/* GTD7D */ +/* This subroutine provides Effective Total Mass Density for output + * d[5] which includes contributions from "anomalous oxygen" which can + * affect satellite drag above 500 km. See the section "output" for + * additional details. + */ +void gtd7d(struct nrlmsise_input *input, \ + struct nrlmsise_flags *flags, \ + struct nrlmsise_output *output); + + +/* GTS7 */ +/* Thermospheric portion of NRLMSISE-00 + */ +void gts7 (struct nrlmsise_input *input, \ + struct nrlmsise_flags *flags, \ + struct nrlmsise_output *output); + + +/* GHP7 */ +/* To specify outputs at a pressure level (press) rather than at + * an altitude. + */ +void ghp7 (struct nrlmsise_input *input, \ + struct nrlmsise_flags *flags, \ + struct nrlmsise_output *output, \ + double press); + + + +/* ------------------------------------------------------------------- */ +/* ----------------------- COMPILATION TWEAKS ------------------------ */ +/* ------------------------------------------------------------------- */ + +/* "inlining" of functions */ +/* Some compilers (e.g. gcc) allow the inlining of functions into the + * calling routine. This means a lot of overhead can be removed, and + * the execution of the program runs much faster. However, the filesize + * and thus the loading time is increased. + */ +#ifdef INLINE +#define __inline_double static inline double +#else +#define __inline_double double +#endif diff --git a/src/cpp/3rdparty/nrlmsise/nrlmsise-00_data.cpp b/src/cpp/3rdparty/nrlmsise/nrlmsise-00_data.cpp new file mode 100644 index 000000000..0175e6cfc --- /dev/null +++ b/src/cpp/3rdparty/nrlmsise/nrlmsise-00_data.cpp @@ -0,0 +1,740 @@ +/* -------------------------------------------------------------------- */ +/* --------- N R L M S I S E - 0 0 M O D E L 2 0 0 1 ---------- */ +/* -------------------------------------------------------------------- */ + +/* This file is part of the NRLMSISE-00 C source code package - release + * 20041227 + * + * The NRLMSISE-00 model was developed by Mike Picone, Alan Hedin, and + * Doug Drob. They also wrote a NRLMSISE-00 distribution package in + * FORTRAN which is available at + * http://uap-www.nrl.navy.mil/models_web/msis/msis_home.htm + * + * Dominik Brodowski implemented and maintains this C version. You can + * reach him at mail@brodo.de. See the file "DOCUMENTATION" for details, + * and check http://www.brodo.de/english/pub/nrlmsise/index.html for + * updated releases of this package. + */ + + + +/* ------------------------------------------------------------------- */ +/* ------------------------ BLOCK DATA GTD7BK ------------------------ */ +/* ------------------------------------------------------------------- */ + +/* TEMPERATURE */ +double pt[150] = { + 9.86573E-01, 1.62228E-02, 1.55270E-02,-1.04323E-01,-3.75801E-03, + -1.18538E-03,-1.24043E-01, 4.56820E-03, 8.76018E-03,-1.36235E-01, + -3.52427E-02, 8.84181E-03,-5.92127E-03,-8.61650E+00, 0.00000E+00, + 1.28492E-02, 0.00000E+00, 1.30096E+02, 1.04567E-02, 1.65686E-03, + -5.53887E-06, 2.97810E-03, 0.00000E+00, 5.13122E-03, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.27026E-06, + 0.00000E+00, 6.74494E+00, 4.93933E-03, 2.21656E-03, 2.50802E-03, + 0.00000E+00, 0.00000E+00,-2.08841E-02,-1.79873E+00, 1.45103E-03, + 2.81769E-04,-1.44703E-03,-5.16394E-05, 8.47001E-02, 1.70147E-01, + 5.72562E-03, 5.07493E-05, 4.36148E-03, 1.17863E-04, 4.74364E-03, + 6.61278E-03, 4.34292E-05, 1.44373E-03, 2.41470E-05, 2.84426E-03, + 8.56560E-04, 2.04028E-03, 0.00000E+00,-3.15994E+03,-2.46423E-03, + 1.13843E-03, 4.20512E-04, 0.00000E+00,-9.77214E+01, 6.77794E-03, + 5.27499E-03, 1.14936E-03, 0.00000E+00,-6.61311E-03,-1.84255E-02, + -1.96259E-02, 2.98618E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 6.44574E+02, 8.84668E-04, 5.05066E-04, 0.00000E+00, 4.02881E+03, + -1.89503E-03, 0.00000E+00, 0.00000E+00, 8.21407E-04, 2.06780E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -1.20410E-02,-3.63963E-03, 9.92070E-05,-1.15284E-04,-6.33059E-05, + -6.05545E-01, 8.34218E-03,-9.13036E+01, 3.71042E-04, 0.00000E+00, + 4.19000E-04, 2.70928E-03, 3.31507E-03,-4.44508E-03,-4.96334E-03, + -1.60449E-03, 3.95119E-03, 2.48924E-03, 5.09815E-04, 4.05302E-03, + 2.24076E-03, 0.00000E+00, 6.84256E-03, 4.66354E-04, 0.00000E+00, + -3.68328E-04, 0.00000E+00, 0.00000E+00,-1.46870E+02, 0.00000E+00, + 0.00000E+00, 1.09501E-03, 4.65156E-04, 5.62583E-04, 3.21596E+00, + 6.43168E-04, 3.14860E-03, 3.40738E-03, 1.78481E-03, 9.62532E-04, + 5.58171E-04, 3.43731E+00,-2.33195E-01, 5.10289E-04, 0.00000E+00, + 0.00000E+00,-9.25347E+04, 0.00000E+00,-1.99639E-03, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}; + +double pd[9][150] = { +/* HE DENSITY */ { + 1.09979E+00,-4.88060E-02,-1.97501E-01,-9.10280E-02,-6.96558E-03, + 2.42136E-02, 3.91333E-01,-7.20068E-03,-3.22718E-02, 1.41508E+00, + 1.68194E-01, 1.85282E-02, 1.09384E-01,-7.24282E+00, 0.00000E+00, + 2.96377E-01,-4.97210E-02, 1.04114E+02,-8.61108E-02,-7.29177E-04, + 1.48998E-06, 1.08629E-03, 0.00000E+00, 0.00000E+00, 8.31090E-02, + 1.12818E-01,-5.75005E-02,-1.29919E-02,-1.78849E-02,-2.86343E-06, + 0.00000E+00,-1.51187E+02,-6.65902E-03, 0.00000E+00,-2.02069E-03, + 0.00000E+00, 0.00000E+00, 4.32264E-02,-2.80444E+01,-3.26789E-03, + 2.47461E-03, 0.00000E+00, 0.00000E+00, 9.82100E-02, 1.22714E-01, + -3.96450E-02, 0.00000E+00,-2.76489E-03, 0.00000E+00, 1.87723E-03, + -8.09813E-03, 4.34428E-05,-7.70932E-03, 0.00000E+00,-2.28894E-03, + -5.69070E-03,-5.22193E-03, 6.00692E-03,-7.80434E+03,-3.48336E-03, + -6.38362E-03,-1.82190E-03, 0.00000E+00,-7.58976E+01,-2.17875E-02, + -1.72524E-02,-9.06287E-03, 0.00000E+00, 2.44725E-02, 8.66040E-02, + 1.05712E-01, 3.02543E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -6.01364E+03,-5.64668E-03,-2.54157E-03, 0.00000E+00, 3.15611E+02, + -5.69158E-03, 0.00000E+00, 0.00000E+00,-4.47216E-03,-4.49523E-03, + 4.64428E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 4.51236E-02, 2.46520E-02, 6.17794E-03, 0.00000E+00, 0.00000E+00, + -3.62944E-01,-4.80022E-02,-7.57230E+01,-1.99656E-03, 0.00000E+00, + -5.18780E-03,-1.73990E-02,-9.03485E-03, 7.48465E-03, 1.53267E-02, + 1.06296E-02, 1.18655E-02, 2.55569E-03, 1.69020E-03, 3.51936E-02, + -1.81242E-02, 0.00000E+00,-1.00529E-01,-5.10574E-03, 0.00000E+00, + 2.10228E-03, 0.00000E+00, 0.00000E+00,-1.73255E+02, 5.07833E-01, + -2.41408E-01, 8.75414E-03, 2.77527E-03,-8.90353E-05,-5.25148E+00, + -5.83899E-03,-2.09122E-02,-9.63530E-03, 9.77164E-03, 4.07051E-03, + 2.53555E-04,-5.52875E+00,-3.55993E-01,-2.49231E-03, 0.00000E+00, + 0.00000E+00, 2.86026E+01, 0.00000E+00, 3.42722E-04, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}, /* O DENSITY */ { + 1.02315E+00,-1.59710E-01,-1.06630E-01,-1.77074E-02,-4.42726E-03, + 3.44803E-02, 4.45613E-02,-3.33751E-02,-5.73598E-02, 3.50360E-01, + 6.33053E-02, 2.16221E-02, 5.42577E-02,-5.74193E+00, 0.00000E+00, + 1.90891E-01,-1.39194E-02, 1.01102E+02, 8.16363E-02, 1.33717E-04, + 6.54403E-06, 3.10295E-03, 0.00000E+00, 0.00000E+00, 5.38205E-02, + 1.23910E-01,-1.39831E-02, 0.00000E+00, 0.00000E+00,-3.95915E-06, + 0.00000E+00,-7.14651E-01,-5.01027E-03, 0.00000E+00,-3.24756E-03, + 0.00000E+00, 0.00000E+00, 4.42173E-02,-1.31598E+01,-3.15626E-03, + 1.24574E-03,-1.47626E-03,-1.55461E-03, 6.40682E-02, 1.34898E-01, + -2.42415E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 6.13666E-04, + -5.40373E-03, 2.61635E-05,-3.33012E-03, 0.00000E+00,-3.08101E-03, + -2.42679E-03,-3.36086E-03, 0.00000E+00,-1.18979E+03,-5.04738E-02, + -2.61547E-03,-1.03132E-03, 1.91583E-04,-8.38132E+01,-1.40517E-02, + -1.14167E-02,-4.08012E-03, 1.73522E-04,-1.39644E-02,-6.64128E-02, + -6.85152E-02,-1.34414E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 6.07916E+02,-4.12220E-03,-2.20996E-03, 0.00000E+00, 1.70277E+03, + -4.63015E-03, 0.00000E+00, 0.00000E+00,-2.25360E-03,-2.96204E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 3.92786E-02, 1.31186E-02,-1.78086E-03, 0.00000E+00, 0.00000E+00, + -3.90083E-01,-2.84741E-02,-7.78400E+01,-1.02601E-03, 0.00000E+00, + -7.26485E-04,-5.42181E-03,-5.59305E-03, 1.22825E-02, 1.23868E-02, + 6.68835E-03,-1.03303E-02,-9.51903E-03, 2.70021E-04,-2.57084E-02, + -1.32430E-02, 0.00000E+00,-3.81000E-02,-3.16810E-03, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-9.05762E-04,-2.14590E-03,-1.17824E-03, 3.66732E+00, + -3.79729E-04,-6.13966E-03,-5.09082E-03,-1.96332E-03,-3.08280E-03, + -9.75222E-04, 4.03315E+00,-2.52710E-01, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}, /* N2 DENSITY */ { + 1.16112E+00, 0.00000E+00, 0.00000E+00, 3.33725E-02, 0.00000E+00, + 3.48637E-02,-5.44368E-03, 0.00000E+00,-6.73940E-02, 1.74754E-01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, + 1.26733E-01, 0.00000E+00, 1.03154E+02, 5.52075E-02, 0.00000E+00, + 0.00000E+00, 8.13525E-04, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-2.50482E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.48894E-03, + 6.16053E-04,-5.79716E-04, 2.95482E-03, 8.47001E-02, 1.70147E-01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}, /* TLB */ { + 9.44846E-01, 0.00000E+00, 0.00000E+00,-3.08617E-02, 0.00000E+00, + -2.44019E-02, 6.48607E-03, 0.00000E+00, 3.08181E-02, 4.59392E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, + 2.13260E-02, 0.00000E+00,-3.56958E+02, 0.00000E+00, 1.82278E-04, + 0.00000E+00, 3.07472E-04, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 3.83054E-03, 0.00000E+00, 0.00000E+00, + -1.93065E-03,-1.45090E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.23493E-03, 1.36736E-03, 8.47001E-02, 1.70147E-01, + 3.71469E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 5.10250E-03, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 3.68756E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}, /* O2 DENSITY */ { + 1.35580E+00, 1.44816E-01, 0.00000E+00, 6.07767E-02, 0.00000E+00, + 2.94777E-02, 7.46900E-02, 0.00000E+00,-9.23822E-02, 8.57342E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.38636E+01, 0.00000E+00, + 7.71653E-02, 0.00000E+00, 8.18751E+01, 1.87736E-02, 0.00000E+00, + 0.00000E+00, 1.49667E-02, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-3.67874E+02, 5.48158E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, + 1.22631E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 8.17187E-03, 3.71617E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.10826E-03, + -3.13640E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -7.35742E-02,-5.00266E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 1.94965E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}, /* AR DENSITY */ { + 1.04761E+00, 2.00165E-01, 2.37697E-01, 3.68552E-02, 0.00000E+00, + 3.57202E-02,-2.14075E-01, 0.00000E+00,-1.08018E-01,-3.73981E-01, + 0.00000E+00, 3.10022E-02,-1.16305E-03,-2.07596E+01, 0.00000E+00, + 8.64502E-02, 0.00000E+00, 9.74908E+01, 5.16707E-02, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 3.46193E+02, 1.34297E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.48509E-03, + -1.54689E-04, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, + 1.47753E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 1.89320E-02, 3.68181E-05, 1.32570E-02, 0.00000E+00, 0.00000E+00, + 3.59719E-03, 7.44328E-03,-1.00023E-03,-6.50528E+03, 0.00000E+00, + 1.03485E-02,-1.00983E-03,-4.06916E-03,-6.60864E+01,-1.71533E-02, + 1.10605E-02, 1.20300E-02,-5.20034E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -2.62769E+03, 7.13755E-03, 4.17999E-03, 0.00000E+00, 1.25910E+04, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.23595E-03, 4.60217E-03, + 5.71794E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -3.18353E-02,-2.35526E-02,-1.36189E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.03522E-02,-6.67837E+01,-1.09724E-03, 0.00000E+00, + -1.38821E-02, 1.60468E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.51574E-02, + -5.44470E-04, 0.00000E+00, 7.28224E-02, 6.59413E-02, 0.00000E+00, + -5.15692E-03, 0.00000E+00, 0.00000E+00,-3.70367E+03, 0.00000E+00, + 0.00000E+00, 1.36131E-02, 5.38153E-03, 0.00000E+00, 4.76285E+00, + -1.75677E-02, 2.26301E-02, 0.00000E+00, 1.76631E-02, 4.77162E-03, + 0.00000E+00, 5.39354E+00, 0.00000E+00,-7.51710E-03, 0.00000E+00, + 0.00000E+00,-8.82736E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}, /* H DENSITY */ { + 1.26376E+00,-2.14304E-01,-1.49984E-01, 2.30404E-01, 2.98237E-02, + 2.68673E-02, 2.96228E-01, 2.21900E-02,-2.07655E-02, 4.52506E-01, + 1.20105E-01, 3.24420E-02, 4.24816E-02,-9.14313E+00, 0.00000E+00, + 2.47178E-02,-2.88229E-02, 8.12805E+01, 5.10380E-02,-5.80611E-03, + 2.51236E-05,-1.24083E-02, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01,-3.48190E-02, 0.00000E+00, 0.00000E+00, 2.89885E-05, + 0.00000E+00, 1.53595E+02,-1.68604E-02, 0.00000E+00, 1.01015E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.84552E-04, + -1.22181E-03, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, + -1.04927E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.91313E-03, + -2.30501E-02, 3.14758E-05, 0.00000E+00, 0.00000E+00, 1.26956E-02, + 8.35489E-03, 3.10513E-04, 0.00000E+00, 3.42119E+03,-2.45017E-03, + -4.27154E-04, 5.45152E-04, 1.89896E-03, 2.89121E+01,-6.49973E-03, + -1.93855E-02,-1.48492E-02, 0.00000E+00,-5.10576E-02, 7.87306E-02, + 9.51981E-02,-1.49422E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 2.65503E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 6.37110E-03, 3.24789E-04, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 6.14274E-02, 1.00376E-02,-8.41083E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.27099E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -3.94077E-03,-1.28601E-02,-7.97616E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-6.71465E-03,-1.69799E-03, 1.93772E-03, 3.81140E+00, + -7.79290E-03,-1.82589E-02,-1.25860E-02,-1.04311E-02,-3.02465E-03, + 2.43063E-03, 3.63237E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}, /* N DENSITY */ { + 7.09557E+01,-3.26740E-01, 0.00000E+00,-5.16829E-01,-1.71664E-03, + 9.09310E-02,-6.71500E-01,-1.47771E-01,-9.27471E-02,-2.30862E-01, + -1.56410E-01, 1.34455E-02,-1.19717E-01, 2.52151E+00, 0.00000E+00, + -2.41582E-01, 5.92939E-02, 4.39756E+00, 9.15280E-02, 4.41292E-03, + 0.00000E+00, 8.66807E-03, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 9.74701E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 6.70217E+01,-1.31660E-03, 0.00000E+00,-1.65317E-02, + 0.00000E+00, 0.00000E+00, 8.50247E-02, 2.77428E+01, 4.98658E-03, + 6.15115E-03, 9.50156E-03,-2.12723E-02, 8.47001E-02, 1.70147E-01, + -2.38645E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.37380E-03, + -8.41918E-03, 2.80145E-05, 7.12383E-03, 0.00000E+00,-1.66209E-02, + 1.03533E-04,-1.68898E-02, 0.00000E+00, 3.64526E+03, 0.00000E+00, + 6.54077E-03, 3.69130E-04, 9.94419E-04, 8.42803E+01,-1.16124E-02, + -7.74414E-03,-1.68844E-03, 1.42809E-03,-1.92955E-03, 1.17225E-01, + -2.41512E-02, 1.50521E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 1.60261E+03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.54403E-04,-1.87270E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 2.76439E-02, 6.43207E-03,-3.54300E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-2.80221E-02, 8.11228E+01,-6.75255E-04, 0.00000E+00, + -1.05162E-02,-3.48292E-03,-6.97321E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.45546E-03,-1.31970E-02,-3.57751E-03,-1.09021E+00, + -1.50181E-02,-7.12841E-03,-6.64590E-03,-3.52610E-03,-1.87773E-02, + -2.22432E-03,-3.93895E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}, /* HOT O DENSITY */ { + 6.04050E-02, 1.57034E+00, 2.99387E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.51018E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.61650E+00, 1.26454E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 5.50878E-03, 0.00000E+00, 0.00000E+00, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 6.23881E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 8.47001E-02, 1.70147E-01, + -9.45934E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}}; +/* S PARAM */ +double ps[150] = { + 9.56827E-01, 6.20637E-02, 3.18433E-02, 0.00000E+00, 0.00000E+00, + 3.94900E-02, 0.00000E+00, 0.00000E+00,-9.24882E-03,-7.94023E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.74712E+02, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.74677E-03, 0.00000E+00, 1.54951E-02, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-6.99007E-04, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 1.24362E-02,-5.28756E-03, 8.47001E-02, 1.70147E-01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.47425E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}; + +/* TURBO */ +double pdl[2][25] = { + { 1.09930E+00, 3.90631E+00, 3.07165E+00, 9.86161E-01, 1.63536E+01, + 4.63830E+00, 1.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 1.28840E+00, 3.10302E-02, 1.18339E-01 }, + { 1.00000E+00, 7.00000E-01, 1.15020E+00, 3.44689E+00, 1.28840E+00, + 1.00000E+00, 1.08738E+00, 1.22947E+00, 1.10016E+00, 7.34129E-01, + 1.15241E+00, 2.22784E+00, 7.95046E-01, 4.01612E+00, 4.47749E+00, + 1.23435E+02,-7.60535E-02, 1.68986E-06, 7.44294E-01, 1.03604E+00, + 1.72783E+02, 1.15020E+00, 3.44689E+00,-7.46230E-01, 9.49154E-01 } +}; +/* LOWER BOUNDARY */ +double ptm[50] = { + 1.04130E+03, 3.86000E+02, 1.95000E+02, 1.66728E+01, 2.13000E+02, + 1.20000E+02, 2.40000E+02, 1.87000E+02,-2.00000E+00, 0.00000E+00 +}; +double pdm[8][10] = { +{ 2.45600E+07, 6.71072E-06, 1.00000E+02, 0.00000E+00, 1.10000E+02, + 1.00000E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 },\ +{ 8.59400E+10, 1.00000E+00, 1.05000E+02,-8.00000E+00, 1.10000E+02, + 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00 },\ +{ 2.81000E+11, 0.00000E+00, 1.05000E+02, 2.80000E+01, 2.89500E+01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 }, +{ 3.30000E+10, 2.68270E-01, 1.05000E+02, 1.00000E+00, 1.10000E+02, + 1.00000E+01, 1.10000E+02,-1.00000E+01, 0.00000E+00, 0.00000E+00 }, +{ 1.33000E+09, 1.19615E-02, 1.05000E+02, 0.00000E+00, 1.10000E+02, + 1.00000E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 }, +{ 1.76100E+05, 1.00000E+00, 9.50000E+01,-8.00000E+00, 1.10000E+02, + 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00, }, +{ 1.00000E+07, 1.00000E+00, 1.05000E+02,-8.00000E+00, 1.10000E+02, + 1.00000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 0.00000E+00 }, +{ 1.00000E+06, 1.00000E+00, 1.05000E+02,-8.00000E+00, 5.50000E+02, + 7.60000E+01, 9.00000E+01, 2.00000E+00, 0.00000E+00, 4.00000E+03 }}; + + +double ptl[4][100] = { +/* TN1(2) */ { + 1.00858E+00, 4.56011E-02,-2.22972E-02,-5.44388E-02, 5.23136E-04, + -1.88849E-02, 5.23707E-02,-9.43646E-03, 6.31707E-03,-7.80460E-02, + -4.88430E-02, 0.00000E+00, 0.00000E+00,-7.60250E+00, 0.00000E+00, + -1.44635E-02,-1.76843E-02,-1.21517E+02, 2.85647E-02, 0.00000E+00, + 0.00000E+00, 6.31792E-04, 0.00000E+00, 5.77197E-03, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-8.90272E+03, 3.30611E-03, 3.02172E-03, 0.00000E+00, + -2.13673E-03,-3.20910E-04, 0.00000E+00, 0.00000E+00, 2.76034E-03, + 2.82487E-03,-2.97592E-04,-4.21534E-03, 8.47001E-02, 1.70147E-01, + 8.96456E-03, 0.00000E+00,-1.08596E-02, 0.00000E+00, 0.00000E+00, + 5.57917E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 9.65405E-03, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN1(3) */ { + 9.39664E-01, 8.56514E-02,-6.79989E-03, 2.65929E-02,-4.74283E-03, + 1.21855E-02,-2.14905E-02, 6.49651E-03,-2.05477E-02,-4.24952E-02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.19148E+01, 0.00000E+00, + 1.18777E-02,-7.28230E-02,-8.15965E+01, 1.73887E-02, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-1.44691E-02, 2.80259E-04, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.16584E+02, 3.18713E-03, 7.37479E-03, 0.00000E+00, + -2.55018E-03,-3.92806E-03, 0.00000E+00, 0.00000E+00,-2.89757E-03, + -1.33549E-03, 1.02661E-03, 3.53775E-04, 8.47001E-02, 1.70147E-01, + -9.17497E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 3.56082E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.00902E-02, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN1(4) */ { + 9.85982E-01,-4.55435E-02, 1.21106E-02, 2.04127E-02,-2.40836E-03, + 1.11383E-02,-4.51926E-02, 1.35074E-02,-6.54139E-03, 1.15275E-01, + 1.28247E-01, 0.00000E+00, 0.00000E+00,-5.30705E+00, 0.00000E+00, + -3.79332E-02,-6.24741E-02, 7.71062E-01, 2.96315E-02, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 6.81051E-03,-4.34767E-03, 8.66784E-02, + 1.58727E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 1.07003E+01,-2.76907E-03, 4.32474E-04, 0.00000E+00, + 1.31497E-03,-6.47517E-04, 0.00000E+00,-2.20621E+01,-1.10804E-03, + -8.09338E-04, 4.18184E-04, 4.29650E-03, 8.47001E-02, 1.70147E-01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -4.04337E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-9.52550E-04, + 8.56253E-04, 4.33114E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.21223E-03, + 2.38694E-04, 9.15245E-04, 1.28385E-03, 8.67668E-04,-5.61425E-06, + 1.04445E+00, 3.41112E+01, 0.00000E+00,-8.40704E-01,-2.39639E+02, + 7.06668E-01,-2.05873E+01,-3.63696E-01, 2.39245E+01, 0.00000E+00, + -1.06657E-03,-7.67292E-04, 1.54534E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN1(5) TN2(1) */ { + 1.00320E+00, 3.83501E-02,-2.38983E-03, 2.83950E-03, 4.20956E-03, + 5.86619E-04, 2.19054E-02,-1.00946E-02,-3.50259E-03, 4.17392E-02, + -8.44404E-03, 0.00000E+00, 0.00000E+00, 4.96949E+00, 0.00000E+00, + -7.06478E-03,-1.46494E-02, 3.13258E+01,-1.86493E-03, 0.00000E+00, + -1.67499E-02, 0.00000E+00, 0.00000E+00, 5.12686E-04, 8.66784E-02, + 1.58727E-01,-4.64167E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 4.37353E-03,-1.99069E+02, 0.00000E+00,-5.34884E-03, 0.00000E+00, + 1.62458E-03, 2.93016E-03, 2.67926E-03, 5.90449E+02, 0.00000E+00, + 0.00000E+00,-1.17266E-03,-3.58890E-04, 8.47001E-02, 1.70147E-01, + 0.00000E+00, 0.00000E+00, 1.38673E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.60571E-03, + 6.28078E-04, 5.05469E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.57829E-03, + -4.00855E-04, 5.04077E-05,-1.39001E-03,-2.33406E-03,-4.81197E-04, + 1.46758E+00, 6.20332E+00, 0.00000E+00, 3.66476E-01,-6.19760E+01, + 3.09198E-01,-1.98999E+01, 0.00000E+00,-3.29933E+02, 0.00000E+00, + -1.10080E-03,-9.39310E-05, 1.39638E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +} }; + +double pma[10][100] = { +/* TN2(2) */ { + 9.81637E-01,-1.41317E-03, 3.87323E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.58707E-02, + -8.63658E-03, 0.00000E+00, 0.00000E+00,-2.02226E+00, 0.00000E+00, + -8.69424E-03,-1.91397E-02, 8.76779E+01, 4.52188E-03, 0.00000E+00, + 2.23760E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-7.07572E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + -4.11210E-03, 3.50060E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-8.36657E-03, 1.61347E+01, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-1.45130E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.24152E-03, + 6.43365E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.33255E-03, + 2.42657E-03, 1.60666E-03,-1.85728E-03,-1.46874E-03,-4.79163E-06, + 1.22464E+00, 3.53510E+01, 0.00000E+00, 4.49223E-01,-4.77466E+01, + 4.70681E-01, 8.41861E+00,-2.88198E-01, 1.67854E+02, 0.00000E+00, + 7.11493E-04, 6.05601E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN2(3) */ { + 1.00422E+00,-7.11212E-03, 5.24480E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.28914E-02, + -2.41301E-02, 0.00000E+00, 0.00000E+00,-2.12219E+01,-1.03830E-02, + -3.28077E-03, 1.65727E-02, 1.68564E+00,-6.68154E-03, 0.00000E+00, + 1.45155E-02, 0.00000E+00, 8.42365E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-4.34645E-03, 0.00000E+00, 0.00000E+00, 2.16780E-02, + 0.00000E+00,-1.38459E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 7.04573E-03,-4.73204E+01, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 1.08767E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.08279E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.21769E-04, + -2.27387E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.26769E-03, + 3.16901E-03, 4.60316E-04,-1.01431E-04, 1.02131E-03, 9.96601E-04, + 1.25707E+00, 2.50114E+01, 0.00000E+00, 4.24472E-01,-2.77655E+01, + 3.44625E-01, 2.75412E+01, 0.00000E+00, 7.94251E+02, 0.00000E+00, + 2.45835E-03, 1.38871E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN2(4) TN3(1) */ { + 1.01890E+00,-2.46603E-02, 1.00078E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-6.70977E-02, + -4.02286E-02, 0.00000E+00, 0.00000E+00,-2.29466E+01,-7.47019E-03, + 2.26580E-03, 2.63931E-02, 3.72625E+01,-6.39041E-03, 0.00000E+00, + 9.58383E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.85291E-03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 1.39717E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 9.19771E-03,-3.69121E+02, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-1.57067E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.07265E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.92953E-03, + -2.77739E-03,-4.40092E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.47280E-03, + 2.95035E-04,-1.81246E-03, 2.81945E-03, 4.27296E-03, 9.78863E-04, + 1.40545E+00,-6.19173E+00, 0.00000E+00, 0.00000E+00,-7.93632E+01, + 4.44643E-01,-4.03085E+02, 0.00000E+00, 1.15603E+01, 0.00000E+00, + 2.25068E-03, 8.48557E-04,-2.98493E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN3(2) */ { + 9.75801E-01, 3.80680E-02,-3.05198E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.85575E-02, + 5.04057E-02, 0.00000E+00, 0.00000E+00,-1.76046E+02, 1.44594E-02, + -1.48297E-03,-3.68560E-03, 3.02185E+01,-3.23338E-03, 0.00000E+00, + 1.53569E-02, 0.00000E+00,-1.15558E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 4.89620E-03, 0.00000E+00, 0.00000E+00,-1.00616E-02, + -8.21324E-03,-1.57757E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 6.63564E-03, 4.58410E+01, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-2.51280E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 9.91215E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-8.73148E-04, + -1.29648E-03,-7.32026E-05, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.68110E-03, + -4.66003E-03,-1.31567E-03,-7.39390E-04, 6.32499E-04,-4.65588E-04, + -1.29785E+00,-1.57139E+02, 0.00000E+00, 2.58350E-01,-3.69453E+01, + 4.10672E-01, 9.78196E+00,-1.52064E-01,-3.85084E+03, 0.00000E+00, + -8.52706E-04,-1.40945E-03,-7.26786E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN3(3) */ { + 9.60722E-01, 7.03757E-02,-3.00266E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.22671E-02, + 4.10423E-02, 0.00000E+00, 0.00000E+00,-1.63070E+02, 1.06073E-02, + 5.40747E-04, 7.79481E-03, 1.44908E+02, 1.51484E-04, 0.00000E+00, + 1.97547E-02, 0.00000E+00,-1.41844E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 5.77884E-03, 0.00000E+00, 0.00000E+00, 9.74319E-03, + 0.00000E+00,-2.88015E+03, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-4.44902E-03,-2.92760E+01, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 2.34419E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.36685E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.65325E-04, + -5.50628E-04, 3.31465E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.06179E-03, + -3.08575E-03,-7.93589E-04,-1.08629E-04, 5.95511E-04,-9.05050E-04, + 1.18997E+00, 4.15924E+01, 0.00000E+00,-4.72064E-01,-9.47150E+02, + 3.98723E-01, 1.98304E+01, 0.00000E+00, 3.73219E+03, 0.00000E+00, + -1.50040E-03,-1.14933E-03,-1.56769E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN3(4) */ { + 1.03123E+00,-7.05124E-02, 8.71615E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-3.82621E-02, + -9.80975E-03, 0.00000E+00, 0.00000E+00, 2.89286E+01, 9.57341E-03, + 0.00000E+00, 0.00000E+00, 8.66153E+01, 7.91938E-04, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 4.68917E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 7.86638E-03, 0.00000E+00, 0.00000E+00, 9.90827E-03, + 0.00000E+00, 6.55573E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-4.00200E+01, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 7.07457E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.72268E-03, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.04970E-04, + 1.21560E-03,-8.05579E-06, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.49941E-03, + -4.57256E-04,-1.59311E-04, 2.96481E-04,-1.77318E-03,-6.37918E-04, + 1.02395E+00, 1.28172E+01, 0.00000E+00, 1.49903E-01,-2.63818E+01, + 0.00000E+00, 4.70628E+01,-2.22139E-01, 4.82292E-02, 0.00000E+00, + -8.67075E-04,-5.86479E-04, 5.32462E-04, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TN3(5) SURFACE TEMP TSL */ { + 1.00828E+00,-9.10404E-02,-2.26549E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-2.32420E-02, + -9.08925E-03, 0.00000E+00, 0.00000E+00, 3.36105E+01, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-1.24957E+01,-5.87939E-03, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.79765E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.01237E+03, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-1.75553E-02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.29699E-03, + 1.26659E-03, 2.68402E-04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 1.17894E-03, + 1.48746E-03, 1.06478E-04, 1.34743E-04,-2.20939E-03,-6.23523E-04, + 6.36539E-01, 1.13621E+01, 0.00000E+00,-3.93777E-01, 2.38687E+03, + 0.00000E+00, 6.61865E+02,-1.21434E-01, 9.27608E+00, 0.00000E+00, + 1.68478E-04, 1.24892E-03, 1.71345E-03, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TGN3(2) SURFACE GRAD TSLG */ { + 1.57293E+00,-6.78400E-01, 6.47500E-01, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-7.62974E-02, + -3.60423E-01, 0.00000E+00, 0.00000E+00, 1.28358E+02, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 4.68038E+01, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.67898E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.90994E+04, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.15706E+01, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TGN2(1) TGN1(2) */ { + 8.60028E-01, 3.77052E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.17570E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 7.77757E-03, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 1.01024E+02, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 6.54251E+02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,-1.56959E-02, + 1.91001E-02, 3.15971E-02, 1.00982E-02,-6.71565E-03, 2.57693E-03, + 1.38692E+00, 2.82132E-01, 0.00000E+00, 0.00000E+00, 3.81511E+02, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +}, /* TGN3(1) TGN2(2) */ { + 1.06029E+00,-5.25231E-02, 3.73034E-01, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.31072E-02, + -3.88409E-01, 0.00000E+00, 0.00000E+00,-1.65295E+02,-2.13801E-01, + -4.38916E-02,-3.22716E-01,-8.82393E+01, 1.18458E-01, 0.00000E+00, + -4.35863E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00,-1.19782E-01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 2.62229E+01, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00,-5.37443E+01, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00,-4.55788E-01, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 3.84009E-02, + 3.96733E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 5.05494E-02, + 7.39617E-02, 1.92200E-02,-8.46151E-03,-1.34244E-02, 1.96338E-02, + 1.50421E+00, 1.88368E+01, 0.00000E+00, 0.00000E+00,-5.13114E+01, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 5.11923E-02, 3.61225E-02, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 2.00000E+00 +} }; + +/* SEMIANNUAL MULT SAM */ +double sam[100] = { + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, 1.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, + 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00 +}; + + +/* MIDDLE ATMOSPHERE AVERAGES */ +double pavgm[10] = { + 2.61000E+02, 2.64000E+02, 2.29000E+02, 2.17000E+02, 2.17000E+02, + 2.23000E+02, 2.86760E+02,-2.93940E+00, 2.50000E+00, 0.00000E+00 }; + diff --git a/src/cpp/CMakeLists.txt b/src/cpp/CMakeLists.txt index 1a5b9b0a9..e50198431 100644 --- a/src/cpp/CMakeLists.txt +++ b/src/cpp/CMakeLists.txt @@ -27,6 +27,9 @@ add_executable(pea 3rdparty/iers2010/hardisp/tdfrph.cpp 3rdparty/iers2010/dehanttideinel/dehanttide_all.cpp + 3rdparty/nrlmsise/nrlmsise-00.cpp + 3rdparty/nrlmsise/nrlmsise-00_data.cpp + pea/main.cpp pea/inputs.cpp pea/outputs.cpp @@ -50,6 +53,7 @@ add_executable(pea common/algebra.cpp common/algebra_old.cpp common/algebraTrace.cpp + common/kalmanBlas.cpp common/attitude.cpp common/compare.cpp common/antenna.cpp @@ -67,23 +71,19 @@ add_executable(pea common/ephKalman.cpp common/ephPrecise.cpp common/ephSSR.cpp + common/ephSBAS.cpp common/erp.cpp common/fileLog.cpp common/fileLog.hpp common/gpx.cpp common/pos.cpp common/gTime.cpp - common/interactiveTerminal.cpp common/ionModels.cpp common/linearCombo.cpp - common/mongo.cpp - common/mongoRead.cpp - common/mongoWrite.cpp common/ntripTrace.cpp common/orbits.cpp common/receiver.cpp - common/receiver.cpp common/rinex.cpp common/rtsSmoothing.cpp common/rtcmDecoder.cpp @@ -99,7 +99,6 @@ add_executable(pea common/orbex.cpp common/orbexWrite.cpp common/tcpSocket.cpp - common/tcpSocket.cpp common/trace.cpp common/testUtils.cpp common/rinexClkWrite.cpp @@ -107,7 +106,6 @@ add_executable(pea common/rinexObsWrite.cpp common/tides.cpp common/ubxDecoder.cpp - common/walkthrough.cpp common/localAtmosRegion.cpp common/streamNtrip.cpp @@ -140,7 +138,6 @@ add_executable(pea slr/slrCom.cpp slr/slrObs.cpp - slr/slrSat.cpp slr/slrRec.cpp other_ssr/otherSSR.hpp @@ -160,53 +157,28 @@ add_executable(pea orbprop/staticField.cpp orbprop/centerMassCorrections.cpp orbprop/oceanPoleTide.cpp + orbprop/spaceWeather.cpp rtklib/lambda.cpp rtklib/rtkcmn.cpp sbas/sisnet.cpp sbas/sbas.cpp + sbas/decodeL1.cpp + sbas/decodeL5.cpp ) -target_include_directories(pea PUBLIC - 3rdparty - 3rdparty/egm96 - 3rdparty/iers2010 - 3rdparty/sofa/src - 3rdparty/jpl - 3rdparty/mqtt_cpp/ - 3rdparty/slr +add_dependencies(pea updateGit) +target_include_directories(pea PUBLIC configurator ../Architecture/ - - ambres - slr - common - iono - trop - inertial - orbprop - pea - peaUploader - rtklib - other_ssr - sbas - + ${CMAKE_CURRENT_SOURCE_DIR} ${EIGEN3_INCLUDE_DIRS} - ${LAPACK_INCLUDE_DIRS} - ${OPENBLAS_INCLUDE_DIRS} ${YAML_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} - ${LIBMONGOCXX_INCLUDE_DIR} - ${LIBBSONCXX_INCLUDE_DIR} - ${OPENSSL_INCLUDE_DIR} - "/usr/local/include/mongocxx/v_noabi" - "/usr/local/include/bsoncxx/v_noabi" - "/usr/local/include/libmongoc-1.0" - "/usr/local/include/libbson-1.0" - "/usr/local/lib" - ) + ${BLAS_INCLUDE_DIRS} + ) target_compile_options(pea PRIVATE -fpie) @@ -218,106 +190,134 @@ target_compile_definitions(pea PRIVATE EIGEN_USE_BLAS=1 ) +if(ENABLE_MONGODB) + target_compile_definitions(pea PRIVATE ENABLE_MONGODB=1) + target_sources(pea PRIVATE + common/mongo.cpp + common/mongoRead.cpp + common/mongoWrite.cpp + ) +endif() + #================================================== # Ocean tide loading - -add_library(otl - STATIC - loading/loading.cpp - loading/tide.cpp - loading/utils.cpp - loading/load_functions.cpp - loading/loadgrid.cpp - ) - -add_executable(make_otl_blq - loading/make_otl_blq.cpp - ) - - -add_executable(interpolate_loading - loading/interpolate_loading.cpp - ) - -target_include_directories(otl PUBLIC - common - loading - ${YAML_INCLUDE_DIRS} - ${Boost_INCLUDE_DIRS} - ${NETCDF_INCLUDES} - ${NETCDF_INCLUDES_CXX} - ) - -target_include_directories(make_otl_blq PUBLIC - loading - ${YAML_INCLUDE_DIRS} - ${Boost_INCLUDE_DIRS} - ${NETCDF_INCLUDES} - ${NETCDF_INCLUDES_CXX} - ) - -target_include_directories(interpolate_loading PUBLIC - loading - ${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} - ) - - -if(OpenMP_CXX_FOUND) - target_link_libraries(make_otl_blq PUBLIC OpenMP::OpenMP_CXX) - target_link_libraries(interpolate_loading PUBLIC OpenMP::OpenMP_CXX) +if (NetCDF_FOUND) + add_library(otl + STATIC + loading/loading.cpp + loading/tide.cpp + loading/utils.cpp + loading/load_functions.cpp + loading/loadgrid.cpp + ) + + add_executable(make_otl_blq + loading/make_otl_blq.cpp + ) + + + add_executable(interpolate_loading + loading/interpolate_loading.cpp + ) + + 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} + ) + + + if(OpenMP_CXX_FOUND) + target_link_libraries(make_otl_blq PUBLIC OpenMP::OpenMP_CXX) + target_link_libraries(interpolate_loading PUBLIC OpenMP::OpenMP_CXX) + endif() endif() - if(ENABLE_PARALLELISATION) target_compile_definitions(pea PRIVATE ENABLE_PARALLELISATION=1) endif() + target_link_libraries(pea PUBLIC - m - pthread - # openblasp sofa_lib ${Boost_LIBRARIES} ${BLAS_LIBRARIES} ${LAPACK_LIBRARIES} ${YAML_CPP_LIBRARIES} ${YAML_CPP_LIB} - mongo::mongocxx_shared - ${BLAS_LIBRARY_DIRS} - ${OPENSSL_LIBRARY_DIRS} - ${OPENSSL_LIBRARIES} - dl - ncurses - ) + OpenSSL::SSL + OpenSSL::Crypto + ) + +# Platform-specific libraries +if(UNIX) + target_link_libraries(pea PUBLIC m pthread dl) +elseif(WIN32) + target_link_libraries(pea PUBLIC ws2_32 wsock32) +endif() + +if(ENABLE_MONGODB) + # Prefer static linking, fall back to shared if static not available + if(TARGET mongo::mongocxx_static) + target_link_libraries(pea PUBLIC mongo::mongocxx_static) + message(STATUS "Linking MongoDB statically") + elseif(TARGET mongo::mongocxx_shared) + target_link_libraries(pea PUBLIC mongo::mongocxx_shared) + message(STATUS "Linking MongoDB dynamically") + else() + message(WARNING "MongoDB targets not found") + endif() +endif() + +# Add gfortran if it exists (needed for some BLAS/LAPACK implementations) +find_library(GFORTRAN_LIB gfortran) +if(GFORTRAN_LIB) + target_link_libraries(pea PUBLIC ${GFORTRAN_LIB}) +endif() set_property(TARGET pea PROPERTY POSITION_INDEPENDENT_CODE FALSE) @@ -333,5 +333,3 @@ add_dependencies(peas docs ) endif() - - diff --git a/src/cpp/ambres/GNSSambres.cpp b/src/cpp/ambres/GNSSambres.cpp index ec027c820..c7f3762ce 100644 --- a/src/cpp/ambres/GNSSambres.cpp +++ b/src/cpp/ambres/GNSSambres.cpp @@ -1,277 +1,299 @@ - -#include "GNSSambres.hpp" -#include "testUtils.hpp" - +#include "ambres/GNSSambres.hpp" #include -#define LOG_PI 1.14472988584940017 -#define SQRT2 1.41421356237309510 -#define AMB_RANG 10 +#define LOG_PI 1.14472988584940017 +#define SQRT2 1.41421356237309510 +#define AMB_RANG 10 -bool AR_VERBO = false; -double FIXED_AMB_VAR = 1e-8; +bool AR_VERBO = false; +double FIXED_AMB_VAR = 1e-8; /** Probability of error (assuming normal distribution) */ double round_perr( - double dx, ///< Distance between value and mean - double var) ///< Variance + double dx, ///< Distance between value and mean + double var ///< Variance +) { - if (var < 1e-20) - return 0; + if (var < 1e-20) + return 0; - double p0 = 0; - double fact = -0.25 / var; + double p0 = 0; + double fact = -0.25 / var; - for (int i = 1; i < AMB_RANG; i++) - { - p0 += exp((i + 2 * dx) * i * fact); - p0 += exp((i - 2 * dx) * i * fact); - } + for (int i = 1; i < AMB_RANG; i++) + { + p0 += exp((i + 2 * dx) * i * fact); + p0 += exp((i - 2 * dx) * i * fact); + } - return p0 / (p0 + 1); + return p0 / (p0 + 1); } /** Simple integer Rounding */ int simple_round( - Trace& trace, ///< Debug trace - GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance - GinAR_opt opt) ///< Object containing processing options + Trace& trace, ///< Debug trace + GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance + GinAR_opt opt ///< Object containing processing options +) { - MatrixXd P = mtrx.Paflt; - VectorXd ret = mtrx.aflt; - int namb = ret.size(), nfix = 0; + MatrixXd P = mtrx.Paflt; + VectorXd ret = mtrx.aflt; + int namb = ret.size(), nfix = 0; - mtrx.Ztrs.resize(0, 0); - mtrx.zfix.resize(0); + mtrx.Ztrs.resize(0, 0); + mtrx.zfix.resize(0); - if (namb <= 0) - return 0; + if (namb <= 0) + return 0; - double ratthr = 1 / (opt.ratthr + 1); - double sucthr = 1 - pow(opt.sucthr, 1.0 / namb); - tracepdeex(4, trace, "\n#ARES_RND Using integer rounding ... %.4e %.4f", sucthr, ratthr); + double ratthr = 1 / (opt.ratthr + 1); + double sucthr = 1 - pow(opt.sucthr, 1.0 / namb); + tracepdeex(4, trace, "\n#ARES_RND Using integer rounding ... %.4e %.4f", sucthr, ratthr); - vector zind; - vector xind; - xind.reserve(namb); + vector zind; + vector xind; + xind.reserve(namb); - for (int i = 0; i < namb; i++) - { - xind.push_back(i); - double dv = ret(i) - ROUND(ret(i)); - double perr = round_perr (dv, P(i, i)); + for (int i = 0; i < namb; i++) + { + xind.push_back(i); + double dv = ret(i) - ROUND(ret(i)); + double perr = round_perr(dv, P(i, i)); - if (fabs(dv) < ratthr && perr < sucthr) - { - ret(i) = ret(i) - dv; - nfix++; - zind.push_back(i); - } - } + if (fabs(dv) < ratthr && perr < sucthr) + { + ret(i) = ret(i) - dv; + nfix++; + zind.push_back(i); + } + } - MatrixXd Z = MatrixXd::Identity(namb, namb); + MatrixXd Z = MatrixXd::Identity(namb, namb); - if (nfix == 0) - return 0; + if (nfix == 0) + return 0; - mtrx.Ztrs = Z(zind, xind); - mtrx.zfix = ret(zind); + mtrx.Ztrs = Z(zind, xind); + mtrx.zfix = ret(zind); - return nfix; + return nfix; } /** Iterative Rounding */ int interat_round( - Trace& trace, ///< Debug trace - GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance - GinAR_opt& opt) ///< Object containing processing options + Trace& trace, ///< Debug trace + GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance + GinAR_opt& opt ///< Object containing processing options +) { - MatrixXd P = mtrx.Paflt; - VectorXd x = mtrx.aflt; - int namb = x.size(); + MatrixXd P = mtrx.Paflt; + VectorXd x = mtrx.aflt; + int namb = x.size(); - mtrx.Ztrs.resize(0, 0); - mtrx.zfix.resize(0); + mtrx.Ztrs.resize(0, 0); + mtrx.zfix.resize(0); - if (namb <= 0) - return 0; + if (namb <= 0) + return 0; - double sucthr = 1 - pow(opt.sucthr, 1.0 / namb); - double ratthr = 1 / (opt.ratthr + 1); + double sucthr = 1 - pow(opt.sucthr, 1.0 / namb); + double ratthr = 1 / (opt.ratthr + 1); - vector zind; - vector xind; - xind.reserve(namb); + vector zind; + vector xind; + xind.reserve(namb); - for (int i = 0; i < namb; i++) - xind.push_back(i); + for (int i = 0; i < namb; i++) + xind.push_back(i); - MatrixXd I = MatrixXd::Identity(namb, namb); - MatrixXd Ztrs; - VectorXd xfix; - VectorXd dvvct = VectorXd::Zero(namb); + MatrixXd I = MatrixXd::Identity(namb, namb); + MatrixXd Ztrs; + VectorXd xfix; + VectorXd dvvct = VectorXd::Zero(namb); - int nfix = 0; - int nnew = 0; + int nfix = 0; + int nnew = 0; - for (int iter = 0; iter < opt.nitr; iter++) - { - zind.clear(); - nnew = 0; + for (int iter = 0; iter < opt.nitr; iter++) + { + zind.clear(); + nnew = 0; - for (int i = 0; i < namb; i++) - { - double dv = x(i) - ROUND(x(i)); - double perr = round_perr (dv, P(i, i)); + for (int i = 0; i < namb; i++) + { + double dv = x(i) - ROUND(x(i)); + double perr = round_perr(dv, P(i, i)); - if ((fabs(dv) < ratthr) && (perr < sucthr)) - { - nnew++; - zind.push_back(i); - dvvct(i) = dv; - } - } + if ((fabs(dv) < ratthr) && (perr < sucthr)) + { + nnew++; + zind.push_back(i); + dvvct(i) = dv; + } + } - if (nnew <= nfix) break; + if (nnew <= nfix) + break; - VectorXd dx = dvvct(zind); + VectorXd dx = dvvct(zind); - Ztrs = I(zind, xind); - xfix = x(zind) - dx; + Ztrs = I(zind, xind); + xfix = x(zind) - dx; - MatrixXd Psel = P(zind, zind) + (FIXED_AMB_VAR * I(zind, zind)); - MatrixXd K = P(xind, zind) * Psel.inverse(); - MatrixXd S = K * P(zind, xind); - x = x - K * dx; - P = P - S; - nfix = nnew; - } + MatrixXd Psel = P(zind, zind) + (FIXED_AMB_VAR * I(zind, zind)); + MatrixXd K = P(xind, zind) * Psel.inverse(); + MatrixXd S = K * P(zind, xind); + x = x - K * dx; + P = P - S; + nfix = nnew; + } - if (nfix == 0) - return 0; + if (nfix == 0) + return 0; - mtrx.Ztrs = Ztrs; - mtrx.zfix = xfix; + mtrx.Ztrs = Ztrs; + mtrx.zfix = xfix; - return nfix; + return nfix; } - bool LTDL_factorization( - GinAR_mtx& mtrx) ///< Reference to structure containing float values and covariance + GinAR_mtx& mtrx ///< Reference to structure containing float values and covariance +) { - int n = mtrx.aflt.size(); - MatrixXd P = mtrx.Paflt; - - MatrixXd L = MatrixXd::Zero(n,n); - VectorXd D = VectorXd::Zero(n); - - for (int i=n-1; i>=0; i--) - { - if (P(i,i)<=0) - return false; - - D(i) = P(i,i); - double a=sqrt(P(i,i)); - L.block(i, 0, 1, i+1) = P.block(i, 0, 1, i+1)/a; - for (int j=0;j= 0; i--) + { + if (P(i, i) <= 0) + return false; + + D(i) = P(i, i); + double a = sqrt(P(i, i)); + L.block(i, 0, 1, i + 1) = P.block(i, 0, 1, i + 1) / a; + for (int j = 0; j < i; j++) + P.block(j, 0, 1, j + 1) -= L(i, j) * L.block(i, 0, 1, j + 1); + L.block(i, 0, 1, i + 1) /= L(i, i); + } + + mtrx.Ltrs = L; + mtrx.Dtrs = D; + return true; } /** Lambda decorrelation (trough Z transform) */ int Ztrans_reduction( - Trace& trace, ///< Debug trace - GinAR_mtx& mtrx) ///< Reference to structure containing float values and covariance + Trace& trace, ///< Debug trace + GinAR_mtx& mtrx ///< Reference to structure containing float values and covariance +) { - int n = mtrx.aflt.size(); - if (n<1) - return -1; - - if (LTDL_factorization(mtrx) == false) - { - tracepdeex(1, trace, "WARNING: LD decomposition error, ambiguity matrix may not positive definite\n"); - return -1; - } - - VectorXd x = mtrx.aflt; - VectorXd D = mtrx.Dtrs; - MatrixXd L = mtrx.Ltrs; - MatrixXd Z = MatrixXd::Identity(n, n); - - if (AR_VERBO) - { - trace << std::setprecision(8); - trace << "\n" << "x =" << "\n" << x.transpose() << "\n"; - trace << "\n" << "Px=" << "\n" << mtrx.Paflt << "\n"; - trace << "\n" << "Lx=" << "\n" << L << "\n"; - trace << "\n" << "Dx=" << "\n" << D.transpose() << "\n"; - } - - int k=n-2; - int j=n-2; - while (j>=0) - { - if (j<=k) - for (int i=j+1; i= 0) + { + if (j <= k) + for (int i = j + 1; i < n; i++) + { + double mu = ROUND(L(i, j)); + if (mu != 0) + { + L.col(j) -= mu * L.col(i); + Z.col(j) -= mu * Z.col(i); + } + } + + double del = D(j) + L(j + 1, j) * L(j + 1, j) * D(j + 1); + if ((del + 1E-6) < D(j + 1)) + { + double eta = D(j) / del; + double lam = D(j + 1) * L(j + 1, j) / del; + + D(j) = eta * D(j + 1); + D(j + 1) = del; + + MatrixXd a0 = L.block(j, 0, 1, j); + MatrixXd a1 = L.block(j + 1, 0, 1, j); + L.block(j, 0, 1, j) = a1 - L(j + 1, j) * a0; + L.block(j + 1, 0, 1, j) = lam * a1 + eta * a0; + L(j + 1, j) = lam; + + VectorXd Ltmp = L.block(j + 2, j, n - j - 2, 1); + L.block(j + 2, j, n - j - 2, 1) = L.block(j + 2, j + 1, n - j - 2, 1); + L.block(j + 2, j + 1, n - j - 2, 1) = Ltmp; + + VectorXd Ztmp = Z.col(j); + Z.col(j) = Z.col(j + 1); + Z.col(j + 1) = Ztmp; + + k = j; + j = n - 2; + } + else + j--; + } + + mtrx.Ztrs = Z.transpose(); + mtrx.zflt = mtrx.Ztrs * x; + mtrx.Ltrs = L; + mtrx.Dtrs = D; + + if (AR_VERBO) + { + trace << std::setprecision(8); + trace << "\n" + << "z =" << "\n" + << mtrx.zflt.transpose() << "\n"; + trace << "\n" + << "Zt=" << "\n" + << mtrx.Ztrs << "\n"; + trace << "\n" + << "Lz=" << "\n" + << L << "\n"; + trace << "\n" + << "Dz=" << "\n" + << D.transpose() << "\n"; + } + + return n; } /** Integer bootstrapping */ @@ -285,8 +307,6 @@ int Ztrans_reduction( // if (info < 0) // return 0; - - // GinAR_mtx mtrx2; // mtrx2.aflt = mtrx.zflt; @@ -302,305 +322,335 @@ int Ztrans_reduction( // } int integer_bootst( - Trace& trace, ///< Debug trace - GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance - GinAR_opt& opt) ///< Object containing processing options + Trace& trace, ///< Debug trace + GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance + GinAR_opt& opt ///< Object containing processing options +) { - LDLT ldlt_; - ldlt_.compute(mtrx.Paflt); - - if (ldlt_.isPositive() == false) - { - tracepdeex(1, trace, "WARNING: LD decomposition error, ambiguity matrix may not positive definite\n"); - return 0; - } - - MatrixXd L_ = ldlt_.matrixL(); - auto tr = ldlt_.transpositionsP (); - - int siz = mtrx.aflt.size(); - MatrixXd I0 = MatrixXd::Identity(siz, siz); - MatrixXd Zt = tr * I0; - VectorXd z_ = tr * mtrx.aflt; - - for (int j = siz - 2; j >= 0; j--) - { - for (int i = j + 1; i < siz; i++) - { - double mu = ROUND(L_(i, j)); - - if (mu != 0) - { - L_.row(i) -= mu * L_.row(j); - Zt.row(i) -= mu * Zt.row(j); - z_ (i) -= mu * z_ (j); - } - } - } - - MatrixXd Pz = Zt*mtrx.Paflt*Zt.transpose(); - - GinAR_mtx mtrx2; - mtrx2.aflt = z_; - mtrx2.Paflt = Pz; - - if (AR_VERBO) - { - trace << "\n" << "x_=" << "\n" << mtrx.aflt.transpose() << "\n"; - trace << "\n" << "Px=" << "\n" << mtrx.Paflt << "\n"; - trace << "\n" << "Zt=" << "\n" << Zt << "\n"; - trace << "\n" << "z_=" << "\n" << z_.transpose() << "\n"; - trace << "\n" << "Pz=" << "\n" << Pz << "\n"; - } - - int nfix = interat_round(trace, mtrx2, opt); - - mtrx.Ztrs = mtrx2.Ztrs * Zt; - mtrx.zfix = mtrx2.zfix; - - return nfix; + LDLT ldlt_; + ldlt_.compute(mtrx.Paflt); + + if (ldlt_.isPositive() == false) + { + tracepdeex( + 1, + trace, + "WARNING: LD decomposition error, ambiguity matrix may not positive definite\n" + ); + return 0; + } + + MatrixXd L_ = ldlt_.matrixL(); + auto tr = ldlt_.transpositionsP(); + + int siz = mtrx.aflt.size(); + MatrixXd I0 = MatrixXd::Identity(siz, siz); + MatrixXd Zt = tr * I0; + VectorXd z_ = tr * mtrx.aflt; + + for (int j = siz - 2; j >= 0; j--) + { + for (int i = j + 1; i < siz; i++) + { + double mu = ROUND(L_(i, j)); + + if (mu != 0) + { + L_.row(i) -= mu * L_.row(j); + Zt.row(i) -= mu * Zt.row(j); + z_(i) -= mu * z_(j); + } + } + } + + MatrixXd Pz = Zt * mtrx.Paflt * Zt.transpose(); + + GinAR_mtx mtrx2; + mtrx2.aflt = z_; + mtrx2.Paflt = Pz; + + if (AR_VERBO) + { + trace << "\n" + << "x_=" << "\n" + << mtrx.aflt.transpose() << "\n"; + trace << "\n" + << "Px=" << "\n" + << mtrx.Paflt << "\n"; + trace << "\n" + << "Zt=" << "\n" + << Zt << "\n"; + trace << "\n" + << "z_=" << "\n" + << z_.transpose() << "\n"; + trace << "\n" + << "Pz=" << "\n" + << Pz << "\n"; + } + + int nfix = interat_round(trace, mtrx2, opt); + + mtrx.Ztrs = mtrx2.Ztrs * Zt; + mtrx.zfix = mtrx2.zfix; + + return nfix; } /** Lambda algorithm and its variations (ILQ, Common set, BIE) */ int lambda_search( - Trace& trace, ///< Debug trace - GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance - GinAR_opt opt) ///< Object containing processing options + Trace& trace, ///< Debug trace + GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance + GinAR_opt opt ///< Object containing processing options +) { - int info = Ztrans_reduction(trace, mtrx); - - if (info < 0) - { - tracepdeex(2, trace, "\n Matrix decorrelation failed ... "); - return 0; - } - - - int nmax = mtrx.Dtrs.size(); - int k = nmax - 1; - int kmax = k; - - double succ = erf(sqrt(1 / (8 * mtrx.Dtrs(k--)))); - - if (succ < opt.sucthr) - return 0; - - int zsiz = 1; - - while (k >= 0) - { - succ *= erf(sqrt(1 / (8 * mtrx.Dtrs(k--)))); - if (succ < opt.sucthr) - break; - zsiz++; - } - if(zsiz<3) - return 0; - - int kmin=kmax-zsiz+1; - - map zfixList; - - MatrixXd L = mtrx.Ltrs; - VectorXd D = mtrx.Dtrs; - VectorXd zflt = mtrx.zflt; - - VectorXd dist = VectorXd::Zero(nmax); - VectorXd zadj = VectorXd::Zero(nmax); - VectorXd zfix = VectorXd::Zero(nmax); - VectorXd zdif = VectorXd::Zero(nmax); - VectorXd step = VectorXd::Zero(nmax); - - k = kmax; - zadj(k) = zflt(k); - zfix(k) = ROUND(zadj(k)); - zdif(k) = zadj(k) - zfix(k); - step(k) = zdif(k) < 0 ? -1 : 1; - bool search = true; - double maxdist = 1e99; - int ncand = 0; - - while (search) - { - double newdist = dist(k) + zdif(k) * zdif(k) / D(k); - - if (newdist < maxdist) - { - if (k != kmin) - { - k--; - dist(k) = newdist; - - zadj(k) = zflt(k); - for(int j = k+1; j 1 - && maxd < maxdist) - maxdist = maxd; - - if(ncand > opt.nset) - break; - - if (opt.nset>0 - && (ncand >= opt.nset)) - { - int ntot = 0; - for (auto it = zfixList.begin(); it != zfixList.end(); ) - { - if (ntot++ >= opt.nset) - it = zfixList.erase(it); - else - { - maxd = it->first; - ++it; - } - } - - if(maxd < maxdist) - maxdist = maxd; - - ncand = zfixList.size(); - } - - zfix(kmin)+= step(kmin); - zdif(kmin) = zadj(kmin) - zfix(kmin); - step(kmin) =-step(kmin) + (step(kmin) < 0 ? 1 : -1); - } - } - else - { - if (k == kmax) break; - else - { - k++; - zfix(k)+= step(k); - zdif(k) = zadj(k) - zfix(k); - step(k) =-step(k) + (step(k) < 0 ? 1 : -1); - } - } - } - - if (zfixList.size() < 1) - return 0; - - double mindist = zfixList.begin()->first; - VectorXd zfix0 = zfixList.begin()->second; - mtrx.zfix = zfix0; - MatrixXd Z = mtrx.Ztrs.bottomRows(zsiz); - mtrx.Ztrs = Z; - - switch (opt.mode) - { - case E_ARmode::LAMBDA: - return zfix0.size(); - - case E_ARmode::LAMBDA_ALT: - { - double first = 0; - double second = 0; - for (auto& [dis, fixvec] : zfixList) - { - if (first == 0) first = dis; - else if (second == 0) second = dis; - else break; - } - - if ((second/first) < opt.ratthr) return 0; - else return zfix0.size(); - } - - case E_ARmode::LAMBDA_AL2: - { - for (auto& [dis, fixvec] : zfixList) - { - if ((dis / mindist) > opt.ratthr) - break; - - for (int l = 0; l < zfix0.size(); l++) - { - if (zfix0(l) == -99999.5) - continue; - - if (zfix0(l) != fixvec(l)) - zfix0(l) = -99999.5; - } - } - - vector zind; - for (int k = 0; k < zfix0.size(); k++) - if (zfix0(k) != -99999.5) - zind.push_back(k); - tracepdeex(2, trace, "... %d ambiguties in common\n", zind.size()); - - vector xind; - for (int k = 0; k < nmax; k++) - xind.push_back(k); - - mtrx.zfix = zfix0(zind); - mtrx.Ztrs = Z(zind, xind); - - return zind.size(); - } - - case E_ARmode::LAMBDA_BIE: - { - double acum = 0; - - for (auto& [dis, fixvec] : zfixList) - { - double fct = exp(-0.5 * (dis-mindist)); - acum += fct; - } - - VectorXd zbie = VectorXd::Zero(zsiz); - - for (auto& [dis, fixvec] : zfixList) - { - double fct = exp(-0.5 * (dis-mindist)) / acum; - if (AR_VERBO) - trace << "\n" << "BIE Candidate found:" << fixvec.transpose() << "; dist= " << dis << "; fact= " << fct; - zbie += fct * fixvec; - } - - mtrx.zfix = zbie; - - return zbie.size(); - } - } - - return 0; + int info = Ztrans_reduction(trace, mtrx); + + if (info < 0) + { + tracepdeex(2, trace, "\n Matrix decorrelation failed ... "); + return 0; + } + + int nmax = mtrx.Dtrs.size(); + int k = nmax - 1; + int kmax = k; + + double succ = erf(sqrt(1 / (8 * mtrx.Dtrs(k--)))); + + if (succ < opt.sucthr) + return 0; + + int zsiz = 1; + + while (k >= 0) + { + succ *= erf(sqrt(1 / (8 * mtrx.Dtrs(k--)))); + if (succ < opt.sucthr) + break; + zsiz++; + } + if (zsiz < 3) + return 0; + + int kmin = kmax - zsiz + 1; + + map zfixList; + + MatrixXd L = mtrx.Ltrs; + VectorXd D = mtrx.Dtrs; + VectorXd zflt = mtrx.zflt; + + VectorXd dist = VectorXd::Zero(nmax); + VectorXd zadj = VectorXd::Zero(nmax); + VectorXd zfix = VectorXd::Zero(nmax); + VectorXd zdif = VectorXd::Zero(nmax); + VectorXd step = VectorXd::Zero(nmax); + + k = kmax; + zadj(k) = zflt(k); + zfix(k) = ROUND(zadj(k)); + zdif(k) = zadj(k) - zfix(k); + step(k) = zdif(k) < 0 ? -1 : 1; + bool search = true; + double maxdist = 1e99; + int ncand = 0; + + while (search) + { + double newdist = dist(k) + zdif(k) * zdif(k) / D(k); + + if (newdist < maxdist) + { + if (k != kmin) + { + k--; + dist(k) = newdist; + + zadj(k) = zflt(k); + for (int j = k + 1; j < nmax; j++) + zadj(k) -= zdif(j) * L(j, k); + + zfix(k) = ROUND(zadj(k)); + zdif(k) = zadj(k) - zfix(k); + step(k) = zdif(k) < 0 ? -1 : 1; + } + else + { + VectorXd zcut = zfix.tail(zsiz); + zfixList[newdist] = zcut; + ncand = zfixList.size(); + double maxd = newdist * opt.ratthr; + + if (ncand > 1 && maxd < maxdist) + maxdist = maxd; + + if (ncand > opt.nset) + break; + + if (opt.nset > 0 && (ncand >= opt.nset)) + { + int ntot = 0; + for (auto it = zfixList.begin(); it != zfixList.end();) + { + if (ntot++ >= opt.nset) + it = zfixList.erase(it); + else + { + maxd = it->first; + ++it; + } + } + + if (maxd < maxdist) + maxdist = maxd; + + ncand = zfixList.size(); + } + + zfix(kmin) += step(kmin); + zdif(kmin) = zadj(kmin) - zfix(kmin); + step(kmin) = -step(kmin) + (step(kmin) < 0 ? 1 : -1); + } + } + else + { + if (k == kmax) + break; + else + { + k++; + zfix(k) += step(k); + zdif(k) = zadj(k) - zfix(k); + step(k) = -step(k) + (step(k) < 0 ? 1 : -1); + } + } + } + + if (zfixList.size() < 1) + return 0; + + double mindist = zfixList.begin()->first; + VectorXd zfix0 = zfixList.begin()->second; + mtrx.zfix = zfix0; + MatrixXd Z = mtrx.Ztrs.bottomRows(zsiz); + mtrx.Ztrs = Z; + + switch (opt.mode) + { + case E_ARmode::LAMBDA: + return zfix0.size(); + + case E_ARmode::LAMBDA_ALT: + { + double first = 0; + double second = 0; + for (auto& [dis, fixvec] : zfixList) + { + if (first == 0) + first = dis; + else if (second == 0) + second = dis; + else + break; + } + + if ((second / first) < opt.ratthr) + return 0; + else + return zfix0.size(); + } + + case E_ARmode::LAMBDA_AL2: + { + for (auto& [dis, fixvec] : zfixList) + { + if ((dis / mindist) > opt.ratthr) + break; + + for (int l = 0; l < zfix0.size(); l++) + { + if (zfix0(l) == -99999.5) + continue; + + if (zfix0(l) != fixvec(l)) + zfix0(l) = -99999.5; + } + } + + vector zind; + for (int k = 0; k < zfix0.size(); k++) + if (zfix0(k) != -99999.5) + zind.push_back(k); + tracepdeex(2, trace, "... %d ambiguties in common\n", zind.size()); + + vector xind; + for (int k = 0; k < nmax; k++) + xind.push_back(k); + + mtrx.zfix = zfix0(zind); + mtrx.Ztrs = Z(zind, xind); + + return zind.size(); + } + + case E_ARmode::LAMBDA_BIE: + { + double acum = 0; + + for (auto& [dis, fixvec] : zfixList) + { + double fct = exp(-0.5 * (dis - mindist)); + acum += fct; + } + + VectorXd zbie = VectorXd::Zero(zsiz); + + for (auto& [dis, fixvec] : zfixList) + { + double fct = exp(-0.5 * (dis - mindist)) / acum; + if (AR_VERBO) + trace << "\n" + << "BIE Candidate found:" << fixvec.transpose() << "; dist= " << dis + << "; fact= " << fct; + zbie += fct * fixvec; + } + + mtrx.zfix = zbie; + + return zbie.size(); + } + } + + return 0; } /** Ambiguity resolution function for Ginan */ int GNSS_AR( - Trace& trace, ///< Debug trace - GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance - GinAR_opt opt) ///< Object containing processing options + Trace& trace, ///< Debug trace + GinAR_mtx& mtrx, ///< Reference to structure containing float values and covariance + GinAR_opt opt ///< Object containing processing options +) { - switch (opt.mode) - { - case E_ARmode::OFF: return 0; - case E_ARmode::ROUND: return simple_round (trace, mtrx, opt); - case E_ARmode::ITER_RND: return interat_round (trace, mtrx, opt); - case E_ARmode::BOOTST: return integer_bootst (trace, mtrx, opt); - case E_ARmode::LAMBDA: return lambda_search (trace, mtrx, opt); - case E_ARmode::LAMBDA_ALT: return lambda_search (trace, mtrx, opt); - case E_ARmode::LAMBDA_AL2: return lambda_search (trace, mtrx, opt); - case E_ARmode::LAMBDA_BIE: return lambda_search (trace, mtrx, opt); - // default: tracepdeex(1, trace, "\n AR mode not supported \n"); - } - - return 0; + switch (opt.mode) + { + case E_ARmode::OFF: + return 0; + case E_ARmode::ROUND: + return simple_round(trace, mtrx, opt); + case E_ARmode::ITER_RND: + return interat_round(trace, mtrx, opt); + case E_ARmode::BOOTST: + return integer_bootst(trace, mtrx, opt); + case E_ARmode::LAMBDA: + return lambda_search(trace, mtrx, opt); + case E_ARmode::LAMBDA_ALT: + return lambda_search(trace, mtrx, opt); + case E_ARmode::LAMBDA_AL2: + return lambda_search(trace, mtrx, opt); + case E_ARmode::LAMBDA_BIE: + return lambda_search(trace, mtrx, opt); + // default: tracepdeex(1, trace, "\n AR mode not supported \n"); + } + + return 0; } diff --git a/src/cpp/ambres/GNSSambres.hpp b/src/cpp/ambres/GNSSambres.hpp index 8040c44f9..99a8568ee 100644 --- a/src/cpp/ambres/GNSSambres.hpp +++ b/src/cpp/ambres/GNSSambres.hpp @@ -1,57 +1,55 @@ - #pragma once -#include "eigenIncluder.hpp" -#include "observations.hpp" -#include "constants.hpp" -#include "receiver.hpp" -#include "algebra.hpp" -#include "satSys.hpp" -#include "common.hpp" -#include "trace.hpp" +#include "common/algebra.hpp" +#include "common/common.hpp" +#include "common/constants.hpp" +#include "common/eigenIncluder.hpp" +#include "common/observations.hpp" +#include "common/receiver.hpp" +#include "common/satSys.hpp" +#include "common/trace.hpp" -extern double FIXED_AMB_VAR; -extern bool AR_VERBO; +extern double FIXED_AMB_VAR; +extern bool AR_VERBO; struct GinAR_mtx { - map ambmap; - VectorXd aflt; - MatrixXd Paflt; + map ambmap; + VectorXd aflt; + MatrixXd Paflt; - MatrixXd Ztrs; - MatrixXd Ltrs; - VectorXd Dtrs; + MatrixXd Ztrs; + MatrixXd Ltrs; + VectorXd Dtrs; - VectorXd zflt; - VectorXd zfix; + VectorXd zflt; + VectorXd zfix; - VectorXd afix; - MatrixXd Pafix; + VectorXd afix; + MatrixXd Pafix; }; struct GinAR_opt { - string recv; - map sys_solve; - - bool endu = false; - int mode = E_ARmode::OFF; /* AR mode */ + string recv; + map sys_solve; - int nset = 0; /* candidate set size for lambda */ - int nitr = 3; /* number of iterations for iter_rnd */ + bool endu = false; + E_ARmode mode = E_ARmode::OFF; /* AR mode */ - double MIN_Elev_prc = D2R * 10; /* min elevation for processing */ - double MIN_Elev_AR = D2R * 15; /* min elevation for AR */ - double MIN_Elev_piv = D2R * 20; /* min elevation for pivot */ + int nset = 0; /* candidate set size for lambda */ + int nitr = 3; /* number of iterations for iter_rnd */ - double sucthr = 0.9999; /* success rate threshold */ - double ratthr = 3; /* ratio test threshold */ + double MIN_Elev_prc = D2R * 10; /* min elevation for processing */ + double MIN_Elev_AR = D2R * 15; /* min elevation for AR */ + double MIN_Elev_piv = D2R * 20; /* min elevation for pivot */ - bool clear_old_amb = false; - int Max_Hold_epc = 0; /* max hold (epoch) */ - double Max_Hold_tim = 600; /* max hold (seconds) */ + double sucthr = 0.9999; /* success rate threshold */ + double ratthr = 3; /* ratio test threshold */ + bool clear_old_amb = false; + int Max_Hold_epc = 0; /* max hold (epoch) */ + double Max_Hold_tim = 600; /* max hold (seconds) */ }; -int GNSS_AR(Trace& trace, GinAR_mtx& mtrx, GinAR_opt opt); +int GNSS_AR(Trace& trace, GinAR_mtx& mtrx, GinAR_opt opt); diff --git a/src/cpp/common/acsConfig.cpp b/src/cpp/common/acsConfig.cpp index a388a2ea4..f8f10d3a3 100644 --- a/src/cpp/common/acsConfig.cpp +++ b/src/cpp/common/acsConfig.cpp @@ -1,103 +1,102 @@ - // #pragma GCC optimize ("O0") +#include "common/acsConfig.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#define getpid _getpid +#endif +#include +#include +#include #include "architectureDocs.hpp" +#include "common/compare.hpp" +#include "common/constants.hpp" +#include "common/debug.hpp" +#include "configurator/htmlFooterTemplate.hpp" +#include "configurator/htmlHeaderTemplate.hpp" +#include "pea/inputsOutputs.hpp" +#include "pea/peaCommitStrings.hpp" + +using std::lock_guard; +using std::map; +using std::multimap; +using std::string; +using std::stringstream; +using std::tuple; +using std::unique_ptr; /** YAML based configuration. - * The Pea uses one or more yaml files as its source of configuration. YAML files are simple heirarchical documents which are straightforward to edit in basic text editors. + * The Pea uses one or more yaml files as its source of configuration. YAML files are simple + heirarchical documents which are straightforward to edit in basic text editors. * * - * The configuration parser performs many functions simultaneously, which may lead to issues if modifications are made without due care. - * In most cases, finding a similar configuration and duplicating all references to it will be the most straightforward method of creating new parameters. + * The configuration parser performs many functions simultaneously, which may lead to issues if + modifications are made without due care. + * In most cases, finding a similar configuration and duplicating all references to it will be the + most straightforward method of creating new parameters. * - * The lines in this file can become very long, because they do perform so many functions at once, however they should be aligned such that columns of similar parameters form naturally on the screen. - * This is preferable to more standard formatting because of the vast amount of duplication that would be required otherwise. The block-select tool in the IDE will come in handy. - * There are cases where artificial scoping is used to allow dummy variables to be used such that rather than copying one variable name into multiple function calls, which is always a source of bugs, - * a single common parameter name may be used across many unrelated lines. `thing` is one such variable name. + * The lines in this file can become very long, because they do perform so many functions at once, + however they should be aligned such that columns of similar parameters form naturally on the + screen. + * This is preferable to more standard formatting because of the vast amount of duplication that + would be required otherwise. The block-select tool in the IDE will come in handy. + * There are cases where artificial scoping is used to allow dummy variables to be used such that + rather than copying one variable name into multiple function calls, which is always a source of + bugs, + * a single common parameter name may be used across many unrelated lines. `thing` is one such + variable name. * */ Architecture Config__() { - DOCS_REFERENCE(Globbing_And_Tags__); - DOCS_REFERENCE(Aliases_And_Inheritance__); - DOCS_REFERENCE(Default_Values_And_Configurator__); + DOCS_REFERENCE(Globbing_And_Tags__); + DOCS_REFERENCE(Aliases_And_Inheritance__); + DOCS_REFERENCE(Default_Values_And_Configurator__); } /** Automatic expansion of configs. * */ -Architecture Globbing_And_Tags__() -{ +Architecture Globbing_And_Tags__() {} -} - -/** Some configuration structures are possible to be configured specific for eg a receiver or signal or combination of both. +/** Some configuration structures are possible to be configured specific for eg a receiver or signal + * or combination of both. * * - * Rather than creating multitudes of 'initialised' variables to correspond with config parameters, a map and pointer algebra is used to keep track of such things. - * Typically, the base container, the member variable, and a boolean to signify initialisation are included in a parsing line. + * Rather than creating multitudes of 'initialised' variables to correspond with config parameters, + * a map and pointer algebra is used to keep track of such things. Typically, the base container, + * the member variable, and a boolean to signify initialisation are included in a parsing line. * - * Inheritance is performed by using the overloaded += operator on an object, with any configurations that have been initialised in the secondary object overwriting those values in the first. + * Inheritance is performed by using the overloaded += operator on an object, with any + * configurations that have been initialised in the secondary object overwriting those values in the + * first. */ -Architecture Aliases_And_Inheritance__() -{ - -} +Architecture Aliases_And_Inheritance__() {} /** * - * Each configuration parameter is defined with a default initialisation value which is most sensible for the majority of use-cases. - * Parameters which are not assigned using the config will default to those values, allowing the size of yaml files to be minimised. + * Each configuration parameter is defined with a default initialisation value which is most + * sensible for the majority of use-cases. Parameters which are not assigned using the config will + * default to those values, allowing the size of yaml files to be minimised. * * While parsing the code */ -Architecture Default_Values_And_Configurator__() -{ - -} - - - -FileType YAML__() -{ - -} - - -#include -#include -#include -#include -#include -#include -#include -#include - -using std::stringstream; -using std::lock_guard; -using std::unique_ptr; -using std::multimap; -using std::string; -using std::tuple; -using std::map; - -#include -#include -#include -#include -#include -#include - -#include - -#include "interactiveTerminal.hpp" -#include "peaCommitStrings.hpp" -#include "inputsOutputs.hpp" -#include "constants.hpp" -#include "acsConfig.hpp" -#include "compare.hpp" -#include "debug.hpp" +Architecture Default_Values_And_Configurator__() {} +FileType YAML__() {} ACSConfig acsConfig = {}; @@ -105,8 +104,8 @@ ACSConfig acsConfig = {}; */ struct EnumDetails { - vector usingOptions; - vector enums; + vector usingOptions; + vector enums; }; map enumDetailsMap; @@ -114,3880 +113,8578 @@ map enumDetailsMap; typedef tuple NodeStack; /** Set value according to variable map entry if found -*/ -template + */ +template void tryGetValFromVM( - boost::program_options::variables_map& vm, ///< Variable map to search in - const string& key, ///< Variable name - TYPE& output) ///< Destination to set -{ - if (vm.count(key)) - { - output = vm[key].as(); - } + boost::program_options::variables_map& vm, ///< Variable map to search in + const string& key, ///< Variable name + TYPE& output ///< Destination to set +) +{ + if (vm.count(key)) + { + output = vm[key].as(); + } } -void conditionalPrefix( - string prefix, - string& path, - bool condition = true) -{ - if (condition == false) - { - return; - } - - if (path.empty()) { return; } - if (path.find(':') != string::npos) { return; } - - replaceString(prefix, "", std::filesystem::current_path()); - replaceString(path, "", std::filesystem::current_path()); - - char* home = std::getenv("HOME"); - if ( prefix[0] == '~' - &&home) - { - prefix.erase(0, 1); - prefix.insert(0, home); - } - - if ( path[0] == '~' - &&home) - { - path.erase(0, 1); - path.insert(0, home); - } - - if (std::filesystem::path(path).is_absolute()) - { - return; - } - - if (prefix.back() == '/') path = prefix + path; - else path = prefix + "/" + path; +void conditionalPrefix(string prefix, string& path, bool condition = true) +{ + if (condition == false) + { + return; + } + + if (path.empty()) + { + return; + } + if (path.find(':') != string::npos) + { + return; + } + + replaceString(prefix, "", std::filesystem::current_path().string()); + replaceString(path, "", std::filesystem::current_path().string()); + + char* home = std::getenv("HOME"); + if (prefix[0] == '~' && home) + { + prefix.erase(0, 1); + prefix.insert(0, home); + } + + if (path[0] == '~' && home) + { + path.erase(0, 1); + path.insert(0, home); + } + + if (std::filesystem::path(path).is_absolute()) + { + return; + } + + if (prefix.back() == '/') + path = prefix + path; + else + path = prefix + "/" + path; } -void conditionalPrefix( - const string& prefix, - vector& paths, - bool condition = true) -{ - for (auto& path : paths) - { - conditionalPrefix(prefix, path, condition); - } +void conditionalPrefix(const string& prefix, vector& paths, bool condition = true) +{ + for (auto& path : paths) + { + conditionalPrefix(prefix, path, condition); + } } void conditionalPrefix( - const string& prefix, - map>& paths, - bool condition = true) -{ - for (auto& [id, path] : paths) - { - conditionalPrefix(prefix, path, condition); - } + const string& prefix, + map>& paths, + bool condition = true +) +{ + for (auto& [id, path] : paths) + { + conditionalPrefix(prefix, path, condition); + } } /** search for and replace section of string -*/ + */ bool replaceString( - string& str, ///< String to search within - string subStr, ///< String to replace - string replacement, ///< Replacement string - bool warn) ///< Optional wanring about undefined values -{ - bool replaced = false; - while (true) - { - size_t index = str.find(subStr); - if (index == -1) - break; - - if ( replacement.empty() - && warn) - { - BOOST_LOG_TRIVIAL(warning) - << "Warning: " << subStr << " is used in config near " << str << " but is not defined..."; - } - - str.erase (index, subStr.size()); - - if (replacement.back() == '/') str.insert(index, replacement.substr(0, replacement.size() -1)); - else str.insert(index, replacement); - - replaced = true; - } - - return replaced; + string& str, ///< String to search within + string subStr, ///< String to replace + string replacement, ///< Replacement string + bool warn ///< Optional wanring about undefined values +) +{ + bool replaced = false; + while (true) + { + size_t index = str.find(subStr); + if (index == -1) + break; + + if (replacement.empty() && warn) + { + BOOST_LOG_TRIVIAL(warning) + << subStr << " is used in config near " << str << " but is not defined..."; + } + + str.erase(index, subStr.size()); + + if (!replacement.empty() && replacement.back() == '/') + str.insert(index, replacement.substr(0, replacement.size() - 1)); + else + str.insert(index, replacement); + + replaced = true; + } + + return replaced; +} + +static map userAliases; + +void getUserAliases(vector& aliasPairs) +{ + userAliases.clear(); + + for (auto& aliasPair : aliasPairs) + { + int colonPos = aliasPair.find(':'); + if (colonPos == string::npos) + { + BOOST_LOG_TRIVIAL(warning) + << "User defined alias '" << aliasPair << "' doesnt take form 'alias:value'"; + continue; + } + + string alias = "<" + aliasPair.substr(0, colonPos) + ">"; + string value = aliasPair.substr(colonPos + 1); + + boost::to_upper(alias); + + userAliases[alias] = value; + } } /** Replace macros for times with configured values. -*/ -void replaceTags( - string& str) ///< String to replace macros within -{ - char* home = std::getenv("HOME"); - - replaceString(str, "", acsConfig.sat_data_root); - replaceString(str, "", acsConfig.gnss_obs_root); - replaceString(str, "", acsConfig.pseudo_obs_root); - replaceString(str, "", acsConfig.rtcm_inputs_root); - replaceString(str, "", acsConfig.sisnet_inputs_root); - replaceString(str, "", acsConfig.root_stream_url); - replaceString(str, "", ginanCommitHash()); - replaceString(str, "", ginanBranchName()); - replaceString(str, "", acsConfig.analysis_agency); - replaceString(str, "", acsConfig.analysis_software.substr(0,3)); - replaceString(str, "", acsConfig.inputs_root); - replaceString(str, "", acsConfig.trace_directory); - replaceString(str, "", acsConfig.bias_sinex_directory); - replaceString(str, "", acsConfig.clocks_directory); - replaceString(str, "", acsConfig.decoded_rtcm_json_directory); - replaceString(str, "", acsConfig.encoded_rtcm_json_directory); - replaceString(str, "", acsConfig.erp_directory); - replaceString(str, "", acsConfig.ionex_directory); - replaceString(str, "", acsConfig.ionstec_directory); - replaceString(str, "", acsConfig.sinex_directory); - replaceString(str, "", acsConfig.log_directory); - replaceString(str, "", acsConfig.gpx_directory); - replaceString(str, "", acsConfig.pos_directory); - replaceString(str, "", acsConfig.ntrip_log_directory); - replaceString(str, "", acsConfig.network_statistics_json_directory); - replaceString(str, "", acsConfig.sp3_directory); - replaceString(str, "", acsConfig.orbit_ics_directory); - replaceString(str, "", acsConfig.orbex_directory); - replaceString(str, "", acsConfig.cost_directory); - replaceString(str, "", acsConfig.rinex_nav_directory); - replaceString(str, "", acsConfig.rinex_obs_directory); - replaceString(str, "", acsConfig.rtcm_nav_directory); - replaceString(str, "", acsConfig.rtcm_obs_directory); - replaceString(str, "", acsConfig.raw_custom_directory); - replaceString(str, "", acsConfig.raw_ubx_directory); - replaceString(str, "", acsConfig.slr_obs_directory); - replaceString(str, "", acsConfig.trop_sinex_directory); - replaceString(str, "", acsConfig.ems_directory); - replaceString(str, "", acsConfig.pppOpts.rts_directory); - replaceString(str, "", acsConfig.outputs_root); - replaceString(str, "", acsConfig.stream_user); - replaceString(str, "", acsConfig.stream_pass); - replaceString(str, "", acsConfig.config_description); - replaceString(str, "", std::filesystem::current_path()); - if (home) replaceString(str, "~", home); + */ +void replaceTags(string& str) ///< String to replace macros within +{ + string todayStr = "
  5. ' + f'' + f'{html_escape(item)} — {title} plot' + f'
  6. *r?{yoio0XeMqXkzR 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 diff --git a/Docs/images/UseCasesv04.jpg b/Docs/images/UseCasesv04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e920d017cd1b5bb19c37e3ecb7a53b8ef63bf8e9 GIT binary patch literal 193772 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 z zPxw^TVf*DIObocQ#tJOxXt0KQmO7Y(!(m@9*~iT@R&m&%>7b6p&d3Y(^=%)zf}3UWhjq``s&T-&&Nx8=7D0RZFsqa>G5Vh;Vh; zi(DJQpXbYBrizP=ry{Oa-rmxKuKnPIXXtTWn1m@zYk#NO_zn33FL-MPdE-RTZe(Ep znoi#~c;&LY0D~3s(;raSsU&_cfukG8^2CtGJW6n&fFQKB{a4!3u4^D-_&q^dYBfbd zU6T(O$7xW;4)X!?wJpngSD;pALhn?#*U{4ky|@)gFn{Ezt2(*72SL$6b9%SB^fio6(T?_Qp4Vjt z&U1*C-DjLa$t}A9>Cy6D$5JBt_V?IHWQAh01@R+yVyIl3@Yi;%<9R+n4Q404Sd^uB z1eKo0nU5Eb*X|rT5&;=h*KVD-{LmL6Yk6Tn1GGjEF%R9_i z2fyFAZ!wuiFkrmI639m^_aWLHxlwuuaEcv$w`0oNM)bAjBpp^XX;n;g_2nP=pGUQ0 zXUm{&NDPJtxc4-RwF|S^DgVIRA6m9Km+j7xZWHI0o3>%d&O;$r!<l)zVhTw1-fDBAU`y;&r}LuX0b&9Bpjh@bEz8NMH@`Sjpn{e#Ly8qSoFHD1ji7h} z>~$WcHYQg_2WfG!wO=3K{9?Fr3BZv0Q{)J8H=wy4@~)JMjW<3rQg`RQ#zPzTB}uJ0 zBuI*zFj}0dPUc1*@h?Sh9}L{#t3DX099VMJ|Hgh-YXi9F8{Y0Ay*yinbg96!D>msZ z+P!fE{ZuVypp(e7ijv(wG6h6&6hIVZ*ijI-C^vGDD_;qo9`*?G_D|3K6!kZMR`Rf>-~w$FbiF!KTX)&)gQHTeJ6qB7 z>8YJ^)r|g!G1x(>Gku!#4$#;LC90lKF|rs3AeC{@V`5V`BBg*eQq(7hJEKI0G&LxO z3`V_?^+*Dt(3B2rdj#lAn2M=~N3{^QMNe`_JCaAvlqMk9XQmAQRAVbG0$m2*M|~WC z@(yw4^Q7cmII$apUqCU!NmyVw2=$<{pR?)I7n7;*G2J3pQ>PG^QowvOC|*Af@q2jv zrl{udGq-!L&YCL|^>xy~@jm^t+OADhPPmjVk$CBI$G&6-H73-7n%YzfV+cw(n#{bt z1Nwaq66tJJ5b}n|^%(<^ps%m{=A-`!W>Wv7vm@&9s=r~-xfJG!I4nnJbK?r6HiZQe zu}i`zJ>jNS!&i3KADWTY=&9hbvhzHcK$R2w0e7Rc)`6wqIk=x{1R;a;DN)+-Wh0K> zc7gEqK0c2`?c2UB8UWzw)NMy6sO&l$0)J97^E&vC`$n3ZyCZM>BRN(XnR!a9;yrR+ z)?FbtT{H9ZjOx**4Yl)yjCX(IPU1EO!T)Gi7;M<6?=GZyjXcrYctY{VmG!(_gxMCK5(Y*Bu8_Nyowg3 zoE5pye!&~E(#UdpF{uCq3^Oh>BJ>S=9tb@UT@^k&?dADwo{+UhRB7UI^;35~zsSOF z>^Mj|YV#oYa${}B%bgMhW^ADzR$qQ~iIQvhH&yijPuzu?V>3Pk=Xp^zl`BX&&LI9U z!l29X)04m6twZ@WtqVUdd}?My6m2COb&VCa)<^%<%;*r@IG??CZ+K*c5zA9>gBlJd z5qnIZ5u|n?Kxt*Lv|3oo$ue8}8T@d&DyeZX_ia_AFob!Y8c7L}dyo7Q()}$l;HbvH zJhb!!chYO2-U}M(fY~;%(Od>IJGH0~$NP7ZVQHY0yEOut=k?J^mq|`9<14)GL+~s_%#HIIfI!i>dk3UkBrPQ$5Ev30Zh; z=Dy>m!^^RDK8B}}a9Zx05#F!{mYNk=>!(Vp^guJm|LeR&6^RF1z>E(ikSo#cUI$b0 zUBAA<@A~;&@?;U)7STn~{i;o%}#=AmH`Ej`1KgG8y^*n?)Qy9yE zSNsX0Yy(&&0ZeA|-9TCCbj#_7lH#?zauP27RC@ToCc+?J>KFI^(HL0N#CFLyanjxhuL^H z9+=uR#xsJWBl2D=I+|#jpn?#MUi8T6ec>K|pK+ieDf)q%?d(S5)J^oTH)|ttKh)ye z?lB=|Tt>NX=|x;%o_eTB){zFVnWzq9CF}oK;w~RiNab;Zo^^Pao$UMYCtY?Og1!A9 z_H)rb(&nJXw!SX9$JlAE=VH(;u`Aj)ZU4VDF9K)p|Lq<5lK&Nx1JA!>Jgq5%%e|8a z|Dq5)Uh5vnG%ed^o&KZ(SK_?g7uqmJsD#o+sy*w}x+$T}3k_tEB2Al@3$)P0*CRjg zxp}&a=1`cAhA8fm9d1G!7;eN6sOSX|QVix^bt%~SW9l{JZmZ?=EFj7%5 znQSncpV}-;$U=#BEFoX!BEU*VYGqlc&z$}W|S-S-`P zq9ddh4(uP}fp&GLXe|`nO-2m3zo~N;3`)u`SOa{smG~X^4)}E-@#b4s8guCr)b)p4 z6aQCOEre^4`nbM{T;3%R4>34W&A)!rY53F6#rH>=Tbm1RVV^RNU6W3@jEui{^@sN_ z2r}-7hmUSn3~;O6?1-f^*?sUa=o#EB0S135xm zqqvjl)_=%;uF!flPusQCetl|(1So}UrG^^O?3e5|pwchoKOuN3Pqfs5oo1(WutB{< zv1*AA#pLKKSvgsuAlWXJP*uRWfnhC*(Tl;xMq0C2`+559HLJqL$|+!0xYM0g18S; zg`iScVGfE(C?WMaGy%7aq!kAgSC~Hfk1kYlKzCj9@++7&nI{=VUqsdWK`x=ml9t{^ z_OEV)q3gLJzIh)n-lR#WYL{};@4g~t^vFbddDB$Lptl+kq%$YdY7J3uM6s&jeLunb zLIUmn&j+{GEshXjcx?4*i^^rQ+zTcZO}sT>0BOX^IHqQ=ZQ7QObN;dE-w*@6Xpw!9i2Bd}I z_(N^328rvdn&|zB1GmT16;ydZ71t4b>?#_iC=mO8=%2J`~cgl1s6uIY|GD&bBk8w3w#I|Rl!)YkeYD6lo4~!)6?zln`leRgaRM*o2e%ensw=BMKlEz6% z&RKFCSxBya0Lp!;k}jX$W~Qy{=25F|*Rj-S5FNxG4UqmcfzwOA!D8S|BJ9@k)rV^# z^|d;P1~iB=)J5QV*HCbMBmL^DL*}5*S9a3+sBC)XDY5@s_y7O(3+;T~j^^CL+(7eh zOuK@b(rH_Ut==W|A3REv-(HGZ;vFWsS&YLIJ_1y>tJM2s2@JtRU2GH~lbT20?@!sjp3D34Bzq=8wq-q^_k)Ei$iL1 zxWh}-JcdX2=b+6ePBaguXd|D8@}Stu*5{+WK^tgG;^FR+BTzbv6iUQ5BELcl4FCDj zZt}4@jgDJqx-?9bt`UeBh6N77{x=* z?N}6RO*Q>KIlcqMRcXA``~wfs;6DHjpG0Qv2UQy972K~oIPlRg;gQZ@y8X3f=q<1$ zI;7K7MLZ-ic5ei7JINT~z`_aVkKG7T!|S!xS`uaaFp{T)6tfwh*{qEt$j1BhJ^z>1 zVc)w09Dn)D9Ld_C$bl`QFT|le4x2AJTCEs6xSQ>JKR0;!BA20hKnggIw3djJV|JF# z%7|Q}6lQ#akP!dl@kK~E-r?$sh~B_NTm5YsnO<0~_fd)ISM~|NU|Af*odu*dq}4fX$)_MW-v0`zn`;%nqEKleMbXb)N; z`t$_Je&e5!g-6?HN2%MER!M90EsC3%7+F0Y`VL=hcOZ_LrSeUn1!OBLzX;Q|b}XHD zEmU5u(e3nVGv#(;>Y03+l zYT615xsl>9$C)=_v+|Yy$&)@}BrBDF1OV+@ujgFJB!sezJ3A|FNPIkWcVUl5IXuII zA1F>V!(ep?>BX5^P!EW&C_y<+-%Ry1^e_9T@KndYUv$k|CuQ>qyZGIg?gOeD+nQCY z;BJDpk8r!WQsN7Sb+S^J!PDRR(S!X8UTh9DC>gib>aGxfdVD_XeJVJhQ`L2zV@ zezJxJ=N&0Apk(*_74VIY(?VZ=%kj>lA5gbz@LPz?&$?1~^UqQ*O&#y>5dk_KG-up&O#0A%x3>C_ zEBr&T%bLjQu;&;|t;nM%SqBeS#P{3WmG*Ddv?n;)*SC@+7v8HfK$pyjZ zS!kX9rclVlnTnpiM%^rsWGaO^U4h zQwhX@VyuROE%dzP3SZbh`;s1{PZeN5KRl14iTHq6fW$X$c{+XppDqv=e)(V04|Y`I zb{C6Q8G6l!Qhank@eIeeTm3`nz^5L#IGN-qpweZp;sONYNGE2|&^b z=d~z$vhk8po!TV2fDm3`-Cs8UQezIO{Rb84(Z$u_41@g$&yDT#&+Z92lSc=I>~&$GB{*o&5IMe{}MjJX2+p zTDK_%`Xj!IReRTF=Db7loE9{?UZ9A7u&#Qr0f;n65J+n2Cp4TqeIri`;t&WwnWLTf zKXk%8gV0h=avLy@cAI{72YBfr7Np<# zB8^kqrK80l&C_2R4S(3o+|wDP3@pgpxh2?_(;?sI#Q#)X#{4_8{{qrlwF?H$j0Emr zoHM9FBKxa;nqEVU^ zDgdg=(d~m>mh!c>w5#!Azs>7tct82Ex0;JY;(6)^t@}_=7mZ_DTG3b1(xhzSa?9c` ziwjS}16+oCQ{&BHKnJFZl4HU01830699r-+=ShEomFqKNjieq>*aHYSG4kHQS+_GKv$-lo>FS@)_^LscWl41bJqnF;(6rl#)ivr`w zn*cLesoBuderoyooj<0F7Fwu#R4KnM0w`AT&drozm%3K(TGo>1eDD1JW%CShq+)JvQP=A@_Np7}ikuy`4BYwW{F^jYkHpcPFEIvb zIMe2ynO!hr%Sn%SfVD7H`D*I$svOs_-%v#UN4MkaapB%PvD5C3m?~>tpO&pK?Qj*R zlwIaT>qOsci=CR3OJ_A=?e@R>Oa9F~N788|U`YZ+MJhnuyOna-zW5VNmCQI6?K>12 z<$t4}r4`#grQ6FGtKLA%P54s%32nZP|Iz8DcD25^HgF|2;HdAyLzi04q}!6>{y5YF zLV5H7h=n$UVGnYVGDd!%kdade{o2};Bpp@a)m7(LuAUlskT1(ei8(X&6)>ZUHo{U* zj}$KKHDdTQ*dwZO8%nf0V`~$&pA?if-jtRMKaS9IBZqWDSSXBr!Z)X-4~kXK$L4== z6--3A$x{#=Xb^C)-G>=BgVpp(zZ`&Ov+o44JoC80XXU?i+|oa+xm87h3x6hna8?8- z3d+&N%^o*{E$ykeEMHos*Md{@Q+@u%pq(y!FP7@`mC`w6Qo~`@I4LvQsFC#}&V0c? z%^X^f4P%U|ac`bJ40n;Z!#0D%A7wwAH3<8Qm<`!x_9x2+jRe2{$$rFKb$R)2%ayuD3dO(`WivT83Ee1N$xpIeTd zSK}Q-TH@c=?cTpFlJbQ;Y?@Twt9=!kusL8D*WKzngb{Dm$a+0gdYpOr?M{Ku34#z& zQ=1Jw)7^0V$G@xai`I~X?=pdR_w$4b-RqLttMpTCpyYhc5tusAjvE|$R|XIhsA07^ z3P<(HmbTCBLPWEiU*wH2A}9vf3}`VnQnW74b_CL(ex>+YO-x7lOiBNZ(5+*eH!9eo zK`(C%j|NW_7&Fu8Dn4Etq`4~ZmyuM@_D$35JK_skB9tafUc)9PGhEq$7{o|BN91ef zWy9ssuH~0oD%~kg1cU%gob*R6i~`adf9B*=<>EU2o9Ft@-MOZN`A|m5%0l+90FKk7 zQ|oGL!n7-yD>geoWd7s)WBSZPtmE_Q8XkKgUdzL=18^u7*}$)Hnk*WfZ& zxrk09WtDm$pk_ws?{R(KTahV-MIOB~vpUQG{yr27Ts?8>T?N;WHIT>I=HolEJ3-1n ze#H1)2ydXfE+R~&*-Ukj6FZcJMQB}SoZdCx_Wsg-NeVOpQ&C$xuI$UEw-&J-7{T4Z zEoQx`>O-fcAFU73FC>3m4$gZK$;Ftc@c=L7Hj}hz8+_y)!j>Vl&{!Cyhn;)FMwVQZ zb<+Zfq2C$zLe8ZUCeeye4!tSyF$Bu4%+co>CQ}?$|F(}7H!{@}vLg5jth@##K0|il{BY%c1tNs0V6GCsEu1! zt85zHjT5+-)gCW`TJKINPaE;t_PYw)wG)1C&e-S!bmjxk13LIu%a^~vbusAgskcl9 zq8mrG3{D9QHj$Z?-(ZF#8laMm3`(){ctihgk?l!or=f9lk9y`>t)|o(Tmag)>Bbf* z3k`7~MHb-UTpOp>R?dw%Hia!Q^RI5kHLr)(X1h{C385M;MUVPzZop0UBO3^MuAVX~e;Tt{M6AMl2Z9!210m&u8e?FxTcOcY8S{ z`|&%AF~qG2X^B;-f1U4ZiwW|d@N^4{jG#r}op}?5q@J?(X_Qv5z}CeNqtZoeV+=8$woddeZ7sgIs12Xv=*P)YyMW$5ot0D#cq zy43`LJ316@6}1C!L|-^k%8fAq-Y^bQp(|VE!EX}aReqc%8Zjk0La0YwC)1md55lT8 zd~dqkKQh%K{&s$DjemQ{d11ohasSYRLDevL^8YZYl1RZbnga0r4apzd2kEpD_V+#% z@CRzg0dknqg^miJu%6S;BI5@GFY{gA(Y0MTw)U(EZuLDrN=|P7g%!K?;>ML#m)g(U z2JR1eZkN5$TnEtU5uAfq722nOt^YlrpYb8}&Q-t~Wehw%WJs_5p*a6Gz*>C+KYyOO z24GOXzMQ%0p2Y6BKg>i896|XKS`JLP$F18_c%V);PWs%D&m&fD2san98-D$_rmdNJ z$Eb%+Ex|AB&j@?@7V!I@aX+MCZY4ghw=g3$$SW_Wq(zPVswWEWbHZEg!}#fNUq5GT z8ng$5z^^F`fI!}h?4SD25pWN2sxi-H3HrBr;(A`0Q+Pr=7>Qjee=xn4d zueR|Cq8GRqMi#amv$Zg^U^pmR9h zXVH}95RF9XV4y!vk_O1OjtS7qJyy-_P?` zc`x_W{PDc}{Xqk;N=zj25H97bku>hR6j1)1Ri5<=9Wq>B`arY!MFo* zjYsB;$-=tP8~Jp5Ie)0?XOGskSW`?i>AQG1ygN;IZvEjAJ10D>-mtRO?YKaeGwjtKZqz$coj4S2;+#Qnl%NF(K;GVA`BNCFkU` zWuAE7RC{Vgxg*!jom&S&dW;94)Ga;FmrY6sKy>r*vQQl^;ZC`440!j|I_=)%fhOif zd)jli6JVg@J`FkRjS)aRvvJzGy~}u9EN&WFDibn_CESMH*T9|l@mwd5yQS?M!37i? zK3)CV9M!g>d*Os14Sb-hbV=;Q)g{+ctA2V*ji-bsIF*Fn$Ipw-HiT~%@SxOrrJL>> z(SG&9E2@)qiLnLz1wS0-A201ZV_X}A4$3^<|81R_nP!gP7IUqsZ=ck909O7s@G8xr zDiEivyypC}xXRV%Z#JqW9gmrN6-5dD_Q@{Oj>okLt3+)To&sHt>w>c}PgjH=13idT z-y7=aXTZftL+*p!g{kP=>{%ZhW_O!RGPETU8>y!nx;p>r{gG{Yn=rRT!eY=)1*_#hntB5hJLxnuZjz?6uY{p&L&I6 zyWaHOzvX=tR*VaJ2i(}u^b~lvD|5KHKvqmtOn>&Y#T@GPavjcpD$t2m8-qP7!zz|`hnTB$ zFzI0HnGXHA{1rR~#fMGv_4!ly+ZB1y#))vN42mbY-k3b$KUc*Y#Nx3db~Ycv*o?4D zwtR|+8rHD8^NfV)%7J{4RcvF`QU`Cv;C6UVR5xwHIksE^@2phNzhzKC6m%PZDh=Z? zJbf%J^@zLM(KHD37Ih&?e_??X#&oy0z+JX9oV7#>ZIlNTp7Rf}ye-JfZR+CUrm6$( zW0B01SMx5W)uy0+-oNcoEz7BGwQ~2fz|v}1!Cf!N>FTJ(F8RP zlmJEj^9_?-*kGX-CYdi9h{)DnE~=^qMOB^u`y=-zoDXx|Qv&G13)Q<>OuAT^HcU30 zR3m(g+Zw(+exq}r!N6+f-Gn(Kekk>*ak7E@bS$F1R`g6cJ2keTrRjS1Ti3`6nIxMEC?1Uhf1Emg}QaNE09#m zE``{BUvwI{L>3$QRVu8?a~GH>)s*U+JF=#PAI0-a32cEKuF93qZ9XvYncZa3XxsHuqgkQvj}-H1DG*7d0tDLuP*li6#1jlYS%J%sGpnoAfp zJ%IzG7Fkp1W1I6Tc$%N9Ix8n6%eY!zo2AfN`vI`&pf}qw6q)*{d34v)MPJ=yeXkZ(e+`0n4$1&DcEfD65jAckS-nT=dj9UO zpX+~hIF&73Wc?PVGi?qT|0jJl#Sg!Cr&rfzt(C? z4KpxC%^0^=?S~(imGyfiX_9CpyZh(U+>gA9#|57}zkkzaj?}UThjIO0l$la!7p86= zt3^~>8jIeqh9I9@-2~4}PENWQKGk2=>C+;-m2Uu$y=rX8YM&|FMVF)MEtx+plE3e^ z=}=<=ra_lbLP5(w9>P3OBI=dhWELSUK6fj`w^z@vaBZv+FfRi-=&9n%u?WP*Y0x(I z>gAqa#h@owiR4P88UH#zzxpQsWO90mn-@e4Z+9G#7tnWH$OdrG>W8CYJhcE`J+eT3 z^C-OAZaU1~#KIxi(fxfVBZruprq3*OU5IbBT(X9lg&Q5oz z$$UZPaobtq<`#S45Z0dg^y>S)S4^W)e--;L+nEuE<63#T2iec0+>mEBWrtZx$q7aR zDM{6;R=amjo4U=I*VRytuBC{LDaZFqZ*bw64(!Wjpk(##f@`n`_{&kqn&2)Ir_@7j z?=M!rhlBX~g%2)7En}jz*hp-soT-fRzSicbnw(Ptel_bKu^U4FrY|r$7B1DtMQM#z zw{DDM+3Ez-TyO?Kx8}ZoTuKNefk?{NA1DhV zgq@x2UD z?Cj}9vG)9*MgatYz=nwef8keNZPxCMmu;s|;DGaDeDFxELimEDW%C`)?&+uUseHdx zRDy*w2dSFpMJR1z>J4bQVfyZXS+>!HWA9wVSVmY;!0PRTO+ekgt;imW22D^oeDNl^ za>|dI?9-**V59ghs~(BieS*CqVUDnguX6qm&ffZ~>Hm%Y2N6*bCLmoZrKCvL1|lT_ zf=V}%113_VOFAYXpdcmP5~HL?Dbg`%wkaJOIbrPedA>h?!1tWb_XmFBoE^q99*^sB zUH99~;Jpn#$D-FQk-h>J`c0ct=uzuVW8ubJXFKHfR)Z^DWoe=3f_>!r(J!7ChQ0sV zjs4@g?!%#det3iGo%7JobbX%hm-H+7d5UI&5-6}?&b%=|wL~AxUCwPhxcF>FCgh2g z#_=N4ry6F&$j!V94WHH9UtL=^WAV(h`cSM+3;@_B@f0ZDRt0Bd5u|X&2mm69 z>FAPZ0YFhT{`VKF{e@Z9(ylRgswxZ^2=yc9I8g_fPIZkLu{v=UrD=h+RW^~ddfb6r z3r>|0IE}ih%v(wndWrvHZl(w>*Xc6jcTU5^(|ETj2t}A0hf$J*);KGIwMtxB+na03 zRLAXHeS;4xTe!d-I6o8nY0>9oXgmVlU|m$ zK&}+Ix@RdoYHAd&{=_bzql}kQ<HVoCZ9yv)Sjoj*Tkn30W}HgB6>v7<1#ITnw3OQgk&cxm zZ6cD$Pms(I!c_Thk}=+|qK2Dix*y=Ol(m*4Q?_(7x$E<~W+)LjiEuKA(A99>d6GNO zT=MvOY5VlP_42ymgwD6*o}g)&gK#h{fi5N9!>eq><1(2y)j=lr*M||VfFF9>>bDK} zowNNCaeG=Xh^ttG8_&KaOBqay)naWSaBEH5`O_ZeIXG|^KGb zhQ{!zwKsd*#Nz^bFf0x z^?7cFICv&IXnELu&T`{ztdio<)c3^$7MBIvPPy!ol9u1Et#?L0n+(&P>7F|`yOl+g z?-HSddG9tA_fl%{g!|^tYe!DFy;QkrB}RsLzL;cg=Ql#aWjp{=&&$@SQn7L=mDyr{ zBZ2B)nzYiYdqS(sODG&2z3aUcF{eQtr;LIanOt3^I%?H19d5#_4Ppic8g`rRrVWE@ z{mv~_7J#4S6e78^r^?j@WW9(&FQ$*HG#cCMIjmZdE!l(nru?SfAEG}dA!-b~FCaug z9Ev6I=2pYP!oqX(qfyv9!dOGgO4uXr0mM5!UvqSdG6XZ*=3!t_KTZBsxNh-z_U4th zH?E$^`0Enrb81|5Dt@mnE^kdqd}?_+%yGZ#dt(9xt_cwVrkf0wt9D7vLVM8)6NA}DD%pVhPA&Z4AM9Q?r+%P3u;PgVw17M! zU9F9i$UVrb_Lg;Rk1`X-vz~300WB{AZ(s8{cC!JV_&o8okV_-<(m4(z6GF@K;~RlIEq>l=764cNZ*UrW$>yUh)v^+<0!b8~tb`@+OMn znwz<%yyXPVd$f7=JpMsai}i!Z7x8dt!0az!PZ^4u&22Qtu?jd>ZeiFgB(>Co%M z>E1ej_w$%WuMpA4wSM=1g<4)DP^|fa?!b@w&cjgoaVB|U+2;}rHVcc^o32-CDzy|z zg-Vf~V^AAi#9%iw_)+kS346l8M(nW|W~ z^A&gaFx8e--N1OvIXVv6O;4=qYcW103rxK6zT+SMN!-pWHo1OyyoMrRtgsxu*bA1P z?cy53CU)AL=v@hU`E+JKBXpX&pC9xvj?EEPR%6*o?5u__Vi2e}AI{Zp#Pojj*i_q{C^aK=iLZ0o9jYt+tm--P|>-mrVpQTXRQ`LX{isvm2JHY+wl@6oa&K}y+z~Nxny_b52d!qYni^G*j<4>IuIYo(T4BFIF+q+-y)71bhWl*Zdi1G??^5jn_;Y_fT*kTD3ym! z2JRz)Y_ECnLQ*?VAvV#qY*q%!>EI=*L$7}Q1Jy_>nuhszFefphBCKyF2W7mlQituP zwKNT>6jL{@cX4dawV-3SuWHloC7j}si6}nhAzvMO#nZ^{-~O3H^2$!_ zC4`NBgE;bIJUF8SDuR)nCCQ`P<+ZKOS%)^{JC^n zYAetx_q~h1v^*XE{Mc^&rpPpg7!U>;vgqdcM+kEsEvn7CM^?d%IB!{M49T6bp8r;fpmUk3EQ8xz$4WGiAA*?;f;gwmbXAm*O_FRJcj~s3p*% za=JJG>EORvxOpvi$N1{jynNOA={DL(kB6=$%G*#b|#upw5hjWhO2>o^u^y=@#E zT6ehrKRHlr-#Mh8+kXiB7|%)4>`Ht-@zmte_WfnL-BJyH5r}H#D?*C5afcj1_^3a> z@uayB8^)=FNgT0y79tyBK6UrV>QnK9D|Kg|==(ag`ociIbRcY5a)+Muq>gh9%7eQ@ zHFwkD7X86^!r|us?r5e>@{I|H=>zGV+#odq-3ro!4x8?w`25dq*b~Zvo4mB458YBp z&w#8L3@hf)uP8)L&Y9=-Y`pDL-b}~TW;ASmO&>8@y0a+cJA6b9BW!6KfF4O%wP zY9x%s2k_4^s4>XKEYDnv`0?jY(tg3sFlzGMZ83uR0qVBfIXa<35}fB*I3p9p8e;*C zw&i&I_s%0>9YE(>ghM1E7ek!;ceg0 z9WL09uahF+@7`)WnG>+p;hBL+os(tW0mavg^KPtmbj#8G+Bzu8$ZDy@8jH(hl2Di( zzD0Ysgus3IH+N{^wa<^pIEVXKwN|e$U)~(8AdmB-yTpKYtah*~E?)W~s8hBN$}OZf ztt;*IIfg%DzWs~OmG1@-|6c>EclVy)LdLMOn=Q zi=c*s`p}8R+j#fnmTAoE4d$GZt|(Lis@mVprnCy9#)t#%9aPpOls;IM%2X%;cU zVvIjh9X;c)tS@LG9m{6Gww(#1E=fAnp?LnQCXG*l>%kD!N;~l16CNp> zS?I#(=p~n0wma72J?vcXpe8SfuW_JTq*t8;$oq|{{as%V*+sqXmRZ_Zwd;I=%*tt? zK#C(+g3WCRhAXH4yfl%ZYcu1UEAuB?iBn8z?A!ag`!u%K*WT*5?fP`SgE56p^4{_d zdjBf#thaIe{$S{fU)u(l?2$$yFF^{`4I-)IFElzP_$DMmr@n2YCF|QIN79iqZ!Nw! zPp^C*rz7#qO2guKuA&{=TA&^@C`8d%V($1ok;v{{f-29Dh7jzTC+=7pXeZ^w4}imO z#=J4hn2h;j7b)!KYIr_1T^`6Q58%-I_AnuV+LK|Ej)K|6@xM7*ovWbK?Md)XZT1?pQI<{4pJSl9T+{9gj7_HgppLBFTT7Y?^6C zHm9(?hTkj_LSRqoSSEd4x^4yA&RUpj>T#Ml+_%aQ8u92y%w(V2=+60E+RqDq>umWR z7p=7S#|*R?_C#c12wna0waPuu+;Lj5LQTt`l^qA@`lF%54&HJ&D|Rrd8ZdMsniX{8 zFq|)4_`E!S1LKFgp|YzhlI{mLWj!u_aGHn38Myh$bjv zjz!x)fi&=QVFQgb74pYCnil*<4+IoaW!?UC7N#l@(YBhjXSfN1n9mNre%M!CbZ~5C zzW=M}!OJ?MBmuV&o2M6izdJysOPgf2>0Qs-;;y{wk;;c5>)>v+sg|l?X`2OrXA`~a zH=R=>^G3-iDDGPIemYgbsVCO-AIx#VA#&mKY!Yw(ji-xktsPr`P-qp zyv~F+>NNEf>63t-ArRmP&JuQ7X5FY?2DcV|2ip=q*T7?lDs#ECqQo{eB89Yjr?PlFXQ=i-y;DVR8bJRa5K&%EU7}*i1 ziR7AtV;ANh@~t%pKeHk{_q8cG%%{v}eBQV#wZ1-Pa7+Yl^2V@sLS*aE64aE{l0E4j z920^#eS2Ob*F}CmGVy16F8OQ1MV-_U!R3F=&+9bhqhfa`KJk2%Tb6EmL6aUjoyQL5 zCn{7oF~B;f&BgRrQ-X&r<*5#3fT|duse^@aEs$VlpMAuH$>=A^A4$n>o8ZZNr;I=T z34}zHo2pluE}1RWeTI2)YTVzbOpSyHOnoxXA@Z2);Q~kfA z?+Pm3fcG!ud=XgD>4Fuo@l0nc0>lWOuUsdRpPvn?orC7S|4$l1gWo&blOs6qDT=)x zcRQ71A-i9H?g{h-)P8St2GMQw0;lS$fYf<~C+(+Ni3=l2<-LfML?$7mPg}t~=%o5T ziaVt1z=aL=@W76|?`oLCFjS3~ttel?7`6!itbB_DcLLq!>qHAYst-6tk|GGboN2Pq zUDi23LXNB6uip0%eN_SRK^P{S<V==i>*r_gMQ*r*Y0fPWGl z-1+5%yI66e-b`_}#is=)C%>$N4?4qRK|VEMH;K*$c<0z^X+2@VScCdF^{eiGf0pm+ z{2@zI9J_!a=%%7+mG{`L|yU+YOd*C4HJApx-0B`7UWYv&-8lk zm^T{m9<q|fQf1NRoe2F(V@@M6; z-AE|HK<0CLt(oSIJKgwa>P~WH=J)E5bWhBRUzm8V=*!}e6aiV)5B9&nw>#r1yER9#FCES=vM%+b|JP*rEQ@>0{pOI}Kas30rs#tm_*I zm`iX&Ge(4@sqNRo;9Z7f@|b)9_l$VCy@uIF<0yJ&q~56 zb758Q^WT^`IfdIXw)B)jRLm%nEink|M{EvuE`*0^5-z{GejN;N#z1m`?jXbTDTh*Wc4j{3B#w%OT^a}=0AHNN!>R%a-Y!48SAz{ z6k7|q0%Ih-{jiw{3UREpq}4pU^W*xXTh#KySJSP>o$-*~ITkWY%;Iw>oFwxW9r!lI znN!eKUFqm3JB# zJ&sbezYb$nvbr0E-+0k6^QN`FJ4j@U$9NjND!eencQ}?B74AKJT(im%Iu2$eG%Y+m zqb)bJcJ&)^q+ZVn#6P#?=JR`*$>m=F%JOWwh9t@n%AU^_Ox5!0q6^aGk2ewXV#}uo zhp)TVHm-H1V`-v~VRX<{YP2?B8{e#z`RJ-q{VZZ}cQjIU8#)P0O9Dhy9&Y@8ccIzV zbLmbW%5pW)eKL$~OlMfAg0EW!n2T@he%~Q+`N>W(7w6g%c{*8#yy0~C=K2fD*30E; zO~n^9c_en=@$c~UCTP7N_~klQ>}XlGH?=`G=QLX)|*{7A$!r@nyZs zyk<$;ilU*SGh$-KPV$+P%%qOIl0Za3T~UX}g&bTRKp?lxdFq#yF0VYWm@HY<@!($L z3MIT+%MV3P<-ZSQH6@90nFq8aq8_vR3}aGL?d?l+?(*hD241>Q4Z@n1nK1r)$`;S2 z3#WZPleql@LR_8Mp62}UihSjjTZ|a&KwwMrYj#$zB9h%Z9%4Y(;Q_UG+rP0C=Dc3F zi#<{WX1g_k)PKVXZ!8B(4emj(fv?-N+vIPZ(rb@pQLuZ|6J_dYiYIk)kop5ug>73? zThy!e>0*zG-+C3#ZJU)^Lbv>bJ0`BcdDP|h)UaosW+a8OdT?>$G+7yn-5CgF!KdT^ zO7Q*_zL}?JmIi1UOJ-^v{YIoIkVK&lbjn#%;ng8{9K?I9eQ-O=tT)pW(SG)+XsoxT zV9-DYL6BNIi0u;5vR`OlwpexDx1J;SVG8oN@T!gqy#V?@yF+1(+vviIh{JH}NyBT! zr7bM-8#X1S{Zf}GcB{(+9|=oMBVTk7UT^f5`@rIi8MO%{g@$jgS9mjZiuv~LB3>i@ znE=)-5SfN(fXOQ=x7qabE?Fjcit0&f3{Ee%3cly|)w=|wJlt<$v!3BAIH$vH4Jf0>2LjWc+rjBzI@b#AOseT@D zHpKiOJ(wa%VI{7UCn0p6GC6?iuVMT4ipHtUt-;|HQ;mC0_Sf(r3S9Y)^$xIQW$jJG zcS*$Jo`(@NF;qVi#~ei6>DP7y{jbq#%2@607fe}>$KYul7P10exx-4vUvpyPn!PL}0@#_V+q zz_ZHn{D%mOclz%%c%AZ0H!!4g#f>9lmym3u*Vvg_(l#x9`OHO(-+Sht+i&*M6KTp` zI3H#`2Mb%^d)-3t&CQR4Nds>f(~2~Wqk935nTsj#^C$N^P}Z&A@pY)~1RkErr+)2< z#C6V>RBhV#BaLz=+A{ z5^CvHxg)N1A&CB!TiF|t(kkkj5j9OYIof}a(J{BYK0GQ8GNA*&w&}fOesrBLXFM{F zZ${tq;BF0GDk943Fv;@$p@`%rv*TK56qTCmi0-Q2NZj7{2Gi&bxLcR3zDCL$L368xSt zG|R8qdJItke%QX{ovCb2KNZDulV4dY(nQ4Cr8j0MUM7rbaa&OEymt%&o`O*C;o(-RzHb1%x>cS?TMl$J&FcZ3q4QmQkI zSL|i$v;G|x`|S)2kp^iHFR|;eD?5Qr_3FjRZeeurl$GnR22K&*>H3=G9*Ct#C1s-t zXG5Jfs8B&7qH12dk!aC7$6(HBob0!}ZS*DFhP^MctrwWyUMxI5pQTfJGVL>Raj~N$ zU+}%#F{mx|?eycMD=Wn}|A|W=vge}3qbdQZI*|MrJU`sOjs(%w#^L+xhgH4rwn>?cMFN)E7h`F8s!tK}u8_fm_cL9&pdn(<@ zwdlX!qx9vxmksXkKRja-F6UdCpqXAfA)OnA?DCFW>r!vOEKr7p;8!S%pf5(Nd$AW* z^eTQ(kg&)_!0S(v@gE(U{UWA1ub^KLqt|#7&1qtNSJ@=uc7qLI@A$&LY22vKWqSKC zrE1rOYSAY*$bfLw|FP?t;R^p_WJe9da=hIqvEZ0WL`yB$z_U5vGL6p4`1+G ztBr|>keLh;o4c>LcZpcS=jI%d&ARi_rcIl3N}uGy7gyOqQTbU{!oh90aLtf&Z_Be- zQb-ZJsGs9BzHCVuea=-yx9jw{kUa(y8Aw<%N#C)4Ob8nQ#nrR-DjK}>) zH(woZO={p_jme=S^T7V>+lJOn6K zMfTi7Qv^B;1jrU07c|u#HA_W;b1Um2Np>Z~3smO9da{V6YYX-JcaNEcpv(?Y!+$vb z4PhjrNcN$3Nk$pL{*Qa1m4L;xM|u-79p;tyJyc)*pO@u$I3F<`*lf-Zk(uZ~1E=Sn zpTCyOE_Wq0i`-x8Pgk&$PEJoB{^ylIaRKpM{XR68;+-co)fF<+B>=C(mc@67VG!51 z<1sIT3bkst0}CRY)1*JpeHg46)ZyP)XybOkhV?_PsuQA~cMyzr4HRJ!={v|h(Js1@hCg2G%4b&_#WGyP`%0ns>NVK+$eAF z*||VwnNqJ;^)juJ%qy|3CpUWe3VLc|6>uL6ylhyABRB_WK8Sn^el(zQPd1xWdZ>*P z=@?9*y1J{)t)dqQC0P@!qwquh-OR|GxN8Vy%3}%8)AHWeDSQDXR9^N0ceyABC_-q5 z3gifM6+K-3Z;=#(b zmM3r1Bsp6iaeEJ#=3ujufs5YQBMWAnTf?v9VnHBqiSN2O{~m2rDY4GzFCL=PxAh0O^{HPq5=s+;7oMq=KRI;!5K{V68B>--q5I&tmp2zK zlaJ_d#N^Uohhk4U-Bi3^;|H>>Ox+Hk9MW-Htv7Dd6-RB0d?5(tU=LUU#I{m^Gj?tg zCPR14%b>No8IXn~IC)Qb+){D`5Hr6bE7KaIJ$o>k=AW z{J_kn9^xkt-0!;{?0=?W5!ReB%u5i-ngJ^j_j#$7e+$fhg->e`9Gj!OegBll94fGR z*cWqukJw;4EdUjF=;8D@`@~91YXY0Kj9OWm7bC!LV!skB9JnIR)P`tej?f6-ZeAh< zoZGjK!F1cCeJK83OV;o-jsZpWP*8g5#y0rOyrR^to2g5XWI)U>bhVaAL@hvK4=|CF zMc)#1J~mlIzDY4r_@N+8d7Mu17Q&&QqQo>N@^>c;ktf+jbim|-S&=*nbH4K(U>U)< z0G&VejF#@3+EiuF(duQ(L?Fq;%Mr|pNp=|A!+P+UMy|UWL9g4t9d4s|oP<-JpeYEQ zu(>S(W&`+Dka%~#BMEnpZtp?A?~hb}pDyS3bqjT4{NNX4Mv1*i?C5j19MUK-Y#;JH zer!44ABM51UlGFt^caI%+lnkTGz&gEo8~u)PK=a-(1Ysa8##`^xCFZs!KeGTz>mYL zknV^1ZQtd$R_{#kRN)Oyf8ym5Nxa2EaU?C*5@P<38v%5~k|Nql70L|OXGO#!ZNk<2 zdQz_%yVWOaHcZC&|-Wki~1dt`>&b7lg@sNIaKWT8l?W`JLARccRH(m zS|W-Jow2-=SkgJuTs@oOptwl)M^b%3_tv{WkY&BI0~tGTls+O~tT<&aj;G6s^H2ty z{~rZ4C7~{AgJng9=v?d*@!;1`{P0Q#BwsbfU5VKCGVsRSv!V6f9Br1p})`BZ6S((NTyC_9{(y%1tfl57AsTx(*Z0>H{h$u4vxlYMkUVJp`S$dWC z=RXRGz_a#Wv(lzA+NTDLKR=J5*_k&d2IOUK?A!@}CvlX2!4$8)+^CBQr2#Wdo`5*u z2&wH*3(J<=k-OVc`B+FvKuyM6J(kXcqNh_$n(a+`YOLXw0C_V6x7`+qRa5UVvoziv3)z_-r6cKYLMLYYG%(9{Da*iMyZd_ zHbg<0%se;n*np!$zhQ9IcLrE+8+9*psx35=tSnaq=6c2^TW{8+XtPzWQv24J^Z@Of zS`xR)aZ5JzTHo;BzT1>+Y$gJM?9|Z@A8%K*oPM^fhZPhn(hC4W&JOA^oduRu3Jt|k?`9AQ% z=#RIFQZ|s~I39EPkAmN~HV8bVcM*NZc_Fhn#9iQytyjReU$J9MBS=kgur0(S9x>Y)NCZf;N{$AxyODQR>o_X51sA5z^s4c$8jya$?xO+ zmNo(Abl@dnoFQ1DGx>dUbYH+o^F)q3aPwv8SX)2Y8dd1|TQ!JpbJg^Wz27tzqS z{P}olJ#U?a`Ak6vI3y`8u^m;$+!ophsppCWi(sV;s$V zFWw-F(}9Sn2@&I>?HfSyxZ2bX{|lT)%)QA{O7fWYvs;0Gg~iv8187hVGx5(@Hk}}) z@K&Y&D83t!ZWF$AG0*1rnfHlxjsl5vPo=b%C&MSBFqbeI-*HZOys{2Xss41ir=+I) zCo9ceMx~n*#8Ld%>Oi~BoX?G-kT6|)-XC|Ra{`17&SEW1Ue;)q?JKHWNT?_NHi^A> z5?E9BT_t6GZm56ZX|w;^lF{ms2JK?@HFWe=_qAXh`~mhpBvyzC{%-Ri{TF0A_U!t} zvCQva5J9sB%S*R7TE1WadrXQhG3v6lrcFXuhwO%ZL62X!uKg?5!hn+}%2Et#1P;VY z^kl3&dfs_YY;@1nMAJ?f?Qyr3aGk7?egFO_-4C3!;1|$=!o>Fa{qXnFTFm(E{4 z(ph85yK+|NLbO<8D(-JDIRqh}b${37eH)e>$R^5FpK-@Hv2ERnZPV7ZP34@g7|;8@ z2w$5N_mgg>$Y*>NoFDCbQ#L{MQGwG(ldYzT!K6EU4Uh*_#;;4a4gAFJ7@B{kzGK8e zVg9gk{;cc;rN*74x5Wt*=FeXSqqfGkbic0;1yY#HPED%bh(LGVX?k(RgUiVM&h|5k zD{J;ZbH|)QugL>o^D!oW%}k)s`7{?Z{=*px!zLr(Llb0A3eq)QDEOr9lO|QKJy_J$9&O2{kkp*iuF3ZO-{&;> zZs`|0Aax3;u4H~*cZlk}eCY4}zntR#Tj|d*_PxkfaSeA^N7Y2Uhvf{LkL~Ze;*3;W zDS!oMf-dS-mZTHY$y-2dZ~3IOItb1UY;*^M1Mpdv1m0LO%YwO%yC8n7(A76+U6wiX z!Ra>}YbaeX4M7eAi6`j?Q#ya$$)<^eSr{>Fw}{=!4jBpOZsk?{LX~iStJal8S=Jj+ zJ$fK^j|94VtSow4Ot$>7bUJ^#vtCeCCa#A`nuT0xmej%I&(D@#=F3P>5mV0m+G+x_ z6f=uJ3g6#d_u8g>O{!YzxAR^F&wLM4q`(`{FOc5WlSe1(d|=l@vS-s2Iy5#U1B#3& z!I~>v5j#n98{uGxIuHeuhF)L{w<#`>YKgpp+bZWn1Uq%- zcpF402XV)2`fKBqL@-mOTcevtdFP1O4A@rmg;o;NcPem35#bCo){T3pELKDGq=UQw zlxD%6xM>;9MVKEukh|^oliBIAvGvl*8U`}Y|9}nuDjoU$PB)W`1S`B!jH6e)Lk5hv zmRt60{M66+R^b=Kr?&yjp%-f_on99YT2LQ@9D#ms!@wu|)wUHjo~}=I?-syCE6LsB zia<~W_&jqWj6l;1emt#gwgwFmGHK7(Y?+d=k~8{(;<0~CD=ZHPx&jx7ApGjU3W#8m zf*fZZEgKEO0zxK}3;BF8ztX8zWy17IXO{CwWROG31BM8|z+8mO>dUnlVuE}3TeG(z zkpz=iadYe_EZv9oCJMxa`*8iha{25%`2nDAJnb$d7CpmnL>(_KgDqaC>*dxu1z2!e z$aB8rOGPsu2{kk))yWCDUGZB!WxH0>0~Qo%{D}EVM~9$a~C%3Uv=J z(GR*tdQQ+=jom#?eu)A<`0Od}*wN}z)?~+nLaP2d8Zes(1?^)nkPZlTkUzUac~o=O$IDHWz3Al;z?)6&pX^CJNqjo%v-d<( zTb$wb+ihIoEf6?kfcUZ(*hcmn@RDHoq46uEA_M+QI@&LFc8jXSau4u@b8($h*r-s>s7~si; z&Pp;!l#a|cYVc6Ik1Gt+={|%X!kUDK3cwc)@zMFQC%PaO%f!Zt?S=+R*S$tpy#mJ> z-z!~-k{ew#NFLJ-A^ZSE@Ms}u@aZTfgh_3J_nG|=qxAd8>pZbn z(G4EBJ9#!EQRumFWv%{v8sAY9b7GY*?P7qn(6l|WVO8kfxShQ2T&tW0j~*en{jSFwX*J z|4IPg;R=5OyG#hnHsPzTMZZrw;nhV-q|`8dW3g zB5beT2Dkp$LBg+|)D<_MPXz=6RuE8fKE1CE?=t-1aU+EluSMdE)}tKQ{tVaN**YQ` z_?^Ku(o3k*nIx+^G7|z_3io1Y^BV(MK|J zN|ud!>BLE;YHOSUZlXi*%Rcu*0;$SOdUYA(jNQby&9_0bHaCe>sJv?_rXI^!CG|rmuSa)cbD9R3K zeTZrt6>4$E)nRPDc7cfkYls9rT4EV$F1urbz%_NH;lc4zF{4VOd7v|D9OMG#=Ah3i zHs3mbR?9NC@~BeDdFR0L(@%)m*R5Ql=+n2zB4c3qDpWRNYawAf7`CRHe|?$db$~k? zDJ>$mYp~6Y)Qsf#D2S6fsbt>5-YwHve)KgntzPcW12=?_>B6(rfrO*2MhDz^g+`Z|UB&pB50u*?1z z&#t3ySG}vG5l9I_XAuR3i1mcp|0pa|{aqIV7AYJ)T>0{Vj67aG&0!w+GI58K->AAX zPDR7J>jp8bgebA6Q)*UI?a%l**W_Zb^CtYa#_KfWk4+Om!6PK}jCL)hMs5Z@hol8M z2Ja5f*UD4o1pUU}(VSqr?1--e3CqMXtQdCxc6AKhGCShml}@44yqOa-WhsXu*Uae= zZc?wOVDvj?E!EI6=3E?z8WIb>vgA5PNELliopzNg{Y(8NwB=fne!{yI&s9DI_rsYw zmp$sR&%?p@@U$fTt`34hoVtzj)VCA`(2u=eY#r~EYpmLr@d>p53_*qoG-f3a@0p+rP?ODuSh8f&Rb-ii(3_?DmW@=o zki};_Yip**;QIkuhBIC(t*{ttF0}IN8hGT+v$^7_I=(i?bePEq(|EyXpvGJ*L0`%2 zF3^g|UN~pD%C-@qrtB~+vpx2x#7NR8VIOh-%U|;^B{D+uWo7GuuwGc)IDxZ|!P+W3 z`@PtorSeVFu-oV8V3DCP=X`26Yno{!o6OxjkAAZKt_O#xLWWj4zXjMXLHdZO`yg8K0La*ZC{RA z8Q)j=k3t)tnPcaaDv;6D^XSf#Vu!7Sff*nl;knb#64)WD>sW5_qw`HTwejiQ*ER`x zkhu;T=Q3_i)hr8Wnz4a^=|5A7KWn_^N1TuazW$>A1<$N<7ur(JAI6ZC7?YbzjE@zn zT`&aSEM8My$en~kCR9TQ_Zrewl! zQj*jMwKa5MT+NBXlaKao)|d0^n9WPS+-D8xMi$};Lqn{*ZnO6ihsYc@d@qv!qrf^q zSwivKJ^nAP57unrWdXi~b%C*J3-6l@r||sGymlL#&|h7&i?yk^1D4H7Dd$_emAR3z zcPWWnuU46qsme+GGphsET*iuGzwP|pI^tTcfH0)0+9p?%FD@gYh+}#DVa*fzPC58? z{=W<|4crsN7A!X_4GjmnOCPDfgRf2hn}bB23;~sg$S7Zbws&9hy~Bed#Cp%B_fm*R z&E+S4r^!0mTV)Fde5q+tHfm5F_9>EvTdJG!BkKY#(M?|1M5-k*$qhA^A>4Ir<4|@Z zB9jlKUn1eB<2lYipxG0FdfEFN(99cDxJ@Tl31zh| z+!!yv`Z#15tB{eK=O*FDdbc)!+-7z6PLi$h@r&ZS)Ry)g*}p3dqsn5F)3?u5+SwFq z-AeD9K)?Qe#xdGa z$ljrfR9P&Z4Nb7N@95_yQ(PL%cnxQ=38eWf(miC z-IjrEQFceOp13P{`KA9pl$bv|t!xKiai5Fki9dg?9O-`l_$2*F4#)CujA;r^1$z8b zIl#@NRQ`UqIGH!JH|&nGUZvs=ix*LUh4+({)vwxtSk*!{0x{wE)9(Mp&dv8do?uso zVt$83k->fCTSMnPl9x;9&6!Q3uQmFzTI(`Zv;ETBKX)(iJj79Xlff10|0p7pU0zy> z$Zs;+0H#K8&f?Oh zADz1OZNcIjNSLWz^~PP$N1l)^O2{sbaDIk+s`zue{qd%MW)0WrgoqTo2mg4+!|^Ao z8^}PxDgvZ0kn_0=h{3fvhCR6>(jtSJpSMR74xT-7%#ZiYrz8d91#5!VlGS?`u9Y}- zv3{vv$weyd?RqCgYq{0SD zg>8ibP??8BVOL8EcExmG?C%T02$QGrRbQIcY6HgLC-~L(jw#$Q%lfI(4F6*Pm+ze{ zPbiKOj%?>{^SSXHP4PZ4W%plur@I??D%eY*CQi*d_6=Vq--jNdiY>pKUkJhL^y+W78R ziLVW_fQr6v-~CQZz^v&;;pwQ04Ls-hw7Ee0jE7jZ>95Iu)4~VIUTraf;nz2oOnyLa zkRVuCT((4VZGPVh2U;9SXS6+cxMBJ+=Tnb;<);Nd;UteMwPAhbth~3uH;7t;qbw}~ zDTs>r{+9{~Rt-X#C9R^&eo4IgE>v4TfHb}R%B*Jz`uO)=wuFi!G~?#v@e+Lidj(Jr zaJrb6#)>z(R?RQD8b(?Z1h4TAS&c4q&>9mxFhK_kbx)?mrF9jgb3k-D0-X_vvl|yZ z;EiQSvNwx4ISLQ!x8X_i;1H>!w-|RKY{nJZI%izjqfIW<PRUv4E9z2D@THc&sE&fmjNGzBdeOhvA%S{ZwY$lc!r4}Z4V`N zsFlP2hkRq}5_L5i7Bioh=4M%ExyUR>?J+$hbEUlltee}TN?3zvWQbi;mBejVv|?IX z>6}Qk^YAvsxX_lxw3R@UnCP$ir$ydMT+~z*;K;6*!21ih{ETqkzW8EzSYP%XV+m-8 zbB*Z|uga4T{h5nLtd<-6f3nYPl2&egCr04Eo8d3>zGnMg?zJY1vwtgTRni&P90b)D z(ACglAu-B&p{M5hon_l{&@U+1+gg%u4|izOk1j#D@v1d>AndklMV04o(S^THa}ao( zN=Vg)o|1JIX1~4|0O{D!9p}@*+nTD1CuM3_p+h1Alq>WUb{Ri5l&7UvV^`?gFO$mV zJom&j%LG%!i)6p`gj9$hT6ldV^+v7P=DSgxm-s^EW^xQzU*w!1#-O%1_;Z{Ww^yFj zYjI_>qWRi_X*a*r(tJd-)UN`IjZ!iYPezZ~P@X_9EpVanA=Q!XH3z6Ftxw(5tK&}$ z&=kCbt@&|AMK0aQSE-8M=H2`s4qqgy^4bH{o=#UDTns6qupTKXTEVqFD9v~86IIWd zjP6t5U@=`9wTQl(3a zh;)&r^dcfP^co-(L3$GqP!NLj7J4tCgd!pxf^-s;UJ`1Az&q#t&HUHQtTh)iS96gS z771siaFXZw?!7;o%(06OY`?_YKz+M7)K#-+0V4Oo#SE*#^X^FI`%5gH=PU{Pv1t4< zIqm@1fJC&N)?|4dfEehZ* zoZmMEr@JvC4_-JRe6r8|s*@$HKOlMW)jeHw3!BT_I7EUyN6>=xtsV~@>~~7rhrz8v z0gE^LgH^GD2z4DCU5R4#fiJ{WT0J&wsm(4XeP>okfHc#Zp+gW-P0QeZYpd4z=YrC} z85PfBpoqlm0wN%E0Gw!+6tR}`pht;y=9pISRZDZ~{+S+n@iRi-gGrh!XCzlCedCH2 zM;~zl{6*AP}t4_W`qky z%oNoVGPPM)U<>`Hg-iUhYj7(;2lH54+*MwjboqkDwWKhY2TX_H2qjhmpZl*aJ+u@p z_mF7e)c$_9VXByeX@3jCh#t_5?>8OEngEELR9YX_Pxo)eUurNmM)VCG3M zIS0o`2AkyeQ_Yvl zqGp{p0&6e6>ypgEm9g3o?J*{TyCuQDgdowsfijl4y|`t^y}Z6iTP2?7!2yCAF;zz$ zC)#w`0~}ad@zF#1c&9gMd$%fc-^M6Y&RxH~Xdk8&H%_!Eh2+Y$HZ`Sbw&Qwgw6h@< zK{aRe_gpv!#L@!c=P&377Cx4R0YP6z^!nRqsir(>4$tB?YHNf zW$%6Uk-~=36NO7)&oA>tL_5q2mCKd~FH2I4<=^~rrh_ccR6HRn?K2UPQME@9`i%eOeol1yy|XS&%RN?1dnjth!QMZ zVfDUik*>f^A9$q8tbnARK!P0uIhvBc4OVV#;^$N2Av8abL42|=l6B2U&bLw6>JL)d z^$;hlrAScNmGP%c?G~hGn8cV;`n1HJMf30q0@jRg?*`7b+!S2N1%>wHZsFBZoe5ua z;`K?Dq?ZRB-6x8W$xsL_M1MSlgwO=ky6#meM8>WsAMm(fERqQ~!8s%jKh&>>$g4KIVH>C_=rj?72A%C|Mv z9yG66gNa@Q2nIw!C(0MjX2==TQYP#u^QmU-oQnN5v2k`^PTWIzQGmU`P7bu{4Sn% zIn$DDo^qcR?J%1yCCIb|Ps=o=(GDrLN2_t?#jRdFfVoT}ISGTkflS94!0T!dU#9Th&QrAd2?A3O##D6a`HF=Y^uCT!Da-G6{sHgutCAxc-5Bs&THOhy@;zbROQc}?Q=Y+ zXNB7m(#DiB-H_dD7NjS4%}bdWXsqdZp=YK;nl#(fmbN0+JJ7Fn_TcZ%buK#Ccxu(N%HHT%8-T&7~Hs$^r$fud6 zWLoUEu^$C<%~_%S(8nCh*z z!TtlPv0}@9KN~w!8l4tF3RY0M->h|AXtdeeH^}J+314@QNH0At)jxz>BiQzeq%N2W z%?fz&w+=T?`&FGw)qK`$t4RxV-45`rA^fypdQ=lEFi1Mz1MA!{F7MQ7cRlXdK4@oIft2zM@snnn@q5LD6{pAQ9V z;TG@HivJK4P#*n0AResaI;n3_pZ%$7w6tH@YtY6?Aiwxeyl!*2d21@GtrM?pq$+2c zSqLX?;2Rdw#W%^&9lT}!!Fr$*1*|4_VSsht-?Z{P+hd{FpHYe_aD9xO~%=u~JFH(@j zoK1hae7|oY_c)$=nK-psRqzq3;EOZv0W;inB&uMBInEUM9+RgBqv#;(U0k6(PmV*M zG}4nmGmcP!G4cgbnD~D9StgC(Zrck(vQ_#$-TCO#z_%iZl>s?POUvl_^iQ5cAaT6u zvMew$NrD#dsD=-#;ii?i@z+wD(-z@U>J5$opu~{q-EGCd*?#LLncW~PeN3(h5IlMz zwq!X%rQ%=AVlwow+lQZw$9L=1b>IsKyq8@`e2EVy$PDN|Jx`pJ*g8WG*Z;IL7?80V zbmUi>IWekU73S~b7MPH_*feuhQCoI7d%VCQi;|ZAYWzL4c4gNWYfVbPg4m&>OjCR{ z>dbOpT6N&hKVG$`ukEJ|1kEmZFZSC}k9qQVJc|(KNnm;vls5U1m0J!+mx8~e9{m!% zD$zsR=daKDt3v&e^y^Qj_rG97qXWU=`$T2$qHerSwUyN6k;J>9$9jbq9^~FDcdkTC z3I3SeMyxg6iqrYebErxa8~Qc*R}s3&U>zSI)?1v0pf-OuBLsS#y2O_;Ao#&4?e3jV z6nDM1G{|wf?|rJH+bAcF9kZx!3#IM)3Hi%2yXlrm{Uq$Hf}=Y8u{^W;D|%CncG2Hc zceL9ee!2IE^vNB9b!Y|Cd;-9eF*d55GSh7l6=4uw(dn~CEIDTwWe5i-F5a4pTy*34 zC|Yfw4)bqmZhVKq5g!51qi z9Jsq|>U>&DNsfkljd!HnSbX3T{Aa|TXhrC!k06qQVmh99+aS`)Oy`*6YKe+!RQ28w4?8*8BSEYi@7amc0$RA z$k;q&`MGxdH3{mN5f(k%$)BQzSfvPe;l?z{p9>5I)Ei)Es>Fy1;oI7Ub(9kM3{1zb zg2zbz;8rH3ol6*_Lzkjp3O;jSvtbY1lWE42VZ3HGjo1^>w`bt+`S@SOr#wT4N-J9D#bp6SqxEw-r7H>p*9u6DfhIo-*iGCtQXbx4!?8g7aw}qK z(8+6vc9^u;yX2W?f8gCW-P{AUfIE;7s`>0}em%!NUzaC4zI2NIa#`2fnPhnUie*!Y zn-By98!d~we%mVCUS>{+Z^^4t0t7ci!k1atUzz#P{Dx6%{MSJ4{zEB?hiQEfbBhzk}GqzQ?I> zh{)YSeN?pru)x^`1eRX8rdHv-%JqSXDx+U7zx`SJO_m$o=nXj66g;t@JB~+?5WaTY zF}wNGw1GC?(IeOPochPI6pW+}J^zr9!k%Eb0u^oSr+pK(D6^;oP9RM!()gO5R}{tS zOeww>f&}gzgH(g8aHmv@;Bp-%cUF^zFN>P-CH!A~s-tK-Y*8c!mpYWQR5atT$yV zW^TEaN$Y;UPyO+q(CGQnUsoenH>Y6o*kFeF0lOOf{c`6=Q}vSB>K3^ho&L88V^6cr zmwza_>jn*fSRrp)x))$PWBla7mVwjN!-*5f(4LT#x%^IrckH+37ENyO*(c685=0q+ zUjyB7f0?bTUVDTMkuih#v+=k*?8(Whw}pQs*rA$+psSy>|L{adeJFM{d_li6ATLV# zI@2o!TMDK*a8kz;?xM@D^H-nSlJ?;5WK#aNcS`S%Is?IX07xl%|MY5k%$bdTp4S9d zP~jCAG)|E*2n539mR*~UoKM@_v)BJ2d50?1X!YIoN80y_3U8xQZwnNx#-s^bc#LwMvZvC=-@IXxND_}h4p~PX{|+pMhRPwo|gdG z2rv(~tG>SMETLtrAFq=e+AxyeAGl-7^JU1EfO5m3`x-4IH6A=L4rhH-j#cfOa| zsJD_Qq8Suv2_Immt*UIZm>-2R7cL1cZ(T3BBKwL)Zj%&LsvT{y;MLj-p**jx4PLDp zkHT+++U0OtN3#QaBy?M0V4TFTZ{bKFcCPTqq|C9+XOK&9WgljfH|JbdY_Ve&j4UK9 zIPgwG9`A~M_&xg-_*m-9Ha*7Q7EsCg@Yr;rClhwbz)*ii z(kT-ti}n=JE)*N{?cL0QtHG!f8LU|Hi5`V>PX~bmW4;IOJN25^7>6d|-5be|C|3jq zD37;Ht!Y94$4Tm-d3K*nmR|xotMA;mB@1JFluk#=5i@lD3eFbtXd9SL?{^5>tpoR~6#9$D;TE^f@BW-oG%*SwifjFf)>*z`#$)Pnpk;R<}I@}vu z0F$19u@r9whC8#Io$qaAE$G2&p0+262OW1;S4oX95B6m#u4k5+^Ztqa?Mj2w5U(nH z6Je!_KRAubT&<4jka{C&y(h1u`YvnxT`Yhu_XMSfqS3zYWag#|MJLU% z)k>rF(cOtOswjGkHBcgn*D0vZA#69X)NV$U;RjYeTVTI>`FW#&R|TsAId^&)B4i|l z9`FN~LJVl1p>;Hg3m4k*gHg*{_{7P(+n!d%<*IUF3IiLbemK{`FrP6>YRzwx4XwzD zxA+%EK~7#Tk3V|R^OZSU*ULmo2`0*IRW-&3tBo$2NnLhc6rMf4reXRy_^JdExWq{M zohVN*2eMU>O)?o)T#XQUNbAVY(n*%#+!DStEPwAM3pOO%)uFNywOdae4~aK@VW@nU zb+J;;*jkk&z^e8_4hk`AoFon-%oYr$zCoak5c7?sy)}N`cb;ca!H`NE1uBsqlN&d0 zAL;xiT$5%>^8rgn0KKuFLOsZA`~S8w^gmb|ph!c;W5By#1hWJD`0xInmyx8wz&<04 z>Y)sk0I6f)h%gDG!8-$Hk}%zWTQbOQuu;T`|9e9bP)+(G+02YYAU}JZ3UIu>f4`*- zM4h(ZGg&{@O+k3k?mj0$2{CP3O!Dat^Qd5Kpn({6d11`_%%hQcENu*1i=A4Rg$ z7WKxeQ)Q35)+6eb0`bNuMMG4bLQ3_5%b?~1ka#kX=c{fuJMaG_{?S8u$HF}p$-HHD zx|I^r@1uVH9Xd8`JcK<|w0t4|S7NuuLOH)A&fHgl?D$}W2?6k@jKS0Z@w#feP_uBZ zYNI9LV)dHh&^4rK^JUEgS96I?E33~VSGDhy+*T}ns&Cr0l*vHN1}BH5dzPg1<>4BW zuXXreHK7|8AQul6cC|Ba=v!Co|rAjSISAL(U(!vjikYy2WXtx%(K)&$%wfKqH3;ExfeR4kAm1&pEL7@Zz z$$zn*=pIp4+uILGTbLgXkV9Q}6&jhi^$tiXCR4&dxjU|-b<3(<`mP|u6Djxw$B=X_ zjvtkkTi|k|z zCf8zIqvR33<&OzK|40rQ52aGzUtNh%66+ZEfw*#SV@Kr!$l?2_!S!!{)Y-msUZCld zknB)z%t&*uA_SG?5@=QKn=SjUBO&LAC-x|o&a3-kw$L=>ClVB|iFWbja_EygI4!E1 zACWF7W#h6nrelJ3KOy7i#I}nv!o(+GmNwr^S1$8V`Ae$)PJc8Q4-#zrIXRPmgef@z zLKXv21pur)+PJ!9Nim=cspt%sZY>S)GJ95Wn6Dnuz?s(K6-rfIKmb`b<5_ygqQSRd z@LfN+UK{hRCAR!bryH!?FJ)XPih>3c!4AP>{mXptK0Rl-v1qC654V>r9Q}(|_8!Rv z#E0KJ%fC2v#}yZ>0*dO(^X#Gz>)W%hA!r%=Pfb2$84GwZu+M^A7{Cr_j^|ld1%*)s^ z!i6^eBa3w4-Ul6u94FC?=G&mZ4v*G0NRE$B^5}F=t0rBglEailE-V}T?^9bVfyLex zM6udzLWV;GVdA)v92-vY?+H?KlK3QKT#ew3L1Yi%_+vB^HxMr)R zid&r&l!MFDJ2m5LXcG$uUcHZ`sHv`&3XM89YF$Iz>2J0Qm}!Wy3txb|=bn4q?zILV zZ4tIUHO7ssBSHg;ov-fU7TM#!xC=?TYUVYkCokXg@JkI$dCIlXy973yyy7MV4);k6 zdLztD2bmTplurEh!&UsxXUF~_xdHS~>tZQlLqseInVQkB9#-pI%P!`5mi{Au&l$%3W28J@Y#H=bh3A=_a31%gv} zTJ3S95+RRX0C2Of+}!M{Q$#mpIn4V);B*T*yi3!GJdqs@53c7vp06Yf_sEIBZE!&- zX0+H=E(p)`+r2{F^>hDPCDr#N^|GBEtIJZN#AKsx6hQR)@X`%j8`75n;)xhJ{6ub@ z&E-KMTYe=<(EbKp>+xG=o5p_rHBZ^!fY&m>-cXM8@tkPMqi4YnM*FNpb~UiPU&SQ? zToAL%sBbq)N=&Q17bVFy^F*qXMC1moqC>mkQA*tLtHKBIx$~MtKLsyl z=EJ^{Cciw{xw=ju1DYORA!iDaGcXl||Q*u6pIB2rE7 z?{{*|RpTm%6~%Z6JIsUd+8eVVIhjZ_PO<9XiSP=Sa`_$_vymH6t?&NciUYtM`=feA z>7y&J-v_NEgU<|Z-pU*Vi*3X$p^;o$foHJ_@>NS5$<2;xDo%AT_$;3_;gg`;)1-&S zNy65E2OVfdhEo&eaQj~{4MEY|cc)8iO11E{w30R<2R~(ps_HmFl&8iXM*ORo1OXE1 zk-B-(tyotBh?7VW9*EL8-x|f_Lnr*gSU5cbs-nRJ4=PQp%yvTVx71Dj7N6TP z4vY;l(Y*eK?+KX#8Gv?M>4wvH*x>K1P8<7Kk>U1BLdyY86E+B85kCX>lppGO^eVai ztEaV%qhN(oD7{d|-;4wErm0U_6HOSNVGs}Z-}@cgXQ%qOzRcNFr?#$`23qM;L#fe} zB4-`ig4!o8d`fyjt4CIyMNS@@ZLWyOx)gU;_I_6TMZfp&hqfKb{^&+_(ZU|C!RG+) zYk%8igE8^}GW_uLz$_BqiWGq)n;|GCNbzObTU9$wD~;s*$~U&$C{E+i z^XOht4kBnmO4tw+opd+&8mV+%+l^YPVK)kqEzW}dh%u}5W7j{Q0wcek&Qc@UQVM)W zWv+=qQ1332VM0dclCvEKSu|s9I!wL}7lPNk47r}=V#8!bsR&t9r>TV6RZXJ4ZL?~2 z@1ZmLTypmX!_v(UF8#W6BuinC970#2Ih6htVHT52TQVofkxVI4q?F^daWSe zjdRs_l3R*Efk|d_3gpdK_srAzmY4jZW1?70$N6)Qs=^-)vMQ%#Is!E&nPT&N@1r)t z-qnX`MzqoC=h0G`WxHv+02c|T9z`b1xy9%*JA@0X%Kh$K03@@F6@PlNr4Mku$D*V{ z=cll;$niaq=hE!1i`;J=T#8g!xc~N4WelD^!C5v)*H~-==M-YMI0-`Ob|MYkZ4$FH znPnw=z!~l#Z)^&bXtaS6U=B`Lc=PtWbR(5IP1=@>J*hXln4P8?9cDn@^C>q!fIb(E zRItNX#|2h37OIq5=a}og%JS0v#K=$tZXfeBX`WAxsk-948aN5&j^1&_#Eq<2ldTD zeI7t};!vZo5AjY1+6KoTLm$I~rjJ)^{8OFkaS=A^-TXrj%zhCu$W2{Gc#lz3L7it? z@J1eoyaEO~X~SNvqojLTo!k*lbWv?`yE?45u5yRo!mDJH zJoq+7cMfoM({V6B8bC73U{q3ygDG7K)VKWx$udX9TAq} zD!}g2<68u<;Ng6qmINKq;~I5xDv+<=d09sC|Jv|SZr0s#HPR{f1}QHF)CKk)pAB!7EsP+L3Le2u?1 zSjNQU4~{r1^Acy_&Dcy8@xzEBf@pFhOwplN35+{P7XAZAo%dH&F!D2SI4m`9%f*eO z8!u&0etbu-I-`2W#Z;tFk$RflY)Xx*XK24%&}EH+pgV?euh#tX+TKZgkSbMTO)_te zahUh`8A5k~9nEnNp?iIkH_B3Lgv(4Y_NlvbhYPe9!Rzo@l~I1tLdXgi37`+HwCu z1|^0vwfwFf9m!u@z~3(R-%7mZV;B%a$T%~SHOwicDbmgZHz(3h9&5@&;v+9Hu17H~ zr#nDT4w>ILm4;(or4I~+^vu=8gnr7s-ys+R&C2mWPgCWrzVj8&)bU@0#vN739y4(@ zHFYx_JRz46(CaTAJDz8}cXDtKw$BjXY|bdJkY8kZVKXSCA)M8UE9eab$ATRX;*5c+ zCx=q2e)913?c01yh-LxT$AEk9VkDrRSH{x>8r^l0f4Us3Ge7_1K;#o>rt`fh`tynl1TIu}#cj*ls}GVlB6UbhquS zzv~9U+Qd@O2+s$%DQyBztbe}2wguN6Xlw+rItD6alm%Cq&Pz?Z3UT}V$XC_Rc%%87 zbOd&Z6Gd@z1I;TS&SgTzkI^&ZmKk5B8HX;_07FR4Yo5bA`u1e4BjDg63xn|&U=wKS z!i7P1Q`zr9FHHs&*1i`nz0?+i0QQ5RD*i?`R!2%o_|ZA_rP7P^)@F}Q&)FLt(LLaX z(fBnk?SXUiWv5f)Hnne38DBu~4CJe1ueKy&_~SjRPQcxx1e#i_nR3C@)h7SZhTh_F zrneAaI)Zm!Qfpl;6At%EOc68HG5d-23OCoE6)(OiVbh-l_A7&{8+%Oo(1`9UP-cJb zgUMZ@W@)A&q(e<}`B`FTGS|*DOn1$dqQ~!HK$UY-n_WX-wLfBhrgESVk@9P1$R5uUgOr7;p#>U(2BUjUkA4kS-*_zX z`0{s*nDR%TF$*e^^9odm-~`dS7;zO}y0Go%(yy@NzbHeOcMd|RW9iu(Lzo_ix@@Df z9J1I7E?#|;iDYwes1h7OQV|ucaNt`bYr`snY`b^Z_7mckM*DNEJgM)3>Mwk)s1=vouglc@eh*vsrQ?p*`B$^R z>#Fy{Vn2(LoD2rGyW%SPuA~W?n1x`Ot6L*{1FU;hdr}aOUM!zwCC^GSGGGi~noMRj z_qkJT341gBD;!*C3a^{+X;v|r@&I-7V+MkK4WJ~m!M?GCtc+v~(3K=79Z|T_?jU62)FL_iq9g`=1iGDglc-xPf>;l*pB|N= zxH>3?n496xq7~#l71)cLZA(V3-w$BG`aY{o3tK#`&Xxo?xK98eM&|S~dZY@j0U46I zLFJ~jH0%H6EaLB1-%^0-!UYB}ZOOD<@jkyA%|GB2KOJ!W5~3+7v)j}Kz;7`#A)-)0 z)nvc0c)?X&tac7!(ebDy< zUnr)<5v8DcCxtZu-on!K_+Ou7KVTCTUG;SHz4RK>k|Fu5+SejZx=42-T-~QjV-59f zya@IiaAkXmMryu~ed!q<1akg+q>7I!z-l153G(U_l8Wk9fTU@)q6as^U%UDIvFr1{ zq-^&5*JsnW<$?#`tDt!n_0Ftb*S4Y*pQ*QpdlAT`Kes#zFZ3wpvef&NDIm*tUFl!q z`Emlv-?#79jIxb|0DcH2m>v%Pe*at3EK%8}$U;ko$6tc{Ezk>VMU98|w1GYx!^3CW zMADs#EKyiN*3=-!aE;ek*B?Khym={?)p`NmY*UX{2*&$e^!YUbgZC-xAY;pCyeZ!r zUs($?S?Bw10=%h{>eyu_0Dk09w#|N-*qFFF(l930hnjgji$hq`6VkNsFEhCd3axue ze|^_5-CLAPl}nf%j@IlN)GR+wsU+HuL z)caQGwsLtc%au5l%YQ9w-$E5^-x2b^*{O2l1t&LHj^q?r*{`9lWT$d3lxOyQ&1DxL z!>)JVl#HJn*~5WEXVyUHWxO5MQfckgqMrGn81H9avRa_u7&TZkw7PN&795Szs{+74 z4Akb4Q6ql$+qtT`MOB&=$xV|n>TwDVxO$YnQx`7E@st@me>i4RPgCEk7q!&RS?}GR zt3wh9kLq$trgjdI1_Z`3=Qg)5%*j3(=&8=`5wxBJXJ{R?TB`-05eX?+0MG#F+O1}x z3fd{H>t4*Vdi1a?Jbk1R!+4{6d>5N=wNIy#u?!lcFbo|Q*|DP;PU+@JOIhxSelgBZ zD8q1sMqur=tEnuU4YecAXmqllEq`$s!#o8+ET9oOjWdXbR(G3Z{Z_RM4tw{UnUEvW zO+on7l^9g;6`tahwHG_}tbU-N7!I z_+1RYSj)FTR!{pi7*awg!nohu0@q1rmpK`s{mwGbP5$h`mv!Z%v#OaN*HZh^-~(Yw zNi#rV+Ce4jkW1ZaYPY!l=Ek6>N2%?~nfT3vh`3cGJJGNc&))}*l?y5IcFB^7-uPhl zLae-|At+uV)X*w)6?|=rTlO+v!668n$i8AY?JAl0aK>%ciwD7xDKF>npat6jA0*Rn z1Twm+`}b3>IL&vYLvg7pZ~NV7I7sxvz%%q%E{#0;dQs#K6@18$ZfLaVZa~SaGebM9YNS#nVS9SmOxL zAk&7)CAEuoXxR6Na(=ABbemZVIYcE_sb)&fs#)>+D}5Ub>UWKzuckQ5|y{ z{w@M_nt8?3YQ+d(?AX9Y8vtLp!s`+8TbY@Y-+ItOsp~zfI^DafpxEjU*{7M8c~{bI z(cRosuI#NBD)DMA_=~| zRy_|aeBHzRi#B54cC{+Q>$iqCo&Y&hbO&`G7^gSl-Jhv8jA;&YYoy?JZ0n%q3yFzH zCfu$z#IfvB#sB4Vt<|cg^K2pS$A#n zxHphK??&`*%*ZQZjWe)?-Vxq7ar7 z&RTX|=1gNfZKW|-stl^GIWx;*1`|&sntOykwrD)-31-H8$#dG%yxMUi!%1OZeTNJZ z+&0Gbv2qMoJQksMQ1!WAC%(=C5OeCZv>S!=4YA38Ol3gVPg;)tjJb-I=v%?+I^Kn# z6lFJ9*)AIbu4p-Q{0xBBBOV88cSiXBck<^eR79Px)a1KhbTwfXJndK@!3j^INmPI5Fe78`H;q3Ov(c^`2`LAzp_ND;g!rlQl5^gVEES z^TSOU@0PU)gOKlSqn(v7t@z&4Spy3*=iBlD-mQXcR{r!_CtbCVI=~H`E4D*dOZl~Q zaaIT3+kw`-o$M>CA?3f0+*T6;MRXwZM5<_y3#0s9WD3Fq^{{_z*JLNxH@u|0EHve) ztIs`V-WsJt<182Bl&dyJ-T++^3{i|m1Z)8LoB0TBd}xgDarT~wE1!1e?S-Gb4ENqb zDUxnhYh5;j?K`z!;X=}@pazpWuhU-mU%2L{9s~=1v6$uY;gGPjOytyjw zGPM!$!(vc!U8M%aE@m&Qh?6s(w5ijw>YYJK!_fRn6oHxMRQOR7{;BN>L^c2T>iqP~lw?`KO}6v)pHAjQ^dQ3l(zXk?Rs5ghykDQ%g5~LNx)Z|bfvxO(!KrI40gt&YRUU86wDsL;U{7s||5Dt0GboV#2=r_1 zGzFP;OccWJG&aZjpdmqm@$mE#>VHUt7QlTJy831e!HKGTm>q46YjF?%yG|^w=$J?T zKL;XOfOISX{37kie}v}$A}nP`BRHa896hBB*wY*fz3NnoBT?=bW{eC!kp=FY`afjk zf2CxB!cn>LYZIC+fs^C^-ObMTw7_F47f_>t0uX`!y8K7;{x31|f4w%)&t2O5-|zch zfAgQ~T9_tw?@ljsm-DQYw&J5-UxhNgg=~_WbR|g}Ro{%Qxn5gPmiyf{6X*DAY#|E+ ztG?KgY|~AY0kVM=; z0&j}%)=4|!(R}adanE)1nR0hBnR`}fNT9P|5jP)OKAn)xc zSM-Z`db0`&Qo}2Ce%moVMFw9bKhSm%37x5@xub)hMO{*3m69Zv0>}3JcJ5|K8$H6y zZhl9lD`6WePayJJRRv9LQ31l{?{+xC-@3K9zkEcFSNk~ssx5DNFSP#cBKPJGg~oXg zuO(SY(orH9n0a2ypwvp6#ZUF<}+w4|u3iv0dcuWTM7iVI0w%i(cq8|j*_ zBu-}z^MmBX9)*fM1g2_((H)i$snJ-(ukCTvRqdXG@y)AKdL~Z=qbg%_`R zmV_dHkZ|5<{}nE9EvaHvkps|`;;XFY|F&f$x9=M4gWDIypy!4U2%&j{SVdcNY0C4c zYL}+wYW|O{kFVv z)q)!CWy&`~n#pV>;(1_hrQu$h8-GYoB!Yv>YS^eb10vs2m7h2-1GTtS*{Q4;Q#5%J zB>Z+^Pnt#F%KUd}1~W}jv_8N>`@;uN+D#j>5yHnS$MOh`?Zd$i;;ekLJ8@f-4aSUX zPLjG1hUhnD37btANwwAIDLtJe7hOG&%CU3Hcj}@z*PKP!vF1jknqa7}FJwg;7~( zoBL&4Ug}z9MMU6{G$M2gov!Xqxl3u(u}@N`JKd1OXOV0UaY@7-U8=0QM@rFi3}Ne? z6p__3GPDp6d{NK5vG`qC+lVAcX=eX9kI;}v^}D3{;4kSLde1w!K94Q`_B5@By~6n- z8zf^Dbc@u>>i@o-srsndKE;-IV~!|}JAsE4;e4dceb{Urvlcx6nEZ*<$2;_d-Xx5@ zn5=B%Qmie?OBz^THj0DZcpRDXGLUKruRiF?(!0X!({*EI%*nagPuxm7NUAsQL-q3q z%S}F^93#G1Q3m0?y^+YiV(w=1mNs}pT{S$m)qc4vg-E#U!% zKJp5*uzlMpFi7J(rhlpbccMZwb#mL!zEp}L@5fieSJ%Sv?~(@ggpI5fobu209XXzS zkC#_hIIh;3lmg6bw4DVwr`wfs;p%~+9m>T{vzo5{p{`Ey1zNQu0!9LI zH54^@R9T=1pK99ed1Oh+!e5!d%2=tzu(b8M($}(k-CV#qj1x)5SgM$HObI_{Vejv5 z?R44r`H{xqu)q+Qok&_TD_UJx)HF=~`G>^VU#hp##XI@aM%LPu2+U(LgqtYRg15@{ zx2=$De_KN>_jw~FhHRZH$}2dML|JrqaFwmmzC+Fh%M9PsEk;bm^JP9+v>9F8m~S_M z(H1NaYCE*KKI>S_3uk^UA$dmk?ERj_AS#)4o2+-XZ8p9`8?(@^>S1)C72JeNpec~x zHkX>-3Q@qFwif7CIGry=?tgK+nYGO}J;*GV@nj93N3nH9JL$8+X0cWs2kz>k+}~h8cq2!_E$VF*Iszv8|!p6Bcy2}TW>8m8)e=J zxF1~mvvS#VlgOQWqOMMe27<-T{_OUdK)CYiiY!(bvCp&=lE+T64$}rIwdDc5SIC%I zhI!KQ6pJ3k{`iCWa*u?R&G&Bls;^`QxPIlmPv;%8N#O>4+Nhd|p&-_j6c2?K?ik4>eo`1x?Ch<}JF(3LvnQi~t&g`m-8dv;d+m&f z5KOh$x+#ZxM6TZlQSYc2kVkoS7siX*=*y_fHfj89ZF=Vva`7bVCU)$)P>gcVae1IS zu#2PEQlgqscu4o9U32=-y<>=a*vE@*!+S`*Rk!!9A7!3v6Gz+Qb-Og1(NMn{DM}L!P$MF<$Np$XY{nwag zTeiKpg9cF!ML9NewVyj9EB`7jyJI#_fyyC6g*-wN)l0U9 zTq}2YbxywTh%CdSnIL^gY24WdX1-ae3a*ECvI$l%YxkA9b0G$UdQCGz*!KF?r0x0F zOPu2^?>x6D^Jsv&Xj$A@ge}^3Qu3E;{>(o2_Nkb7y#`U@_Q94#Qs#vOfm$1Vw<}t= z1-klmabfFlN#pe6@Yj74&3U@ua!W%jcWou?O*76RYAtMG3r9WTb?&?LyTX%G1C0E| zpBr9#Qk{V0!Sg3eh2DfcnGkApI<_n;cg`vt;i|mD{am{_!^Ica2)~$8;3#rZ;+ugf zL!4{3v+`BBrU5>qNo_>^)>)!a4Z$082{yo!9~)6l?fkkLU-lRJheTxYR&YfG;6v!d zijoqH`vR-`!1O9bfDgXVmFC=Kz{Hor>$QQxA&1{Y#E!`xU#gwH|A8jA!0FAC?mW6U-8p#ofvtn9yHijJ;`6)a)X{U3!dTYd18f@dUfuv0 z>&~hr2WCB~%_`bn*j1a2tJy7p^TgkFh#%ci0ceU1IzO$r2?1r$N4>MAN{?I3A=XW; ziMG6&sN3V=lu|_)-S{h>CPIqCDx4D%pbE7Sp#jd zrdNiPI!#JuA>+G#@-10JL*cFSGQb#hlJEx~p>+wr;uxy3Ky5VGxVpHcw`RStF;)tr z3rRnu2=7`g%;kybzXiBX^}m@6|7M^6BP`Ap-O5x6&d@JfH8O(M%_Ibn5Wi$%TH%w@^@Zlj)J zc=!Mkjk69r24CCE2n6^R^K*mlRQ>BJR29e@GLHu|6(RY64`vNS3B=|k=iC5hP*R6w zDKz{9>2$y8$33OU1)-1*<1Q%Z)Pdl>N}jM4!dNR&qPbsiM-y(*6i{uD|M518HS$_{ zd3gf4ei*r?DkF*h|KVcjePmbx07@VL`B4z(j!`^rn%qU7JayUioMBD=0)%6xRv1YS zn+FgwSM0;5wYoG=4<2bb=iuCfSF!#!hVs8!Gy-F!LZ<8!Rd za}=78ZLi5$E=no1{(x;j;d{63Ktf|EF?I9=Ih5X2>RlMQ9v&llNo7H=Mutb(%CQn$^FZL97l&e!rNEXR3QoheStMU;2bcy^Z0V@@2C+S z%k=B@11>k+euMl@6U|O#vE1jS*h@~!^kid5k6y&ak+*E}$!Tg?VY}Lrer|CV9un^l zK7*mr;{nPjUhXHe(+n>ct{ig1AQ^Wgj)V5%Cj$=)DQASiJv>5H1ikP>OFu(<>f{B{ae;nSV@l{=)^H>I%DiJHCs8&d>!v8$4!6wJUj$@t!eY!n>V~g< zd-|*J?%OMUa}Cg&6i173!?48RiYetVJ27o@z2T;sNVHX0zQgZn6k zQO3KnpxO_Vke{}ddC$v4MZF7eFRTgKD?sNOm0YiplDCii6d%rgx%lGu4K*g+?31xR zlz9a+l1d*${^b#O3;-c8UPL-=61qtwqv*X@Nnv;Xybq%Jc%1q!IQvA`bFjx9c##34 zSrneG^av~^lhf%%wOnS(w&CNo3sHyibfT#{>Q@J$*)B1W|(Z$0NNN2r& z>u1ZH#XBYLEzr4e-cokZqss^DWiNs5p0`$43Fp|vZ=WJ+>-*Haq;v(X_Id@06&V-& zjsdK|N|;g?!4b<%bcmA~3=~^B!gm@Wl{ncizo63K6GGF!QyQh2mJSd3^XL-B^+=B> zP=ZekQIwgkfTeqnYANSxNx8T~-+hfx?xVbXW}Jb3J0T7w)%q2qmeq)|tt%8Z9Ed8E zyYZi$Jjk}$vj>c;vn>*;5EDZOW#M0Jhb>ZVX7wFFp|iCWc8W|BP?2M;>!+mf(EYKv zz&_rH)7QS($tXac^eG$2^wvj|i@)-l?;7_k5jGuA`l;IXc`9AHt5#RRq7ar(C9o0JS-KYGRAL(2|5H6FO2pSa*g8e#8_ zzFr}EhojX6LgkxBWlD;25xLL%ljxYz=&4Qpkzk!s9LeBJa)aj%VJ)N7ikC=}QUDuU zQaVA32ZV{jusaQ z%?D`_j(a&=yAo+OvHW8{A|Fpe|GtnCm=;b}^uJ#o{K`m#^3t=Fm4k|KEQAyw9=u~tHGs5!x!JU&;5u%61Ps4z1wfAaGC3Rm=g);&ca&S%Odq7+QesOC`O z#oI!sNnl9q2FIxg66`B{ip9go*OAW7HsAPnqU@5NFHJmZ+Y4Z%)^h@#W+Y?p5>o-d zRcizwzcM?QZ-K#)Fijih0w=>KR;Zj05m&5>gg<*sW7B~i;-+QXR4Tcs)TwE4uW)R{ z%d2>yCa)=kx_cuur7fEwcy&<};|~aG0h_ILD&0rq*C(*D`GDhcBrA}^>V8D_!H;k| zBEHqeqRb6+XukYn6f@0T#?UihKlBfw>SI;Fq`;o6+Qlxj6sOw`qv6~B8K;k00Ru$O zL|fZgi_)}s=YEKpcvnS?HOFqP-=d}T8H1D1*XkN9>RSD55C2O{HAP!LFeLQ4Ovdsy zg_M>mzTzN-=tcAJrQpCpm|HGhmIjBqzWa|FGduLfT|`Cu&&L}#hVRLFfsAv|cL>;e zTwtu3XA?~rS_{`oXKB{YSVmp#y{>9O-_2_m&5+3`M7g;ocd6@=q++5r(RywV`6)2C zsj<2#qvvf?b+n7gG??hF#3)ze?i~R&EM}nhj*#)O_J~6&%MnXuz-zmQhIP-fR0)7 z4o6o`#MM@+=ae zt5h>#7(D{NO{%?vp0WE((b(S4bx;P5TxM{FJ6f9e za*V`!i9V0`QZkskX_S^;rCN9WVKF5LET%QA3-W)ij#lu;gPBmRovl|6w8%Pz=GOD1 z;q{09_H&PQMcjJYxtk?=E7+MA(GN*aUB8lAagiaSFg5&@gOcq}GnlM1^`RdtU(?hC zEp8}z0v~b_xofu7edpo`mA{~jTnnTpgC}a?u>mrK%cp<~i6&Hf8|nXmK~h<(jY+;; z;PGBt&x6yy`XuizMa$luKN*5?VzrBwHpH8!8efq4!}yEiL62Wu7Va8gL1ikCKl+MG zq%_M_RlZBUU^CJ-ZqNx!L0SjC+vne6OHtMjrZEaWD9@IsPtVNRZ}-d^r+g>>lnKr! zi(2X&nL@6Y!?f_Un5Pa1nP2bM4c)lw)0OZtk5>{Hwbp(IsjMwXd?sqF_;Q>$8DY+| zRrpYTor-$}J`T7uYX)K5?PpjKl9{hko5$i+}$DIcP=kKiSo>^B(p`F+( zEfXIs=eJp{-1*1f*`oevu^Zm!shzqQpZivL#B+nt^C?g^EmL6n<%COx^Dy5dJ(1$T zu%sSG%2^|I7d3}3jWn>CRFcE0QT}QmyCve3HB?+2ErmcG$?@=}ZSuz3J>e2D;Rk9# ztz3acsaZ4~>C=k29;4M3@++zsQ4{}Ew-Mtrt!m3}_n`Og%JI#t%GQn{CcQB)@QsHv z%X9ijv+%Ffn53^l)z@eruVfuHkJ3!ib;NN^6tU(5@yGvN0{WkA=D%;VzHTn5oR?w% zBv-SEm(18f$wm1&pSJS>{B`7r3X}{cjCAd4PZ&<79d#O48@}5zvz#!u zpi+KTb;#2b1WWH$p(2c5j9F0=KBuv~BN@Ejmtm8xrQ8``4xw21LVMMtzV-##VC-Q3 zGFgwfmx}vf^L@e#Ou1m?lqNe^ck_!$)Vw3kq+;Ew-|I|!}w zue_MQ*;^7_-JDU0tC&6RP+=WC+?5n^b>*~-YA@VkopBYN+|ZEt&7-;==a~(@M5O7o zWW%rb|o}S;*p3tCOo_XhH`7LRsg~sB%)GMcGW7;?ZX!=+R zIHh_qo_3PAPO4sNi8UZS#VwlS7VAUo>pyI;5Iecvz zm`%v|{Vgvh!i%#If;w zHufNL0tfX-GDR_&YZ9%LKaY@Gp4Xpm>29L(u|<7yZ`^9jGCM?IsY!;b4`*s3>Kc*U zZ+Bhy(|1Liw9;MJcskdts9Z>&a2K)otQ}}!Ctv8pMx8;)s?>R2F$#dtP16GM$!b@C zM)q2Bgnrk}7<;?mL-)%|WeT4Oj^TF2EqCZL8F~fMJ6YXnWi9&LD`9FU0p(c%avKj( zoj~BuJ04>Wk8))-tZ!+0uJiM?cwPu+w?S`4On}SX9e6cBpqarnk=PK);*qwg1tAax zOdp?^xu@&YBfeFjP@@;m8hNmP30U@_(xWq--%Qz>^E$-lI=l>#&RB%&r1f zSe&FgyKEhi`b^-9KPR<(r~g;I^z9)(Nau3U1sln&c)_VIF|Y*5)O+Mnh-7v4knH{k z1RJeiv@S6FeF&bV!7b(c6vK|@KACLM|IzuT3BVL$q37!+?HvFUq@*G;fFBcyTaP;s zrFlf`F3XC@uFmS5%gcFe_Z}iC+Eys|?NMl2yw=X39)`89z?>Mrtcv~wAP~kgOzA+M$ZK-xY-dA~{ zOZ{#|gP`}i^?kGg!HKL$LgoR8zOzA2_~&CpOmf$`Nd}JWOgK8m)9g~MoIg0$nZ=FJ z%@6u<|6M&kcKuUoJ1I2ikDMz6k};twl!R6wQn?stK~xXVKWafAk8hJDB#)INq#vd z%eS4??wT?`5b&z>5NUtI@!zywJ+(Crtg@%CGV;n0oT2X?zB-_(*9NVsfPspe)@a~h z_KZkfo!{&4ZG%UsjPUnZ+RWdOU1@An$@8%CCtgxk!zXqGa+0!C3C3YHkgNEO@_clJ zuC{l4A3{F}R%MF1JTtw-P7q7NahAES>qe-5GQ-HtL}<}PzApsUys1Ge#-FVN`T*F$ zq41*a9`6)GMBa$VsrJ?l(Qik;K5Kw!1f?l{Epd)|X0f1cNN};*Z*yCQ)+B|zkL6!Y z>Gz`UnZr@R&&dWz>MWoYJLC$~@s(9f0?q&~ z6Gu7&8A#Sgb_9*qd<4r8$70KH%A1Tho#ziRa0(UbIiQi$dwUQI#2j$jjsL&2nO_vIVd z8{3(5BM{5D)yvHo8tlA*XkULB5KA=$33k8zxDwx5s^#(?( z7_r0#Bpdv3$XK76MQR6O=@`kX4+BDd8+tANP$C`9H8S9i<99^B?P|noa97$yCsbV- z6J(2aXMDEW;+_HD8Nx9L^qSRX_#>YywtPYyKiyLNoT9b%z5O8{8RN}0N)u12Zf^?g z%$jAKuq5KMp*X{;@N%*b5Z8Jw^;9!`HbDov? zf>>_eLZ3hy7sJ^4;K?x<0Lpc)4Dmu}+HuC!=3f|vr7O<>p+Np#aoAOlnIMGqnw?WyN8^Tgu^0WUs(@*FGoy|9%;TU8!!LKZGC9Q`~Wu#tE<-({7|s?;HRP6=v0d!PCS7L z4*`k_M#)S#B)8bh6rCK4N7a_9gNee__aND@j00 znoe#9)d|KIRUX|R9eNS5d*q4?ES)L;w@V<=9r*ioKY9eegNm^#pMul zq~tp5&}Ot-+LJc3Td>ukr=*j7IFx5M{M!+~5ubUVn?b1)0xTGYV5G`6ImZ71Ic=F? zQ{=ul_!MqQJ|>d$-^P7*Zr=nHEF}^Ei(%|_ASM$W(;gh_&D1j2eK0RXvD&n${q!nF zcCfHq+yZvsut;NeAIKQTBKz~|-zVPn3$y(dNfX*`8*?15c7!IR9w&&w^x-u-`+JEW zy{Pj4c00d0)80>(=#*tJol?1s(ljA3epxx$*<*S<@nriBZ=)R$>CvK3PPo4|O7jl2 z{pdSEvv)gQu2c5==Cp4`s2BSol2e+N7S+4=<=q?3lp(j%nNB$UW?-dEYm~ERtfSXj zZODn24Etwo0j_=6!78`##y%DK3ZXv4X4+_ZcP@Ufk~ZY8!K6!u-$!03CuOGUdUl!c za+npzqzXqOXv9?t_2$8?Kd_@4uNfN9=Kx_M9`(2F;;vp4-RdH|lLOvmPWOd}&MzKn zkZS0sVbJiQ{!;E6<&z3y5(8eDraOy4dVZe~A)15m%Zoh|4T9JwSw!kRrXGRD3<0G@ z`#4utr{1T@e=Ww#GcMPs)+}m2x?EY<#+V!8)`P?lvchXxDK}U$#Nr+66rI01C-ALv z_w)VHf~714!3J04wa`99tKSwfXUGr*@SE5hVUWjxitBJY^ADa`Oz7u?U-MVcYTzjT zrVMs@AKo|g?qGuiDNEP|#|(^u*kS3XHIY9_mcyfxMGfV_BMxqCCxX3Rb}Q$?NW6F( zoEEJ!Rd(q7K&cFF9uBLWJSa)4Yp}c6mORG+DJbYs2Wp+>4xIFMw8MegZ3Mlx3Fu}3wF8v9+<3np4#?60CBZgEw_q?p6~@IOv7@EX}%a5v|7G_t_#{V2=g z<+pMy@q=2d?kB@5PgI%rcy5zyfFbOr70PAn&)5`M>5W*;#_pO#-V={S>7%2alg;x1NN;mq%7AKu>2IrF8U0mcFPtNe7u`4C%H3QRB(6YU6Vb73RO zi%IHDmC0_$(H0?*|E{5st`je9pqZ{*43uZSgkK|vu#YAuCci|NF!EJi0Qc9Jn9RE2wE*&+D^ zUX4$NrwLy%d8zgJu0zBSRXZ(LSKL2;1&=oO(M^GO=8!ksWDLGZpT2@CZ zT%0fK=*M{}w0^LEC6J-+Qt%&`R*4KNT7i~eMZwV-^tyAaeEjdpQ~RO zI|{l7D!Nm0tZ`@R*1uD9<}R`$!3=12G{rkp^qBn_P>);94NirBV4iZ=!1EnCzSXukhp@}F(W~6Kb8r(|A1VbT_3_5#xdICyd`h42hS#5 z&vux}$bCCy$NwUVfd6IOSR3> zbyZE(9IPQ$)=GCN&-PEn4@fY)dL}-A7yV!y7pvAXu_LmVtkb+*$iWx>yh_8oNPa{3 z5P}DH#na&6kxcqCE*V1Zw?eMG>zC;~xDuscuk!w$BmlSOkL<+?60n$z&!-v%*@MEG zGF2hty}Ztj{mL|fW$xbAAg}#v%9&aCM932If~~qyr)Gj@Sbi<(-JW`~zWV_9HDfA& z;;x(>Pf@`<@`8?N93gs0Ys6{3`=Ik!Qp$fJRnWy_a{71DnJUV`gw$G$mNW2hE&mzE z&i7ay`kJBEe1=B;NIGZ_^ROAqd7IJXVZaSF=I_*e4EMIh@i0 z*qXvKwmY|97yd%83&5o)LNeu{IThK)czZy)rHzb&DG0aJ_C(vTXcsqDb=0fgJfhXg1pm~?m2t6TM1{if8Cnn zRN8*zZ0Pta;da<;^WHW2zG4pRoo2gyl8PlQM$N~QZ)U~{**MD1q3ZGd%vf_ZF)>3t z>`)Z0RXO3McXNAYy5o3kK4P@BZI>-y@)^-kqf^5B6ES15=XPljIDGd#qOer zha!?!lsOZ%L4n=N5%++D_YaQO>D37DI&Zh-pUZ8OGA&&`LT7qb1%S;E#+19&L=*Y4 zX8N$62pA`3_cO24qCW@Mc!J!_&lNU=Gl})FdZ9b7_cT)dOFqSZ*u^woUfOI}^#Ked z-r0r{P;yb&m*BVFW5oRi}Os+SU3YoO``^z zDPP0z+{@$5igdb%yJ^;9ER$7D0rGlA;K`;cgZ=!7-VC9F1#_Nts03j{HdRER-TZJv z;$2MV70)KV)|=`KXt1xo`tt|W zWPSA;5*}xG|_6|oelncA%&J3BU7A7j;vTp4s1V_ ztLk-dh^r~{68R`4VbDV>nmM)ccKPc11eSRN%QWWIT)}98sC)>;RzDZxqZDo6VPg?g zsofNb`8g2L3x(zjTAZE&5WuxeP7=hMH5MFAw#6gRrlt8|8FtP7siL_dwD4&gV+GMI z-B$>BwVEbQX`g0mNfE>Qt%DAaC$)A*i_|F$1R5iNKG7y z%5SwY9dhevRe5<bJ0HDTpYe*h>_}+der%aIUc!mZtYW&0+-p)Vt{?rTS4y zA0<|JO7hAu>M(b{)d}>M!DA)0yqR#;j%9Y%cFZ9xPNjBv6knR)^tyFPnZZ~62c3^A zpCo-=j6rW9l;)s3uq*3Yjq6oXZf6E#u1F<-e#m$va@|&>yzkQo3GRE!PVp68pL{Mv ztVnETI!7gz3NkEBBYn?5xh$7~-f)0-CeG7wU!JxfX9_h+U_=c8!7bZ$;8~)ZT%G+9z)u{S7VOCDQ6S_VBj&& z0mYXJgmBr0_bKwxn5H*n+be_QocAFCn3>O8%M?3@;-QL=&ZEiS3ClW2Bb{zl%O7G~ zul(>Wf~Het(xQ^<`q+kq>47=fG(;ZG;F(=C{P9d&=a~^Axtk{s#)|JDbXDEetYDD; zTHG~%O=bT##ioBZ*n(qBpTj%3DP4-U$6kfq z;^n{Fi8_SKX& ze_m}MxCVQc33nNr8P%>#KA)%!54ZR%FwPCWe0}aq5t(-8Ni7nu%+youxFm{{shX^X zRPt3iUCnzpa$TNYs(r}2l^*R4?rNpnjg5KRf)DrEw{i{B z4H}6uYu@!dFRA`4+Rpwt_npZO;avKftS8DH#CJFO(jm+~2=?aUN7KVhkRD@y@nZgB zU6ELsU5R;6irJ4nbKxB+()4;LU}taDn#rDosS_{lcj?dPn3SfCD>=@?y9E~ZcvGH{ zR&8?83MwL#(z;K&YDcT;#|xrFhq`{u8$1iForGwA>HvPo&Uz*jHA4>VX62DyzE$$6 zNB#8LEpiPT01w3@t9B{Ntq4swRaR!?BWAVtLwt6^qvG^&;60wsj8C1dlrjn!-CX37 z2r+&+##r-`u4Iq7SA@{3`S=~0OhKqnS^JvZzQ9W}?=o%MUqsIUlDy5p}0Pei=%E z^7P7V-mIbM0&GdT@ezPAR zdE5q8e(O->t=-m)*+k8Fpw9P>*jj`o)o;)6XUyq8^sflkNm%CJr0ClC2gHCIyWk^5 zc5nLdrEIf&pQ~-uXOZA}bALYrC)R53XGe%Z)Nz~-C6pk`hRT0Bh1}~|LMVo`b~W^M z`-O{wP~U>j;MVhG)oNm56xzWD&tR`Cx&HXg(e_OxyWSr$QuS<`Ddw_}%e$okRQ-fW zCl8qtGhIiZtDNM+-cLFI7Jl{cF3`Y!xIcR^Bp6W(2cwerJ2Ux5YYr!PCvnlQbfQ3% zuPZY-zag3;+vr-aIm&LAkwy$#h`Raf^QDIyNiY4hB!Y_AR&Ce^l0B@3v{ZSVR{+G? z-O*zg-bJ8Iuk!~++1?l@*& z{t9g9=7uIX<^%C8ZQ!9ozy=fLcv{TA&-(YujhO%JK&b8DzxTv}|MRf_UgeoI98;y$ zpn?5+p)Pt8Wa!Mrb=6D=l=nZ+1m)e5GsyT~FX2Bx(f=gD{!N4ZAAbdf*6rWSES4-E ze>a=&8P|b+zWkh>@0`k_BDvLdB+d}!&|a5GIcAw)ZYSc8V}v36An)kb7GHf@o!)x7CULMU4qP8Y5gHtDa|})o*C+wSpzfxza4R@?e9^z^FT`07TEqhv~ReaXkFy^&* zH?S5lS60;Z4*o!5QO`}4>$s~m;VBtTWG>GtCJ6#|Gnf6{9q80?&kMvWdF%JdDCk5j zs=Q#OdH)((lQduMD6!tH!ZBUyr3~k`6Lw9H-59t+MoxL@g{UYJS2v9^Wr)A9`FV)? z(dANE#h3T_s=D06KYibRC)`AUcT%h@bCdSFtX<9wh{`{tPN*c-eB5^CGf%cvd87#Y z&&IJzXaiEHax>;F2jD$?ecKBXaHHqB9klXkrcf2pxEnHC&~a~;l-@b#9SZ}0?YZr2 zt&=s-7&H5X66V}~%d1DYnozOR>##Dyj8>nj@poU>3+;Ya%qP3WT*@US;W#oC-TiDz zg@*i)_%;?$@0X(k_{xqW_$b7Z85E$+jZxSCNN1APj)y=-M7dCyEigmruIP3Ruio@c z-Zrz4utX>pNkrp7($1WTO>Z>rm$$C0_aLymTXTECWg9n84&A5h*7bLg47SFB z*GSbCUUG9O8c@rqdr2ChdxZztj7&%GKAA@L@UZ%AJNxeA@ZEApF{n4+KkJdHj>z4h z5$#{K);ArG0_hGAWIB9{xqf9zx6${(qs{q|+J&EvY_e=%`wCa$dKej5jAS`Y3`bk? zM3jJ2WwU%7>#ykyt9cC6H=lo-l25dlI@D2yb^?5opPQ%t6XxZAg6E8d2OVxPE+PkWr^pXlui(Fjc4d5) z0*0u!o?Vl$KNF>#eLZ^Fhxlf_Sh-9Vf?l_>)6opx?jKO+Inpphs&KaVrM=D%dT95b zq~EqsXeOeSDu8wg^=aiIS=?1z8owkEcujR?Y_B1M{~gc{v++ZL>`3`Um(ev8QX?1a zc`j!)s7>BWz<%UQjNxLozrGZxAs@lmPV1ellJL2wM`;Pv9Iu20#{dBu)UV7B~xhC-CT z>&Xm~Wu~Ofojue-dwmPyU{XY?{Z@bFgDTN4lUSR1j_V(e?zQz=A?FUVg4eZ(0^ucU zWsj0Kt+*Ah`nU!4D)ZA+vTd5ATVKZ5;Wumi#Mw!6%jTn=gAJ}}@}7q=cpqNp{@?I# zsz8>O#uPBUO&Aa$9ioB8;|W7rP?PKw_UqN(b^ifHcvt~*Un&bUn2;Wc@yI1bnqa1( zm|BkHrA5q`RbFYyEg{s?zg#Z>9;hP0Kd}o;yOqf^ITfFfL#HM%@mD+-{6-ZX?xS1Z zX3dN=bTsphgE=+rhaYI+8|tePhF+%q%I!^Gmp}X2b~;Ken%))L8txm}9WQ9qlICUy zK3Ym}$s6=}N`ECC_O9w0m0Lz#WjVqFtxOQ87mPZ5*fjoX+ABEshD*7?+kxKZTg75~ zZ~U$vS;tye|IQ*B_hjCj`HfBPJmYSeu*$X3F ztiYEFrKw=v%c8ua4M`iF29Db^zAsKL;YL zspm+stM%<+4NcfEYb&e!x9Y#7yf9I_)Nq#fH(6M58)sS9a;9rRdb7>ZQkU+4i}ecR z-A(fgXMDqB-9lQI*$HIB-n2wN_|)0-Eu}Q@1u3%g*xB0#H5#qYVp(1qSqQzke1$AL zS(6pz&|}t9m+`GkHi_kn`}g^%q(zhDEgDsSA<7OH>}}*SZ2&jfaLidoXIHDn-(^{M zd|yDoJj(X7fQH`E3--!PQc2Te#kyep{*@(^rW$d4p=C?6RHdPNx1iqUtoK&s)%$C< z%-eFy)fZW%okZ$o!&aX%w5d1u64bH+$$K}l zbr$e>vp&LHPWlJ*k=v0H126*FVE!xquAS*jG8o1DytE-nM*Vv|>L{+jYbpW(!%M7n zp}i_x6EL-v7EzWtOFzx5SKr(2mGC9WS7v9RVL(4$+h`B(rG`KsF zxxL8g^h> zROnQZ!1xyH-lgkA>sYead9`=8_AYaNOL99UwS@yEsby<7v2D?#6Dve~*u)xRTLTyi z1Tyx{v@?Gwos4T3eB|4rkcWqZPBawma`#yXav$ee9SWlvq>$Ki|**6_A>BOVoH|R1v zckqQ(_o8=f=e74q&)m((K;8Lg^cU|b{(?g;ZX>OjNJ&KZ2@Qf(3EuBhysfZkQTz*y zKW&#D{I~*9b>g(9+RUO4;B*Y&CVuIu7L2MbGuk!=$|feR^HpizcMP_$V+#EooO!q( zKtXZ^#(a1b%(|Fv^)32-;L$3(_2*e80 z1Hu6Hft||Ai&2^&QXu|q3t_q!f7p_Wtu2^yJbhla_r(r>=W;#E_~YM_?z965SJrF& zz(E%@z4@DSa#3qU<0vt&<=Zah!-D_anJ^U=Ad1!Lpgkdmy$wJa7&Z%>%ww5#KCV*IawYpx zOdfZG&?oq91+p7~6L*nQvgQ4E5yF|Dlq6<;ZmxTa+%pzB*HUd15g3`p2BXa}*{8eoB{^?&{_WJp5|Ks0%_Zw`X0_Ax^Zz37I5duwO7MFLC3mE3)$)?Y3ic zzs^iU1Mge`p}bu?mZg{o;;h0LReoS+pO4QCt$3r`C}MyZ}($iTep|h zI*68f#jf@2gM%SI|2T112YN&835+9io=d6fmjA{j-~V%h@c-p{JFz!jx*{@@6-%V9 zmondctIdO8rc0#?;$qa}GulP9?xy?$5}03oA{j6Oxro;Wwd7w+@_;5QoSp3_%WDdn z>`eHkMDpsiv^Hx$Q&<&OxvJZ}*y_BUK6+3N?#^T$hnW-H{TN=3)9#vU4oVlp-##>@ z2fw^R!;7^Z50G*?wxR}%5IT#-T^!CW!-@RAj&ic+Kknzp^E=|71CCpE(Op%DJu0+| z#foKYuh!D=m$QiI(;pcQD|gq~&($rfu(A5JA-`#E+?S+JWl>dLF*{T8C=3Hv+Vw9n#{QQxjdK~j2LOZ1MGp&>qd z*Udj)*p%7B9HtceiZV1oh;m!Em-sC$;EU7}7EW019^G z?Q-{OGo=^es*;Y)*}A>|0Wn^1qDFc+sAE@^_3&0L)YU~A^~s2%osbUsoxHoy| zC4f!o8<=nqZ`J#}xw7f(6^BLDeebm*`-F#Q+X;Dz_l;pvcpYn&4OzvO$j^24Nhe9M zQD%4U`5Oe?I!FG}3tnd2mu?Zq^HjU8!msU*d+8jov^rV~;8Gd+-r1%JrX*@ljggV~ zRxXq#jwVbLt>%@tgi6%)TYPF-58u>~KP`2hjo$Lpr!F%Y+1rl*UU61ZVyHp2Gn@rQo4HHUn6*3jNLa9axvzNYntOze=^ z5+`C_*Zz4`8q1r~gbybR6+|Auh4adx^>41AeDCVBnlqeF-n`Z8xoXwwoju9sN^3ck z(gR{v?-0Zoz%vQXjYL3pXYM&Rxuggs%-yD|63=Wn|Gmz&UJLT`1}yGodYzn{?6qur z<$JZX-DPQ6wyd9&f8;qhCE5(@zDDVFf$@ppIVWcu@BEML@K;#f6pi~(dMw6)SP?IS z+%;acuP3M(P0F0>XiQazi+gUTvir>axNSTLdrnaA|5f4KI|2#(sY5$XeUBo6qXO3aJbK?PM;o|kMOnV5aNxwF#wzD z9quZEO;fG+#G4%hR$~UVOkf#ux}*rfP`K6fEs`SRLZ7Sn?mNk#&%c@h3Rb^pT%eiZ z4t2ewK zjQ`5BxZ|FKwYFmLT=!=CjWr;&b}k}&m#<;P{bPHY>ceYDGG6`}N+KRQa_#LN#xWWnrh?^f>Y@ft`} zrqFZ^gaM*t)i7C}y1VuvF>Uq{=mw4GrYgjc-1+%vQ>%G7^whWuh|LB_XOSL})qrpl zgmh*-Su^-iVELxf!6U+n1XobL>cE?m}Rhy*V{I?KR zm}M!jVo8l=C^Fr8I&W>|l=0A*^-344gfF_qd^T7i8|*{xbMgGgGFoi{;J!sEx`zJJ zN+>Ec)AC`WqP`cUs-T=Gv*uwAoXbacL)bQ>@w>(ynOuY};%ql{?5zl7^uV5jchB2| z&U}vmF#8BUfsJP*(rOBk_5aa_R4VzKBK?BebHU-K&xPk1%QY|9` zxJl8gu9uvA&yS-^4^r!!sVL4MD?lbOO zsXbh^J-2vq2?*U>1s%5ZQo;;U5-)1eiG*IfT%3qkA_V23*+>+qshhhtSrbNlrD`qG zdn?h1CXRB`_<{metwR)yk@gOZ$>dAvPRO(`z{UGK)`9MxOhN4^9%Oz3f*W#W+ix?u z#$9D}AGkT&A~ig=Eo+}rRq9+<|81Exw*BAqG0wVT1VTVT4|6gs&ku&iXw0Fb>RM&^VX&u_5A~Pv%dSGd92|e>| z_waKsoK@GSSormbi*b_Tka4#Kl_xS#&~OOk{{n83)us*t<1!o~ULOVm`Y6|o=U_>y zD_d`!XPW#VP58)Af+Cl#OvrJj3NSJEZX%Wc1G?@N0qoyJux{dcTU`cUhaNDjRmfwAI;emd7Zt2Vu<=T>)Fb~)0y$YF8!j+y`d&XJ2#;Ub;k z>|5Pdo%JkYdwj#*a-^kr)tGkiMfK531pjf~+<&$YU?I1}>CuR+hmDw4;51CN?&w1M zWPHx-9o8mV={vhTzy|#D#4q6HuZl0N4qyCKXVCYd8}OU8`%F#qDSkPquq+h;W$x$i zncIg|`zmr~0QrXl#2vLErPVMqa#~O33*^AwLltzUT?hm5C0r}Lh)}2cJ9Q5aH{QK| z^edsuZrW8SAc@tjM>V&tau-WvjXdmoHJ+(64$=uf$+1n4yVob!dXwUST5lpzdx$8| zLKA+#t!CWJ=v?Z&^xJ@Kkl7#^bcn~=5ygANndTg^Qbem%^e%N7o-UA%zt;lI*~?hZ0F#*xS30(K}fX`DbuuGFZi ziC!z_@xyc2qcq( z9KesZOz*a4sU4wTLh{@lMZ>?&w+G`m9T4l8>I*}B#gfHRK{P-pcEoKl^hrOr4@8?Z zaE7nwV!L4b0NcNznPZ9ye!WM2@cQaj)~uZ*|7;?L6ZE~m!uI@&j5DW+u9-g#{z<{A zKkTw`^)$qP=>129{#&wF+S*rRKt}_>3AZF!2TupTx*>Op%@GP27+}dXrR`zX2vj8L z9XXCxz!6z<;+CZRa;R4Brbj4(>vOivMX|)i#tHUY)BgM_zGW{*ES~VGGr?~1Fxu8{ zDvms_6R-z=ovHV|B2R^iELsg1jz%;IQ>9M)A-9eVgD>jrfBnoP6G>M=2iMgv{qOX+ zuR(W^X0OeL6a5N?DI&I5ga1f%UV@jQmCk6llF7=XF~A`_HSZJH;=>!A0Qcd%2F$2D zW6m0zjN9Q{xe&^KKrVV$N$qYd>3B-k&&Jt;$A+&?Q$inuTRi^0{PNhkWqV&Q?|-$d zYyB&H4p9?o`3a|B9FYEOOL87Oy@^<&!^M) zU#f(~)D*L3_c{SPxI&%cxSr60nAE3yo2chxnF~lOpvM#%G=RJV`lPz?Ke~z|psoP7 zUzH68q#c2=O%1Al@4mk!rB%Z-xg|s)8zUQM%k{6yQsdFhsrjELt8=RV`yh2rO^g5B z0VqMf|8B&{CN%u3)c~5#{|43j|6yBu1*=GgHId2)6_$ju)Ja)mvoObKSm?GvUFhQY*u@ zHCTqzl>s?Nl{2wB{RhfldenPi+DD_Tai4q0PizhmC#y)DuLYqFtQpaPtBF@pxmu)P zJcGcaLnORvZr-tOuX0{i>n|N##G^jZY5f%mN~GzsVw(^Ju>VXO(@VDqYed~_Ctux~ zP#g6*^^SV|Rd%|4>-#6R3fns08Q)W^HVKAY2%(aZpWsn|5)oNIAWv7B>)PbZKjQG; zuJ@2SU{j{D17;S9-C38*KjC)aJ&Jr6;O|LBcgjT;w%`slyf*^YpHO;r`(ej)FP>50 z(z0mKZn%mYsILrk7GpE**jt&%#$DH2DZB6b?_PV(hHh949a0nA`3F?K2}E8l`%>3B zAXect9WU!K$+Ae~dz;Ak-laL@l5DE-dBA(y$z!0CIC7(v3jMUq>GU`n=2p6yo+*GG ze>E<0djjfJQym?lgYI;D>6@k9T!y|eS*tfY-i!O;`=!)3jOCsAc}*@`HRq*AtMa$p zth#UinOf!w7YyFCIPP5LK?E|vWHWvz*q~d<@&EoCwc1)$duu5zMeP-$ z%czo8TbocbVpOdl_TEKHOHs8)sEQr2SF5$7H9|rusuD9Le9rsxJ=Zzc?~il-I=}OW zak+3^$$PzC_w&9VckA91!aG?%t#xGOWgh6qq?h2(15DFPk>aSUlKx)RbOXpIC;@8j za!p#05t%tz1)*?p^XRf|t#tu5&9_3OXbYT$y<}=Qb_elWtYc`!Xc)|z3?iREehy$E z0pvE7vZu;31OmBe(gSji1nZ?p1*n??d05m`Z*-V%s zek)4zhj5^*nkM%zTx$zPK@K0Q?kj6(6BE}<6i*S&HX_4goefAi&)PER*Ap3OOiz=?AhY zt@08SwxcQ9v7JEL~hxCx3HH~tsq1G7W+P9Kv$0nG-oLx!YENxi{`Cu3R ztyxvE-lbRwH%@G$Igj;tHg)M>YWmvfLQL5H~dq6A{>8E;(Al!_3481Zl6@AHt>2b_+TQEwbYX(am}F! zF}CtV{BNP)OdIVX{`rHo_BLG!cO@j$oFY=Ty95t`6&$|viVwtC?Qy#aNH78T=Xz^o ziQXr?mwLYTZ*wHJC#ET0-vA8?R!Gt1M(#YIW>!*bi1?0Dcnx|;*cx5f=)GIMN_5Ha zp*Up5Sm7>EuTl7jO9`|G{u%u6C&=t)vo_|4nhyP257Pu|46dd>U3HVqJ39SPs41d2 zmw)t0Q{vzQhXnN*IUhET3PL022Xfo*<^3&o%?p`#f20yPav66z^n&qy=Ja7*2T4Dd zbZTte6v4EK7c{M6o&OndUC>ZEL%3um^ML0^u+yjY`DBhSi%RbFJt)YD&gLA3pge|)rQ?VX&!Eavuf zK66ooXj;heK}?+{?8PC=#+A!!(h$d>&Ls&Czf+GRwPX<7N!EAXp# z+>S2#rgxnxH>?FYdb=(InshfgH$`>(<=7M4{h7bg{J z*6@iU6{XzSzM0uL|DdkJ%Em{0nS4L!>?%e74N(NVw8hu3aTxym{G?QCm@ zq)WFn8Jr!~BEiyq5zOJ@^VE4eN_Z1#QSo5g*wBVQyw9Uo)`%yGK{o8y@g6=+0oc5y zW1p;yu_JK0fbm@=gf*TtgAuYYs&kP|9Uf)k!%Yk#7Ms(x3N;RDtFVvD=BZgrcS3(; zTed~j6*oRd4J8?}uKW2)4T&E*^0Ti3Z0DjI;ZhXoL={;@^?8h@nQWKht&Vqh z<);eu+5AzWkW3(V26)8X(k!fCf}6UE(<=RVtVsp%6Q$q0+ag%bRN|Qfi*9IMe>;^6 z7+_mN5-=H@)IL2g<=dUjdFMU7oXy+PuPUxJ{C=U)J&4%2yBE;?hXbwc4T+D`;9#a{ zW64YXV?xM0M5|l#EbjZ2mQ*z=w#*^O+3EdZy>iFtyMrF0&`GGy%R!NF2v=8<%FoiY zLQp?NxF+3T5|=`J8N&KJGOUum2Lp@w59m?=u(=bXw4`G1m)L$S|FQV9K-(nxUOGhe zVsz?4p`o;@_Sx8RZBe5|22Bbn8>xd4k!$?05S5%H0|p_EjtO0+uKUxvqpH z_tfZ+g`YNpdb2HTpUbAo8?3w|>AxzfAU;XE*(9mV>R!vU2$NQ}if`i#Iy z6fqT=&$y+2e5X`OsVZs~yl0gM?d9M{K0}29JTOl9p;mKeZ5mM2`~fa;Z;meg+0oFm zdgnryXn|`ehBV{sSx``IJrlLhgtC;djQ7vYvbivKPd`U3ElL0SlC0khAWN@hkZ1ez zI8wcVhxqQ1ei+nXvQE6InS9VOASBD$_+V7&+Yo(IU{T{3O){v6^-lnl$Ab&DIaiwn zqq{Yf$@PASUo^^>nr)wQRd)U5FH470osF-}*XBORr1wSYFazB;B9x%s2)>PoOR60F zH1Mp-H{B!iq^r}IZExmKcrvSG@{=_S5uL59)8hZm)s}^|e3Dc(4q(D|9=Fo8woFpK zoZBcBx`&vSN!^1PMR4-3)!4(Eb0_c2s*Nvz6>t()nXWGp1paGcQ3aZ;Os8?o8{u>| zZ8uA&O5zb-`l?@tg2YCphWuJv)~!BKnMrG1rJH$>WE7P66Em{E;kj}))|Bbo8g!?6 zH|OMoo1|Q*lL$7_XiM*CV+(LwT<|^7rKT8=mm>etRO^2Hpw9OOMmQMfzb&ZUx#Ad~ zuBYpu!N5t#nOz2Y77Pn-;b~5F8?%Ks@tE8RH1D9ajrMs_;3*1Ae6<#9<0eT$tVIE5)S;pafPDZV`pG{T3D+qQeKP@EoSU|&xRzX z8jNJAKW9>o`4m-T4UmOm2=WuC0KXN51naeeip{*VTD5j`d;?b}dL-4+nNib%KK;!$ zK)=`1a?O0_WVY(ykY!L|4Wj%5%Th8<=%hsz#$o+MaJXc1sPqZ_9ShM%XS!VjW+-v$ zT>#TCsP!LED$OLUY>dSATw{A~_Fm)woE1JL@ayf7R4088PIZzNR;pH3Y9xtIQOWt^ zl2meKoEXp1=@YV^VGRo3Tt1Vad64USPi4;5t?+VJJDQ`)?I7PpQL!9v?i{SH?A6u7 zT3Z{(4}?;lE&1_aGcF&0GBenMB?0eTlNcwmFP z#0kQ%M0255krYzTuE!4F?@QCt)cyUM(i(b|tiu*Ii{XSx{|D5gd5~uCh^ky^ZC7=| zR`_89A*CUsZ^>bg*y1@U0q24KYv}r49aNmSjrz>4<&togFdL$IF3H&cKcEK_KDQT` zufQ_KBN1D9BwbbHHQg(y8x*z36~DzF!wr{}lR!5swlW`HECN#TX+!;x7#z&Ok$dH#hp>%z2^T&dXz{buKfPzp+h9zO|x|Lb6+3+>;%c)-Q|7rG} zIEXazjTzt56K%O43`yyN`!G}0?K>yyfQhQHLyym2;1V@XyqWYLP+=WbVJmqbEn$h7 z#dO%|_agt+)|FTl=PAGOmk^K9 z7P~GgV%S%cfze%jJXwZnaF3#rH+lc#=I&d$zolm9hUu=^MqJ?6{00g!;rW#vK1pDR z>4B57D;IR%sZKODKgGNn3sN|8%n&Lf^Hh;{RubaQq~t%eJI|V1zEXmwFz%who6YP_0cQ`n5{)lIP*a z3_D&CzDjQ_3w$$orv2+xXS)zP7b1EUY8N&>ZCzUGbCW!Tcul&d$r~~wt_+Q~4FZPP z&}Gll8#Buhz)aGko@1+s?aD05NDf~Rif|F0-8HKCDr{!Bjge%?AY3*C$s<~+L6on3 zOI)h5XozVGkS9Aq_K$hhz;~_NQr|P#^)ZC8fkLkFj3HNsLLhzh>|^My7MSy=#OfDu zi;L+zJO(%W_s_K!a2@85!<)*sENb(b-?raxz_ukkopNc;9%kODbll?@jMTBuYGd;w zRV2`~mpjCGV7Ay0)h(M^vi znckt(!+YN+hMwq&CUi^|!ERA&4$~*m;qp{ws|1^ll%Cul5zHtZJokjFthcOu|q4z-TDq+Glw;z@SJmXG9|84J-;g zz9(fEj5xf@G@hLU) zi}3ntJ0tit%r$2p0z)8w!1DeWf4u`)dRuIHNt9ZE(ylO$U@DPxHTJ8tGV&3btNfS8 z`$8Rn5k5eu23sTp{ooNyNCW$h$vXCm2)SIoSwY~kxnxyjWT(%45?F6A8IS_K1P^Ra zOY60>T-3i7?>_eBcc1t;490 za`zEOyGXo|bVXZT1kp8xl0O9F@C*pEATb|`Y;X51-0Aws`*&@+l9S1-m0`I>i~#7S zLJuvp6E*7J>9V&B6eTXrtE)q}?kU2xuHJhi_yAdr3;?kJ9=U!e?PRW*c+39x}VNkWQ85sAa#5DImJ9h(DwA> z&{dYNK^j(1McHgcnPp2 zwFbu)GIMVab)iuQLEM>SoH zXEzN>QDQS#;}toVRNnT1RWW$cwdC5j_ny&97DiqK9p#zyv#zMF0}%6n#Eiteso!|n z%8mA}*KX-!mY+kdq%t%JQ8@A`LhSG@btHi7)~$Bjl+4%F+L2Cx><7M6%iePnVOP!w z0UZj;j;x-RgZbb@9tz_^dsHCwX?j$j1v4 zERQ~r6=(aNUuE6dG^)4%JnhGEGH9o=wCuyVKq!b()$G(rfMcHdVSD3D%lTgK1rdJMXzlcU zy464!Kh_Eup}3K<=C^Y9VASy4^O(gaPn&n;qYX#;s}@o-?OZEy8dN8eLW0(}opc}6 zeZE9he*bL)Xj|On#d}o+6H&wB`!#aFBCF#mxuQSC#K!7V+YHSj!0YAb9s$FZ-7i@k z(9a~G!8mb=^?-wKj>;};lj!KfvDw_R_b+Cr);O9ENM4D^p&nEE2J96(V+M{B;`Z5? z4o%d)d^NehsQi((YY4SB1);}&5Y$_!e2&~%`AJV6PxF*HMz7i#Wf?d<>mphJ78T#F)oinX0p#AGH>b0l*59&caX#2uERE6aP`?O3Uku7 zKP+wz8Xtb8z@(|u+{Tr1DM?cK&a<~nk9!<6L`j|6B&K}I#jxj(Kinst+T1G?kN;|+ z&&Czx2?RB#c!=RLRJU4`OasvCy%Ci25*ZESf(Ex+zO|ODOP}X4xDQ$m_ow^uW7A%W zYDB3qCfA`?gz7K3>qK&Aa=bU#v_jsoO5jgo`=8&4xr7# z`xtw0p9TxSP;rY#)NT#%^UmkcD1tOKfy8D2qZ}4HiTs|dn8bXwyRMi%{ZLUdF5{-* zN(IlDhZw`z4P;8U79@JdhX!#f*-+;l&9)jOxJ=$zPEAjIq3s%}ma{Sd5!97Jc9=F$ zOm^bHs>Gq>35h9A)K-3u$>``1)k25adL5X@(p!>u(o~@Fb1kW=hGtU_wbD!K;P}wG zouCI>f}jtvAO*=#3%^Q~5~V%{6Od9Y>4^Ep3E+aCuh1=aZL?-PMYDcHk4I z3$_crM;R=thWo5k-puQJ5^wp=^Dj~Ud-AQ9WkF8hwMdSiD}#B14I~9_cXf-o^p=ks zXHVw>YpQMd-CTV4`hT7bKtmP)=@bp^jo_y-2_8)KhGJUcEnm(REcW^-y{9Oh89nFLi=7qC2{X%Hz5j!PE4vX^f33ZAFns zmxdMf9Uj-!RKlAFZ91yTM=Ysg*B`%sA6R!ps`!H^C`^DaRJf~GmCHv~xxBRX^K;qd z%g8W`Jdiuk{3Mqno@&02x)NHG9+HJU> z(6_pu|DdeN9kHJ@=^+pPf)V5j62|}sGgk1hxWw9{IphtlqTJ@^;5QJmarXg~z*b~B zBVR;bB8es9fNb#qK?XaqjmD@HFcp={7U4RAymkCG0Ef*mj)yAts0E0hvsmcLOs`>D z+VCI4l=N`-M$4v$7mv91d@bf6!YDba;&PXmLb|sPIHw_LA9CgL@cfrAaiFbN=e29P zrovgMOL~(uJEFGiHyNMvTYfOx0dVoaw8@JLKhxi`ha8&&stS&EGhB+wQbHAF=|DEI zD7)Z-#&S^ac=E-f$C+6uETq@CE7XR1r1yG%`%t{U$>r-4e0Q7RZtVqw#>$Z|Y6h7@ z)+DJt6!w+w=EEGV*JO|8Gx-$5&v;dB^OTc26SFU|RjKdYm_^@gon8Ge!i60LWU3vP zjXTw#+m#XRl-2j{lyN2O1$Q5Jq6vC+5gKy3t4t$r@LOm!a{(`EU+GEVW zWCOyTmzE(G#ndCx!9h`fnz@P0b{Tmm7NvV!Ucu`2tF`rwAH&1&WDeTJjxg_2t=ypQnu6|Krit0anlforFyF35F zDH#sws?4Wv=z*{$i9$1{uo74>l(_m-@a@o*3%uH6e>C>-TA?+!zO^s=UkpF=8wd&s z7)lG5M>$h)F?^{zSj7*PHddz1%`fT;GbC>($LoMJ$A~s(d}PQV2fe$RJ@1sN7zYe) zE+dYa*^%(`6}}k$XjsFQK6S1AKcI6om1&eJ#b&Tj2HO^oR2@c}YmgX}Fh)&DhtipP zVj48y2;X0UMq0-ewvdF9d}zufO6f?@WxB%|t2+{gxDw5k&Y%dc9botphaj4WnNgZM z`QjAjtOCS1Z=N$+zG9He{uGrMtht^1yB<)^!i>$R5i5(F2mBH^JodI!i=*xZ!~9Y9 zsK{gUg&c?gTU@>p)p{e-S~gAVS@xH=s18`fBP7^Kc${gc$Lb@Nys-C^x=8b@P^lR1 zc&(6sen*IB)1XWBml!9QqY|;y)Ep(!k{n=iJ7y6ULj1gHmV zJ_3Ne$tO>9Mm<6fcIMrhr%HZ$c?ZVL#gMn2>0U>HDLOj<1+m%2s&iDrC1az7%o*t z5);@o6HtwD&rH)jzp`{yuOy>b72d?bO%tQOsi1mq4XER!CV+F3VB#fJTJ6@<7Ok|- zs#V3mJR3wQA%-2L1C(`9yg$niz@){44E(dWvA1x;{XJ*1;d>6EBdvzx4i!cMGB7uB4i@{McnxW9Bbzj@c*Ak41uf!(h!;u7tCz=?nzvf1u|KIQTTAK$ru zxU&P-zHsiZU0ld~QcyZpeK}3NU)^4$C8m$OC0JMNT(ZqIyv(Qv1&OxR&H%T`JF~<> zG3`vl$0J;Hc0n6!=M1$&_JwEfSKeLm zi5;hD1DBje2c9$|zwLjrC}YVfhpMv3xXFIoUbiV}S&U%|#_PY7*1R;I(>^*kjmKvq zF7-M!Wv)EI8qCTlr$!^@btP$X(_j~3#(8-2?5#evln8i`tkFcj<*=CTBj!>M%>H|APxSc;Og68`KiaV?RqEri6S zaxL1xCu}NSQA$VEzCh&-MZ!P;G$5Rt+A%-?#|(;UwoiR%pRAQ`oWv}u``s7%(h9dr z9MOI6Yrek|eOyrVXUdQKt8V*y#kKc`OYQ=@eMSu$k6y0_s_25{cU8!j33Sjf{tY2 zvfIzU=$_QED{k1$toQ8jQejU413OkMtfC;W&mcp88m(}|5;{E2R?Gjg%+&Xw0(hW6 z(lH%NT+}aiFd!Ba3wY9YpFi7u_h);%WCJuN4W>=gB>=bL9P8S=w6g??E1+O=Fuen} zXz5o8me$CRCf!+9+7@5+UuzT5g(O)g{5jzppr;6uH=E7PJQO!-yo+42G!~PLv>SPz zj7@gLQ=ud;=)kO~7R6am?VWGA`)uI&-0$K>%>Yff?TWZll&wYY5(CPRns%ZK@LzpB ztaXb3seGm+k0RmI%;F^Loyxi*>Fh>`6#H1$Mu}L-o!>9IWJEL!P`L2Z-tH@05KHwd zlH(^cL8TJUw18pR5-^lYY7aOoIc=*&y4h#wf10Vgdb(2E%@g!&^C?&Nmi<>?&=P9` zd`f<@mGe?R@9J;!x%=oAt4sHgr;4_5v%oQ{>!X;lY@2wEC9A$|3R5F3xS%D|Kz9gT zw?42dm{OzAREq%uclJFBG?C`)Q@_%PzZsyTnI$IIvj}z*>zMnoLTHCX?E$JwX3b{% zz)@P>Y~QhImmg~gO6|+8-Bm?V+q&@0bj6m*4jpDFb>#3=i_Lzg&)3T4OZ?_BL0Z{P zU-yvjBQMQ2?*~*#uLtKYizg_7^!=a z>ZAx@%|j5!bfWs1B&VyuGyJFsd-s3sghf)lh@BQ zegN9{N=KK>iCf=szrFbQ{`LbV4z|?`TAvGVec?Fxz$W-_&da-xxU=nrwnrTK!gH(- z6F{Fk+PpHg2m6)0!AvU}^&g=R1}J>7{!o7F6uTg!#+mJ?;rL^Q*Yun8^f5rXsnDm% zj)kPK%~QYB6Ctt`w?B4q#I4FhZM*%NlDdrW3rlvx+Kda-8fz*PO_jbqf$}Or-6h){ zkSu#DN@WkX3=Op<8I{hPuFg&!E5!Ii*x@^l0LoD-02p1>HaEB2GZ;IH6h2dZVnFpF z0y|cw|A36igzO3!X9mXlr-9JQCX+k;FOJw|WvdNcxe3*-b$4<-X3FTBLVt8EH@8ia;b{EXUPac;+tlh&y8p1(C!Vc!RPV!1G;Re?D zn^RxAc}-vpi_Wk2FIz?5Nob%u8d~ec?dhkL(CdvE*)ODb;)vaX*aNnwp|RDDqMXHi(RA ziY|;w)ScPaO&}{g%pM|W;;}}h8-k0t#$dLAyv4cFZx>5%8~#fWPTn5@L(0e4@^s=F zjFkj`TJu~zr@_diYsOp#LP7=w0Ev+KY`lx5>6^0woS0bLfie1dr^hF7IoNaPpfRHBOZ5okWyX!%V2;%SrugbN{nFDLyp4Eqtazw_D#YmSlgS{WQZL z&1F1OYrbo&qqV5=dw4I$IZxphk8stn=YK#4TWE4iGA4uJY}#He7uMAR6qbRt%Z(7K ztI;a&uU;Y=ZQc*H6}yNMLIec(a>V&tR}5 z`_@&-QnE4VmZ?sdOnee~r}s<%o3&#={`;nE(Ku5QpX1yAhvU(MC;*j=826?zuk8HH z=+@$`Suuujc<@$v9?r_jypvOqSkT`Gv&T9D)n7dsiG{L7q^6^orfs+T6f{Qb1vUjX zDKR%r`-OVvJGgSxT5k)B!?CA%_fjqHKMwSJ!BoV1O&7hp>v5ET5-O(Z3S1fA4;2x; z!ESfc>;F6sMF8wY3}cW$(c+ia-76y%A7r-f0D*f8~D7ky1PQ5 z!{$%NoSLEpD*eTn+{1aAa+dA`P4RDC)(rokC$BlBI>*ri=R#5cSh6S|zRMLt4-d5d zh5lZpI+(>+3T=OOLxyjm)HO>09jZ!R`Z~9uB9UQ2bq4;{0Yv@t1=s{?II5Qa-?p>CgRk4Fu z7^Xk6pO@?ikBb@rA$9A~?&0#MH8_44{PNtUmY@4vl~M7diN-41Pb+ny`2T=nO47yV z=7%;TuVmk8$m~t^_8L_R+nV*nN`KtQwl;>BT3U~|3ElT3&tFyHEW%dm{k#2_(K5rJ zA;T>Dk+C*%=htK5Tx0${=iBdZcsfVviE0Bc<{lm;QH7MgJ9%In!PY}!S#N2lFitxT zWXY`mTHKBIj?CMY6@sxX(L5=3IW;SAnnfv;s3~`#V{LBRUzT{~LFy&tc@5PAd?ztX zcECE+a!W9vuF7>tim=O*qZIaHaTA>oiBd1?z1j%GfGuMoZu4xV9^kr-slP?0KzI%k zOLHNK#o1pcOUL=|2)O?TbQyc4(pD}GfVC;i{~-+%ug5aII;U*Z)}z zFZ<5#*FS8ex)F6$D-cjNIB$87)wN4M`akMFdE?rKVu1NNVr^QbBOl3h5Cp-pNz2!kT1<8^ z_=~9bL0I-hwDo6JXZMGn58ys*a*xI(eP`xJhlLWy;;_)b+>o&Zl}__*(7etfaFZXp zox^~iDM-T=A$ZWWit{%2I-HVu&L`?{v&R524=2icKa>q+O&vxlqLEochGb~$nw1aw zuQ!8N)e^gzZpdH(w5t9iN{+P6JbNGt4i{qoJo3&<< zwtDkKW=7F#Mk;gu@_&b@DLf;p6~$EJ`(&)yz{d?Wt$Su7VCvhmHJK2XpQ!8zI{bay2d@{^%yIAw!6M6=ezG15C{!@4rg4*78<<|kB($+_?p zr_!XjHwB$<-;I6_D-_RPIB*>@3quHQ5i??a>YcG|WL%1Gu*5r2l8LTV=EA}AftMzr zVCi0)Vv6$0Y%=Z&eBuF4Fy&!}enWeJjQk?ZubqCz;KAf+<=eXaWpCdi?jLm7QAejb zKp5JyxG_pRS|yrW$Xp}WqjDegLPqYc!RMKrYdB0a<$0z{np={sMAfikW{)U_j@Wex z&~f6ssfe*r6r?(%ezBfCODKc-L|3E>h%1JeWgZ6@KQ9UU9jOykf&#_~wXv zPA+Y*gsJ#i0XBDBe`3IYf?ftN5LtjDL!&t;x zte2s-*gTk}S%K_X)oRh%V1+;FE^OVX2CGti@BTr91qxhzR`{DTJAvoX}&N|MyJp?p>hK)AO(V_LbJc z?@%>@5to$opt?fw0PJI=z^`OG3}9-`N2+v$y#Js7P&QO=ZGHV5MB#(*#lX1$ZqT<1 z5qHHi=7#->2|d=Ia~U^}*NX0g)P2^s^ryB=0W3`#_P;C}*Qt(r?rq#Y2C!FCOdRyJ zxC$AKtVD0l7L!}z4@_KFfv6|`^Wt#LboG_z%)j(FsWk(FLD4Anh&qqYD%Jt9;m*4< zIahAeSJbOQO{g)&9(f12G{(Q%4-nkbmzSKJ0o5QNX%KSpU(6t|BfWGc9zG^G*3b52 za17yC_2aY(tnT9`&|(wl+hm;C6?n@p@2^UcC}PeaRZNsTR>am;XFhHZb4A$ zBx9dwdmNgN1l$b01#XeN!AwSzZ;x%kZuSvj0v-{V|MJ<-B_T%A_)bz1?)FdL57fBM~dP+u5UqM-1`|N5AXL^>3oU`5U22vq|%i5<* zvh@9D^gxw5Ed%i84q5C5Clc;_<{nuW*nnvOg85=qQK}#;PMiX;Ceu}Po_`@Z<7P0~ zE|QlAKgn6co7bRca$pY8H~)&rb`A=baiZYm6&*;1}r1O zo|SFz3i1}_XnB>YsrjZ}>uhIuc1&lXyn8(Ry#>aDwW1hX0gZk%#YzL>y8}B$)l-|N zhWF|j3@-9s^H#FK7gOJ@6!FqHCj1vAEGc=L7J`3^4@12|!XQdkclhML2|KYbP@)Kt zx|sw(2i5}qF9#YmM6Ou5c0`lgR_JX$Q^WzM;%0NU(Y1ra953gFLd93g4R>WZzT!LS z53?u~T4-3P+y@5h?{2z^Y=H!1uVAh1vEgO^`bPka>iXPw?xqdKQcm}Wy>>5Jg;PkVlfdW=Sz z?#@k_PP-;OS7-V~m->K@UIyw7lw)Z);?w!xzeKF*K1!oD~A*~XP3mu^7%P4D0RuqnoQCX3Re=9g`RYwL{;Keecln)T z8m?_EQfGZoRDJ?{1L1bs_OhAm!6)l<$^WKg=I%LRc0h_`C%CJjeTk>M#)hkty|s9? zQjW&ItLL=cvSAu6gBjc&V&_rJHMR?Vr%lo+%M(6zyo|Eo=LY884tH`uFK>m(h$R3; zzbxSsFdlDl3VQpsx!?T4LaBKsfZuU+6ms@$3b#%7)e|$%5qe=7UWn<4T;B<`7{v+9 zoL!@tlSOv=3ls+3?!{rZ&Eg%QiG#J ztIvF0)8ot?6aNpzqyH@j+*d<_)HpFMQb!l(HK_>Rb8ZT2k~p~j)| zNX9Z;btHTcg6WC}-zM+$*|W|BJVuae4AVZ_EUPL(dlqy+LmLk{ z<`?Iqwuf|Q*2vV36LnHh@)aBfd>P*Eoh!2SyZ4-Aj`P)Za*QfJcRQc zRJ^~deES9d@b8xQmlp&_`d^^g`y_=l!bNi42D*9+kFTn-R^2YiGAz6{_+X`;ARIcI zgyN@&#bTf_qF}AgwOlSXW3!Wo%EtUz4;vi~!K;Tn3FP`d7{vm`+&C+DdCicfQ}r-& zaghImQi=YHpFKaGK4Qjh!Njm!A2+m2OVp|)tE%hs-F$rLv3u7y?>RVgplv=;H+N2H`5(^k$47^&uYe@&2R)TwdBdkNb*Nt% z;By+^wJqqGqrJ`)kc}(9(xVzM`LWF>s{I-MVOT_?pa{Q8VV~TiwRH7giWL{2{109{Lv(>0psYyGCIH zU~9l(7UTKGo#ICn;}|KwD^yP#kh~)m;VPuBO`Ay}=NjTD2Hn9JDj0(zBrH%l=AJ*AuK5 zP`He`+#Z$w>i7h)&8rJ*FcLtC)y7BaR(Dd81{nZc5D*c|JMKvzdwdhIzRzB>2!lm|{;)#*ICtSftdcTwwUTDaPh+ysdUY z*l&IKfnML)PdVv69nq~pSmV;6$ZUTapZ>(z*K6fv6$?N08?9TNx`N%l|JiFzcc=fo zBZ%@RS464an*uT`Rq}aPbiW!~ygfgr|0UI7mWL8^_<9N~yBxs^;D~0)OV$X%_kjeT z*UqAkJdL}5cL%C|amPvF51^d3RApnpwLrAv=+iQJD_HxgG*>H21!Y+F{Tzt7o=`78 zAsH?9^Jp95V`l(uqQ=(EB|SS&`A6%bzo_Fo01D+xjc`w4fut<4uVe6QP2{i{T-hOY zHSU#(q;L2iFA2%NV5h2jYcj_m^dikT<*^snBL%7V26M#L)ILe^)OZ(fy`hlZrNk#2 z;o_&&y6pcH+^VckQ_LMPwN-77!xELl;8{6Jj0rBAlsixpc7fl+!k)?EX+nHv%qPf` zPn^_o<_7EkzT=(vtDjQLQ6`X|#aMS?8)GToCYQlUKl6j7Ir{E!A^d@e(4*Op2~R)e zT$gUT`0Xk-SpWp`>k*!fMk4ywfs>HV-$tk?=~yHqUJ$=1I{(LaK(4WzBgYbVH=h4D zTOf=9V!WUWKC2FFRjq1H~*^+S#1<{BtDAt2SsP%zk0Vex9PL zNA)9xYY4gwou#Pm`6NpicDWSA&dw_CjBhLW|2#0Md9%w9O{cG$(ZX5{I!=N_EnRoreFS;W& z1pXrHm8t)L<(4?*sOPH>hpmnM7HmGnm)`2qn)ah2>~o8coaxxdCyKv6eOq2i5{!FO z)`oYc=+_I{{c{+}Op6J!Jr@!>&Ro1Dh6##N(Q#cdZJUvF@>0n34PxcEhEaI$6%6VY zCnI2kO9J2<Ykj}_M>k%M^1k|Tg?dhm$00ZQH@xl_x4BB176t}FaEaX z8O+zM>olruqmPoZhUDJ+%ndt(UZj5)!`TF1pccM{?GKcQxb81vMvhA-NlHRqLxnnD z1b^UwwV!^(aw0zb*j(~%%_AIIiN|YrNx~l{rEOXWb7tZ(SY=8_5ANo8fXVZ(mg(9$ z3TF`9bh&i%3Ph{rDt3xwFpvarxR9Ye1UCG>ov7SHs2rhF5-w9RsOMg?dLz5suaGg= zHHCO8Gyp7@RDT2W_;jZk|M_pyrtuM+Vlpb}UPGYRf4b%%=QF8D-PoxJI0qx*n~J&m z(ba&KtThF#@As@8b2nT!Ty?N0L_!YpT7Wly(e>}TeF^|ZjTfp4U%;EG@K$ETy;L0g zny#~f69SmzsB0@r7m*yW&T*8RW6jU+IeF>1{5B~r=~thXJ~F&$ARyvr8=hOev3%Qt2n%(41Ii+Qr6Y;T6daj&ID7q5=!?Bbu{H5YTojd*fmD6gRu*t2* zwZosVz31!l=5908*|-BS-;sLDykFu`Ykr9n#yN-dl4s9$r3c26bQx#<0|I7z=YJE# z@ctv-Yi*NeBD(ndN`q#WU*m6oabkPFQiKkJtg!iFm6to6EYcSiN1UAGzRYK=$i!~b z0FP9p$9Q{sP`dB@FsZw#NFS z;o+_R$%kM1#H*u*>vM5esrgovmhJ-C{^O&55!Hw$c{%Br-eVM}Oa`Iq!uy?tP9qUT z`vLOWcf!hHF$KHZBb+@{yg`(7VT`wUFZwZ3$M`WiW6~!PSAR-0EB&U)Fg?1DKz@;ob%k*6m=W*8W#aqum1i;G6 z8vYxqvPr9F<4~oUo7;1)Zz8a2>F;ObkbDmsG7clC_=dUe>Hr;2u3xFI1gN?n0{5#h zfIhIR(C5Pd*ILYoyV_134T+tbS(TOe1XQNV4S|jTX!s6Ediz@O#u9Tt20zmEOuKEn zl($NFy!rR~*g~e_n-v{|TVD}IJ+_I1K_q<7OpRh=aH$50H7-LC^wFH5Igfdk2V=kzbC3rh>r%}=%$rRGg51O9DQHf8&UhDmhYa~rhYpma-f zVC?zA``?*02cg!Wqp@=|XP`b)dn;c=YSU#}Zsl@k!GauX2M1km%i|rp_U6Ac%etau z!~*bZ>xl;dS;Muh#@c$}K4V&{V*e4R*87~f{#UookNMjFyZpvJSOs+a{{`apzw>Xu zQ+N}+NM0u)y$ll}q-2}EIxn|aT{7rQbkxB4oFC9r_Ma`W9ls24Zgp6XJs zSfbcvlp`^JjOVvK5Bc{*)A@r7O=w~(*y zyWIq7T-IUeI~U~NZB+l3k?uhsM$d7 zF*R=}l_ul}$PGuwC^ZwWYFn(yYspm};efg)W~X^rn;N9WDa*3-}*{l5q-DgF$=QCi2-Er;<5>jfl2!=3?%*#7gbN ze^HVz6jRODW6n6X@;LtN)FQ72%cVC2h9zy7ln^O%ylj7oURXfOU)n4LcTY1OLmv!Y8t_936bWrUekO;QmN8aS4tVkgjE z>L#9i-lCe8yFEY#>i8mH@PpA6rC4VqqpwlSvx#ZUcjIq>srzmA!GCR2rT8;m(T^OU z#nP5IR234f4HD3p!8jNcUwd!?yw<%-dZp~NbgomH`yyx;7_6YwXH=ncEIntcam8m} zfPVU~vT<&w#9%{Nj8g^AAVRFMu!MvL?2w0Xt7^@KH?t<~lt>b7%gE;-<@&}bs>QfJ zP?x0M#eQ~^_B7=Vc|G0Y_9RWUe6cSeIH+uN*VaRd>-D3G#2#@jyPe*m`1Xounioak z8P1)pA~E24-5NXMZuUOBo~bg~y; zUx?(KGg^Y@&1&HH<<48CCZRB*v7YF&YLxO!y{?8mvO!erPa9TM{#7+>ol7OTFV(*r zJPYw5!(a)A7Sm^<0&?kXeiaMWB|f9K3zBS`wfr{hw+w-i(SoV+Y&-*J-okDm&cU1bCGv&`-PaMF3vs=D2FxBSTNt5>EU$C z^6gNrXy5DZ!>jdn$WSP5rnX5oMWYP&nZ5alRo6Y{MGq9mh=e6~xqE5NlDhil^C6dQ z43_1f52wAo*Ss$qBR8cTq> z9YlSYMs|| z5pb@%3@734neElb^ilsT7>TesH{wug({Fz-<$hmWv9@Qt=5iL#o38fMO?oj0kD(NZ zdwl}#0(QFgAcC{UE41R_E8DqRAIZw^9l5KIjQIa*aMMjt`-npcy5cqi%!!y#J97oNFX@v`^XZykpxv1_x$H5NWy2S-i-1>jw_{$V1YzgEgQ@;zVX_Gn>1{dkQg6`b zpFnb=C3#aQ#!E$Ua973jJTvl{wnY&ZS;GRj_`&~2d*}YobpOWjtz?lyN|c*+V^J|8 zhn(G&kmPI_EmY<>ha9&YlC6@7!kuJIPQ|$8FgayL?s6mN^O!>9u*GK0?EZfGFTRiO zun~P}@j$BKpusW>P3U7(8XvY}L%tC1 z3gu^zle|n%JGy?w#o5szU$d#9G>wbki`UY-eV5(Gn6r3))-L0I{*n8GT7TAY zRSv3TWjZ59Yg&%6r`l&_^mkgN>3o3#zAWT`9@x{c-1My+=-jsV*q?D@-1|+uXx4`F zSd~7&Kax+cmNb{agt1{x7;sk8!iX*4{^rlxrb7?J-3mT+yH#qi&&Tv5*HgwE-9jTw zVsAtsHJCGI`mdwY>K;Vf`pRuYQbzSpZLFJ@ zw{yQZ%zqVJ!I0(+0fThtd}2k>cPH=3<(fKb%D}5^5l4^OUpwjWVDj(J=Mc%aMxP^| zbmaS^&ix5*i?2{ZGs8aAr4O-_*Y1|tY8ppYK zeby%61wN&cPIVuKujgCo7VG#wxZ%`iMwIk!ZlbTQsL_^Ul?Mffb^T^v9R1;GpoyZJPGpxaS^sTwDNbYy2H$QQN}bB}g0o z8;Sblc@+MdJUcCoLaZTApuD^!klirAdWX#D@G4q~K zmgmq;b)NZVELr#>_Vj9ekwZasDOT#m=@ybG?ugdw@)b$u3$rSYEPDOx54@Voc8Zcw z)#O7^cWOt1TkCGlRsv2e_9ljl42%&BShbJ0rGi`)^6>N2Cy0@{=O(Uf$pvZ-_Zi}W z`0F1)i8|4X8FYm7JQhf^`-#<+tEN^=)IOfYCHcpU6+ynv5D1TtHVyWQUn zudR|F3g`77BGXVWMyx*-^TmgYlFv-h5>&l{aw#Q~^=;){8-8e-=Qu~xWK^AyD9%&u zh?;UJ<^^?p!p579m)hVtAu^8>w)Cd}nRe=XZ$yOP2+P!5Sz$g7WpD3jw?yga0DD@r zLF^eEV2R=?Nm^hAPwPq#l%oVhL~|#6buy~WqZXmu+P)ypd;HdS`^YaI5Bp??*Ht{! zn%p`w6?R9|Kf43enG6h5Qf18k{Sb3#j@aWE4{J(8EaVjLEVM=y2CH_W#Bi$fz2+U= z9<1FQPD%2CV8yJ=>6Fi>Rv`5;vW^gwVu1x~Jrxs=DML57$80}5G=G%om|cF9E+vTY zl9p2XHZ*~q%(xr zqchtzDC^_+#(zOdI8_#*lME5S7|^;A#;nS*6CyUrT&ZFx9iUYS;q|Qb)*Tm|IQhpc@Um|97n@v_&Nw7va>#NWU^L^$<`RQFav8OphgB!^UC7<0T&=NwBOLnBj-iC{z zpAfJ$+R`KO)!VaTfeKobilAHtTueT`a`^=3p9|9Ae>b~QKLc8$ybXM77R0-WvCO2o zqhz%5obR#4FCP7gY!@CBY<4A(Ivr%ui7LG9xsxs0NoBJu?)*DK^UhB++w`yQsxQ?` z61pFm%HB>AS#duu!}{=~+%`TufOO^k&+CoTQL2jU#xXE!X^!#MTzUB#Yy_uI=9yd_ zpezuFt_+OZY}%=d0w2TlWFx>M#FiCHzox=Qv`gF>+IB<$+5VKTTZGH^}I!#K1%}yM=evBTIL;x{8F&#f&8f1 zR7h6LZn1m!RJk3tNfSo*@bavOw))3+m;!E0IPElJdGi6;veNq>Fpd~$oxm^brg^E6 zFt2H$rGJPR7i64R4JtcaD{IkAoV84$=tCuWUGO3z2I7sgZSXNR zm9A|HhfR!=LzWb8wKcQ5Y5k&t%kLT*+1iBAXp`sR?x&_An^LDDAKUOp*npgV0dSOc zB2QF3@m1*wzE$IQ&hx5UGN=ES+f`SnZol1bQJc6~NnK1vn{Yb*RzLDyk zS-a^&KmwLK-iK4?64X|3e_H@!oBgHWO=3r~??3{YCra;{S z@5d^6`0eavbu7FR@sE%N%ihlsaVMlD%J43<^UcX6-wKb{{q=VENcQ@<;U~tAT`P28 zS*b*z1?-wA>qAy?BE4aIG$E8yztD|0{a8h|I%J}|nfu{tkdKAk+mW^txIK0~9${Zl zk~20Hx)AW)^M5Qg>TfAk-@R?;AXbJ~8Jx?cf5&j$hH{c_t9&rqoaq?jHnsO66yWtWfNV&o0!fDlL{0R7ny$k5iQm*4Zb)LcE^0Co;mC^_-IY5P`NhcX-|tT<`V~E)EB(+Tc(8P$)><>RQ%tqzX3RGHnunv|23FbF9BV1(GomgL0QqA1y919y h*zFR!=^N This page provides an overview of the documentation and artefacts available to support your use of Ginan. -#### Videos: -The Ginan team have made a series of short videos to describe Ginan and help with its installation. They are: - -* [Ginan Video Tutorial - Introduction](https://www.youtube.com/watch?v=oP_vk5sci1k&list=PL0jP_ahe-BFnChGLpQmXYpHNFiRze4DZR&index=2) -* [Ginan Video Tutorial - Installation](https://www.youtube.com/watch?v=FAi2fg-7tbs&list=PL0jP_ahe-BFnChGLpQmXYpHNFiRze4DZR&index=2) -* [Ginan Video Tutorial - Docker Install](https://www.youtube.com/watch?v=uW1DcIbZk1g&list=PL0jP_ahe-BFnChGLpQmXYpHNFiRze4DZR&index=3) - #### Installation: The written notes on how to download and install supporting packages and the Ginan software are detailed [here](page.html?c=on&p=install.index) diff --git a/Docs/resources.md b/Docs/resources.md index a892979d1..51d95504a 100644 --- a/Docs/resources.md +++ b/Docs/resources.md @@ -10,11 +10,14 @@ > [![](images/GinanWorkshopS.png) Slides from the Ginan Workshop hosted at the IGNSS 2024 conference.](resources/Ginan_Workshop_Slides_-_IGNSS_2024.pdf) -> [![](images/GinanWorkshop1.png) Set Up Guide for the Ginan Workshop.](resources/Ginan_Workshop_1_Set-up_Guide_-_IGNSS_2024_-_6_Feb.pdf) +> [![](images/GinanWorkshop2025WindowsSetUp-TN2.png) Set-up Guide for Windows Users from 2025 Geodesy Workshop at ANU.](resources/Ginan-Workshop--Windows-Set-up--2025-06-23.pdf) -> [![](images/GinanWorkshop2.png) Guide outlining how to run Ginan in the Workshop Tutorial.](resources/Ginan_Workshop_2_Running_Ginan_-_IGNSS_2024_-_6_Feb.pdf) +> [![](images/GinanWorkshop1.png) Set Up Guide for the Ginan 2024 Workshop.](resources/Ginan_Workshop_1_Set-up_Guide_-_IGNSS_2024_-_6_Feb.pdf) + +> [![](images/GinanWorkshop2.png) Guide outlining how to run Ginan in the 2024 Workshop Tutorial.](resources/Ginan_Workshop_2_Running_Ginan_-_IGNSS_2024_-_6_Feb.pdf) + +> [![](images/GinanWorkshopD.png) Guide for those using Docker to run Ginan - 2024.](resources/Ginan_Workshop_Docker_Guide_-_IGNSS_2024_-_6_Feb.pdf) -> [![](images/GinanWorkshopD.png) Guide for those using Docker to run Ginan.](resources/Ginan_Workshop_Docker_Guide_-_IGNSS_2024_-_6_Feb.pdf) *** diff --git a/Docs/resources/Ginan-Workshop--Windows-Set-up--2025-06-23.pdf b/Docs/resources/Ginan-Workshop--Windows-Set-up--2025-06-23.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4054e102a1faf4c5705edb46edc0fd16363f1a98 GIT binary patch literal 137199 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