Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a83dd58
Merge pull request #16 from pnnl/main
yanyanzhu-pnnl Jun 23, 2026
5cf5b0c
Merge pull request #20 from pnnl/main
yanyanzhu-pnnl Jul 14, 2026
cf9be85
remove PR template from develop
yanyanzhu-pnnl Jul 14, 2026
c1b8ed0
Add interior and exterior lighting
yanyanzhu-pnnl Jul 14, 2026
20a10ae
Merge branch 'main' into feature/lighting
yanyanzhu-pnnl Jul 24, 2026
5c2e0aa
update to full name instead of abbreviations
yanyanzhu-pnnl Jul 24, 2026
eb056e2
Merge branch 'main' into feature/lighting
yanyanzhu-pnnl Jul 24, 2026
5d437a4
Fix interior lightin examples
yanyanzhu-pnnl Jul 24, 2026
d61e33c
Fix exterior examples and todo documentation
yanyanzhu-pnnl Jul 24, 2026
227fa9d
remove doc
yanyanzhu-pnnl Jul 24, 2026
7c3bd6d
a correct comment of fixtureType
yanyanzhu-pnnl Jul 28, 2026
d56653b
WIP: update schema
jslane-h Aug 6, 2026
d090464
Move temp files to scratch folder
jslane-h Aug 6, 2026
7a1e8ff
remove exclude_unset=True
jslane-h Aug 6, 2026
1a39db6
update lighting related fields
yanyanzhu-pnnl Aug 7, 2026
c42a95e
update core_types
yanyanzhu-pnnl Aug 7, 2026
41ffeb2
remove ACTIVITY_COMMON_OFFIC
yanyanzhu-pnnl Aug 7, 2026
84ef77f
Merge pull request #25 from pnnl/schema-update
yanyanzhu-pnnl Aug 7, 2026
c3402aa
put exclude_unset back
yanyanzhu-pnnl Aug 10, 2026
1be16f8
update docs/
yanyanzhu-pnnl Aug 10, 2026
1153123
post merge schema correction
yanyanzhu-pnnl Aug 10, 2026
966b3fe
Remove use-default for model generation and exclude_unset on export
jslane-h Aug 12, 2026
b15752e
silent pydantic model warning, clenup examples
yanyanzhu-pnnl Aug 13, 2026
30130b6
clean up scratch
yanyanzhu-pnnl Aug 13, 2026
1c4509e
building area description should be unique
yanyanzhu-pnnl Aug 13, 2026
5c12c3d
verify areaDescription uniqueness
yanyanzhu-pnnl Aug 13, 2026
6530776
initial update for github pages and skills
yanyanzhu-pnnl Aug 13, 2026
dc12c52
update of github pages, skills
yanyanzhu-pnnl Aug 13, 2026
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
17 changes: 0 additions & 17 deletions .github/pull_request_template.md

This file was deleted.

4 changes: 4 additions & 0 deletions comcheck_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
from .project_operations import (
project_building_area_operations,
project_envelope_operations,
project_exterior_lighting_operations,
project_interior_lighting_operations,
)

