diff --git a/.env-coastal-lifecycle b/.env-coastal-lifecycle new file mode 100644 index 0000000..9b9a033 --- /dev/null +++ b/.env-coastal-lifecycle @@ -0,0 +1,3 @@ +CC_STORE_TYPE=FS +FSB_ROOT_PATH="/workspaces/consequences-runner/data/" +CC_PAYLOAD_ID=coastal \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 00a4c25..e9ed2a4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { "name": "Launch Package", "type": "go", @@ -43,6 +44,17 @@ "Test_Compute" ], "envFile": "/workspaces/.env-milliman" + },{ + "name": "Test Coastal Lifecycle", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}", + "args": [ + "-test.run", + "Test_Main" + ], + "envFile": "/workspaces/consequences-runner/.env-coastal-lifecycle" },{ "name": "Download", "type": "go", diff --git a/Dockerfile b/Dockerfile index bcd6c86..d988cb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ -FROM ghcr.io/usace-cloud-compute/cc-tiledb-base:latest as dev +FROM ghcr.io/osgeo/gdal:ubuntu-full-3.8.3 as dev -ARG TILEDB_LIB=/usr/local/lib/tiledb +# ARG TILEDB_LIB=/usr/local/lib/tiledb ARG GO_VERSION=1.24.5 ARG TARGETARCH ENV PATH=/go/bin:$PATH -ENV CGO_LDFLAGS="-L${TILEDB_LIB}/lib" -ENV CGO_CFLAGS="-I${TILEDB_LIB}/include" +# ENV CGO_LDFLAGS="-L${TILEDB_LIB}/lib" +# ENV CGO_CFLAGS="-I${TILEDB_LIB}/include" ENV GOROOT=/go ENV GOPATH=/src/go @@ -15,6 +15,7 @@ RUN echo "Building for arch: ${TARGETARCH}" &&\ tar -xvzf /go${GO_VERSION}.linux-${TARGETARCH}.tar.gz -C / RUN apt update +RUN apt -y install git RUN apt -y install gdal-data RUN apt -y install gdal-bin RUN apt -y install libgdal-dev diff --git a/actions/compute-payload.go b/actions/compute-payload.go index e6f10c5..c6ff7a3 100644 --- a/actions/compute-payload.go +++ b/actions/compute-payload.go @@ -17,7 +17,9 @@ import ( "github.com/USACE/go-consequences/structureprovider" "github.com/USACE/go-consequences/structures" "github.com/usace-cloud-compute/cc-go-sdk" + "github.com/usace-cloud-compute/consequences-runner/crresultswriters" lrw "github.com/usace-cloud-compute/consequences-runner/crresultswriters" + lhp "github.com/usace-cloud-compute/consequences-runner/hazardproviders" "github.com/usace-cloud-compute/consequences-runner/structureproviders" ) @@ -32,34 +34,46 @@ const ( outputLayerName string = "damages" structureInventoryPathKey string = "Inventory" //plugin datasource name required //seedsDatasourceName string = "seeds.json" //plugin datasource name required - depthgridDatasourceName string = "depth-grid" //plugin datasource name required - velocitygridDatasourceName string = "velocity-grid" //plugin datasource name required - durationgridDatasourceName string = "duration-grid" //plugin datasource name required - outputDatasourceName string = "Damages" //plugin output datasource name required - localData string = "/app/data" - pluginName string = "consequences" - DepthGridPathsKey string = "depth-grids" // expected to contain the fully qualified vsis3 path set comma separated or the local path if the resource is included as an inputdatasource - VelocityGridPathsKey string = "velocity-grids" // expected to contain the fully qualified vsis3 path set comma separated or the local path if the resource is included as an inputdatasource - FrequenciesKey string = "frequencies" //expected to be comma separated string - inventoryPathKey string = "Inventory" //expected this is local - needs to agree with the payload input datasource name - damageFunctionPathKey string = "damage-functions" //expected this is local - needs to agree with the payload input datasource name - projectIdKey string = "project-id" - runIdKey string = "run-id" - pgUserKey string = "PG_USER" - pgPasswordKey string = "PG_PASSWORD" - pgDbnameKey string = "PG_DBNAME" - pgHostKey string = "PG_HOST" - pgPortKey string = "PG_PORT" - pgSchemaKey string = "PG_SCHEMA" - computeEventActionName string = "compute-event" - computeFrequencyActionName string = "compute-frequency" - computeCoastalEventActionName string = "compute-coastal-event" + depthgridDatasourceName string = "depth-grid" //plugin datasource name required + velocitygridDatasourceName string = "velocity-grid" //plugin datasource name required + durationgridDatasourceName string = "duration-grid" //plugin datasource name required + stormSimEventsPath string = "ss-events" // + stormSimEventsDriver string = "ss-events-driver" + stormSimEventsLayer string = "ss-events-layer" + stormSimResponsesPath string = "ss-responses" + stormSimResponsesDriver string = "ss-responses-driver" + stormSimResponsesLayer string = "ss-responses-layer" + stormSimReachesPath string = "ss-reaches" + stormSimReachesDriver string = "ss-reaches-driver" + stormSimReachesLayer string = "ss-reaches-layer" + stormSimLifecycle string = "ss-lifecycle" + outputDatasourceName string = "Damages" //plugin output datasource name required + localData string = "/app/data" + pluginName string = "consequences" + DepthGridPathsKey string = "depth-grids" // expected to contain the fully qualified vsis3 path set comma separated or the local path if the resource is included as an inputdatasource + VelocityGridPathsKey string = "velocity-grids" // expected to contain the fully qualified vsis3 path set comma separated or the local path if the resource is included as an inputdatasource + FrequenciesKey string = "frequencies" //expected to be comma separated string + inventoryPathKey string = "Inventory" //expected this is local - needs to agree with the payload input datasource name + damageFunctionPathKey string = "damage-functions" //expected this is local - needs to agree with the payload input datasource name + projectIdKey string = "project-id" + runIdKey string = "run-id" + pgUserKey string = "PG_USER" + pgPasswordKey string = "PG_PASSWORD" + pgDbnameKey string = "PG_DBNAME" + pgHostKey string = "PG_HOST" + pgPortKey string = "PG_PORT" + pgSchemaKey string = "PG_SCHEMA" + computeEventActionName string = "compute-event" + computeFrequencyActionName string = "compute-frequency" + computeCoastalEventActionName string = "compute-coastal-event" + computeCoastalLifecycleActionName string = "compute-coastal-lifecycle" ) func init() { cc.ActionRegistry.RegisterAction(computeEventActionName, &ComputeEventAction{}) cc.ActionRegistry.RegisterAction(computeFrequencyActionName, &ComputeFrequencyAction{}) cc.ActionRegistry.RegisterAction(computeCoastalEventActionName, &ComputeCoastalEventAction{}) + cc.ActionRegistry.RegisterAction(computeCoastalLifecycleActionName, &ComputeCoastalLifecycleAction{}) } type ComputeEventAction struct { @@ -71,6 +85,9 @@ type ComputeFrequencyAction struct { type ComputeCoastalEventAction struct { cc.ActionRunnerBase } +type ComputeCoastalLifecycleAction struct { + cc.ActionRunnerBase +} func (ar *ComputeEventAction) Run() error { a := ar.Action @@ -381,6 +398,118 @@ func (ar *ComputeFrequencyAction) Run() error { ComputeMultiFrequency(hps, frequencies, abstractSP, rw) return nil } + +func (ar *ComputeCoastalLifecycleAction) Run() error { + a := ar.Action + // get all relevant parameters + tablename := a.Attributes.GetStringOrFail(tablenameKey) + stormSimEventsPathString := a.Attributes.GetStringOrFail(stormSimEventsPath) + stormSimResponsesPathString := a.Attributes.GetStringOrFail(stormSimResponsesPath) + stormSimReachesPathString := a.Attributes.GetStringOrFail(stormSimReachesPath) + + ssEventsDriverString := a.Attributes.GetStringOrFail(stormSimEventsDriver) + ssResponsesDriverString := a.Attributes.GetStringOrFail(stormSimResponsesDriver) + ssReachesDriverString := a.Attributes.GetStringOrFail(stormSimReachesDriver) + + ssEventsLayerString := a.Attributes.GetStringOrDefault(stormSimEventsLayer, "") + ssResponsesLayerString := a.Attributes.GetStringOrDefault(stormSimResponsesLayer, "") + ssReachesLayerString := a.Attributes.GetStringOrDefault(stormSimReachesLayer, "") + + ssLC := a.Attributes.GetStringOrFail(stormSimLifecycle) + ssLifecycle, err := strconv.Atoi(ssLC) + if err != nil { + panic(err) + } + + inventoryPath := a.Attributes.GetStringOrFail(inventoryPathKey) //expected this is local - needs to agree with the payload input datasource name + inventoryDriver := a.Attributes.GetStringOrFail(inventoryDriverKey) + + outputDriver := a.Attributes.GetStringOrFail(outputDriverKey) + outputFileName := a.Attributes.GetStringOrFail(outputFileNameKey) //expected this is local - needs to agree with the payload output datasource name + //useKnowledgeUncertainty, err := strconv.ParseBool(a.Parameters.GetStringOrFail(useKnowledgeUncertaintyKey)) + damageFunctionPath := a.Attributes.GetStringOrFail(damageFunctionPathKey) //expected this is local - needs to agree with the payload input datasource name + + ssi := lhp.StormSimInfo{ + EventsFP: stormSimEventsPathString, + EventsDriver: ssEventsDriverString, + EventsLayername: ssEventsLayerString, + ResponsesFP: stormSimResponsesPathString, + ResponsesDriver: ssResponsesDriverString, + ResponsesLayername: ssResponsesLayerString, + ReachesFP: stormSimReachesPathString, + ReachesDriver: ssReachesDriverString, + ReachesLayername: ssReachesLayerString, + Lifecycle: ssLifecycle, + } + + hp, err := lhp.InitStormSim(ssi) + if err != nil { + panic(err) + } + defer hp.Close() + + var abstractSP consequences.StreamProvider + // var sr string + if inventoryDriver == "MILLIMAN" { + sp, err := structureproviders.InitMillimanStructureProviderwithOcctypePath(inventoryPath, damageFunctionPath) + sp.SetDeterministic(true) + if err != nil { + return err + } + fmt.Sprintln(sp.FilePath) + // sr = sp.SpatialReference() + abstractSP = sp + } else { + sp, err := structureprovider.InitStructureProviderwithOcctypePath(inventoryPath, tablename, inventoryDriver, damageFunctionPath) + sp.SetDeterministic(true) + if err != nil { + return err + } + fmt.Sprintln(sp.FilePath) + // sr = sp.SpatialReference() + abstractSP = sp + } + + //initalize a results writer + //TODO: add more key-value pairs to payload for summary and events results writer details + summaryResultsFile := fmt.Sprintf("summary_%s", outputFileName) + eventsResultsFile := fmt.Sprintf("events_%s", outputFileName) + rw, err := crresultswriters.InitLifecycleResultsWriter(summaryResultsFile, "summary_results", outputDriver, eventsResultsFile, "event_results", outputDriver) + if err != nil { + panic(err) + } + defer rw.Close() + + //compute results + //get boundingbox + fmt.Println("Getting bbox") + bbox, err := hp.HazardBoundary() + if err != nil { + log.Panicf("Unable to get the raster bounding box: %s", err) + } + fmt.Println(bbox.ToString()) + abstractSP.ByBbox(bbox, func(f consequences.Receptor) { + //ProvideHazard works off of a geography.Location + d, err2 := hp.Hazard(geography.Location{X: f.Location().X, Y: f.Location().Y}) + + //compute damages based on hazard being able to provide depth + if err2 == nil { + r, err3 := f.Compute(d) + r.Headers = append(r.Headers, "multihazard") + bytes, err := json.Marshal(d) + s := "" + if err == nil { + s = string(bytes) + } + r.Result = append(r.Result, s) + if err3 == nil { + rw.Write(r) + } + } + }) + return nil + +} func ComputeMultiFrequency(hps []hazardproviders.HazardProvider, freqs []float64, sp consequences.StreamProvider, w consequences.ResultsWriter) { fmt.Printf("Computing %v frequencies\n", len(freqs)) //ASSUMPTION hazard providers and frequencies are in the same order diff --git a/crresultswriters/lifecycle-results-writer.go b/crresultswriters/lifecycle-results-writer.go new file mode 100644 index 0000000..55245de --- /dev/null +++ b/crresultswriters/lifecycle-results-writer.go @@ -0,0 +1,69 @@ +package crresultswriters + +import ( + "github.com/USACE/go-consequences/consequences" + "github.com/USACE/go-consequences/resultswriters" +) + +type lifecycleResultsWriter struct { + SummaryWriter consequences.ResultsWriter + EventsWriter consequences.ResultsWriter +} + +func InitLifecycleResultsWriter( + summaryFilepath string, + summaryLayername string, + summaryDriver string, + eventsFilepath string, + eventsLayername string, + eventsDriver string, +) (*lifecycleResultsWriter, error) { + srw, err := resultswriters.InitSpatialResultsWriter(summaryFilepath, summaryLayername, summaryDriver) + if err != nil { + panic(err) + } + erw, err := resultswriters.InitSpatialResultsWriter(eventsFilepath, eventsLayername, eventsDriver) + if err != nil { + panic(err) + } + return &lifecycleResultsWriter{SummaryWriter: srw, EventsWriter: erw}, nil +} + +func (lrw *lifecycleResultsWriter) Write(r consequences.Result) { + // Lifecycle compute includes a column which contains results for each event in the lifecycle + // 1. remove the event results column and parse them into a series of discrete Results + // 2. pass each event result to lrw.EventsWriter.Write() + // 3. pass the remaining columns to lrw.SummaryWriter.Write() + fd_id, err := r.Fetch("fd_id") + if err != nil { + panic(err) + } + e, err := r.Fetch("hazard results") + if err != nil { + panic(err) + } + eventResults := e.(consequences.Result) + for _, ei := range eventResults.Result { + er := ei.(consequences.Result) + hi := append([]string{"fd_id"}, er.Headers...) + ri := append([]interface{}{fd_id}, er.Result...) + lrw.EventsWriter.Write(consequences.Result{Headers: hi, Result: ri}) + } + + summaryHeaders := make([]string, 0) + summaryResults := make([]interface{}, 0) + + for i, h := range r.Headers { + if h != "hazard results" { + summaryHeaders = append(summaryHeaders, h) + summaryResults = append(summaryResults, r.Result[i]) + } + } + lrw.SummaryWriter.Write(consequences.Result{Headers: summaryHeaders, Result: summaryResults}) +} + +func (lrw *lifecycleResultsWriter) Close() { + // Will this fail if both writers are using the same geopackage? + lrw.SummaryWriter.Close() + lrw.EventsWriter.Close() +} diff --git a/go.mod b/go.mod index 8eeec48..a580d81 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,14 @@ go 1.24 require ( github.com/HydrologicEngineeringCenter/go-statistics v0.0.0-20240126145250-a17483ae0981 - github.com/USACE/go-consequences v0.0.0-20250731200338-7eee6afc377f + github.com/USACE/go-consequences v0.0.0-20260303155249-41dfc39dd84d github.com/dewberry/gdal v0.3.4 github.com/google/uuid v1.6.0 // indirect github.com/usace-cloud-compute/cc-go-sdk v0.0.0-20251124210849-b455e063a7ea ) +require github.com/usace-cloud-compute/filesapi v0.0.0-20251107191432-8084e0da4b5c + require ( github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect @@ -40,7 +42,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/usace-cloud-compute/filesapi v0.0.0-20251107191432-8084e0da4b5c // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect gonum.org/v1/gonum v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index b25f0ef..5b3da85 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zum github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/HydrologicEngineeringCenter/go-statistics v0.0.0-20240126145250-a17483ae0981 h1:JFLYdHvF3mPN56AkXeaWm41bQ9eJFuSceCtXXtMWTNI= github.com/HydrologicEngineeringCenter/go-statistics v0.0.0-20240126145250-a17483ae0981/go.mod h1:ABek1cDzTXjGA4uwrB91/NriEROj8AnUDaX7xU+1L2E= -github.com/USACE/go-consequences v0.0.0-20250731200338-7eee6afc377f h1:i4a5qJ/6ghF6VxJKCadp+Z6JHUzLgHwuoAXr7WEi2sQ= -github.com/USACE/go-consequences v0.0.0-20250731200338-7eee6afc377f/go.mod h1:+HMvu5PLsam+TS7GCpeiraeCWjwD04/hcgz85kuQTM4= +github.com/USACE/go-consequences v0.0.0-20260303155249-41dfc39dd84d h1:Fi60yreROXSA9kWDBdW8rvAJiDgNV4jj2VMZuDTc1eA= +github.com/USACE/go-consequences v0.0.0-20260303155249-41dfc39dd84d/go.mod h1:+HMvu5PLsam+TS7GCpeiraeCWjwD04/hcgz85kuQTM4= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4= github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= @@ -105,8 +105,6 @@ github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qq github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/usace-cloud-compute/cc-go-sdk v0.0.0-20251118163833-620acc79f197 h1:oM3e8JDPNyYjK3/aZHQkNDeWiE0D2/SPtFAr6haz3Hc= -github.com/usace-cloud-compute/cc-go-sdk v0.0.0-20251118163833-620acc79f197/go.mod h1:HZsGTrNUUdjfqqJJaLpSBEKoIDmu/vnye2KK/AwchDo= github.com/usace-cloud-compute/cc-go-sdk v0.0.0-20251124210849-b455e063a7ea h1:tlM91GCRJ1gqc4dcFvfI6h8MI2l5FNR3Ehn+Rh4oDJw= github.com/usace-cloud-compute/cc-go-sdk v0.0.0-20251124210849-b455e063a7ea/go.mod h1:HZsGTrNUUdjfqqJJaLpSBEKoIDmu/vnye2KK/AwchDo= github.com/usace-cloud-compute/filesapi v0.0.0-20251107191432-8084e0da4b5c h1:1Srw2WpT9tcEIUY853oeojXJjPH8iTYFGLXPaIrUe+I= diff --git a/hazardproviders/csv_multihazard_provider.go b/hazardproviders/csv_multihazard_provider.go new file mode 100644 index 0000000..d325183 --- /dev/null +++ b/hazardproviders/csv_multihazard_provider.go @@ -0,0 +1,111 @@ +package hazardproviders + +import ( + "encoding/csv" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/USACE/go-consequences/geography" + gc "github.com/USACE/go-consequences/hazardproviders" + "github.com/USACE/go-consequences/hazards" +) + +type csvArrivalDepthDurationMultiHazardProvider struct { + f *os.File + arrivals []time.Time + depths []float64 + durations []float64 + process gc.HazardFunction + bbox geography.BBox +} + +func InitCSV(fp string, b geography.BBox) (csvArrivalDepthDurationMultiHazardProvider, error) { + fmt.Println("Connecting to: " + fp) // should we confirm file is csv? + file, err := os.Open(fp) + if err != nil { + panic(err) + } + + reader := csv.NewReader(file) + + rows, err := reader.ReadAll() + if err != nil { + panic(err) + } + arrivals := make([]time.Time, len(rows)-1) + depths := make([]float64, len(rows)-1) + durations := make([]float64, len(rows)-1) + + for i, row := range rows[1:] { + year, err := strconv.Atoi(row[2]) + if err != nil { + panic(err) + } + month, err := strconv.Atoi(strings.TrimSpace(row[3])) + if err != nil { + panic(err) + } + day, err := strconv.Atoi(strings.TrimSpace(row[4])) + if err != nil { + panic(err) + } + at := time.Date(year, time.Month(month), int(day), 0, 0, 0, 0, time.UTC) + depth, err := strconv.ParseFloat(row[5], 64) + if err != nil { + panic(err) + } + dur, err := strconv.ParseFloat(row[6], 64) + if err != nil { + panic(err) + } + + arrivals[i] = at + depths[i] = depth + durations[i] = dur + } + + return csvArrivalDepthDurationMultiHazardProvider{ + f: file, + arrivals: arrivals, + depths: depths, + durations: durations, + process: gc.ArrivalDepthAndDurationHazardFunction(), + bbox: b, + }, nil + +} + +func (c csvArrivalDepthDurationMultiHazardProvider) Close() { + c.f.Close() +} + +func (c csvArrivalDepthDurationMultiHazardProvider) Hazard(l geography.Location) (hazards.HazardEvent, error) { + var hm hazards.ArrivalDepthandDurationEventMulti + + for i, d := range c.depths { + hd := hazards.HazardData{ + Depth: d, + Velocity: 0, + ArrivalTime: c.arrivals[i], + Erosion: 0, + Duration: c.durations[i], + WaveHeight: 0, + Salinity: false, + Qualitative: "", + } + var h hazards.HazardEvent + h, err := c.process(hd, h) + if err != nil { + panic(err) + } + hm.Append(h.(hazards.ArrivalDepthandDurationEvent)) + } + return &hm, nil +} + +func (c csvArrivalDepthDurationMultiHazardProvider) HazardBoundary() (geography.BBox, error) { + return c.bbox, nil +} diff --git a/hazardproviders/stormSim_lifecycle_hazardprovider.go b/hazardproviders/stormSim_lifecycle_hazardprovider.go new file mode 100644 index 0000000..5735a0b --- /dev/null +++ b/hazardproviders/stormSim_lifecycle_hazardprovider.go @@ -0,0 +1,338 @@ +package hazardproviders + +import ( + "errors" + "fmt" + "time" + + "github.com/USACE/go-consequences/geography" + gc "github.com/USACE/go-consequences/hazardproviders" + "github.com/USACE/go-consequences/hazards" + "github.com/dewberry/gdal" +) + +type stormsimLifecycleMultiHazardProvider struct { + // specify drivers for each? + arrivals []time.Time + depths []float64 + durations []float64 + process gc.HazardFunction + bbox geography.BBox + geom gdal.Geometry +} + +// create struct to hold the arguments and + +type StormSimInfo struct { + EventsFP string + EventsDriver string + EventsLayername string + ResponsesFP string + ResponsesDriver string + ResponsesLayername string + ReachesFP string + ReachesDriver string + ReachesLayername string + Lifecycle int +} + +func InitStormSim(ssi StormSimInfo) (stormsimLifecycleMultiHazardProvider, error) { + + reaches, err := parseReachesFile(ssi.ReachesFP, ssi.ReachesLayername, ssi.ReachesDriver) + if err != nil { + panic(err) + } + events, err := parseEventsFile(ssi.EventsFP, ssi.EventsLayername, ssi.EventsDriver) + if err != nil { + panic(err) + } + + //TODO: how to handle case where there are multiple reaches? + reach := reaches[0] + reachevents := events[reach.reachID][ssi.Lifecycle] + + add, err := parseResponsesFile(ssi.ResponsesFP, ssi.ResponsesLayername, ssi.ResponsesDriver, len(reachevents), ssi.Lifecycle) + + return stormsimLifecycleMultiHazardProvider{ + arrivals: add.arrivals, + depths: add.depths, + durations: add.durations, + process: gc.ArrivalDepthAndDurationHazardFunction(), + bbox: reach.Bbox, + geom: reach.Geom, + }, nil +} + +func eventsSchema() []string { + s := []string{"location_id", "lifecycle", "year_offset", "year", "month", "day", "hour", "timestamp", "storm_id"} + return s +} + +func responsesSchema() []string { + s := []string{"location_id", "date", "stormevent_id", "storm_id", "lifecycle", "stage"} + return s +} + +func reachesSchema() []string { + s := []string{"Name", "NameAbbreviated", "Note", "NOAAStation", "NOAAStationName", "SavePoint"} + return s +} + +type Reach struct { + reachID string + reachName string + Bbox geography.BBox + Geom gdal.Geometry +} + +func parseReachesFile(filepath string, layername string, driver string) ([]Reach, error) { + var ret = []Reach{} + + d := gdal.OGRDriverByName(driver) + ds, dsok := d.Open(filepath, int(gdal.ReadOnly)) + if !dsok { + return ret, errors.New("error opening reaches file of type " + driver) + } + + var layer gdal.Layer + if layername != "" { + layer = ds.LayerByName(layername) + } else { + layer = ds.LayerByIndex(0) + } + def := layer.Definition() + s := reachesSchema() + sIDX := make([]int, len(s)) + + for i, f := range s { + idx := def.FieldIndex(f) + + if idx < 0 { + return ret, errors.New("gdal dataset at path " + filepath + " Expected field named " + f + " none was found") + } + sIDX[i] = idx + } + + fc, _ := layer.FeatureCount(true) + ret = make([]Reach, fc) + + for i := range fc { + feat := layer.NextFeature() + if feat != nil { + //NOTE: "Name", "NameAbbreviated", "Note", "NOAAStation", "NOAAStationName", "SavePoint", "geom" + reach_id := feat.FieldAsString(sIDX[1]) + reach_name := feat.FieldAsString(sIDX[0]) + geom := feat.Geometry() + env := geom.Envelope() + xmin := env.MinX() + xmax := env.MaxX() + ymin := env.MinY() + ymax := env.MaxY() + bb := geography.BBox{Bbox: []float64{xmin, ymin, xmax, ymax}} + ret[i] = Reach{reachID: reach_id, reachName: reach_name, Geom: geom, Bbox: bb} + } + } + + return ret, nil +} + +func parseEventsFile(filepath string, layername string, driver string) (map[string]map[int][]string, error) { + ret := make(map[string]map[int][]string) // map[location_id]map[lifecycle][]storm_ids + + d := gdal.OGRDriverByName(driver) + ds, dsok := d.Open(filepath, int(gdal.ReadOnly)) + if !dsok { + return ret, errors.New("error opening events file of type " + driver) + } + + var layer gdal.Layer + if layername != "" { + layer = ds.LayerByName(layername) + } else { + layer = ds.LayerByIndex(0) + } + def := layer.Definition() + s := eventsSchema() + sIDX := make([]int, len(s)) + + for i, f := range s { + idx := def.FieldIndex(f) + if idx < 0 { + return ret, errors.New("gdal dataset at path " + filepath + " Expected field named " + f + " none was found") + } + sIDX[i] = idx + } + + fc, _ := layer.FeatureCount(true) + + for range fc { + feat := layer.NextFeature() + if feat != nil { + //NOTE: "location_id", "lifecycle", "year_offset", "year", "month", "day", "hour", "timestamp", "storm_id" + loc_id := feat.FieldAsString(sIDX[0]) + lifecycle := feat.FieldAsInteger(sIDX[1]) + storm_id := feat.FieldAsString(sIDX[8]) + + if ret[loc_id] == nil { + ret[loc_id] = make(map[int][]string) + } + if ret[loc_id][lifecycle] == nil { + ret[loc_id][lifecycle] = []string{storm_id} + } else { + ret[loc_id][lifecycle] = append(ret[loc_id][lifecycle], storm_id) + } + } + } + + return ret, nil +} + +type ADDInfo struct { + storm_ids []string + arrivals []time.Time + depths []float64 + durations []float64 +} + +func parseResponsesFile(filepath string, layername string, driver string, n int, lifecycle int) (ADDInfo, error) { + + arrivals := make([]time.Time, n) + depths := make([]float64, n) + durations := make([]float64, n) + ret := ADDInfo{ + arrivals: arrivals, + depths: depths, + durations: durations, + } + + d := gdal.OGRDriverByName(driver) + // for i := 0; i < gdal.OGRDriverCount(); i++ { // can use this to ensure given driver is available + // fmt.Println(gdal.OGRDriverByIndex(i).Name()) + // } + + ds, dsok := d.Open(filepath, int(gdal.ReadOnly)) + if !dsok { + return ret, errors.New("error opening responses file of type " + driver) + } + + var layer gdal.Layer + if layername != "" { + layer = ds.LayerByName(layername) + } else { + layer = ds.LayerByIndex(0) + } + def := layer.Definition() + + s := responsesSchema() + sIDX := make([]int, len(s)) + + for i, f := range s { + idx := def.FieldIndex(f) + if idx < 0 { + return ret, errors.New("gdal dataset at path " + filepath + " Expected field named " + f + " none was found") + } + sIDX[i] = idx + } + + fc, _ := layer.FeatureCount(true) + // fmt.Printf("The layer has %v features\n", fc) + var curStormIndex int = 0 + var curStormID string + var curStormLifecycle int // + var curStormStart time.Time + var curStormEnd time.Time + var curStormPeakStage float64 + + for i := range fc { + feat := layer.NextFeature() + if feat != nil { + var date_i time.Time + date_i, dtok := feat.FieldAsDateTime(sIDX[1]) + if !dtok { + // GDAL doesn't seem to recognize DateTime strings in csv files as DateTimes + fmt.Println("Failed to parse date field as DateTime. Attempting to parse from string with format 'YYYY-MM-DD HH:MM:SS'") + ds := feat.FieldAsString(sIDX[1]) + var err error + date_i, err = time.Parse("2006-01-02 15:04:05", ds) + if err != nil { + panic(err) + } + } + + storm_id := feat.FieldAsString(sIDX[2]) + lifecycle_i := feat.FieldAsInteger(sIDX[4]) + //TODO: handle NA values for stage + stage_i := feat.FieldAsFloat64(sIDX[5]) + + if storm_id == curStormID { + curStormEnd = date_i + if stage_i > curStormPeakStage { + curStormPeakStage = stage_i + // curStormPeakTime = date_i + } + } else { + // we've reached a new storm in the series + // save results from previous storm + if i > 0 { // can't save previous storm if we're on row 0 + if curStormLifecycle == lifecycle { // not handling multiple lifecycles currently + ret.arrivals[curStormIndex] = curStormStart + ret.depths[curStormIndex] = curStormPeakStage + duration := curStormEnd.Sub(curStormStart) + ret.durations[curStormIndex] = duration.Hours() / 24.0 + curStormIndex++ + } + } + // reset all tracking variables to current row + curStormID = storm_id + curStormLifecycle = lifecycle_i + curStormStart = date_i + curStormEnd = date_i + curStormPeakStage = stage_i + // curStormPeakTime = date_i + } + } + } + ret.arrivals[curStormIndex] = curStormStart + ret.depths[curStormIndex] = curStormPeakStage + duration := curStormEnd.Sub(curStormStart) + ret.durations[curStormIndex] = duration.Hours() / 24.0 + return ret, nil +} + +func (c stormsimLifecycleMultiHazardProvider) Close() { + // what goes here after switching to using gdal? +} + +func (c stormsimLifecycleMultiHazardProvider) Hazard(l geography.Location) (hazards.HazardEvent, error) { + var hm hazards.ArrivalDepthandDurationEventMulti + + for i, d := range c.depths { + hd := hazards.HazardData{ + Depth: d, + Velocity: 0, + ArrivalTime: c.arrivals[i], + Erosion: 0, + Duration: c.durations[i], + WaveHeight: 0, + Salinity: false, + Qualitative: "", + } + var h hazards.HazardEvent + h, err := c.process(hd, h) + if err != nil { + panic(err) + } + hm.Append(h.(hazards.ArrivalDepthandDurationEvent)) + } + test_geom := gdal.Create(gdal.GT_Point) + test_geom.AddPoint(l.X, l.Y, 0) + + if !c.geom.Contains(test_geom) { + return &hm, errors.New("Provided hazard location is outside the reach boundary") + } + return &hm, nil +} + +func (c stormsimLifecycleMultiHazardProvider) HazardBoundary() (geography.BBox, error) { + return c.bbox, nil +} diff --git a/hazardproviders/stormSim_lifecycle_hazardprovider_test.go b/hazardproviders/stormSim_lifecycle_hazardprovider_test.go new file mode 100644 index 0000000..58fda15 --- /dev/null +++ b/hazardproviders/stormSim_lifecycle_hazardprovider_test.go @@ -0,0 +1,88 @@ +package hazardproviders + +import ( + "fmt" + "testing" + "time" +) + +func Test_ParseReachesFile(t *testing.T) {} + +func Test_ParseEventsFile(t *testing.T) { + eventsFP := "/workspaces/consequences-runner/data/coastal/EventDate_LC.csv" + + events, err := parseEventsFile(eventsFP, "", "CSV") + if err != nil { + panic(err) + } + + expected := []string{"190", "485", "833", "990", "575", "664", "176", "609", "984", "294", "610", "562", "112", "268", "161", "859", "335"} + got := events["DE001"][0] + + for i, val := range got { + e := expected[i] + if val != e { + t.Errorf("Fail: Expected %v, got %v", e, val) + } + } +} + +func Test_ParseResponsesFile(t *testing.T) { + responsesFP := "/workspaces/consequences-runner/data/coastal/stage_responses_loc_DE001_lc_0.csv" + + arrivals_expected := []time.Time{ + time.Date(2033, time.Month(8), 26, 5, 50, 29, 0, time.UTC), + time.Date(2033, time.Month(9), 10, 1, 13, 39, 0, time.UTC), + time.Date(2033, time.Month(10), 4, 7, 47, 32, 0, time.UTC), + time.Date(2033, time.Month(10), 14, 22, 47, 59, 0, time.UTC), + time.Date(2033, time.Month(11), 8, 11, 10, 48, 0, time.UTC), + time.Date(2034, time.Month(9), 19, 14, 33, 50, 0, time.UTC), + time.Date(2035, time.Month(8), 20, 22, 31, 42, 0, time.UTC), + time.Date(2036, time.Month(6), 19, 20, 38, 29, 0, time.UTC), + time.Date(2036, time.Month(9), 29, 4, 32, 9, 0, time.UTC), + time.Date(2037, time.Month(11), 4, 5, 20, 28, 0, time.UTC), + time.Date(2038, time.Month(7), 9, 2, 11, 13, 0, time.UTC), + time.Date(2039, time.Month(9), 18, 8, 33, 4, 0, time.UTC), + time.Date(2040, time.Month(9), 8, 14, 17, 10, 0, time.UTC), + time.Date(2041, time.Month(9), 3, 15, 12, 6, 0, time.UTC), + time.Date(2041, time.Month(10), 12, 5, 26, 57, 0, time.UTC), + time.Date(2042, time.Month(9), 20, 21, 50, 47, 0, time.UTC), + time.Date(2042, time.Month(11), 19, 2, 48, 24, 0, time.UTC), + } + depths_expected := []float64{ + 0.979270524147806, 1.54121902357747, 3.34393311836401e-07, + 2.54157606740306e-06, 13, 3.10613618627195, 0.0118805618230561, + 0.000230443433672538, 0.000439569934077712, 0.0123576597179041, + 7.99622779970534e-05, 13, 6.78775218723548e-05, 1.02305022821015, + 2.68301460998456e-05, 0.00243471711551489, 1.90291529387485e-06, + } + durations_expected := []float64{ + 2.0, 2.0, 2.0, 1.0, + 2.0, 2.0, 2.0, 1.0, + 2.0, 1.0, 2.0, 1.0, + 2.0, 4.0, 4.0, 1.0, 4.0, + } + add, err := parseResponsesFile(responsesFP, "", "CSV", len(arrivals_expected), 0) + if err != nil { + panic(err) + } + + for i, arrival := range add.arrivals { + if arrival != arrivals_expected[i] { + t.Errorf("Fail: Expected %v, got %v", arrivals_expected[i], arrival) + } else { + fmt.Println("Arrival Correct") + } + if add.depths[i] != depths_expected[i] { + t.Errorf("Fail: Expected %v, got %v", depths_expected[i], add.depths[i]) + } else { + fmt.Println("Depth correct") + } + if add.durations[i] != durations_expected[i] { + t.Errorf("Fail: Expected %v, got %v", durations_expected[i], add.durations[i]) + } else { + fmt.Println("Duration Correct") + } + } + +}