Skip to content

Zoning

Automatic thermal zoning for EnergyPlus models. Splits a 2-D building footprint into thermal zones and creates all Zone, BuildingSurface:Detailed, and (optionally) Construction:AirBoundary objects needed for simulation.

Quick Start

from idfkit import new_document, create_building, ZoningScheme
from idfkit.zoning import footprint_rectangle

doc = new_document()
create_building(
    doc,
    name="Office",
    footprint=footprint_rectangle(50, 30),
    floor_to_floor=3.5,
    num_stories=3,
    zoning=ZoningScheme.CORE_PERIMETER,
)

# 3 stories × 5 zones (4 perimeter + 1 core) = 15 zones
print(len(doc["Zone"]))  # 15

Zoning Schemes

create_building supports three zoning strategies via the zoning parameter:

Scheme Zones per floor Description
BY_STOREY 1 One zone per floor (default).
CORE_PERIMETER 5 Four orientation-based perimeter zones plus an interior core zone.
CUSTOM User-defined Caller supplies named zone polygons via custom_zones.

By Storey (default)

The simplest scheme — one thermal zone per floor:

from idfkit import new_document, create_building

doc = new_document()
create_building(
    doc,
    name="Warehouse",
    footprint=[(0, 0), (40, 0), (40, 20), (0, 20)],
    floor_to_floor=4.0,
    num_stories=2,
)

print(len(doc["Zone"]))  # 2

Core-Perimeter

Splits each floor into four perimeter zones (North, East, South, West) and one interior core zone. The perimeter depth defaults to 4.57 m (15 ft) per ASHRAE 90.1 Appendix G and the DOE prototype buildings.

from idfkit import new_document, create_building, ZoningScheme
from idfkit.zoning import footprint_rectangle

doc = new_document()
create_building(
    doc,
    name="Office",
    footprint=footprint_rectangle(50, 30),
    floor_to_floor=3.5,
    num_stories=3,
    zoning=ZoningScheme.CORE_PERIMETER,
)

# 3 stories × 5 zones = 15 zones
print(len(doc["Zone"]))  # 15

You can override the perimeter depth:

from idfkit import new_document, create_building, ZoningScheme
from idfkit.zoning import footprint_rectangle

doc = new_document()
create_building(
    doc,
    name="Office",
    footprint=footprint_rectangle(50, 30),
    floor_to_floor=3.5,
    num_stories=1,
    zoning=ZoningScheme.CORE_PERIMETER,
    perimeter_depth=3.0,  # 3 m instead of the default 4.57 m
)

Note

When the footprint is too small for the requested perimeter depth (i.e. the inradius is less than 0.5 m after insetting), zoning automatically falls back to a single zone per floor.

Custom Zoning

Supply your own named zone polygons per floor using custom_zones. Each entry is a (name, polygon) tuple:

from idfkit import ZoneFootprint, ZoningScheme, create_building, new_document

doc = new_document()
create_building(
    doc,
    name="Lab",
    footprint=[(0, 0), (30, 0), (30, 20), (0, 20)],
    floor_to_floor=3.5,
    num_stories=1,
    zoning=ZoningScheme.CUSTOM,
    custom_zones=[
        ZoneFootprint("Wet Lab", [(0, 0), (15, 0), (15, 20), (0, 20)]),
        ZoneFootprint("Dry Lab", [(15, 0), (30, 0), (30, 20), (15, 20)]),
    ],
)

print(len(doc["Zone"]))  # 2

Air Boundaries

Set air_boundary=True to apply Construction:AirBoundary to all inter-zone walls. This is useful for open-plan spaces where zone boundaries are notional rather than physical:

from idfkit import new_document, create_building, ZoningScheme
from idfkit.zoning import footprint_rectangle

doc = new_document()
create_building(
    doc,
    name="Open Office",
    footprint=footprint_rectangle(50, 30),
    floor_to_floor=3.5,
    num_stories=1,
    zoning=ZoningScheme.CORE_PERIMETER,
    air_boundary=True,
)

# A Construction:AirBoundary object is created automatically
print(len(doc["Construction:AirBoundary"]))  # 1

Multi-Story Boundary Conditions

