diff --git a/docs/api.md b/docs/api.md index beefdff..87986f1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -62,7 +62,7 @@ The body also accepts an optional `surfaces` list, one entry per physical elemen - A category that is not listed keeps the archetype's default. - The caller owns the accuracy of the geometry. ignis uses the areas, U-values and orientations as given and does not check them against the archetype. - `azimuth` is degrees clockwise from North (0 North, 90 East, 180 South, 270 West). For windows it also selects which direction's solar irradiance the window counts toward, rounded to the nearest of North, East, South or West. A window with no `azimuth` is assumed to face South. -- `tilt` is degrees from horizontal: 0 is a flat skylight, 90 a vertical window. A window within 5 degrees of horizontal counts toward horizontal irradiance rather than a compass direction. city2tabula measures tilt the other way round (0 for a vertical wall, 90 for a flat roof), so convert its values with `ignis_tilt = 90 - c2t_tilt` before sending them. +- `tilt` is degrees from horizontal: 0 is a flat skylight, 90 a vertical window. A window tilted below 30 degrees counts toward horizontal irradiance rather than a compass direction. city2tabula measures tilt the other way round (0 for a vertical wall, 90 for a flat roof), so convert its values with `ignis_tilt = 90 - c2t_tilt` before sending them. ??? example "A building with five listed surfaces" ```json diff --git a/docs/openapi/openapi.yaml b/docs/openapi/openapi.yaml index 3273e06..8299f16 100644 --- a/docs/openapi/openapi.yaml +++ b/docs/openapi/openapi.yaml @@ -600,8 +600,8 @@ components: format: double description: > Degrees from horizontal, windows only: 0 is a flat skylight, 90 a - vertical window. Within 5° of 0 the window is bucketed as - Horizontal regardless of azimuth. This is the inverse of + vertical window. Below 30° the window is bucketed as Horizontal + regardless of azimuth. This is the inverse of city2tabula's surface tilt (0 = vertical wall, 90 = flat roof); a caller passing city2tabula geometry must convert first with ignis_tilt = 90 - c2t_tilt. diff --git a/internal/api/handler/surfaces.go b/internal/api/handler/surfaces.go index f897088..649b016 100644 --- a/internal/api/handler/surfaces.go +++ b/internal/api/handler/surfaces.go @@ -18,12 +18,13 @@ type Surface struct { Area float64 `json:"area"` // m² UValue float64 `json:"u_value"` // W/m²K Azimuth *float64 `json:"azimuth,omitempty"` // degrees, 0=N/90=E/180=S/270=W; windows only, defaults to South (180) - Tilt *float64 `json:"tilt,omitempty"` // degrees; windows only, ~0 = horizontal (skylight) + Tilt *float64 `json:"tilt,omitempty"` // degrees; windows only, below 30 = horizontal } -// horizontalTiltThreshold is how close Tilt must be to 0 for a window to be -// bucketed as horizontal (skylight) rather than by azimuth. -const horizontalTiltThreshold = 5.0 +// horizontalTiltThreshold is the tilt below which a window is bucketed as +// horizontal rather than by azimuth. It matches TABULA's definition of +// A_Window_Horizontal (see calc_level_01.go). +const horizontalTiltThreshold = 30.0 // aggregateSurfaces collapses an arbitrary list of physical surfaces into the // area-weighted category totals ignis's pipeline already consumes @@ -93,11 +94,11 @@ func aggregateSurfaces(surfaces []Surface, building *models.TabulaBuildingParame } // windowOrientationBucket maps a window surface to one of ignis's five -// existing solar-gain buckets (North/East/South/West/Horizontal). Tilt near -// 0 wins (skylight); otherwise azimuth rounds to the nearest cardinal. +// existing solar-gain buckets (North/East/South/West/Horizontal). Tilt below +// horizontalTiltThreshold wins; otherwise azimuth rounds to the nearest cardinal. // A window with no azimuth given defaults to South. func windowOrientationBucket(s Surface) string { - if s.Tilt != nil && math.Abs(*s.Tilt) <= horizontalTiltThreshold { + if s.Tilt != nil && math.Abs(*s.Tilt) < horizontalTiltThreshold { return "Horizontal" } if s.Azimuth == nil { diff --git a/internal/api/handler/surfaces_test.go b/internal/api/handler/surfaces_test.go index 12600c4..53ccedc 100644 --- a/internal/api/handler/surfaces_test.go +++ b/internal/api/handler/surfaces_test.go @@ -89,6 +89,27 @@ func TestAggregateSurfaces_windowOrientationBucketing(t *testing.T) { } } +func TestWindowOrientationBucket_tiltThreshold(t *testing.T) { + south := 180.0 + cases := []struct { + tilt float64 + want string + }{ + {0, "Horizontal"}, + {20, "Horizontal"}, + {29.9, "Horizontal"}, + {30, "South"}, + {90, "South"}, + } + for _, c := range cases { + tilt := c.tilt + got := windowOrientationBucket(Surface{ID: "win", Type: "window", Azimuth: &south, Tilt: &tilt}) + if got != c.want { + t.Errorf("tilt %v: bucket = %q, want %q", c.tilt, got, c.want) + } + } +} + func TestAggregateSurfaces_unknownType_returnsError(t *testing.T) { building := realisticBuilding() surfaces := []Surface{{ID: "mystery", Type: "skylight-tube", Area: 1, UValue: 1}} diff --git a/schemas/request_schema.json b/schemas/request_schema.json index 53a5c82..73c5677 100644 --- a/schemas/request_schema.json +++ b/schemas/request_schema.json @@ -123,7 +123,7 @@ }, "tilt": { "type": "number", - "description": "Degrees from horizontal, windows only: 0 is a flat skylight, 90 a vertical window. Within 5 degrees of 0 the window is bucketed as horizontal regardless of azimuth. This is the inverse of city2tabula's surface tilt (0 = vertical wall, 90 = flat roof); a caller passing city2tabula geometry must convert first with ignis_tilt = 90 - c2t_tilt." + "description": "Degrees from horizontal, windows only: 0 is a flat skylight, 90 a vertical window. Below 30 degrees the window is bucketed as horizontal regardless of azimuth. This is the inverse of city2tabula's surface tilt (0 = vertical wall, 90 = flat roof); a caller passing city2tabula geometry must convert first with ignis_tilt = 90 - c2t_tilt." } } }