Model/backend
KhalimCK 04b7c26f67
PasHub SAP accuracy: infiltration inputs + immersion DHW + lights/lobby/heat-network (#1615)
* Electric-immersion DHW: bill at 100%, not the space boiler's efficiency 🟩

A separate electric immersion heater (SAP water-heating code 903) is 100%
efficient (SAP 10.2 Table 4a) and the space-heating boiler provides no water
heating, so Appendix D2.1 Eq D1 (the boiler seasonal-efficiency cascade) must
not apply to it. But `_water_heating_main` resolves DHW to the SPACE main — a
gas/oil boiler keeps its PCDB record — so three water-efficiency branches in
cert_to_inputs billed the immersion-heated cylinder at the boiler's ~87% summer
efficiency (a mongrel: electric fuel price x gas-boiler efficiency):

  1. the scalar `water_eff = water_pcdb_main.summer_efficiency_pct / 100`,
  2. the SAP §9.4.11 / Table 4c(2) -5pp no-interlock adjustment, and
  3. the Eq D1 (winter, summer) seasonal pair from `pcdb_main`.

Gate all three on `not dhw_is_electric_immersion`; the correct immersion path
(`_water_efficiency_with_category_inherit` -> 1.0, Eq D1 off) then applies,
mirroring the Table 3 zero-primary-loss gate already present for WHC 903.

General bug — fires for any WHC-903 dwelling whose space main is a PCDB gas/oil
boiler. It surfaced via the PasHub campaign's #1600 no-water-heating default
(WHC 999 -> 903) on a gas-combi dwelling: 58 Hackle St M11 4WU, SAP 55.30 ->
57.67 (pre_sap 58, verified 59).

Guardrails: the gov-API RdSAP corpus IMPROVES 78.8% -> 78.9% within-0.5, MAE
0.625 -> 0.622 (a handful of corpus certs with a boiler space main + electric
immersion move closer to accredited) — MAE ceiling ratcheted 0.626 -> 0.625.
Regression pinned in test_cert_to_inputs (RED before / GREEN after). pyright
0-new (cert_to_inputs baseline 30).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PasHub SAP accuracy: thread low-energy lights + draught lobby (from_site_notes) 🟩

Two systematic silent-drops in the PAS Hub `from_site_notes` path, each a field
the gov-API/Elmhurst mappers thread but site-notes dropped, letting a favourable
default reach the SAP-10.2 calculator. Validated against the correct pashub
oracle (property_baseline_performance.effective_sap_score, portfolio 838):
cohort within-0.5 55.1% → 62.6%, MAE 0.599 → 0.521.

- Low-energy lights: when "exact LED/CFL known = No", PAS Hub lodges an aggregate
  "Number of fixed low energy lights?" count. Previously dropped (no dataclass
  field / extractor key / mapper thread) → calculator saw 0 low-energy bulbs and
  applied the pessimistic L5b/L8c no-data default, under-rating SAP. Now threaded
  into low_energy_fixed_lighting_bulbs_count, mirroring from_elmhurst_site_notes.
  ~6% of the cohort; resolves 58 Hackle (−0.43 vs oracle 59 with #1615).
- Draught lobby: _map_sap_ventilation set only the legacy `draught_lobby` field,
  never the canonical `has_draught_lobby` §2 (13) gate the cascade reads, so the
  surveyed lobby was ignored and infiltration over-stated. Now mirrors Elmhurst.

Two other audited levers were REJECTED against the pashub oracle (they matched
the gov-cert/RdSAP convention but pashub does not apply them): percent_draughtproofed
(cohort 30.8%) and the §A.2.2 assumed secondary heater (19.2%). A heat-network
control-code fix (2306→2303 for 16 Bingley) is HELD pending spec/Elmhurst
adjudication — it contradicts ADR-0053; see the NOTE on _PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PasHub heat-network control: room-thermostat-only maps to 2308, not 2306 🟩

The Table 4e Group 3 label "charging linked to use of community heating, room
thermostat only" was mapped to 2306 — the linked-to-use *with-TRVs* code
(control type 3, space charging 1.00). The label explicitly excludes TRVs, so
it is control type 2 / space 1.05 = code 2308/2309 (linked-to-use → DHW 1.00).
2306 asserted TRVs the survey doesn't have and over-rated the cohort's sole
heat-network dwelling (16 Bingley Close 56.89 → 54.01 vs pashub oracle 52).

Verified against SAP 10.2 Table 4e as encoded in cert_to_inputs.py
(_CONTROL_TYPE_BY_CODE 2308→2, space-charging 2308→1.05, DHW 2308→1.00) and the
RdSAP control-label vocabulary in MainheatControlAttributes.py. 2303 (the earlier
audit's guess) is rejected: it is a flat-rate code (DHW 1.05) whose oracle match
was a coincidence of two offsetting spec violations. The residual +2.0 to oracle
52 is the documented SAP-10.2-engine-vs-lodged offset, not a fuel/flags gap
(those were threaded by the #1590 follow-up) — supersedes the ADR-0053 /
HANDOVER_838 "community fuel/flags" attribution.

16 Bingley is the only heat-network fixture in portfolio 838, so this moves one
fixture strictly toward its oracle. Harness ratcheted: within-0.5 0.51→0.58,
MAE 0.625→0.521 (observed 58.5% / 0.520 with all three 2026-07-15 levers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PasHub SAP accuracy: restore 3 dropped §2 infiltration inputs (from_site_notes)

`from_site_notes` uniquely dropped three ventilation inputs that the gov-API
and Elmhurst sibling mappers all set, causing a systematic cohort-wide SAP
under-rate vs the company's own accredited SAP-10.2 certs
(property_baseline_performance.effective_sap_score, portfolio 838):

1. percent_draughtproofed — never set, so §2(15) window infiltration was
   pinned at its 0.25-ACH worst case on every dwelling. Reconstructed as the
   area-weighted % of draught-proofed windows (mirrors Elmhurst
   `draught_proofing_percent`). Dominant term.
2. Upper-storey +0.25 m joist void — omitted; now added, matching
   `_UPPER_FLOOR_HEIGHT_ADD_M` on the gov-API/Elmhurst paths. This
   RE-ADJUDICATES #1601 ("keep raw"): that call was confounded by the
   then-present draught-proofing drop suppressing every verified dwelling.
3. sheltered_sides — left None → calculator's flat default of 2, which
   over-shelters end/semi/detached (RdSAP §S5 = 1/1/0). Now derived from
   built form.

The three are NON-ADDITIVE — each overshoots alone (which is why all three
were previously rejected individually) — but together they land all 7
verified ground-truth dwellings toward truth (none regress) and every built
form near zero. Cohort 62.1% -> 83.3% within-0.5, MAE 0.507 -> 0.377.

Guardrails: gov-API RdSAP corpus unchanged (78.9%, these are from_site_notes-
only helpers). pashub harness 58.5% -> 79.5% / MAE 0.520 -> 0.389; ratchets
tightened 0.58->0.78 and 0.521->0.40. pyright zero-new (mapper baseline 39).
New focused tests in TestFromSiteNotesInfiltrationFixes cover all three
fields incl. area-weighting and the built-form shelter map; goldens updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 00:30:55 +01:00
..
address2UPRN refactor(address2uprn): name the match/decision return types; rename helper 2026-07-07 16:00:00 +00:00
addresses fixing broken unit tests 2026-05-01 08:46:17 +00:00
apis GoogleSolarApi translates BuildingInsightsNotFoundError to sentinel dict 🟩 2026-06-01 16:28:47 +00:00
app Add remaining planning/address-profiling fields to HubSpot ETL sync 2026-07-08 15:12:24 +00:00
bulk_address2uprn_combiner python upgraded due to enum 2026-06-12 15:47:28 +00:00
categorisation python upgraded due to enum 2026-06-12 15:47:28 +00:00
condition smoke tests 2026-05-14 16:57:31 +00:00
diagnostics switched off hhrsh for comunity heating in place 2026-01-06 18:35:08 +00:00
docker fix engine dockerfile 2026-06-12 16:07:39 +00:00
documents_parser PasHub SAP accuracy: infiltration inputs + immersion DHW + lights/lobby/heat-network (#1615) 2026-07-16 00:30:55 +01:00
ecmk_fetcher upgraded python version 2026-06-17 09:28:24 +00:00
engine Merge branch 'main' into feature/integrate_new_epc_with_historical_epc 2026-05-13 08:38:50 +00:00
epc_api Fix cylinder_size/boiler_flue_type input errors for UPRN 100010086084 (SAP-15.0) 2026-07-10 09:11:41 +00:00
etl fixed s3 location 2026-05-11 08:44:55 +00:00
export feat(modelling): drop the plan_recommendations m2m 2026-06-03 21:13:00 +00:00
ml_models Pulling out rebaseling predictions 2026-03-23 19:45:19 +00:00
onboarders testing rebaselining for Instagroup and changing multi glaze proportion for Partiy onboarder to 0-100 2026-03-24 18:21:19 +00:00
ordnanceSurvey python upgraded due to enum 2026-06-12 15:47:28 +00:00
pashub_fetcher Drop the unused DownloadedFile import from the client tests 🟪 2026-07-14 14:07:23 +00:00
postcode_splitter python upgraded due to enum 2026-06-12 15:47:28 +00:00
scripts added local devconaitner 2026-04-16 22:21:54 +00:00
tests pr review, move domain and orhcestration 2026-06-01 14:00:31 +00:00
utils removing redundant code 2026-05-13 08:40:51 +00:00
.env.example working on integrating new EPC api into address2UPRN 2026-04-27 11:32:44 +00:00
.env.test address final PR comments 2026-02-27 15:27:07 +00:00
__init__.py Trying to get /backend and /model_data working together 2023-07-18 16:45:11 +01:00
DbClient.py pulling together OS client and DBClient 2023-12-30 20:48:40 +00:00
Funding.py implemented some handling for mixed translation descriptions 2025-11-30 18:16:09 +00:00
OrdnanceSurvey.py deleted the wrong folder aded back the origional 2026-03-06 14:58:49 +00:00
Outputs.py feat(modelling): cut plan→recommendation readers onto plan_id 2026-06-03 21:09:43 +00:00
package-lock.json re-deploy route53 to use dev domain and updating github actions and sls 2023-07-17 14:14:02 +01:00
package.json re-deploy route53 to use dev domain and updating github actions and sls 2023-07-17 14:14:02 +01:00
Property.py added env impact scores to db 2026-04-09 16:31:50 +01:00
README.md empty commit 2026-03-06 10:58:20 +00:00
run_curl.sh add my code to main 2025-11-14 13:36:09 +00:00
run_local.sh implemented onboarding 2026-04-21 20:23:33 +00:00
SearchEpc.py adding hacky handling for matching on lmk key or uprn 2026-04-09 14:47:54 +01:00
test_event.json downgrade cryptography 2023-07-17 19:23:09 +01:00

Backend

This is the api service that will supply the frontend with the insights that are driven by the machine learning and data modelling services.

Usage

Prerequisites

Python 3.8+ Poetry for managing project dependencies and virtual environment.

Installation and setup

  1. Clone this directory and navigate into the project directory.
git clone https://github.com/Hestia-Homes/Model.git
cd backend
  1. For environment management, I'm using conda with pycharm which is a convenient setup for development on a mac M1 however using tools such as poetry or pipenv is also fine.

For example, to install conda and create a virtual environment for this project, run the following commands:

conda create -n backend python=3.10
conda activate backend

then enter the virtual environment and install the dependencies using conda.

conda install --file requirements/base.txt
  1. Duplicate .env.example and rename it to .env
cp .env.example .env
  1. Open .env and fill in the required environment variables.

Running the Application

from model/backend/ you can run with the following command:

uvicorn app.main:app --reload

Or run sh run_local.sh, which runs that same uvicorn command.

You application will be available at the designated url

API Documentation

FastAPI automatically generates interactive API documentation for your application. To access the docs, start your server and visit /docs in your browser. Alternatively, you can go to /redoc to view the documentation in the ReDoc format.

Building the lambda's backend docker image locally

To build the backend docker image locally, run the following command from the root of the project directory:

docker build -t fastapi-lambda-image:latest -f backend/docker/lambda.Dockerfile .

To check the size of the resulting image, run the following command:

docker images | grep fastapi-lambda-image

To run a shell inside the Docker container to inspect its contents, run:

docker run -it fastapi-lambda-image:latest /bin/bash

Running in lambda results in running in a slightly different format compared to running the fastapi application locally. If you want to run the fastapi application locally, in docker, we have a docker file which builds the same environment as in lambda but runs the fast api application with uvicorn.

Run

docker build -t fastapi-local-image:latest -f backend/docker/Dockerfile .

This will be the image. To run it, simply run

docker run -p 8000:8000 -v ~/.aws:/root/.aws fastapi-local-image:latest

This assumes you have a ~/.aws folder with your aws credentials in it. If you don't have this, you can run the following command with your aws access token exported into your environment.

docker run -p 8000:8000 -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION fastapi-local-image:latest

Emulating the lambda locally

I have set up a script called run_local_lambda.sh which will allow you to emulate the lambda locally. You need to have a .env file with the necessary environment variables at backend/env and also and aws credentials file at ~/.aws/credentials, locally.

To run this, firstly run:

chmod +x run_lambda_local.sh

Now you can run the script with

./run_lambda_local.sh

In order to make a request to it, there is a specific format the request must be in, to emuate lambda. If using postman, the url you want is http://localhost:8000/2015-03-31/functions/function/invocations and you need to pass a body like this:

{
  "httpMethod": "POST",
  "body": "{\"portfolio_id\": 4, \"housing_type\": \"Private\", \"goal\": \"Increase EPC\", \"goal_value\": \"C\", \"trigger_file_path\": \"2/4/portfolio_plan_properties-20230724T093542483Z.csv\"}",
  "path": "/v1/plan/trigger",
  "resource": "/",
  "headers": {
    "Accept": "*/*",
    "Content-Type": "application/json",
    "Authorization": "Bearer YOUR_TOKEN_HERE",
    "x-api-key": "YOUR_API_KEY_HERE"
  },
  "requestContext": {},
  "multiValueQueryStringParameters": null
}

Logs for the container can quickly be seen via Docker desktop

Testing

To run tests, run the following command from the root of the project directory:

pytest

Local Development

During local development, you may need to generate and use a dummy JWT to test protected endpoints of the application.

Generating a Dummy JWT

FastAPI provides a convenient way to generate a dummy JWT for testing. To generate a dummy JWT, follow the steps below:

Make sure your application is running in a local environment. The dummy token endpoint is only available in a local environment.

While your application is running, visit the /dummy-token endpoint using a tool like curl or any HTTP client like Postman.

For instance, if your server is running locally on port 8000, you can use curl to get a dummy token:

curl http://localhost:8000/local/dummy-token

You will receive a response containing the dummy JWT

{
  "dummy_token": "<Your Dummy Token>"
}

Using the Dummy JWT

Once you've obtained a dummy JWT, you can use it to make requests to protected endpoints in your application:

  1. When making a request, include an Authorization header with the value Bearer . Replace with the token you received from the /dummy-token endpoint.

  2. Now you can make requests to the protected endpoints of the application.

Remember, the dummy JWT is meant for testing purposes only and should not be used in production environments. The /dummy-token endpoint is not available in non-local environments.

Custom Domain Setup for AWS API Gateway

Before you deploy your Serverless application for the first time, you need to set up a custom domain for AWS API Gateway. This is done using the sls create_domain command, which creates a custom domain in API Gateway that your services can use.

To set up a custom domain, use the following command:

sls create_domain --stage dev --aws-profile DevAdmin --verbose

Replace dev with the name of the stage you're deploying to. This command only needs to be run once per custom domain, and not every time you deploy your application. After running this command, you can associate your AWS Lambda functions with this domain using the customDomain configuration in your serverless.yml file.

This command requires the Serverless Domain Manager plugin, so make sure you have it installed and properly configured in your serverless.yml file.

Please note that the process of creating and associating a custom domain can take up to 40 minutes. Once the custom domain is created, it's immediately available for use in your Serverless applications.

Remember to replace DevAdmin with the profile that has appropriate permissions in your AWS account. The --verbose flag is optional and is used to print detailed logs to the console.

Creating a CNAME Record in Google Domains

After deploying the AWS Lambda function for the first time, you need to set up a CNAME record in Google Domains to route traffic from your custom domain to the CloudFront distribution created by API Gateway. This will re-route traffic from your custom domain to the CloudFront distribution created by API Gateway, and therefore to your lambda. See here for AWS' documentation on this.

You can find the CloudFront domain by going to the API Gateway console and clicking on Custom Domain Names.

Here are the steps to create a CNAME record:

  1. Log in to Google Domains.
  2. Select the name of your domain.
  3. Open the menu, if it's not already open.
  4. Click "DNS."
  5. Scroll down to the "Custom resource records" section.
  6. In the "Name" field, enter your subdomain (e.g., api if your API is available at api.example.com).
  7. In the "Type" dropdown menu, select "CNAME."
  8. In the "TTL" field, enter 1H to set it to 1 hour (or another suitable value).
  9. In the "Data" field, enter the CloudFront domain that was created by API Gateway (you can find this in the API Gateway console, under Custom Domain Names).
  10. Click "Add."

This will direct any traffic from your custom domain to your AWS CloudFront distribution. Please note that DNS changes might take some time (up to 24-48 hours in some cases) to propagate across the internet.

Also, please make sure that your CloudFront distribution is configured to accept your custom domain as a valid domain name. In AWS API Gateway, under Custom Domain Names, make sure that your custom domain is listed and mapped to the appropriate API stage.

Remember to replace api and the CloudFront domain with your actual subdomain and CloudFront domain.

Certainly! Here's a detailed documentation for your README:


Deployment Troubleshooting for fastapi-lambda

Context:

When deploying the fastapi-lambda using Serverless Framework, you may encounter issues related to domain management, especially if you're using a custom domain for your API. This documentation provides troubleshooting steps and details on how to resolve potential conflicts.

Potential Issues & Solutions:

1. Conflict with Existing CloudFront Distribution:

Error Message:

csharpCopy code

One or more aliases specified for the distribution includes an incorrectly configured DNS record that points to another CloudFront distribution.

Cause: This can occur if there's an existing CNAME record in your DNS provider pointing to a CloudFront distribution.

Solution:

  • Check your DNS provider (e.g., Google Domains) and verify the CNAME record for api.dev.hestia.homes.
  • Temporarily remove or update the conflicting CNAME record.
  • Run the sls create_domain command again.
  • Update the DNS settings in your DNS provider based on the new configuration provided by the serverless-domain-manager plugin.

2. Conflict with Route53:

Error Message:

csharpCopy code

Deleting RestApi failed. Please remove all base path mappings related to the RestApi in your domains.

Cause: This can occur if there are residual AWS configurations, especially in Route53, from previous deployments.

Solution:

  • Navigate to the AWS Route53 Console.
  • Identify and delete any residual Hosted Zones or Record Sets related to api.dev.hestia.homes.
  • Ensure that you have backed up any necessary configurations before deleting.

3. Other AWS Resources Conflicts:

You might encounter issues where AWS resources, such as S3 buckets or CloudFront distributions, are not properly deleted or are conflicting with new deployments.

Solution:

  • Navigate to the respective AWS service dashboard.
  • Manually identify and rectify any conflicting resources. This might involve emptying S3 buckets or deleting CloudFront distributions.
  • Ensure backups and proper precautions before deleting any resources.

Additional Notes:

  • Backup Configurations: Always backup your configurations before making changes. This ensures that you can revert to a previous state if needed.
  • DNS Propagation: Remember that DNS changes can take some time to propagate globally. After making DNS changes, you might not see immediate effects.
  • CloudFront Distributions: If you can't find a CloudFront distribution in the AWS CloudFront console, it's possible that it was automatically created by another AWS service like API Gateway. It might need to be managed or deleted from that service's dashboard.

After succesfully running creating the custom domain

After successfully creating the custom domain with the serverless-domain-manager plugin, you should add back the CNAME record into Google Domains (or whatever platform is being used to manage domains now) to ensure that the custom domain properly points to the CloudFront distribution managed by AWS.

Here's what you should do:

  1. Log in to Google Domains:

    • Go to Google Domains.
    • Navigate to the management page for hestia.homes.
  2. Add/Update the CNAME Record:

    • Find the section for custom resource records.
    • Add (or update if it already exists) a CNAME record for api.dev.
    • Point it to the CloudFront distribution domain name (e.g., d2d269kjy1nyhz.cloudfront.net.). Ensure you include the trailing dot at the end. This can be found in API gateway
  3. Check DNS Propagation:

    • Keep in mind that DNS changes might take some time to propagate. You can use online tools like DNS Checker to verify the propagation status worldwide.
    • Test your API endpoint api.dev.hestia.homes to ensure it's resolving correctly and accessing your Lambda function.

By following these steps, you should have your custom domain properly configured and pointing to your AWS Lambda function via the CloudFront distribution