Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eco_scenario.json endpoint to support modeling #24

Merged
merged 1 commit into from
Jul 16, 2014
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 212 additions & 31 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,35 @@ type config struct {

// We can't marshall maps directly with
// go-rest so we just wrap it here
type wrapper struct {
type BenefitsWrapper struct {
Benefits map[string]float64
}

type PostData struct {
type SummaryPostData struct {
Region string
Query string
Instance_id string
}

type ScenarioTree struct {
Otmcode string
Species_id int
Region string
Diameters []float64
}

type ScenarioPostData struct {
Region string
Instance_id string
Years int
Scenario_trees []ScenarioTree
}

type Scenario struct {
Total map[string]float64
Years []map[string]float64
}

// Given a values list return the single value
// associated with a given key or an error
func getSingleValue(in url.Values, key string) (string, error) {
Expand Down Expand Up @@ -128,7 +147,51 @@ func main() {
panic(err)
}

rest.HandleGET("/eco.json", func(in url.Values) (*wrapper, error) {
// This is implemented as an anonymous function so it can
// close over the variables set at the start of the main
// function, which can be expensive to load and only need to
// be loaded once.
getItreeCode := func(otmcode string, speciesId int, region string, instanceId int) (string, error) {
speciesDataForRegion, found := speciesdata[region]
if !found {
return "", errors.New(fmt.Sprintf("Species data not found for the %v region",
region))
}

itreeCode, foundItree := speciesDataForRegion[otmcode]
notFoundMessage := fmt.Sprintf("iTree code not found for otmcode %v in region %v",
otmcode, region)

overidesForInstance, found := overrides[instanceId]
if found {
overridesForRegion, found := overidesForInstance[region]
if found {
overrideCode, found := overridesForRegion[speciesId]
if found {
itreeCode = overrideCode
foundItree = true
} else {
notFoundMessage = fmt.Sprintf("There are overrides "+
"defined for instance %v in the %v region "+
"but not for species ID %v", instanceId, region, speciesId)
}
} else {
notFoundMessage = fmt.Sprintf("There are overrides defined for "+
"the instance, but not for the %v region", region)
}
}
// It is normal for an instance to not have any
// overrides defined, so there is no else block to set
// an error message in the not-found case.

if !foundItree {
return "", errors.New(notFoundMessage)
} else {
return itreeCode, nil
}
}

rest.HandleGET("/eco.json", func(in url.Values) (*BenefitsWrapper, error) {
instanceid, err := getSingleIntValue(in, "instanceid")

if err != nil {
Expand Down Expand Up @@ -167,37 +230,15 @@ func main() {
return nil, err
}

speciesDataForRegion, found := speciesdata[region]

if !found {
return nil, errors.New("invalid region")
}

factorDataForRegion, found := regiondata[region]

if !found {
return nil, errors.New("invalid region")
}

itreecode, founditree := speciesDataForRegion[otmcode]

overidesForInstance, found := overrides[instanceid]

if found {
overridesForRegion, found := overidesForInstance[region]

if found {
overrideCode, found := overridesForRegion[speciesid]

if found {
itreecode = overrideCode
founditree = true
}
}
}

if !founditree {
return nil, errors.New("invalid otm code for region")
itreecode, err := getItreeCode(otmcode, speciesid, region, instanceid)
if err != nil {
return nil, err
}

factorsum := make([]float64, len(eco.Factors))
Expand All @@ -208,10 +249,10 @@ func main() {
diameter,
factorsum)

return &wrapper{Benefits: eco.FactorArrayToMap(factorsum)}, nil
return &BenefitsWrapper{Benefits: eco.FactorArrayToMap(factorsum)}, nil
})

rest.HandlePOST("/eco_summary.json", func(data *PostData) (*wrapper, error) {
rest.HandlePOST("/eco_summary.json", func(data *SummaryPostData) (*BenefitsWrapper, error) {
query := data.Query
region := data.Region

Expand Down Expand Up @@ -265,7 +306,147 @@ func main() {
return nil, err
}

return &wrapper{Benefits: factorsums}, nil
return &BenefitsWrapper{Benefits: factorsums}, nil
})

// Take an array of prospective trees where each tree contains
// an array of diamaters, one for each year the tree is alive,
// and return an array of eco calulations, one for each year
//
// Trees will die of as part of the scenario, so the
// `diameters` arrays for the trees may have different
// lengths. Trees that die may be replaced with other trees,
// so there will be trees that appear in the scenario at t >
// 0, so the `diameters` array may have initial elements set
// to 0.
//
// Specifying a "region" for an individual tree will override the
// scenario-level "region" value.
//
// The "years" parameter must be >= the length of the longest
// "diameters" array under "scenario_trees".
//
// Request (with bogus example parameters):
//
// POST /eco_scenario.json
//
// {
// "region": "NoEastXXX",
// "instance_id": 1,
// "years": 3
// "scenario_trees": [
// {
// "otmcode": "CACO",
// "species_id": 1,
// "region": "NoEastXXX",
// "diameters": [1, 1.3, 1.7]
// }
// ]
// }
//
// Response (with bogus example values):
//
// {
// "Years": [
// {
// "aq_nox_avoided": 0.01548490,
// "aq_nox_dep": 0.00771784,
// "aq_pm10_avoided": 0.00546863
// },
// {
// "aq_nox_avoided": 0.02548420,
// "aq_nox_dep": 0.01973722,
// "aq_pm10_avoided": 0.00676823
// },
// {
// "aq_nox_avoided": 0.05484902,
// "aq_nox_dep": 0.04774471,
// "aq_pm10_avoided": 0.00946822
// }
// ],
// "Total": {
// "aq_nox_avoided": ... ,
// "aq_nox_dep": ... ,
// "aq_pm10_avoided": ...
// }
// }
rest.HandlePOST("/eco_scenario.json", func(data *ScenarioPostData) (*Scenario, error) {
t := time.Now()

scenarioTrees := data.Scenario_trees
scenarioRegion := data.Region

instanceId, err := strconv.Atoi(data.Instance_id)

if err != nil {
return nil, err
}

if len(scenarioRegion) == 0 {
var regions []eco.Region
regions, err = db.GetRegionsForInstance(
regiongeometry, instanceId)

if err != nil {
return nil, err
}

if len(regions) == 1 {
scenarioRegion = regions[0].Code
}
}

yearTotals := make([][]float64, data.Years)
grandTotals := make([]float64, len(eco.Factors))
for i := range yearTotals {
yearTotals[i] = make([]float64, len(eco.Factors))
}

for _, tree := range scenarioTrees {
effectiveRegion := scenarioRegion
if len(tree.Region) != 0 {
effectiveRegion = tree.Region
}

factorDataForRegion, found := regiondata[effectiveRegion]
if !found {
return nil, errors.New("No data is available for the iTree region with code " + effectiveRegion)
}

itreecode, err := getItreeCode(tree.Otmcode,
tree.Species_id, effectiveRegion, instanceId)
if err != nil {
return nil, err
}

for i, diameter := range tree.Diameters {
factorSum := make([]float64, len(eco.Factors))
eco.CalcOneTree(
factorDataForRegion,
itreecode,
diameter,
factorSum)
for j, value := range factorSum {
yearTotals[i][j] = value
grandTotals[j] += value
}
}
}

// The requests are written to stdout like this:
// 2014/07/15 14:06:10 POST /eco_scenario.json
// Indenting the timing report aligns it with the http
// verb on the previous line.
fmt.Println(" ",
int64(time.Since(t)/time.Millisecond), "ms (total)")

years := make([]map[string]float64, data.Years)
for i, a := range yearTotals {
years[i] = eco.FactorArrayToMap(a)
}
return &Scenario{
Total: eco.FactorArrayToMap(grandTotals),
Years: years}, nil
})

rest.RunServer(
Expand Down