For multi-story buildings, inter-story floors and ceilings are automatically linked with Surface boundary conditions:

Story Floor BC Ceiling BC
Ground floor Ground Surface (story above)
Mid floors Surface (story below) Surface (story above)
Top floor Surface (story below) Outdoors (Roof)

Footprint Helpers

Pre-built footprint generators for common commercial building shapes. All return a list of (x, y) tuples in counter-clockwise order.

footprint_rectangle

from idfkit.zoning import footprint_rectangle

fp = footprint_rectangle(50, 30)
# [(0, 0), (50, 0), (50, 30), (0, 30)]

footprint_l_shape

┌────────┐
│  wing  │
│        │
├────────┴──────────┐
│      base         │
└───────────────────┘
from idfkit.zoning import footprint_l_shape

fp = footprint_l_shape(width=40, depth=10, wing_width=15, wing_depth=20)

footprint_u_shape

┌──────┐    ┌──────┐
│      │    │      │
│      └────┘      │
│                  │
└──────────────────┘
from idfkit.zoning import footprint_u_shape

fp = footprint_u_shape(width=40, depth=30, courtyard_width=20, courtyard_depth=15)

footprint_t_shape

┌──────────────────────┐
│       top bar        │
└───┐              ┌───┘
    │    base      │
    └──────────────┘
from idfkit.zoning import footprint_t_shape

fp = footprint_t_shape(base_width=20, base_depth=15, top_width=40, top_depth=10)

footprint_h_shape

┌──────┐    ┌──────┐
│      └────┘      │
│     connector    │
│      ┌────┐      │
└──────┘    └──────┘
from idfkit.zoning import footprint_h_shape

fp = footprint_h_shape(width=40, depth=30, courtyard_width=20, courtyard_depth=10)

footprint_courtyard

┌──────────────────┐
│  ┌────────────┐  │
│  │  courtyard │  │
│  └────────────┘  │
└──────────────────┘
from idfkit.zoning import footprint_courtyard

fp = footprint_courtyard(outer_width=50, outer_depth=40, inner_width=30, inner_depth=20)

Using Footprint Helpers with create_building

All footprint helpers plug directly into create_building:

from idfkit import new_document, create_building, ZoningScheme
from idfkit.zoning import footprint_l_shape

doc = new_document()
create_building(
    doc,
    name="L-Wing",
    footprint=footprint_l_shape(40, 10, 15, 20),
    floor_to_floor=3.5,
    num_stories=2,
    zoning=ZoningScheme.CORE_PERIMETER,
)

ZonedBlock (Describe-then-Apply)

ZonedBlock is a frozen dataclass alternative that validates all parameters up front. Call build() to realise the geometry. This describe-then-apply pattern lets you inspect computed properties before committing to a document.

from idfkit import new_document, ZonedBlock, ZoningScheme
from idfkit.zoning import footprint_rectangle

block = ZonedBlock(
    name="Office",
    footprint=footprint_rectangle(50, 30),
    floor_to_floor=3.5,
    num_stories=3,
    zoning=ZoningScheme.CORE_PERIMETER,
)

print(f"Building height: {block.height} m")              # 10.5
print(f"Floor area: {block.floor_area} m²")               # 1500.0
print(f"Total floor area: {block.total_floor_area} m²")  # 4500.0

doc = new_document()
objects = block.build(doc)
print(len(objects))  # all created Zone + BuildingSurface:Detailed objects

API Reference

Automatic thermal zoning for EnergyPlus models.

Splits a 2-D building footprint into thermal zones using one of several standard schemes and creates all Zone, BuildingSurface:Detailed, and (optionally) Construction:AirBoundary objects needed for simulation.

Three zoning schemes are provided:

  • by_storey - one zone per floor.
  • core_perimeter - four orientation-based perimeter zones plus an interior core zone per floor. Perimeter depth defaults to 4.57 m (15 ft) per ASHRAE 90.1 Appendix G and the DOE prototype buildings.
  • custom - the caller supplies named polygons for each floor.

Footprint helpers for common commercial shapes (rectangle, L, U, T, H, courtyard) are included so users never have to compute vertices by hand.

Examples:

