From 025472820b34a266a7ff6a82b7f73d9114cfb1ea Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Wed, 12 Aug 2026 07:00:59 +0200 Subject: [PATCH] fix(bandopen): the moon is not a band opening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 2 m opening was announced at 9650 km towards Japan. The callsigns gave it away — 7M4RRM, JA7RPC, JF1AWC — all working EME, which is routine on 2 m and reported to PSK Reporter like anything else. Real contacts, real grids, and no evidence whatsoever about the band: an operator turning a beam on that bearing would hear nothing. Removing the flat 2400 km ceiling was right; it threw away genuine multi-hop Es on 6 m. "No limit anywhere" was the overcorrection. The limit is per band now, and it is physics rather than a threshold: 2 m reaches a few thousand kilometres by tropospheric duct or a chain of Es clouds and no further, so 3500 km, and 4 m 4000. Six and ten metres keep no ceiling — multi-hop Es and F2 genuinely do go round the world, which is the case that started all this. Pinned from both sides: the 9650 km Japanese burst produces nothing, and a 2000 km 2 m burst in one sector still counts. --- changelog.json | 6 +++-- internal/bandopen/bandopen.go | 28 +++++++++++++++++++ internal/bandopen/eme_test.go | 51 +++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 internal/bandopen/eme_test.go diff --git a/changelog.json b/changelog.json index ef9ca92..05946de 100644 --- a/changelog.json +++ b/changelog.json @@ -3,10 +3,12 @@ "version": "0.24.7", "date": "", "en": [ - "QSOs logged from WSJT-X now carry the space weather and the distance, like hand-logged ones. The UDP path stamped the station profile, the DXCC and the QSL defaults but not SFI, A, K or distance — so an operator running digital had those fields empty across the whole log. Space weather is only stamped on a contact less than a day old: a logger re-broadcasting its backlog would otherwise be handed this morning readings for last month contacts." + "QSOs logged from WSJT-X now carry the space weather and the distance, like hand-logged ones. The UDP path stamped the station profile, the DXCC and the QSL defaults but not SFI, A, K or distance — so an operator running digital had those fields empty across the whole log. Space weather is only stamped on a contact less than a day old: a logger re-broadcasting its backlog would otherwise be handed this morning readings for last month contacts.", + "Band openings: EME contacts are no longer mistaken for an opening. A 2 m opening was announced at 9650 km towards Japan on stations working moonbounce — real contacts, but the moon says nothing about the band, and an antenna pointed that way finds nothing. Each band now has the longest path the atmosphere can actually carry: 3500 km on 2 m, 4000 on 4 m, and no limit at all on 6 and 10 m where multi-hop really does go round the world." ], "fr": [ - "Les QSO enregistrés depuis WSJT-X portent désormais la météo spatiale et la distance, comme ceux saisis à la main. Le chemin UDP posait le profil station, le DXCC et les défauts QSL mais ni SFI, ni A, ni K, ni distance — un opérateur en numérique avait donc ces champs vides sur tout son log. La météo spatiale n est posée que sur un contact de moins d un jour : sinon un logiciel qui rediffuse son historique se verrait attribuer les relevés de ce matin sur des contacts du mois dernier." + "Les QSO enregistrés depuis WSJT-X portent désormais la météo spatiale et la distance, comme ceux saisis à la main. Le chemin UDP posait le profil station, le DXCC et les défauts QSL mais ni SFI, ni A, ni K, ni distance — un opérateur en numérique avait donc ces champs vides sur tout son log. La météo spatiale n est posée que sur un contact de moins d un jour : sinon un logiciel qui rediffuse son historique se verrait attribuer les relevés de ce matin sur des contacts du mois dernier.", + "Ouvertures de bande : les contacts EME ne sont plus pris pour une ouverture. Une ouverture 2 m était annoncée à 9650 km vers le Japon sur des stations en rebond lunaire — de vrais contacts, mais la Lune ne dit rien de la bande, et une antenne pointée par là ne trouve rien. Chaque bande a désormais la distance maximale que l atmosphère peut réellement porter : 3500 km en 2 m, 4000 en 4 m, et aucune limite en 6 et 10 m où les sauts multiples font vraiment le tour du monde." ] }, { diff --git a/internal/bandopen/bandopen.go b/internal/bandopen/bandopen.go index 2a77f95..d25b69a 100644 --- a/internal/bandopen/bandopen.go +++ b/internal/bandopen/bandopen.go @@ -81,6 +81,28 @@ var watched = map[string]bool{"10m": true, "6m": true, "4m": true, "2m": true} // Watched reports whether a band is one the detector looks at. func Watched(band string) bool { return watched[strings.ToLower(strings.TrimSpace(band))] } +// maxTerrestrialKm is the longest path a band can carry through the atmosphere. +// Zero means no limit. +// +// The flat 2400 km ceiling was removed because it threw away real multi-hop Es +// on 6 m, and that was right — but "no limit anywhere" then let something else +// through. A 2 m opening was announced at 9650 km towards Japan, on stations +// that were unmistakably working EME: the moon is not an opening, and pointing +// an antenna at that bearing would find nothing. +// +// So the limit is per band, and it is physics rather than a threshold. Two +// metres reaches a few thousand kilometres by tropospheric duct or a chain of Es +// clouds and no further; beyond that the path went via the moon or a satellite, +// neither of which says anything about the band. Six and ten metres have no +// ceiling at all — multi-hop Es and F2 genuinely go round the world. +var maxTerrestrialKm = map[string]int{ + "2m": 3500, + "4m": 4000, +} + +// MaxKmFor returns the plausibility ceiling for a band, 0 for none. +func MaxKmFor(band string) int { return maxTerrestrialKm[strings.ToLower(strings.TrimSpace(band))] } + // Opening is a detected opening, ready to be announced. type Opening struct { Band string `json:"band"` @@ -130,6 +152,12 @@ func (d *Detector) Add(s Spot, lat float64) *Opening { if s.DistKm < d.cfg.MinKm || (d.cfg.MaxKm > 0 && s.DistKm > d.cfg.MaxKm) { return nil } + // Past what the atmosphere can carry on this band, the path went via the moon + // or a satellite. Those are real contacts and real reports; they are simply + // not evidence about the band. + if m := MaxKmFor(band); m > 0 && s.DistKm > m { + return nil + } d.recent = append(d.recent, s) d.prune(s.At) diff --git a/internal/bandopen/eme_test.go b/internal/bandopen/eme_test.go new file mode 100644 index 0000000..fc739bc --- /dev/null +++ b/internal/bandopen/eme_test.go @@ -0,0 +1,51 @@ +package bandopen + +import ( + "testing" + "time" +) + +// A 2 m "opening" was announced at 9650 km towards Japan on stations that were +// plainly working EME. The moon is not an opening: an operator pointing an +// antenna at that bearing finds nothing. +func TestEMEIsNotAnOpeningOnTwoMetres(t *testing.T) { + d := New(DefaultConfig()) + base := time.Date(2026, 8, 12, 6, 0, 0, 0, time.UTC) + for i, call := range []string{"7M4RRM", "JA7RPC", "JF1AWC", "JK1TPA", "JH1JCQ"} { + if op := d.Add(Spot{ + Call: call, Band: "2m", DistKm: 9650, Bearing: 40 + i*3, + At: base.Add(time.Duration(i) * time.Minute), + }, 47.0); op != nil { + t.Fatalf("a 9650 km 2 m path was announced as an opening: %+v", op) + } + } +} + +// But a real 2 m opening — an Es chain at a plausible distance — must survive. +func TestLongButPlausibleTwoMetresStillCounts(t *testing.T) { + d := New(DefaultConfig()) + base := time.Date(2026, 6, 20, 18, 0, 0, 0, time.UTC) + var got *Opening + for i, call := range []string{"EA1AA", "CT1BB", "EA7CC", "CT7DD"} { + if op := d.Add(Spot{ + Call: call, Band: "2m", DistKm: 2000 + i*30, Bearing: 200 + i*4, + At: base.Add(time.Duration(i) * time.Minute), + }, 47.0); op != nil { + got = op + } + } + if got == nil { + t.Fatal("a 2000 km 2 m burst in one sector was not reported") + } +} + +// Six metres keeps no ceiling: multi-hop Es genuinely goes that far, which is +// why the flat limit was removed in the first place. +func TestSixMetresHasNoCeiling(t *testing.T) { + if MaxKmFor("6m") != 0 || MaxKmFor("10m") != 0 { + t.Error("6 m and 10 m must have no distance ceiling") + } + if MaxKmFor("2m") == 0 { + t.Error("2 m must have one") + } +}