Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions experiments/build_gsdc2023_bridge_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def build_config(args: argparse.Namespace) -> BridgeConfig:
hatch_smoothing_n=int(getattr(args, "hatch_smoothing_n", 100)),
use_rtklib_tropo=bool(getattr(args, "use_rtklib_tropo", False)),
apply_base_correction=bool(getattr(args, "base_correction", False)),
base_correction_fgo_only_rows=bool(getattr(args, "base_correction_fgo_only_rows", False)),
position_source=args.position_source,
chunk_epochs=args.chunk_epochs,
gated_baseline_threshold=args.gated_threshold,
Expand Down Expand Up @@ -521,6 +522,7 @@ def run_bridge_submission(args: argparse.Namespace) -> tuple[pd.DataFrame, dict[
"max_epochs": args.max_epochs,
"dual_frequency": bool(args.dual_frequency),
"base_correction": bool(config.apply_base_correction),
"base_correction_fgo_only_rows": bool(config.base_correction_fgo_only_rows),
"ct_rbpf_fgo": bool(config.ct_rbpf_fgo_enabled),
"taroz_marupaku": taroz_marupaku,
"taroz_phone_aware": taroz_phone_aware,
Expand Down Expand Up @@ -589,6 +591,12 @@ def main(argv: list[str] | None = None) -> int:
default=False,
help="Mirror the post-fill pseudorange residual mask into the FGO weights (weights_fgo), so residual-rejected PR rows are dropped from the FGO objective too.",
)
parser.add_argument(
"--base-correction-fgo-only-rows",
action=argparse.BooleanOptionalAction,
default=False,
help="Also apply the DGNSS base correction to FGO-only rows (weights==0 but weights_fgo>0) — rows whose WLS weight was masked while the FGO weight was kept. Off by default keeps the legacy WLS-active-only behaviour.",
)
parser.add_argument("--stop-attitude-sigma-rad", type=float, default=0.0)
parser.add_argument(
"--taroz-fgo",
Expand Down
7 changes: 7 additions & 0 deletions experiments/gsdc2023_bridge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ class BridgeConfig:
apply_relative_height: bool = False
apply_position_offset: bool = False
apply_base_correction: bool = False
# When True, the DGNSS base correction is also applied to FGO-only rows
# (weights==0 but weights_fgo>0): FGO-only constellations and rows whose WLS
# weight was masked out while the FGO weight was kept. Off by default keeps
# the legacy WLS-active-only behaviour.
base_correction_fgo_only_rows: bool = False
graph_relative_height: bool = False
relative_height_sigma_m: float = 0.5
relative_height_huber_k: float = 0.0
Expand Down Expand Up @@ -385,6 +390,8 @@ def __post_init__(self) -> None:
raise ValueError("fgo_lm_damping must be >= 0")
if not isinstance(self.fgo_extra_constellations, bool):
raise ValueError("fgo_extra_constellations must be a bool")
if not isinstance(self.base_correction_fgo_only_rows, bool):
raise ValueError("base_correction_fgo_only_rows must be a bool")
for name in (
"stop_velocity_huber_k",
"stop_position_huber_k",
Expand Down
4 changes: 4 additions & 0 deletions experiments/gsdc2023_raw_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ def build_trip_arrays(
tdcp_cycle_jump_mask_cycles: float = 0.0,
tdcp_doppler_endpoint_mask: bool = True,
apply_base_correction: bool = False,
base_correction_fgo_only_rows: bool = False,
data_root: Path | None = None,
trip: str | None = None,
apply_observation_mask: bool = False,
Expand Down Expand Up @@ -801,6 +802,7 @@ def build_trip_arrays(
fgo_extra_constellations=fgo_extra_constellations,
factor_dt_max_s=factor_dt_max_s,
apply_base_correction=apply_base_correction,
base_correction_fgo_only_rows=base_correction_fgo_only_rows,
data_root=data_root,
trip=trip,
doppler_residual_mask_mps=doppler_residual_mask_mps,
Expand Down Expand Up @@ -3374,6 +3376,7 @@ def _build_taroz_fgo_candidate_batch(
tdcp_cycle_jump_mask_cycles=config.tdcp_cycle_jump_mask_cycles,
tdcp_doppler_endpoint_mask=config.tdcp_doppler_endpoint_mask,
apply_base_correction=config.apply_base_correction,
base_correction_fgo_only_rows=bool(getattr(config, "base_correction_fgo_only_rows", False)),
data_root=data_root,
trip=trip,
apply_observation_mask=config.apply_observation_mask,
Expand Down Expand Up @@ -4060,6 +4063,7 @@ def validate_raw_gsdc2023_trip(
tdcp_cycle_jump_mask_cycles=cfg.tdcp_cycle_jump_mask_cycles,
tdcp_doppler_endpoint_mask=cfg.tdcp_doppler_endpoint_mask,
apply_base_correction=cfg.apply_base_correction,
base_correction_fgo_only_rows=bool(getattr(cfg, "base_correction_fgo_only_rows", False)),
data_root=data_root,
trip=trip,
apply_observation_mask=cfg.apply_observation_mask,
Expand Down
20 changes: 19 additions & 1 deletion experiments/gsdc2023_trip_stages.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ class PostObservationStageConfig:
default_pr_l1_threshold_m: float
default_pr_l5_threshold_m: float
fgo_extra_constellations: bool = False
base_correction_fgo_only_rows: bool = False
tdcp_cycle_jump_mask_cycles: float = 0.0
tdcp_doppler_endpoint_mask: bool = True
tdcp_ddl_sign_fixed: bool = False
Expand Down Expand Up @@ -492,6 +493,8 @@ def apply_base_correction_to_pseudorange(
signal_type: str,
pseudorange: np.ndarray,
weights: np.ndarray,
weights_fgo: np.ndarray | None = None,
include_fgo_only: bool = False,
correction_matrix_fn: BaseCorrectionMatrixFn,
) -> int:
if data_root is None or trip is None:
Expand All @@ -503,7 +506,16 @@ def apply_base_correction_to_pseudorange(
slot_keys,
signal_type,
)
valid_base_correction = np.isfinite(base_correction) & (weights > 0.0)
# The shared pseudorange feeds both WLS (weights) and FGO (weights_fgo).
# By default only WLS-active rows are corrected (legacy behaviour). When
# ``include_fgo_only`` is set, rows that are FGO-only (weights==0 but
# weights_fgo>0 — FGO-only constellations, or rows whose WLS weight was
# masked while the FGO weight was kept) also receive the DGNSS correction
# so the FGO objective sees corrected pseudoranges.
active = weights > 0.0
if include_fgo_only and weights_fgo is not None:
active = active | (weights_fgo > 0.0)
valid_base_correction = np.isfinite(base_correction) & active
pseudorange[valid_base_correction] -= base_correction[valid_base_correction]
return int(np.count_nonzero(valid_base_correction))

Expand Down Expand Up @@ -1649,6 +1661,7 @@ def build_observation_mask_base_correction_stage(
*,
apply_observation_mask: bool,
apply_base_correction: bool,
base_correction_fgo_only_rows: bool = False,
data_root: Path | None,
trip: str | None,
times_ms: np.ndarray,
Expand Down Expand Up @@ -1837,6 +1850,8 @@ def build_observation_mask_base_correction_stage(
signal_type=signal_type,
pseudorange=pseudorange,
weights=weights,
weights_fgo=weights_fgo,
include_fgo_only=base_correction_fgo_only_rows,
correction_matrix_fn=correction_matrix_fn,
)

Expand Down Expand Up @@ -1901,6 +1916,7 @@ def build_post_observation_stages(
clock_drift_context_mps: np.ndarray | None,
build_trip_arrays_fn: BuildTripArraysFn,
apply_base_correction: bool,
base_correction_fgo_only_rows: bool = False,
data_root: Path | None,
trip: str | None,
n_clock: int,
Expand Down Expand Up @@ -2028,6 +2044,7 @@ def build_post_observation_stages(
mask_base_stage = build_observation_mask_base_correction_stage(
apply_observation_mask=apply_observation_mask,
apply_base_correction=apply_base_correction,
base_correction_fgo_only_rows=base_correction_fgo_only_rows,
data_root=data_root,
trip=trip,
times_ms=times_ms,
Expand Down Expand Up @@ -2191,6 +2208,7 @@ def build_configured_post_observation_stages(
clock_drift_context_mps=observation_products.clock_drift_context_mps,
build_trip_arrays_fn=dependencies.build_trip_arrays_fn,
apply_base_correction=config.apply_base_correction,
base_correction_fgo_only_rows=config.base_correction_fgo_only_rows,
data_root=config.data_root,
trip=config.trip,
n_clock=observation_products.n_clock,
Expand Down
6 changes: 6 additions & 0 deletions tests/test_gsdc2023_bridge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def test_bridge_config_defaults_match_public_factor_dt() -> None:
assert cfg.taroz_imu_factor_mask_csv is None
assert cfg.taroz_stop_mask_from_seed_velocity is False
assert cfg.fgo_extra_constellations is False
assert cfg.base_correction_fgo_only_rows is False


def test_bridge_config_rejects_invalid_position_source() -> None:
Expand All @@ -53,6 +54,11 @@ def test_bridge_config_rejects_non_bool_fgo_extra_constellations() -> None:
BridgeConfig(fgo_extra_constellations=1) # type: ignore[arg-type]


def test_bridge_config_rejects_non_bool_base_correction_fgo_only_rows() -> None:
with pytest.raises(ValueError, match="base_correction_fgo_only_rows must be a bool"):
BridgeConfig(base_correction_fgo_only_rows=1) # type: ignore[arg-type]


def test_taroz_presets_enable_base_correction_and_unscaled_tdcp_weights() -> None:
fgo = apply_taroz_fgo_preset(BridgeConfig())
gnss_only = apply_taroz_gnss_only_preset(BridgeConfig())
Expand Down
54 changes: 54 additions & 0 deletions tests/test_gsdc2023_trip_stages.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,60 @@ def correction_matrix_fn(
assert calls == [(tmp_path, "train/course/phone", ("G01", "G02"), "GPS_L1_CA")]


def test_apply_base_correction_to_pseudorange_skips_fgo_only_rows_by_default(tmp_path: Path) -> None:
# Row [0, 1] is FGO-only: WLS weight masked (0) but FGO weight kept (>0).
pseudorange = np.array([[10.0, 20.0]], dtype=np.float64)
weights = np.array([[1.0, 0.0]], dtype=np.float64)
weights_fgo = np.array([[1.0, 5.0]], dtype=np.float64)
correction = np.array([[1.5, 2.0]], dtype=np.float64)

def correction_matrix_fn(*_args: object) -> np.ndarray:
return correction

count = apply_base_correction_to_pseudorange(
data_root=tmp_path,
trip="train/course/phone",
times_ms=np.array([1000.0], dtype=np.float64),
slot_keys=["G01", "G02"],
signal_type="GPS_L1_CA",
pseudorange=pseudorange,
weights=weights,
weights_fgo=weights_fgo,
correction_matrix_fn=correction_matrix_fn,
)

# Legacy default: only the WLS-active row [0, 0] is corrected.
assert count == 1
np.testing.assert_allclose(pseudorange, [[8.5, 20.0]])


def test_apply_base_correction_to_pseudorange_corrects_fgo_only_rows_when_enabled(tmp_path: Path) -> None:
pseudorange = np.array([[10.0, 20.0]], dtype=np.float64)
weights = np.array([[1.0, 0.0]], dtype=np.float64)
weights_fgo = np.array([[1.0, 5.0]], dtype=np.float64)
correction = np.array([[1.5, 2.0]], dtype=np.float64)

def correction_matrix_fn(*_args: object) -> np.ndarray:
return correction

count = apply_base_correction_to_pseudorange(
data_root=tmp_path,
trip="train/course/phone",
times_ms=np.array([1000.0], dtype=np.float64),
slot_keys=["G01", "G02"],
signal_type="GPS_L1_CA",
pseudorange=pseudorange,
weights=weights,
weights_fgo=weights_fgo,
include_fgo_only=True,
correction_matrix_fn=correction_matrix_fn,
)

# The FGO-only row [0, 1] now also receives the DGNSS correction.
assert count == 2
np.testing.assert_allclose(pseudorange, [[8.5, 18.0]])


def test_apply_base_correction_to_pseudorange_requires_trip_context() -> None:
with pytest.raises(RuntimeError, match="data_root and trip"):
apply_base_correction_to_pseudorange(
Expand Down
Loading