From fdb6e5d1819c0b6bcf7a49f494916a42f9f868b9 Mon Sep 17 00:00:00 2001 From: weilixu Date: Sat, 27 Jun 2026 08:56:44 -0700 Subject: [PATCH 1/7] add workflow for assemblies u factor --- comcheck_api/api/api_services.py | 25 +++++++++++++++++ comcheck_api/client/comcheck_client.py | 38 +++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/comcheck_api/api/api_services.py b/comcheck_api/api/api_services.py index dbf349f..83bf35d 100644 --- a/comcheck_api/api/api_services.py +++ b/comcheck_api/api/api_services.py @@ -187,6 +187,31 @@ def update_project( except Exception as error: self._handle_api_error(error) + def assemblies_uvalue(self, envelope_data: Dict[str, Any], energy_code) -> Dict[str, Any]: + """get assemblies u values + + Args: + envelope_data: The envelope data to send in the request body + energy_code: The energy code for the api end point path + + Returns: + RunSimulationResponse with session information + + Raises: + COMCheckHTTPError: If the API returns an error status + COMCheckConnectionError: If the request fails + """ + try: + client = self._get_client() + response = client.post( + f"/{energy_code}/assemblies/uvalues", json=envelope_data + ) + response.raise_for_status() + # may need validation here. + return response.json() + except Exception as error: + self._handle_api_error(error) + def start_run_simulation( self, project_data: Dict[str, Any] ) -> RunSimulationResponse: diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index 896280e..69e02e6 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -244,6 +244,38 @@ def _parse_data(self, data, mode): return ComBuilding(**data) return data + def assemblie_uvalues(self, project: ComBuilding) -> Any: + """ + + Args: + project: + + Returns: + + """ + logger = logging.getLogger(__name__) + # update assemblies uvalue + energy_code = str(project.control.code) + envelope_data = project.envelope.model_dump(mode="json", exclude_unset=True) + + def fill_empty_description(assemblies: list[dict]) -> list[dict]: + for assembly in assemblies: + if not assembly.get("description", ""): + assembly["description"] = assembly.get("assemblyType") + return assemblies + + # need to refactor this part to ensure the data integrity + reformatted_envelope_data = { + "agWall": fill_empty_description(envelope_data["agWall"]), + "roof": fill_empty_description(envelope_data["roof"]), + "skylight": fill_empty_description(envelope_data["skylight"]), + "window": fill_empty_description(envelope_data["window"]), + "door": fill_empty_description(envelope_data["door"]), + "floor": fill_empty_description(envelope_data["floor"]) + } + assemblie_uvalue = self._service.assemblies_uvalue(reformatted_envelope_data, energy_code) + return assemblie_uvalue + def start_run_simulation( self, project: ComBuilding, project_id: Optional[int] = None ) -> str: @@ -268,7 +300,11 @@ def start_run_simulation( if project_id: logger.info("Updating project: %s", project_id) - self.update_project(str(project_id), project) + project = self.update_project(str(project_id), project) + else: + # update assemblies uvalue in the project + assemblie_uvalue = self.assemblie_uvalues(project) + # Need to glue this back to the assemblies in the project before dump for simulation project_data = project.model_dump(mode="json", exclude_unset=True) run_result = self._service.start_run_simulation(project_data) From c9593bc3ceea25d9f71c7c136723f3af70357de1 Mon Sep 17 00:00:00 2001 From: weilixu Date: Sat, 27 Jun 2026 10:16:50 -0700 Subject: [PATCH 2/7] add glue code to update u factors --- comcheck_api/client/comcheck_client.py | 30 ++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index 69e02e6..eec566f 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -264,6 +264,11 @@ def fill_empty_description(assemblies: list[dict]) -> list[dict]: assembly["description"] = assembly.get("assemblyType") return assemblies + def transform_floor(floors: list[dict]) -> list[dict]: + for floor in floors: + floor.pop('slabFullInsulBelowMinRValue', None) + return floors + # need to refactor this part to ensure the data integrity reformatted_envelope_data = { "agWall": fill_empty_description(envelope_data["agWall"]), @@ -271,10 +276,10 @@ def fill_empty_description(assemblies: list[dict]) -> list[dict]: "skylight": fill_empty_description(envelope_data["skylight"]), "window": fill_empty_description(envelope_data["window"]), "door": fill_empty_description(envelope_data["door"]), - "floor": fill_empty_description(envelope_data["floor"]) + "floor": transform_floor(fill_empty_description(envelope_data["floor"])) } assemblie_uvalue = self._service.assemblies_uvalue(reformatted_envelope_data, energy_code) - return assemblie_uvalue + return assemblie_uvalue["data"] def start_run_simulation( self, project: ComBuilding, project_id: Optional[int] = None @@ -298,13 +303,30 @@ def start_run_simulation( """ logger = logging.getLogger(__name__) + project_data = None if project_id: logger.info("Updating project: %s", project_id) project = self.update_project(str(project_id), project) else: # update assemblies uvalue in the project - assemblie_uvalue = self.assemblie_uvalues(project) - # Need to glue this back to the assemblies in the project before dump for simulation + updated_assembly_uvalues = self.assemblie_uvalues(project) + # still missing + ag_Walls = project.envelope.agWall + roofs = project.envelope.roof + floors = project.envelope.floor + for wall in ag_Walls: + for uvalue in updated_assembly_uvalues.get("agWall", []): + if wall.assemblyType == uvalue["description"]: + wall.effectiveUFactor = float(uvalue["propUValue"]) + wall.propUValue = float(uvalue["propUValue"]) + for roof in roofs: + for uvalue in updated_assembly_uvalues.get("roof", []): + if roof.assemblyType == uvalue["description"]: + roof.propUValue = float(uvalue["propUValue"]) + for floor in floors: + for uvalue in updated_assembly_uvalues.get("floor", []): + if floor.assemblyType == uvalue["description"]: + floor.propUValue = float(uvalue["propUValue"]) project_data = project.model_dump(mode="json", exclude_unset=True) run_result = self._service.start_run_simulation(project_data) From 330d56ee4a540273c1456d39764acc26b1ad53b7 Mon Sep 17 00:00:00 2001 From: weilixu Date: Sat, 27 Jun 2026 10:25:09 -0700 Subject: [PATCH 3/7] remove floor tranform --- comcheck_api/client/comcheck_client.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index eec566f..85e8aa8 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -264,11 +264,6 @@ def fill_empty_description(assemblies: list[dict]) -> list[dict]: assembly["description"] = assembly.get("assemblyType") return assemblies - def transform_floor(floors: list[dict]) -> list[dict]: - for floor in floors: - floor.pop('slabFullInsulBelowMinRValue', None) - return floors - # need to refactor this part to ensure the data integrity reformatted_envelope_data = { "agWall": fill_empty_description(envelope_data["agWall"]), @@ -276,7 +271,7 @@ def transform_floor(floors: list[dict]) -> list[dict]: "skylight": fill_empty_description(envelope_data["skylight"]), "window": fill_empty_description(envelope_data["window"]), "door": fill_empty_description(envelope_data["door"]), - "floor": transform_floor(fill_empty_description(envelope_data["floor"])) + "floor": fill_empty_description(envelope_data["floor"]) } assemblie_uvalue = self._service.assemblies_uvalue(reformatted_envelope_data, energy_code) return assemblie_uvalue["data"] From 9d40cb745daca40db433bf717bcce1463a03cea5 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Mon, 29 Jun 2026 12:45:17 -0700 Subject: [PATCH 4/7] Add effectiveUFactor, compliance, requirements, and report functions, update documentation and skills --- .gitignore | 1 + README.md | 2 +- comcheck_api/ai/skill/SKILL.md | 42 ++++++++- comcheck_api/ai/skill/reference/compliance.md | 86 ++++++++++++++++++ comcheck_api/api/api_services.py | 89 ++++++++++++++++-- comcheck_api/client/comcheck_client.py | 91 ++++++++++++++++++- docs_site/api/compliance.md | 73 +++++++++++++++ examples/README.md | 14 +++ examples/client/compliance_and_report.py | 47 ++++++++++ mkdocs.yml | 1 + 10 files changed, 430 insertions(+), 16 deletions(-) create mode 100644 comcheck_api/ai/skill/reference/compliance.md create mode 100644 docs_site/api/compliance.md create mode 100644 examples/client/compliance_and_report.py diff --git a/.gitignore b/.gitignore index f910562..1f38fac 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ site/ # Build artifacts dist/ +reports/ diff --git a/README.md b/README.md index 17996b8..aec01d9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # COMcheck API — Python Package -A type-safe Python package for the [COMcheck Web](https://comcheck.energycode.pnl.gov) — PNNL's hosted service for commercial building energy-code (ASHRAE 90.1 / IECC) compliance. Use it to build COMcheck projects, run compliance simulations, and read results from Python over the live API. Requires a free Personal Access Token (see below). The package is maintained with `uv`. +A type-safe Python package for the [COMcheck Web](https://comcheck.energycode.pnl.gov) — PNNL's hosted service for commercial building energy-code (ASHRAE 90.1 / IECC) compliance. Use it to build COMcheck projects, run compliance simulations, check compliance and requirements, generate PDF reports, and read results from Python over the live API. Requires a free Personal Access Token (see below). The package is maintained with `uv`. **Requirements:** - Python: `>=3.12` diff --git a/comcheck_api/ai/skill/SKILL.md b/comcheck_api/ai/skill/SKILL.md index 5f22f32..ec27645 100644 --- a/comcheck_api/ai/skill/SKILL.md +++ b/comcheck_api/ai/skill/SKILL.md @@ -150,9 +150,11 @@ print(result["performanceRating"]) `COMcheckClient` user methods (`list_projects`, `get_project`, `update_project`, `start_run_simulation`, `get_simulation_status`, `get_simulation_result`, `set_api_key`) are fully supported and - fine to use. 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 + fine to use. The compliance/report client methods + (`check_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). @@ -230,6 +232,38 @@ else: raise TimeoutError(f"Simulation {session_id} did not complete in 5 min") ``` +### Checking compliance/requirements and generating a report + +These are synchronous (no polling). All three take a `ComBuilding` +directly. + +```python +# Per-category compliance status +compliance = client.check_compliance(project) +if compliance["mandatoryRequirementsMet"]: + ... + +# Applicable requirements +requirements = client.check_requirements(project) + +# PDF report — returns {url, expires, fileName}. The url is a short-lived +# presigned S3 URL (expires within minutes); don't cache it. +report = client.generate_report(project) + +# Download the PDF (saves using the server fileName; default dir is ~/Downloads) +report = client.generate_report(project, download=True) +report = client.generate_report(project, download=True, download_dir="./out") + +# Toggle report sections (all default to True) +report = client.generate_report( + project, envelope=True, intlighting=True, extlighting=False, mechanical=True +) +``` + +`generate_report` deliberately does **not** open a browser — it's a +library, so it returns metadata and lets the caller decide +(`webbrowser.open(report["url"])`). + ## Gotchas - **Field names are lowercase camelCase**, not PascalCase. @@ -281,5 +315,7 @@ else: - For Pydantic model field-level details → read `reference/types.md`. - For the simulation start/poll/fetch flow → read `reference/simulation.md`. +- For compliance checks, requirements, and PDF report generation → + read `reference/compliance.md`. - To validate generated code against a mocked client → run `scripts/validate_code.py`. diff --git a/comcheck_api/ai/skill/reference/compliance.md b/comcheck_api/ai/skill/reference/compliance.md new file mode 100644 index 0000000..6f3dc1b --- /dev/null +++ b/comcheck_api/ai/skill/reference/compliance.md @@ -0,0 +1,86 @@ +# Compliance, Requirements & Reports Reference + +Three client methods evaluate a project without running the full +async simulation flow. All three accept a `ComBuilding` and serialize +it for you — pass the project model directly. + +```python +from comcheck_api import COMcheckClient +client = COMcheckClient(api_key="...") +``` + +| Method | Returns | Purpose | +|---|---|---| +| `client.check_compliance(project)` | `dict` | Per-category compliance status for the project. | +| `client.check_requirements(project)` | `dict` | The applicable requirements for the project. | +| `client.generate_report(project, ...)` | `dict` | Generate a PDF report; returns `{url, expires, fileName}`. | + +## check_compliance + +Returns a dict with a top-level `mandatoryRequirementsMet` flag and a +status object for each category: + +- `envelopeStatus` +- `interiorLightingStatus` +- `exteriorLightingStatus` +- `renewableStatus` +- `energyCreditStatus` + +```python +compliance = client.check_compliance(project) +if compliance["mandatoryRequirementsMet"]: + ... +``` + +## check_requirements + +Returns the applicable requirements payload for the project. Like +`check_compliance`, it's a single synchronous call — no polling. + +```python +requirements = client.check_requirements(project) +``` + +## generate_report + +Builds a PDF report. The PDF is stored in S3; the API returns a +**short-lived presigned URL** (expires within a few minutes) plus the +file name: + +```python +report = client.generate_report(project) +# {"url": "...", "expires": "in 5 minutes", "fileName": "report...pdf"} +``` + +### Signature + +```python +client.generate_report( + project, + envelope=True, + extlighting=True, + intlighting=True, + mechanical=True, + download=False, + download_dir=None, +) +``` + +- The four section flags toggle which sections appear in the report + (all default to `True`). +- `download=True` fetches the PDF from the presigned URL and saves it + using the server-provided `fileName`. `download_dir` defaults to the + user's `~/Downloads` folder. + +```python +report = client.generate_report(project, download=True) # ~/Downloads +report = client.generate_report(project, download=True, download_dir="./out") +``` + +## Don't + +- Don't try to open the report in a browser from library code — this + method intentionally returns metadata only. If a browser is wanted, + the caller does `webbrowser.open(report["url"])`. +- Don't cache the presigned `url` — it expires within minutes. + Re-call `generate_report` to get a fresh one. diff --git a/comcheck_api/api/api_services.py b/comcheck_api/api/api_services.py index 83bf35d..299c00e 100644 --- a/comcheck_api/api/api_services.py +++ b/comcheck_api/api/api_services.py @@ -187,19 +187,21 @@ def update_project( except Exception as error: self._handle_api_error(error) - def assemblies_uvalue(self, envelope_data: Dict[str, Any], energy_code) -> Dict[str, Any]: + def assemblies_uvalue( + self, envelope_data: Dict[str, Any], energy_code + ) -> Dict[str, Any]: """get assemblies u values - Args: - envelope_data: The envelope data to send in the request body - energy_code: The energy code for the api end point path + Args: + envelope_data: The envelope data to send in the request body + energy_code: The energy code for the api end point path - Returns: - RunSimulationResponse with session information + Returns: + RunSimulationResponse with session information - Raises: - COMCheckHTTPError: If the API returns an error status - COMCheckConnectionError: If the request fails + Raises: + COMCheckHTTPError: If the API returns an error status + COMCheckConnectionError: If the request fails """ try: client = self._get_client() @@ -212,6 +214,75 @@ def assemblies_uvalue(self, envelope_data: Dict[str, Any], energy_code) -> Dict[ except Exception as error: self._handle_api_error(error) + def check_compliance(self, project_data: Dict[str, Any]) -> Dict[str, Any]: + """Check compliance for a project. + + Args: + project_data: The project data to send in the request body + + Returns: + API response data as dictionary + + Raises: + COMCheckHTTPError: If the API returns an error status + COMCheckConnectionError: If the request fails + """ + try: + client = self._get_client() + response = client.post("/compliance", json=project_data) + response.raise_for_status() + return response.json() + except Exception as error: + self._handle_api_error(error) + + def check_requirements(self, project_data: Dict[str, Any]) -> Dict[str, Any]: + """Check requirements for a project. + + Args: + project_data: The project data to send in the request body + + Returns: + API response data as dictionary + + Raises: + COMCheckHTTPError: If the API returns an error status + COMCheckConnectionError: If the request fails + """ + try: + client = self._get_client() + response = client.post("/requirements", json=project_data) + response.raise_for_status() + return response.json() + except Exception as error: + self._handle_api_error(error) + + def generate_report(self, report_data: Dict[str, Any]) -> Dict[str, Any]: + """Generate a PDF report for a project. + + The API stores the generated PDF in S3 and returns a presigned URL + to download it. + + Args: + report_data: The report request body, containing ``building`` (the + project data) and the ``envelope``, ``extlighting``, + ``intlighting``, and ``mechanical`` section flags. + + Returns: + API response as a dictionary with ``url`` (the presigned S3 URL), + ``expires``, and ``fileName`` + + Raises: + COMCheckHTTPError: If the API returns an error status + COMCheckConnectionError: If the request fails + """ + try: + client = self._get_client() + response = client.post("/report", json=report_data) + response.raise_for_status() + return response.json() + except Exception as error: + self._handle_api_error(error) + def start_run_simulation( self, project_data: Dict[str, Any] ) -> RunSimulationResponse: diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index 85e8aa8..e93774c 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -4,8 +4,11 @@ and return either Pydantic models, primitives, or raw dicts depending on the operation.""" import logging +import os from typing import Any, Dict, List, Literal, Optional, Union, overload +import httpx + from comcheck_api.api import COMCheckApiService from comcheck_api.constants.building_area_constants import DEFAULT_BUILDING_AREA from comcheck_api.exceptions import ( @@ -271,11 +274,93 @@ def fill_empty_description(assemblies: list[dict]) -> list[dict]: "skylight": fill_empty_description(envelope_data["skylight"]), "window": fill_empty_description(envelope_data["window"]), "door": fill_empty_description(envelope_data["door"]), - "floor": fill_empty_description(envelope_data["floor"]) + "floor": fill_empty_description(envelope_data["floor"]), } - assemblie_uvalue = self._service.assemblies_uvalue(reformatted_envelope_data, energy_code) + assemblie_uvalue = self._service.assemblies_uvalue( + reformatted_envelope_data, energy_code + ) return assemblie_uvalue["data"] + def check_compliance(self, project: ComBuilding) -> Any: + """Check code compliance for a project. + + Args: + project: The project data to evaluate. + + Returns: + The compliance results payload returned by the API. + """ + project_data = project.model_dump(mode="json", exclude_unset=True) + response = self._service.check_compliance(project_data) + return response.get("data") + + def check_requirements(self, project: ComBuilding) -> Any: + """Check the applicable requirements for a project. + + Args: + project: The project data to evaluate. + + Returns: + The requirements payload returned by the API. + """ + project_data = project.model_dump(mode="json", exclude_unset=True) + response = self._service.check_requirements(project_data) + return response.get("data") + + def generate_report( + self, + project: ComBuilding, + envelope: bool = True, + extlighting: bool = True, + intlighting: bool = True, + mechanical: bool = True, + download: bool = False, + download_dir: Optional[str] = None, + ) -> Optional[Dict[str, Any]]: + """Generate a PDF report for a project. + + The report PDF is stored in S3; the API returns a presigned URL to + download it. The URL is short-lived (expires within a few minutes). + + This method does not open the report in a browser — as a library it + returns the report metadata and lets the caller decide what to do + (e.g. ``webbrowser.open(report["url"])``, stream it onward, or save it + via *download*). + + Args: + project: The project data to generate a report for. + envelope: Whether to include the envelope section in the report. + extlighting: Whether to include the exterior lighting section. + intlighting: Whether to include the interior lighting section. + mechanical: Whether to include the mechanical section. + download: When ``True``, fetch the PDF from the presigned URL and + save it using the server-provided ``fileName``. + download_dir: Directory to save the PDF into when *download* is + ``True``. Defaults to the user's ``~/Downloads`` folder. + + Returns: + The report metadata dict with ``url`` (the presigned S3 URL), + ``expires``, and ``fileName``. + """ + report_data = { + "building": project.model_dump(mode="json", exclude_unset=True), + "envelope": envelope, + "extlighting": extlighting, + "intlighting": intlighting, + "mechanical": mechanical, + } + response = self._service.generate_report(report_data) + if response and download: + if download_dir is None: + download_dir = os.path.join(os.path.expanduser("~"), "Downloads") + os.makedirs(download_dir, exist_ok=True) + pdf_response = httpx.get(response["url"], timeout=60.0) + pdf_response.raise_for_status() + output_path = os.path.join(download_dir, response["fileName"]) + with open(output_path, "wb") as f: + f.write(pdf_response.content) + return response + def start_run_simulation( self, project: ComBuilding, project_id: Optional[int] = None ) -> str: @@ -312,7 +397,7 @@ def start_run_simulation( for wall in ag_Walls: for uvalue in updated_assembly_uvalues.get("agWall", []): if wall.assemblyType == uvalue["description"]: - wall.effectiveUFactor = float(uvalue["propUValue"]) + wall.effectiveUFactor = float(uvalue["effectiveUFactor"]) wall.propUValue = float(uvalue["propUValue"]) for roof in roofs: for uvalue in updated_assembly_uvalues.get("roof", []): diff --git a/docs_site/api/compliance.md b/docs_site/api/compliance.md new file mode 100644 index 0000000..68f2060 --- /dev/null +++ b/docs_site/api/compliance.md @@ -0,0 +1,73 @@ +# Compliance & Reports + +Beyond running a full simulation, the client exposes lighter-weight checks +and a PDF report generator. + +## Check compliance + +`check_compliance(project)` evaluates a project against its energy code and +returns the per-category compliance status. + +```python +from comcheck_api import COMcheckClient +from comcheck_api.defaults import get_default_project_template + +client = COMcheckClient(api_key="your-key") +project = get_default_project_template() + +compliance = client.check_compliance(project) +# { +# "mandatoryRequirementsMet": ..., +# "envelopeStatus": {...}, "interiorLightingStatus": {...}, +# "exteriorLightingStatus": {...}, "renewableStatus": {...}, +# "energyCreditStatus": {...}, +# } +``` + +## Check requirements + +`check_requirements(project)` returns the applicable requirements for a +project. + +```python +requirements = client.check_requirements(project) +``` + +## Generate a PDF report + +`generate_report(project, ...)` builds a PDF report. The PDF is stored in S3 +and the API returns a short-lived **presigned URL** (it expires within a few +minutes), along with the file name. + +```python +report = client.generate_report(project) +# {"url": "...", "expires": "in 5 minutes", "fileName": "report...pdf"} +``` + +This method does **not** open a browser — as a library it returns the +metadata and lets you decide what to do (e.g. `webbrowser.open(report["url"])`). + +### Selecting report sections + +Toggle which sections appear in the report (all default to `True`): + +```python +report = client.generate_report( + project, + envelope=True, + intlighting=True, + extlighting=False, + mechanical=True, +) +``` + +### Downloading the PDF + +Pass `download=True` to fetch the PDF from the presigned URL and save it using +the server-provided file name. `download_dir` defaults to your `~/Downloads` +folder: + +```python +report = client.generate_report(project, download=True) # ~/Downloads +report = client.generate_report(project, download=True, download_dir="./out") +``` diff --git a/examples/README.md b/examples/README.md index 8c75843..fa23138 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,6 +9,7 @@ examples/ ├── README.md # This file ├── client/ # API client examples │ ├── simulation.py # Simulation API workflow examples +│ ├── compliance_and_report.py # Compliance, requirements & report examples │ └── user_functions.py # User-facing function examples └── project_operations/ # Project operations examples ├── building_area_operations.py # Building area operations @@ -33,6 +34,19 @@ Demonstrates the simulation workflow for running compliance checks. python examples/client/simulation.py ``` +#### Compliance & Report Examples (`client/compliance_and_report.py`) +Demonstrates compliance checks, requirements lookup, and PDF report generation. + +**What it demonstrates:** +- `check_compliance()` - Checking code compliance for a project +- `check_requirements()` - Retrieving applicable requirements +- `generate_report()` - Generating a PDF report (presigned S3 URL, optional download) + +**Usage:** +```bash +python examples/client/compliance_and_report.py +``` + #### User Function Examples (`client/user_functions.py`) Demonstrates user-facing API client functions. diff --git a/examples/client/compliance_and_report.py b/examples/client/compliance_and_report.py new file mode 100644 index 0000000..c50599b --- /dev/null +++ b/examples/client/compliance_and_report.py @@ -0,0 +1,47 @@ +"""Example of using COMcheck API client compliance, requirements, and report functions.""" + +import os +from dotenv import load_dotenv +from comcheck_api.client import COMcheckClient +from comcheck_api.constants.common_constants import PROJECT_TEMPLATE +from comcheck_api.types.core_types import EnergyCodeOptions + +# Initialize client +load_dotenv() +client = COMcheckClient() +api_key = os.getenv("COM_API_KEY") or "your-api-key-here" +client.set_api_key(api_key) + +# Build a project to evaluate +project = PROJECT_TEMPLATE.model_copy(deep=True) +project.control.code = EnergyCodeOptions.CEZ_90_1_2022 + +# Example 1: Check code compliance for the project +compliance = client.check_compliance(project) +print(f"Compliance: {compliance}") + +# Example 2: Check the applicable requirements for the project +requirements = client.check_requirements(project) +print(f"Requirements: {requirements}") + +# Example 3: Generate a PDF report (returns metadata: url, expires, fileName) +report = client.generate_report(project) +if report: + print(f"Report metadata: {report}") + +# Example 4: Generate a report and download the PDF to ~/Downloads +report = client.generate_report( + project, + envelope=True, + intlighting=True, + extlighting=True, + mechanical=True, + download=True, +) +if report: + print(f"Downloaded report: {report['fileName']}") + +# Example 5: Download into a specific directory +report = client.generate_report(project, download=True, download_dir="./reports") +if report: + print(f"Saved report to ./reports/{report['fileName']}") diff --git a/mkdocs.yml b/mkdocs.yml index d9d8832..6a2d4c4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,7 @@ nav: - Types Guide: types-guide.md - COMcheckClient: api/client.md - Simulation: api/simulation.md + - Compliance & Reports: api/compliance.md - Project Operations: - Building Area: api/operations/building-area.md - Envelope: api/operations/envelope.md From 530869b4a49f7a198ecd477e00c0d64bfc493ea4 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Mon, 29 Jun 2026 17:50:51 -0700 Subject: [PATCH 5/7] make uvalue caculation standalone and reusable, update docs and skills related to it --- comcheck_api/ai/skill/SKILL.md | 12 ++- comcheck_api/ai/skill/reference/operations.md | 21 +++++ comcheck_api/ai/skill/reference/simulation.md | 3 +- comcheck_api/client/comcheck_client.py | 83 ++++++++----------- docs_site/api/simulation.md | 24 ++++++ examples/client/assemblies.py | 72 ++++++++++++++++ 6 files changed, 165 insertions(+), 50 deletions(-) create mode 100644 examples/client/assemblies.py diff --git a/comcheck_api/ai/skill/SKILL.md b/comcheck_api/ai/skill/SKILL.md index ec27645..0386dfe 100644 --- a/comcheck_api/ai/skill/SKILL.md +++ b/comcheck_api/ai/skill/SKILL.md @@ -57,6 +57,13 @@ Triggers: `get_simulation_result`. See `comcheck_api.types.SimulationStatus` for known lifecycle values (the catalog isn't exhaustive — only the terminal pair is guaranteed stable). +- **U-values are calculated server-side**: `update_uvalues(project)` + fetches the proposed/effective u-values for the envelope and writes + them back onto the matching `agWall`, `bgWall`, `roof`, and `floor` + assemblies (only these get calculated u-values; `effectiveUFactor` + is `agWall`-only). `start_run_simulation` calls this automatically, + so you rarely call it directly — use it only when you need refreshed + u-values on a project outside the simulation flow. ## Quick start @@ -148,8 +155,9 @@ print(result["performanceRating"]) editable through `project_building_area_operations`; the per- activity lighting nested under `activityUse[]` is not. The `COMcheckClient` user methods (`list_projects`, `get_project`, - `update_project`, `start_run_simulation`, `get_simulation_status`, - `get_simulation_result`, `set_api_key`) are fully supported and + `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_compliance`, `check_requirements`, `generate_report`) are also fully supported. If asked for an unsupported mutation area, diff --git a/comcheck_api/ai/skill/reference/operations.md b/comcheck_api/ai/skill/reference/operations.md index 3378f9e..f290618 100644 --- a/comcheck_api/ai/skill/reference/operations.md +++ b/comcheck_api/ai/skill/reference/operations.md @@ -99,3 +99,24 @@ roof.cavityRValue = 38.0 roof.orientation = OrientationOptions.UNSPECIFIED_ORIENTATION project = env_ops.add_roof_to_project(project, area_key, roof) ``` + +## U-value calculation requires a construction type + +When `update_uvalues` (or `start_run_simulation`) recalculates assembly +u-values, the engine needs a valid construction-type field to classify +each assembly — `roofType` for roofs, `wallType` for walls, etc. If it's +missing or null, the engine falls back to an `"Other"` classification, +returns a `propUValue` of `0.0`, and the response comes back with +`assemblyType: "Other"` instead of the value you sent. Because the client +matches results back by `assemblyType`, an `"Other"` result won't match +your assembly and its u-value is silently left unchanged. + +The default templates set these fields (e.g. `roofType=ABOVE_DECK_ROOF`), +so this only bites when you build an assembly by hand or clear the type. +Keep the construction-type field populated: + +```python +from comcheck_api.types import RoofTypeOptions + +roof.roofType = RoofTypeOptions.ABOVE_DECK_ROOF # don't leave this null +``` diff --git a/comcheck_api/ai/skill/reference/simulation.md b/comcheck_api/ai/skill/reference/simulation.md index 9359a88..26c790f 100644 --- a/comcheck_api/ai/skill/reference/simulation.md +++ b/comcheck_api/ai/skill/reference/simulation.md @@ -43,7 +43,8 @@ client = COMcheckClient(api_key="...") | Method | Returns | Purpose | |---|---|---| -| `client.start_run_simulation(project, project_id=None)` | `str` (session ID) | Kick off the simulation. If `project_id` is provided, the project is also saved/updated. | +| `client.start_run_simulation(project, project_id=None)` | `str` (session ID) | Kick off the simulation. Always refreshes the envelope u-values first (via `update_uvalues`). If `project_id` is provided, the project is also saved/updated. | +| `client.update_uvalues(project)` | `ComBuilding` | Calculate assembly u-values and write them back onto the project's `agWall`, `bgWall`, `roof`, and `floor` assemblies (matched by `assemblyType`). Mutates and returns the same project. Called automatically by `start_run_simulation`. | | `client.get_simulation_status(session_id)` | `dict` | Current status. Fields: `sessionId`, `status` (see the Status values table above), optional `message`. | | `client.get_simulation_result(session_id)` | `dict` | Final result. Fields: `sessionId`, `performanceRating`, `energyCreditPerformanceRating`, `proposedBpf`, `baselineBpf`. | diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index e93774c..416bff4 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -247,39 +247,46 @@ def _parse_data(self, data, mode): return ComBuilding(**data) return data - def assemblie_uvalues(self, project: ComBuilding) -> Any: - """ + def update_uvalues(self, project: ComBuilding) -> ComBuilding: + """Calculate assembly u-values and update the project in place. + + Sends the full envelope to the u-value endpoint and writes the returned + values back onto the matching ``agWall``, ``bgWall``, ``roof``, and + ``floor`` components (matched by ``assemblyType``). Only these assembly + types receive calculated u-values; ``effectiveUFactor`` applies to + ``agWall`` only. Args: - project: + project: The project whose envelope u-values should be refreshed. Returns: - + The same ``project`` instance, with u-values updated. """ - logger = logging.getLogger(__name__) - # update assemblies uvalue energy_code = str(project.control.code) envelope_data = project.envelope.model_dump(mode="json", exclude_unset=True) - - def fill_empty_description(assemblies: list[dict]) -> list[dict]: - for assembly in assemblies: - if not assembly.get("description", ""): - assembly["description"] = assembly.get("assemblyType") - return assemblies - - # need to refactor this part to ensure the data integrity - reformatted_envelope_data = { - "agWall": fill_empty_description(envelope_data["agWall"]), - "roof": fill_empty_description(envelope_data["roof"]), - "skylight": fill_empty_description(envelope_data["skylight"]), - "window": fill_empty_description(envelope_data["window"]), - "door": fill_empty_description(envelope_data["door"]), - "floor": fill_empty_description(envelope_data["floor"]), - } - assemblie_uvalue = self._service.assemblies_uvalue( - reformatted_envelope_data, energy_code - ) - return assemblie_uvalue["data"] + updated_assembly_uvalues = self._service.assemblies_uvalue( + envelope_data, energy_code + )["data"] + + for wall in project.envelope.agWall: + for uvalue in updated_assembly_uvalues.get("agWall", []): + if wall.assemblyType == uvalue["assemblyType"]: + wall.effectiveUFactor = float(uvalue["effectiveUFactor"]) + wall.propUValue = float(uvalue["propUValue"]) + for wall in project.envelope.bgWall: + for uvalue in updated_assembly_uvalues.get("bgWall", []): + if wall.assemblyType == uvalue["assemblyType"]: + wall.propUValue = float(uvalue["propUValue"]) + for roof in project.envelope.roof: + for uvalue in updated_assembly_uvalues.get("roof", []): + if roof.assemblyType == uvalue["assemblyType"]: + roof.propUValue = float(uvalue["propUValue"]) + for floor in project.envelope.floor: + for uvalue in updated_assembly_uvalues.get("floor", []): + if floor.assemblyType == uvalue["assemblyType"]: + floor.propUValue = float(uvalue["propUValue"]) + + return project def check_compliance(self, project: ComBuilding) -> Any: """Check code compliance for a project. @@ -383,30 +390,12 @@ def start_run_simulation( """ logger = logging.getLogger(__name__) - project_data = None + # Always refresh the calculated assembly u-values before simulating. + project = self.update_uvalues(project) + if project_id: logger.info("Updating project: %s", project_id) project = self.update_project(str(project_id), project) - else: - # update assemblies uvalue in the project - updated_assembly_uvalues = self.assemblie_uvalues(project) - # still missing - ag_Walls = project.envelope.agWall - roofs = project.envelope.roof - floors = project.envelope.floor - for wall in ag_Walls: - for uvalue in updated_assembly_uvalues.get("agWall", []): - if wall.assemblyType == uvalue["description"]: - wall.effectiveUFactor = float(uvalue["effectiveUFactor"]) - wall.propUValue = float(uvalue["propUValue"]) - for roof in roofs: - for uvalue in updated_assembly_uvalues.get("roof", []): - if roof.assemblyType == uvalue["description"]: - roof.propUValue = float(uvalue["propUValue"]) - for floor in floors: - for uvalue in updated_assembly_uvalues.get("floor", []): - if floor.assemblyType == uvalue["description"]: - floor.propUValue = float(uvalue["propUValue"]) project_data = project.model_dump(mode="json", exclude_unset=True) run_result = self._service.start_run_simulation(project_data) diff --git a/docs_site/api/simulation.md b/docs_site/api/simulation.md index bdb5f44..6a524ad 100644 --- a/docs_site/api/simulation.md +++ b/docs_site/api/simulation.md @@ -63,6 +63,30 @@ When you pass a `project_id`, the client saves the project via session_id = client.start_run_simulation(project, project_id="your-project-id") ``` +## Envelope u-values + +`start_run_simulation` always refreshes the calculated envelope u-values +before submitting, by calling `update_uvalues(project)`. This fetches the +proposed/effective u-values for the envelope assemblies and writes them +back onto the matching `agWall`, `bgWall`, `roof`, and `floor` components +(matched by `assemblyType`). Only these assembly types receive calculated +u-values, and `effectiveUFactor` applies to `agWall` only. + +You normally don't need to call it yourself, but it's available when you +want refreshed u-values on a project outside the simulation flow: + +```python +project = client.update_uvalues(project) +``` + +!!! warning "Each assembly needs a construction type" + The engine classifies an assembly by its construction-type field + (`roofType`, `wallType`, …) to calculate its u-value. If that field is + missing or null, the engine returns an `"Other"` classification with a + `propUValue` of `0.0`, and the result won't match your assembly — so its + u-value is silently left unchanged. The default templates set these + fields; keep them populated if you build an assembly by hand. + ## Simulation status `get_simulation_status` returns a dict with: diff --git a/examples/client/assemblies.py b/examples/client/assemblies.py new file mode 100644 index 0000000..ba8f07c --- /dev/null +++ b/examples/client/assemblies.py @@ -0,0 +1,72 @@ +"""Example of using COMcheck API client assembly u-value calculation.""" + +import os +from dotenv import load_dotenv +from comcheck_api.client import COMcheckClient +from comcheck_api.defaults import ( + get_default_project_template, + get_default_building_area_template, + get_default_ag_wall_template, + get_default_bg_wall_template, + get_default_roof_template, + get_default_floor_template, +) +from comcheck_api.project_operations import ( + project_building_area_operations, + project_envelope_operations, +) +from comcheck_api.types.core_types import EnergyCodeOptions + +# Initialize client +load_dotenv() +client = COMcheckClient() +api_key = os.getenv("COM_API_KEY") or "your-api-key-here" +client.set_api_key(api_key) + +# Build a project with one of each calculated-u-value assembly. Only agWall, +# bgWall, roof, and floor receive calculated u-values (effectiveUFactor is +# agWall-only). +project = get_default_project_template() +project.control.code = EnergyCodeOptions.CEZ_90_1_2022 + +project = project_building_area_operations.add_building_area_to_project( + project, get_default_building_area_template() +) +building_area_key = str(project.lighting.wholeBldgUse[0].key) + +project = project_envelope_operations.add_ag_wall_to_project( + project, building_area_key, get_default_ag_wall_template() +) +project = project_envelope_operations.add_bg_wall_to_project( + project, building_area_key, get_default_bg_wall_template() +) +project = project_envelope_operations.add_roof_to_project( + project, building_area_key, get_default_roof_template() +) +project = project_envelope_operations.add_floor_to_project( + project, building_area_key, get_default_floor_template() +) + + +def print_uvalues(label: str) -> None: + print(f"\n{label}") + for kind, items in [ + ("agWall", project.envelope.agWall), + ("bgWall", project.envelope.bgWall), + ("roof", project.envelope.roof), + ("floor", project.envelope.floor), + ]: + for item in items: + print( + f" {kind:7} {item.assemblyType!r:30} " + f"effectiveUFactor={getattr(item, 'effectiveUFactor', None)} " + f"propUValue={item.propUValue}" + ) + + +# Example: Calculate assembly u-values and update the project in place. +# (start_run_simulation calls this automatically; call it directly when you +# want refreshed u-values outside the simulation flow.) +print_uvalues("Before update_uvalues:") +project = client.update_uvalues(project) +print_uvalues("After update_uvalues:") From 9af40f9dc92aa562adf17f631f8531c8b9db7ae6 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Tue, 14 Jul 2026 13:18:30 -0700 Subject: [PATCH 6/7] Update the check compliance to check UA compliance --- comcheck_api/ai/skill/SKILL.md | 4 ++-- comcheck_api/ai/skill/reference/compliance.md | 8 ++++---- comcheck_api/api/api_services.py | 4 ++-- comcheck_api/client/comcheck_client.py | 6 +++--- docs_site/api/compliance.md | 4 ++-- examples/README.md | 2 +- examples/client/compliance_and_report.py | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/comcheck_api/ai/skill/SKILL.md b/comcheck_api/ai/skill/SKILL.md index 0386dfe..4426ed2 100644 --- a/comcheck_api/ai/skill/SKILL.md +++ b/comcheck_api/ai/skill/SKILL.md @@ -159,7 +159,7 @@ print(result["performanceRating"]) `get_simulation_status`, `get_simulation_result`, `set_api_key`) are fully supported and fine to use. The compliance/report client methods - (`check_compliance`, `check_requirements`, `generate_report`) are + (`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 @@ -247,7 +247,7 @@ directly. ```python # Per-category compliance status -compliance = client.check_compliance(project) +compliance = client.check_UA_compliance(project) if compliance["mandatoryRequirementsMet"]: ... diff --git a/comcheck_api/ai/skill/reference/compliance.md b/comcheck_api/ai/skill/reference/compliance.md index 6f3dc1b..b9d8da2 100644 --- a/comcheck_api/ai/skill/reference/compliance.md +++ b/comcheck_api/ai/skill/reference/compliance.md @@ -11,11 +11,11 @@ client = COMcheckClient(api_key="...") | Method | Returns | Purpose | |---|---|---| -| `client.check_compliance(project)` | `dict` | Per-category compliance status for the project. | +| `client.check_UA_compliance(project)` | `dict` | Per-category compliance status for the project. | | `client.check_requirements(project)` | `dict` | The applicable requirements for the project. | | `client.generate_report(project, ...)` | `dict` | Generate a PDF report; returns `{url, expires, fileName}`. | -## check_compliance +## check_UA_compliance Returns a dict with a top-level `mandatoryRequirementsMet` flag and a status object for each category: @@ -27,7 +27,7 @@ status object for each category: - `energyCreditStatus` ```python -compliance = client.check_compliance(project) +compliance = client.check_UA_compliance(project) if compliance["mandatoryRequirementsMet"]: ... ``` @@ -35,7 +35,7 @@ if compliance["mandatoryRequirementsMet"]: ## check_requirements Returns the applicable requirements payload for the project. Like -`check_compliance`, it's a single synchronous call — no polling. +`check_UA_compliance`, it's a single synchronous call — no polling. ```python requirements = client.check_requirements(project) diff --git a/comcheck_api/api/api_services.py b/comcheck_api/api/api_services.py index 299c00e..eaa4e1b 100644 --- a/comcheck_api/api/api_services.py +++ b/comcheck_api/api/api_services.py @@ -214,8 +214,8 @@ def assemblies_uvalue( except Exception as error: self._handle_api_error(error) - def check_compliance(self, project_data: Dict[str, Any]) -> Dict[str, Any]: - """Check compliance for a project. + def check_UA_compliance(self, project_data: Dict[str, Any]) -> Dict[str, Any]: + """Check UA path compliance for a project. Args: project_data: The project data to send in the request body diff --git a/comcheck_api/client/comcheck_client.py b/comcheck_api/client/comcheck_client.py index 416bff4..f59a744 100644 --- a/comcheck_api/client/comcheck_client.py +++ b/comcheck_api/client/comcheck_client.py @@ -288,8 +288,8 @@ def update_uvalues(self, project: ComBuilding) -> ComBuilding: return project - def check_compliance(self, project: ComBuilding) -> Any: - """Check code compliance for a project. + def check_UA_compliance(self, project: ComBuilding) -> Any: + """Check UA path compliance for a project. Args: project: The project data to evaluate. @@ -298,7 +298,7 @@ def check_compliance(self, project: ComBuilding) -> Any: The compliance results payload returned by the API. """ project_data = project.model_dump(mode="json", exclude_unset=True) - response = self._service.check_compliance(project_data) + response = self._service.check_UA_compliance(project_data) return response.get("data") def check_requirements(self, project: ComBuilding) -> Any: diff --git a/docs_site/api/compliance.md b/docs_site/api/compliance.md index 68f2060..5bcc607 100644 --- a/docs_site/api/compliance.md +++ b/docs_site/api/compliance.md @@ -5,7 +5,7 @@ and a PDF report generator. ## Check compliance -`check_compliance(project)` evaluates a project against its energy code and +`check_UA_compliance(project)` evaluates a project against its energy code and returns the per-category compliance status. ```python @@ -15,7 +15,7 @@ from comcheck_api.defaults import get_default_project_template client = COMcheckClient(api_key="your-key") project = get_default_project_template() -compliance = client.check_compliance(project) +compliance = client.check_UA_compliance(project) # { # "mandatoryRequirementsMet": ..., # "envelopeStatus": {...}, "interiorLightingStatus": {...}, diff --git a/examples/README.md b/examples/README.md index fa23138..36d6221 100644 --- a/examples/README.md +++ b/examples/README.md @@ -38,7 +38,7 @@ python examples/client/simulation.py Demonstrates compliance checks, requirements lookup, and PDF report generation. **What it demonstrates:** -- `check_compliance()` - Checking code compliance for a project +- `check_UA_compliance()` - Checking code compliance for a project - `check_requirements()` - Retrieving applicable requirements - `generate_report()` - Generating a PDF report (presigned S3 URL, optional download) diff --git a/examples/client/compliance_and_report.py b/examples/client/compliance_and_report.py index c50599b..43847e1 100644 --- a/examples/client/compliance_and_report.py +++ b/examples/client/compliance_and_report.py @@ -17,7 +17,7 @@ project.control.code = EnergyCodeOptions.CEZ_90_1_2022 # Example 1: Check code compliance for the project -compliance = client.check_compliance(project) +compliance = client.check_UA_compliance(project) print(f"Compliance: {compliance}") # Example 2: Check the applicable requirements for the project From 8fa04e2839a37c1141f88b768aed59eb1dd35eb0 Mon Sep 17 00:00:00 2001 From: yanz571 Date: Tue, 14 Jul 2026 13:26:40 -0700 Subject: [PATCH 7/7] update docs and pages about UA and full compliance path --- docs_site/api/compliance.md | 54 +++++++++++++++++++++--- examples/client/compliance_and_report.py | 49 +++++++++++++++++++-- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/docs_site/api/compliance.md b/docs_site/api/compliance.md index 5bcc607..6b50ad1 100644 --- a/docs_site/api/compliance.md +++ b/docs_site/api/compliance.md @@ -1,12 +1,56 @@ # Compliance & Reports -Beyond running a full simulation, the client exposes lighter-weight checks -and a PDF report generator. +## UA path vs. full compliance -## Check compliance +For ASHRAE 90.1-based codes (and state codes derived from them), there are two +distinct compliance checks: -`check_UA_compliance(project)` evaluates a project against its energy code and -returns the per-category compliance status. +| Check | Method | What it covers | +|---|---|---| +| UA path | `check_UA_compliance` | Envelope trade-off path only — fast, synchronous | +| Full compliance | `check_UA_compliance` → simulation | All systems (envelope, lighting, mechanical, renewables) | + +Use `check_UA_compliance` alone when you only need to verify the envelope +trade-off path. For a complete compliance determination — or to generate an +official report — run the UA check first, and if it passes, launch the full +simulation. + +## Full compliance workflow + +```python +import time +from comcheck_api import COMcheckClient +from comcheck_api.defaults import get_default_project_template +from comcheck_api.types import SimulationStatus + +client = COMcheckClient(api_key="your-key") +project = get_default_project_template() + +# Step 1: UA path check +ua = client.check_UA_compliance(project) +if not ua or not ua.get("mandatoryRequirementsMet"): + print("UA path failed — full simulation not needed.") +else: + # Step 2: Full simulation + session_id = client.start_run_simulation(project) + while True: + status = client.get_simulation_status(session_id) + if status["status"] == SimulationStatus.SUCCESS: + result = client.get_simulation_result(session_id) + print(f"Full compliance result: {result}") + break + if status["status"] == SimulationStatus.FAILED: + print(f"Simulation failed: {status.get('message')}") + break + time.sleep(5) +``` + +See [Simulation](simulation.md) for details on polling and result fields. + +## Check UA path compliance + +`check_UA_compliance(project)` evaluates the project's envelope assemblies +against the UA trade-off path and returns the per-category compliance status. ```python from comcheck_api import COMcheckClient diff --git a/examples/client/compliance_and_report.py b/examples/client/compliance_and_report.py index 43847e1..2c654ed 100644 --- a/examples/client/compliance_and_report.py +++ b/examples/client/compliance_and_report.py @@ -1,10 +1,31 @@ -"""Example of using COMcheck API client compliance, requirements, and report functions.""" +"""Example of using COMcheck API client compliance, requirements, and report functions. + +UA path vs. full compliance +---------------------------- +ASHRAE 90.1-based codes (and state codes derived from it) require TWO checks to +establish full envelope compliance: + + 1. UA path (check_UA_compliance) — envelope-only trade-off check. The + aggregate UA of proposed assemblies must not exceed the prescriptive + baseline. This is fast and synchronous. + + 2. Full simulation (start_run_simulation / get_simulation_result) — whole- + building energy simulation that covers all systems (envelope, lighting, + mechanical, renewables, etc.). Required when the UA check passes and a + complete code compliance determination is needed. + +If your project only needs to verify the envelope trade-off path, Example 1 is +sufficient. If you need a full compliance determination (e.g. to generate an +official report), run Example 1 first; if it passes, proceed with Example 6. +""" import os +import time from dotenv import load_dotenv from comcheck_api.client import COMcheckClient from comcheck_api.constants.common_constants import PROJECT_TEMPLATE from comcheck_api.types.core_types import EnergyCodeOptions +from comcheck_api.types import SimulationStatus # Initialize client load_dotenv() @@ -16,9 +37,9 @@ project = PROJECT_TEMPLATE.model_copy(deep=True) project.control.code = EnergyCodeOptions.CEZ_90_1_2022 -# Example 1: Check code compliance for the project +# Example 1: UA path compliance check (envelope trade-off path only) compliance = client.check_UA_compliance(project) -print(f"Compliance: {compliance}") +print(f"UA compliance: {compliance}") # Example 2: Check the applicable requirements for the project requirements = client.check_requirements(project) @@ -45,3 +66,25 @@ report = client.generate_report(project, download=True, download_dir="./reports") if report: print(f"Saved report to ./reports/{report['fileName']}") + +# Example 6: Full compliance check (ASHRAE 90.1-based codes) +# For a complete compliance determination, first verify the UA path passes, +# then run the full simulation. +ua_result = client.check_UA_compliance(project) +if not ua_result or not ua_result.get("mandatoryRequirementsMet"): + print("UA path failed — skipping full simulation.") +else: + session_id = client.start_run_simulation(project) + deadline = time.time() + 300 # 5 min timeout + while time.time() < deadline: + status = client.get_simulation_status(session_id) + if status["status"] == SimulationStatus.SUCCESS: + result = client.get_simulation_result(session_id) + print(f"Full compliance result: {result}") + break + if status["status"] == SimulationStatus.FAILED: + print(f"Simulation failed: {status.get('message')}") + break + time.sleep(5) + else: + print(f"Simulation {session_id} did not complete within 5 minutes.")