from idfkit import new_document
from idfkit.zoning import (
    ZoningScheme,
    create_building,
    footprint_rectangle,
)

doc = new_document()
zones = create_building(
    doc,
    name="Office",
    footprint=footprint_rectangle(50, 30),
    floor_to_floor=3.5,
    num_stories=3,
    zoning=ZoningScheme.CORE_PERIMETER,
    perimeter_depth=4.57,
)

ZoneFootprint dataclass

Named 2-D polygon for one thermal zone on one floor.

Source code in src/idfkit/zoning.py
@dataclass(frozen=True)
class ZoneFootprint:
    """Named 2-D polygon for one thermal zone on one floor."""

    name_suffix: str  # e.g. "Core", "Perimeter_South"
    polygon: list[tuple[float, float]]

ZonedBlock dataclass

Describes a building block with a zoning strategy.

This is a pure data object. Call build() to realise the geometry in an IDFDocument.

Attributes:

Name Type Description
name str

Base name for zones and surfaces.

footprint Sequence[tuple[float, float]]

2-D footprint as (x, y) tuples (CCW).

floor_to_floor float

Floor-to-floor height in metres.

num_stories int

Number of above-ground stories.

zoning ZoningScheme

Zoning strategy.

perimeter_depth float

Perimeter zone depth (metres), used only when zoning is CORE_PERIMETER.

custom_zones list[ZoneFootprint] | None

Per-floor named zone polygons, used only when zoning is CUSTOM.

air_boundary bool

Whether to use Construction:AirBoundary between inter-zone walls.

Source code in src/idfkit/zoning.py
@dataclass(frozen=True)
class ZonedBlock:
    """Describes a building block with a zoning strategy.

    This is a pure data object.  Call `build()` to realise the
    geometry in an [IDFDocument][idfkit.document.IDFDocument].

    Attributes:
        name: Base name for zones and surfaces.
        footprint: 2-D footprint as ``(x, y)`` tuples (CCW).
        floor_to_floor: Floor-to-floor height in metres.
        num_stories: Number of above-ground stories.
        zoning: Zoning strategy.
        perimeter_depth: Perimeter zone depth (metres), used only
            when ``zoning`` is ``CORE_PERIMETER``.
        custom_zones: Per-floor named zone polygons, used only when
            ``zoning`` is ``CUSTOM``.
        air_boundary: Whether to use ``Construction:AirBoundary``
            between inter-zone walls.
    """

    name: str
    footprint: Sequence[tuple[float, float]]
    floor_to_floor: float
    num_stories: int = 1
    zoning: ZoningScheme = ZoningScheme.BY_STOREY
    perimeter_depth: float = ASHRAE_PERIMETER_DEPTH
    custom_zones: list[ZoneFootprint] | None = None
    air_boundary: bool = False

    def __post_init__(self) -> None:
        if len(self.footprint) < 3:
            msg = f"Footprint must have at least 3 vertices, got {len(self.footprint)}"
            raise ValueError(msg)
        if self.floor_to_floor <= 0:
            msg = f"floor_to_floor must be positive, got {self.floor_to_floor}"
            raise ValueError(msg)
        if self.num_stories < 1:
            msg = f"num_stories must be >= 1, got {self.num_stories}"
            raise ValueError(msg)
        if self.perimeter_depth <= 0:
            msg = f"perimeter_depth must be positive, got {self.perimeter_depth}"
            raise ValueError(msg)
        if self.zoning == ZoningScheme.CUSTOM and not self.custom_zones:
            msg = "custom_zones is required when zoning is CUSTOM"
            raise ValueError(msg)

    @property
    def height(self) -> float:
        """Total building height in metres."""
        return self.floor_to_floor * self.num_stories

    @property
    def floor_area(self) -> float:
        """Single-floor footprint area in square metres."""
        return abs(_polygon_area_signed(list(self.footprint)))

    @property
    def total_floor_area(self) -> float:
        """Total floor area across all stories in square metres."""
        return self.floor_area * self.num_stories

    def build(self, doc: IDFDocument) -> list[IDFObject]:
        """Realise the zoned geometry in the document.

        Returns:
            All created [IDFObject][idfkit.objects.IDFObject] instances.
        """
        fp = list(self.footprint)

        # Determine zone layout per floor
        if self.zoning == ZoningScheme.CORE_PERIMETER:
            zone_footprints = _split_core_perimeter(fp, self.perimeter_depth)
        elif self.zoning == ZoningScheme.CUSTOM:
            zone_footprints = self.custom_zones or [ZoneFootprint("Whole", fp)]
        else:  # BY_STOREY
            zone_footprints = [ZoneFootprint("Whole", fp)]

        # Build story specs
        story_specs: list[_StorySpec] = []
        for i in range(self.num_stories):
            z_bot = i * self.floor_to_floor
            z_top = (i + 1) * self.floor_to_floor
            story_specs.append(_StorySpec(i + 1, z_bot, z_top, zone_footprints))

        # Create surfaces for each story
        created: list[IDFObject] = []
        for spec in story_specs:
            created.extend(
                _build_story_surfaces(
                    doc,
                    self.name,
                    spec,
                    self.num_stories,
                    all_story_specs=story_specs,
                    air_boundary=self.air_boundary,
                )
            )
        return created

