241 lines
7.7 KiB
Go
241 lines
7.7 KiB
Go
package uplift
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
|
|
"salty/terrain/internal/plates"
|
|
"salty/terrain/internal/world"
|
|
)
|
|
|
|
// A straight north-south margin down the middle of a planet, closing head-on. Everything a belt fault is
|
|
// supposed to do is measurable against a line whose direction is known: the traces should run along it, sit
|
|
// near it, and face away from it.
|
|
func straightMargin(t *testing.T, p world.Planet) []plates.Boundary {
|
|
t.Helper()
|
|
const n = 200
|
|
xM := p.CircumferenceM() / 2
|
|
v := make([]plates.Vertex, n)
|
|
for i := range v {
|
|
v[i] = plates.Vertex{
|
|
XM: xM,
|
|
YM: p.HeightM() * float64(i) / float64(n-1),
|
|
NX: 1, // the margin runs north-south, so its normal points east
|
|
NY: 0,
|
|
ClosingMYr: 0.04,
|
|
Kind: plates.Collision,
|
|
Over: -1,
|
|
}
|
|
}
|
|
return []plates.Boundary{{A: 0, B: 1, V: v}}
|
|
}
|
|
|
|
func beltPlanet(t *testing.T) world.Planet {
|
|
t.Helper()
|
|
p, err := world.New(40000, 8, 100, 50, 0, 40000)
|
|
if err != nil {
|
|
t.Fatalf("planet: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func testBelt() plates.Belt {
|
|
b := plates.DefaultBelt()
|
|
b.ZoneKm = 3
|
|
b.Per1000Km2 = 400
|
|
none := 0.0
|
|
b.ConjugateFraction = &none // measured separately; the main set has to be parallel on its own
|
|
return b
|
|
}
|
|
|
|
func allLand(xM, yM float64) bool { return true }
|
|
|
|
func TestBeltFaultsRunAlongTheMargin(t *testing.T) {
|
|
p := beltPlanet(t)
|
|
fs := BuildBeltFaults(p, 7, testBelt(), straightMargin(t, p), allLand)
|
|
if len(fs) < 20 {
|
|
t.Fatalf("%d traces; not enough to measure anything", len(fs))
|
|
}
|
|
|
|
// The margin runs north-south, so every trace should too. Measured as the angle between the trace's own
|
|
// end-to-end direction and the line, folded into 0..90 because a fault has no head or tail.
|
|
worst, total := 0.0, 0.0
|
|
for _, f := range fs {
|
|
a, b := f.PointsM[0], f.PointsM[len(f.PointsM)-1]
|
|
deg := foldedAngleDeg(math.Atan2(b[1]-a[1], b[0]-a[0]), math.Pi/2)
|
|
total += deg
|
|
if deg > worst {
|
|
worst = deg
|
|
}
|
|
}
|
|
mean := total / float64(len(fs))
|
|
// The configured spread is 11 degrees, and the walk wanders on top of it. A mean much above that would
|
|
// mean the strike is not coming from the boundary at all, which is the defect this whole file exists for.
|
|
if mean > 20 {
|
|
t.Errorf("traces average %.1f degrees off the margin; they are not following it", mean)
|
|
}
|
|
if worst > 55 {
|
|
t.Errorf("a trace is %.1f degrees off the margin; nothing should be near perpendicular to it", worst)
|
|
}
|
|
}
|
|
|
|
// foldedAngleDeg is the angle between two directions, in degrees, folded into 0..90: a line at 170 degrees
|
|
// and one at 10 are twenty degrees apart, not a hundred and sixty.
|
|
func foldedAngleDeg(a, b float64) float64 {
|
|
d := math.Abs(a-b) * 180 / math.Pi
|
|
d = math.Mod(d, 180)
|
|
if d > 90 {
|
|
d = 180 - d
|
|
}
|
|
return d
|
|
}
|
|
|
|
func TestBeltFaultsStayInTheDeformationZone(t *testing.T) {
|
|
p := beltPlanet(t)
|
|
cfg := testBelt()
|
|
fs := BuildBeltFaults(p, 7, cfg, straightMargin(t, p), allLand)
|
|
if len(fs) == 0 {
|
|
t.Fatal("no traces")
|
|
}
|
|
|
|
xM := p.CircumferenceM() / 2
|
|
// The zone half-width here is the configured width times the collision multiplier times the rate scale.
|
|
// A *fault's* centre is placed inside it, but a trace's need not be: a fault over twelve kilometres is
|
|
// broken into en-echelon segments staggered up to 0.06 of its length across strike, which is the whole
|
|
// point of the stepping. So the bound on a segment centre is the zone plus that stagger, and the bound on
|
|
// any point of it is a further half-length beyond that.
|
|
half := cfg.ZoneKm * 1000 * beltWidth[plates.Collision] * math.Sqrt(0.04/(cfg.ReferenceCmYr/100))
|
|
longest := cfg.LengthKm[1] * 1000 * 2.2
|
|
centreBound := half + 0.06*longest
|
|
anyBound := centreBound + longest
|
|
|
|
far := 0
|
|
inZone := 0
|
|
for _, f := range fs {
|
|
mid := f.PointsM[len(f.PointsM)/2]
|
|
d := math.Abs(mid[0] - xM)
|
|
if d > centreBound {
|
|
far++
|
|
}
|
|
if d <= half {
|
|
inZone++
|
|
}
|
|
for _, pt := range f.PointsM {
|
|
if math.Abs(pt[0]-xM) > anyBound {
|
|
t.Fatalf("a trace reaches %.0f m from the margin; the zone, the stagger and a trace is %.0f m",
|
|
math.Abs(pt[0]-xM), anyBound)
|
|
}
|
|
}
|
|
}
|
|
if far > 0 {
|
|
t.Errorf("%d of %d trace centres sit outside the deformation zone and its en-echelon stagger",
|
|
far, len(fs))
|
|
}
|
|
// And they should be *concentrated* near the line rather than spread evenly across the zone: that is what
|
|
// beltFalloff is for, and what the reference map shows.
|
|
near := 0
|
|
for _, f := range fs {
|
|
if math.Abs(f.PointsM[len(f.PointsM)/2][0]-xM) < half/2 {
|
|
near++
|
|
}
|
|
}
|
|
if float64(near)/float64(inZone) < 0.55 {
|
|
t.Errorf("only %d of %d traces are in the inner half of the zone; the falloff is not biting",
|
|
near, inZone)
|
|
}
|
|
}
|
|
|
|
func TestBeltFaultsVergeAwayFromTheMargin(t *testing.T) {
|
|
p := beltPlanet(t)
|
|
cfg := testBelt()
|
|
// Short faults only. Vergence is decided per *fault*, from which side of the line it was placed on, and
|
|
// then every en-echelon segment of it inherits that - correctly, since the segments are one fault. Keeping
|
|
// every fault under enEchelonM means one trace per placement, so the side a trace sits on and the side it
|
|
// was placed on are the same thing and the property can be measured at all.
|
|
cfg.LengthKm = [2]float64{2, 4}
|
|
fs := BuildBeltFaults(p, 7, cfg, straightMargin(t, p), allLand)
|
|
if len(fs) < 20 {
|
|
t.Fatalf("%d traces; not enough to measure anything", len(fs))
|
|
}
|
|
|
|
xM := p.CircumferenceM() / 2
|
|
wrong := 0
|
|
for _, f := range fs {
|
|
mid := f.PointsM[len(f.PointsM)/2]
|
|
// A doubly-vergent belt faces outwards on both flanks, so the two sides must disagree about which
|
|
// block goes up. Which flank got which sign does not matter; that they are consistent within a flank
|
|
// does, because the alternative is the coin flip a class fault set has to use.
|
|
if (mid[0] > xM) != f.Reverse {
|
|
wrong++
|
|
}
|
|
}
|
|
if wrong != 0 && wrong != len(fs) {
|
|
t.Errorf("%d of %d traces disagree with their own flank about vergence; a belt is doubly vergent, "+
|
|
"not randomly vergent", min(wrong, len(fs)-wrong), len(fs))
|
|
}
|
|
}
|
|
|
|
func TestBeltFaultsNeedLand(t *testing.T) {
|
|
p := beltPlanet(t)
|
|
m := straightMargin(t, p)
|
|
if got := BuildBeltFaults(p, 7, testBelt(), m, func(xM, yM float64) bool { return false }); len(got) != 0 {
|
|
t.Errorf("%d traces on a planet with no land", len(got))
|
|
}
|
|
// A coast down one side of the margin: every trace must be mostly on the land side.
|
|
xM := p.CircumferenceM() / 2
|
|
half := func(x, y float64) bool { return x < xM }
|
|
fs := BuildBeltFaults(p, 7, testBelt(), m, half)
|
|
if len(fs) == 0 {
|
|
t.Fatal("no traces on a half-land planet")
|
|
}
|
|
for _, f := range fs {
|
|
on := 0
|
|
for _, pt := range f.PointsM {
|
|
if half(pt[0], pt[1]) {
|
|
on++
|
|
}
|
|
}
|
|
if share := float64(on) / float64(len(f.PointsM)); share < 0.3 {
|
|
t.Errorf("a trace is only %.0f%% on land; the span test should have refused it", share*100)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestABeltWithNoNumbersAsksForNothing(t *testing.T) {
|
|
p := beltPlanet(t)
|
|
if got := BuildBeltFaults(p, 7, plates.Belt{}, straightMargin(t, p), allLand); got != nil {
|
|
t.Errorf("%d traces from an empty config; leaving the block out must leave the feature off", len(got))
|
|
}
|
|
}
|
|
|
|
func TestAFasterMarginDeformsAWiderBelt(t *testing.T) {
|
|
p := beltPlanet(t)
|
|
cfg := testBelt()
|
|
|
|
spread := func(closing float64) float64 {
|
|
bs := straightMargin(t, p)
|
|
for i := range bs[0].V {
|
|
bs[0].V[i].ClosingMYr = closing
|
|
}
|
|
fs := BuildBeltFaults(p, 7, cfg, bs, allLand)
|
|
if len(fs) == 0 {
|
|
t.Fatalf("no traces at %.3g m/yr", closing)
|
|
}
|
|
xM := p.CircumferenceM() / 2
|
|
worst := 0.0
|
|
for _, f := range fs {
|
|
mid := f.PointsM[len(f.PointsM)/2]
|
|
if d := math.Abs(mid[0] - xM); d > worst {
|
|
worst = d
|
|
}
|
|
}
|
|
return worst
|
|
}
|
|
|
|
slow, fast := spread(0.01), spread(0.08)
|
|
if fast <= slow*1.4 {
|
|
t.Errorf("a margin closing eight times faster deforms a belt %.0f m wide against %.0f m; the zone is "+
|
|
"not scaling with the rate", fast, slow)
|
|
}
|
|
}
|