package uplift import ( "math" "testing" "salty/terrain/internal/manifest" "salty/terrain/internal/world" ) // The defect D-62 caused and D-63 answers: a sub-parallel fault set stacked. // // Widening a fault's reach from 600 m to 6 km made overlap the ordinary case rather than a rarity, and a // fault set is sub-parallel by construction - traces within one cell of the orientation grain share a // strike. On Bake_018's region 11, 75 % of the faulted ground had two or more faults on it and the sum was // a median 1.77 and up to 4.46 times the largest single contribution, which took the landmass from 75 m to // 221 m and fired the repose clamp on 4.1 % of it. // // Five parallel traces two kilometres apart is that in miniature: at a 6 km footwall every one of them // reaches every other, so an unbounded sum would be several throws deep. func TestParallelFaultsDoNotStack(t *testing.T) { const w, h, cellM = 512, 1600, 16.0 const throw, runYears = 300.0, 1.5e6 p := testPlanet(t, w, h, cellM) f := world.Whole(p) trace := func(yM float64) FaultTrace { pts := make([][2]float64, 17) for i := range pts { pts[i] = [2]float64{float64(i) * float64(w) * cellM / 16, yM} } return FaultTrace{PointsM: pts, ThrowM: throw, LengthM: float64(w) * cellM} } midM := float64(h) * cellM / 2 var many []FaultTrace for i := -2; i <= 2; i++ { many = append(many, trace(midM+float64(i)*2000)) } one := []FaultTrace{trace(midM)} peakOf := func(faults []FaultTrace) (hi, lo float64) { d := FaultDelta(f, faults, runYears) if d == nil { t.Fatal("the traces reached nothing") } col := w / 2 for y := 0; y < h; y++ { v := float64(d[y*w+col]) * runYears hi = math.Max(hi, v) lo = math.Min(lo, v) } return hi, lo } hi1, lo1 := peakOf(one) hiN, loN := peakOf(many) // A belt still stands higher than one fault does, which is the point of a belt. if hiN <= hi1 { t.Errorf("five faults build %.0f m against one fault's %.0f m; the stack is suppressed entirely", hiN, hi1) } // But not five times higher. The asymptote is 1+faultStackBonus times the largest single contribution // at a cell, and the crest of the stack sits near the crest of its strongest member. limit := (1 + faultStackBonus) * 1.05 if hiN > hi1*limit { t.Errorf("five parallel faults build %.0f m against one fault's %.0f m, a factor of %.2f; the "+ "bound is %.2f", hiN, hi1, hiN/hi1, limit) } // The hanging walls are bounded on the same terms, or a fault set digs a hole instead of building one. if loN < lo1*limit { t.Errorf("five parallel faults drop %.0f m against one fault's %.0f m, a factor of %.2f", loN, lo1, loN/lo1) } t.Logf("one fault %+.0f/%+.0f m, five at 2 km spacing %+.0f/%+.0f m (x%.2f)", hi1, lo1, hiN, loN, hiN/hi1) // And it is still solvable ground: bending the stack must not put a step back into the rate field. d := FaultDelta(f, many, runYears) var steepest float64 col := w / 2 for y := 1; y < h; y++ { g := math.Abs(float64(d[y*w+col]-d[(y-1)*w+col])) * runYears / cellM steepest = math.Max(steepest, g) } if deg := math.Atan(steepest) * 180 / math.Pi; deg > 25 { t.Errorf("the steepest cell-to-cell step across the stack is %.1f degrees", deg) } } // One fault is untouched by the bound, so D-62's calibration still holds: the step across a fault is the // throw its author asked for. softStack is the identity below its knee and the knee is the largest single // contribution, so this is the property that makes the two changes compose rather than fight. func TestOneFaultIsNotBentByTheStackBound(t *testing.T) { const w, h, cellM = 512, 1600, 16.0 const throw, runYears = 300.0, 1.5e6 p := testPlanet(t, w, h, cellM) f := world.Whole(p) midM := float64(h) * cellM / 2 pts := make([][2]float64, 17) for i := range pts { pts[i] = [2]float64{float64(i) * float64(w) * cellM / 16, midM} } d := FaultDelta(f, []FaultTrace{{PointsM: pts, ThrowM: throw, LengthM: float64(w) * cellM}}, runYears) if d == nil { t.Fatal("the trace reached nothing") } col := w / 2 var hi, lo float64 for y := 0; y < h; y++ { v := float64(d[y*w+col]) * runYears hi = math.Max(hi, v) lo = math.Min(lo, v) } if step := hi - lo; math.Abs(step-throw) > throw/20 { t.Errorf("the step across a lone fault is %.0f m against a throw of %.0f m; the stack bound is "+ "biting on a single fault", step, throw) } } // The initial relief is scaled by the rate *before* the faults (D-63). // // It used to be scaled by the finished rate with the fault delta in it and no upper bound, so D-62's six // kilometre footwalls raised the stamped noise from about 39 m to about 166 m on a landmass whose whole // relief was 221 m - and a thousand steps cannot erase initial relief the size of the landscape, so the // ridged fBm stopped breaking the symmetry and became the flank texture instead. The initial relief exists // to give the solve something to bite on; how much noise sits on a hillside is not a fault's decision. func TestTheInitialReliefIgnoresFaults(t *testing.T) { const w, h, cellM = 256, 256, 64.0 p := testPlanet(t, w, h, cellM) f := world.Whole(p) class := make([]uint8, w*h) land := make([]bool, w*h) for y := 0; y < h; y++ { for x := 0; x < w; x++ { if x >= 16 { class[y*w+x], land[y*w+x] = 1, true } } } midM := float64(h) * cellM / 2 pts := make([][2]float64, 17) for i := range pts { pts[i] = [2]float64{float64(i) * float64(w) * cellM / 16, midM} } faults := []FaultTrace{{PointsM: pts, ThrowM: 400, LengthM: float64(w) * cellM}} m := manifest.Defaults() m.Source.Seed = 7 build := func(fs []FaultTrace) *Result { return FromTemplate(Paint{ Frame: f, Class: class, Land: land, Rates: []float32{0, 0.00025}, // 0.25 mm/yr, the shipped highland Ks: []float32{1, 1}, Faults: fs, RunYears: 1.5e6, Variation: 0, // The repose bound off, so the fault delta survives into the rate and the difference this // test is looking for is actually there to find. ClampCeilM: 0, }, m) } with, without := build(faults), build(nil) differs := 0 for i := range with.Rate.Data { if with.Rate.Data[i] != without.Rate.Data[i] { differs++ } } if differs == 0 { t.Fatal("the fault changed no rate at all; this test measured nothing") } for i := range with.Height.Data { if with.Height.Data[i] != without.Height.Data[i] { t.Fatalf("the initial relief at cell %d is %.3f m with faults and %.3f m without, over %d "+ "cells whose rate the fault changed; the amplitude is still reading the finished rate", i, with.Height.Data[i], without.Height.Data[i], differs) } } t.Logf("the fault moved %d of %d rates and no initial relief at all", differs, len(with.Rate.Data)) }