floor_area property

Single-floor footprint area in square metres.

height property

Total building height in metres.

total_floor_area property

Total floor area across all stories in square metres.

build(doc)

Realise the zoned geometry in the document.

Returns:

Type Description
list[IDFObject]

All created IDFObject instances.

Source code in src/idfkit/zoning.py
def build(self, doc: IDFDocument) -> list[IDFObject]:
    """Realise the zoned geometry in the document.

    Returns:
        All created [IDFObject][idfkit.objects.IDFObject] instances.
    """
    fp = list(self.footprint)

    # Determine zone layout per floor
    if self.zoning == ZoningScheme.CORE_PERIMETER:
        zone_footprints = _split_core_perimeter(fp, self.perimeter_depth)
    elif self.zoning == ZoningScheme.CUSTOM:
        zone_footprints = self.custom_zones or [ZoneFootprint("Whole", fp)]
    else:  # BY_STOREY
        zone_footprints = [ZoneFootprint("Whole", fp)]

    # Build story specs
    story_specs: list[_StorySpec] = []
    for i in range(self.num_stories):
        z_bot = i * self.floor_to_floor
        z_top = (i + 1) * self.floor_to_floor
        story_specs.append(_StorySpec(i + 1, z_bot, z_top, zone_footprints))

    # Create surfaces for each story
    created: list[IDFObject] = []
    for spec in story_specs:
        created.extend(
            _build_story_surfaces(
                doc,
                self.name,
                spec,
                self.num_stories,
                all_story_specs=story_specs,
                air_boundary=self.air_boundary,
            )
        )
    return created

ZoningScheme

Bases: Enum

Thermal zoning strategy.

Attributes:

Name Type Description
BY_STOREY

One zone per floor.

CORE_PERIMETER

Core + 4 perimeter zones per floor.

CUSTOM

User-supplied zone polygons per floor.

Source code in src/idfkit/zoning.py
class ZoningScheme(enum.Enum):
    """Thermal zoning strategy.

    Attributes:
        BY_STOREY: One zone per floor.
        CORE_PERIMETER: Core + 4 perimeter zones per floor.
        CUSTOM: User-supplied zone polygons per floor.
    """

    BY_STOREY = "by_storey"
    CORE_PERIMETER = "core_perimeter"
    CUSTOM = "custom"

create_building(doc, name, footprint, floor_to_floor, num_stories=1, *, zoning=ZoningScheme.BY_STOREY, perimeter_depth=ASHRAE_PERIMETER_DEPTH, custom_zones=None, air_boundary=False)

Create a fully-zoned building in one call.

This is the primary entry point for the zoning module. It combines footprint definition, zoning strategy, and multi-story extrusion into a single function call.

Parameters:

Name Type Description Default
doc IDFDocument

The document to add objects to.

required
name str

Base name for zones and surfaces (e.g. "Office").

required
footprint Sequence[tuple[float, float]]

2-D footprint as (x, y) tuples (CCW order).

required
floor_to_floor float

Floor-to-floor height in metres.

required
num_stories int

