fix(bandopen): find the busiest sector, and name it correctly
Two faults, both found from one field log: 70 000 decodes, 10 and 12 m plainly open, nothing announced — and the two openings that DID fire named the wrong direction. evaluate() required EVERY distinct station in the window to fit inside one 90° arc. That is the shape of a sporadic-E cloud and of nothing else. With 10 and 12 m open on F2 the reports arrive from all round the compass, the arc is 360°, and the test can never pass — so the busier the band, the less likely an opening was announced. Exactly backwards. It now finds the DENSEST sector instead, which keeps the Es signature intact (a cloud still makes one direction dense) and lets a real F2 opening be seen through the handful of neighbours who are always there. Scattered-but-busy still reports nothing: the point was to stop demanding global agreement, not to call every open band an opening. Sector() averaged the two bearings arithmetically, so an arc crossing north was labelled by its opposite: 353–61° averaged to 207° and went out as SW when it was NE. Not a vague error — a reversed one, given to an operator who may turn a beam on it. The detector has handled the 0/360 wrap since it was written; only this label had not.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package bandopen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -225,3 +226,85 @@ func TestGroundWaveIsStillIgnored(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A sector crossing north must not be labelled by its opposite.
|
||||
//
|
||||
// Sector() averaged the two bearings arithmetically, so 353–61° — plainly north
|
||||
// — came out as (353+61)/2 = 207°, announced as SW. The worst possible error:
|
||||
// not vague, but exactly reversed, to an operator who may turn a beam on it.
|
||||
func TestSectorAcrossNorth(t *testing.T) {
|
||||
cases := []struct {
|
||||
min, max int
|
||||
want string
|
||||
}{
|
||||
{353, 61, "NE"}, // the one seen in the field
|
||||
{303, 11, "NW"}, // the other one
|
||||
{35, 75, "NE"}, // no wrap, unchanged
|
||||
{170, 190, "S"}, // due south, no wrap
|
||||
{340, 20, "N"}, // straddling north evenly
|
||||
{260, 300, "W"}, // due west
|
||||
}
|
||||
for _, c := range cases {
|
||||
o := Opening{BearingMin: c.min, BearingMax: c.max}
|
||||
got := o.Sector()
|
||||
if !strings.HasPrefix(got, c.want+" ") {
|
||||
t.Errorf("Sector(%d–%d°) = %q, want it to start with %q", c.min, c.max, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A band open in one direction must be found THROUGH the everyday noise.
|
||||
//
|
||||
// The detector used to demand that every station fit inside one 90° arc. On 10
|
||||
// and 12 m with F2 open, reports arrive from all round the compass, the arc is
|
||||
// 360°, and the test could never pass: the busier the band, the less likely an
|
||||
// opening was announced. Reported from the field — 70 000 decodes, 10 and 12 m
|
||||
// plainly open, nothing said.
|
||||
func TestOpeningFoundThroughAllRoundNoise(t *testing.T) {
|
||||
d := New(DefaultConfig())
|
||||
base := time.Date(2026, 8, 11, 14, 0, 0, 0, time.UTC)
|
||||
// Four stations concentrated to the south-west — the opening.
|
||||
south := []struct {
|
||||
call string
|
||||
deg int
|
||||
}{{"PY2AA", 220}, {"LU3BB", 228}, {"PY5CC", 235}, {"CX4DD", 240}}
|
||||
// And the usual Europeans scattered everywhere, which is what used to veto it.
|
||||
noise := []struct {
|
||||
call string
|
||||
deg int
|
||||
}{{"DL1XX", 40}, {"SM2YY", 20}, {"EA3ZZ", 190}, {"UA9QQ", 70}, {"G4WW", 320}}
|
||||
|
||||
var got *Opening
|
||||
for i, s := range noise {
|
||||
d.Add(Spot{Call: s.call, Band: "10m", DistKm: 1200, Bearing: s.deg,
|
||||
At: base.Add(time.Duration(i) * time.Second)}, 47.0)
|
||||
}
|
||||
for i, s := range south {
|
||||
if op := d.Add(Spot{Call: s.call, Band: "10m", DistKm: 9800, Bearing: s.deg,
|
||||
At: base.Add(time.Duration(10+i) * time.Second)}, 47.0); op != nil {
|
||||
got = op
|
||||
}
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("an opening concentrated to the SW was not found among stations all round the compass")
|
||||
}
|
||||
mid := midBearing(got.BearingMin, got.BearingMax)
|
||||
if mid < 200 || mid > 260 {
|
||||
t.Errorf("sector middle %.0f° — the reported sector is not the busy one (%d–%d°)",
|
||||
mid, got.BearingMin, got.BearingMax)
|
||||
}
|
||||
}
|
||||
|
||||
// Stations evenly all round the compass and nothing concentrated: still nothing.
|
||||
// The point of the change was to stop requiring global agreement, not to start
|
||||
// calling every busy band an opening.
|
||||
func TestScatteredBandIsNotAnOpening(t *testing.T) {
|
||||
d := New(DefaultConfig())
|
||||
base := time.Date(2026, 8, 11, 14, 0, 0, 0, time.UTC)
|
||||
for i, deg := range []int{0, 60, 120, 180, 240, 300} {
|
||||
if op := d.Add(Spot{Call: string(rune('A'+i)) + "1AAA", Band: "10m",
|
||||
DistKm: 3000, Bearing: deg, At: base.Add(time.Duration(i) * time.Second)}, 47.0); op != nil {
|
||||
t.Fatalf("evenly scattered stations were called an opening: %+v", op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user