From 69c0da00ca100fef29e79d19b577e578c1f5a6a2 Mon Sep 17 00:00:00 2001 From: Franklyn Dunbar Date: Thu, 30 Jul 2026 14:50:04 -0800 Subject: [PATCH] Add missing AI Ambiguity validation and ISB model keys to pdp3 config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pdp3 (v3.2.10) unconditionally reads 'AI Ambiguity validation' from the ctrl file; when the key is absent, get_ctrl returns a value with embedded newlines that crashes pdp3.sh's sed substitution, and every PPP run silently produces 0 KIN files. The generated config was missing the key in three places, so it was dropped even when the installed PRIDE template had it: the AmbiguityFixingOptions model had no field, read_config_file had no parsing branch (load_default reads the installed template and stripped it), and write_config_file never emitted it. The same read/write round-trip also stripped 'ISB model' — present in the shipped config_template but absent from the model, reader, and writer. earthscope-sfg-workflows currently monkeypatches write_config_file to inject it; with this fix that workaround can be dropped. New test_config_file.py covers the round-trip for both keys and adds a drift guard: PRIDE-PPPAR's shipped table/config_template at the pinned commit b7451a8 is vendored as a test resource, and tests assert that the shipped template, the writer output, and the read→write round-trip all cover every upstream key. The ISB gap was found by this guard. Refresh the vendored copy when re-pinning the table source. Fixes #28. Co-Authored-By: Claude Fable 5 --- .../src/pride_ppp/specifications/config.py | 18 ++ .../pride_ppp/specifications/config_template | 1 + .../data/upstream_config_template_b7451a8 | 212 ++++++++++++++++++ packages/pride-ppp/tests/test_config_file.py | 119 ++++++++++ 4 files changed, 350 insertions(+) create mode 100644 packages/pride-ppp/tests/data/upstream_config_template_b7451a8 create mode 100644 packages/pride-ppp/tests/test_config_file.py diff --git a/packages/pride-ppp/src/pride_ppp/specifications/config.py b/packages/pride-ppp/src/pride_ppp/specifications/config.py index 63cda5b..8d0d691 100644 --- a/packages/pride-ppp/src/pride_ppp/specifications/config.py +++ b/packages/pride-ppp/src/pride_ppp/specifications/config.py @@ -289,6 +289,8 @@ class DataProcessingStrategies(BaseModel): rck_model : str Receiver clock model: ``"WNO"`` (white noise) or ``"STO"`` (random walk). + isb_model : str + GNSS receiver inter-system biases to be processed. ztd_model : str Zenith troposphere delay model: ``"PWC:60"`` (piece-wise constant, 60 min) or ``"STO"`` (random walk). @@ -304,6 +306,7 @@ class DataProcessingStrategies(BaseModel): strict_editing: str = "Default" rck_model: str = "Default" + isb_model: str = "Default" ztd_model: str = "Default" htg_model: str = "Default" iono_2nd: str = "Default" @@ -320,6 +323,10 @@ class AmbiguityFixingOptions(BaseModel): ``"YES"`` to use LAMBDA method for ambiguity fixing. ambiguity_duration : int Minimum time duration in seconds for a resolvable ambiguity. + ai_ambiguity_validation : str + ``"YES"``/``"NO"`` — validate ambiguity fixing with SVM. Required + by pdp3 (v3.2.10+); omitting the key crashes its ``sed`` handling + and every run silently produces 0 KIN files. cutoff_elevation : int Cutoff mean elevation angle (degrees) for eligible ambiguities. pco_on_wide_lane : str @@ -339,6 +346,7 @@ class AmbiguityFixingOptions(BaseModel): ambiguity_co_var: str = "Default" ambiguity_duration: int = 600 + ai_ambiguity_validation: str = "YES" cutoff_elevation: int = 15 pco_on_wide_lane: str = "YES" widelane_decision: list[float] = Field(default_factory=lambda: [0.20, 0.15, 1000.0]) @@ -522,6 +530,9 @@ def write_config_file(self, filepath: str | Path): f.write( f"RCK model = {proc.rck_model} ! receiver clock (WNO/STO). WNO, white noise\n" ) + f.write( + f"ISB model = {proc.isb_model} ! GNSS receiver inter-system biases to be processed\n" + ) f.write( f"ZTD model = {proc.ztd_model} ! zenith troposphere delay (PWC/STO). PWC:60, piece-wise constant for 60 min. STO, random walk\n" ) @@ -547,6 +558,9 @@ def write_config_file(self, filepath: str | Path): f.write( f"Ambiguity duration = {amb.ambiguity_duration} ! time duration in seconds for a resolvable ambiguity\n" ) + f.write( + f"AI Ambiguity validation = {amb.ai_ambiguity_validation} ! Ambiguity fixing validation is SVM or not\n" + ) f.write( f"Cutoff elevation = {amb.cutoff_elevation} ! cutoff mean elevation for eligible ambiguities to be resolved\n" ) @@ -714,6 +728,8 @@ def parse_satellite_list(lines): proc_kwargs["strict_editing"] = get_value(line) elif "RCK model" in line: proc_kwargs["rck_model"] = get_value(line) + elif "ISB model" in line: + proc_kwargs["isb_model"] = get_value(line) elif "ZTD model" in line: proc_kwargs["ztd_model"] = get_value(line) elif "HTG model" in line: @@ -734,6 +750,8 @@ def parse_satellite_list(lines): amb_kwargs["ambiguity_co_var"] = get_value(line) elif "Ambiguity duration" in line: amb_kwargs["ambiguity_duration"] = int(get_value(line)) + elif "AI Ambiguity validation" in line: + amb_kwargs["ai_ambiguity_validation"] = get_value(line) elif "Cutoff elevation" in line: amb_kwargs["cutoff_elevation"] = int(get_value(line)) elif "PCO on wide-lane" in line: diff --git a/packages/pride-ppp/src/pride_ppp/specifications/config_template b/packages/pride-ppp/src/pride_ppp/specifications/config_template index e72ea06..715cd68 100644 --- a/packages/pride-ppp/src/pride_ppp/specifications/config_template +++ b/packages/pride-ppp/src/pride_ppp/specifications/config_template @@ -29,6 +29,7 @@ Multipath = Default ! use the multipath correction ## Ambiguity fixing options Ambiguity co-var = Default ! change to YES if the Ambiguity fixing method is LAMBDA Ambiguity duration = 600 ! time duration in seconds for a resolvable ambiguity +AI Ambiguity validation = YES ! Ambiguity fixing validation is SVM or not Cutoff elevation = 15 ! cutoff mean elevation for eligible ambiguities to be resolved PCO on wide-lane = YES ! pco corrections on Melbourne-Wubbena or not Widelane decision = 0.20 0.15 1000. ! deviation (cycle), sigma (cycle) and decision threshold for WL ambiguities diff --git a/packages/pride-ppp/tests/data/upstream_config_template_b7451a8 b/packages/pride-ppp/tests/data/upstream_config_template_b7451a8 new file mode 100644 index 0000000..216dbff --- /dev/null +++ b/packages/pride-ppp/tests/data/upstream_config_template_b7451a8 @@ -0,0 +1,212 @@ +# Configuration template for PRIDE PPP-AR 3 + +## Observation configuration +Frequency combination = Default +Interval = Default +Time window = 0.01 +Session time = -YYYY- -MM- -DD- -HH- -MI- -SS- -SE- +Table directory = Default + +## Satellite product +Product directory = Default +Satellite orbit = Default +Satellite clock = Default +ERP = Default +Quaternions = Default +Code/phase bias = Default +LEO quaternions = Default + +## Data processing strategies +Strict editing = Default ! change to NO if using high-dynamic data with bad quality +RCK model = Default ! receiver clock (WNO/STO). WNO, white noise +ISB model = Default ! GNSS receiver inter-system biases to be processed +ZTD model = Default ! zenith troposphere delay (PWC/STO). PWC:60, piece-wise constant for 60 min. STO, random walk +HTG model = Default ! horizontal troposphere gradient (PWC/STO/NON) +Iono 2nd = Default ! change to YES if correcting 2-order ionospheric delays +Tides = SOLID/OCEAN/POLE ! remove any to shut it down, or changed to NON if not correcting tidal errors +Multipath = Default ! use the multipath correction model (YES/NO) + +## Ambiguity fixing options +Ambiguity co-var = Default ! change to YES if the Ambiguity fixing method is LAMBDA +Ambiguity duration = 600 ! time duration in seconds for a resolvable ambiguity +AI Ambiguity validation = YES ! Ambiguity fixing validation is SVM or not +Cutoff elevation = 15 ! cutoff mean elevation for eligible ambiguities to be resolved +PCO on wide-lane = YES ! pco corrections on Melbourne-Wubbena or not +Widelane decision = 0.20 0.15 1000. ! deviation (cycle), sigma (cycle) and decision threshold for WL ambiguities +Narrowlane decision = 0.15 0.15 1000. ! deviation (cycle), sigma (cycle) and decision threshold for NL ambiguities +Critical search = 3 4 1.8 3.0 ! highest number of ambiguities to be excluded, lowest number to be reserved, fixed/float, ratio threshold +Truncate at midnight = Default ! truncate all ambiguities at midnight to avoid day boundary discontinuity +Verbose output = NO ! output detailed information of ambiguity resolution + +## Satellite list +# Inserting `#' at the beginning of individual GNSS PRN means not to use this satellite ++GNSS satellites +*PRN variance + G01 1 + G02 1 + G03 1 + G04 1 + G05 1 + G06 1 + G07 1 + G08 1 + G09 1 + G10 1 + G11 1 + G12 1 + G13 1 + G14 1 + G15 1 + G16 1 + G17 1 + G18 1 + G19 1 + G20 1 + G21 1 + G22 1 + G23 1 + G24 1 + G25 1 + G26 1 + G27 1 + G28 1 + G29 1 + G30 1 + G31 1 + G32 1 + R01 1 + R02 1 + R03 1 + R04 1 + R05 1 + R06 1 + R07 1 + R08 1 + R09 1 + R10 1 + R11 1 + R12 1 + R13 1 + R14 1 + R15 1 + R16 1 + R17 1 + R18 1 + R19 1 + R20 1 + R21 1 + R22 1 + R23 1 + R24 1 + E01 1 + E02 1 + E03 1 + E04 1 + E05 1 + E06 1 + E07 1 + E08 1 + E09 1 + E10 1 + E11 1 + E12 1 + E13 1 + E14 1 + E15 1 + E16 1 + E17 1 + E18 1 + E19 1 + E20 1 + E21 1 + E22 1 + E23 1 + E24 1 + E25 1 + E26 1 + E27 1 + E28 1 + E29 1 + E30 1 + E31 1 + E32 1 + E33 1 + E34 1 + E35 1 + E36 1 +#C01 3 +#C02 3 +#C03 3 +#C04 3 +#C05 3 + C06 1 + C07 1 + C08 1 + C09 1 + C10 1 + C11 1 + C12 1 + C13 1 + C14 1 + C15 1 + C16 1 + C17 1 + C18 3 + C19 1 + C20 1 + C21 1 + C22 1 + C23 1 + C24 1 + C25 1 + C26 1 + C27 1 + C28 1 + C29 1 + C30 1 + C31 1 + C32 1 + C33 1 + C34 1 + C35 1 + C36 1 + C37 1 + C38 1 + C39 1 + C40 1 + C41 1 + C42 1 + C43 1 + C44 1 + C45 1 + C46 1 + C47 1 + C48 1 + C56 1 + C57 1 + C58 1 +#C59 3 +#C60 3 +#C61 3 + J01 1 + J02 1 + J03 1 +#J07 3 +-GNSS satellites + +## Option line +# There should be only one option line to be processed +# Arguments can be replaced by command-line automatically +# Available positioning mode: S -- static +# P -- piec-wise +# K -- kinematic +# F -- fixed +# Available mapping function: NIE -- Niell Mapping Function (NMF) +# GMF -- Global Mapping Function (GMF) +# VM1 -- Vienna Mapping Function (VMF1) +# VM3 -- Vienna Mapping Function (VMF3) +# Other arguments can be kept if you are not familiar with them ++Station used +*NAME TP MAP CLKm PoDm EV ZTDm PoDm HTGm PoDm RAGm PHSc PoLns PoXEm PoYNm PoZHm + xxxx X XXX 9000 xxxxx xx 0.20 xxxxx .005 xxxxx 0.30 0.01 xxxxx 10.00 10.00 10.00 +-Station used diff --git a/packages/pride-ppp/tests/test_config_file.py b/packages/pride-ppp/tests/test_config_file.py new file mode 100644 index 0000000..64ad188 --- /dev/null +++ b/packages/pride-ppp/tests/test_config_file.py @@ -0,0 +1,119 @@ +"""Tests for pride_ppp.specifications.config — pdp3 config_file generation. + +Guards against ctrl-file key drift (issue #28): pdp3's set of required +keys grows over time (`ISB model`, then `AI Ambiguity validation`), and a +generated config_file missing a required key makes every PPP run silently +produce 0 KIN files via a sed crash inside pdp3.sh. + +``data/upstream_config_template_b7451a8`` is PRIDE-PPPAR's shipped +``table/config_template`` at the commit the table source is pinned to +(see pride_table_config.yaml). Refresh it when re-pinning. +""" + +from __future__ import annotations + +from pathlib import Path + +from pride_ppp.specifications.config import ( + ObservationConfig, + PRIDEPPPFileConfig, + SatelliteProducts, +) + +DATA_DIR = Path(__file__).parent / "data" +UPSTREAM_TEMPLATE = DATA_DIR / "upstream_config_template_b7451a8" +SHIPPED_TEMPLATE = ( + Path(__file__).parents[1] / "src" / "pride_ppp" / "specifications" / "config_template" +) + + +def _config_keys(text: str) -> list[str]: + """Extract ``key`` names from ``key = value`` ctrl-file lines.""" + keys = [] + for line in text.splitlines(): + line = line.rstrip() + if not line or line.startswith(("#", "!", "+", "-", "*")) or "=" not in line: + continue + key = line.split("=")[0].strip() + if key: + keys.append(key) + return keys + + +def _default_config() -> PRIDEPPPFileConfig: + return PRIDEPPPFileConfig( + observation=ObservationConfig(table_directory="Default"), + satellite_products=SatelliteProducts(), + ) + + +def _write_to_text(config: PRIDEPPPFileConfig, tmp_path: Path) -> str: + dest = tmp_path / "config_file" + config.write_config_file(dest) + return dest.read_text() + + +class TestAiAmbiguityValidation: + def test_written_config_contains_key(self, tmp_path: Path) -> None: + text = _write_to_text(_default_config(), tmp_path) + assert "AI Ambiguity validation = YES" in text + + def test_key_round_trips(self, tmp_path: Path) -> None: + """A template that sets the key to NO must keep NO through + read → write.""" + config = _default_config() + config.ambiguity.ai_ambiguity_validation = "NO" + dest = tmp_path / "config_file" + config.write_config_file(dest) + + reread = PRIDEPPPFileConfig.read_config_file(str(dest)) + assert reread.ambiguity.ai_ambiguity_validation == "NO" + + def test_key_injected_when_template_lacks_it(self, tmp_path: Path) -> None: + """Reading an old installed template without the key (pre-3.2.10 + PRIDE) must still produce a config_file that includes it.""" + old_template = tmp_path / "old_template" + old_template.write_text( + SHIPPED_TEMPLATE.read_text().replace( + "AI Ambiguity validation = YES ! Ambiguity fixing validation is SVM or not\n", + "", + ) + ) + config = PRIDEPPPFileConfig.read_config_file(str(old_template)) + assert config.ambiguity.ai_ambiguity_validation == "YES" + assert "AI Ambiguity validation = YES" in _write_to_text(config, tmp_path) + + +class TestUpstreamKeyDrift: + """Fail when pdp3's ctrl-file keys drift ahead of this wrapper.""" + + def test_shipped_template_covers_upstream_keys(self) -> None: + upstream = set(_config_keys(UPSTREAM_TEMPLATE.read_text())) + shipped = set(_config_keys(SHIPPED_TEMPLATE.read_text())) + missing = upstream - shipped + assert not missing, ( + f"config_template is missing keys that PRIDE-PPPAR's shipped " + f"template declares: {sorted(missing)}. pdp3 may crash on the " + f"absent keys (see issue #28)." + ) + + def test_written_config_covers_upstream_keys(self, tmp_path: Path) -> None: + """The writer itself must emit every upstream key — a model field + without a corresponding write line would silently drop it.""" + upstream = set(_config_keys(UPSTREAM_TEMPLATE.read_text())) + written = set(_config_keys(_write_to_text(_default_config(), tmp_path))) + missing = upstream - written + assert not missing, ( + f"write_config_file omits keys that PRIDE-PPPAR's shipped " + f"template declares: {sorted(missing)} (see issue #28)." + ) + + def test_upstream_keys_survive_read_write_round_trip(self, tmp_path: Path) -> None: + """Reading the upstream template and writing it back must not strip + any of its keys — this is exactly how issue #28 manifested: the + installed template had the key, but read → write dropped it.""" + upstream_text = UPSTREAM_TEMPLATE.read_text() + config = PRIDEPPPFileConfig.read_config_file(str(UPSTREAM_TEMPLATE)) + written = set(_config_keys(_write_to_text(config, tmp_path))) + missing = set(_config_keys(upstream_text)) - written + assert not missing, f"read_config_file → write_config_file strips keys: {sorted(missing)}"