Number of above-ground stories.

1
zoning ZoningScheme

Zoning strategy (default: one zone per floor).

BY_STOREY
perimeter_depth float

Depth of perimeter zones in metres. Only used when zoning is CORE_PERIMETER. Defaults to 4.57 m (ASHRAE 90.1 / DOE prototypes).

ASHRAE_PERIMETER_DEPTH
custom_zones list[ZoneFootprint] | None

Named zone polygons, required when zoning is CUSTOM.

None
air_boundary bool

If True, apply Construction:AirBoundary to all inter-zone walls (for open-plan spaces).

False

Returns:

Type Description
list[IDFObject]

All created IDFObject instances.

Examples:

Core-perimeter zoning for a 3-story office:

```python
from idfkit import new_document
from idfkit.zoning import (
    ZoningScheme,
    create_building,
    footprint_rectangle,
)

doc = new_document()
create_building(
    doc,
    name="Office",
    footprint=footprint_rectangle(50, 30),
    floor_to_floor=3.5,
    num_stories=3,
    zoning=ZoningScheme.CORE_PERIMETER,
)
```
Source code in src/idfkit/zoning.py
def create_building(
    doc: IDFDocument,
    name: str,
    footprint: Sequence[tuple[float, float]],
    floor_to_floor: float,
    num_stories: int = 1,
    *,
    zoning: ZoningScheme = ZoningScheme.BY_STOREY,
    perimeter_depth: float = ASHRAE_PERIMETER_DEPTH,
    custom_zones: list[ZoneFootprint] | None = None,
    air_boundary: bool = False,
) -> list[IDFObject]:
    """Create a fully-zoned building in one call.

    This is the primary entry point for the zoning module.  It combines
    footprint definition, zoning strategy, and multi-story extrusion into
    a single function call.

    Args:
        doc: The document to add objects to.
        name: Base name for zones and surfaces (e.g. ``"Office"``).
        footprint: 2-D footprint as ``(x, y)`` tuples (CCW order).
        floor_to_floor: Floor-to-floor height in metres.
        num_stories: Number of above-ground stories.
        zoning: Zoning strategy (default: one zone per floor).
        perimeter_depth: Depth of perimeter zones in metres.
            Only used when ``zoning`` is ``CORE_PERIMETER``.
            Defaults to 4.57 m (ASHRAE 90.1 / DOE prototypes).
        custom_zones: Named zone polygons, required when ``zoning``
            is ``CUSTOM``.
        air_boundary: If ``True``, apply ``Construction:AirBoundary``
            to all inter-zone walls (for open-plan spaces).

    Returns:
        All created [IDFObject][idfkit.objects.IDFObject] instances.

    Examples:
        Core-perimeter zoning for a 3-story office:

            ```python
            from idfkit import new_document
            from idfkit.zoning import (
                ZoningScheme,
                create_building,
                footprint_rectangle,
            )

            doc = new_document()
            create_building(
                doc,
                name="Office",
                footprint=footprint_rectangle(50, 30),
                floor_to_floor=3.5,
                num_stories=3,
                zoning=ZoningScheme.CORE_PERIMETER,
            )
            ```
    """
    block = ZonedBlock(
        name=name,
        footprint=footprint,
        floor_to_floor=floor_to_floor,
        num_stories=num_stories,
        zoning=zoning,
        perimeter_depth=perimeter_depth,
        custom_zones=custom_zones,
        air_boundary=air_boundary,
    )
    return block.build(doc)

footprint_courtyard(outer_width, outer_depth, inner_width, inner_depth, origin=(0.0, 0.0))

Return a courtyard (donut) footprint as a single slit polygon.

The polygon traces the outer boundary counter-clockwise, steps into the inner courtyard through a slit at the bottom-right corner, traces the courtyard clockwise, and returns. This is a valid simple polygon that EnergyPlus can handle.

```text
┌──────────────────┐
│  ┌────────────┐  │
│  │  courtyard │  │
│  └────────────┘  │
└──────────────────┘
```

Parameters:

Name Type Description Default
outer_width float

Outer bounding box width (X).

required
outer_depth float

Outer bounding box depth (Y).

required
inner_width float

