Skip to content

Commit 26bc117

Browse files
author
1jhu1
committed
Merge branch 'door-enhancement-rsmeans' of https://github.com/NatLabRockies/openstudio-ee-gem into replace-windows-EC
2 parents 1025c4f + d952480 commit 26bc117

13 files changed

Lines changed: 1006 additions & 1366 deletions

File tree

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,86 @@
1-
# Door Enhancement Measure
2-
3-
## Summary
4-
The Door Enhancement measure improves door-related performance in OpenStudio models by:
5-
- reducing infiltration in spaces containing doors,
6-
- optionally replacing doors and adding seals,
7-
- calculating embodied carbon from EC3 EPD data,
8-
- estimating costs through RSMeans or custom cost inputs.
9-
10-
## Cost Modes
11-
- RSMeans mode (`use_custom_costs = False`): derives a door search term from model size/material context, runs multi-catalog lookup (`bc-mf`, `gb-mf`, `rp-mf`), applies 10% overhead/profit, and stores summary + hit details in AdditionalProperties.
12-
- Custom mode (`use_custom_costs = True`): bypasses RSMeans API and uses user-provided cost inputs directly.
13-
14-
## RSMeans Fallback Behavior
15-
When direct RSMeans search has no match, the helper can use door-specific fallback unit cost line IDs for known materials (for example weatherstrips, automatic door bottom, and core steel door variants). The measure logs fallback warnings and fallback counts when this path is used.
16-
17-
## Credentials
18-
- EC3: API token read from `config.ini` (`[EC3_API_TOKEN] API_TOKEN=...`) or environment.
19-
- RSMeans: `client_id` and `client_secret` environment variables (or `.env`).
20-
21-
## Primary Files
22-
- `measure.py` - main measure logic
23-
- `resources/call_rsmeans_api.py` - RSMeans lookup client/helper
24-
- `resources/EC3_lookup.py` - EC3 data retrieval and processing
25-
- `apply_measure.py` - local integration harness
26-
27-
## Additional Docs
28-
- `docs/USAGE_GUIDE.md` - user workflow and examples
29-
- `docs/QUICK_REFERENCE.md` - concise operational reference
30-
- `docs/TECHNICAL.md` - architecture, data flow, and algorithms
1+
# DoorEnhancement
2+
3+
This measure improves door performance in OpenStudio models by adding sealing products (weatherstrip, automatic door bottom, silicone smoke gasket) and optionally replacing existing doors with more thermally efficient alternatives. It estimates added embodied carbon from EC3 EPD data and estimates installed cost using RSMeans (or user-provided custom cost inputs).
4+
5+
## What It Does
6+
7+
- Reduces space infiltration rates by a user-specified percentage to simulate improved air sealing from weatherstripping.
8+
- Fetches EC3 EPD data for the selected bottom seal, top/side seal, and door replacement materials; calculates life-cycle embodied carbon (kg CO₂ eq) over the analysis period.
9+
- When replacing doors, updates door construction R-values from material properties (thickness, conductivity, density).
10+
- Runs RSMeans lookup for door components (door unit, bottom seal, top/side seal) and stores full match/search diagnostics.
11+
- Writes standardized outputs into the five standard AdditionalProperties buckets.
12+
13+
## Arguments
14+
15+
| Argument | Type | Default | Description |
16+
|---|---|---|---|
17+
| `space_type` | Choice | *Entire Building* | Space type to apply infiltration reduction |
18+
| `space_infiltration_reduction_percent` | Double | 30.0 | Infiltration reduction (%) |
19+
| `alter_coef` | Boolean | false | Alter temperature/wind coefficients (disabled; always preserved) |
20+
| `door_area_per_unit` | Double | 1.95 | Door leaf area per unit (m²) per PCR declared unit |
21+
| `analysis_period` | Integer | 30 | Analysis period for embodied carbon (years) |
22+
| `door_bottom_seal_option` | Choice | automatic door bottom | Bottom seal product (none/brush weatherstrip/automatic door bottom/silicone adhesive smoke gasket) |
23+
| `door_top_side_seal_option` | Choice | jamb weatherstrip | Top and side seal product (none/silicone adhesive smoke gasket/jamb weatherstrip) |
24+
| `door_option` | Choice | wooden door | Door replacement type (none or any steel/glass/wood/garage option) |
25+
| `strip_lifetime` | Integer | 15 | Service life of sealing strip (years) |
26+
| `door_lifetime` | Integer | 30 | Service life of door (years) |
27+
| `gwp_statistic` | Choice | median | GWP statistic for EPD values (minimum/maximum/mean/median) |
28+
| `api_key` | String || EC3 API token |
29+
| `length_per_unit_bottom_side` | Double | 0.9144 | Length per unit of bottom seal (m); 0.0 = use product default |
30+
| `length_per_unit_other_sides` | Double | 5.1816 | Length per unit of top/side seal (m); 0.0 = use product default |
31+
| `door_thermal_conductivity` | Double | 0.0 | Door conductivity (W/m·K); 0.0 = use material default |
32+
| `door_density` | Double | 0.0 | Door density (kg/m³); 0.0 = use material default |
33+
| `door_thickness` | Double | 0.0 | Door thickness (m); 0.0 = use material default |
34+
| `use_custom_costs` | Boolean | false | Skip RSMeans; use custom cost inputs |
35+
| `custom_door_cost_per_unit` | Double | 0.0 | Custom door cost ($/m²) |
36+
| `custom_bottom_seal_cost` | Double | 0.0 | Custom bottom seal cost ($/m) |
37+
| `custom_top_side_seal_cost` | Double | 0.0 | Custom top/side seal cost ($/m) |
38+
| `rsmeans_unit_costline_id` | String | "" | Optional exact RSMeans unit cost line ID |
39+
40+
## AdditionalProperties Bucket Map
41+
42+
| OpenStudio Object | Variable | Contents |
43+
|---|---|---|
44+
| Building | `basic_input` | Measure name, analysis period, GWP statistic, construction names |
45+
| Site | `reno_detail` | Door/seal options, total door area, sealing lengths, door count |
46+
| Facility | `factors` | Cost totals, cost_source, cost_factor_basis, cost_unit_basis, GWP factors |
47+
| SimulationControl | `results` | Mirrored cost scalars + three RSMeans diagnostic JSON payloads |
48+
| SizingParameters | `mtrl_prop` | Door material properties, seal lengths, RSMeans-extracted hints |
49+
50+
## Cost Source Modes
51+
52+
- `rsmeans_api` — RSMeans search or exact-ID lookup succeeded.
53+
- `custom_input` — custom cost mode used.
54+
- `none` — costing disabled or unresolved.
55+
56+
Cost basis is persisted as:
57+
- `door_enhancement_cost_factor_basis` (e.g. `cost_per_unit`, `cost_per_length`, `mixed`, `custom_cost_per_unit`)
58+
- `door_enhancement_cost_unit_basis` (e.g. `EA`, `LF`, `EA, LF`)
59+
60+
## RSMeans Search Strategy
61+
62+
When `use_custom_costs = False`, the measure builds search materials for:
63+
- The door (unit: `ea`, division `08`)
64+
- Bottom seal (unit: `lf`, division `08`)
65+
- Top/side seal (unit: `lf`, division `08`)
66+
67+
These are searched across three catalogs (`bc-mf`, `gb-mf`, `rp-mf`). If no search result scores ≥ 50.0 (the minimum acceptable match score), the helper falls back to hard-coded costline IDs in `DOOR_FALLBACK_RSMEANS_IDS`. Full search diagnostics are stored in three JSON fields on `SimulationControl`.
68+
69+
## Quick Start
70+
71+
```powershell
72+
cd lib/measures/door_enhancement
73+
./setup_environment.ps1
74+
python apply_measure.py
75+
```
76+
77+
Outputs are written to:
78+
- `tests/output/DOE_small_office_door_enhanced.osm`
79+
- `tests/output/apply_measure_results.json`
80+
81+
## Documentation
82+
83+
- [docs/USAGE_GUIDE.md](docs/USAGE_GUIDE.md) — user workflow and examples
84+
- [docs/QUICK_REFERENCE.md](docs/QUICK_REFERENCE.md) — concise operational reference
85+
- [docs/TECHNICAL.md](docs/TECHNICAL.md) — architecture, data flow, and algorithms
86+
- [docs/CUSTOM_COSTS_IMPLEMENTATION.md](docs/CUSTOM_COSTS_IMPLEMENTATION.md) — custom cost mode details