# Introspection helpers
Expand Down Expand Up @@ -87,6 +89,8 @@
# Project Operations
"project_building_area_operations",
"project_envelope_operations",
"project_exterior_lighting_operations",
"project_interior_lighting_operations",
# Introspection
"list_operations",
"lookup_type",
Expand Down
119 changes: 92 additions & 27 deletions comcheck_api/ai/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,15 @@ Triggers:
`envelope`, `lighting` (which contains `wholeBldgUse[]` — the
building areas), `hvac`, `renewable`, and `control` (energy code).
No `Project`/`Control` PascalCase aliases exist.
The fields `hvac`, `renewable`, and the **interior-lighting fixtures
inside `activityUse[]`**, plus exterior lighting (`exteriorUse[]`)
and the shared `fixtureSchedule[]`, exist on the model but have
**no operation functions** — leave them at template defaults. Only
`lighting.wholeBldgUse[]` (building areas, including each area's
own `interiorLightingSpace` singleton) is mutable, via
`project_building_area_operations`.
- **Operation modules (functional)**: building areas and envelope
components are added/updated/removed via free functions in
`project_building_area_operations` and `project_envelope_operations`.
Each function takes a `ComBuilding` and returns a new `ComBuilding`.
The fields `hvac`, `renewable`, and the shared `fixtureSchedule[]`
exist on the model but have **no operation functions** — leave them
at template defaults. All other mutable areas have dedicated modules.
- **Operation modules (functional)**: all mutations go through free
functions that take a `ComBuilding` and return a new `ComBuilding`:
- `project_building_area_operations` — `WholeBldgUse` items
- `project_envelope_operations` — roofs, walls, floors, windows, doors, skylights, thermal bridges
- `project_interior_lighting_operations` — `ActivityUse` items (interior lighting spaces + fixtures)
- `project_exterior_lighting_operations` — `ExteriorUse` items + zone type
- **Envelope items attach to a building-area key**: every
`add_*_to_project` envelope function takes
`(project, building_area_key, new_component)`. Look up the key
Expand Down Expand Up @@ -147,24 +145,18 @@ print(result["performanceRating"])
bypass the validation logic in the operation modules. Always go
through `project_envelope_operations` and
`project_building_area_operations` instead.
- Don't add, update, or remove interior lighting (the
`activityUse[]` fixtures), exterior lighting (`exteriorUse[]`,
`fixtureSchedule[]`), HVAC/mechanical, or renewable-energy
components — no operations exist for them. The whole-building
`interiorLightingSpace` singleton on each `WholeBldgUse` *is*
editable through `project_building_area_operations`; the per-
activity lighting nested under `activityUse[]` is not. The
`COMcheckClient` user methods (`list_projects`, `get_project`,
- Don't add, update, or remove `fixtureSchedule[]`, HVAC/mechanical, or
renewable-energy components — no operations exist for them yet.
The `COMcheckClient` user methods (`list_projects`, `get_project`,
`update_project`, `update_uvalues`, `start_run_simulation`,
`get_simulation_status`, `get_simulation_result`, `set_api_key`)
are fully supported and
fine to use. The compliance/report client methods
(`check_UA_compliance`, `check_requirements`, `generate_report`) are
also fully supported. If asked for an unsupported mutation area,
tell the user it's not implemented and offer building-area /
envelope / simulation instead. Confirm operation scope with
`comcheck_api.list_operations()` (only `building_area` and
`envelope` groups exist).
are fully supported and fine to use. The compliance/report client
methods (`check_UA_compliance`, `check_requirements`,
`generate_report`) are also fully supported. If asked for an
unsupported mutation area, tell the user it's not implemented and
offer building-area / envelope / lighting / simulation instead.
`comcheck_api.list_operations()` enumerates the `building_area`,
`envelope`, `interior_lighting`, and `exterior_lighting` groups.

## Common patterns

Expand Down Expand Up @@ -240,6 +232,79 @@ else:
raise TimeoutError(f"Simulation {session_id} did not complete in 5 min")
```

### Adding interior lighting (ActivityUse + fixtures)

Interior lighting lives under `wholeBldgUse[i].activityUse[]`. Use
`project_interior_lighting_operations` — there are no fixture-level ops;
edit the `activityUse`'s `interiorLightingSpace.fixture[]` and pass the
whole `activityUse` through `update_interior_lighting_space_in_project`.

```python
from comcheck_api import project_interior_lighting_operations as il_ops
from comcheck_api.defaults import get_default_interior_lighting_space_template, get_default_fixture_template
from comcheck_api.types.core_types import ActivityTypeOptions

# fixtureType is the required identifier (a description string); lightingType
# is optional and marked for deprecation, so leave it unset.
fixture = get_default_fixture_template()
fixture.description = "Recessed LED"
fixture.fixtureType = "Recessed LED"
fixture.fixtureWattage = 20.0
fixture.quantity = 10

activity_use = get_default_interior_lighting_space_template()
activity_use.areaDescription = "Open Office"
activity_use.activityType = ActivityTypeOptions.ACTIVITY_COMMON_OFFICE_OPEN
activity_use.interiorLightingSpace = activity_use.interiorLightingSpace.model_copy(
deep=True, update={"fixture": [fixture]}
)
project = il_ops.add_interior_lighting_space_to_project(project, area_key, activity_use)

# Update a field — fixtures are preserved unless you also pass interiorLightingSpace
project = il_ops.update_interior_lighting_space_in_project(
project, area_key, "Open Office", {"floorArea": 2500.0}
)

# Remove
project = il_ops.remove_interior_lighting_space_from_project(project, area_key, "Open Office")
```

### Adding exterior lighting (ExteriorUse + zone type)

Exterior lighting lives under `lighting.exteriorUse[]`. Set a real zone type
first, then add `ExteriorUse` items with fixtures inline.

```python
from comcheck_api import project_exterior_lighting_operations as el_ops
from comcheck_api.defaults import get_default_exterior_lighting_area_template, get_default_fixture_template
from comcheck_api.types.core_types import ExteriorLightingZoneTypeOptions, ExteriorUseTypeOptions

# Must set zone type before exterior compliance can be evaluated.
# EXT_ZONE_UNSPECIFIED raises ValueError; a raw string raises TypeError.
project = el_ops.set_exterior_lighting_zone_type_in_project(
project, ExteriorLightingZoneTypeOptions.EXT_ZONE_NEIGHBORHOOD_BUS_DISTRICT
)

fixture = get_default_fixture_template()
fixture.description = "Parking LED"
fixture.fixtureWattage = 150.0
fixture.quantity = 8

exterior_use = get_default_exterior_lighting_area_template()
exterior_use.areaDescription = "Main Parking Area"
exterior_use.exteriorType = ExteriorUseTypeOptions.EXTERIOR_PARKING_AREA
exterior_use.exteriorLightingSpace = exterior_use.exteriorLightingSpace.model_copy(
deep=True, update={"fixture": [fixture]}
)
project = el_ops.add_exterior_lighting_area_to_project(project, exterior_use)

# Adding while zone is EXT_ZONE_UNSPECIFIED emits UserWarning (not an error)
project = el_ops.update_exterior_lighting_area_in_project(
project, "Main Parking Area", {"useQuantity": 6000.0}
)
project = el_ops.remove_exterior_lighting_area_from_project(project, "Main Parking Area")
```

### Checking compliance/requirements and generating a report

These are synchronous (no polling). All three take a `ComBuilding`
Expand Down
60 changes: 54 additions & 6 deletions comcheck_api/ai/skill/reference/operations.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Project Operations Reference

Operation functions are free functions in two modules:
Operation functions are free functions in four modules:

- `comcheck_api.project_operations.project_building_area_operations`
- `comcheck_api.project_operations.project_envelope_operations`
- `comcheck_api.project_operations.project_interior_lighting_operations`
- `comcheck_api.project_operations.project_exterior_lighting_operations`

Each function takes a `ComBuilding` and a payload, and returns a new
`ComBuilding`. Treat them as immutable transformations.
Expand All @@ -16,8 +18,8 @@ from comcheck_api import project_building_area_operations as ba_ops

| Function | Purpose |
|---|---|
| `add_building_area_to_project(project, new_building_area)` | Add a `WholeBldgUse` building area to the project. |
| `update_building_area_in_project(project, building_area_key, updates)` | Update fields of an existing building area by key. |
| `add_building_area_to_project(project, new_building_area)` | Add a `WholeBldgUse` building area to the project. Raises `ValueError` if `areaDescription` already exists. |
| `update_building_area_in_project(project, building_area_key, updates)` | Update fields of an existing building area by key. Raises `ValueError` if the new `areaDescription` collides with another area. |
| `remove_building_area_from_project(project, building_area_key)` | Remove a building area by key. |
| `get_building_area_keys_from_project(project)` | List `[{key, areaDescription}, …]` for the project. |

Expand All @@ -33,9 +35,9 @@ a building-area key. Default projects have no areas — add one first:
```python
from comcheck_api.defaults import get_default_building_area_template

area = get_default_building_area_template()
area.areaDescription = "Open office"
project = ba_ops.add_building_area_to_project(project, area)
area = get_default_building_area_template() # unique key + areaDescription per call
area.areaDescription = "Open office" # optional override — must be unique within the project
project = ba_ops.add_building_area_to_project(project, area) # raises ValueError if areaDescription already exists

area_key = ba_ops.get_building_area_keys_from_project(project)[0]["key"]
```
Expand Down Expand Up @@ -100,6 +102,52 @@ roof.orientation = OrientationOptions.UNSPECIFIED_ORIENTATION
project = env_ops.add_roof_to_project(project, area_key, roof)
```

## Interior lighting operations

```python
from comcheck_api import project_interior_lighting_operations as il_ops
```

Interior lighting spaces are `ActivityUse` objects nested under
`lighting.wholeBldgUse[i].activityUse[]`. There are no fixture-level ops —
edit `activityUse.interiorLightingSpace.fixture[]` and pass the whole
`ActivityUse` through `update_interior_lighting_space_in_project`.

| Function | Purpose |
|---|---|
| `add_interior_lighting_space_to_project(project, building_area_key, new_activity_use)` | Add an `ActivityUse` to a building area. `activityUse.key` is auto-set to `building_area_key`. |
| `update_interior_lighting_space_in_project(project, building_area_key, area_description, updates)` | Update an `ActivityUse` by its `areaDescription`. |
| `remove_interior_lighting_space_from_project(project, building_area_key, area_description)` | Remove an `ActivityUse` by its `areaDescription`. |
| `get_interior_lighting_space_keys_from_project(project, building_area_key)` | List `[{areaDescription, activityType}, …]` for a building area. |

Use `get_default_interior_lighting_space_template()` as a starting point.
`areaDescription` is the identifier — it is unique within a building area's
`activityUse[]` list and is auto-generated if missing.

## Exterior lighting operations

```python
from comcheck_api import project_exterior_lighting_operations as el_ops
```

Exterior lighting spaces are `ExteriorUse` objects in
`lighting.exteriorUse[]`. Set a real zone type before exterior compliance
can be evaluated. There are no fixture-level ops — edit
`exteriorUse.exteriorLightingSpace.fixture[]` and pass the whole
`ExteriorUse` through `update_exterior_lighting_area_in_project`.

| Function | Purpose |
|---|---|
| `set_exterior_lighting_zone_type_in_project(project, zone_type)` | Set `lighting.exteriorLightingZoneType`. Raises `ValueError` for `EXT_ZONE_UNSPECIFIED`, `TypeError` for non-enum values. |
| `add_exterior_lighting_area_to_project(project, new_exterior_lighting_area)` | Add an `ExteriorUse`. Emits `UserWarning` if zone type is still `EXT_ZONE_UNSPECIFIED`. |
| `update_exterior_lighting_area_in_project(project, area_description, updates)` | Update an `ExteriorUse` by its `areaDescription`. |
| `remove_exterior_lighting_area_from_project(project, area_description)` | Remove an `ExteriorUse` by its `areaDescription`. |
| `get_exterior_lighting_area_keys_from_project(project)` | List `[{areaDescription, exteriorType}, …]` for the project. |

Use `get_default_exterior_lighting_area_template()` as a starting point.
`areaDescription` is the identifier — it is unique within `exteriorUse[]`
and is auto-generated if missing.

## U-value calculation requires a construction type

When `update_uvalues` (or `start_run_simulation`) recalculates assembly
Expand Down
15 changes: 11 additions & 4 deletions comcheck_api/ai/skill/scripts/validate_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ def _read_input(arg: str) -> str:


UNSUPPORTED_PROJECT_ATTRS = {"hvac", "renewable"}
# `wholeBldgUse` (building areas + interior lighting), `activityUse` (interior
# lighting spaces), `exteriorUse` (exterior lighting), and
# `exteriorLightingZoneType` all have operation modules. Only `fixtureSchedule`
# has no operations yet.
UNSUPPORTED_LIGHTING_ATTRS = {
"activityUse",
"exteriorUse",
"fixtureSchedule",
}

Expand Down Expand Up @@ -79,7 +81,7 @@ def validate(code: str) -> dict:
2. Import check on every imported module name.
3. Scope check that the code only uses operations actually exposed
by the SDK and does not mutate the unsupported `hvac`,
`renewable`, or non-`wholeBldgUse` lighting subtrees.
`renewable`, or `lighting.fixtureSchedule` subtrees.
"""
errors: list[dict] = []

Expand Down Expand Up @@ -127,6 +129,8 @@ def validate(code: str) -> dict:
if alias.name in {
"project_envelope_operations",
"project_building_area_operations",
"project_interior_lighting_operations",
"project_exterior_lighting_operations",
}:
op_module_aliases.add(alias.asname or alias.name)
elif isinstance(node, ast.ImportFrom) and node.module == (
Expand All @@ -136,6 +140,8 @@ def validate(code: str) -> dict:
if alias.name in {
"project_envelope_operations",
"project_building_area_operations",
"project_interior_lighting_operations",
"project_exterior_lighting_operations",
}:
op_module_aliases.add(alias.asname or alias.name)

Expand Down Expand Up @@ -165,7 +171,8 @@ def validate(code: str) -> dict:
"line": node.lineno,
"message": (
f"`project.lighting.{node.attr}` has no operations; "
"only `lighting.wholeBldgUse[]` is editable."
"edit lighting via the building-area, interior-lighting, "
"and exterior-lighting operation modules instead."
),
}
)
Expand Down
12 changes: 6 additions & 6 deletions comcheck_api/client/comcheck_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def update_project(
if not old_project:
raise COMCheckProjectNotFoundError(project_id)

project_data_json = project_data.model_dump(mode="json", exclude_unset=True)
project_data_json = project_data.model_dump(mode="json")

# Preserve user project reference
user_project = old_project["userProject"]
Expand Down Expand Up @@ -262,7 +262,7 @@ def update_uvalues(self, project: ComBuilding) -> ComBuilding:
The same ``project`` instance, with u-values updated.
"""
energy_code = str(project.control.code)
envelope_data = project.envelope.model_dump(mode="json", exclude_unset=True)
envelope_data = project.envelope.model_dump(mode="json")
updated_assembly_uvalues = self._service.assemblies_uvalue(
envelope_data, energy_code
)["data"]
Expand Down Expand Up @@ -296,7 +296,7 @@ def check_UA_compliance(self, project: ComBuilding) -> Any:
Returns:
The compliance results payload returned by the API.
"""
project_data = project.model_dump(mode="json", exclude_unset=True)
project_data = project.model_dump(mode="json")
response = self._service.check_UA_compliance(project_data)
return response.get("data")

Expand All @@ -309,7 +309,7 @@ def check_requirements(self, project: ComBuilding) -> Any:
Returns:
The requirements payload returned by the API.
"""
project_data = project.model_dump(mode="json", exclude_unset=True)
project_data = project.model_dump(mode="json")
response = self._service.check_requirements(project_data)
return response.get("data")

Expand Down Expand Up @@ -349,7 +349,7 @@ def generate_report(
``expires``, and ``fileName``.
"""
report_data = {
"building": project.model_dump(mode="json", exclude_unset=True),
"building": project.model_dump(mode="json"),
"envelope": envelope,
"extlighting": extlighting,
"intlighting": intlighting,
Expand Down Expand Up @@ -396,7 +396,7 @@ def start_run_simulation(
logger.info("Updating project: %s", project_id)
project = self.update_project(str(project_id), project)

project_data = project.model_dump(mode="json", exclude_unset=True)
project_data = project.model_dump(mode="json")
run_result = self._service.start_run_simulation(project_data)
if run_result.data is None:
raise COMCheckSimulationError(
Expand Down
Loading