Courtyard width (X), must be < outer_width.

required
inner_depth float

Courtyard depth (Y), must be < outer_depth.

required
origin tuple[float, float]

(x, y) of the lower-left corner.

(0.0, 0.0)
Source code in src/idfkit/zoning.py
def footprint_courtyard(
    outer_width: float,
    outer_depth: float,
    inner_width: float,
    inner_depth: float,
    origin: tuple[float, float] = (0.0, 0.0),
) -> list[tuple[float, float]]:
    """Return a courtyard (donut) footprint as a single slit polygon.

    The polygon traces the outer boundary counter-clockwise, steps into
    the inner courtyard through a slit at the bottom-right corner, traces
    the courtyard clockwise, and returns.  This is a valid simple polygon
    that EnergyPlus can handle.

        ```text
        ┌──────────────────┐
        │  ┌────────────┐  │
        │  │  courtyard │  │
        │  └────────────┘  │
        └──────────────────┘
        ```

    Args:
        outer_width: Outer bounding box width (X).
        outer_depth: Outer bounding box depth (Y).
        inner_width: Courtyard width (X), must be < *outer_width*.
        inner_depth: Courtyard depth (Y), must be < *outer_depth*.
        origin: ``(x, y)`` of the lower-left corner.
    """
    if inner_width >= outer_width:
        msg = f"inner_width ({inner_width}) must be < outer_width ({outer_width})"
        raise ValueError(msg)
    if inner_depth >= outer_depth:
        msg = f"inner_depth ({inner_depth}) must be < outer_depth ({outer_depth})"
        raise ValueError(msg)
    x, y = origin
    margin_x = (outer_width - inner_width) / 2
    margin_y = (outer_depth - inner_depth) / 2
    # Outer CCW
    ix0 = x + margin_x
    iy0 = y + margin_y
    ix1 = ix0 + inner_width
    iy1 = iy0 + inner_depth
    return [
        # Outer rectangle (CCW)
        (x, y),
        (x + outer_width, y),
        (x + outer_width, y + outer_depth),
        (x, y + outer_depth),
        # Slit down to inner (from outer top-left back down to inner)
        (x, y + margin_y),  # slit entry
        # Inner rectangle (CW to cut a hole)
        (ix0, iy0),
        (ix0, iy1),
        (ix1, iy1),
        (ix1, iy0),
        # Slit back out
        (x, y + margin_y),  # slit exit (same point, degenerate edge)
    ]

footprint_h_shape(width, depth, courtyard_width, courtyard_depth, origin=(0.0, 0.0))

Return an H-shaped footprint (counter-clockwise).

Two symmetrical courtyards are cut from the left and right sides of the bounding rectangle.

```text
┌──────┐    ┌──────┐
│      └────┘      │
│     connector    │
│      ┌────┐      │
└──────┘    └──────┘
```

Parameters:

Name Type Description Default
width float

Overall width (X).

required
depth float

Overall depth (Y).

required
courtyard_width float

Width of each courtyard notch (X).

required
courtyard_depth float

Depth of each courtyard notch (Y).

required
origin tuple[float, float]

(x, y) of the lower-left corner.

(0.0, 0.0)
Source code in src/idfkit/zoning.py
def footprint_h_shape(
    width: float,
    depth: float,
    courtyard_width: float,
    courtyard_depth: float,
    origin: tuple[float, float] = (0.0, 0.0),
) -> list[tuple[float, float]]:
    """Return an H-shaped footprint (counter-clockwise).

    Two symmetrical courtyards are cut from the left and right sides of
    the bounding rectangle.

        ```text
        ┌──────┐    ┌──────┐
        │      └────┘      │
        │     connector    │
        │      ┌────┐      │
        └──────┘    └──────┘
        ```

    Args:
        width: Overall width (X).
        depth: Overall depth (Y).
        courtyard_width: Width of each courtyard notch (X).
        courtyard_depth: Depth of each courtyard notch (Y).
        origin: ``(x, y)`` of the lower-left corner.
    """
    if courtyard_width >= width:
        msg = f"courtyard_width ({courtyard_width}) must be < width ({width})"
        raise ValueError(msg)
    if 2 * courtyard_depth >= depth:
        msg = f"2 * courtyard_depth ({2 * courtyard_depth}) must be < depth ({depth})"
        raise ValueError(msg)
    x, y = origin
    w2 = (width - courtyard_width) / 2
    cy_bot_top = y + courtyard_depth
    cy_top_bot = y + depth - courtyard_depth
    cx_left = x + w2
    cx_right = x + w2 + courtyard_width
    return [
        (x, y),
        (cx_left, y),
        (cx_left, cy_bot_top),
        (cx_right, cy_bot_top),
        (cx_right, y),
        (x + width, y),
        (x + width, y + depth),
        (cx_right, y + depth),
        (cx_right, cy_top_bot),
        (cx_left, cy_top_bot),
        (cx_left, y + depth),
        (x, y + depth),
    ]