lib/measures/door_enhancement/apply_measure.py

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
1. Loads a test OSM file from the tests/ folder
66
2. Runs the ModelMeasure with a set of configurable arguments
77
3. Saves the modified model to tests/output/
8-
4. Verifies that AdditionalProperties were attached to modified door subsurfaces
8+
4. Verifies that AdditionalProperties were attached to the standard
9+
5-bucket objects (Building, Site, Facility, SimulationControl,
10+
SizingParameters)
911
"""
1012

1113
from pathlib import Path
@@ -16,15 +18,37 @@
1618
# ---------------------------------------------------------------------------
1719
# OpenStudio path setup
1820
# ---------------------------------------------------------------------------
19-
OPENSTUDIO_VERSION = "3.11.0"
20-
openstudio_path = f"/Applications/OpenStudio-{OPENSTUDIO_VERSION}/Python"
2121

22-
if Path(openstudio_path).exists():
23-
sys.path.insert(0, openstudio_path)
24-
print(f"Using OpenStudio from: {openstudio_path}")
25-
else:
26-
print(f"Warning: OpenStudio path not found at {openstudio_path}")
27-
print("Will attempt to use system OpenStudio installation")
22+
def configure_openstudio_python_path():
23+
"""Locate and add an OpenStudio Python directory to sys.path.
24+
25+
Checks (in order):
26+
1. OPENSTUDIO_PYTHON_PATH environment variable.
27+
2. Standard Windows install paths for versions 3.11, 3.10, 3.9.
28+
3. Standard macOS install paths for the same versions.
29+
4. Assumes the openstudio package is already importable (installed).
30+
"""
31+
import os
32+
explicit = os.environ.get("OPENSTUDIO_PYTHON_PATH", "").strip()
33+
if explicit and Path(explicit).exists():
34+
sys.path.insert(0, explicit)
35+
print(f"Using OpenStudio from OPENSTUDIO_PYTHON_PATH: {explicit}")
36+
return
37+
38+
candidates = []
39+
for ver in ("3.11.0", "3.10.0", "3.9.0"):
40+
candidates.append(Path(f"C:/openstudio-{ver}/Python"))
41+
candidates.append(Path(f"/Applications/OpenStudio-{ver}/Python"))
42+
for candidate in candidates:
43+
if candidate.exists():
44+
sys.path.insert(0, str(candidate))
45+
print(f"Using OpenStudio from: {candidate}")
46+
return
47+
48+
print("Using OpenStudio from installed package")
49+
50+
51+
configure_openstudio_python_path()
2852

2953
import openstudio
3054
from measure import DoorEnhancement
@@ -175,20 +199,27 @@ def print_runner_output(runner):
175199

176200
def verify_additional_properties(model):
177201
"""
178-
Check the new separate Facility AdditionalProperties (summary block).
179-
Returns a list of (object_name, prop_name, value) tuples.
202+
Check all five standard AdditionalProperties buckets and return a list of
203+
(bucket_label, property_name, value) tuples.
180204
"""
181205
found = []
182-
facility = model.getFacility()
183-
ap = facility.additionalProperties()
184-
for feature_name in ap.featureNames():
185-
val_opt = ap.getFeatureAsDouble(feature_name)
186-
if val_opt.is_initialized():
187-
found.append(("Facility", feature_name, val_opt.get()))
188-
else:
189-
val_str = ap.getFeatureAsString(feature_name)
190-
if val_str.is_initialized():
191-
found.append(("Facility", feature_name, val_str.get()))
206+
207+
def _collect_props(label, ap):
208+
for feature_name in ap.featureNames():
209+
val_opt = ap.getFeatureAsDouble(feature_name)
210+
if val_opt.is_initialized():
211+
found.append((label, feature_name, val_opt.get()))
212+
else:
213+
val_str = ap.getFeatureAsString(feature_name)
214+
if val_str.is_initialized():
215+
found.append((label, feature_name, val_str.get()))
216+
217+
_collect_props("Building", model.getBuilding().additionalProperties())
218+
_collect_props("Site", model.getSite().additionalProperties())
219+
_collect_props("Facility", model.getFacility().additionalProperties())
220+
_collect_props("SimulationControl", model.getSimulationControl().additionalProperties())
221+
_collect_props("SizingParameters", model.getSizingParameters().additionalProperties())
222+
192223
return found
193224

194225

@@ -265,16 +296,27 @@ def _sv_value(sv):
265296

266297
# Verify AdditionalProperties on Building
267298
print("\n" + "=" * 80)
268-
print("VERIFYING SEPARATE FACILITY ADDITIONAL PROPERTIES")
299+
print("VERIFYING SEPARATE ADDITIONAL PROPERTIES")
269300
print("=" * 80)
270301

271302
ap_data = verify_additional_properties(model)
272303
if ap_data:
273-
print(f"Found {len(ap_data)} properties in separate Facility AdditionalProperties:")
304+
print(f"Found {len(ap_data)} properties in separate AdditionalProperties:")
274305
for obj_name, prop_name, value in ap_data:
275306
print(f" [{obj_name}] {prop_name}: {value}")
276307
else:
277-
print(" No properties found on Facility.")
308+
print(" No properties found on standard buckets.")
309+
310+
# Gather cost_factor_basis and cost_unit_basis for summary
311+
cost_factor_basis = next(
312+
(v for lbl, k, v in ap_data if k == "door_enhancement_cost_factor_basis"), "not_found"
313+
)
314+
cost_unit_basis = next(
315+
(v for lbl, k, v in ap_data if k == "door_enhancement_cost_unit_basis"), ""
316+
)
317+
cost_source = next(
318+
(v for lbl, k, v in ap_data if k == "door_enhancement_cost_source"), "not_found"
319+
)
278320

279321
# Save modified model
280322
print("\n" + "=" * 80)
@@ -288,12 +330,15 @@ def _sv_value(sv):
288330
results = {
289331
"measure": "DoorEnhancement",
290332
"success": success,
333+
"cost_source": cost_source,
334+
"cost_factor_basis": cost_factor_basis,
335+
"cost_unit_basis": cost_unit_basis,
291336
"step_values": step_values,
292337
"doors_in_model": len(doors),
293338
"additional_properties_count": len(ap_data),
294339
"additional_properties_sample": [
295-
{"subsurface": s, "property": p, "value": v}
296-
for s, p, v in ap_data[:10]
340+
{"bucket": s, "property": p, "value": v}
341+
for s, p, v in ap_data[:15]
297342
]
298343
}
299344
with open(results_json_path, "w") as f:

0 commit comments

Comments
 (0)