From 8baef24b9b21138f71add4f77342ba7c3caef97e Mon Sep 17 00:00:00 2001 From: babattles Date: Mon, 20 Feb 2023 16:47:43 -0800 Subject: [PATCH] initial commit - really rough calculations based on temp & relative humidity --- crust/crust.go | 53 ++++++++++++ crust/crust_test.go | 155 ++++++++++++++++++++++++++++++++++++ go.mod | 10 +++ go.sum | 16 ++++ inversion/inversion.go | 64 +++++++++++++++ inversion/inversion_test.go | 155 ++++++++++++++++++++++++++++++++++++ models/models.go | 33 ++++++++ sun/sun.go | 25 ++++++ sun/sun_test.go | 82 +++++++++++++++++++ 9 files changed, 593 insertions(+) create mode 100644 crust/crust.go create mode 100644 crust/crust_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 inversion/inversion.go create mode 100644 inversion/inversion_test.go create mode 100644 models/models.go create mode 100644 sun/sun.go create mode 100644 sun/sun_test.go diff --git a/crust/crust.go b/crust/crust.go new file mode 100644 index 0000000..f02299a --- /dev/null +++ b/crust/crust.go @@ -0,0 +1,53 @@ +package crust + +import ( + "github.com/babattles/snoqualmie-crust-calculator/inversion" + "github.com/babattles/snoqualmie-crust-calculator/models" + "github.com/babattles/snoqualmie-crust-calculator/sun" +) + +type CrustConfidence string + +const ( + CrustYes CrustConfidence = "yes" + CrustNo CrustConfidence = "no" + CrustMaybe CrustConfidence = "maybe" +) + +// returns an array of bools where the index is true when there likely +// exists a sun crust based on temperature inversions & sun effect +func FindSunCrust(data []models.WeatherStationData) []CrustConfidence { + res := make([]CrustConfidence, len(data)) + inversionsBelow := inversion.FindInversionsBelow(data) + sunExposures := sun.FindSunEffect(data) + for i, layer := range(data) { + gotSun := sunExposures[i] + inversionBelow := inversionsBelow[i] + + // if there was a temperature inversion detected below + // AND this layer might have recieved sun exposure + // AND the temperature is above freezing + // there is very likely a sun crust + // NOTE: this assumption is likely to upset many people + if inversionBelow && + gotSun && + !layer.BelowFreezing() { + res[i] = CrustYes + continue + } + + // because our sun exposure estimate isn't perfect, we provide a maybe if we can't + // guess more precisely because we detected an inversion + // so if the layer might have received sun exposure + // AND is above freezing, it might have a crust + // NOTE: this check assumes mid-winter conditions that will return this layer to freezing at some point + // YMMV + if gotSun && !layer.BelowFreezing() { + res[i] = CrustMaybe + continue + } + + res[i] = CrustNo + } + return res +} \ No newline at end of file diff --git a/crust/crust_test.go b/crust/crust_test.go new file mode 100644 index 0000000..76691a7 --- /dev/null +++ b/crust/crust_test.go @@ -0,0 +1,155 @@ +package crust_test + +import ( + "testing" + + "github.com/babattles/snoqualmie-crust-calculator/crust" + "github.com/babattles/snoqualmie-crust-calculator/models" + "github.com/stretchr/testify/assert" +) + +func TestFindSunCrust(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + data []models.WeatherStationData + expected []crust.CrustConfidence + }{ + { + name: "no inversions - no clouds - all above freezing - all maybes", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 36, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 500, + TemperatureF: 34, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 1000, + TemperatureF: 32, + RelativeHumidityPercent: 0, + }, + }, + expected: []crust.CrustConfidence{crust.CrustMaybe, crust.CrustMaybe, crust.CrustMaybe}, + }, + { + name: "no inversions - all clouds - all above freezing - all nos", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 36, + RelativeHumidityPercent: 100, + }, + { + ElevationFt: 500, + TemperatureF: 34, + RelativeHumidityPercent: 100, + }, + { + ElevationFt: 1000, + TemperatureF: 32, + RelativeHumidityPercent: 100, + }, + }, + expected: []crust.CrustConfidence{crust.CrustNo, crust.CrustNo, crust.CrustNo}, + }, + { + name: "no inversions - no clouds - all below freezing - all nos", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 28, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 500, + TemperatureF: 26, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 1000, + TemperatureF: 24, + RelativeHumidityPercent: 0, + }, + }, + expected: []crust.CrustConfidence{crust.CrustNo, crust.CrustNo, crust.CrustNo}, + }, + { + name: "all inversions & all clouds - no crusts", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 30, + RelativeHumidityPercent: 100, + }, + { + ElevationFt: 500, + TemperatureF: 32, + RelativeHumidityPercent: 100, + }, + { + ElevationFt: 1000, + TemperatureF: 34, + RelativeHumidityPercent: 100, + }, + }, + expected: []crust.CrustConfidence{crust.CrustNo, crust.CrustNo, crust.CrustNo}, + }, + { + name: "all inversions - no clouds - all below freezing - no crusts", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 12, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 500, + TemperatureF: 20, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 1000, + TemperatureF: 30, + RelativeHumidityPercent: 0, + }, + }, + expected: []crust.CrustConfidence{crust.CrustNo, crust.CrustNo, crust.CrustNo}, + }, + { + name: "all inversions - no clouds - one below freezing - two crusts", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 30, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 500, + TemperatureF: 32, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 1000, + TemperatureF: 34, + RelativeHumidityPercent: 0, + }, + }, + expected: []crust.CrustConfidence{crust.CrustNo, crust.CrustYes, crust.CrustYes}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc := tc + res := crust.FindSunCrust(tc.data) + assert.Equal(t, tc.expected, res) + }) + } +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6b4c274 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/babattles/snoqualmie-crust-calculator + +go 1.20 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.8.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dc5d0e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/inversion/inversion.go b/inversion/inversion.go new file mode 100644 index 0000000..f51bc66 --- /dev/null +++ b/inversion/inversion.go @@ -0,0 +1,64 @@ +package inversion + +import ( + "errors" + + "github.com/babattles/snoqualmie-crust-calculator/models" +) + +var ( + ErrImproperElevationOrdering = errors.New("improper elevation ordering") +) + +type InversionData struct { + LowerElevationFt int + HigherElevationFt int + InversionPresent bool +} + +// for each elevation band, return if there was an inversion between it and the above elevation band +// (the uppermost elevation band will always be false) +func FindInversionsAbove(data []models.WeatherStationData) []bool { + // sort first for peace of mind + models.SortByElevation(data) + + res := make([]bool, len(data)) + for i, layer := range(data) { + // uppermost layer + if i == len(data) - 1 { + res[i] = false + return res + } + + res[i] = temperatureInversionExists(layer, data[i+1]) + } + + return res +} + +// for each elevation band, return if there was an inversion between it and the elevation band below +// (the lowest elevation band will always be false) +func FindInversionsBelow(data []models.WeatherStationData) []bool { + // sort first for peace of mind + models.SortByElevation(data) + + res := make([]bool, len(data)) + for i := len(data)-1; i >= 0; i-- { + // lowest layer + if i == 0 { + res[i] = false + return res + } + + res[i] = temperatureInversionExists(data[i-1], data[i]) + } + + return res +} + +// calculates if there was a temperature inversion between two elevation bands +func temperatureInversionExists( + lowerBand models.WeatherStationData, higherBand models.WeatherStationData, +) bool { + return higherBand.TemperatureF > lowerBand.TemperatureF +} \ No newline at end of file diff --git a/inversion/inversion_test.go b/inversion/inversion_test.go new file mode 100644 index 0000000..1b57b10 --- /dev/null +++ b/inversion/inversion_test.go @@ -0,0 +1,155 @@ +package inversion_test + +import ( + "testing" + + "github.com/babattles/snoqualmie-crust-calculator/inversion" + "github.com/babattles/snoqualmie-crust-calculator/models" + "github.com/stretchr/testify/assert" +) + +func TestFindInversionsAbove(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + data []models.WeatherStationData + expected []bool + }{ + { + name: "no inversions", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 32, + }, + { + ElevationFt: 500, + TemperatureF: 30, + }, + { + ElevationFt: 1000, + TemperatureF: 28, + }, + }, + expected: []bool{false, false, false}, + }, + { + name: "inversion between all elevations", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 28, + }, + { + ElevationFt: 500, + TemperatureF: 30, + }, + { + ElevationFt: 1000, + TemperatureF: 100, + }, + }, + expected: []bool{true, true, false}, + }, + { + name: "inversion between lowest and middle elevations", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 28, + }, + { + ElevationFt: 500, + TemperatureF: 30, + }, + { + ElevationFt: 1000, + TemperatureF: 25, + }, + }, + expected: []bool{true, false, false}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + res := inversion.FindInversionsAbove(tc.data) + assert.Equal(t, tc.expected, res) + }) + } +} + +func TestFindInversionsBelow(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + data []models.WeatherStationData + expected []bool + }{ + { + name: "no inversions", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 32, + }, + { + ElevationFt: 500, + TemperatureF: 30, + }, + { + ElevationFt: 1000, + TemperatureF: 28, + }, + }, + expected: []bool{false, false, false}, + }, + { + name: "inversion between all elevations", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 28, + }, + { + ElevationFt: 500, + TemperatureF: 30, + }, + { + ElevationFt: 1000, + TemperatureF: 100, + }, + }, + expected: []bool{false, true, true}, + }, + { + name: "inversion between lowest and middle elevations", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + TemperatureF: 28, + }, + { + ElevationFt: 500, + TemperatureF: 30, + }, + { + ElevationFt: 1000, + TemperatureF: 25, + }, + }, + expected: []bool{false, true, false}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + res := inversion.FindInversionsBelow(tc.data) + assert.Equal(t, tc.expected, res) + }) + } +} \ No newline at end of file diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..f471c71 --- /dev/null +++ b/models/models.go @@ -0,0 +1,33 @@ +package models + +import "sort" + +const ( + CloudBreakHumidityThreshold int = 70 + FreezingTempF int = 32 +) + +type WeatherStationData struct { + ElevationFt int + TemperatureF int + RelativeHumidityPercent int +} + +func SortByElevation(data []WeatherStationData) { + sort.Slice(data, func(i, j int) bool { + return data[i].LowerThan(data[j]) + }) +} + +func (e WeatherStationData) LowerThan(band WeatherStationData) bool { + return e.ElevationFt < band.ElevationFt +} + +func (e WeatherStationData) CloudBreak() bool { + return e.RelativeHumidityPercent < CloudBreakHumidityThreshold +} + +func (e WeatherStationData) BelowFreezing() bool { + return e.TemperatureF < FreezingTempF +} + diff --git a/sun/sun.go b/sun/sun.go new file mode 100644 index 0000000..fc6142d --- /dev/null +++ b/sun/sun.go @@ -0,0 +1,25 @@ +package sun + +import "github.com/babattles/snoqualmie-crust-calculator/models" + +func FindSunEffect(data []models.WeatherStationData) []bool { + // sort first for peace of mind + models.SortByElevation(data) + + res := make([]bool, len(data)) + for i := len(data)-1; i >= 0; i-- { + sunOut := data[i].CloudBreak() + if sunOut { + res[i] = true + } else { + // sun isn't out, everything below won't experience sun + res[i] = false + for j := i-1; j >= 0; j-- { + res[j] = false + } + return res + } + } + + return res +} \ No newline at end of file diff --git a/sun/sun_test.go b/sun/sun_test.go new file mode 100644 index 0000000..c843454 --- /dev/null +++ b/sun/sun_test.go @@ -0,0 +1,82 @@ +package sun_test + +import ( + "testing" + + "github.com/babattles/snoqualmie-crust-calculator/models" + "github.com/babattles/snoqualmie-crust-calculator/sun" + "github.com/stretchr/testify/assert" +) + +func TestFindSunEffect(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + data []models.WeatherStationData + expected []bool + }{ + { + name: "all sun", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 500, + RelativeHumidityPercent: 0, + }, + { + ElevationFt: 1000, + RelativeHumidityPercent: 0, + }, + }, + expected: []bool{true, true, true}, + }, + { + name: "no sun", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + RelativeHumidityPercent: 100, + }, + { + ElevationFt: 500, + RelativeHumidityPercent: 100, + }, + { + ElevationFt: 1000, + RelativeHumidityPercent: 100, + }, + }, + expected: []bool{false, false, false}, + }, + { + name: "uppermost sun", + data: []models.WeatherStationData{ + { + ElevationFt: 0, + RelativeHumidityPercent: 100, + }, + { + ElevationFt: 500, + RelativeHumidityPercent: 100, + }, + { + ElevationFt: 1000, + RelativeHumidityPercent: 0, + }, + }, + expected: []bool{false, false, true}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + res := sun.FindSunEffect(tc.data) + assert.Equal(t, tc.expected, res) + }) + } +} \ No newline at end of file