footprint_l_shape(width, depth, wing_width, wing_depth, origin=(0.0, 0.0))

Return an L-shaped footprint (counter-clockwise).

The base rectangle runs from the origin along width (X) and depth (Y). A shorter wing extends upward from the left side with dimensions wing_width x wing_depth.

```text
┌────────┐
│  wing  │
│        │
├────────┴──────────┐
│      base         │
└───────────────────┘
```

Parameters:

Name Type Description Default
width float

Base width (X).

required
depth float

Base depth (Y).

required
wing_width float

Wing width (X), must be <= width.

required
wing_depth float

Wing depth (Y), added above the base.

required
origin tuple[float, float]

(x, y) of the lower-left corner.

(0.0, 0.0)
Source code in src/idfkit/zoning.py
def footprint_l_shape(
    width: float,
    depth: float,
    wing_width: float,
    wing_depth: float,
    origin: tuple[float, float] = (0.0, 0.0),
) -> list[tuple[float, float]]:
    """Return an L-shaped footprint (counter-clockwise).

    The *base* rectangle runs from the origin along ``width`` (X) and
    ``depth`` (Y).  A shorter wing extends upward from the left side
    with dimensions ``wing_width`` x ``wing_depth``.

        ```text
        ┌────────┐
        │  wing  │
        │        │
        ├────────┴──────────┐
        │      base         │
        └───────────────────┘
        ```

    Args:
        width: Base width (X).
        depth: Base depth (Y).
        wing_width: Wing width (X), must be <= *width*.
        wing_depth: Wing depth (Y), added above the base.
        origin: ``(x, y)`` of the lower-left corner.
    """
    if wing_width > width:
        msg = f"wing_width ({wing_width}) must be <= width ({width})"
        raise ValueError(msg)
    x, y = origin
    return [
        (x, y),
        (x + width, y),
        (x + width, y + depth),
        (x + wing_width, y + depth),
        (x + wing_width, y + depth + wing_depth),
        (x, y + depth + wing_depth),
    ]

footprint_rectangle(width, depth, origin=(0.0, 0.0))

Return a rectangular footprint (counter-clockwise).

Parameters:

Name Type Description Default
width float

Dimension along the X axis (metres).

required
depth float

Dimension along the Y axis (metres).

required
origin tuple[float, float]

(x, y) of the lower-left corner.

(0.0, 0.0)
Source code in src/idfkit/zoning.py
def footprint_rectangle(
    width: float,
    depth: float,
    origin: tuple[float, float] = (0.0, 0.0),
) -> list[tuple[float, float]]:
    """Return a rectangular footprint (counter-clockwise).

    Args:
        width: Dimension along the X axis (metres).
        depth: Dimension along the Y axis (metres).
        origin: ``(x, y)`` of the lower-left corner.
    """
    x, y = origin
    return [(x, y), (x + width, y), (x + width, y + depth), (x, y + depth)]

footprint_t_shape(base_width, base_depth, top_width, top_depth, origin=(0.0, 0.0))

Return a T-shaped footprint (counter-clockwise).

A narrower base rectangle is centred below a wider top bar.

```text
┌──────────────────────┐
│       top bar        │
└───┐              ┌───┘
    │    base      │
    └──────────────┘
```

Parameters:

Name Type Description Default
base_width float

Width of the stem (X).

required
base_depth float

Depth of the stem (Y).

