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