Tooling
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user