required
top_width float

Width of the top bar (X), must be >= base_width.

required
top_depth float

Depth of the top bar (Y).

required
origin tuple[float, float]

(x, y) of the lower-left corner of the stem.

(0.0, 0.0)
Source code in src/idfkit/zoning.py
def footprint_t_shape(
    base_width: float,
    base_depth: float,
    top_width: float,
    top_depth: float,
    origin: tuple[float, float] = (0.0, 0.0),
) -> list[tuple[float, float]]:
    """Return a T-shaped footprint (counter-clockwise).

    A narrower base rectangle is centred below a wider top bar.

        ```text
        ┌──────────────────────┐
        │       top bar        │
        └───┐              ┌───┘
            │    base      │
            └──────────────┘
        ```

    Args:
        base_width: Width of the stem (X).
        base_depth: Depth of the stem (Y).
        top_width: Width of the top bar (X), must be >= *base_width*.
        top_depth: Depth of the top bar (Y).
        origin: ``(x, y)`` of the lower-left corner of the stem.
    """
    if top_width < base_width:
        msg = f"top_width ({top_width}) must be >= base_width ({base_width})"
        raise ValueError(msg)
    x, y = origin
    overhang = (top_width - base_width) / 2
    return [
        (x, y),
        (x + base_width, y),
        (x + base_width, y + base_depth),
        (x + base_width + overhang, y + base_depth),
        (x + base_width + overhang, y + base_depth + top_depth),
        (x - overhang, y + base_depth + top_depth),
        (x - overhang, y + base_depth),
        (x, y + base_depth),
    ]

footprint_u_shape(width, depth, courtyard_width, courtyard_depth, origin=(0.0, 0.0))

Return a U-shaped footprint (counter-clockwise).

The overall bounding box is width x depth. A rectangular courtyard is cut from the top centre of the footprint.

```text
┌──────┐    ┌──────┐
│      │    │      │
│      └────┘      │
│                  │
└──────────────────┘
```

Parameters:

Name Type Description Default
width float

Overall width (X).

required
depth float

Overall depth (Y).

required
courtyard_width float

Width of the courtyard opening (X).

required
courtyard_depth float

Depth of the courtyard from the top edge (Y).

required
origin tuple[float, float]

(x, y) of the lower-left corner.

(0.0, 0.0)
Source code in src/idfkit/zoning.py
def footprint_u_shape(
    width: float,
    depth: float,
    courtyard_width: float,
    courtyard_depth: float,
    origin: tuple[float, float] = (0.0, 0.0),
) -> list[tuple[float, float]]:
    """Return a U-shaped footprint (counter-clockwise).

    The overall bounding box is ``width`` x ``depth``.  A rectangular
    courtyard is cut from the top centre of the footprint.

        ```text
        ┌──────┐    ┌──────┐
        │      │    │      │
        │      └────┘      │
        │                  │
        └──────────────────┘
        ```

    Args:
        width: Overall width (X).
        depth: Overall depth (Y).
        courtyard_width: Width of the courtyard opening (X).
        courtyard_depth: Depth of the courtyard from the top edge (Y).
        origin: ``(x, y)`` of the lower-left corner.
    """
    if courtyard_width >= width:
        msg = f"courtyard_width ({courtyard_width}) must be < width ({width})"
        raise ValueError(msg)
    if courtyard_depth >= depth:
        msg = f"courtyard_depth ({courtyard_depth}) must be < depth ({depth})"
        raise ValueError(msg)
    x, y = origin
    cx_left = x + (width - courtyard_width) / 2
    cx_right = cx_left + courtyard_width
    cy_bottom = y + depth - courtyard_depth
    return [
        (x, y),
        (x + width, y),
        (x + width, y + depth),
        (cx_right, y + depth),
        (cx_right, cy_bottom),
        (cx_left, cy_bottom),
        (cx_left, y + depth),
        (x, y + depth),
    ]

See Also

  • Geometry Builders -- Shading blocks and utility functions (bounding_box, scale_building, set_default_constructions)
  • Geometry -- Lower-level 3D primitives, coordinate transforms, and surface intersection
  • Visualization -- 3D rendering of building geometry