bernese: why LUZON cannot be reprocessed under I14 on 5.4 - #67
bernese: why LUZON cannot be reprocessed under I14 on 5.4#67alfieprojectsdev wants to merge 17 commits into
Conversation
…t is retired Answers the question the previous commit left open by reading her own retained processing summary, R2S251210.PRC, 56,781 lines. Her run completed with 26,172 warnings and 3 errors, and produced F1_251210.SNX regardless. In Bernese ### is a warning and *** an error; she had all three *** and the run still finished. So 5.2 pressed on through conditions 5.4 treats as fatal. The version difference is one of tolerance, not capability. The RCVOBS warnings also confirm her data was multi-GNSS -- Galileo and GLONASS PRNs on a TRIMBLE ALLOY -- so V_SATSYS=GPS meant those observations were skipped with a warning rather than being absent. Two independent blockers follow, neither a misconfiguration. The satellite tables end in 2023: hers 2023-01-31, 5.4's I14 2023-08-10, 5.4's I20 2024-09-17. AIUB no longer publishes SATELLIT_I14.SAT at all, 404 for I14 against 200 for I20. Against 2025 data the PRN-to-SVN resolution lands on stale entries and CODSPP stops on BLOCK IIR-A 044, a satellite that is present in the antenna file. And the I14 ANTEX fails 5.4's consistency validation outright: given SVN and PRN inconsistent, PRN 22 against SVN G041, file not converted. All three variants in her tree fail it. I14.ATX carries four G041 entries with different PRN mappings across epochs; the other two carry one each and fail identically. A file 5.2 consumed without complaint is invalid to 5.4. Setting V_MYATX therefore fails at PID 002 where leaving it blank reaches PID 232, so it is left blank with the reasoning recorded at the override rather than as a bare default. The recommendation is to run I20 first, explicitly as a pipeline test and not as a comparison. That establishes the chain executes end to end and is BRN-001 acceptance evidence in its own right. The finding that I14 cannot be run on 5.4 at this epoch is itself a result worth putting to Abegail, since it bears on how the LUZON series can be continued.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe changes document Bernese 5.4 Luzon reprocessing findings, identify I14 blockers, record I20 results, add frame selection, provide BLQ retrieval and merge tooling, and add a resumable month-long processing script. ChangesBernese 5.4 Luzon reprocessing
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Nine attempts at DOY 121 of 2025. No solution yet; the run reaches PID 232 CODSPP after importing RINEX, preparing orbits and converting 92 observation files in about 100 seconds. The plan was wrong in its premise rather than its details. Adapting the 5.2 PCF by renaming scripts and repairing WAIT lists cannot work, because the PCF file format changed between versions, and 5.4 answers a 5.2 PCF with a segmentation fault rather than a parse error. Four attempts passed before that surfaced. Section 14's conclusion that FTP_DWLD could be dropped was also wrong: the products are local in 5.2-era legacy naming and 5.4 reads long-name, so they were present and unusable at once. Two further model files had to be fetched from AIUB, including SAT_2025.CRX where the installed set stopped at 2019 -- readiness gap #6 arriving as predicted. These were reported here as needing credentials; they do not. ftp.aiub.unibe.ch is firewalled from this machine and the timeout was taken as proof of inaccessibility rather than as a reason to look for another route. The most instructive error was V_SATSYS. The override set copied V_GNSSAR=ALL on the reasoning that she resolved ambiguities across all constellations, which reads the variable backwards: V_GNSSAR selects which of the already-selected systems get resolution, while V_SATSYS selects the systems, and hers reads GPS. The run attempted GLONASS and died on a satellite absent from every I14 table. It presented as a missing file and was a selection error, and switching to I20 to "fix" it would have introduced the exact frame confound the exercise exists to isolate, with plausible-looking results. Her own retained log settles why 5.2 managed what 5.4 will not: 26,172 warnings and 3 errors, and it produced a solution regardless. The version difference is one of tolerance. Behind it lie two real blockers -- satellite tables that end in 2023 and are no longer published for I14, and an I14 ANTEX that fails 5.4's SVN-to-PRN consistency check in all three of her variants. Also records three tooling defects found by use, three Bernese environment variables that collided with script locals, and one commit pushed directly to main in breach of Rule 1.
…the last gap I20 cleared the I14 blocker, confirming the previous commit's diagnosis. Same PCF, same data, same GPS-only selection; only the frame and antenna model changed. The 24-station configuration is the deepest yet: it reaches PID 513 in 4m12s and produces FIN_20251210.NQ0, a final ambiguity-fixed solution. Its only failure is the closing QC gate, HELMR1 reporting no redundancy, and the cause is understood. 5.4's stock RNX2SNX has no V_RX3DIR -- that variable belongs to her extended 5.2 PCF -- so the RINEX 3 fiducials were never staged and none of the 24 locals is an IGS20 reference station. There was nothing to transform against. 5.4's RNX_COP does handle RINEX 3 natively, globbing long names from the same directory, so the fix was to stage both conventions into one place rather than two. All 32 stations then staged and RXOBV3 passed with every station matching a LUZON.STA entry. That surfaced the next stop: GTOCNL cannot find ocean-loading values for ALIC. No BLQ file anywhere in her tree contains it -- all four copies are identical and cover only the local network. So either her run used the fiducials purely as datum constraints rather than processing their observations, or 5.2 warned where 5.4 errors. Given that her run completed carrying three *** errors, the second is more likely, and it is the fourth instance of 5.4 enforcing a check 5.2 did not. Consequently no configuration completes a single day cleanly, and a multi-day batch is not yet worth running: 24 stations reaches the end without datum control, 32 stations stops at baseline editing. The next task is ocean-loading coefficients for the nine fiducials, freely available from the Onsala BLQ service, merged into LUZON.BLQ. That closes the last known gap and makes datum verification and a multi-day run meaningful together.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
scripts/derive_luzon_pcf.py (1)
137-159: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winKeep frame-specific overrides local to one invocation.
OVERRIDES.update(...)mutates module state. If a test or embedding process callsmain()with--frame i20and then with the defaulti14, the second call still uses I20 values. Copy the base mapping per invocation and pass it toapply_overrides.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/derive_luzon_pcf.py` around lines 137 - 159, Keep frame-specific override values scoped to each main() invocation instead of mutating the module-level OVERRIDES mapping. Create a per-invocation copy of the base mapping after argument parsing, apply the I20 updates to that copy, and pass the local mapping to apply_overrides; leave subsequent default I14 invocations using the original values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/bernese54_luzon_reprocessing_runbook.md`:
- Around line 590-597: Add the text language identifier to the fenced Markdown
blocks containing Bernese logs and output, including the blocks around the
visible SR RCVOBS example and the referenced sections. Preserve all block
contents unchanged while updating each fence to use text.
- Around line 647-652: Revise the I20 recommendation in the runbook to avoid
claiming that it already establishes end-to-end 31-day execution. Use future or
conditional wording that limits the claim to what the recorded partial runs
demonstrate, while preserving the separate I14 comparison and reporting
conclusions.
In `@docs/gps3-sessions/SESSION_LOG_20260729_storage.md`:
- Around line 1438-1439: Update the statement in the GPS-only processing
discussion to avoid claiming that GPS-only makes I14 valid or usable for 2025
data. Replace that causal wording with a narrower description that GPS-only
preserves the intended comparison or removes the GLONASS-861 constellation error
as one failure mode, consistent with the stale-table and ANTEX limitations
documented nearby.
---
Nitpick comments:
In `@scripts/derive_luzon_pcf.py`:
- Around line 137-159: Keep frame-specific override values scoped to each main()
invocation instead of mutating the module-level OVERRIDES mapping. Create a
per-invocation copy of the base mapping after argument parsing, apply the I20
updates to that copy, and pass the local mapping to apply_overrides; leave
subsequent default I14 invocations using the original values.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: cf4afdef-e6a0-4d3b-bd63-b4662ecd93a5
📒 Files selected for processing (3)
docs/bernese54_luzon_reprocessing_runbook.mddocs/gps3-sessions/SESSION_LOG_20260729_storage.mdscripts/derive_luzon_pcf.py
| ``` | ||
| ### SR RCVOBS: Satellite/system not found (x thousands) | ||
| Receiver name : TRIMBLE ALLOY | ||
| PRN : E02 / E03 / R01 | ||
|
|
||
| *** SR R2RDOH : NUMBER OF SAT. (NUMLST) > MAXSAT 136 > 135 | ||
| *** PG RXOBV3: TOO MANY OUTPUT FILES DEFINED 31 defined, 30 found | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add language identifiers to the fenced blocks.
markdownlint-cli2 reports MD040 at Lines 590, 622, 667, and 684. Use text for these Bernese log and output blocks.
Also applies to: 622-625, 667-669, 684-688
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 590-590: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/bernese54_luzon_reprocessing_runbook.md` around lines 590 - 597, Add the
text language identifier to the fenced Markdown blocks containing Bernese logs
and output, including the blocks around the visible SR RCVOBS example and the
referenced sections. Preserve all block contents unchanged while updating each
fence to use text.
Source: Linters/SAST tools
| Processing GPS-only is not a workaround. It is what she did, and it is why I14 | ||
| is usable against 2025 data at all. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not equate GPS-only selection with I14 model validity.
GPS-only removes the GLONASS-861 constellation error. It does not make I14 usable for 2025. Lines 1448–1456 immediately document stale I14 tables and ANTEX rejection. Replace “why I14 is usable against 2025 data at all” with wording that says GPS-only preserves the intended comparison or removes one failure mode.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/gps3-sessions/SESSION_LOG_20260729_storage.md` around lines 1438 - 1439,
Update the statement in the GPS-only processing discussion to avoid claiming
that GPS-only makes I14 valid or usable for 2025 data. Replace that causal
wording with a narrower description that GPS-only preserves the intended
comparison or removes the GLONASS-861 constellation error as one failure mode,
consistent with the stale-table and ANTEX limitations documented nearby.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/bernese/otl/README.md`:
- Line 7: Update the diagnostic code block in README.md to specify the text
language on its opening fence, changing the bare fence to a text fence while
preserving the block contents.
- Around line 27-33: Add a step in the README instructions to select CREATE
REQUEST on the linked portal before the existing step that pastes
REQUEST_fiducials.txt into the coordinate box, and renumber subsequent steps so
the workflow reaches the request form in the correct order.
In `@scripts/merge_blq.py`:
- Around line 116-124: Update the incoming-block validation near the `bad` loop
to require exactly six non-comment data rows, reject any additional data rows,
and verify every token in each row is a finite numeric value rather than
checking only `len(r.split())`. Preserve comment and station-line filtering,
ensure invalid blocks prevent `--apply` from writing them, and add regression
tests covering an extra row and a nonnumeric value.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 93675562-6ac6-4003-a663-2e875169b990
📒 Files selected for processing (3)
config/bernese/otl/README.mdconfig/bernese/otl/REQUEST_fiducials.txtscripts/merge_blq.py
| fiducials** the LUZON network uses as datum control. With those staged, | ||
| `GPSEDT` stops: | ||
|
|
||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Specify a language for the diagnostic block.
Replace the bare fence with ```text. This resolves markdownlint MD040.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 7-7: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/bernese/otl/README.md` at line 7, Update the diagnostic code block in
README.md to specify the text language on its opening fence, changing the bare
fence to a text fence while preserving the block contents.
Source: Linters/SAST tools
| 1. Open <https://barre.oso.chalmers.se/loading/l.php> | ||
| (the older Holt portal is down — a mail-system fault since 2024-05.) | ||
| 2. Paste the contents of `REQUEST_fiducials.txt` into the coordinate box. | ||
| 3. Ocean tide model: **FES2014b** unless there is a reason to match an older | ||
| run. Note what you chose — the model is part of the provenance. | ||
| 4. Output format: **BLQ**, with radial and horizontal components. | ||
| 5. Give your email address; the reply arrives as plain text. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add the request-creation step.
Line 29 directs the user to paste coordinates on the entry page. The linked page requires CREATE REQUEST before it shows the request form and coordinate input. Add this step before the paste instruction. The current sequence prevents a new user from reaching the input form. (barre.oso.chalmers.se)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/bernese/otl/README.md` around lines 27 - 33, Add a step in the README
instructions to select CREATE REQUEST on the linked portal before the existing
step that pastes REQUEST_fiducials.txt into the coordinate box, and renumber
subsequent steps so the workflow reaches the request form in the correct order.
| # Sanity-check each incoming block: 6 numeric rows, 11 columns each. | ||
| bad = [] | ||
| for name in to_add + (clash if args.replace else []): | ||
| rows = [ln for ln in incoming[name] | ||
| if ln.strip() and not ln.strip().startswith("$$") | ||
| and not _STATION_RE.match(ln)] | ||
| numeric = [r for r in rows if len(r.split()) == 11] | ||
| if len(numeric) != 6: | ||
| bad.append(f"{name}: {len(numeric)} rows of 11 values (expected 6)") |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reject nonnumeric and extra data rows.
Line 122 checks only the token count. A block with six 11-token rows plus an extra row, or with nonnumeric tokens, passes validation. --apply then writes an invalid block into the target BLQ file.
Require exactly six non-comment data rows. Validate every value as a finite number. Add regression tests for an extra row and a nonnumeric value. As per coding guidelines, use a TDD approach where applicable.
Proposed validation
import argparse
+import math
import re
...
+def _is_finite_number(value: str) -> bool:
+ try:
+ return math.isfinite(float(value.replace("D", "E").replace("d", "e")))
+ except (OverflowError, ValueError):
+ return False
+
...
- numeric = [r for r in rows if len(r.split()) == 11]
- if len(numeric) != 6:
- bad.append(f"{name}: {len(numeric)} rows of 11 values (expected 6)")
+ valid_rows = [
+ row
+ for row in rows
+ if len(row.split()) == 11
+ and all(_is_finite_number(value) for value in row.split())
+ ]
+ if len(rows) != 6 or len(valid_rows) != 6:
+ bad.append(f"{name}: expected 6 rows x 11 finite numeric values")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Sanity-check each incoming block: 6 numeric rows, 11 columns each. | |
| bad = [] | |
| for name in to_add + (clash if args.replace else []): | |
| rows = [ln for ln in incoming[name] | |
| if ln.strip() and not ln.strip().startswith("$$") | |
| and not _STATION_RE.match(ln)] | |
| numeric = [r for r in rows if len(r.split()) == 11] | |
| if len(numeric) != 6: | |
| bad.append(f"{name}: {len(numeric)} rows of 11 values (expected 6)") | |
| # Sanity-check each incoming block: 6 numeric rows, 11 columns each. | |
| bad = [] | |
| for name in to_add + (clash if args.replace else []): | |
| rows = [ln for ln in incoming[name] | |
| if ln.strip() and not ln.strip().startswith("$$") | |
| and not _STATION_RE.match(ln)] | |
| valid_rows = [ | |
| row | |
| for row in rows | |
| if len(row.split()) == 11 | |
| and all(_is_finite_number(value) for value in row.split()) | |
| ] | |
| if len(rows) != 6 or len(valid_rows) != 6: | |
| bad.append(f"{name}: expected 6 rows x 11 finite numeric values") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/merge_blq.py` around lines 116 - 124, Update the incoming-block
validation near the `bad` loop to require exactly six non-comment data rows,
reject any additional data rows, and verify every token in each row is a finite
numeric value rather than checking only `len(r.split())`. Preserve comment and
station-line filtering, ensure invalid blocks prevent `--apply` from writing
them, and add regression tests covering an extra row and a nonnumeric value.
Source: Coding guidelines
LUZON.BLQ covers the 135 local stations and none of the seven IGS fiducials, so with those staged GPSEDT stops on missing ocean-loading values for ALIC. No BLQ anywhere in the 5.2 capture contains it, so the coefficients were never computed for those sites rather than being mislaid. They are free from the Chalmers/Onsala service, but that service delivers by email, so one manual step cannot be automated away. Prepared either side of it instead. REQUEST_fiducials.txt holds the seven stations as longitude, latitude and height, the format the request form expects, derived from the same LUZON.CRD coordinates the BPE processes so the coefficients apply to the exact points in use. Six of the seven were cross-checked against Abegail's own F1_251210.SNX SITE/ID block and agree to the arcsecond; the seventh, PIMO, does not appear in her solution. merge_blq.py merges the reply back. It refuses to add a station already present unless asked, because a duplicate BLQ block is worse than a missing one -- which of two conflicting entries Bernese honours is not evident from the file -- and it validates that each incoming block is six rows of eleven values before writing anything, then backs up. The README records that the older Holt portal has been down with a mail fault since 2024-05 and that requests now go to Barre, and asks for the chosen ocean tide model to be written down, since it is part of the provenance of every coordinate that follows.
The first version matched a station line as two spaces followed by a
single token, which is what the format documentation shows. Run against
the actual LUZON.BLQ it found ZERO stations in a file holding 135.
Real Scherneck output repeats the name on the header line, sometimes
separated by a tab:
" ABUY ABUY"
" ALBU <TAB> ALBU"
Data rows also begin with two spaces, so the discriminator is the token
count -- one or two, with a non-numeric first token -- rather than the
indentation. Amplitude and phase rows carry eleven numeric values.
Now parses all 135 stations and validates each block as six rows of
eleven values before writing anything.
Worth noting how it was caught: the tool was run against a file whose
contents were already known, so a wrong answer was recognisable as
wrong. Had it first been run against the Onsala reply, zero parsed
stations would have looked like a problem with the reply.
Correcting the earlier recommendation of FES2014b. LUZON.BLQ records its own provenance in the header, and the 135 stations already in it were computed with FES2004, CMC NO, and the Gutenberg-Bullen Greens function. Requesting a newer model would put the seven datum fiducials on a different ocean model from the 135 stations they constrain. That inconsistency does not announce itself in the file -- the header would describe the new blocks correctly and the old ones would keep their own banner -- and it would bias the datum relative to the network in a way that is hard to trace back. A newer model is preferable only if the whole file is recomputed, which is a separate decision and a larger one. Also switches the request from geodetic to ECEF XYZ. The form accepts name X Y Z as an alternative to longitude, latitude and height, so the coordinates now go in exactly as they appear in LUZON.CRD. That removes the geodetic conversion entirely, and with it the lon/lat swap the form warns about and asks users to promise against. Station names are bare four-character codes, matching how the existing blocks are written rather than the name-plus-DOMES form the error message displays. Records the full form settings with the reason for each, the queue and why-not links for a reply that does not arrive, and the request to cite Bos and Scherneck if the results reach publication.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
scripts/merge_blq.py (3)
170-170: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftWrite the merged BLQ through an atomic replacement.
Path.write_texttruncates the target before the write completes. An interruption or write failure can leave the active BLQ partially written even though a backup exists.Write to a temporary file in the same directory, flush it, and replace the target atomically after the backup succeeds.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/merge_blq.py` at line 170, Replace the direct args.blq.write_text call with an atomic write flow: after the backup succeeds, create a temporary file in the target’s directory, write and flush the merged BLQ contents, then atomically replace args.blq with it. Ensure temporary-file cleanup on failure while preserving the existing ASCII encoding and backup ordering.
158-160: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winMake backup names collision-safe.
The timestamp has one-second precision. Two
--applyruns within the same second use the same backup path, so the secondcopy2call overwrites the first backup.Use an exclusive, unique backup name or fail when the path already exists.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/merge_blq.py` around lines 158 - 160, Update the backup creation around the timestamp, backup, and shutil.copy2 calls so each --apply run uses a collision-safe name or fails if the generated path already exists; preserve the existing .BLQ.bak naming pattern while preventing a second run from overwriting the first backup.
75-83: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winKeep file-level BLQ headers outside station blocks.
Before the first station,
pendingcollects the file header. Line 80 prepends that header to the first block. If--replacetargets the first station, Line 168 replaces the header together with the station block. This can remove the ocean-model, column-order, and provenance comments fromLUZON.BLQ.Preserve the file preamble separately and replace only the station block.
Also applies to: 165-168
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/merge_blq.py` around lines 75 - 83, Keep the file-level preamble collected in pending separate from station block data when finalizing the first station in the block-building flow. Update the --replace handling around blocks/current so replacing a station only replaces that station’s lines and preserves the preamble comments in the output. Ensure pending is emitted independently before the station blocks rather than prepended into buf.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/bernese/otl/README.md`:
- Around line 53-54: Update the README example currently shown as an inline code
span so the meaningful leading spaces are preserved in a fenced text block.
Remove the inline formatting for that sample while keeping the example content
unchanged and resolving Markdownlint MD038.
In `@scripts/merge_blq.py`:
- Around line 75-80: Update the station-block parsing flow around _station_name
and the blocks assignment to detect repeated station names instead of
overwriting the existing entry. Track duplicates during parsing, propagate the
parsing failure to main(), and abort before any output is written or
validation/apply proceeds.
---
Outside diff comments:
In `@scripts/merge_blq.py`:
- Line 170: Replace the direct args.blq.write_text call with an atomic write
flow: after the backup succeeds, create a temporary file in the target’s
directory, write and flush the merged BLQ contents, then atomically replace
args.blq with it. Ensure temporary-file cleanup on failure while preserving the
existing ASCII encoding and backup ordering.
- Around line 158-160: Update the backup creation around the timestamp, backup,
and shutil.copy2 calls so each --apply run uses a collision-safe name or fails
if the generated path already exists; preserve the existing .BLQ.bak naming
pattern while preventing a second run from overwriting the first backup.
- Around line 75-83: Keep the file-level preamble collected in pending separate
from station block data when finalizing the first station in the block-building
flow. Update the --replace handling around blocks/current so replacing a station
only replaces that station’s lines and preserves the preamble comments in the
output. Ensure pending is emitted independently before the station blocks rather
than prepended into buf.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: bd0b190d-70a0-4223-9a61-946118e1b865
📒 Files selected for processing (3)
config/bernese/otl/README.mdconfig/bernese/otl/REQUEST_fiducials.txtscripts/merge_blq.py
🚧 Files skipped from review as they are similar to previous changes (1)
- config/bernese/otl/REQUEST_fiducials.txt
| Names are bare 4-character codes, matching how the existing blocks are written | ||
| (` ABUY ABUY`). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Avoid leading spaces inside the inline code span.
Line 54 triggers Markdownlint MD038. The leading spaces are meaningful in this example, so move the sample to a text fenced block instead of deleting them.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 54-54: Spaces inside code span elements
(MD038, no-space-in-code)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/bernese/otl/README.md` around lines 53 - 54, Update the README example
currently shown as an inline code span so the meaningful leading spaces are
preserved in a fenced text block. Remove the inline formatting for that sample
while keeping the example content unchanged and resolving Markdownlint MD038.
Source: Linters/SAST tools
| name = _station_name(ln) | ||
| if name: | ||
| if current: | ||
| blocks[current] = buf | ||
| current = name | ||
| buf = pending + [ln] |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reject duplicate station blocks during parsing.
blocks is keyed by station. When the same station appears again, the assignment to blocks[current] overwrites the earlier block. main() then reports one station and can discard a conflicting reply block before validation or apply.
Track duplicate names and fail before writing instead of keeping the last block.
Also applies to: 89-90
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 80-80: Consider [*pending, ln] instead of concatenation
Replace with [*pending, ln]
(RUF005)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/merge_blq.py` around lines 75 - 80, Update the station-block parsing
flow around _station_name and the blocks assignment to detect repeated station
names instead of overwriting the existing entry. Track duplicates during
parsing, propagate the parsing failure to main(), and abort before any output is
written or validation/apply proceeds.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/merge_blq.py`:
- Around line 173-184: Update the block-processing logic around key_idx, want,
and the six-row validation to treat both a missing key line (key_idx is None)
and a key line after offset 3 (key_idx > want) as malformed-block errors rather
than warnings; record these stations in the existing malformed errors collection
and return before backup or write. Preserve the existing insertion behavior for
key_idx < want, and add regression tests covering both unresolved-key cases.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 375864f1-8018-4806-b2bf-02a49e78b81b
📒 Files selected for processing (2)
config/bernese/otl/onsala-reply-20260806.txtscripts/merge_blq.py
| if key_idx is None: | ||
| print(f" WARNING {name}: no '$$ {name}, ...' key line found; " | ||
| f"GTOCNL will not match this station") | ||
| continue | ||
| want = name_idx + 3 | ||
| if key_idx < want: | ||
| blk = blk[:key_idx] + ["$$"] * (want - key_idx) + blk[key_idx:] | ||
| incoming[name] = blk | ||
| print(f" {name}: key line moved from position {key_idx - name_idx} to 3") | ||
| elif key_idx > want: | ||
| print(f" WARNING {name}: key line at position {key_idx - name_idx}, " | ||
| f"expected 3") |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Reject blocks with an unresolved GTOCNL key-line position.
If key_idx is None or greater than want, the script only prints a warning. The six-row validation can then pass and --apply writes the block. GTOCNL reads a different record and cannot match that station.
Add these stations to the malformed-block errors and return before backup or write. Add regression tests for a missing key line and a key line after offset 3.
Proposed fix
+ layout_errors: list[str] = []
for name in list(incoming):
...
if key_idx is None:
- print(f" WARNING {name}: no '$$ {name}, ...' key line found; "
- f"GTOCNL will not match this station")
+ layout_errors.append(f"{name}: no '$$ {name}, ...' key line found")
continue
...
elif key_idx > want:
- print(f" WARNING {name}: key line at position {key_idx - name_idx}, "
- f"expected 3")
+ layout_errors.append(
+ f"{name}: key line at position {key_idx - name_idx}, expected 3"
+ )
- bad = []
+ bad = layout_errorsAs per coding guidelines, use a TDD approach where applicable.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if key_idx is None: | |
| print(f" WARNING {name}: no '$$ {name}, ...' key line found; " | |
| f"GTOCNL will not match this station") | |
| continue | |
| want = name_idx + 3 | |
| if key_idx < want: | |
| blk = blk[:key_idx] + ["$$"] * (want - key_idx) + blk[key_idx:] | |
| incoming[name] = blk | |
| print(f" {name}: key line moved from position {key_idx - name_idx} to 3") | |
| elif key_idx > want: | |
| print(f" WARNING {name}: key line at position {key_idx - name_idx}, " | |
| f"expected 3") | |
| layout_errors: list[str] = [] | |
| for name in list(incoming): | |
| if key_idx is None: | |
| layout_errors.append(f"{name}: no '$$ {name}, ...' key line found") | |
| continue | |
| want = name_idx + 3 | |
| if key_idx < want: | |
| blk = blk[:key_idx] + ["$$"] * (want - key_idx) + blk[key_idx:] | |
| incoming[name] = blk | |
| print(f" {name}: key line moved from position {key_idx - name_idx} to 3") | |
| elif key_idx > want: | |
| layout_errors.append( | |
| f"{name}: key line at position {key_idx - name_idx}, expected 3" | |
| ) | |
| bad = layout_errors |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/merge_blq.py` around lines 173 - 184, Update the block-processing
logic around key_idx, want, and the six-row validation to treat both a missing
key line (key_idx is None) and a key line after offset 3 (key_idx > want) as
malformed-block errors rather than warnings; record these stations in the
existing malformed errors collection and return before backup or write. Preserve
the existing insertion behavior for key_idx < want, and add regression tests
covering both unresolved-key cases.
Source: Coding guidelines
Sessions finished: OK: 1, Error: 0, in 5m36s. Thirty stations in FIN_20251210.SNX, the same count as Abegail's solution, with HELMCHK and COMPARF both passing. The last blocker was a BLQ formatting subtlety that took four wrong guesses before reading the source. GTOCNL reads the station name with FORMAT(2X,A4), then re-reads with FORMAT(//,2X,A10) -- the // skips TWO records, so the ten-character key comes from the THIRD line after the name and must carry the station name at columns 4-7, the match being OLNAME == OLNUMB(2:5). Onsala emits three comment lines for interpolated sites and only two for sites needing no interpolation. ALIC came back as "Complete FES2004", one line short, so the read landed on an amplitude row and the station silently failed to match while the six interpolated fiducials were fine. merge_blq.py now positions the key line at exactly the third position, padding before it rather than after. Three further defects in that tool, each found by running it rather than by reading it: It parsed a station line as two spaces and a single token, which is the documented form, and found zero stations in a file holding 135. Real Scherneck output repeats the name, sometimes tab-separated. It then required those two tokens to be equal, which held for all 135 local stations and rejected every fiducial the moment one appeared -- locals repeat the name where IGS sites carry a DOMES. And it appended to end-of-file. Bernese stops reading at the first $$ END TABLE, so the new blocks sat past the terminator: present on inspection, invisible to the software. It also rewrote CRLF as LF, because Path.read_text uses universal newlines and had already normalised them before the check ran. Blocks are now inserted before the terminator and line endings detected from raw bytes. The coefficients themselves are FES2004 with CMC NO, matching the header of the existing 135 stations rather than the newer model first recommended. Mixing ocean models would have put the seven datum fiducials on a different basis from the network they constrain.
Adds scripts/run_luzon_month.sh — 2025 DOY 121-151 through LUZON_DLY,
idempotent, resumable, continue-on-error, one log per day.
V_RESULT moves from the stock ${S}/RNX2SNX/$Y+0 to ${S}/LUZON/$Y+0. The
stock value is shared by every RNX2SNX-derived campaign, so EXAMPLE's
output would land beside LUZON's with nothing in the filename to tell
them apart. DOY 121 already ran under the old value and will re-run
here; five minutes buys one tree holding one configuration.
DOY 139 is excluded, not skipped for convenience. Our copy of her
DATAPOOL holds one RINEX2 station for that day where every neighbour
holds twenty-five, yet F1_251390.SNX exists — so the observations were
there when she processed and our copy of that day is short. Running it
would produce a solution from the nine fiducials alone, sitting in SOL/
beside thirty proper ones and distinguishable only by opening it.
The run is under I20 and is a PIPELINE TEST. I14's satellite tables end
in 2023, AIUB no longer publishes them, and the I14 ANTEX fails 5.4's
SVN/PRN check. Coordinates from this month are NOT comparable with the
I14 series; both the script header and its final summary say so, because
the output will outlive the conversation that produced it.
Two defects, both found by dry-running the loop with the BPE call stubbed out rather than by reading it. LOADGPS.setvar exports PCF, and the script set PCF=LUZON_DLY *above* the source, so the source clobbered it and the first launch died looking for `$U/PCF/$U/PCF.PCF`. This is the fourth such collision in this campaign after $SRC, $S and $P, so config names now carry a LUZON_ prefix and an assertion across the source fails loudly if a future one is added without it. Renaming the one variable would have left the trap armed. Worse: LUZON_DLY.OUT is rewritten in place each run, and the success test grepped it without checking whose run wrote it. A day whose BPE never started would be scored against the PREVIOUS day's summary and counted OK. The stubbed dry run scored all thirty days OK from one file left by DOY 121 — a full month of green with nothing executed. The summary must now be at least as new as the day's start, and a missing or stale one is a failure carrying the exit code. With the guard the same dry run reports 30 FAILED, 1 EXCLUDED. That is the defect this campaign keeps producing: a check that reports success without having inspected anything.
Records that the two failure modes §4b.7 left open are both resolved and that DOY 121 completed with 30 stations, matching her count. Also records a finding that is about our data holdings rather than about processing: DOY 139 in our copy of her DATAPOOL holds one RINEX2 station where neighbouring days hold 25, yet F1_251390.SNX exists in her SAVEDISK. The observations were there when she processed; our copy of that day is short. Nothing has yet checked for other such days outside this 31-day window, which is the part worth acting on.
§7's claim that only DOY 121-151 of 2025 has raw observations was carried forward from the transfer handover and had never been checked. It is correct: a find across both the array and the live GPSDATA tree returns exactly those 31 days and nothing else, against 365 solved days in 2025. Records the command so a successor can re-run it rather than trust this paragraph, and notes why the boundary falls there — her DATAPOOL/LUZON is a rolling staging area holding about a month, so the transfer captured a snapshot of that window, not a chosen interval. Within the window DOY 139 is short one day's stations, so the reproducible month is 30 days rather than 31.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/run_luzon_month.sh`:
- Around line 136-140: Ensure the BPE run cannot accept a pre-existing summary
file: remove or archive the `$out` file before the BPE launch around line 123,
then update the freshness check in the surrounding run flow to require that BPE
creates a new `$out` file during this run rather than accepting an
equal-timestamp stale file.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fab173ed-b0f4-45d6-be58-e2343fb6d0ac
📒 Files selected for processing (4)
docs/bernese54_luzon_reprocessing_runbook.mddocs/gps3-sessions/SESSION_LOG_20260729_storage.mdscripts/derive_luzon_pcf.pyscripts/run_luzon_month.sh
…ndings for Abegail
Adds scripts/coord_repeatability.py — day-to-day scatter of each station about its own mean, from the FIN_*.SNX solutions a run produces. Every check this campaign has had so far asks whether the BPE finished. A BPE can finish cleanly and produce centimetre scatter, and nothing in the pipeline would have said so. Over the first eight days the run gives median N 2.9 mm, E 3.4 mm, U 7.4 mm across 31 stations, which is ordinary for daily double-difference solutions and is the first evidence that the derived PCF is not merely executable but correct. The docstring is emphatic that this is PRECISION and not accuracy: it would look just as good for a solution in the wrong reference frame, because every day would be wrong the same way. It is not evidence about I20 versus I14 and must not be quoted as such. TGDN stands out at 13/20 mm horizontal against a 3 mm median. That is the same station that is alone on DOY 139 and was dropped from DOY 121, so it reads as a station problem rather than a configuration one — if the configuration were wrong, every station would degrade together.
…ilently Station-for-station against her DOY 122: 30 of 31 agree. She has S01R and not PIMO; we have PIMO and not S01R. The counts match at 31, so a station-count check passes and the difference is invisible. PIMO is explained — it is one of the nine fiducials we stage and her set did not include it. S01R is not. It is absent from all ten of our solutions, present in hers, and its input IS staged, so this is a processing gap rather than a staging one. It appears in FIN_*.CRD without an estimation flag — the a priori value passed through, never solved — and no *** message anywhere in the BPE logs names it. The obvious cause does not survive checking: its RINEX samples at 15 s against everyone else's 30 s, but V_SAMPL is 180 and divides evenly by both. The section says explicitly not to record sampling as the explanation until someone traces the import step. Better an open question than a plausible wrong answer written down as fact. §4b.10 records repeatability — median N 2.9, E 3.4, U 7.4 mm over ten days — and is emphatic that this is precision and not accuracy, since a solution in the wrong frame would look identical. TGDN's 13/20 mm is explained by a 43%-length session, the only short one in the network. Also corrects the DOY 139 rationale: TGDN is dropped on DOY 121 only, not generally, so that day would yield ten stations rather than nine. Still degenerate, but the stated reasoning was wrong.
30 of 31 days processed, 0 failures, 2h47m wall. Repeatability over the full month: median N 2.8 mm, E 3.0 mm, U 10.9 mm. Horizontal held steady as the series lengthened from ten days to thirty, which is what a stable configuration looks like. The useful result is the shape of the bad days rather than their count. Scanning for stations more than 30 mm from their own mean: 25 days are completely clean, five have exactly one bad station, and only two stations are ever involved. A bad configuration degrades every station on every day; bad stations degrade themselves. TGDN is fully explained — its sessions run 112 to 1119 epochs against a 2880 full day, and its two worst days are its two shortest. The 43% figure recorded earlier came from one day and understated the variability. LGYE is NOT explained and is flagged as the open item: full 2880 epochs on every one of its bad days, yet a 200 mm excursion on DOY 137. Session length is ruled out. Recorded as open rather than attributed to a guess.
…lying Adds §19.6 (results) and §19.7 (measured capacity) to the session log. Also rewrites the runbook's status header, which still read "Status: not yet run" after the run had completed. Anyone opening that document would have taken its opening line as current. It now states plainly that the run succeeded, that it was under I20 and therefore does NOT achieve the document's own stated goal of reproducing her numbers, and that only 31 days of 2025 can be reprocessed at all. Saying so at the top matters more than saying it in §4b.6, because the person most likely to be misled is the one who reads the first screen and stops.
…iers This project exists to detect earthquakes. §4b.10's quality check scanned for any single station exceeding 30 mm from its own mean and concluded "no day is bad network-wide" -- the wrong test for a seismic signal, which is several NEARBY stations moving TOGETHER, often below what would flag any one of them alone. scripts/network_coherence_scan.py checks pairs of stations within 120 km both exceeding 8 mm horizontal in the same direction. It found what the single-station scan missed: DOY 126 moved 14 stations 8-30 mm together across the whole southern-to-central Luzon cluster, invisible before because no single station individually cleared 30 mm by much. DOY 129 and 145 show smaller versions of the same pattern. Distinguishing real from artifact: a coseismic offset is a permanent step; a processing artifact specific to one day's products is a spike that reverts. DOY 126 reverts completely by DOY 127 -- a spike, not a step. Corroborated against the PHIVOLCS/USGS catalog: no earthquake recorded on any of the three flagged dates. The confirmed M4.6 near Quezon on DOY 147 produces no anomaly at the nearest stations either -- a useful negative control, since M4.6 at tens of km is below what daily static GNSS resolves, and the scan correctly stays quiet rather than manufacturing a signal from noise. The technical cause of the spikes is not identified. Orbit file size and the fiducial-fixing list were both checked and ruled out. Left open rather than assigned a plausible guess. Also closes the LGYE question raised in §4b.10 as a possible earthquake: its nearest neighbour (BLN2, 51 km) stays within 3 mm on both of LGYE's worst days, ruling out a real coseismic origin for that excursion. It remains open as a station or processing problem.
Investigation of why the LUZON reprocessing cannot run under I14 on Bernese
5.4, answered from Abegail's own retained processing log. Docs plus the
derivation script's override reasoning.
Her run tolerated what 5.4 rejects
SAVEDISK/2025/OUT/R2S251210.PRC— 56,781 lines, 26,172 warnings and 3errors — and it produced
F1_251210.SNXregardless.In Bernese
###is a warning and***an error. She had all three***andthe run still finished. The version difference is one of tolerance, not
capability.
The
RCVOBSwarnings also confirm her data was multi-GNSS — Galileo andGLONASS PRNs on a TRIMBLE ALLOY — so
V_SATSYS=GPSskipped those observationswith a warning rather than them being absent.
The I14 model set is retired
Two independent blockers, neither a misconfiguration:
Satellite tables end in 2023. Hers 2023-01-31; 5.4's
SATELLIT_I14.SAT2023-08-10;
SATELLIT_I20.SAT2024-09-17. AIUB no longer publishes the I14table —
BSWUSER54/CONFIG/returns 404 for I14 and 200 for I20. Against 2025data, PRN→SVN resolution lands on stale entries and CODSPP stops on
BLOCK IIR-A 044, a satellite that is present in the antenna file.The I14 ANTEX fails 5.4's consistency validation.
All three variants in her tree fail it.
I14.ATXcarries fourG041entrieswith different PRN mappings across epochs;
I14-orig.ATXandI14_1.ATXcarryone each and fail identically. A file 5.2 consumed without complaint is invalid
to 5.4.
Consequently
V_MYATXis left blank — setting it fails at PID 002 whereblank reaches PID 232. The reasoning is recorded at the override rather than
left as a bare default.
Recommendation
Run I20 first, explicitly as a pipeline test and not as a comparison. It
establishes that the chain executes end to end, which is BRN-001 acceptance
evidence in its own right. The I14 comparison then becomes a separate question.
The finding that I14 cannot be run on 5.4 at this epoch is itself a result
worth putting to Abegail — it bears on how the LUZON series can be continued,
since the current workflow depends on a model set that is no longer maintained.
Also here
V_SATSYSwas the override originally missed.V_GNSSARselects which of thealready-selected systems get ambiguity resolution;
V_SATSYSselects thesystems, and hers reads
GPSwhere 5.4 shipsGRE. Leaving it at the defaultmade the run attempt GLONASS and die on GLONASS-M 861 — post-I14 and absent from
every I14 table. That presented as a missing file and was a constellation
selection error; "fixing" it by switching to I20 would have introduced the exact
frame confound the exercise exists to avoid, with plausible-looking numbers.
Summary by CodeRabbit
New Features
Documentation