Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 8 additions & 7 deletions internal/api/handler/surfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions internal/api/handler/surfaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
2 changes: 1 addition & 1 deletion schemas/request_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Expand Down
Loading