Tooling
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
package plates
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A painted layer and its legend, built by hand: two plates split at a quarter and three quarters of the way
|
||||
// round, driven into each other along X with no spin.
|
||||
func paintedStripes(w, h int) ([]uint8, *PaintLegend) {
|
||||
lg := &PaintLegend{
|
||||
WarnDistance: 60,
|
||||
Plates: []PaintPlate{
|
||||
{Name: "west", RGB: [3]int{200, 60, 60}, SpeedCmYr: 2, HeadingDeg: 90}, // due east
|
||||
{Name: "east", RGB: [3]int{60, 60, 200}, SpeedCmYr: 2, HeadingDeg: 270}, // due west
|
||||
},
|
||||
}
|
||||
px := make([]uint8, w*h*3)
|
||||
for y := range h {
|
||||
for x := range w {
|
||||
id := 0
|
||||
if x >= w/4 && x < 3*w/4 {
|
||||
id = 1
|
||||
}
|
||||
o := (y*w + x) * 3
|
||||
c := lg.Plates[id].RGB
|
||||
px[o], px[o+1], px[o+2] = uint8(c[0]), uint8(c[1]), uint8(c[2])
|
||||
}
|
||||
}
|
||||
return px, lg
|
||||
}
|
||||
|
||||
func TestAPaintedLayerBecomesAPlanet(t *testing.T) {
|
||||
p := testPlanet(t)
|
||||
px, lg := paintedStripes(400, 200)
|
||||
|
||||
m, match, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt)
|
||||
if err != nil {
|
||||
t.Fatalf("from painting: %v", err)
|
||||
}
|
||||
if match.Far != 0 {
|
||||
t.Errorf("%d of %d sampled cells did not match a plate colour", match.Far, match.Cells)
|
||||
}
|
||||
if len(m.Plates) != 2 {
|
||||
t.Fatalf("%d plates from a two-colour legend", len(m.Plates))
|
||||
}
|
||||
if len(m.Boundaries) != 2 {
|
||||
t.Fatalf("%d boundaries; two stripes on a cylinder make two contacts", len(m.Boundaries))
|
||||
}
|
||||
|
||||
// The same invariant the generated path has: with a pure translation one margin closes and the other
|
||||
// opens, by the same amount. Two plates at 2 cm/yr closing head-on give 4 cm/yr.
|
||||
means := sortedMeans(m.Boundaries)
|
||||
if means[0] >= 0 || means[1] <= 0 {
|
||||
t.Fatalf("closing rates %.4g and %.4g; one of each is the only arrangement possible", means[0], means[1])
|
||||
}
|
||||
if got := math.Abs(means[1]); math.Abs(got-0.04) > 1e-3 {
|
||||
t.Errorf("painted plates at 2 cm/yr each close at %.4g m/yr, want 0.04", got)
|
||||
}
|
||||
// Both painted as land, so both are continental and the closing margin is a collision.
|
||||
if got, want := kinds(m.Boundaries), []string{"collision", "rift"}; !sameStrings(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func allLandAt(xM, yM float64) bool { return true }
|
||||
|
||||
func TestAHeadingIsACompassBearing(t *testing.T) {
|
||||
p := testPlanet(t)
|
||||
px, lg := paintedStripes(400, 200)
|
||||
// 0 points at the top of the image, which is -Y; 90 to the right, which is +X.
|
||||
lg.Plates[0].HeadingDeg = 0
|
||||
lg.Plates[1].HeadingDeg = 90
|
||||
|
||||
m, _, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt)
|
||||
if err != nil {
|
||||
t.Fatalf("from painting: %v", err)
|
||||
}
|
||||
north, east := m.Plates[0], m.Plates[1]
|
||||
if math.Abs(north.TransXM) > 1e-9 || north.TransYM >= 0 {
|
||||
t.Errorf("heading 0 gives (%.4g, %.4g); it should point at the top of the map",
|
||||
north.TransXM, north.TransYM)
|
||||
}
|
||||
if math.Abs(east.TransYM) > 1e-9 || east.TransXM <= 0 {
|
||||
t.Errorf("heading 90 gives (%.4g, %.4g); it should point to the right of the map",
|
||||
east.TransXM, east.TransYM)
|
||||
}
|
||||
if got := math.Hypot(east.TransXM, east.TransYM); math.Abs(got-0.02) > 1e-9 {
|
||||
t.Errorf("2 cm/yr came out as %.4g m/yr", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThePaintingCanOverruleTheLandMask(t *testing.T) {
|
||||
p := testPlanet(t)
|
||||
px, lg := paintedStripes(400, 200)
|
||||
|
||||
// Every cell is land, so both plates are continental and the closing margin is a collision.
|
||||
m, _, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt)
|
||||
if err != nil {
|
||||
t.Fatalf("from painting: %v", err)
|
||||
}
|
||||
if !m.Plates[0].Continental || !m.Plates[1].Continental {
|
||||
t.Fatal("a planet of land has an oceanic plate on it")
|
||||
}
|
||||
|
||||
// The legend says otherwise about one of them, and a legend that bothers to say so wins.
|
||||
oceanic := false
|
||||
lg.Plates[1].Continental = &oceanic
|
||||
m, _, err = FromPainting(p, Default(), lg, px, 400, 200, allLandAt)
|
||||
if err != nil {
|
||||
t.Fatalf("from painting: %v", err)
|
||||
}
|
||||
if m.Plates[1].Continental {
|
||||
t.Error("the legend called plate 1 oceanic and the land mask overruled it")
|
||||
}
|
||||
// And the consequence is the point of the override: the same margin is now a subduction zone.
|
||||
if got, want := kinds(m.Boundaries), []string{"ridge", "subduction"}; !sameStrings(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACentroidIsMeasuredTheShortWayRound(t *testing.T) {
|
||||
p := testPlanet(t)
|
||||
const w, h = 400, 200
|
||||
lg := &PaintLegend{
|
||||
WarnDistance: 60,
|
||||
Plates: []PaintPlate{
|
||||
{Name: "seam", RGB: [3]int{200, 60, 60}, SpeedCmYr: 2, HeadingDeg: 90},
|
||||
{Name: "rest", RGB: [3]int{60, 60, 200}, SpeedCmYr: 2, HeadingDeg: 270},
|
||||
},
|
||||
}
|
||||
// Plate 0 is painted across the meridian: the left eighth and the right eighth of the image. Its centre
|
||||
// is the seam, and an arithmetic mean of those columns would put it on the far side of the planet - and
|
||||
// with it the pole it spins about.
|
||||
px := make([]uint8, w*h*3)
|
||||
for y := range h {
|
||||
for x := range w {
|
||||
id := 1
|
||||
if x < w/8 || x >= 7*w/8 {
|
||||
id = 0
|
||||
}
|
||||
o := (y*w + x) * 3
|
||||
c := lg.Plates[id].RGB
|
||||
px[o], px[o+1], px[o+2] = uint8(c[0]), uint8(c[1]), uint8(c[2])
|
||||
}
|
||||
}
|
||||
|
||||
m, _, err := FromPainting(p, Default(), lg, px, w, h, allLandAt)
|
||||
if err != nil {
|
||||
t.Fatalf("from painting: %v", err)
|
||||
}
|
||||
circ := p.CircumferenceM()
|
||||
got := m.Plates[0].SiteXM
|
||||
// Near the meridian, measured the short way round: either just above 0 or just below the circumference.
|
||||
if d := math.Abs(wrapDelta(got, circ)); d > circ/16 {
|
||||
t.Errorf("the seam-straddling plate's centre is at %.0f m of %.0f; it should be near the meridian, "+
|
||||
"and the arithmetic mean would have put it near %.0f", got, circ, circ/2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAProposalReadsBackAsTheSamePlanet(t *testing.T) {
|
||||
p := testPlanet(t)
|
||||
cfg := Default()
|
||||
cfg.Count = 6
|
||||
land := func(xM, yM float64) bool { return yM > 4000 && yM < 14000 }
|
||||
|
||||
made, err := Build(p, 3630, cfg, land)
|
||||
if err != nil {
|
||||
t.Fatalf("build: %v", err)
|
||||
}
|
||||
|
||||
// The round trip is what makes Propose worth having: what it writes has to be a layer that comes back as
|
||||
// the planet it was written from, or an author's first edit starts from something that was never true.
|
||||
px, w, h, lg := made.Propose(1600)
|
||||
read, match, err := FromPainting(p, cfg, lg, px, w, h, land)
|
||||
if err != nil {
|
||||
t.Fatalf("read back: %v", err)
|
||||
}
|
||||
if match.Far != 0 {
|
||||
t.Errorf("%d of %d cells of its own proposal did not match its own legend", match.Far, match.Cells)
|
||||
}
|
||||
if len(read.Plates) != len(made.Plates) {
|
||||
t.Fatalf("%d plates written, %d read back", len(made.Plates), len(read.Plates))
|
||||
}
|
||||
|
||||
// The motions survive the trip through the legend's cm/yr and degrees. Rounded when written - a tenth of
|
||||
// a cm/yr and a whole degree - so the tolerance is the rounding, not a fudge.
|
||||
for i := range made.Plates {
|
||||
a, b := made.Plates[i], read.Plates[i]
|
||||
if d := math.Hypot(a.TransXM-b.TransXM, a.TransYM-b.TransYM); d > 0.0006 {
|
||||
t.Errorf("plate %d drifts %.5g m/yr differently after the round trip", i, d)
|
||||
}
|
||||
if a.Continental != b.Continental {
|
||||
t.Errorf("plate %d was %v continental and reads back %v", i, a.Continental, b.Continental)
|
||||
}
|
||||
}
|
||||
|
||||
// And the tectonics: the same margins doing the same things. Not vertex-for-vertex - the proposal is a
|
||||
// raster at 1600 px and the model was traced at the tectonic grid - but the same boundaries by count and
|
||||
// by what each one is.
|
||||
if len(read.Boundaries) != len(made.Boundaries) {
|
||||
t.Errorf("%d boundaries written, %d read back", len(made.Boundaries), len(read.Boundaries))
|
||||
}
|
||||
if got, want := kinds(read.Boundaries), kinds(made.Boundaries); !sameStrings(got, want) {
|
||||
t.Errorf("margins read back as %v, were %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestALegendThatCannotBeAPlanetIsRefused(t *testing.T) {
|
||||
one := &PaintLegend{Plates: []PaintPlate{{Name: "only", RGB: [3]int{1, 2, 3}}}}
|
||||
if err := one.validate("test"); err == nil {
|
||||
t.Error("a planet in one plate was accepted; it has no boundaries")
|
||||
}
|
||||
dup := &PaintLegend{Plates: []PaintPlate{
|
||||
{Name: "a", RGB: [3]int{1, 2, 3}},
|
||||
{Name: "b", RGB: [3]int{1, 2, 3}},
|
||||
}}
|
||||
if err := dup.validate("test"); err == nil {
|
||||
t.Error("two plates sharing a colour were accepted; a colour is one plate")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user