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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ All notable changes to this project are documented in this file.
- `COMcheckClient.area_for_renewable` — calculate `largestThreeFloorArea` and `roofAreaForRenewable` via `POST /renewable/area-for-renewable` and write them onto `project.renewable`
- `COMcheckClient.renewable_capacity` — calculate required/proposed renewable capacity and off-site renewable energy via `POST /renewable/renewable-capacity` and write them onto `project.renewable`
- `COMCheckApiService.area_for_renewable` and `COMCheckApiService.renewable_capacity` service methods
- `COMcheckClient.set_renewable_exception` — set the renewable exception and refresh renewable capacity via `POST /renewable/renewable-capacity` in one call; the project is only changed if the API call succeeds
- `update_renewable_in_project` accepts `largestThreeFloorArea` when `numberOfFloors` is greater than 3, and rejects values larger than the total building area floor area (sum of `lighting.wholeBldgUse[].floorArea`). With 3 floors or fewer it stays API-calculated and is rejected
- `examples/client/renewable.py` example
- `RenewableSystem` and `OffsiteRenewableProcurement` added to `MODEL_TO_ID_INFO` with auto-generated descriptions
- Mechanical operations module (`project_mechanical_operations`) for managing HVAC systems, fan systems, HVAC plants, and water heaters
Expand Down
31 changes: 26 additions & 5 deletions comcheck_api/ai/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ Triggers:
`proposedOffsiteRenewableEnergy`. Both mutate and return the same
project, like `update_uvalues`. Both send the full project; the energy
code comes from `project.control.code`.
- **Changing the renewable exception changes the capacity**: use
`client.set_renewable_exception(project, exception)`, which sets the
exception and calls `renewable_capacity` in one step (the project is only
changed if the API call succeeds). If you use
`rn_ops.set_renewable_exception_in_project` directly, call
`client.renewable_capacity(project)` right after.
- **`largestThreeFloorArea` depends on `numberOfFloors`**: with 3 floors or
fewer it is the total building area floor area and `area_for_renewable`
calculates it (setting it via `update_renewable_in_project` raises). With
more than 3 floors **ask the user for it** and set it with
`update_renewable_in_project(project, {"numberOfFloors": n,
"largestThreeFloorArea": area})`; it can't exceed the sum of
`lighting.wholeBldgUse[].floorArea`, or the op raises `ValueError`.
- **Energy credits are calculated server-side**:
`calculate_energy_credits(project)` returns an `EnergyCreditResult`
(`energyCredits[]` per building area and credit type, `proposed`,
Expand Down Expand Up @@ -406,9 +419,10 @@ library, so it returns metadata and lets the caller decide

### Adding renewable energy systems

Renewable requirements only apply under some energy codes; the operations
don't check `control.code`, so ask the user whether the project needs
renewables rather than deciding from the code. Exception
Renewable requirements only apply under some energy codes, but the
operations don't check `control.code` and you don't need to either. The API
decides whether the code uses renewable data, and renewable inputs on other
codes do no harm. Exception
and on-site systems coexist — setting an exception doesn't preclude adding
renewable systems. **Confirm with the user before adding off-site
procurement** (not always present on projects).
Expand All @@ -426,11 +440,18 @@ from comcheck_api.types import (
OffsiteRenewableProcurementType,
)

# 1. Set a renewable exception (coexists with systems)
project = rn_ops.set_renewable_exception_in_project(
# 1. Set a renewable exception (coexists with systems). This also refreshes
# the renewable capacity from the API.
project = client.set_renewable_exception(
project, RenewableExceptionOptions.RENEWABLE_ONSITE_EXCEPTION_NONE
)

# 1b. More than 3 floors: largestThreeFloorArea comes from the user and can't
# exceed the total building area floor area
# project = rn_ops.update_renewable_in_project(
# project, {"numberOfFloors": 5, "largestThreeFloorArea": 30000.0}
# )

# 2. Add an on-site renewable system
renewable_system = get_default_renewable_system_template()
renewable_system.description = "Rooftop Solar"
Expand Down
9 changes: 5 additions & 4 deletions comcheck_api/ai/skill/reference/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,16 @@ from comcheck_api import project_renewable_operations as rn_ops

Renewable energy is managed at the project level through the
`ComBuilding.renewable` singleton. Renewable requirements only apply under
some energy codes; the operations don't check `control.code` — ask the user
whether the project needs renewables. Exception and on-site systems coexist — setting an
some energy codes, but the operations don't check `control.code` and you
don't need to either: the API decides whether the code uses renewable data,
and renewable inputs on other codes do no harm. Exception and on-site systems coexist — setting an
exception does not preclude adding renewable systems. **Confirm with the user
before adding off-site procurement** (not always present on projects).

| Function | Purpose |
|---|---|
| `set_renewable_exception_in_project(project, renewable_exception)` | Set `renewable.renewableException`. Raises `ValueError` for `NoneType_None`, `TypeError` for non-enum values. |
| `update_renewable_in_project(project, updates)` | Update scalar fields on the renewable singleton. Raises `ValueError` for engine-calculated, API-calculated, or list fields. |
| `set_renewable_exception_in_project(project, renewable_exception)` | Set `renewable.renewableException`. Raises `ValueError` for `NoneType_None`, `TypeError` for non-enum values. Doesn't refresh capacity; prefer `client.set_renewable_exception(project, renewable_exception)`, which also calls the renewable-capacity API. |
| `update_renewable_in_project(project, updates)` | Update `numberOfFloors` and, with more than 3 floors, `largestThreeFloorArea` (must be ≤ the sum of `lighting.wholeBldgUse[].floorArea`). Raises `ValueError` for `largestThreeFloorArea` with 3 floors or fewer (calculated by `area_for_renewable`), and for engine-calculated, API-calculated, or list fields. |
| `add_renewable_system_to_project(project, new_renewable_system)` | Add an on-site `RenewableSystem`. |
| `update_renewable_system_in_project(project, renewable_system_description, updates)` | Update a `RenewableSystem` by its `description`. |
| `remove_renewable_system_from_project(project, renewable_system_description)` | Remove a `RenewableSystem` by its `description`. |
Expand Down
36 changes: 36 additions & 0 deletions comcheck_api/client/comcheck_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from comcheck_api.project_operations.project_credit_operations import (
add_energy_credit_to_project,
)
from comcheck_api.project_operations.project_renewable_operations import (
set_renewable_exception_in_project,
)
from comcheck_api.types.api_types import (
EnergyCreditResult,
WaterHeaterEfficiency,
Expand All @@ -30,6 +33,7 @@
from comcheck_api.types.core_types import (
ComBuilding,
EfficiencyPackage,
RenewableExceptionOptions,
ServiceWaterHeatingSystem,
HVACSystem,
HVACPlant,
Expand Down Expand Up @@ -379,6 +383,38 @@ def renewable_capacity(self, project: ComBuilding) -> ComBuilding:
)
return project

def set_renewable_exception(
self,
project: ComBuilding,
renewable_exception: RenewableExceptionOptions,
) -> ComBuilding:
"""Set the renewable exception and refresh renewable capacity in place.

The capacity fields depend on the exception, so this applies
:func:`~comcheck_api.project_operations.project_renewable_operations.set_renewable_exception_in_project`
and then calls :meth:`renewable_capacity`. ``project`` is only changed
if the API call succeeds.

Args:
project: The project to update.
renewable_exception: A :class:`~comcheck_api.types.core_types.RenewableExceptionOptions`
value. Must not be ``NoneType_None``.

Returns:
The same ``project`` instance, with the exception set and renewable
capacity updated.

Raises:
TypeError: If renewable_exception is not a RenewableExceptionOptions member.
ValueError: If renewable_exception is NoneType_None.
"""
updated_project = set_renewable_exception_in_project(
project, renewable_exception
)
self.renewable_capacity(updated_project)
project.renewable = updated_project.renewable
return project

def calculate_energy_credits(self, project: ComBuilding) -> EnergyCreditResult:
"""Calculate the proposed and required energy credits of a project.

Expand Down
79 changes: 70 additions & 9 deletions comcheck_api/project_operations/project_renewable_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,23 @@
Energy codes
------------
Renewable requirements only apply under some energy codes (and state codes
based on them). These operations don't check ``control.code`` — whether the
project needs renewable data is up to you.
based on them). These operations don't check ``control.code``. The COMcheck Web
API decides whether the project's code uses renewable data, and renewable
inputs on a project whose code doesn't use them do no harm.

Largest three floor area
------------------------
With 3 floors or fewer, ``largestThreeFloorArea`` is the sum of the building
areas' ``floorArea`` and is calculated by
:meth:`~comcheck_api.client.COMcheckClient.area_for_renewable`. With more than
3 floors you provide it through :func:`update_renewable_in_project`, and it
can't exceed the total building area floor area.

Renewable capacity
------------------
The capacity fields depend on the renewable exception. Use
:meth:`~comcheck_api.client.COMcheckClient.set_renewable_exception` to set the
exception and refresh the capacity from the API in one call.

Exception and systems coexistence
----------------------------------
Expand Down Expand Up @@ -54,11 +69,14 @@
"proposedCapacity",
"requiredOffsiteRenewableEnergy",
"proposedOffsiteRenewableEnergy",
"largestThreeFloorArea",
"roofAreaForRenewable",
}
)

# Above this many floors, largestThreeFloorArea is user-provided instead of
# being the sum of the building areas' floorArea.
_MAX_FLOORS_FOR_CALCULATED_AREA = 3

# List fields that must not be writable via update_renewable_in_project (use list operations instead)
_LIST_FIELDS = frozenset(
{
Expand All @@ -77,6 +95,10 @@ def set_renewable_exception_in_project(
On-site renewable systems and an exception can coexist — setting an exception
does not preclude adding ``renewableSystem[]``, and vice versa.

This doesn't refresh the capacity fields, which depend on the exception.
Use :meth:`~comcheck_api.client.COMcheckClient.set_renewable_exception` to
set the exception and call the renewable-capacity API together.

Args:
project: The project to modify.
renewable_exception: A :class:`~comcheck_api.types.core_types.RenewableExceptionOptions`
Expand Down Expand Up @@ -111,12 +133,18 @@ def update_renewable_in_project(
) -> ComBuilding:
"""Update scalar fields on the renewable singleton.

Only user-writable scalar fields (``numberOfFloors``) may be updated here.
User-writable scalar fields are ``numberOfFloors`` and, when the project
has more than 3 floors, ``largestThreeFloorArea``. With more than 3 floors,
``largestThreeFloorArea`` can't exceed the total building area floor area
(the sum of ``lighting.wholeBldgUse[].floorArea``). With 3 floors or fewer
it is calculated by
:meth:`~comcheck_api.client.COMcheckClient.area_for_renewable` and can't
be set here.

Engine-calculated fields (``requiredCapacity``, ``proposedCapacity``,
``requiredOffsiteRenewableEnergy``, ``proposedOffsiteRenewableEnergy``),
API-calculated fields (``largestThreeFloorArea``, ``roofAreaForRenewable``),
and list fields (``renewableSystem``, ``offsiteRenewableProcurement``) are
rejected.
the API-calculated ``roofAreaForRenewable``, and list fields
(``renewableSystem``, ``offsiteRenewableProcurement``) are rejected.

Args:
project: The project to modify.
Expand All @@ -128,7 +156,9 @@ def update_renewable_in_project(
Raises:
ValueError: If updates contains renewableException, engine-calculated,
API-calculated, or list fields; if a value fails validation (e.g.
negative numberOfFloors).
negative numberOfFloors); if largestThreeFloorArea is set with 3
floors or fewer; or if, with more than 3 floors,
largestThreeFloorArea exceeds the total building area floor area.
"""
if "renewableException" in updates:
raise ValueError(
Expand All @@ -146,10 +176,41 @@ def update_renewable_in_project(
# so e.g. a negative or non-integer numberOfFloors would slip through.
updated_project = project.model_copy(deep=True)
merged = {**updated_project.renewable.model_dump(mode="python"), **updates}
updated_project.renewable = Renewable.model_validate(merged)
renewable = Renewable.model_validate(merged)

if renewable.numberOfFloors <= _MAX_FLOORS_FOR_CALCULATED_AREA:
if "largestThreeFloorArea" in updates:
raise ValueError(
f"largestThreeFloorArea can only be set when numberOfFloors is "
f"greater than {_MAX_FLOORS_FOR_CALCULATED_AREA} (got "
f"{renewable.numberOfFloors}). With "
f"{_MAX_FLOORS_FOR_CALCULATED_AREA} floors or fewer it is the "
f"total building area floor area; use "
f"COMcheckClient.area_for_renewable to calculate it."
)
elif renewable.largestThreeFloorArea is not None:
total_floor_area = _total_building_area_floor_area(updated_project)
if renewable.largestThreeFloorArea > total_floor_area:
raise ValueError(
f"largestThreeFloorArea ({renewable.largestThreeFloorArea} ft2) "
f"cannot exceed the total building area floor area "
f"({total_floor_area} ft2) when numberOfFloors is greater than "
f"{_MAX_FLOORS_FOR_CALCULATED_AREA}."
)

updated_project.renewable = renewable
return updated_project


def _total_building_area_floor_area(project: ComBuilding) -> float:
"""Sum ``floorArea`` across ``lighting.wholeBldgUse``, skipping unset values."""
return sum(
float(area.floorArea)
for area in project.lighting.wholeBldgUse
if area.floorArea is not MISSING and area.floorArea is not None
)


def add_renewable_system_to_project(
project: ComBuilding,
new_renewable_system: RenewableSystem,
Expand Down
49 changes: 40 additions & 9 deletions docs_site/api/operations/renewable-energy.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ Renewable energy is managed at the **project level** through the
## Energy codes

Renewable requirements only apply under some energy codes (and state codes
based on them). These operations don't check `control.code` — whether the
project needs renewable data is up to you.
based on them). These operations don't check `control.code`. The COMcheck Web
API decides whether the project's code uses renewable data, and renewable
inputs on a project whose code doesn't use them do no harm.

## Key concepts

Expand Down Expand Up @@ -67,6 +68,18 @@ project = rn_ops.set_renewable_exception_in_project(
`NoneType_None` is rejected with a `ValueError`. A raw string raises a
`TypeError` — always use the enum.

The renewable capacity depends on the exception, so after changing it call
the renewable-capacity API. `COMcheckClient.set_renewable_exception` does
both: it sets the exception, calls `renewable_capacity`, and updates the
project in place (only if the API call succeeds):

```python
project = client.set_renewable_exception(
project, RenewableExceptionOptions.RENEWABLE_ONSITE_EXCEPTION_LOW_SOLAR_RAD
)
print(project.renewable.requiredCapacity, project.renewable.proposedCapacity)
```

## Adding an on-site renewable system

```python
Expand Down Expand Up @@ -118,16 +131,34 @@ project = rn_ops.add_offsite_renewable_procurement_to_project(project, offsite_p

## Updating scalar fields

Only user-writable scalar fields (currently just `numberOfFloors`) may be
updated via `update_renewable_in_project`. Engine-calculated fields
Only user-writable scalar fields may be updated via
`update_renewable_in_project`: `numberOfFloors` and, when the project has
more than 3 floors, `largestThreeFloorArea`. Engine-calculated fields
(`requiredCapacity`, `proposedCapacity`, `requiredOffsiteRenewableEnergy`,
`proposedOffsiteRenewableEnergy`), API-calculated fields
(`largestThreeFloorArea`, `roofAreaForRenewable`), and list fields
(`renewableSystem`, `offsiteRenewableProcurement`) are rejected with a
`ValueError`:
`proposedOffsiteRenewableEnergy`), the API-calculated `roofAreaForRenewable`,
and list fields (`renewableSystem`, `offsiteRenewableProcurement`) are
rejected with a `ValueError`:

```python
project = rn_ops.update_renewable_in_project(project, {"numberOfFloors": 5})
project = rn_ops.update_renewable_in_project(project, {"numberOfFloors": 2})
```

### Largest three floor area

How `largestThreeFloorArea` is set depends on `numberOfFloors`:

- **3 floors or fewer**: it is the total building area floor area (the sum of
`lighting.wholeBldgUse[].floorArea`), calculated by `area_for_renewable`.
Setting it through `update_renewable_in_project` raises a `ValueError`.
- **More than 3 floors**: you provide it. It can't exceed the total building
area floor area, or `update_renewable_in_project` raises a `ValueError`.
The check also runs when you raise `numberOfFloors` above 3 with a stored
value already in place.

```python
project = rn_ops.update_renewable_in_project(
project, {"numberOfFloors": 5, "largestThreeFloorArea": 30000.0}
)
```

## Renewable calculations (API)
Expand Down
Loading
Loading