IFCore Deploy commited on
Commit
51982d6
Β·
0 Parent(s):

deploy(prod): 2026-02-21T01:10:43Z

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. Dockerfile +13 -0
  2. README.md +10 -0
  3. deploy.sh +48 -0
  4. main.py +475 -0
  5. orchestrator.py +106 -0
  6. requirements.txt +6 -0
  7. teams/Mastodonte/.claude/skills/IFCore-skill/SKILL.md +229 -0
  8. teams/Mastodonte/.claude/skills/IFCore-skill/references/architecture.md +73 -0
  9. teams/Mastodonte/.claude/skills/IFCore-skill/references/development-patterns.md +84 -0
  10. teams/Mastodonte/.claude/skills/IFCore-skill/references/frontend-architecture.md +241 -0
  11. teams/Mastodonte/.claude/skills/IFCore-skill/references/repo-structure.md +53 -0
  12. teams/Mastodonte/.claude/skills/IFCore-skill/references/validation-schema.md +164 -0
  13. teams/Mastodonte/.gitignore +45 -0
  14. teams/Mastodonte/01_team_b +303 -0
  15. teams/Mastodonte/COMPLIANCE_AUDIT.md +246 -0
  16. teams/Mastodonte/Mastodontes_Tool_E.ipynb +372 -0
  17. teams/Mastodonte/Mastodontes_Tools.ipynb +857 -0
  18. teams/Mastodonte/USER_PORTAL_PRD.md +166 -0
  19. teams/Mastodonte/main.py +97 -0
  20. teams/Mastodonte/requirements.txt +1 -0
  21. teams/Mastodonte/tools/__init__.py +22 -0
  22. teams/Mastodonte/tools/checker_dwelling.py +101 -0
  23. teams/Mastodonte/tools/checker_heights.py +97 -0
  24. teams/Mastodonte/tools/checker_living_rooms.py +148 -0
  25. teams/Mastodonte/tools/checker_occupancy.py +173 -0
  26. teams/Mastodonte/tools/checker_service_spaces.py +109 -0
  27. teams/demo/tools/checker_demo.py +32 -0
  28. teams/lux-ai/AGENT_HANDOFF_REPORT.md +526 -0
  29. teams/lux-ai/API function/SOLAR_PRODUCTION_PIPELINE.md +356 -0
  30. teams/lux-ai/API function/run_solar_analysis.py +0 -0
  31. teams/lux-ai/API function/scan_ifc_models.py +427 -0
  32. teams/lux-ai/API function/solar_production_engine.py +211 -0
  33. teams/lux-ai/Final pipeline/README.md +352 -0
  34. teams/lux-ai/Final pipeline/__init__.py +15 -0
  35. teams/lux-ai/Final pipeline/analyze.py +306 -0
  36. teams/lux-ai/Final pipeline/config.py +50 -0
  37. teams/lux-ai/Final pipeline/ifc_metadata_extractor.py +442 -0
  38. teams/lux-ai/Final pipeline/ifc_roof_parser.py +359 -0
  39. teams/lux-ai/Final pipeline/key_aliases.json +3323 -0
  40. teams/lux-ai/Final pipeline/run_solar_analysis.py +325 -0
  41. teams/lux-ai/Final pipeline/solar_production_engine.py +167 -0
  42. teams/lux-ai/IFC key checker/discover_ifc_keys.py +387 -0
  43. teams/lux-ai/IFC key checker/ifc_key_inventory.json +0 -0
  44. teams/lux-ai/IFC key checker/key_aliases.json +3275 -0
  45. teams/lux-ai/Logs/ifc_scan.log +74 -0
  46. teams/lux-ai/Logs/ifc_scan_results.csv +37 -0
  47. teams/lux-ai/Logs/solar_pipeline.log +6 -0
  48. teams/lux-ai/Logs/solar_pipeline_scan.csv +37 -0
  49. teams/lux-ai/Lux ai tool/README.md +101 -0
  50. teams/lux-ai/Lux ai tool/final_pipeline/__init__.py +15 -0
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+ COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt
6
+
7
+ COPY . .
8
+
9
+ RUN useradd -m -u 1000 user
10
+ USER user
11
+
12
+ EXPOSE 7860
13
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: IFCore Platform
3
+ emoji: "\U0001F3D7\uFE0F"
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ ---
9
+
10
+ IFC compliance checker backend. Auto-discovers `check_*` functions from team modules.
deploy.sh ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+
6
+ # --- Target selection (default: staging for safety) ---
7
+ TARGET="${1:-staging}"
8
+ case "$TARGET" in
9
+ staging) HF_REPO="${HF_REPO:-serJD/ifcore-platform-staging}" ;;
10
+ prod) HF_REPO="${HF_REPO:-serJD/ifcore-platform}" ;;
11
+ *) echo "Usage: deploy.sh [staging|prod]"; exit 1 ;;
12
+ esac
13
+
14
+ echo "Deploying to $TARGET -> https://huggingface.co/spaces/$HF_REPO"
15
+
16
+ cd "$SCRIPT_DIR"
17
+
18
+ git -C "$SCRIPT_DIR/.." submodule update --init --recursive --remote
19
+
20
+ TMPDIR=$(mktemp -d)
21
+ trap 'rm -rf "$TMPDIR"' EXIT
22
+
23
+ rsync -a --exclude='.git' --exclude='__pycache__' \
24
+ --exclude='*.pdf' --exclude='*.png' --exclude='*.jpg' --exclude='*.jpeg' \
25
+ --exclude='*.gif' --exclude='*.zip' --exclude='*.mp4' --exclude='*.mov' \
26
+ --exclude='*.ifc' --exclude='*.ifczip' --exclude='*.glb' --exclude='*.obj' \
27
+ . "$TMPDIR/"
28
+
29
+ find "$TMPDIR/teams" -name ".git" -type f -delete 2>/dev/null || true
30
+ find "$TMPDIR/teams" -name ".git" -type d -exec rm -rf {} + 2>/dev/null || true
31
+
32
+ cd "$TMPDIR"
33
+
34
+ git init -b main
35
+ git config user.email "deploy@ifcore"
36
+ git config user.name "IFCore Deploy"
37
+ git add .
38
+ git commit --no-gpg-sign -m "deploy($TARGET): $(date -u +%Y-%m-%dT%H:%M:%SZ)"
39
+ # Use token auth in CI (set HF_TOKEN env var or GitHub secret)
40
+ if [ -n "${HF_TOKEN:-}" ]; then
41
+ HF_REMOTE="https://serJD:${HF_TOKEN}@huggingface.co/spaces/$HF_REPO"
42
+ else
43
+ HF_REMOTE="https://huggingface.co/spaces/$HF_REPO"
44
+ fi
45
+ git remote add hf "$HF_REMOTE"
46
+ git push hf main --force
47
+
48
+ echo "Deployed to https://huggingface.co/spaces/$HF_REPO"
main.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import base64
4
+ import uuid
5
+ import logging
6
+ import tempfile
7
+ from contextlib import asynccontextmanager
8
+ from dataclasses import dataclass
9
+ from typing import Optional
10
+ import httpx
11
+ from fastapi import FastAPI, BackgroundTasks
12
+ from fastapi.responses import JSONResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from pydantic import BaseModel, Field
15
+ from pydantic_ai import Agent, RunContext
16
+ from pydantic_ai.usage import UsageLimits
17
+ from orchestrator import discover_checks, run_all_checks
18
+
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger("ifcore")
21
+
22
+ # In-memory job store β€” CF Worker polls this
23
+ _jobs: dict = {}
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Regulation knowledge base β€” Spanish / Catalan building bye-laws
27
+ # Each entry contains the official regulation, article/section reference,
28
+ # PDF link, content reference, compliance threshold, and required action.
29
+ # ---------------------------------------------------------------------------
30
+ REGULATIONS_KB: dict[str, dict] = {
31
+ "walls": {
32
+ "regulation": "CTE DB SE-F β€” Seguridad Estructural: Cimientos",
33
+ "reference": "CTE DB SE-F, Section 4.1 (Muros); EHE-08, Art. 23",
34
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SE/DBSEF.pdf",
35
+ "page_ref": "Section 4.1, p. 14 β€” Minimum wall thickness 100 mm",
36
+ "threshold": "Minimum wall thickness: β‰₯ 100 mm",
37
+ "action": (
38
+ "Increase wall thickness to β‰₯ 100 mm. For load-bearing walls, a qualified "
39
+ "structural engineer must verify revised stability calculations under CTE DB SE. "
40
+ "Update architectural and structural drawings accordingly."
41
+ ),
42
+ },
43
+ "beams": {
44
+ "regulation": "EHE-08 β€” InstrucciΓ³n de HormigΓ³n Estructural",
45
+ "reference": "EHE-08, Art. 23 (Vigas) and Art. 42.3 (Dimensiones mΓ­nimas)",
46
+ "pdf": "https://www.mitma.gob.es/recursos_mfom/0820200.pdf",
47
+ "page_ref": "Art. 23.1, p. 62 β€” Minimum depth 200 mm; Art. 23.2 β€” Minimum width 150 mm",
48
+ "threshold": "Minimum beam depth: β‰₯ 200 mm; minimum beam width: β‰₯ 150 mm",
49
+ "action": (
50
+ "Redesign beam cross-section to achieve depth β‰₯ 200 mm and width β‰₯ 150 mm. "
51
+ "Recheck load and deflection calculations. Have a licensed structural engineer "
52
+ "verify and sign off the revised design. Update structural drawings."
53
+ ),
54
+ },
55
+ "columns": {
56
+ "regulation": "EHE-08 β€” InstrucciΓ³n de HormigΓ³n Estructural",
57
+ "reference": "EHE-08, Art. 24 (Pilares) and Art. 42.3",
58
+ "pdf": "https://www.mitma.gob.es/recursos_mfom/0820200.pdf",
59
+ "page_ref": "Art. 24.1, p. 65 β€” Minimum column dimension 250 mm",
60
+ "threshold": "Minimum column dimension: β‰₯ 250 mm",
61
+ "action": (
62
+ "Increase the smaller column dimension to β‰₯ 250 mm. Re-evaluate reinforcement "
63
+ "ratios and load capacity. Update column schedule and structural calculations. "
64
+ "Coordinate changes with the foundation design."
65
+ ),
66
+ },
67
+ "foundations": {
68
+ "regulation": "CTE DB SE-C β€” Seguridad Estructural: Cimientos; EHE-08",
69
+ "reference": "EHE-08, Art. 69 (Cimentaciones); CTE DB SE-C, Section 4.1",
70
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SE/DBSEC.pdf",
71
+ "page_ref": "Art. 69.1 β€” Minimum foundation element depth 200 mm; DB SE-C Section 4.1, p. 18",
72
+ "threshold": "Minimum foundation depth: β‰₯ 200 mm",
73
+ "action": (
74
+ "Deepen or redesign foundation elements to β‰₯ 200 mm. If a geotechnical study "
75
+ "has not been done, commission one. Submit revised foundation drawings to the "
76
+ "project certifier. Ensure compliance with DB SE-C soil bearing capacity requirements."
77
+ ),
78
+ },
79
+ "slabs": {
80
+ "regulation": "CTE DB HE β€” Ahorro de EnergΓ­a; EHE-08",
81
+ "reference": "CTE DB HE1, Table 2.3 (Transmitancias lΓ­mite); EHE-08, Art. 22",
82
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/HE/DBHE.pdf",
83
+ "page_ref": "HE1 Table 2.3, p. 11 β€” Slab thickness 150–200 mm; Art. 22 structural dimensions",
84
+ "threshold": "Slab thickness: 150–200 mm",
85
+ "action": (
86
+ "Adjust slab thickness to the 150–200 mm range. Verify structural load capacity "
87
+ "for the revised thickness. If thermal performance is affected, recalculate U-values "
88
+ "for the slab assembly using HULC or equivalent CTE tool."
89
+ ),
90
+ },
91
+ "doors": {
92
+ "regulation": "CTE DB SUA β€” Seguridad de UtilizaciΓ³n y Accesibilidad",
93
+ "reference": "CTE DB SUA, SUA-9 (Accesibilidad), Section 1.1.1 and Table 2.1",
94
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SUA/DBSUA.pdf",
95
+ "page_ref": "SUA-9 Section 1.1.1, p. 47 β€” Minimum door clear width 800 mm; Table 2.1, p. 49",
96
+ "threshold": "Minimum door clear width: β‰₯ 800 mm",
97
+ "action": (
98
+ "Replace or widen door frames to achieve β‰₯ 800 mm clear passage width. "
99
+ "For full wheelchair access, 900 mm is recommended. Update the door schedule "
100
+ "in architectural drawings. In Catalan projects, also verify Decreto 141/2012."
101
+ ),
102
+ },
103
+ "windows": {
104
+ "regulation": "CTE DB SUA β€” Seguridad de UtilizaciΓ³n y Accesibilidad",
105
+ "reference": "CTE DB SUA, SUA-1, Section 2.1 (ProtecciΓ³n frente al riesgo de caΓ­da)",
106
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SUA/DBSUA.pdf",
107
+ "page_ref": "SUA-1 Section 2.1, p. 6 β€” Minimum window sill height 1200 mm above finished floor",
108
+ "threshold": "Minimum window sill height: β‰₯ 1200 mm, or protective barrier required",
109
+ "action": (
110
+ "Raise window sill to β‰₯ 1200 mm above finished floor level, or install a "
111
+ "compliant protective barrier (parapet or railing) at the required height. "
112
+ "Verify glazing impact resistance under CTE DB SUA-2."
113
+ ),
114
+ },
115
+ "corridors": {
116
+ "regulation": "CTE DB SUA β€” Accesibilidad; Decreto 141/2012 (Catalonia)",
117
+ "reference": "CTE DB SUA, SUA-9, Table 2.1; Decreto 141/2012, Art. 18",
118
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SUA/DBSUA.pdf",
119
+ "page_ref": "SUA-9 Table 2.1, p. 49 β€” Min. corridor width β‰₯ 1200 mm (public); β‰₯ 1100 mm (housing); Decreto 141/2012 Art. 18",
120
+ "threshold": "Minimum corridor width: β‰₯ 1100 mm in dwellings; β‰₯ 1200 mm in public routes",
121
+ "action": (
122
+ "Widen corridor to the applicable minimum. Revise floor-plan layout if needed. "
123
+ "For Catalan housing projects, additionally verify Decreto 141/2012 Art. 18 "
124
+ "(PDF: https://portaldogc.gencat.cat/utilsEADOP/PDF/6138/1223437.pdf)."
125
+ ),
126
+ },
127
+ "ceiling": {
128
+ "regulation": "CTE DB SUA β€” Accesibilidad; Decreto 141/2012 (Catalonia)",
129
+ "reference": "CTE DB SUA, SUA-9, Section 1.1; Decreto 141/2012, Art. 15",
130
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SUA/DBSUA.pdf",
131
+ "page_ref": "SUA-9 Section 1.1, p. 47 β€” Minimum clear ceiling height 2200 mm; Decreto 141/2012 Art. 15",
132
+ "threshold": "Minimum clear ceiling height: β‰₯ 2200 mm (β‰₯ 2500 mm in Catalan living spaces)",
133
+ "action": (
134
+ "Increase floor-to-ceiling clear height to β‰₯ 2200 mm. Review structural floor "
135
+ "depth and finish build-up. For Catalan housing, Decreto 141/2012 Art. 15 requires "
136
+ "β‰₯ 2500 mm in habitable rooms β€” verify and revise section drawings."
137
+ ),
138
+ },
139
+ "stairs": {
140
+ "regulation": "CTE DB SUA β€” Seguridad de UtilizaciΓ³n y Accesibilidad",
141
+ "reference": "CTE DB SUA, SUA-1, Section 4.2.1 (Escaleras de uso general)",
142
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SUA/DBSUA.pdf",
143
+ "page_ref": "SUA-1 Section 4.2.1, p. 12 β€” Riser 130–185 mm; Tread β‰₯ 280 mm; formula: 2R + H = 620–640 mm",
144
+ "threshold": "Stair riser: 130–185 mm; stair tread: β‰₯ 280 mm",
145
+ "action": (
146
+ "Redesign stair geometry so riser falls within 130–185 mm and tread is β‰₯ 280 mm. "
147
+ "Apply the ergonomic formula: 2Γ—riser + tread = 620–640 mm. "
148
+ "Update stair detail drawings and structural calculations."
149
+ ),
150
+ },
151
+ "railings": {
152
+ "regulation": "CTE DB SUA β€” Seguridad de UtilizaciΓ³n y Accesibilidad",
153
+ "reference": "CTE DB SUA, SUA-1, Section 3.2.1 (ProtecciΓ³n en los bordes de los forjados)",
154
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SUA/DBSUA.pdf",
155
+ "page_ref": "SUA-1 Section 3.2.1, p. 9 β€” Min. height 900 mm; β‰₯ 1100 mm where drop > 6 m",
156
+ "threshold": "Minimum railing height: β‰₯ 900 mm; β‰₯ 1100 mm where floor-to-ground > 6 m",
157
+ "action": (
158
+ "Raise railing/balustrade to β‰₯ 900 mm (or β‰₯ 1100 mm where applicable). "
159
+ "Ensure baluster spacing ≀ 100 mm to prevent climbing. "
160
+ "Verify structural fixing adequacy under CTE DB SE."
161
+ ),
162
+ },
163
+ "energy": {
164
+ "regulation": "CTE DB HE β€” Ahorro de EnergΓ­a",
165
+ "reference": "CTE DB HE, HE1, Section 2.2 (Transmitancia tΓ©rmica mΓ‘xima de cerramientos)",
166
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/HE/DBHE.pdf",
167
+ "page_ref": "HE1 Table 2.3, p. 11 β€” Maximum wall U-value 0.80 W/mΒ²K (Climate Zone B)",
168
+ "threshold": "Maximum wall U-value: ≀ 0.80 W/mΒ²K (Spain Climate Zone B)",
169
+ "action": (
170
+ "Add or upgrade thermal insulation in the wall assembly to bring U-value below "
171
+ "0.80 W/mΒ²K. Use HULC or CYPETHERM software to recalculate. Specify insulation "
172
+ "type, thickness, and Ξ»-value on building specifications."
173
+ ),
174
+ },
175
+ "fire": {
176
+ "regulation": "CTE DB SI β€” Seguridad en caso de Incendio",
177
+ "reference": "CTE DB SI, SI-2 (PropagaciΓ³n interior); SI-6 (Resistencia al fuego)",
178
+ "pdf": "https://www.codigotecnico.org/pdf/Documentos/SI/DBSI.pdf",
179
+ "page_ref": "DB SI Table 1.2, p. 8 β€” Fire resistance by use and height (R60–R120); SI-6 structural resistance",
180
+ "threshold": "Fire resistance: R60–R120 depending on building use and height",
181
+ "action": (
182
+ "Review fire compartmentation plan. Ensure separating elements achieve the "
183
+ "required fire resistance rating. Apply appropriate fireproofing to structural "
184
+ "members. Coordinate with the project fire safety engineer and document in "
185
+ "the fire safety report."
186
+ ),
187
+ },
188
+ "reinforcement": {
189
+ "regulation": "EHE-08 β€” InstrucciΓ³n de HormigΓ³n Estructural",
190
+ "reference": "EHE-08, Art. 42 (Recubrimientos) and Art. 58 (CuantΓ­as mΓ­nimas de armadura)",
191
+ "pdf": "https://www.mitma.gob.es/recursos_mfom/0820200.pdf",
192
+ "page_ref": "Art. 42.1, p. 88 β€” Cover 20–45 mm by exposure class; Art. 58, p. 112 β€” Min. reinforcement ratios",
193
+ "threshold": "Concrete cover: β‰₯ 20 mm (interior) to β‰₯ 45 mm (severe exposure); min. reinforcement ratio per Art. 58",
194
+ "action": (
195
+ "Revise reinforcement detailing: increase cover to meet the exposure class requirement "
196
+ "and ensure rebar quantity meets Art. 58 minimum ratios. Update structural drawings "
197
+ "and have them verified and signed off by a licensed structural engineer."
198
+ ),
199
+ },
200
+ }
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # PydanticAI β€” deps + agent definition
205
+ # ---------------------------------------------------------------------------
206
+
207
+ @dataclass
208
+ class ChatDeps:
209
+ check_results: list[dict]
210
+ element_results: list[dict]
211
+
212
+
213
+ _chat_agent: Agent | None = None
214
+
215
+
216
+ def _get_chat_agent() -> Agent:
217
+ global _chat_agent
218
+ if _chat_agent is not None:
219
+ return _chat_agent
220
+
221
+ _chat_agent = Agent(
222
+ "google-gla:gemini-2.0-flash",
223
+ deps_type=ChatDeps,
224
+ instructions=(
225
+ "You are a building compliance assistant for the IFCore platform. "
226
+ "You answer questions about IFC model compliance check results.\n\n"
227
+ "FORMATTING β€” the chat UI renders markdown, so use it:\n"
228
+ "- Use **bold** for element names and key values.\n"
229
+ "- Use `inline code` for IFC type names and property names.\n"
230
+ "- Use bullet lists (- item) for multiple items.\n"
231
+ "- Use [link text](url) for ALL URLs β€” never paste bare URLs.\n"
232
+ "- Use > blockquotes for regulation citations or ads.\n"
233
+ "- Use --- for horizontal rules.\n"
234
+ "- Keep paragraphs short. Use line breaks between sections.\n\n"
235
+ "RESPONSE RULES β€” follow every time:\n"
236
+ "1. Always call the relevant tool(s) first to retrieve actual data before answering.\n"
237
+ "2. When citing a failure, always state: element name, actual value, required value.\n"
238
+ "3. For every bye-law reference, you MUST quote: the bye-law name, "
239
+ "the PDF link (as a markdown link), and the specific article/section/page or content number.\n"
240
+ "4. Give further detail on what the bye-law requires and exactly what must be "
241
+ "done to achieve compliance.\n"
242
+ "5. Use βœ…/❌ for pass/fail status indicators.\n"
243
+ "6. Be specific and factual β€” no vague generalisations.\n"
244
+ "7. If no compliance data is available, ask the user to upload and run an IFC check first.\n\n"
245
+ "MANDATORY ADVERTISEMENT β€” append to EVERY response, no exceptions:\n"
246
+ "End with a horizontal rule (---) and ONE sponsored blockquote ad. "
247
+ "Rotate between two sponsors based on context:\n\n"
248
+ "SPONSOR A β€” LightDesk (use most of the time):\n"
249
+ "> πŸ’‘ *[1-sentence joke connecting this compliance topic to the sorry state of the user's photo library]* "
250
+ "[**LightDesk β†’**](https://sites.run8n.xyz/lightdesk/) *Your photos. Cloudlessly alive.*\n\n"
251
+ "SPONSOR B β€” ULTRAVIOLET.CITY (use when many things fail or the situation is dramatic):\n"
252
+ "> πŸ’‘ *[1-sentence joke: this building fails so hard it deserves total simulated ecosystem annihilation "
253
+ "β€” reference the platform's absurd power: sub-100ns ecosystem collapse, infinite parallel timelines]* "
254
+ "[**ULTRAVIOLET.CITY β†’**](https://sites.run8n.xyz/ultraviolet-city/) *Design. Obliterate. Transcend.*\n\n"
255
+ "Rules: joke must reference something specific from the current check results. 1 sentence max. Always include the link as a markdown link."
256
+ ),
257
+ )
258
+
259
+ # ── Tool 1: overall summary ──────────────────────────────────────────
260
+ @_chat_agent.tool
261
+ def get_compliance_summary(ctx: RunContext[ChatDeps]) -> str:
262
+ """Get the overall compliance summary: total checks, pass/fail counts, and per-team breakdown."""
263
+ crs = ctx.deps.check_results
264
+ if not crs:
265
+ return "No compliance data available. Ask the user to upload and run an IFC check first."
266
+
267
+ total = len(crs)
268
+ passed = sum(1 for cr in crs if cr.get("status") == "pass")
269
+ failed = sum(1 for cr in crs if cr.get("status") == "fail")
270
+ other = total - passed - failed
271
+
272
+ lines = [f"Total checks: {total} | Pass: {passed} | Fail: {failed} | Other: {other}"]
273
+
274
+ teams: dict[str, dict] = {}
275
+ for cr in crs:
276
+ team = cr.get("team", "unknown")
277
+ if team not in teams:
278
+ teams[team] = {"pass": 0, "fail": 0, "other": 0, "names": []}
279
+ status = cr.get("status", "unknown")
280
+ if status == "pass":
281
+ teams[team]["pass"] += 1
282
+ elif status == "fail":
283
+ teams[team]["fail"] += 1
284
+ teams[team]["names"].append(cr.get("check_name", "?"))
285
+ else:
286
+ teams[team]["other"] += 1
287
+
288
+ lines.append("\nTeam breakdown:")
289
+ for team, counts in teams.items():
290
+ detail = ""
291
+ if counts["names"]:
292
+ detail = f" β€” failing: {', '.join(counts['names'][:5])}"
293
+ lines.append(f" {team}: {counts['pass']} pass, {counts['fail']} fail{detail}")
294
+
295
+ return "\n".join(lines)
296
+
297
+ # ── Tool 2: search failing elements ─────────────────────────────────
298
+ @_chat_agent.tool
299
+ def search_failing_elements(ctx: RunContext[ChatDeps], element_type: str = "") -> str:
300
+ """Search for failing, warning, or blocked elements, optionally filtered by element type or name.
301
+
302
+ Args:
303
+ element_type: Optional keyword to filter by β€” e.g. 'IfcWall', 'beam', 'door', 'column'.
304
+ Leave empty to return all failures.
305
+ """
306
+ ers = ctx.deps.element_results
307
+ failing = [e for e in ers if e.get("check_status") in ("fail", "warning", "blocked")]
308
+
309
+ if element_type:
310
+ q = element_type.lower()
311
+ failing = [
312
+ e for e in failing
313
+ if q in (e.get("element_type") or "").lower()
314
+ or q in (e.get("element_name") or "").lower()
315
+ or q in (e.get("comment") or "").lower()
316
+ ]
317
+
318
+ if not failing:
319
+ suffix = f" matching '{element_type}'" if element_type else ""
320
+ return f"No failing/warning elements found{suffix}."
321
+
322
+ suffix = f" matching '{element_type}'" if element_type else ""
323
+ lines = [f"Found {len(failing)} failing/warning element(s){suffix}:"]
324
+ for e in failing[:40]:
325
+ name = e.get("element_name") or e.get("element_type") or "Unknown"
326
+ comment = (e.get("comment") or "")[:180]
327
+ lines.append(
328
+ f" [{e.get('check_status', '?').upper()}] **{name}** β€” "
329
+ f"actual: {e.get('actual_value', 'N/A')}, "
330
+ f"required: {e.get('required_value', 'N/A')}"
331
+ + (f", note: {comment}" if comment else "")
332
+ )
333
+ if len(failing) > 40:
334
+ lines.append(f" … and {len(failing) - 40} more elements.")
335
+ return "\n".join(lines)
336
+
337
+ # ── Tool 3: regulation lookup ────────────────────────────────────────
338
+ @_chat_agent.tool
339
+ def lookup_regulation(ctx: RunContext[ChatDeps], topic: str) -> str:
340
+ """Look up the applicable Spanish/Catalan building bye-law for a topic or element type.
341
+ Returns the regulation name, PDF link, article/content reference, threshold, and action.
342
+
343
+ Args:
344
+ topic: The element type or compliance topic β€” e.g. 'beam', 'wall', 'door',
345
+ 'foundation', 'fire', 'energy', 'reinforcement', 'stairs', 'railing'.
346
+ """
347
+ q = topic.lower()
348
+
349
+ # Score candidates: 0 = key match, 1 = content match
350
+ matches: list[tuple[int, dict]] = []
351
+ for key, data in REGULATIONS_KB.items():
352
+ if q in key or key in q:
353
+ matches.append((0, data))
354
+ elif any(q in str(v).lower() for v in data.values()):
355
+ matches.append((1, data))
356
+
357
+ if not matches:
358
+ return (
359
+ f"No specific bye-law found for '{topic}'. "
360
+ "Available topics: walls, beams, columns, foundations, slabs, doors, windows, "
361
+ "corridors, ceiling, stairs, railings, energy, fire, reinforcement. "
362
+ "Try one of these terms."
363
+ )
364
+
365
+ matches.sort(key=lambda x: x[0])
366
+ d = matches[0][1]
367
+ return (
368
+ f"**Bye-law: {d['regulation']}**\n"
369
+ f"**Reference:** {d['reference']}\n"
370
+ f"**PDF:** {d['pdf']}\n"
371
+ f"**Content/Page:** {d['page_ref']}\n"
372
+ f"**Threshold:** {d['threshold']}\n"
373
+ f"**Required action:** {d['action']}"
374
+ )
375
+
376
+ return _chat_agent
377
+
378
+
379
+ class ChatRequest(BaseModel):
380
+ message: str = Field(max_length=2000)
381
+ check_results: list[dict] = Field(default_factory=list, max_length=50)
382
+ element_results: list[dict] = Field(default_factory=list, max_length=200)
383
+
384
+
385
+ @asynccontextmanager
386
+ async def lifespan(app):
387
+ yield
388
+
389
+ app = FastAPI(title="IFCore Platform", lifespan=lifespan)
390
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
391
+
392
+
393
+ class CheckRequest(BaseModel):
394
+ ifc_url: Optional[str] = None # URL to download IFC from
395
+ ifc_b64: Optional[str] = None # Base64-encoded IFC bytes (preferred β€” avoids DNS issues)
396
+ project_id: Optional[str] = None
397
+
398
+
399
+ @app.get("/health")
400
+ def health():
401
+ checks = discover_checks()
402
+ return {"status": "ok", "checks_discovered": len(checks),
403
+ "checks": [{"team": t, "name": n} for t, n, _ in checks]}
404
+
405
+
406
+ @app.get("/jobs/{job_id}")
407
+ def get_job(job_id: str):
408
+ """Poll endpoint β€” CF Worker calls this to get results."""
409
+ job = _jobs.get(job_id)
410
+ if not job:
411
+ return {"job_id": job_id, "status": "unknown"}
412
+ return job
413
+
414
+
415
+ @app.post("/check")
416
+ async def check(req: CheckRequest, background_tasks: BackgroundTasks):
417
+ job_id = str(uuid.uuid4())
418
+ _jobs[job_id] = {"job_id": job_id, "status": "running"}
419
+ logger.info(f"[{job_id}] queued (b64={req.ifc_b64 is not None}, url={req.ifc_url})")
420
+ background_tasks.add_task(run_check_job, req.ifc_url, req.ifc_b64, job_id, req.project_id)
421
+ return {"job_id": job_id, "status": "running"}
422
+
423
+
424
+ @app.post("/chat")
425
+ async def chat_endpoint(req: ChatRequest):
426
+ deps = ChatDeps(
427
+ check_results=req.check_results,
428
+ element_results=req.element_results,
429
+ )
430
+ try:
431
+ result = await asyncio.wait_for(
432
+ _get_chat_agent().run(
433
+ req.message[:2000],
434
+ deps=deps,
435
+ usage_limits=UsageLimits(request_limit=5),
436
+ ),
437
+ timeout=45.0,
438
+ )
439
+ return {"response": result.output}
440
+ except asyncio.TimeoutError:
441
+ return JSONResponse(status_code=504, content={"error": "AI model timed out. Please try again."})
442
+ except Exception as e:
443
+ logger.exception("chat failed")
444
+ return JSONResponse(status_code=502, content={"error": f"AI model error: {type(e).__name__}"})
445
+
446
+
447
+ def run_check_job(ifc_url, ifc_b64, job_id, project_id):
448
+ try:
449
+ with tempfile.TemporaryDirectory() as tmpdir:
450
+ ifc_path = os.path.join(tmpdir, "model.ifc")
451
+
452
+ if ifc_b64:
453
+ logger.info(f"[{job_id}] decoding base64 IFC ({len(ifc_b64)} chars)")
454
+ with open(ifc_path, "wb") as f:
455
+ f.write(base64.b64decode(ifc_b64))
456
+ elif ifc_url:
457
+ logger.info(f"[{job_id}] downloading {ifc_url}")
458
+ with httpx.Client(timeout=120) as client:
459
+ resp = client.get(ifc_url)
460
+ resp.raise_for_status()
461
+ with open(ifc_path, "wb") as f:
462
+ f.write(resp.content)
463
+ else:
464
+ raise ValueError("Either ifc_url or ifc_b64 must be provided")
465
+
466
+ logger.info(f"[{job_id}] running checks")
467
+ results = run_all_checks(ifc_path, job_id, project_id)
468
+ n = len(results.get("check_results", []))
469
+ logger.info(f"[{job_id}] done: {n} checks")
470
+ _jobs[job_id] = {"job_id": job_id, "status": "done", **results}
471
+
472
+ except Exception as exc:
473
+ logger.exception(f"[{job_id}] failed: {exc}")
474
+ _jobs[job_id] = {"job_id": job_id, "status": "error", "error": str(exc),
475
+ "check_results": [], "element_results": []}
orchestrator.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib.util
2
+ import os
3
+ import glob
4
+ import uuid
5
+ import time
6
+ import logging
7
+ import ifcopenshell
8
+
9
+ logger = logging.getLogger("ifcore")
10
+
11
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
12
+
13
+ TEAM_FIELDS = [
14
+ "element_id", "element_type", "element_name", "element_name_long",
15
+ "check_status", "actual_value", "required_value", "comment", "log",
16
+ ]
17
+
18
+
19
+ def discover_checks():
20
+ checks = []
21
+ pattern = os.path.join(BASE_DIR, "teams", "*", "tools", "checker_*.py")
22
+ for path in sorted(glob.glob(pattern)):
23
+ parts = path.replace(BASE_DIR + os.sep, "").split(os.sep)
24
+ team = parts[1]
25
+ module_name = os.path.splitext(os.path.basename(path))[0]
26
+ try:
27
+ spec = importlib.util.spec_from_file_location(f"teams.{team}.{module_name}", path)
28
+ mod = importlib.util.module_from_spec(spec)
29
+ spec.loader.exec_module(mod)
30
+ for attr in dir(mod):
31
+ if attr.startswith("check_") and callable(getattr(mod, attr)):
32
+ checks.append((team, attr, getattr(mod, attr)))
33
+ except Exception as exc:
34
+ logger.warning(f"[discover] skipping {team}/{module_name}: {exc}")
35
+ return checks
36
+
37
+
38
+ def _aggregate_status(elements):
39
+ statuses = [e.get("check_status", "blocked") for e in elements]
40
+ if any(s == "fail" for s in statuses):
41
+ return "fail"
42
+ if any(s == "warning" for s in statuses):
43
+ return "warning"
44
+ if all(s in ("pass", "log") for s in statuses):
45
+ return "pass"
46
+ return "unknown"
47
+
48
+
49
+ def _build_summary(elements):
50
+ p = sum(1 for e in elements if e.get("check_status") == "pass")
51
+ f = sum(1 for e in elements if e.get("check_status") == "fail")
52
+ w = sum(1 for e in elements if e.get("check_status") == "warning")
53
+ b = sum(1 for e in elements if e.get("check_status") == "blocked")
54
+ total = len(elements)
55
+ parts = []
56
+ if p: parts.append(f"{p} pass")
57
+ if f: parts.append(f"{f} fail")
58
+ if w: parts.append(f"{w} warning")
59
+ if b: parts.append(f"{b} blocked")
60
+ return f"{total} elements: {', '.join(parts)}" if parts else f"{total} elements"
61
+
62
+
63
+ def run_all_checks(ifc_path, job_id, project_id):
64
+ model = ifcopenshell.open(ifc_path)
65
+ checks = discover_checks()
66
+ check_results = []
67
+ element_results = []
68
+
69
+ for team, func_name, func in checks:
70
+ check_id = str(uuid.uuid4())
71
+ try:
72
+ elements = func(model)
73
+ status = _aggregate_status(elements)
74
+ summary = _build_summary(elements)
75
+
76
+ check_results.append({
77
+ "id": check_id,
78
+ "job_id": job_id,
79
+ "project_id": project_id,
80
+ "check_name": func_name,
81
+ "team": team,
82
+ "status": status,
83
+ "summary": summary,
84
+ "has_elements": 1 if elements else 0,
85
+ "created_at": int(time.time() * 1000),
86
+ })
87
+
88
+ for el in elements:
89
+ row = {"id": str(uuid.uuid4()), "check_result_id": check_id}
90
+ for field in TEAM_FIELDS:
91
+ row[field] = el.get(field)
92
+ element_results.append(row)
93
+ except Exception as exc:
94
+ check_results.append({
95
+ "id": check_id,
96
+ "job_id": job_id,
97
+ "project_id": project_id,
98
+ "check_name": func_name,
99
+ "team": team,
100
+ "status": "error",
101
+ "summary": str(exc)[:200],
102
+ "has_elements": 0,
103
+ "created_at": int(time.time() * 1000),
104
+ })
105
+
106
+ return {"check_results": check_results, "element_results": element_results}
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.32.0
3
+ ifcopenshell>=0.8.1
4
+ httpx==0.28.0
5
+ python-multipart==0.0.20
6
+ pydantic-ai-slim[google]
teams/Mastodonte/.claude/skills/IFCore-skill/SKILL.md ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: IFCore
3
+ description: Use when developing on the IFCore compliance checker. Covers contracts, check function conventions, issue reporting, app structure, and development patterns.
4
+ ---
5
+
6
+ # IFCore β€” Company Skill
7
+
8
+ > **Living document.** Sections marked [TBD] are decided in board meetings.
9
+ > When a [TBD] is resolved, update this skill and tell your agent to adapt.
10
+
11
+ ## When This Skill Activates
12
+
13
+ Welcome the user. Introduce yourself as their IFCore development assistant. Explain:
14
+
15
+ 1. **What you know:** The IFCore platform contracts β€” how check functions must be written,
16
+ the file naming convention, the database schema, and how team repos integrate into the
17
+ platform via git submodules.
18
+
19
+ 2. **What you can do:**
20
+ - Help write `check_*` functions that comply with the platform contracts
21
+ - Review existing code for contract compliance
22
+ - Explain IFC file structure and ifcopenshell patterns
23
+ - Help with feature planning (PRDs, user stories)
24
+ - File issues to the shared skills repo when contracts are unclear
25
+
26
+ 3. **Offer a codebase review.** Ask to scan the current repo and check:
27
+ - Are `checker_*.py` files directly inside `tools/`?
28
+ - Do all `check_*` functions follow the contract (signature, return type)?
29
+ - Is there anything that would block platform integration?
30
+
31
+ 4. **Respect their setup.** Teams may have their own Gradio app, FastAPI server, notebooks,
32
+ test scripts, or any other tooling in their repo. **That's fine.** The platform only cares
33
+ about `tools/checker_*.py` files β€” everything else is ignored during integration.
34
+ The only hard rule: don't put anything in `tools/` that breaks the `checker_*.py` import
35
+ chain (e.g. conflicting `__init__.py` files or dependencies not in `requirements.txt`).
36
+
37
+ 5. **Offer to explain Agent Skills.** If the user seems unsure what this is, explain:
38
+ "An Agent Skill is a set of instructions that your AI coding assistant reads automatically.
39
+ It's like a company handbook β€” it tells me (your AI) the engineering standards, naming
40
+ conventions, and contracts so I can help you write code that works with everyone else's.
41
+ You installed it once; now I follow it in every conversation."
42
+
43
+ 6. **How to install & update this skill.** Install the skill **globally** so it works
44
+ in every project on your machine (not just one repo):
45
+ ```
46
+ Install (once):
47
+ 1. Clone: git clone https://github.com/SerjoschDuering/iaac-bimwise-skills.git
48
+ (put it somewhere permanent, e.g. ~/skills/ or ~/Documents/)
49
+ 2. Add the skill GLOBALLY in your AI coding tool:
50
+ - VS Code/Copilot: Chat panel β†’ Add Agent Skill β†’ pick the SKILL.md file.
51
+ Use "User" scope (not "Workspace") so it applies to ALL projects.
52
+ - Cursor: Settings β†’ Agent Skills β†’ Add β†’ point to the cloned folder.
53
+ This is global by default.
54
+ - Claude Code: add to ~/.claude/settings.json under agent skills,
55
+ or install as a plugin β€” it applies to all sessions automatically.
56
+ 3. Start a new chat session β€” your AI now knows IFCore standards.
57
+
58
+ Update (after board meetings):
59
+ 1. cd into your cloned skills folder
60
+ 2. git pull
61
+ 3. Start a fresh chat session β€” the AI reloads the updated instructions
62
+ ```
63
+ If you're not sure whether your skill is up to date, ask your AI:
64
+ "What board meeting is the latest in your IFCore skill?" and compare with your team.
65
+
66
+ ## Contracts β€” READ THIS FIRST
67
+
68
+ These contracts are how teams stay aligned. The platform auto-discovers your code.
69
+ Break a contract β†’ the platform silently skips your checks. Follow them β†’ it just works.
70
+
71
+ ### 1. Check Function Contract
72
+
73
+ ```python
74
+ # Function naming: check_<what>
75
+ # Location: tools/checker_*.py (directly inside tools/, no subdirectories)
76
+ # Signature: first arg is always the ifcopenshell model
77
+ # Return: list[dict] β€” one dict per element, maps to element_results DB rows
78
+
79
+ def check_door_width(model, min_width_mm=800):
80
+ results = []
81
+ for door in model.by_type("IfcDoor"):
82
+ width_mm = round(door.OverallWidth * 1000) if door.OverallWidth else None
83
+ results.append({
84
+ "element_id": door.GlobalId,
85
+ "element_type": "IfcDoor",
86
+ "element_name": door.Name or f"Door #{door.id()}",
87
+ "element_name_long": f"{door.Name} (Level 1, Zone A)",
88
+ "check_status": "blocked" if width_mm is None
89
+ else "pass" if width_mm >= min_width_mm
90
+ else "fail",
91
+ "actual_value": f"{width_mm} mm" if width_mm else None,
92
+ "required_value": f"{min_width_mm} mm",
93
+ "comment": None if width_mm and width_mm >= min_width_mm
94
+ else f"Door is {min_width_mm - width_mm} mm too narrow"
95
+ if width_mm else "Width property missing",
96
+ "log": None,
97
+ })
98
+ return results
99
+ ```
100
+
101
+ **Rules:**
102
+ - Prefix: `check_` β€” the platform discovers functions by this prefix
103
+ - First argument: `model` (an `ifcopenshell.file` object) β€” always
104
+ - Optional keyword args after `model` are fine (e.g. `min_width_mm=800`)
105
+ - Return: `list[dict]` β€” each dict has fields matching `element_results` (see [Validation Schema](./references/validation-schema.md))
106
+ - `check_status` values: `"pass"`, `"fail"`, `"warning"`, `"blocked"`, `"log"`
107
+ - One function per regulation check β€” don't combine multiple rules
108
+ - Functions can live across multiple `checker_*.py` files directly inside `tools/`
109
+
110
+ ### 2. File Structure Contract
111
+
112
+ ```
113
+ your-team-repo/
114
+ β”œβ”€β”€ tools/
115
+ β”‚ β”œβ”€β”€ checker_doors.py ← check_door_width, check_door_clearance
116
+ β”‚ β”œβ”€β”€ checker_fire_safety.py ← check_fire_rating, check_exit_count
117
+ β”‚ └── checker_rooms.py ← check_room_area, check_ceiling_height
118
+ β”œβ”€β”€ requirements.txt ← team dependencies
119
+ └── README.md
120
+ ```
121
+
122
+ **File naming:** `checker_<topic>.py` β€” group related checks by topic. Examples:
123
+ - `checker_doors.py` β€” door width, clearance, accessibility
124
+ - `checker_walls.py` β€” thickness, fire rating, insulation
125
+ - `checker_stairs.py` β€” riser height, tread length, handrails
126
+ - `checker_spaces.py` β€” room area, ceiling height, ventilation
127
+
128
+ The platform scans **all `checker_*.py` files directly inside `tools/`** (no subdirectories) and collects every `check_*` function. You don't need a wrapper or registry β€” just follow the naming conventions.
129
+
130
+ **Important:** Only `checker_*.py` files are scanned. Helper files (e.g. `tools/utils.py`) are fine for shared code but won't be scanned for `check_*` functions β€” import them from your `checker_*.py` files.
131
+
132
+ **Local testing:** Run your checks locally before pushing:
133
+ ```python
134
+ import ifcopenshell
135
+
136
+ model = ifcopenshell.open("path/to/model.ifc")
137
+ from tools.checker_doors import check_door_width
138
+ results = check_door_width(model)
139
+ for r in results:
140
+ print(f"[{r['check_status'].upper()}] {r['element_name']}: {r['actual_value']} (req: {r['required_value']})")
141
+ ```
142
+ The `model` object is exactly what the platform passes to your functions.
143
+
144
+ ### 3. Issue Reporting Contract β€” MANDATORY
145
+
146
+ When your AI agent encounters any of these during development, it **MUST** file an issue:
147
+
148
+ | Trigger | Label |
149
+ |---|---|
150
+ | Contract unclear or ambiguous | `contract-gap` |
151
+ | Skill instructions don't match reality | `skill-drift` |
152
+ | Found a workaround for a known limitation | `learning` |
153
+ | Schema format needs a new field | `schema-change` |
154
+ | Team code works locally but breaks on platform | `integration-bug` |
155
+
156
+ **How to file:**
157
+ ```bash
158
+ gh issue create \
159
+ --repo SerjoschDuering/iaac-bimwise-skills \
160
+ --title "contract-gap: check functions with multiple models" \
161
+ --label "contract-gap" \
162
+ --body "$(cat <<'EOF'
163
+ ## What happened
164
+ Tried to write a check that compares two IFC models side-by-side.
165
+ The contract says first arg is `model` (singular).
166
+
167
+ ## What I expected
168
+ Guidance on multi-model checks.
169
+
170
+ ## Workaround (if any)
171
+ Loaded second model inside the function body.
172
+
173
+ ## Team
174
+ Team A
175
+ EOF
176
+ )"
177
+ ```
178
+
179
+ **This is not optional.** Every contract issue that goes unreported costs another team hours.
180
+ AI agents: if you detect a contract mismatch during development, file the issue before continuing.
181
+
182
+ ## Company Context
183
+
184
+ IFCore is building an AI-powered building compliance checker. **5 teams** each develop in their **own GitHub repo** (cloned from a shared template). Teams write `check_*` functions independently β€” the platform integrates them automatically.
185
+
186
+ **How integration works:**
187
+ 1. Each team pushes `checker_*.py` files to their own repo under `tools/`
188
+ 2. The **platform repo** (`ifcore-platform`) pulls all 5 team repos as **git submodules**
189
+ 3. `deploy.sh` flattens submodules into `teams/<team-repo>/tools/` (real files, not symlinks β€” we don't configure HF to resolve submodules)
190
+ 4. The FastAPI orchestrator scans `teams/*/tools/checker_*.py` for `check_*` functions
191
+ 5. All discovered functions run against uploaded IFC files
192
+
193
+ **Deployment architecture:**
194
+
195
+ | Component | Deploys to | Who manages |
196
+ |-----------|-----------|-------------|
197
+ | Team check functions (`checker_*.py`) | Own GitHub repo β†’ pulled into platform | Each team |
198
+ | Backend + orchestrator (`ifcore-platform`) | **HuggingFace Space** (Docker, FastAPI) | Captains |
199
+ | Frontend (dashboard, 3D viewer, upload) | **Cloudflare Pages** | Captains |
200
+ | API gateway (async jobs, proxies to HF) | **Cloudflare Worker** | Captains |
201
+ | File storage (IFC uploads) | **Cloudflare R2** (S3-compatible) | Captains |
202
+ | Results database | **Cloudflare D1** (SQLite) | Captains |
203
+
204
+ **Flow:** User uploads IFC β†’ stored in R2 β†’ frontend calls CF Worker β†’ Worker proxies to HF Space β†’ orchestrator runs all `check_*` functions β†’ results posted back to Worker β†’ stored in D1 β†’ frontend polls and displays.
205
+
206
+ **Teams never touch the platform repo.** They only push to their own team repo. Captains handle `deploy.sh` which pulls, flattens, and pushes to HF.
207
+
208
+ **Teams:**
209
+ | Team | Focus area | Repo |
210
+ |------|-----------|------|
211
+ | [TBD] | [TBD] | [TBD] |
212
+ | [TBD] | [TBD] | [TBD] |
213
+ | [TBD] | [TBD] | [TBD] |
214
+ | [TBD] | [TBD] | [TBD] |
215
+ | [TBD] | [TBD] | [TBD] |
216
+
217
+ ## References
218
+
219
+ - [Validation Schema](./references/validation-schema.md) β€” database schema (`users`, `projects`, `check_results`, `element_results`) and how team `list[dict]` maps to rows
220
+ - [Architecture](./references/architecture.md) β€” project structure, AGENTS.md template, code conventions
221
+ - [Repo Structure](./references/repo-structure.md) β€” concrete file tree examples for all 4 repos (team, platform, frontend, gateway)
222
+ - [Frontend Architecture](./references/frontend-architecture.md) β€” modules, shared Zustand store, API client, D1 tables, how to add features
223
+ - [Development Patterns](./references/development-patterns.md) β€” how to plan and build new features
224
+
225
+ ### Related Skills (separate repos, installed alongside this one)
226
+
227
+ - **pydantic-ai** β€” PydanticAI agent framework: tools, structured output, orchestration, chat patterns
228
+ - **huggingface-deploy** β€” deploy the platform (`ifcore-platform`) as a Docker Space on HuggingFace; covers Dockerfile, secrets, R2 caching, and the flatten-before-push submodule pattern
229
+ - **cloudflare** β€” deploy the frontend + API gateway on Cloudflare Pages/Workers
teams/Mastodonte/.claude/skills/IFCore-skill/references/architecture.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture & Conventions
2
+
3
+ ## Project Structure
4
+
5
+ ```
6
+ your-team-repo/
7
+ β”œβ”€β”€ tools/
8
+ β”‚ β”œβ”€β”€ checker_doors.py # check_door_width, check_door_clearance
9
+ β”‚ β”œβ”€β”€ checker_fire_safety.py # check_fire_rating, check_exit_count
10
+ β”‚ └── checker_rooms.py # check_room_area, check_ceiling_height
11
+ β”œβ”€β”€ requirements.txt # team dependencies
12
+ └── README.md
13
+ ```
14
+
15
+ **File naming:** `checker_<topic>.py` β€” group related checks by topic.
16
+ Only `tools/checker_*.py` matters to the platform. Everything else (local test scripts,
17
+ notebooks, Gradio apps, CLI tools) is your choice β€” the platform ignores it.
18
+
19
+ **Platform auto-discovery:** the orchestrator scans `teams/*/tools/checker_*.py` and collects
20
+ every `check_*` function. No subdirectories β€” files must be directly inside `tools/`.
21
+ Helper files (e.g. `tools/utils.py`) are fine for shared code but won't be scanned.
22
+
23
+ **Platform integration:** the platform (`ifcore-platform`) pulls all 5 team repos via git
24
+ submodules and flattens them into `teams/<your-repo>/tools/` before building the Docker image.
25
+ Your repo structure (`tools/checker_*.py` with `check_*` functions) must match this layout
26
+ exactly for auto-discovery to work. Captains handle the pull and flatten via `deploy.sh` β€”
27
+ teams never push to the platform repo directly.
28
+
29
+ ## Code Conventions
30
+
31
+ - **Max 300 lines per file.** Split into modules when approaching the limit.
32
+ - **One function per check.** Don't combine multiple regulation checks.
33
+ - **File names:** `checker_<topic>.py` β€” e.g. `checker_doors.py`, `checker_fire_safety.py`.
34
+ - **Function names:** `check_<what>` β€” e.g. `check_door_width`, `check_room_area`.
35
+ - **First arg is always `model`** β€” an `ifcopenshell.file` object.
36
+ - **Return `list[str]`** β€” each string prefixed `[PASS]`, `[FAIL]`, or `[???]`.
37
+ - **No bare try/except.** Only catch specific known errors.
38
+
39
+ **What is `model`?** It's an `ifcopenshell.file` object β€” a parsed IFC file loaded into memory.
40
+ You query it with `model.by_type("IfcDoor")` to get all doors, `model.by_type("IfcWall")` for
41
+ walls, etc. Each element has properties like `.Name`, `.GlobalId`, and type-specific attributes.
42
+
43
+ ## AGENTS.md / CLAUDE.md
44
+
45
+ Every team MUST have this file in their repo root. Your AI assistant reads it automatically.
46
+ If it does not exist, create it before starting any work.
47
+
48
+ **Template:**
49
+ ```markdown
50
+ # <Project Name>
51
+
52
+ Always read the IFCore skill before developing on this project.
53
+
54
+ ## Structure
55
+ <paste your app/ directory tree here>
56
+
57
+ ## Conventions
58
+ - Max 300 lines per file
59
+ - One function per regulation check
60
+ - Files: tools/checker_<topic>.py β€” only checker_*.py files are scanned
61
+ - Functions: check_*(model, ...) -> list[str] with [PASS]/[FAIL]/[???] prefix
62
+
63
+ ## Issue Reporting
64
+ When you encounter a contract mismatch, skill gap, or integration problem:
65
+ gh issue create --repo SerjoschDuering/iaac-bimwise-skills --label "<label>" --title "<title>"
66
+ Labels: contract-gap, skill-drift, learning, schema-change, integration-bug
67
+
68
+ ## Learnings
69
+ <!-- Add here after every debugging session that reveals a recurring issue -->
70
+ ```
71
+
72
+ **Keep it updated.** After any session where you hit a recurring error, add it to Learnings.
73
+ The AI gets smarter with every fix β€” only if you write it down.
teams/Mastodonte/.claude/skills/IFCore-skill/references/development-patterns.md ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Development Patterns
2
+
3
+ ## How to Work With the User
4
+
5
+ You're helping someone who may be new to coding, AI, or both. Before doing anything:
6
+
7
+ - **Ask what they want to build.** Don't assume β€” let them explain in their own words.
8
+ - **Suggest an approach and explain why.** If you recommend something, say what it does
9
+ and why it's a good fit. Offer to explain any term they might not know.
10
+ - **Break things into small steps.** One thing at a time. Check in after each step.
11
+ - **Use plain language.** Say "a function that checks door widths" not "a callable that
12
+ validates dimensional properties." If you must use a technical term, explain it inline.
13
+
14
+ ## When Someone Wants to Add a New Check
15
+
16
+ Start a conversation β€” don't jump straight to code.
17
+
18
+ 1. **Understand what they want.** Ask: "What rule or regulation should this check enforce?"
19
+ Get a concrete example β€” "doors must be at least 900mm wide" is better than "door check."
20
+ 2. **Talk through the approach.** Explain what the check function will do, what IFC data
21
+ it needs, and roughly how it works. Ask if that sounds right.
22
+ 3. **Suggest a plan.** Offer to write a short plan first (see template below). This is
23
+ a simple document that describes the goal and what "done" looks like. It helps avoid
24
+ building the wrong thing. If the user prefers to skip it, that's fine β€” go straight to code.
25
+ 4. **Build it together.** Write the check function, test it, and walk through the result.
26
+ Explain what each part does. Update AGENTS.md with anything you learned.
27
+ 5. **Review when done.** Suggest a quick review β€” either in a fresh chat or with a subagent
28
+ if available. This catches things you both might have missed.
29
+
30
+ ## Feature Plans (Optional but Helpful)
31
+
32
+ For bigger features, a plan folder keeps things organized:
33
+
34
+ ```
35
+ feature-plans/
36
+ └── F1-door-width-check/
37
+ β”œβ”€β”€ plan.md # what we're building and why
38
+ β”œβ”€β”€ phase-a/ # if the feature has multiple phases
39
+ β”‚ └── plan.md # scope for this phase only
40
+ └── learnings.md # what went wrong, what to do differently
41
+ ```
42
+
43
+ A "plan" (sometimes called a PRD β€” Product Requirements Document) is just a short
44
+ description of what you're building. It helps you think before you code.
45
+
46
+ ## Plan Template
47
+
48
+ ```markdown
49
+ # F<N>: <Feature Name>
50
+
51
+ ## What Are We Building?
52
+ <!-- Explain the goal in plain language. What problem does this solve?
53
+ What will users be able to do after this is built? -->
54
+
55
+ ## What Does "Done" Look Like?
56
+ - [ ] ...
57
+ <!-- A checklist. When every box is ticked, the feature is done. -->
58
+
59
+ ## How It Works
60
+ <!-- Brief description of the approach. What IFC data do we need?
61
+ What does the check function do? -->
62
+
63
+ ## Phases (if it's a big feature)
64
+ - Phase A: ...
65
+ - Phase B: ...
66
+ ```
67
+
68
+ Don't overthink this. A few sentences per section is fine. Skip "Phases" for small checks.
69
+
70
+ ## After You Finish
71
+
72
+ Suggest a review:
73
+
74
+ > "That check is working. Want me to do a quick review to catch anything we missed?
75
+ > I can do it right now, or you can start a fresh chat and paste this:"
76
+
77
+ ```
78
+ I just finished implementing [feature name].
79
+ Codebase is at [path]. Key files changed: [list].
80
+ Please review against the plan at feature-plans/F<N>-<name>/plan.md
81
+ and check for IFCore contract compliance.
82
+ ```
83
+
84
+ If subagents are available, offer to run the review automatically.
teams/Mastodonte/.claude/skills/IFCore-skill/references/frontend-architecture.md ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Frontend Architecture
2
+
3
+ Modular web app. Each feature (upload, results, 3D viewer, dashboard) is a
4
+ **module** β€” a self-contained folder. Backend operations are **async jobs**.
5
+
6
+ > **New to web development?** A frontend is the part users see and click on
7
+ > (the website). The backend is the server doing the heavy work (running checks).
8
+ > They talk to each other through an **API** β€” a set of URLs the frontend
9
+ > calls to send or receive data.
10
+
11
+ ## Structure
12
+
13
+ ```
14
+ src/
15
+ β”œβ”€β”€ app.js ← shell: nav bar + router
16
+ β”œβ”€β”€ api.js ← shared API client β†’ CF Worker
17
+ β”œβ”€β”€ store.js ← shared state (Zustand)
18
+ β”œβ”€β”€ poller.js ← polls active jobs, updates store
19
+ β”œβ”€β”€ modules/
20
+ β”‚ β”œβ”€β”€ upload/ ← file upload
21
+ β”‚ β”œβ”€β”€ results/ ← results table
22
+ β”‚ β”œβ”€β”€ viewer-3d/ ← IFC 3D viewer
23
+ β”‚ └── dashboard/ ← analytics
24
+ └── shared/ ← reusable components
25
+ ```
26
+
27
+ ## Shell + Router
28
+
29
+ Nav bar at top, content area below. Router swaps modules like tabs.
30
+
31
+ > **What's a router?** It watches the URL in the browser. When the URL says
32
+ > `/results`, it shows the results module. When it says `/dashboard`, it shows
33
+ > the dashboard. Each "page" is a module, but it's all one app β€” no page reloads.
34
+
35
+ ```
36
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
37
+ β”‚ Nav: Upload | Results | 3D | Dash β”‚
38
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
39
+ β”‚ ← active module renders here β†’ β”‚
40
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
41
+ ```
42
+
43
+ Each module exports `mount(container)`. That's the only contract.
44
+
45
+ ## Async Job Pattern
46
+
47
+ Backend tasks (IFC checks, AI agents) take 10-60 seconds. Too slow for
48
+ a normal request-response. Everything uses **async jobs**.
49
+
50
+ > **What's async?** Normally when you click a button, the browser waits for
51
+ > the server to respond β€” that's synchronous ("sync"). But if the server needs
52
+ > 30 seconds, the browser would freeze. **Async** means: "start the work,
53
+ > come back and check later." The server returns a job ID immediately,
54
+ > and the frontend keeps checking ("polling") until the job is done.
55
+
56
+ ```
57
+ Frontend Worker (proxy) HF Space (FastAPI)
58
+ ──────── ────────────── ──────────────────
59
+ POST /check ───> proxy ───> start background task
60
+ <─── {jobId} <─── return {jobId} immediately
61
+
62
+ poll GET /jobs/id read D1 ...working...
63
+ <─── {status:"running"}
64
+
65
+ poll GET /jobs/id read D1 ───> POST /jobs/id/complete (callback)
66
+ <─── {status:"done", data:[...]}
67
+ ```
68
+
69
+ **Why this pattern?** The CF Worker has a 10ms CPU limit on the free tier β€”
70
+ it physically can't wait 30 seconds. So it just reads/writes to the database
71
+ and returns. The heavy work happens on the HF Space.
72
+
73
+ > **What's a callback?** When the HF Space finishes, it calls the Worker
74
+ > back ("hey, job X is done, here are the results"). The Worker writes
75
+ > the results to the database. Next time the frontend polls, it gets them.
76
+
77
+ ### Recipe: Adding a New Async Endpoint
78
+
79
+ Three files. Always the same pattern.
80
+
81
+ | File | What to add |
82
+ |------|-------------|
83
+ | **HF Space** `main.py` | `POST /your-thing` β†’ starts `BackgroundTasks`, returns `{jobId}`. When done, POSTs results back to Worker. |
84
+ | **Worker** | Proxy route for `POST /api/your-thing`. Job tracking routes (`GET /api/jobs/:id`, `POST /api/jobs/:id/complete`) are shared β€” built once. |
85
+ | **Frontend** `api.js` | `startYourThing(fileUrl)` β†’ returns `{jobId}`. Call `store.trackJob(jobId)` β€” poller handles the rest. |
86
+
87
+ > **What's an endpoint?** A specific URL the server listens on. `POST /check`
88
+ > is an endpoint. `GET /jobs/123` is another. Think of them as doors into the
89
+ > backend β€” each door does one thing.
90
+
91
+ ## Shared State (Zustand)
92
+
93
+ **Zustand** is a tiny state library (~1KB). Think of it as a shared whiteboard β€”
94
+ any module can read or write to it. The poller updates it when jobs complete.
95
+
96
+ > **What's state?** The data your app is currently showing. "Which file is
97
+ > selected? Are checks running? What were the results?" That's all state.
98
+ > Without a shared store, each module would have its own copy and they'd
99
+ > get out of sync. Zustand keeps one source of truth.
100
+
101
+ **How modules use it:**
102
+ - **Upload** sets `currentFile`, starts a job β†’ `trackJob(jobId)`
103
+ - **Results** reads `getActiveResults()` β†’ renders a table
104
+ - **3D Viewer** reads results β†’ highlights failing elements in red
105
+ - **Dashboard** reads results β†’ shows charts and stats
106
+
107
+ They all see the same data. When a job completes, everything re-renders.
108
+
109
+ ```javascript
110
+ // store.js β€” schematic
111
+ {
112
+ currentFile: null, // { url, name }
113
+ jobs: {}, // { [jobId]: { status, data, startedAt } }
114
+ activeJobId: null,
115
+ trackJob(jobId), // start tracking
116
+ completeJob(jobId, data), // poller calls this
117
+ getActiveResults(), // results for active job
118
+ }
119
+ ```
120
+
121
+ **Poller:** every 2s, calls `GET /api/jobs/:id` for running jobs.
122
+ When status flips to `"done"`, calls `store.completeJob()`.
123
+
124
+ **API client** (`api.js`): all modules go through this β€” never call `fetch()` directly.
125
+ Key functions: `uploadFile()`, `startCheck()`, `getJob()`, `getStats()`.
126
+
127
+ > **What's `fetch()`?** The browser's built-in way to call an API. `api.js`
128
+ > wraps it so you don't repeat the base URL and error handling everywhere.
129
+
130
+ ## Module Pattern
131
+
132
+ Modules render once, then **subscribe** to re-render on state changes:
133
+
134
+ ```javascript
135
+ // modules/summary/index.js β€” schematic
136
+ export function mount(container) {
137
+ function render() {
138
+ const results = useStore.getState().getActiveResults()
139
+ container.innerHTML = `${passed} passed, ${failed} failed`
140
+ }
141
+ render() // initial
142
+ useStore.subscribe(render) // re-render on change
143
+ }
144
+ ```
145
+
146
+ > **What's subscribe?** "Notify me when something changes." Without it, your
147
+ > module renders once and never updates. With `subscribe(render)`, every time
148
+ > the store changes (new results, job completed), your module re-renders
149
+ > automatically.
150
+
151
+ **Rules:**
152
+ - Don't import from other modules
153
+ - Read state from `store.js` via `subscribe()`
154
+ - Call backend through `api.js`
155
+ - One folder, one concern
156
+
157
+ ## Adding a Module (Checklist)
158
+
159
+ 1. Create `src/modules/<name>/index.js` with `mount()` + `subscribe()`
160
+ 2. Register route in `app.js`
161
+ 3. If it needs a new backend endpoint β†’ follow async recipe above
162
+
163
+ ## Adding Shared State
164
+
165
+ Only if multiple modules need it. Otherwise keep local.
166
+
167
+ 1. Add to `store.js`: state field + setter
168
+ 2. Read from modules via `getState()` + `subscribe()`
169
+
170
+ ## Database (D1)
171
+
172
+ > **What's D1?** Cloudflare's database service. It runs SQLite (a simple
173
+ > database that stores data in tables, like a spreadsheet). The frontend
174
+ > never talks to D1 directly β€” it goes through the Worker API.
175
+
176
+ ```sql
177
+ CREATE TABLE users (
178
+ id TEXT PRIMARY KEY,
179
+ name TEXT,
180
+ team TEXT, -- e.g. "ifcore-team-a", nullable
181
+ created_at INTEGER
182
+ );
183
+
184
+ CREATE TABLE projects (
185
+ id TEXT PRIMARY KEY,
186
+ user_id TEXT REFERENCES users(id),
187
+ name TEXT,
188
+ file_url TEXT,
189
+ ifc_schema TEXT, -- e.g. "IFC4", null if unknown
190
+ region TEXT, -- e.g. "CH", null if unknown
191
+ building_type TEXT, -- e.g. "residential", null
192
+ metadata TEXT, -- JSON blob, nullable
193
+ created_at INTEGER
194
+ );
195
+
196
+ CREATE TABLE check_results (
197
+ id TEXT PRIMARY KEY,
198
+ project_id TEXT REFERENCES projects(id),
199
+ job_id TEXT, -- groups results from one run
200
+ check_name TEXT, -- e.g. "check_door_width"
201
+ team TEXT, -- e.g. "ifcore-team-a"
202
+ status TEXT DEFAULT 'running', -- pass | fail | unknown | error | running
203
+ summary TEXT, -- "14 doors: 12 pass, 2 fail"
204
+ has_elements INTEGER DEFAULT 0,
205
+ created_at INTEGER
206
+ );
207
+
208
+ CREATE TABLE element_results (
209
+ id TEXT PRIMARY KEY,
210
+ check_result_id TEXT REFERENCES check_results(id),
211
+ element_id TEXT, -- IFC GlobalId (nullable)
212
+ element_type TEXT, -- e.g. "IfcDoor" (nullable)
213
+ element_name TEXT, -- e.g. "Door #42" (nullable)
214
+ element_name_long TEXT, -- e.g. "Door #42 (Level 1, Zone A)" (nullable)
215
+ check_status TEXT, -- pass | fail | warning | blocked | log
216
+ actual_value TEXT, -- e.g. "750 mm"
217
+ required_value TEXT, -- e.g. "800 mm"
218
+ comment TEXT, -- human-readable explanation (nullable)
219
+ log TEXT -- debug/trace info (nullable)
220
+ );
221
+ ```
222
+
223
+ See [Validation Schema](./validation-schema.md) for how team `list[dict]` output maps to these rows.
224
+
225
+ > **What's a migration?** A file that changes the database structure
226
+ > (adds a table, adds a column). You run it once with `wrangler d1 execute`.
227
+ > It's like a recipe for the database β€” run it, and the new table exists.
228
+
229
+ **Adding a new table:** migration file β†’ `wrangler d1 execute` β†’ Worker endpoint β†’ `api.js` function β†’ module uses it.
230
+
231
+ ## PRD Review (Wednesday)
232
+
233
+ > **What's a PRD?** Product Requirements Document β€” a short plan describing
234
+ > what you're building, why, and what "done" looks like. Doesn't need to be
235
+ > fancy. Half a page is fine.
236
+
237
+ Before building, each team writes a PRD for their module. All reviewed together:
238
+ - What each team builds
239
+ - What async endpoints are needed
240
+ - What shared state each module expects
241
+ - Whether modules overlap
teams/Mastodonte/.claude/skills/IFCore-skill/references/repo-structure.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Repository Structure
2
+
3
+ ## Team repos β€” `ifcore-team-a` … `ifcore-team-e`
4
+ One repo per team. Students only ever touch their own.
5
+ ```
6
+ ifcore-team-a/
7
+ β”œβ”€β”€ tools/
8
+ β”‚ β”œβ”€β”€ checker_doors.py # check_door_width, check_door_clearance
9
+ β”‚ β”œβ”€β”€ checker_fire_safety.py # check_fire_rating, check_exit_count
10
+ β”‚ └── checker_rooms.py # check_room_area, check_ceiling_height
11
+ β”œβ”€β”€ requirements.txt
12
+ β”œβ”€β”€ AGENTS.md
13
+ └── README.md
14
+ ```
15
+
16
+ ## Platform monorepo β€” `ifcore-platform`
17
+ **One repo. Two folders. Two deployments.**
18
+ ```
19
+ ifcore-platform/ ← ONE git repo
20
+ β”‚
21
+ β”œβ”€β”€ backend/ β†’ deploys to HuggingFace Space
22
+ β”‚ β”œβ”€β”€ README.md ← HF frontmatter (sdk: docker, app_port: 7860)
23
+ β”‚ β”œβ”€β”€ Dockerfile
24
+ β”‚ β”œβ”€β”€ requirements.txt
25
+ β”‚ β”œβ”€β”€ main.py
26
+ β”‚ β”œβ”€β”€ orchestrator.py
27
+ β”‚ β”œβ”€β”€ deploy.sh
28
+ β”‚ └── teams/ ← gitignored, populated by deploy.sh
29
+ β”‚ β”œβ”€β”€ ifcore-team-a/tools/checker_*.py
30
+ β”‚ β”œβ”€β”€ ifcore-team-b/tools/checker_*.py
31
+ β”‚ └── ... ← one folder per team, flattened from submodules
32
+ β”‚
33
+ └── frontend/ β†’ deploys to Cloudflare (Pages + Worker)
34
+ β”œβ”€β”€ public/
35
+ β”‚ └── index.html
36
+ β”œβ”€β”€ src/ ← static app (CF Pages)
37
+ β”‚ β”œβ”€β”€ app.js
38
+ β”‚ β”œβ”€β”€ api.js
39
+ β”‚ β”œβ”€β”€ store.js
40
+ β”‚ β”œβ”€β”€ poller.js
41
+ β”‚ └── modules/
42
+ β”‚ β”œβ”€β”€ upload/index.js
43
+ β”‚ β”œβ”€β”€ results/index.js
44
+ β”‚ β”œβ”€β”€ viewer-3d/index.js
45
+ β”‚ └── dashboard/index.js
46
+ β”œβ”€β”€ functions/ ← API gateway (CF Pages Functions = Worker)
47
+ β”‚ └── api/
48
+ β”‚ └── [[route]].js
49
+ β”œβ”€β”€ migrations/
50
+ β”‚ └── 0001_create_jobs.sql
51
+ β”œβ”€β”€ package.json
52
+ └── wrangler.toml ← D1 + R2 bindings, custom domain
53
+ ```
teams/Mastodonte/.claude/skills/IFCore-skill/references/validation-schema.md ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Validation Schema
2
+
3
+ Two layers: what teams produce, and how the platform stores it.
4
+
5
+ ## Team Output (locked β€” Board Meeting #1)
6
+
7
+ Each `check_*` function returns `list[dict]`. Each dict is one element checked,
8
+ mapping directly to one `element_results` row in the database.
9
+
10
+ ```python
11
+ def check_door_width(model, min_width_mm=800):
12
+ results = []
13
+ for door in model.by_type("IfcDoor"):
14
+ width_mm = round(door.OverallWidth * 1000) if door.OverallWidth else None
15
+ results.append({
16
+ "element_id": door.GlobalId,
17
+ "element_type": "IfcDoor",
18
+ "element_name": door.Name or f"Door #{door.id()}",
19
+ "element_name_long": f"{door.Name} (Level 1, Zone A)",
20
+ "check_status": "blocked" if width_mm is None
21
+ else "pass" if width_mm >= min_width_mm
22
+ else "fail",
23
+ "actual_value": f"{width_mm} mm" if width_mm else None,
24
+ "required_value": f"{min_width_mm} mm",
25
+ "comment": None if width_mm and width_mm >= min_width_mm
26
+ else f"Door is {min_width_mm - width_mm} mm too narrow"
27
+ if width_mm else "Width property missing",
28
+ "log": None,
29
+ })
30
+ return results
31
+ ```
32
+
33
+ **Required dict fields** (teams produce these β€” `id` and `check_result_id` are added by the orchestrator):
34
+
35
+ | Field | Type | Description |
36
+ |-------|------|-------------|
37
+ | `element_id` | string \| null | IFC GlobalId |
38
+ | `element_type` | string \| null | e.g. `"IfcDoor"`, `"IfcWall"` |
39
+ | `element_name` | string \| null | Short name, e.g. `"Door #42"` |
40
+ | `element_name_long` | string \| null | Detailed name with context, e.g. `"Door #42 (Level 1, Zone A)"` |
41
+ | `check_status` | string | **`pass`** \| **`fail`** \| **`warning`** \| **`blocked`** \| **`log`** |
42
+ | `actual_value` | string \| null | What was found, e.g. `"750 mm"` |
43
+ | `required_value` | string \| null | What the regulation requires, e.g. `"800 mm"` |
44
+ | `comment` | string \| null | Human-readable explanation (why it failed, what's wrong) |
45
+ | `log` | string \| null | Debug/trace info (optional, for troubleshooting) |
46
+
47
+ **`check_status` values:**
48
+ - `"pass"` β€” element meets the requirement
49
+ - `"fail"` β€” element violates the requirement
50
+ - `"warning"` β€” element is borderline or needs manual review
51
+ - `"blocked"` β€” data missing, check cannot run (e.g. property not found)
52
+ - `"log"` β€” informational output, not a pass/fail judgment
53
+
54
+ ## Platform Database Schema (D1)
55
+
56
+ Four tables. The frontend reads from these via the CF Worker API.
57
+
58
+ ```
59
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
60
+ β”‚ users β”‚ 1───* β”‚ projects β”‚ 1───* β”‚ check_results β”‚ 1───* β”‚ element_results β”‚
61
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
62
+ ```
63
+
64
+ ### `users` β€” one row per person
65
+
66
+ ```json
67
+ {
68
+ "id": "string",
69
+ "name": "string",
70
+ "team": "string | null",
71
+ "created_at": "integer"
72
+ }
73
+ ```
74
+
75
+ ### `projects` β€” one row per uploaded IFC file
76
+
77
+ ```json
78
+ {
79
+ "id": "string",
80
+ "user_id": "string",
81
+ "name": "string",
82
+ "file_url": "string",
83
+ "ifc_schema": "string | null",
84
+ "region": "string | null",
85
+ "building_type": "string | null",
86
+ "metadata": "string | null (JSON)",
87
+ "created_at": "integer"
88
+ }
89
+ ```
90
+
91
+ ### `check_results` β€” one row per `check_*` function run
92
+
93
+ ```json
94
+ {
95
+ "id": "string",
96
+ "project_id": "string",
97
+ "job_id": "string",
98
+ "check_name": "string",
99
+ "team": "string",
100
+ "status": "string (running | pass | fail | unknown | error)",
101
+ "summary": "string",
102
+ "has_elements": "integer (0 | 1)",
103
+ "created_at": "integer"
104
+ }
105
+ ```
106
+
107
+ - `check_name`: the function name, e.g. `check_door_width`
108
+ - `team`: derived from the repo folder name, e.g. `ifcore-team-a`
109
+ - `status`: `running` while job is in progress; then aggregate β€” `pass` if all elements pass, `fail` if any fail, `error` if the function threw
110
+ - `summary`: human-readable, e.g. "14 doors checked: 12 pass, 2 fail"
111
+ - `has_elements`: `1` if the check produced element-level results, `0` otherwise
112
+
113
+ ### `element_results` β€” one row per element checked
114
+
115
+ ```json
116
+ {
117
+ "id": "string",
118
+ "check_result_id": "string",
119
+ "element_id": "string | null",
120
+ "element_type": "string | null",
121
+ "element_name": "string | null",
122
+ "element_name_long":"string | null",
123
+ "check_status": "string (pass | fail | warning | blocked | log)",
124
+ "actual_value": "string | null",
125
+ "required_value": "string | null",
126
+ "comment": "string | null",
127
+ "log": "string | null"
128
+ }
129
+ ```
130
+
131
+ - `id`, `check_result_id`: added by the orchestrator (teams don't produce these)
132
+ - `element_id`: IFC GlobalId (if available)
133
+ - `check_status`: matches what the team function returns β€” not aggregated
134
+ - `comment`: human-readable explanation of the result
135
+ - `log`: debug/trace info for troubleshooting
136
+
137
+ ## How It Fits Together
138
+
139
+ ```
140
+ Team function returns:
141
+ [
142
+ {"element_id": "2O2Fr$t4X7Z", "element_type": "IfcDoor",
143
+ "element_name": "Door #42", "element_name_long": "Door #42 (Level 1)",
144
+ "check_status": "pass", "actual_value": "850 mm", "required_value": "800 mm",
145
+ "comment": null, "log": null},
146
+ {"element_id": "1B3Rs$u5Y8A", "element_type": "IfcDoor",
147
+ "element_name": "Door #17", "element_name_long": "Door #17 (Level 2)",
148
+ "check_status": "fail", "actual_value": "750 mm", "required_value": "800 mm",
149
+ "comment": "Door is 50 mm too narrow", "log": null}
150
+ ]
151
+
152
+ Orchestrator creates:
153
+
154
+ check_results row:
155
+ check_name = "check_door_width"
156
+ team = "ifcore-team-a"
157
+ status = "fail" ← any fail β†’ whole check fails
158
+ summary = "2 doors: 1 pass, 1 fail"
159
+ has_elements = 1
160
+
161
+ element_results rows: (one per dict, id + check_result_id added)
162
+ { element_name: "Door #42", check_status: "pass", actual_value: "850 mm", ... }
163
+ { element_name: "Door #17", check_status: "fail", actual_value: "750 mm", comment: "Door is 50 mm too narrow", ... }
164
+ ```
teams/Mastodonte/.gitignore ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =========================
2
+ # Python cache
3
+ # =========================
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+
8
+ # =========================
9
+ # Virtual environments
10
+ # =========================
11
+ venv/
12
+ .env/
13
+ .venv/
14
+ env/
15
+ ENV/
16
+
17
+ # =========================
18
+ # Jupyter
19
+ # =========================
20
+ .ipynb_checkpoints/
21
+
22
+ # =========================
23
+ # OS files
24
+ # =========================
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # =========================
29
+ # Logs
30
+ # =========================
31
+ *.log
32
+
33
+ # =========================
34
+ # IDE / Editor
35
+ # =========================
36
+ .vscode/
37
+ .idea/
38
+
39
+ # =========================
40
+ # Build / Packaging
41
+ # =========================
42
+ build/
43
+ dist/
44
+ *.egg-info/
45
+ .ifc/
teams/Mastodonte/01_team_b ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Team B β€” User Portal
2
+
3
+ **Branch:** `feature/team-B-adminpanel` | **Verdict:** NEEDS CHANGES (major scope reduction)
4
+
5
+ | Criterion | Score | Notes |
6
+ |-----------|-------|-------|
7
+ | Architecture Fit | 3/5 | Correctly uses CF Worker + Pages + D1, but ignores existing patterns |
8
+ | Scope Feasibility | 1/5 | 4-week enterprise plan for a 1-day sprint |
9
+ | Shared State | 1/5 | No Zustand slice docs. No Reads/Writes section |
10
+ | API Design | 2/5 | 13+ new endpoints, none follow async job pattern |
11
+ | Security | 3/5 | Good on paper, impossible to implement correctly in 1 day |
12
+
13
+ **Shared State:** Reads `store.projects` (profile page). Writes: none (uses Better Auth `useSession()` hook).
14
+
15
+ ---
16
+
17
+ Hey Team B! You took on one of the most important features in the entire platform.
18
+ Authentication is the backbone that every other team depends on.
19
+ Here is where you stand and what to do tomorrow.
20
+
21
+ ---
22
+
23
+ ## What You Did Well
24
+
25
+ 1. **You thought like a real product team.** Your PRD covers email verification,
26
+ billing, support tickets, account deletion, RBAC, monitoring -- that is a
27
+ complete SaaS auth system. The fact that you thought through edge cases
28
+ like stale Stripe webhooks and partial provisioning rollback shows genuine
29
+ product thinking. That matters.
30
+
31
+ 2. **You picked the right infrastructure.** Cloudflare Workers for API, Pages
32
+ for frontend, D1 for storage -- this matches the platform architecture exactly.
33
+ You did not invent your own deployment target or try to spin up a separate
34
+ server. That alignment saves everyone time.
35
+
36
+ 3. **Security was on your mind from the start.** HTTP-only cookies, CSRF protection,
37
+ SameSite headers, encrypted fields, PCI considerations for Stripe -- you
38
+ clearly researched what secure auth looks like. That awareness is more valuable
39
+ than most people realize.
40
+
41
+ ---
42
+
43
+ ## The Big Picture
44
+
45
+ The IFCore platform has a specific architecture. Here is the part that matters for you:
46
+
47
+ ```
48
+ Browser --> Cloudflare Worker (API + auth) --> HuggingFace Space (IFC checks)
49
+ |
50
+ D1 (database)
51
+ R2 (file storage)
52
+ ```
53
+
54
+ Your job is to add **one thing** to this picture: a way for users to sign up,
55
+ log in, and have the platform know who they are. That is it. Every other team
56
+ (3D viewer, dashboard, report generator) just needs `useSession()` to get the
57
+ current user. If you ship that, you unblock everyone.
58
+
59
+ The platform already has a `users` table in D1:
60
+ ```sql
61
+ CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT, team TEXT, created_at INTEGER);
62
+ ```
63
+
64
+ Your auth system needs to work **alongside** this, not replace it.
65
+
66
+ ---
67
+
68
+ ## What Needs to Change
69
+
70
+ ### 1. Scope: cut 80% of the features
71
+
72
+ **Why:** Your PRD describes a 4-week enterprise build. The course ends tomorrow.
73
+ Even experienced engineers at companies like Auth0 took months to build what you
74
+ described. This is not a criticism -- it means you were thinking big. But shipping
75
+ something real beats having a perfect plan that never runs.
76
+
77
+ **What to keep:** Sign up, log in, log out, and a profile page. That is your MVP.
78
+
79
+ ### 2. Use Better Auth instead of building auth from scratch
80
+
81
+ **Why:** The Cloudflare skill that your AI agent has access to includes a complete
82
+ Better Auth blueprint. Better Auth is an open-source library that handles password
83
+ hashing, sessions, cookies, and database tables automatically. It works with D1,
84
+ Drizzle, and Hono -- exactly the stack the platform uses.
85
+
86
+ Building auth from scratch (your own password hashing, session tokens, CSRF
87
+ protection) is a weeks-long project with serious security risks if done wrong.
88
+ Better Auth gives you all of that in about 50 lines of config.
89
+
90
+ **Critical detail:** Do NOT use bcrypt for password hashing. Cloudflare Workers
91
+ have a 10ms CPU limit on the free tier. Bcrypt exceeds that limit and your signup
92
+ will fail with Error 1102. The Cloudflare skill has a PBKDF2 hasher that works
93
+ within Workers limits.
94
+
95
+ ### 3. Do not modify the existing `users` table
96
+
97
+ **Why:** Other teams already depend on the current schema. If you change it,
98
+ their code breaks. Better Auth creates its own tables (`user`, `session`,
99
+ `account`, `verification`) -- it does not touch the existing `users` table.
100
+ This is additive, not destructive.
101
+
102
+ ### 4. Follow the platform's shared state pattern
103
+
104
+ **Why:** The platform uses Zustand for client-side state. Each feature gets a
105
+ "slice" that other teams can read from. Your PRD did not include a Shared State
106
+ section, which means other teams would not know how to access auth data.
107
+
108
+ The good news: Better Auth provides a `useSession()` React hook. Other teams
109
+ can just call it. You do not even need a Zustand slice -- the hook is the
110
+ interface.
111
+
112
+ ### 5. Drop all external service dependencies
113
+
114
+ **Why:** Stripe requires a merchant account. Email verification requires a
115
+ transactional email provider (SendGrid, Resend, etc.). TOTP/2FA requires
116
+ a mobile app integration. None of these are provisioned, and setting them up
117
+ takes longer than building the auth feature itself.
118
+
119
+ ---
120
+
121
+ ## Your 1-Day Game Plan
122
+
123
+ Your AI coding agent has the **Cloudflare skill** installed. It contains the
124
+ exact Better Auth blueprint below. Tell your agent: "Use the Cloudflare skill's
125
+ Better Auth pattern to set up authentication." It will know what to do.
126
+
127
+ ### Morning (3 hours): Backend auth
128
+
129
+ **Step 1 -- Install Better Auth** (10 min)
130
+ ```bash
131
+ cd frontend
132
+ npm install better-auth drizzle-orm
133
+ ```
134
+
135
+ **Step 2 -- Add the nodejs_compat flag** (5 min)
136
+
137
+ In `frontend/wrangler.jsonc`, add:
138
+ ```jsonc
139
+ "compatibility_flags": ["nodejs_compat"]
140
+ ```
141
+
142
+ **Step 2b -- Update the Bindings type** (5 min)
143
+
144
+ In `frontend/worker/types.ts`, add the auth secrets:
145
+ ```typescript
146
+ export type Bindings = {
147
+ DB: D1Database;
148
+ STORAGE: R2Bucket;
149
+ ASSETS: Fetcher;
150
+ HF_SPACE_URL: string;
151
+ BETTER_AUTH_SECRET: string; // add this
152
+ BETTER_AUTH_URL: string; // add this
153
+ };
154
+ ```
155
+
156
+ **Step 3 -- Create the auth config** (30 min)
157
+
158
+ Create `frontend/worker/lib/auth.ts`. The Cloudflare skill has the full file
159
+ including the PBKDF2 hasher. Key points:
160
+ - Use `drizzleAdapter(db, { provider: 'sqlite' })` for D1
161
+ - Set `basePath: '/api/auth'` (NOT `/auth` -- see note below)
162
+ - Add both localhost and production URLs to `trustedOrigins`
163
+ - Use the PBKDF2 hash/verify functions (NOT bcrypt)
164
+
165
+ **Critical path note:** The Worker only intercepts requests matching
166
+ `run_worker_first: ["/api/*"]` in `wrangler.jsonc`. If you mount auth
167
+ at `/auth/*`, requests will never reach the Worker -- they hit the
168
+ static asset handler and 404. Always use `/api/auth/*`.
169
+
170
+ **Step 4 -- Mount auth routes in the Worker** (15 min)
171
+
172
+ First, fix CORS. The existing `cors({ origin: "*" })` is incompatible with
173
+ auth cookies (`credentials: 'include'`). Browsers reject wildcard origins
174
+ when credentials are sent. Replace with an explicit allowlist:
175
+ ```typescript
176
+ app.use("/api/*", cors({
177
+ origin: ["http://localhost:5173", "https://ifcore-platform.tralala798.workers.dev"],
178
+ credentials: true,
179
+ }));
180
+ ```
181
+
182
+ Then mount the auth routes under `/api/auth/*`:
183
+ ```typescript
184
+ app.on(['GET', 'POST'], '/api/auth/*', (c) => {
185
+ const auth = createAuth(c.env)
186
+ return auth.handler(c.req.raw)
187
+ })
188
+ ```
189
+
190
+ **Step 5 -- Set secrets** (10 min)
191
+
192
+ For production:
193
+ ```bash
194
+ wrangler secret put BETTER_AUTH_SECRET # generate with: openssl rand -base64 32
195
+ wrangler secret put BETTER_AUTH_URL # https://ifcore-platform.tralala798.workers.dev
196
+ ```
197
+
198
+ For local development, create `frontend/.dev.vars`:
199
+ ```env
200
+ BETTER_AUTH_SECRET=local-dev-secret-change-me
201
+ BETTER_AUTH_URL=http://localhost:5173
202
+ ```
203
+ Without `.dev.vars`, local `npm run dev` will crash on missing env vars.
204
+
205
+ **Step 6 -- Create the D1 migration** (20 min)
206
+
207
+ Add a new migration file (e.g., `0003_auth.sql`) with the Better Auth tables:
208
+ `session`, `account`, `verification`. Better Auth defaults to a table named
209
+ `user` (singular), but the platform already has `users` (plural). To avoid
210
+ two separate user tables, configure Better Auth to use the existing table:
211
+ ```typescript
212
+ // in auth.ts config
213
+ user: { tableName: "users" },
214
+ ```
215
+ Then the migration only adds columns to `users` (e.g., `email_verified`,
216
+ `image`) and creates the three new tables. Do NOT drop or recreate `users`.
217
+
218
+ **Note:** The `@better-auth/cli generate` command does NOT work with the
219
+ factory-pattern auth config required for Workers. Create the SQL manually.
220
+ See the Better Auth docs for exact column definitions.
221
+
222
+ ### Afternoon (3 hours): Frontend
223
+
224
+ **Step 7 -- Create the auth client** (15 min)
225
+
226
+ Create `frontend/src/lib/auth-client.ts`:
227
+ ```typescript
228
+ import { createAuthClient } from 'better-auth/react'
229
+
230
+ export const authClient = createAuthClient({
231
+ baseURL: '/api/auth',
232
+ fetchOptions: { credentials: 'include' },
233
+ })
234
+
235
+ export const { useSession } = authClient
236
+ ```
237
+
238
+ **Step 8 -- Build the login page** (45 min)
239
+
240
+ A simple form with email + password fields, a "Log In" button, and a
241
+ "Sign Up" button. The Cloudflare skill has a complete `AuthForm.tsx` component
242
+ you can adapt. Add it as a route at `/login` using TanStack Router.
243
+
244
+ **Step 9 -- Build the profile page** (30 min)
245
+
246
+ Show the current user's name, email, and team. Add a "Log Out" button.
247
+ Route: `/profile`.
248
+
249
+ **Step 10 -- Update the navbar** (15 min)
250
+
251
+ If logged in: show the user's name and a link to `/profile`.
252
+ If not logged in: show a "Log In" link to `/login`.
253
+
254
+ **Step 11 -- Test end to end** (30 min)
255
+
256
+ 1. Sign up with an email and password
257
+ 2. Verify you are redirected and the navbar shows your name
258
+ 3. Log out and verify the navbar changes
259
+ 4. Log back in and verify your profile page works
260
+
261
+ **Step 12 -- Deploy** (15 min)
262
+ ```bash
263
+ cd frontend
264
+ npm run db:migrate:remote # applies the new auth migration
265
+ npm run deploy # builds + deploys to Cloudflare
266
+ ```
267
+
268
+ ---
269
+
270
+ ## What to Cut (and Why That is OK)
271
+
272
+ | Feature | Why it is cut |
273
+ |---------|---------------|
274
+ | Stripe billing | Requires a merchant account nobody has set up |
275
+ | Email verification | Requires a transactional email provider |
276
+ | Password reset | Depends on email sending |
277
+ | 2FA / TOTP | Nice for security, but adds a full day of work |
278
+ | 4-role RBAC | Use `role: "member" or "captain"` -- two roles is enough |
279
+ | Support ticketing | Completely separate feature, not auth |
280
+ | Account deletion with 30-day retention | Post-launch feature |
281
+ | API key management | Not needed for the demo |
282
+ | Monitoring and alerting | Not needed for the demo |
283
+
284
+ Cutting features is not failure. Every real product launch involves cutting scope.
285
+ The difference between a shipped product and a perfect plan is that one of them
286
+ actually exists. You aimed high -- now let's ship what matters.
287
+
288
+ ---
289
+
290
+ ## How Your Feature Connects to Others
291
+
292
+ Authentication is a **foundation feature**. Here is how other teams use it:
293
+
294
+ - **Team D (3D Viewer):** Calls `useSession()` to know which user uploaded a model
295
+ - **Team E (Dashboard):** Filters check results by user/team using the session
296
+ - **Report Generator:** Attaches user info to exported compliance reports
297
+ - **Every team:** Can protect routes with your auth -- only logged-in users see the app
298
+
299
+ If you ship login + signup + session management, you give every other team
300
+ a `useSession()` hook that returns `{ id, email, name }`. That is the most
301
+ valuable single feature anyone can deliver tomorrow.
302
+
303
+ You aimed high and that is great. Now let's focus on what we can ship.
teams/Mastodonte/COMPLIANCE_AUDIT.md ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # IFCore Compliance Audit Report
2
+
3
+ **Date:** February 18, 2026
4
+ **Project:** AI Speed Run - Week 05
5
+ **Status:** βœ… **FULLY COMPLIANT**
6
+
7
+ ---
8
+
9
+ ## Executive Summary
10
+
11
+ All Python files in `tools/` directory comply with IFCore platform contracts:
12
+ - βœ… Naming conventions enforced
13
+ - βœ… Function signatures correct
14
+ - βœ… Return types compliant
15
+ - βœ… IFCore schema implemented
16
+ - βœ… Module exports configured
17
+
18
+ ---
19
+
20
+ ## File-by-File Audit
21
+
22
+ ### 1. **checker_dwelling.py** βœ… COMPLIANT
23
+
24
+ | Requirement | Status | Details |
25
+ |---|---|---|
26
+ | **Filename** | βœ… | Follows `checker_*.py` pattern |
27
+ | **Function Name** | βœ… | `check_dwelling_area()` follows `check_*` prefix |
28
+ | **Signature** | βœ… | `check_dwelling_area(model, min_area=36.0)` - model is 1st arg |
29
+ | **Return Type** | βœ… | Returns `list[dict]` |
30
+ | **Schema Keys** | βœ… | All IFCore keys present: element_id, element_type, element_name, element_name_long, check_status, actual_value, required_value, comment, log |
31
+ | **Status Values** | βœ… | Uses only valid statuses: "pass", "fail" |
32
+ | **Helper Functions** | βœ… | `calculate_space_area()` is properly isolated |
33
+ | **Testing** | βœ… | Local test harness included (`if __name__ == "__main__"`) |
34
+
35
+ **Notes:** Function correctly evaluates total dwelling area and applies aggregate pass/fail status to all spaces.
36
+
37
+ ---
38
+
39
+ ### 2. **checker_heights.py** βœ… COMPLIANT
40
+
41
+ | Requirement | Status | Details |
42
+ |---|---|---|
43
+ | **Filename** | βœ… | Follows `checker_*.py` pattern |
44
+ | **Function Name** | βœ… | `check_living_area_height()` follows `check_*` prefix |
45
+ | **Signature** | βœ… | `check_living_area_height(model, min_height=2.50)` - model is 1st arg |
46
+ | **Return Type** | βœ… | Returns `list[dict]` |
47
+ | **Schema Keys** | βœ… | All IFCore keys present |
48
+ | **Status Values** | βœ… | Uses only valid statuses: "pass", "fail" |
49
+ | **Helper Functions** | βœ… | `get_main_living_areas()`, `get_space_height()` properly isolated |
50
+ | **Testing** | βœ… | Local test harness included |
51
+ | **Keywords** | βœ… | Spaces filtered by: "living", "bedroom", "hall" |
52
+
53
+ **Notes:** Old `load_model()` function present but unused (harmless legacy code).
54
+
55
+ ---
56
+
57
+ ### 3. **checker_living_rooms.py** βœ… COMPLIANT
58
+
59
+ | Requirement | Status | Details |
60
+ |---|---|---|
61
+ | **Filename** | βœ… | Follows `checker_*.py` pattern |
62
+ | **Function Name** | βœ… | `check_living_room_compliance()` follows `check_*` prefix |
63
+ | **Signature** | βœ… | `check_living_room_compliance(model)` - model is 1st arg |
64
+ | **Return Type** | βœ… | Returns `list[dict]` |
65
+ | **Schema Keys** | βœ… | All IFCore keys present |
66
+ | **Status Values** | βœ… | Uses only valid statuses: "pass", "fail" |
67
+ | **Rules Encoded** | βœ… | Min area 10 mΒ² (or 14 mΒ² if living+kitchen), clearance check 2.4 x 2.4 m |
68
+ | **Helper Functions** | βœ… | `calculate_space_area()`, `get_space_name()`, `can_fit_square()` properly isolated |
69
+ | **Testing** | βœ… | Local test harness included |
70
+
71
+ **Notes:** Rules correctly cascade and aggregate reasons field. Old `load_model()` function present but unused (harmless legacy code).
72
+
73
+ ---
74
+
75
+ ### 4. **checker_service_spaces.py** βœ… COMPLIANT
76
+
77
+ | Requirement | Status | Details |
78
+ |---|---|---|
79
+ | **Filename** | βœ… | Follows `checker_*.py` pattern |
80
+ | **Function Name** | βœ… | `check_service_spaces_min_height()` follows `check_*` prefix |
81
+ | **Signature** | βœ… | `check_service_spaces_min_height(model, min_height=2.20)` - model is 1st arg |
82
+ | **Return Type** | βœ… | Returns `list[dict]` |
83
+ | **Schema Keys** | βœ… | All IFCore keys present |
84
+ | **Status Values** | βœ… | Uses only valid statuses: "pass", "fail" |
85
+ | **Service Space Keywords** | βœ… | Spaces filtered by: "bath", "bathroom", "wc", "toilet", "kitchen", "cocina", "hall", "hallway", "corridor", "pasillo" |
86
+ | **Helper Functions** | βœ… | `get_space_name()`, `get_space_height()`, `is_service_space()` properly isolated |
87
+ | **Testing** | βœ… | Local test harness included |
88
+
89
+ **Notes:** Old `load_model()` function present but unused (harmless legacy code).
90
+
91
+ ---
92
+
93
+ ### 5. **__init__.py** βœ… COMPLIANT
94
+
95
+ | Requirement | Status | Details |
96
+ |---|---|---|
97
+ | **Exports** | βœ… | All 4 check functions exported: `check_dwelling_area`, `check_living_area_height`, `check_living_room_compliance`, `check_service_spaces_min_height` |
98
+ | **__all__ Declaration** | βœ… | Properly declared for auto-discovery |
99
+ | **Import Paths** | βœ… | Relative imports from checker modules correct |
100
+ | **Documentation** | βœ… | Clear docstring explaining IFCore contract |
101
+
102
+ **Notes:** Properly configured for platform auto-discovery.
103
+
104
+ ---
105
+
106
+ ## IFCore Schema Validation
107
+
108
+ ### Required Fields (All Present in All Checkers)
109
+
110
+ ```python
111
+ {
112
+ "element_id": str, # βœ… GlobalId of IFC element
113
+ "element_type": str, # βœ… "IfcSpace", etc.
114
+ "element_name": str, # βœ… Short name
115
+ "element_name_long": str, # βœ… Descriptive name with context
116
+ "check_status": str, # βœ… "pass", "fail", "warning", "blocked", "log"
117
+ "actual_value": str|None, # βœ… Measured value with units
118
+ "required_value": str|None, # βœ… Required threshold with units
119
+ "comment": str|None, # βœ… Failure explanation or None
120
+ "log": str|None, # βœ… Debug info or None
121
+ }
122
+ ```
123
+
124
+ **Compliance:** βœ… All checkers implement all 9 required fields
125
+
126
+ ---
127
+
128
+ ## Function Signature Validation
129
+
130
+ ### Convention: `check_<what>(model, **optional_kwargs)`
131
+
132
+ ```
133
+ βœ… check_dwelling_area(model, min_area=36.0)
134
+ βœ… check_living_area_height(model, min_height=2.50)
135
+ βœ… check_living_room_compliance(model)
136
+ βœ… check_service_spaces_min_height(model, min_height=2.20)
137
+ ```
138
+
139
+ **Compliance:** βœ… All functions follow contract exactly
140
+
141
+ ---
142
+
143
+ ## Return Type Validation
144
+
145
+ ### Contract: All functions return `list[dict]`
146
+
147
+ ```
148
+ βœ… check_dwelling_area() β†’ list[dict] (269 spaces tested)
149
+ βœ… check_living_area_height() β†’ list[dict] (per living area)
150
+ βœ… check_living_room_compliance() β†’ list[dict] (per living room)
151
+ βœ… check_service_spaces_min_height() β†’ list[dict] (63 service spaces tested)
152
+ ```
153
+
154
+ **Compliance:** βœ… All functions return correct type
155
+
156
+ ---
157
+
158
+ ## File Structure Validation
159
+
160
+ ```
161
+ tools/
162
+ β”œβ”€β”€ __init__.py βœ… Exports all checkers
163
+ β”œβ”€β”€ checker_dwelling.py βœ… check_dwelling_area()
164
+ β”œβ”€β”€ checker_heights.py βœ… check_living_area_height()
165
+ β”œβ”€β”€ checker_living_rooms.py βœ… check_living_room_compliance()
166
+ β”œβ”€β”€ checker_service_spaces.py βœ… check_service_spaces_min_height()
167
+ └── __pycache__/
168
+
169
+ main.py βœ… Integration test harness
170
+ β”œβ”€β”€ Loads model once
171
+ β”œβ”€β”€ Calls all 4 checkers
172
+ └── Collects & summarizes results
173
+ ```
174
+
175
+ **Compliance:** βœ… File structure correct
176
+
177
+ ---
178
+
179
+ ## Platform Integration Readiness
180
+
181
+ ### Discovery Phase βœ…
182
+ - Platform scans `tools/checker_*.py` files
183
+ - Finds all `check_*` functions by name prefix
184
+ - Reads function signature for parameter mapping
185
+
186
+ ### Execution Phase βœ…
187
+ - Platform loads IFC model once
188
+ - Passes loaded `model` object to each `check_*` function
189
+ - Each function receives correct argument type (ifcopenshell.file)
190
+
191
+ ### Result Aggregation Phase βœ…
192
+ - Platform collects `list[dict]` results from each checker
193
+ - Maps to `element_results` database table
194
+ - Schema keys directly match table columns
195
+
196
+ ---
197
+
198
+ ## Removed Files
199
+
200
+ | File | Reason |
201
+ |------|--------|
202
+ | `_init_.py` | ❌ Removed - old file conflicting with `__init__.py` |
203
+
204
+ ---
205
+
206
+ ## Recommendations
207
+
208
+ ### βœ… No Changes Required
209
+
210
+ All files are compliant and production-ready. The codebase:
211
+ - Follows IFCore naming conventions exactly
212
+ - Implements function contracts correctly
213
+ - Returns proper schema
214
+ - Is ready for platform integration
215
+
216
+ ### Optional: Code Cleanup
217
+
218
+ The following unused legacy functions can remain (they don't break functionality):
219
+ - `checker_heights.py`: `load_model()` β€” not used, kept for reference
220
+ - `checker_living_rooms.py`: `load_model()` β€” not used, kept for reference
221
+ - `checker_service_spaces.py`: `load_model()` β€” not used, kept for reference
222
+
223
+ These don't interfere with platform discovery (platform only looks for `check_*` functions).
224
+
225
+ ---
226
+
227
+ ## Test Results Summary
228
+
229
+ ```
230
+ βœ… dwelling_area: 269 βœ“ / 0 βœ—
231
+ βœ… service_spaces_min_height: 63 βœ“ / 0 βœ—
232
+ βœ… living_area_height: 0 results (no matching spaces in model)
233
+ βœ… living_room_compliance: 0 results (no matching spaces in model)
234
+ ```
235
+
236
+ All functions executed without errors and returned correct schema.
237
+
238
+ ---
239
+
240
+ ## Conclusion
241
+
242
+ **Status: βœ… FULLY COMPLIANT WITH ICORE CONTRACTS**
243
+
244
+ Your codebase is ready for deployment to the IFCore platform. All naming conventions, function signatures, return types, and schema requirements are met.
245
+
246
+ **Next step:** Push to your team repository. The platform will auto-discover and integrate your check functions.
teams/Mastodonte/Mastodontes_Tool_E.ipynb ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nbformat": 4,
3
+ "nbformat_minor": 0,
4
+ "metadata": {
5
+ "colab": {
6
+ "provenance": []
7
+ },
8
+ "kernelspec": {
9
+ "name": "python3",
10
+ "display_name": "Python 3"
11
+ },
12
+ "language_info": {
13
+ "name": "python"
14
+ }
15
+ },
16
+ "cells": [
17
+ {
18
+ "cell_type": "markdown",
19
+ "source": [
20
+ "Dependencies"
21
+ ],
22
+ "metadata": {
23
+ "id": "8dq-7nvZdH8R"
24
+ }
25
+ },
26
+ {
27
+ "cell_type": "code",
28
+ "source": [
29
+ "# Install ifcopenshell for IFC parsing\n",
30
+ "!pip install ifcopenshell matplotlib"
31
+ ],
32
+ "metadata": {
33
+ "colab": {
34
+ "base_uri": "https://localhost:8080/"
35
+ },
36
+ "id": "XO7xZxExcr7A",
37
+ "outputId": "7829861f-5134-4946-8830-b925276f0ee1"
38
+ },
39
+ "execution_count": null,
40
+ "outputs": [
41
+ {
42
+ "output_type": "stream",
43
+ "name": "stdout",
44
+ "text": [
45
+ "Collecting ifcopenshell\n",
46
+ " Downloading ifcopenshell-0.8.4.post1-py312-none-manylinux_2_31_x86_64.whl.metadata (12 kB)\n",
47
+ "Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0)\n",
48
+ "Requirement already satisfied: shapely in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (2.1.2)\n",
49
+ "Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (2.0.2)\n",
50
+ "Collecting isodate (from ifcopenshell)\n",
51
+ " Downloading isodate-0.7.2-py3-none-any.whl.metadata (11 kB)\n",
52
+ "Requirement already satisfied: python-dateutil in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (2.9.0.post0)\n",
53
+ "Requirement already satisfied: lark in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (1.3.1)\n",
54
+ "Requirement already satisfied: typing-extensions in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (4.15.0)\n",
55
+ "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)\n",
56
+ "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)\n",
57
+ "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.61.1)\n",
58
+ "Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.4.9)\n",
59
+ "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.0)\n",
60
+ "Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0)\n",
61
+ "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2)\n",
62
+ "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil->ifcopenshell) (1.17.0)\n",
63
+ "Downloading ifcopenshell-0.8.4.post1-py312-none-manylinux_2_31_x86_64.whl (42.6 MB)\n",
64
+ "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m42.6/42.6 MB\u001b[0m \u001b[31m21.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
65
+ "\u001b[?25hDownloading isodate-0.7.2-py3-none-any.whl (22 kB)\n",
66
+ "Installing collected packages: isodate, ifcopenshell\n",
67
+ "Successfully installed ifcopenshell-0.8.4.post1 isodate-0.7.2\n"
68
+ ]
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ "cell_type": "markdown",
74
+ "source": [
75
+ "Cloning Repo"
76
+ ],
77
+ "metadata": {
78
+ "id": "0vgb8bCZdLgH"
79
+ }
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "source": [
84
+ "!git clone https://github.com/sylvainHellin/ifc-bench.git"
85
+ ],
86
+ "metadata": {
87
+ "colab": {
88
+ "base_uri": "https://localhost:8080/"
89
+ },
90
+ "id": "EYiAOI_9dQDU",
91
+ "outputId": "abd0c597-bb0a-40b3-f840-f04f54e3d927"
92
+ },
93
+ "execution_count": null,
94
+ "outputs": [
95
+ {
96
+ "output_type": "stream",
97
+ "name": "stdout",
98
+ "text": [
99
+ "Cloning into 'ifc-bench'...\n",
100
+ "remote: Enumerating objects: 90, done.\u001b[K\n",
101
+ "remote: Counting objects: 100% (12/12), done.\u001b[K\n",
102
+ "remote: Compressing objects: 100% (12/12), done.\u001b[K\n",
103
+ "remote: Total 90 (delta 1), reused 6 (delta 0), pack-reused 78 (from 1)\u001b[K\n",
104
+ "Receiving objects: 100% (90/90), 45.07 MiB | 22.28 MiB/s, done.\n",
105
+ "Resolving deltas: 100% (10/10), done.\n",
106
+ "Filtering content: 100% (5/5), 163.22 MiB | 22.39 MiB/s, done.\n"
107
+ ]
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ "cell_type": "markdown",
113
+ "source": [
114
+ "Tool E Area-Occupancy"
115
+ ],
116
+ "metadata": {
117
+ "id": "lUnjwOstdRKE"
118
+ }
119
+ },
120
+ {
121
+ "cell_type": "code",
122
+ "source": [
123
+ "import ifcopenshell\n",
124
+ "import ifcopenshell.geom\n",
125
+ "import math"
126
+ ],
127
+ "metadata": {
128
+ "id": "d5CtifBBdduf"
129
+ },
130
+ "execution_count": null,
131
+ "outputs": []
132
+ },
133
+ {
134
+ "cell_type": "code",
135
+ "source": [
136
+ "# Requirements\n",
137
+ "import math\n",
138
+ "import json\n",
139
+ "import ifcopenshell\n",
140
+ "import ifcopenshell.geom\n",
141
+ "\n",
142
+ "# --------------------------\n",
143
+ "# Geometry settings\n",
144
+ "# --------------------------\n",
145
+ "settings = ifcopenshell.geom.settings()\n",
146
+ "settings.set(settings.USE_WORLD_COORDS, True)\n",
147
+ "\n",
148
+ "# --------------------------\n",
149
+ "# IFC helpers\n",
150
+ "# --------------------------\n",
151
+ "def calculate_space_area(space):\n",
152
+ " \"\"\"Approximate area by summing triangle areas from the space mesh.\"\"\"\n",
153
+ " try:\n",
154
+ " shape = ifcopenshell.geom.create_shape(settings, space)\n",
155
+ " verts = shape.geometry.verts\n",
156
+ " faces = shape.geometry.faces\n",
157
+ " area = 0.0\n",
158
+ "\n",
159
+ " for i in range(0, len(faces), 3):\n",
160
+ " i0, i1, i2 = faces[i] * 3, faces[i + 1] * 3, faces[i + 2] * 3\n",
161
+ " v0 = verts[i0:i0 + 3]\n",
162
+ " v1 = verts[i1:i1 + 3]\n",
163
+ " v2 = verts[i2:i2 + 3]\n",
164
+ "\n",
165
+ " a = math.sqrt((v1[0]-v0[0])**2 + (v1[1]-v0[1])**2 + (v1[2]-v0[2])**2)\n",
166
+ " b = math.sqrt((v2[0]-v1[0])**2 + (v2[1]-v1[1])**2 + (v2[2]-v1[2])**2)\n",
167
+ " c = math.sqrt((v0[0]-v2[0])**2 + (v0[1]-v2[1])**2 + (v0[2]-v2[2])**2)\n",
168
+ "\n",
169
+ " s = (a + b + c) / 2.0\n",
170
+ " area += math.sqrt(max(s * (s - a) * (s - b) * (s - c), 0.0))\n",
171
+ "\n",
172
+ " return float(area)\n",
173
+ " except Exception:\n",
174
+ " return 0.0\n",
175
+ "\n",
176
+ "\n",
177
+ "def _space_label(space):\n",
178
+ " \"\"\"Return a readable label.\"\"\"\n",
179
+ " return getattr(space, \"LongName\", None) or getattr(space, \"Name\", None) or \"Unnamed\"\n",
180
+ "\n",
181
+ "\n",
182
+ "def _matches_any_keyword(text, keywords):\n",
183
+ " t = (text or \"\").lower()\n",
184
+ " return any(k in t for k in keywords)\n",
185
+ "\n",
186
+ "\n",
187
+ "def get_spaces_by_keywords(model, keywords):\n",
188
+ " \"\"\"Filter IfcSpace by semantic keywords.\"\"\"\n",
189
+ " spaces = model.by_type(\"IfcSpace\")\n",
190
+ " return [s for s in spaces if _matches_any_keyword(_space_label(s), keywords)]\n",
191
+ "\n",
192
+ "\n",
193
+ "# --------------------------\n",
194
+ "# Regulation logic\n",
195
+ "# --------------------------\n",
196
+ "def area_to_occupancy(area):\n",
197
+ " \"\"\"\n",
198
+ " Mapping regulation:\n",
199
+ " <5 mΒ² -> not valid bedroom\n",
200
+ " β‰₯5 mΒ² -> 1 person\n",
201
+ " β‰₯8 mΒ² -> 2 people\n",
202
+ " β‰₯12 mΒ² -> 3 people\n",
203
+ " \"\"\"\n",
204
+ " if area < 5.0:\n",
205
+ " return 0\n",
206
+ " elif area < 8.0:\n",
207
+ " return 1\n",
208
+ " elif area < 12.0:\n",
209
+ " return 2\n",
210
+ " else:\n",
211
+ " return 3\n",
212
+ "\n",
213
+ "\n",
214
+ "def bedroom_occupancy_check(ifc_model_path):\n",
215
+ " \"\"\"\n",
216
+ " Determines allowed occupancy from bedroom areas.\n",
217
+ "\n",
218
+ " Special rule:\n",
219
+ " - If NO bedrooms exist (studio dwelling), occupancy is limited to max 2 people.\n",
220
+ " \"\"\"\n",
221
+ "\n",
222
+ " model = ifcopenshell.open(ifc_model_path)\n",
223
+ "\n",
224
+ " bedroom_keywords = [\"bedroom\", \"habitacion\", \"habitaciΓ³n\", \"dormitorio\"]\n",
225
+ " living_keywords = [\"living\", \"salon\", \"salΓ³n\", \"studio\", \"estudio\", \"common\"]\n",
226
+ "\n",
227
+ " bedrooms = get_spaces_by_keywords(model, bedroom_keywords)\n",
228
+ " living_spaces = get_spaces_by_keywords(model, living_keywords)\n",
229
+ "\n",
230
+ " room_areas = {}\n",
231
+ " occupancy = {}\n",
232
+ " total_people = 0\n",
233
+ "\n",
234
+ " # --------------------------\n",
235
+ " # Studio case (no bedrooms)\n",
236
+ " # --------------------------\n",
237
+ " if len(bedrooms) == 0:\n",
238
+ " if not living_spaces:\n",
239
+ " return {\n",
240
+ " \"result\": \"fail\",\n",
241
+ " \"reason\": \"No habitable space identified\",\n",
242
+ " \"room_areas\": {},\n",
243
+ " \"occupancy\": {},\n",
244
+ " \"total_allowed_people\": 0\n",
245
+ " }\n",
246
+ "\n",
247
+ " main_space = living_spaces[0]\n",
248
+ " label = _space_label(main_space)\n",
249
+ " area = calculate_space_area(main_space)\n",
250
+ "\n",
251
+ " allowed = min(area_to_occupancy(area), 2)\n",
252
+ "\n",
253
+ " room_areas[label] = float(area)\n",
254
+ " occupancy[label] = allowed\n",
255
+ "\n",
256
+ " return {\n",
257
+ " \"result\": \"pass\",\n",
258
+ " \"reason\": \"Studio dwelling limited to maximum 2 occupants\",\n",
259
+ " \"room_areas\": room_areas,\n",
260
+ " \"occupancy\": occupancy,\n",
261
+ " \"total_allowed_people\": allowed\n",
262
+ " }\n",
263
+ "\n",
264
+ " # --------------------------\n",
265
+ " # Bedroom-based occupancy\n",
266
+ " # --------------------------\n",
267
+ " for space in bedrooms:\n",
268
+ " label = _space_label(space)\n",
269
+ " area = calculate_space_area(space)\n",
270
+ " allowed = area_to_occupancy(area)\n",
271
+ "\n",
272
+ " if allowed == 0:\n",
273
+ " return {\n",
274
+ " \"result\": \"fail\",\n",
275
+ " \"reason\": f\"{label} area {area:.2f} mΒ² is below minimum 5 mΒ²\",\n",
276
+ " \"room_areas\": room_areas,\n",
277
+ " \"occupancy\": occupancy,\n",
278
+ " \"total_allowed_people\": total_people\n",
279
+ " }\n",
280
+ "\n",
281
+ " room_areas[label] = float(area)\n",
282
+ " occupancy[label] = allowed\n",
283
+ " total_people += allowed\n",
284
+ "\n",
285
+ " return {\n",
286
+ " \"result\": \"pass\",\n",
287
+ " \"reason\": \"Bedroom areas satisfy occupancy requirements\",\n",
288
+ " \"room_areas\": room_areas,\n",
289
+ " \"occupancy\": occupancy,\n",
290
+ " \"total_allowed_people\": total_people\n",
291
+ " }\n",
292
+ "\n",
293
+ "\n",
294
+ "# --------------------------\n",
295
+ "# Tool entrypoint\n",
296
+ "# --------------------------\n",
297
+ "def bedroom_occupancy_check_tool(ifc_model_path: str):\n",
298
+ " return bedroom_occupancy_check(ifc_model_path)\n",
299
+ "\n",
300
+ "\n",
301
+ "# --------------------------\n",
302
+ "# Schema (LLM-callable description)\n",
303
+ "# --------------------------\n",
304
+ "BEDROOM_OCCUPANCY_CHECK_SCHEMA = {\n",
305
+ " \"name\": \"bedroom_occupancy_check_tool\",\n",
306
+ " \"description\": \"Evaluates bedroom areas in an IFC model to determine the legally allowed number of occupants per room and for the dwelling.\",\n",
307
+ " \"parameters\": {\n",
308
+ " \"type\": \"object\",\n",
309
+ " \"properties\": {\n",
310
+ " \"ifc_model_path\": {\n",
311
+ " \"type\": \"string\",\n",
312
+ " \"description\": \"Filesystem path to the IFC model to evaluate.\"\n",
313
+ " }\n",
314
+ " },\n",
315
+ " \"required\": [\"ifc_model_path\"]\n",
316
+ " }\n",
317
+ "}\n",
318
+ "\n",
319
+ "\n",
320
+ "# --------------------------\n",
321
+ "# Optional: local sanity test\n",
322
+ "# --------------------------\n",
323
+ "if __name__ == \"__main__\":\n",
324
+ " print(\"Schema OK:\")\n",
325
+ " print(json.dumps(BEDROOM_OCCUPANCY_CHECK_SCHEMA, indent=2))\n",
326
+ "\n",
327
+ " ifc_path = \"/content/ifc-bench/projects/duplex/arc.ifc\"\n",
328
+ " result = bedroom_occupancy_check_tool(ifc_path)\n",
329
+ "\n",
330
+ " print(result[\"result\"])\n",
331
+ " print(result[\"reason\"])\n",
332
+ " print(\"Allowed occupants:\", result[\"total_allowed_people\"])"
333
+ ],
334
+ "metadata": {
335
+ "colab": {
336
+ "base_uri": "https://localhost:8080/"
337
+ },
338
+ "id": "LWQ59ic1jaq2",
339
+ "outputId": "89b0e5cc-bae6-4fe3-9bc1-fa174a4c0a9c"
340
+ },
341
+ "execution_count": null,
342
+ "outputs": [
343
+ {
344
+ "output_type": "stream",
345
+ "name": "stdout",
346
+ "text": [
347
+ "Schema OK:\n",
348
+ "{\n",
349
+ " \"name\": \"bedroom_occupancy_check_tool\",\n",
350
+ " \"description\": \"Evaluates bedroom areas in an IFC model to determine the legally allowed number of occupants per room and for the dwelling.\",\n",
351
+ " \"parameters\": {\n",
352
+ " \"type\": \"object\",\n",
353
+ " \"properties\": {\n",
354
+ " \"ifc_model_path\": {\n",
355
+ " \"type\": \"string\",\n",
356
+ " \"description\": \"Filesystem path to the IFC model to evaluate.\"\n",
357
+ " }\n",
358
+ " },\n",
359
+ " \"required\": [\n",
360
+ " \"ifc_model_path\"\n",
361
+ " ]\n",
362
+ " }\n",
363
+ "}\n",
364
+ "pass\n",
365
+ "Bedroom areas satisfy occupancy requirements\n",
366
+ "Allowed occupants: 12\n"
367
+ ]
368
+ }
369
+ ]
370
+ }
371
+ ]
372
+ }
teams/Mastodonte/Mastodontes_Tools.ipynb ADDED
@@ -0,0 +1,857 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 4,
6
+ "metadata": {
7
+ "id": "Sml3OcRVDn34",
8
+ "colab": {
9
+ "base_uri": "https://localhost:8080/"
10
+ },
11
+ "outputId": "98ebd21d-e3d0-42da-8db2-dda12723f747"
12
+ },
13
+ "outputs": [
14
+ {
15
+ "output_type": "stream",
16
+ "name": "stdout",
17
+ "text": [
18
+ "Collecting ifcopenshell\n",
19
+ " Downloading ifcopenshell-0.8.4.post1-py312-none-manylinux_2_31_x86_64.whl.metadata (12 kB)\n",
20
+ "Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0)\n",
21
+ "Requirement already satisfied: shapely in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (2.1.2)\n",
22
+ "Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (2.0.2)\n",
23
+ "Collecting isodate (from ifcopenshell)\n",
24
+ " Downloading isodate-0.7.2-py3-none-any.whl.metadata (11 kB)\n",
25
+ "Requirement already satisfied: python-dateutil in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (2.9.0.post0)\n",
26
+ "Requirement already satisfied: lark in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (1.3.1)\n",
27
+ "Requirement already satisfied: typing-extensions in /usr/local/lib/python3.12/dist-packages (from ifcopenshell) (4.15.0)\n",
28
+ "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)\n",
29
+ "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)\n",
30
+ "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.61.1)\n",
31
+ "Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.4.9)\n",
32
+ "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.0)\n",
33
+ "Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0)\n",
34
+ "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2)\n",
35
+ "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil->ifcopenshell) (1.17.0)\n",
36
+ "Downloading ifcopenshell-0.8.4.post1-py312-none-manylinux_2_31_x86_64.whl (42.6 MB)\n",
37
+ "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m42.6/42.6 MB\u001b[0m \u001b[31m17.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
38
+ "\u001b[?25hDownloading isodate-0.7.2-py3-none-any.whl (22 kB)\n",
39
+ "Installing collected packages: isodate, ifcopenshell\n",
40
+ "Successfully installed ifcopenshell-0.8.4.post1 isodate-0.7.2\n"
41
+ ]
42
+ }
43
+ ],
44
+ "source": [
45
+ "# Install ifcopenshell for IFC parsing\n",
46
+ "!pip install ifcopenshell matplotlib"
47
+ ]
48
+ },
49
+ {
50
+ "cell_type": "markdown",
51
+ "source": [
52
+ "Cloning Repo - Depending on IFC File required"
53
+ ],
54
+ "metadata": {
55
+ "id": "pQdoZ0ZMITx4"
56
+ }
57
+ },
58
+ {
59
+ "cell_type": "code",
60
+ "execution_count": 6,
61
+ "metadata": {
62
+ "id": "LH-AJrKQD3-3",
63
+ "colab": {
64
+ "base_uri": "https://localhost:8080/"
65
+ },
66
+ "outputId": "0b26ee51-3475-4f24-be68-b3a7d71c9559"
67
+ },
68
+ "outputs": [
69
+ {
70
+ "output_type": "stream",
71
+ "name": "stdout",
72
+ "text": [
73
+ "Cloning into 'ifc-bench'...\n",
74
+ "remote: Enumerating objects: 90, done.\u001b[K\n",
75
+ "remote: Counting objects: 100% (12/12), done.\u001b[K\n",
76
+ "remote: Compressing objects: 100% (12/12), done.\u001b[K\n",
77
+ "remote: Total 90 (delta 1), reused 6 (delta 0), pack-reused 78 (from 1)\u001b[K\n",
78
+ "Receiving objects: 100% (90/90), 45.07 MiB | 30.23 MiB/s, done.\n",
79
+ "Resolving deltas: 100% (10/10), done.\n",
80
+ "Filtering content: 100% (5/5), 163.22 MiB | 62.23 MiB/s, done.\n"
81
+ ]
82
+ }
83
+ ],
84
+ "source": [
85
+ "!git clone https://github.com/sylvainHellin/ifc-bench.git"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "markdown",
90
+ "source": [
91
+ "# Tool A (Useful Surface Area)"
92
+ ],
93
+ "metadata": {
94
+ "id": "H5XL0-eub_8E"
95
+ }
96
+ },
97
+ {
98
+ "cell_type": "code",
99
+ "source": [
100
+ "\n",
101
+ "# Requirements\n",
102
+ "import math\n",
103
+ "import json\n",
104
+ "import ifcopenshell\n",
105
+ "import ifcopenshell.geom\n",
106
+ "\n",
107
+ "# --------------------------\n",
108
+ "# Geometry settings\n",
109
+ "# --------------------------\n",
110
+ "settings = ifcopenshell.geom.settings()\n",
111
+ "settings.set(settings.USE_WORLD_COORDS, True)\n",
112
+ "\n",
113
+ "# --------------------------\n",
114
+ "# IFC helpers\n",
115
+ "# --------------------------\n",
116
+ "def calculate_space_area(space):\n",
117
+ " \"\"\"Approximate area by summing triangle areas from the space mesh.\"\"\"\n",
118
+ " try:\n",
119
+ " shape = ifcopenshell.geom.create_shape(settings, space)\n",
120
+ " verts = shape.geometry.verts\n",
121
+ " faces = shape.geometry.faces\n",
122
+ " area = 0.0\n",
123
+ "\n",
124
+ " for i in range(0, len(faces), 3):\n",
125
+ " i0, i1, i2 = faces[i] * 3, faces[i + 1] * 3, faces[i + 2] * 3\n",
126
+ " v0 = verts[i0:i0 + 3]\n",
127
+ " v1 = verts[i1:i1 + 3]\n",
128
+ " v2 = verts[i2:i2 + 3]\n",
129
+ "\n",
130
+ " a = math.sqrt((v1[0]-v0[0])**2 + (v1[1]-v0[1])**2 + (v1[2]-v0[2])**2)\n",
131
+ " b = math.sqrt((v2[0]-v1[0])**2 + (v2[1]-v1[1])**2 + (v2[2]-v1[2])**2)\n",
132
+ " c = math.sqrt((v0[0]-v2[0])**2 + (v0[1]-v2[1])**2 + (v0[2]-v2[2])**2)\n",
133
+ "\n",
134
+ " s = (a + b + c) / 2.0\n",
135
+ " area += math.sqrt(max(s * (s - a) * (s - b) * (s - c), 0.0))\n",
136
+ "\n",
137
+ " return float(area)\n",
138
+ " except Exception:\n",
139
+ " return 0.0\n",
140
+ "\n",
141
+ "def dwelling_area_check(ifc_model_path, min_area=36.0):\n",
142
+ " \"\"\"Check if total dwelling useful area >= min_area.\"\"\"\n",
143
+ " model = ifcopenshell.open(ifc_model_path)\n",
144
+ " spaces = model.by_type(\"IfcSpace\")\n",
145
+ "\n",
146
+ " total_area = 0.0\n",
147
+ " room_areas = {}\n",
148
+ "\n",
149
+ " for space in spaces:\n",
150
+ " area = calculate_space_area(space)\n",
151
+ " room_name = getattr(space, \"LongName\", None) or getattr(space, \"Name\", None) or \"Unnamed\"\n",
152
+ " room_areas[str(room_name)] = float(area)\n",
153
+ " total_area += float(area)\n",
154
+ "\n",
155
+ " if total_area >= float(min_area):\n",
156
+ " return {\n",
157
+ " \"result\": \"pass\",\n",
158
+ " \"reason\": f\"Total useful area {total_area:.2f} mΒ² >= {float(min_area):.2f} mΒ²\",\n",
159
+ " \"total_area\": float(total_area),\n",
160
+ " \"room_areas\": room_areas,\n",
161
+ " }\n",
162
+ "\n",
163
+ " return {\n",
164
+ " \"result\": \"fail\",\n",
165
+ " \"reason\": f\"Total useful area {total_area:.2f} mΒ² < {float(min_area):.2f} mΒ²\",\n",
166
+ " \"total_area\": float(total_area),\n",
167
+ " \"room_areas\": room_areas,\n",
168
+ " }\n",
169
+ "\n",
170
+ "# --------------------------\n",
171
+ "# Tool entrypoint (what your LLM router will call later)\n",
172
+ "# --------------------------\n",
173
+ "def dwelling_area_check_tool(ifc_model_path: str, min_area: float = 36.0):\n",
174
+ " return dwelling_area_check(ifc_model_path, min_area)\n",
175
+ "\n",
176
+ "# --------------------------\n",
177
+ "# Schema (API key not needed)\n",
178
+ "# Plain JSON-schema-like dict, easy to serialize + pass to any LLM framework\n",
179
+ "# --------------------------\n",
180
+ "DWELLING_AREA_CHECK_SCHEMA = {\n",
181
+ " \"name\": \"dwelling_area_check_tool\",\n",
182
+ " \"description\": \"Checks whether an IFC dwelling meets a minimum total useful area requirement and returns a room-by-room breakdown.\",\n",
183
+ " \"parameters\": {\n",
184
+ " \"type\": \"object\",\n",
185
+ " \"properties\": {\n",
186
+ " \"ifc_model_path\": {\n",
187
+ " \"type\": \"string\",\n",
188
+ " \"description\": \"Filesystem path to the IFC model.\"\n",
189
+ " },\n",
190
+ " \"min_area\": {\n",
191
+ " \"type\": \"number\",\n",
192
+ " \"description\": \"Minimum total area in mΒ². Default is 36.\"\n",
193
+ " }\n",
194
+ " },\n",
195
+ " \"required\": [\"ifc_model_path\"]\n",
196
+ " }\n",
197
+ "}\n",
198
+ "\n",
199
+ "# --------------------------\n",
200
+ "# Optional: local sanity test (no API usage)\n",
201
+ "# --------------------------\n",
202
+ "if __name__ == \"__main__\":\n",
203
+ " print(\"Schema OK:\")\n",
204
+ " print(json.dumps(DWELLING_AREA_CHECK_SCHEMA, indent=2))\n",
205
+ "\n",
206
+ " ifc_path = \"/content/ifc-bench/projects/duplex/arc.ifc\"\n",
207
+ " result = dwelling_area_check_tool(ifc_path, min_area=36.0)\n",
208
+ " print(result[\"result\"])\n",
209
+ " print(result[\"reason\"])\n"
210
+ ],
211
+ "metadata": {
212
+ "colab": {
213
+ "base_uri": "https://localhost:8080/"
214
+ },
215
+ "id": "kLyNP9RDa9bN",
216
+ "outputId": "f8d9ef19-977d-4ac8-a547-fd5fed1ac5b2"
217
+ },
218
+ "execution_count": null,
219
+ "outputs": [
220
+ {
221
+ "output_type": "stream",
222
+ "name": "stdout",
223
+ "text": [
224
+ "Schema OK:\n",
225
+ "{\n",
226
+ " \"name\": \"dwelling_area_check_tool\",\n",
227
+ " \"description\": \"Checks whether an IFC dwelling meets a minimum total useful area requirement and returns a room-by-room breakdown.\",\n",
228
+ " \"parameters\": {\n",
229
+ " \"type\": \"object\",\n",
230
+ " \"properties\": {\n",
231
+ " \"ifc_model_path\": {\n",
232
+ " \"type\": \"string\",\n",
233
+ " \"description\": \"Filesystem path to the IFC model.\"\n",
234
+ " },\n",
235
+ " \"min_area\": {\n",
236
+ " \"type\": \"number\",\n",
237
+ " \"description\": \"Minimum total area in m\\u00b2. Default is 36.\"\n",
238
+ " }\n",
239
+ " },\n",
240
+ " \"required\": [\n",
241
+ " \"ifc_model_path\"\n",
242
+ " ]\n",
243
+ " }\n",
244
+ "}\n",
245
+ "pass\n",
246
+ "Total useful area 1711.01 mΒ² >= 36.00 mΒ²\n"
247
+ ]
248
+ }
249
+ ]
250
+ },
251
+ {
252
+ "cell_type": "markdown",
253
+ "metadata": {
254
+ "id": "w65dIpWxJpmH"
255
+ },
256
+ "source": [
257
+ "# TOOL B - Living room Height Regulation"
258
+ ]
259
+ },
260
+ {
261
+ "cell_type": "code",
262
+ "source": [
263
+ "import json\n",
264
+ "import ifcopenshell\n",
265
+ "import ifcopenshell.geom\n",
266
+ "\n",
267
+ "# --------------------------\n",
268
+ "# Geometry settings\n",
269
+ "# --------------------------\n",
270
+ "settings = ifcopenshell.geom.settings()\n",
271
+ "settings.set(settings.USE_WORLD_COORDS, True)\n",
272
+ "\n",
273
+ "# --------------------------\n",
274
+ "# Helper functions\n",
275
+ "# --------------------------\n",
276
+ "def load_model(ifc_model_path):\n",
277
+ " \"\"\"Load IFC model\"\"\"\n",
278
+ " return ifcopenshell.open(ifc_model_path)\n",
279
+ "\n",
280
+ "def get_main_living_areas(ifc_model):\n",
281
+ " \"\"\"\n",
282
+ " Return spaces considered main living areas.\n",
283
+ " Filtering by common keywords in space name.\n",
284
+ " \"\"\"\n",
285
+ " spaces = ifc_model.by_type(\"IfcSpace\")\n",
286
+ " main_spaces = []\n",
287
+ " for s in spaces:\n",
288
+ " if s.Name and any(keyword in s.Name.lower() for keyword in [\"living\", \"bedroom\", \"hall\"]):\n",
289
+ " main_spaces.append(s)\n",
290
+ " return main_spaces\n",
291
+ "\n",
292
+ "def get_space_height(space):\n",
293
+ " \"\"\"\n",
294
+ " Approximate height of a space from geometry bounding box.\n",
295
+ " Uses Z coordinates of all vertices.\n",
296
+ " \"\"\"\n",
297
+ " try:\n",
298
+ " shape = ifcopenshell.geom.create_shape(settings, space)\n",
299
+ " verts = shape.geometry.verts\n",
300
+ " if not verts:\n",
301
+ " return 0.0\n",
302
+ " zs = verts[2::3]\n",
303
+ " return float(max(zs) - min(zs))\n",
304
+ " except Exception:\n",
305
+ " return 0.0\n",
306
+ "\n",
307
+ "# --------------------------\n",
308
+ "# Main check function\n",
309
+ "# --------------------------\n",
310
+ "def living_area_height_check(ifc_model_path, min_height=2.50):\n",
311
+ " \"\"\"\n",
312
+ " Check if all main living areas meet the minimum height requirement.\n",
313
+ "\n",
314
+ " Returns a dictionary with:\n",
315
+ " - result: 'pass' or 'fail'\n",
316
+ " - reason: explanation string\n",
317
+ " - room_heights: dictionary of room_name -> calculated height\n",
318
+ " \"\"\"\n",
319
+ " model = load_model(ifc_model_path)\n",
320
+ " spaces = get_main_living_areas(model)\n",
321
+ "\n",
322
+ " room_heights = {}\n",
323
+ " for s in spaces:\n",
324
+ " height = get_space_height(s)\n",
325
+ " room_name = s.Name or \"Unnamed\"\n",
326
+ " room_heights[room_name] = height\n",
327
+ " if height < min_height:\n",
328
+ " return {\n",
329
+ " \"result\": \"fail\",\n",
330
+ " \"reason\": f\"{room_name} height below {min_height}m\",\n",
331
+ " \"room_heights\": room_heights\n",
332
+ " }\n",
333
+ "\n",
334
+ " return {\n",
335
+ " \"result\": \"pass\",\n",
336
+ " \"reason\": f\"All main living areas meet minimum height {min_height}m\",\n",
337
+ " \"room_heights\": room_heights\n",
338
+ " }\n",
339
+ "\n",
340
+ "# --------------------------\n",
341
+ "# Tool entrypoint (for an LLM router later)\n",
342
+ "# --------------------------\n",
343
+ "def living_area_height_check_tool(ifc_model_path: str, min_height: float = 2.50):\n",
344
+ " return living_area_height_check(ifc_model_path, min_height)\n",
345
+ "\n",
346
+ "# --------------------------\n",
347
+ "# Schema (no API key needed)\n",
348
+ "# --------------------------\n",
349
+ "LIVING_AREA_HEIGHT_CHECK_SCHEMA = {\n",
350
+ " \"name\": \"living_area_height_check_tool\",\n",
351
+ " \"description\": \"Checks whether main living areas in an IFC meet a minimum height requirement and returns a room-by-room height breakdown.\",\n",
352
+ " \"parameters\": {\n",
353
+ " \"type\": \"object\",\n",
354
+ " \"properties\": {\n",
355
+ " \"ifc_model_path\": {\n",
356
+ " \"type\": \"string\",\n",
357
+ " \"description\": \"Filesystem path to the IFC model.\"\n",
358
+ " },\n",
359
+ " \"min_height\": {\n",
360
+ " \"type\": \"number\",\n",
361
+ " \"description\": \"Minimum height in meters. Default is 2.50.\"\n",
362
+ " }\n",
363
+ " },\n",
364
+ " \"required\": [\"ifc_model_path\"]\n",
365
+ " }\n",
366
+ "}\n",
367
+ "\n",
368
+ "# --------------------------\n",
369
+ "# Usage example (prints schema + pass/fail)\n",
370
+ "# --------------------------\n",
371
+ "if __name__ == \"__main__\":\n",
372
+ " print(\"Schema OK:\")\n",
373
+ " print(json.dumps(LIVING_AREA_HEIGHT_CHECK_SCHEMA, indent=2))\n",
374
+ "\n",
375
+ " ifc_path = \"/content/ifc-bench/projects/duplex/arc.ifc\" # change if needed\n",
376
+ " check = living_area_height_check_tool(ifc_path, min_height=2.50)\n",
377
+ "\n",
378
+ " print(\"\\nCheck result:\")\n",
379
+ " print(check[\"result\"])\n",
380
+ " print(check[\"reason\"])\n",
381
+ " print(\"Room heights:\", check[\"room_heights\"])\n"
382
+ ],
383
+ "metadata": {
384
+ "colab": {
385
+ "base_uri": "https://localhost:8080/"
386
+ },
387
+ "id": "2ZM-IGvFfMDj",
388
+ "outputId": "57d50092-fece-4522-fa79-ff940129a895"
389
+ },
390
+ "execution_count": 7,
391
+ "outputs": [
392
+ {
393
+ "output_type": "stream",
394
+ "name": "stdout",
395
+ "text": [
396
+ "Schema OK:\n",
397
+ "{\n",
398
+ " \"name\": \"living_area_height_check_tool\",\n",
399
+ " \"description\": \"Checks whether main living areas in an IFC meet a minimum height requirement and returns a room-by-room height breakdown.\",\n",
400
+ " \"parameters\": {\n",
401
+ " \"type\": \"object\",\n",
402
+ " \"properties\": {\n",
403
+ " \"ifc_model_path\": {\n",
404
+ " \"type\": \"string\",\n",
405
+ " \"description\": \"Filesystem path to the IFC model.\"\n",
406
+ " },\n",
407
+ " \"min_height\": {\n",
408
+ " \"type\": \"number\",\n",
409
+ " \"description\": \"Minimum height in meters. Default is 2.50.\"\n",
410
+ " }\n",
411
+ " },\n",
412
+ " \"required\": [\n",
413
+ " \"ifc_model_path\"\n",
414
+ " ]\n",
415
+ " }\n",
416
+ "}\n",
417
+ "\n",
418
+ "Check result:\n",
419
+ "pass\n",
420
+ "All main living areas meet minimum height 2.5m\n",
421
+ "Room heights: {}\n"
422
+ ]
423
+ }
424
+ ]
425
+ },
426
+ {
427
+ "cell_type": "markdown",
428
+ "metadata": {
429
+ "id": "1WSJIjCPKUk2"
430
+ },
431
+ "source": [
432
+ "# TOOL C - Living Room Area Check"
433
+ ]
434
+ },
435
+ {
436
+ "cell_type": "code",
437
+ "source": [
438
+ "import json\n",
439
+ "import math\n",
440
+ "import ifcopenshell\n",
441
+ "import ifcopenshell.geom\n",
442
+ "\n",
443
+ "# Optional: only needed if you truly have it available\n",
444
+ "# from ifcopenshell.util.element import get_psets as _get_psets\n",
445
+ "\n",
446
+ "# --------------------------\n",
447
+ "# Geometry settings\n",
448
+ "# --------------------------\n",
449
+ "settings = ifcopenshell.geom.settings()\n",
450
+ "settings.set(settings.USE_WORLD_COORDS, True)\n",
451
+ "\n",
452
+ "# --------------------------\n",
453
+ "# Helper functions\n",
454
+ "# --------------------------\n",
455
+ "def load_model(ifc_model_path):\n",
456
+ " return ifcopenshell.open(ifc_model_path)\n",
457
+ "\n",
458
+ "def calculate_space_area(space):\n",
459
+ " \"\"\"\n",
460
+ " Approximate area from mesh triangles.\n",
461
+ " Note: this is mesh surface area, not guaranteed to be floor area.\n",
462
+ " \"\"\"\n",
463
+ " if hasattr(space, \"NetFloorArea\") and space.NetFloorArea:\n",
464
+ " return float(space.NetFloorArea)\n",
465
+ "\n",
466
+ " try:\n",
467
+ " shape = ifcopenshell.geom.create_shape(settings, space)\n",
468
+ " verts = shape.geometry.verts\n",
469
+ " faces = shape.geometry.faces\n",
470
+ " area = 0.0\n",
471
+ "\n",
472
+ " for i in range(0, len(faces), 3):\n",
473
+ " idx0, idx1, idx2 = faces[i] * 3, faces[i + 1] * 3, faces[i + 2] * 3\n",
474
+ " v0 = verts[idx0:idx0 + 3]\n",
475
+ " v1 = verts[idx1:idx1 + 3]\n",
476
+ " v2 = verts[idx2:idx2 + 3]\n",
477
+ "\n",
478
+ " a = math.sqrt((v1[0]-v0[0])**2 + (v1[1]-v0[1])**2 + (v1[2]-v0[2])**2)\n",
479
+ " b = math.sqrt((v2[0]-v1[0])**2 + (v2[1]-v1[1])**2 + (v2[2]-v1[2])**2)\n",
480
+ " c = math.sqrt((v0[0]-v2[0])**2 + (v0[1]-v2[1])**2 + (v0[2]-v2[2])**2)\n",
481
+ "\n",
482
+ " s = (a + b + c) / 2.0\n",
483
+ " area += math.sqrt(max(s * (s - a) * (s - b) * (s - c), 0.0))\n",
484
+ "\n",
485
+ " return float(area)\n",
486
+ " except Exception:\n",
487
+ " return 0.0\n",
488
+ "\n",
489
+ "def get_space_name(space):\n",
490
+ " \"\"\"Get descriptive name if available, fallback to LongName/Name/GlobalId.\"\"\"\n",
491
+ " if getattr(space, \"LongName\", None):\n",
492
+ " return str(space.LongName)\n",
493
+ " if getattr(space, \"Name\", None):\n",
494
+ " return str(space.Name)\n",
495
+ "\n",
496
+ " # If you have ifcopenshell.util.element available, you can enable this block:\n",
497
+ " # try:\n",
498
+ " # psets = _get_psets(space)\n",
499
+ " # for props in (psets or {}).values():\n",
500
+ " # for k, v in (props or {}).items():\n",
501
+ " # if \"name\" in str(k).lower() and isinstance(v, str) and v.strip():\n",
502
+ " # return v.strip()\n",
503
+ " # except Exception:\n",
504
+ " # pass\n",
505
+ "\n",
506
+ " return str(getattr(space, \"GlobalId\", \"Unnamed\"))\n",
507
+ "\n",
508
+ "def can_fit_square(area_m2, width=2.4, depth=2.4):\n",
509
+ " \"\"\"Approx check: area must allow a width x depth square.\"\"\"\n",
510
+ " return float(area_m2) >= float(width) * float(depth)\n",
511
+ "\n",
512
+ "# --------------------------\n",
513
+ "# Living room compliance\n",
514
+ "# --------------------------\n",
515
+ "def living_room_compliance(ifc_model_path):\n",
516
+ " \"\"\"\n",
517
+ " Rule encoded:\n",
518
+ " - Living room min area 10 mΒ²\n",
519
+ " - If living+kitchen combined: min area 14 mΒ²\n",
520
+ " - Must allow 2.40 x 2.40 m clearance (approx via area check)\n",
521
+ " Returns list of per-space results.\n",
522
+ " \"\"\"\n",
523
+ " model = load_model(ifc_model_path)\n",
524
+ " spaces = model.by_type(\"IfcSpace\")\n",
525
+ "\n",
526
+ " results = []\n",
527
+ "\n",
528
+ " for space in spaces:\n",
529
+ " raw_name = get_space_name(space)\n",
530
+ " name = raw_name.lower()\n",
531
+ " area = calculate_space_area(space)\n",
532
+ " if area <= 0:\n",
533
+ " continue\n",
534
+ "\n",
535
+ " if \"living\" in name:\n",
536
+ " has_kitchen = \"kitchen\" in name\n",
537
+ " min_area = 14.0 if has_kitchen else 10.0\n",
538
+ "\n",
539
+ " clearance_ok = can_fit_square(area, 2.4, 2.4)\n",
540
+ " passed = (area >= min_area) and clearance_ok\n",
541
+ "\n",
542
+ " reasons = []\n",
543
+ " if area < min_area:\n",
544
+ " reasons.append(f\"Area {area:.2f} mΒ² < required {min_area:.2f} mΒ²\")\n",
545
+ " if not clearance_ok:\n",
546
+ " reasons.append(\"Does not allow 2.40 m x 2.40 m square (approx)\")\n",
547
+ "\n",
548
+ " results.append({\n",
549
+ " \"space_name\": raw_name,\n",
550
+ " \"area_m2\": float(area),\n",
551
+ " \"min_required_m2\": float(min_area),\n",
552
+ " \"clearance_ok\": bool(clearance_ok),\n",
553
+ " \"result\": \"pass\" if passed else \"fail\",\n",
554
+ " \"reason\": \"; \".join(reasons) if reasons else \"Complies\"\n",
555
+ " })\n",
556
+ "\n",
557
+ " return results\n",
558
+ "\n",
559
+ "# --------------------------\n",
560
+ "# Tool entrypoint (for an LLM router later)\n",
561
+ "# --------------------------\n",
562
+ "def living_room_compliance_tool(ifc_model_path: str):\n",
563
+ " return living_room_compliance(ifc_model_path)\n",
564
+ "\n",
565
+ "# --------------------------\n",
566
+ "# Schema (no API key needed)\n",
567
+ "# --------------------------\n",
568
+ "LIVING_ROOM_COMPLIANCE_SCHEMA = {\n",
569
+ " \"name\": \"living_room_compliance_tool\",\n",
570
+ " \"description\": \"Checks living room spaces in an IFC for minimum area and clearance rules and returns per-space compliance results.\",\n",
571
+ " \"parameters\": {\n",
572
+ " \"type\": \"object\",\n",
573
+ " \"properties\": {\n",
574
+ " \"ifc_model_path\": {\n",
575
+ " \"type\": \"string\",\n",
576
+ " \"description\": \"Filesystem path to the IFC model.\"\n",
577
+ " }\n",
578
+ " },\n",
579
+ " \"required\": [\"ifc_model_path\"]\n",
580
+ " }\n",
581
+ "}\n",
582
+ "\n",
583
+ "# --------------------------\n",
584
+ "# Usage example (prints schema + report)\n",
585
+ "# --------------------------\n",
586
+ "if __name__ == \"__main__\":\n",
587
+ " print(\"Schema OK:\")\n",
588
+ " print(json.dumps(LIVING_ROOM_COMPLIANCE_SCHEMA, indent=2))\n",
589
+ "\n",
590
+ " ifc_path = \"/content/ifc-bench/projects/duplex/arc.ifc\" # change if needed\n",
591
+ " report = living_room_compliance_tool(ifc_path)\n",
592
+ "\n",
593
+ " print(\"\\nReport:\")\n",
594
+ " if not report:\n",
595
+ " print(\"No living room spaces matched (keyword 'living') or no computable areas.\")\n",
596
+ " else:\n",
597
+ " for r in report:\n",
598
+ " print(f\"{r['space_name']}: {r['area_m2']:.2f} mΒ² (min {r['min_required_m2']:.2f} mΒ²) β†’ {r['result']}\")\n",
599
+ " if r[\"result\"] == \"fail\":\n",
600
+ " print(f\" Reason: {r['reason']}\")"
601
+ ],
602
+ "metadata": {
603
+ "colab": {
604
+ "base_uri": "https://localhost:8080/"
605
+ },
606
+ "id": "VdNVP6ZxlSMv",
607
+ "outputId": "1199768c-fb27-4aa6-f63f-6f0be8259a5d"
608
+ },
609
+ "execution_count": 8,
610
+ "outputs": [
611
+ {
612
+ "output_type": "stream",
613
+ "name": "stdout",
614
+ "text": [
615
+ "Schema OK:\n",
616
+ "{\n",
617
+ " \"name\": \"living_room_compliance_tool\",\n",
618
+ " \"description\": \"Checks living room spaces in an IFC for minimum area and clearance rules and returns per-space compliance results.\",\n",
619
+ " \"parameters\": {\n",
620
+ " \"type\": \"object\",\n",
621
+ " \"properties\": {\n",
622
+ " \"ifc_model_path\": {\n",
623
+ " \"type\": \"string\",\n",
624
+ " \"description\": \"Filesystem path to the IFC model.\"\n",
625
+ " }\n",
626
+ " },\n",
627
+ " \"required\": [\n",
628
+ " \"ifc_model_path\"\n",
629
+ " ]\n",
630
+ " }\n",
631
+ "}\n",
632
+ "\n",
633
+ "Report:\n",
634
+ "Living Room: 109.86 mΒ² (min 10.00 mΒ²) β†’ pass\n",
635
+ "Living Room: 109.86 mΒ² (min 10.00 mΒ²) β†’ pass\n"
636
+ ]
637
+ }
638
+ ]
639
+ },
640
+ {
641
+ "cell_type": "markdown",
642
+ "source": [
643
+ "# Tool D Service Area Heights"
644
+ ],
645
+ "metadata": {
646
+ "id": "4aFj5ZiBlYYm"
647
+ }
648
+ },
649
+ {
650
+ "cell_type": "code",
651
+ "source": [
652
+ "import json\n",
653
+ "import ifcopenshell\n",
654
+ "import ifcopenshell.geom\n",
655
+ "\n",
656
+ "# --------------------------\n",
657
+ "# Geometry settings\n",
658
+ "# --------------------------\n",
659
+ "settings = ifcopenshell.geom.settings()\n",
660
+ "settings.set(settings.USE_WORLD_COORDS, True)\n",
661
+ "\n",
662
+ "# --------------------------\n",
663
+ "# Helper functions\n",
664
+ "# --------------------------\n",
665
+ "def load_model(ifc_model_path):\n",
666
+ " return ifcopenshell.open(ifc_model_path)\n",
667
+ "\n",
668
+ "def get_space_name(space):\n",
669
+ " \"\"\"Get descriptive name if available, fallback to LongName/Name/GlobalId.\"\"\"\n",
670
+ " if getattr(space, \"LongName\", None):\n",
671
+ " return str(space.LongName)\n",
672
+ " if getattr(space, \"Name\", None):\n",
673
+ " return str(space.Name)\n",
674
+ " return str(getattr(space, \"GlobalId\", \"Unnamed\"))\n",
675
+ "\n",
676
+ "def get_space_height(space):\n",
677
+ " \"\"\"\n",
678
+ " Approximate height of a space from geometry bounding box.\n",
679
+ " Uses Z coordinates of all vertices.\n",
680
+ " \"\"\"\n",
681
+ " try:\n",
682
+ " shape = ifcopenshell.geom.create_shape(settings, space)\n",
683
+ " verts = shape.geometry.verts\n",
684
+ " if not verts:\n",
685
+ " return 0.0\n",
686
+ " zs = verts[2::3]\n",
687
+ " return float(max(zs) - min(zs))\n",
688
+ " except Exception:\n",
689
+ " return 0.0\n",
690
+ "\n",
691
+ "def is_service_space(space_name_lower):\n",
692
+ " \"\"\"Bathrooms, kitchens, hallways by keywords.\"\"\"\n",
693
+ " service_keywords = [\n",
694
+ " \"bath\", \"bathroom\", \"baΓ±o\", \"bano\", \"wc\", \"toilet\",\n",
695
+ " \"kitchen\", \"cocina\",\n",
696
+ " \"hall\", \"hallway\", \"corridor\", \"pasillo\"\n",
697
+ " ]\n",
698
+ " return any(k in space_name_lower for k in service_keywords)\n",
699
+ "\n",
700
+ "# --------------------------\n",
701
+ "# Compliance check\n",
702
+ "# --------------------------\n",
703
+ "def service_spaces_min_height_check(ifc_model_path, min_height=2.20):\n",
704
+ " \"\"\"\n",
705
+ " Regulation:\n",
706
+ " - Minimum height in bathrooms, kitchens, and hallways is 2.20 m\n",
707
+ "\n",
708
+ " Returns:\n",
709
+ " - result: 'pass' or 'fail'\n",
710
+ " - reason\n",
711
+ " - room_heights: dict label -> height\n",
712
+ " - checked_spaces: list of labels that were evaluated\n",
713
+ " \"\"\"\n",
714
+ " model = load_model(ifc_model_path)\n",
715
+ " spaces = model.by_type(\"IfcSpace\")\n",
716
+ "\n",
717
+ " room_heights = {}\n",
718
+ " checked_spaces = []\n",
719
+ "\n",
720
+ " for space in spaces:\n",
721
+ " label = get_space_name(space)\n",
722
+ " label_l = label.lower()\n",
723
+ "\n",
724
+ " if not is_service_space(label_l):\n",
725
+ " continue\n",
726
+ "\n",
727
+ " checked_spaces.append(label)\n",
728
+ " h = get_space_height(space)\n",
729
+ " room_heights[label] = h\n",
730
+ "\n",
731
+ " if h < float(min_height):\n",
732
+ " return {\n",
733
+ " \"result\": \"fail\",\n",
734
+ " \"reason\": f\"{label} height below {float(min_height):.2f}m\",\n",
735
+ " \"room_heights\": room_heights,\n",
736
+ " \"checked_spaces\": checked_spaces\n",
737
+ " }\n",
738
+ "\n",
739
+ " if not checked_spaces:\n",
740
+ " return {\n",
741
+ " \"result\": \"fail\",\n",
742
+ " \"reason\": \"No bathrooms/kitchens/hallways matched by keywords, nothing checked\",\n",
743
+ " \"room_heights\": room_heights,\n",
744
+ " \"checked_spaces\": checked_spaces\n",
745
+ " }\n",
746
+ "\n",
747
+ " return {\n",
748
+ " \"result\": \"pass\",\n",
749
+ " \"reason\": f\"All checked bathrooms/kitchens/hallways meet minimum height {float(min_height):.2f}m\",\n",
750
+ " \"room_heights\": room_heights,\n",
751
+ " \"checked_spaces\": checked_spaces\n",
752
+ " }\n",
753
+ "\n",
754
+ "# --------------------------\n",
755
+ "# Tool entrypoint (for an LLM router later)\n",
756
+ "# --------------------------\n",
757
+ "def service_spaces_min_height_check_tool(ifc_model_path: str, min_height: float = 2.20):\n",
758
+ " return service_spaces_min_height_check(ifc_model_path, min_height)\n",
759
+ "\n",
760
+ "# --------------------------\n",
761
+ "# Schema (no API key needed)\n",
762
+ "# --------------------------\n",
763
+ "SERVICE_SPACES_MIN_HEIGHT_SCHEMA = {\n",
764
+ " \"name\": \"service_spaces_min_height_check_tool\",\n",
765
+ " \"description\": \"Checks bathrooms, kitchens, and hallways in an IFC for a minimum height requirement (default 2.20m).\",\n",
766
+ " \"parameters\": {\n",
767
+ " \"type\": \"object\",\n",
768
+ " \"properties\": {\n",
769
+ " \"ifc_model_path\": {\n",
770
+ " \"type\": \"string\",\n",
771
+ " \"description\": \"Filesystem path to the IFC model.\"\n",
772
+ " },\n",
773
+ " \"min_height\": {\n",
774
+ " \"type\": \"number\",\n",
775
+ " \"description\": \"Minimum height in meters. Default is 2.20.\"\n",
776
+ " }\n",
777
+ " },\n",
778
+ " \"required\": [\"ifc_model_path\"]\n",
779
+ " }\n",
780
+ "}\n",
781
+ "\n",
782
+ "# --------------------------\n",
783
+ "# Usage example (prints schema + pass/fail)\n",
784
+ "# --------------------------\n",
785
+ "if __name__ == \"__main__\":\n",
786
+ " print(\"Schema OK:\")\n",
787
+ " print(json.dumps(SERVICE_SPACES_MIN_HEIGHT_SCHEMA, indent=2))\n",
788
+ "\n",
789
+ " ifc_path = \"/content/ifc-bench/projects/duplex/arc.ifc\" # change if needed\n",
790
+ " check = service_spaces_min_height_check_tool(ifc_path, min_height=2.20)\n",
791
+ "\n",
792
+ " print(\"\\nCheck result:\")\n",
793
+ " print(check[\"result\"])\n",
794
+ " print(check[\"reason\"])\n",
795
+ " print(\"Room heights:\", check[\"room_heights\"])\n",
796
+ " print(\"Checked spaces:\", check[\"checked_spaces\"])"
797
+ ],
798
+ "metadata": {
799
+ "colab": {
800
+ "base_uri": "https://localhost:8080/"
801
+ },
802
+ "id": "IWZ7ISP3mYg4",
803
+ "outputId": "aeaa8d7f-6b14-4339-984b-6da478889046"
804
+ },
805
+ "execution_count": 9,
806
+ "outputs": [
807
+ {
808
+ "output_type": "stream",
809
+ "name": "stdout",
810
+ "text": [
811
+ "Schema OK:\n",
812
+ "{\n",
813
+ " \"name\": \"service_spaces_min_height_check_tool\",\n",
814
+ " \"description\": \"Checks bathrooms, kitchens, and hallways in an IFC for a minimum height requirement (default 2.20m).\",\n",
815
+ " \"parameters\": {\n",
816
+ " \"type\": \"object\",\n",
817
+ " \"properties\": {\n",
818
+ " \"ifc_model_path\": {\n",
819
+ " \"type\": \"string\",\n",
820
+ " \"description\": \"Filesystem path to the IFC model.\"\n",
821
+ " },\n",
822
+ " \"min_height\": {\n",
823
+ " \"type\": \"number\",\n",
824
+ " \"description\": \"Minimum height in meters. Default is 2.20.\"\n",
825
+ " }\n",
826
+ " },\n",
827
+ " \"required\": [\n",
828
+ " \"ifc_model_path\"\n",
829
+ " ]\n",
830
+ " }\n",
831
+ "}\n",
832
+ "\n",
833
+ "Check result:\n",
834
+ "pass\n",
835
+ "All checked bathrooms/kitchens/hallways meet minimum height 2.20m\n",
836
+ "Room heights: {'Hallway': 2.8810000000001947, 'Bathroom 2': 2.5870000000001836, 'Bathroom 1': 2.587000000000001, 'Kitchen': 2.587000000000001}\n",
837
+ "Checked spaces: ['Hallway', 'Hallway', 'Bathroom 2', 'Bathroom 1', 'Kitchen', 'Kitchen', 'Bathroom 2', 'Bathroom 1']\n"
838
+ ]
839
+ }
840
+ ]
841
+ }
842
+ ],
843
+ "metadata": {
844
+ "colab": {
845
+ "provenance": []
846
+ },
847
+ "kernelspec": {
848
+ "display_name": "Python 3",
849
+ "name": "python3"
850
+ },
851
+ "language_info": {
852
+ "name": "python"
853
+ }
854
+ },
855
+ "nbformat": 4,
856
+ "nbformat_minor": 0
857
+ }
teams/Mastodonte/USER_PORTAL_PRD.md ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # User Portal β€” PRD (IFCore) β€” MVP
2
+
3
+ ## Overview (Revised per Team B feedback)
4
+ A lightweight authentication system for IFCore enabling sign up, login, logout, and profile management. This is a **foundation feature** that unblocks all other teams by providing `useSession()` hooks and role-based access.
5
+
6
+ **Scope:** Signup β†’ Login β†’ Profile page only. **Duration:** 4–6 hours (not 4 weeks).
7
+
8
+ Uses:
9
+ - **Frontend:** Cloudflare Pages (TanStack Router, Zustand)
10
+ - **Auth backend:** Cloudflare Worker with **Better Auth** (replaces manual auth implementation)
11
+ - **Database:** D1 (Better Auth creates its own tables; does NOT modify existing `users` table)
12
+ - **Email/Stripe/TOTP:** Cut from MVP (post-launch features)
13
+
14
+ ## Users & Roles (Simplified)
15
+ - **All users**: sign up with email + password, log in, view profile, log out.
16
+ - **Role model**: `role: "member"` (default) or `role: "captain"` (admins). Simple, not four-role RBAC.
17
+
18
+ ## Goals
19
+ 1. **Unblock all other teams** by providing `useSession()` React hook and persistent session state.
20
+ 2. Ship a working login/signup flow in < 6 hours using Better Auth (not custom hashing/session logic).
21
+ 3. Integrate with D1 and Cloudflare Workers following IFCore architecture.
22
+ 4. Respect existing `users` table schema β€” use Better Auth's default tables instead.
23
+
24
+ ## MVP Scope (Shippable in 1 day)
25
+
26
+ 1. **Sign up** β€” email + password. Create user in Better Auth's `user` table.
27
+ 2. **Log in** β€” email + password. Session cookie set by Worker.
28
+ 3. **Log out** β€” clear session, redirect to home.
29
+ 4. **Profile page** β€” show current user's name and email. Link to logout.
30
+ 5. **Navigation** β€” show "Log in" if unauthenticated, show user name + "Profile" link if authenticated.
31
+ 6. **RBAC enforcement** β€” each user has `role: "member"` or `"captain"` (stored in Better Auth's user table).
32
+
33
+ ### NOT in MVP (cut for later)
34
+ - Email verification
35
+ - Password reset
36
+ - Stripe billing / subscription management
37
+ - 2FA / TOTP
38
+ - Support ticketing
39
+ - Account deletion
40
+ - API key management
41
+ - Onboarding questionnaire
42
+ - T&Cs flow (optional: can add with a checkbox, but no signing logic)
43
+ - Support ticket system
44
+
45
+ ## Data Model (Better Auth defaults + minimal custom fields)
46
+
47
+ Better Auth creates these tables automatically:
48
+ - `user` β€” id, email, password (hashed with PBKDF2, NOT bcrypt), name, email_verified (boolean), image (optional), role (TEXT: "member" or "captain")
49
+ - `session` β€” session management (Better Auth handles this)
50
+ - `account` β€” OAuth/social login (not used in MVP)
51
+ - `verification` β€” email verification tokens (not used in MVP)
52
+
53
+ **Do NOT modify the existing `users` table.** It is used by other teams and the platform. Better Auth's `user` table is separate and additive.
54
+
55
+ ## API Endpoints (Worker, via Better Auth)
56
+
57
+ All endpoints are auto-generated by Better Auth under `/api/auth/*`:
58
+ - `POST /api/auth/sign-up` β€” Create new user (email, password, name)
59
+ - `POST /api/auth/sign-in` β€” Log in (email, password) β†’ returns session cookie
60
+ - `POST /api/auth/sign-out` β€” Log out β†’ clear session cookie
61
+ - `GET /api/auth/session` β€” Get current session (for `useSession()` hook)
62
+
63
+ Other teams call `useSession()` to access the session state. No custom API design needed.
64
+
65
+ ## UX Flows (simplified)
66
+
67
+ 1. **Sign up** β†’ Form (email, password, name) β†’ POST /api/auth/sign-up β†’ Session cookie set β†’ Redirect to `/profile`
68
+ 2. **Log in** β†’ Form (email, password) β†’ POST /api/auth/sign-in β†’ Session cookie set β†’ Redirect to `/profile`
69
+ 3. **Log out** β†’ POST /api/auth/sign-out β†’ Clear cookie β†’ Redirect to `/`
70
+ 4. **Profile page** β†’ Show user name, email. Link to logout.
71
+ 5. **Nav bar** β†’ If logged in, show name + "Profile" link. Else, show "Log In" link.
72
+
73
+ ## Why Better Auth (not custom auth)
74
+
75
+ **Rule:** Do NOT use bcrypt for password hashing in Cloudflare Workers. Bcrypt exceeds the 10ms CPU limit and causes Error 1102. Better Auth handles this automatically with PBKDF2.
76
+
77
+ **Why not build custom auth:**
78
+ - Passwords: hashing, validation, salting (security-critical, weeks of testing).
79
+ - Sessions: cookie management, CSRF tokens, expiry, refresh logic.
80
+ - Database schema: user tables, session tables, verifications (tedious DDL).
81
+ - Cookies: HTTP-only, SameSite, Secure flags.
82
+
83
+ Better Auth does all of this in ~50 lines of config. Use it.
84
+
85
+ ## Security & Compliance (simplified for MVP)
86
+
87
+ - **Passwords:** Better Auth uses PBKDF2 (works in Workers). Enforces strong passwords at signup.
88
+ - **Sessions:** HTTP-only, Secure, SameSite=Strict cookies managed by Better Auth.
89
+ - **CSRF:** Automatic via SameSite cookies.
90
+ - **Database tables:** Better Auth creates its own; does NOT touch existing `users` table.
91
+ - **Env variables:** BETTER_AUTH_SECRET and BETTER_AUTH_URL stored in Cloudflare secrets or `.dev.vars`.
92
+
93
+ ## Monitoring & Alerts
94
+ - Not in MVP. Post-launch: monitor signup/login failure spikes, session errors, Worker errors via D1 logs.
95
+
96
+ ## Edge Cases & Error Handling
97
+ - Invalid credentials: show error on login form.
98
+ - Password too weak: show validation error.
99
+ - Email already registered: show error on signup.
100
+ - Session expired: redirect to login.
101
+ - Database errors: log and show generic "something went wrong" message.
102
+
103
+ No complex retry logic, idempotency, or partial provisioning recovery needed for MVP.
104
+
105
+ ## MVP (v1) β€” 4–6 hours, shippable feature
106
+
107
+ **Backend (2–3 hours)**
108
+ - Install Better Auth and configure for D1 + PBKDF2.
109
+ - Mount auth routes in Cloudflare Worker at `/api/auth/*`.
110
+ - Create D1 migration for Better Auth tables (`user`, `session`, `account`, `verification`).
111
+ - Ensure `run_worker_first: ["/api/*"]` in wrangler.jsonc so Worker intercepts requests.
112
+ - Set `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` environment variables.
113
+
114
+ **Frontend (2–3 hours)**
115
+ - Create `auth-client.ts` with `createAuthClient` and `useSession` hook.
116
+ - Build login page: email + password form, "Sign Up" button.
117
+ - Build signup page: email + password + name form.
118
+ - Build profile page: show user name, email, logout button.
119
+ - Update navbar: show login/logout state conditionally.
120
+ - Deploy to Cloudflare Pages.
121
+
122
+ **Testing (30 min)**
123
+ - Sign up with new email.
124
+ - Verify navbar shows your name.
125
+ - Log out and verify navbar changes.
126
+ - Log back in and verify profile page works.
127
+
128
+ ## Acceptance Criteria
129
+ - Users can sign up with email + password and are logged in automatically.
130
+ - Users can log in with existing email + password.
131
+ - Users can log out and navbar reflects unauthenticated state.
132
+ - Profile page shows user name and email; only accessible when logged in.
133
+ - Session persists across page reloads (cookie-based).
134
+ - Better Auth tables created in D1 without modifying the existing `users` table.
135
+
136
+ ## How This Unblocks Other Teams
137
+
138
+ Every other team uses `useSession()` to gate features and filter data:
139
+ ```jsx
140
+ import { useSession } from '@/lib/auth-client'
141
+
142
+ export function Profile() {
143
+ const { data: session } = useSession()
144
+ if (!session) return <Redirect to="/login" />
145
+ return <h1>Welcome {session.user.name}</h1>
146
+ }
147
+ ```
148
+
149
+ You ship login/signup/profile. Other teams ship the dashboard, 3D viewer, report generator. Together = complete product.
150
+
151
+ ## Next Steps / Deliverables
152
+ 1. Review this MVP scope and approve (should take ~1 hour, not 4 weeks).
153
+ 2. Scaffold Cloudflare Worker Better Auth config (use the Cloudflare skill blueprint).
154
+ 3. Create frontend components (LoginPage, SignupPage, ProfilePage).
155
+ 4. Deploy to staging and test end-to-end.
156
+ 5. Do quick RBAC test (ensure captain role is stored/retrieved).
157
+ 6. Deploy to production.
158
+
159
+ ## Post-Launch Roadmap (out of scope for MVP)
160
+ - Email verification and password reset
161
+ - Stripe billing + subscription management
162
+ - 2FA / TOTP
163
+ - Support ticket system
164
+ - Account deletion with retention window
165
+ - Team invite / member management
166
+ - API key generation and rotation
teams/Mastodonte/main.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ IFCore Compliance Checker - Integration Test
3
+
4
+ This script tests all check_* functions against an IFC model.
5
+ Each check_* function returns list[dict] following the IFCore schema.
6
+ """
7
+ import ifcopenshell
8
+ from tools.checker_dwelling import check_dwelling_area
9
+ from tools.checker_heights import check_living_area_height
10
+ from tools.checker_living_rooms import check_living_room_compliance
11
+ from tools.checker_service_spaces import check_service_spaces_min_height
12
+
13
+
14
+ # Map of check names to functions and their parameters
15
+ CHECKS = {
16
+ "dwelling_area": {
17
+ "func": check_dwelling_area,
18
+ "kwargs": {"min_area": 36.0},
19
+ },
20
+ "living_area_height": {
21
+ "func": check_living_area_height,
22
+ "kwargs": {"min_height": 2.50},
23
+ },
24
+ "living_room_compliance": {
25
+ "func": check_living_room_compliance,
26
+ "kwargs": {},
27
+ },
28
+ "service_spaces_min_height": {
29
+ "func": check_service_spaces_min_height,
30
+ "kwargs": {"min_height": 2.20},
31
+ },
32
+ }
33
+
34
+
35
+ def run_all_checks(ifc_model_path):
36
+ """
37
+ Load IFC model once and run all checks.
38
+
39
+ Returns:
40
+ dict: { check_name: [results], ... }
41
+ """
42
+ print(f"Loading IFC model: {ifc_model_path}")
43
+ model = ifcopenshell.open(ifc_model_path)
44
+
45
+ all_results = {}
46
+
47
+ for check_name, check_info in CHECKS.items():
48
+ print(f"\nRunning {check_name}...")
49
+ func = check_info["func"]
50
+ kwargs = check_info["kwargs"]
51
+
52
+ try:
53
+ results = func(model, **kwargs)
54
+ all_results[check_name] = results
55
+ print(str(results))
56
+
57
+ # Print summary
58
+ if results:
59
+ passed = sum(1 for r in results if r["check_status"] == "pass")
60
+ failed = sum(1 for r in results if r["check_status"] == "fail")
61
+ print(f" [OK] {check_name}: {passed} passed, {failed} failed")
62
+
63
+ for r in results:
64
+ status_symbol = "[PASS]" if r["check_status"] == "pass" else "[FAIL]"
65
+ print(f" {status_symbol} {r['element_name']}: {r['actual_value']} (req: {r['required_value']})")
66
+ if r['comment']:
67
+ print(f" -> {r['comment']}")
68
+ else:
69
+ print(f" [INFO] {check_name}: No elements checked")
70
+
71
+ except Exception as e:
72
+ print(f" [ERROR] {check_name} FAILED: {str(e)}")
73
+ all_results[check_name] = []
74
+
75
+ return all_results
76
+
77
+
78
+ if __name__ == "__main__":
79
+ ifc_path = r"C:/Users/OWNER/Desktop/IAAC/T05/AI_SpeedRun/AI-Speed-Run-Week/ifc_models/arc.ifc"
80
+
81
+ print("=" * 70)
82
+ print("IFCore Compliance Checker - Integration Test")
83
+ print("=" * 70)
84
+
85
+ results = run_all_checks(ifc_path)
86
+
87
+ print("\n" + "=" * 70)
88
+ print("SUMMARY")
89
+ print("=" * 70)
90
+ for check_name, check_results in results.items():
91
+ if check_results:
92
+ passed = sum(1 for r in check_results if r["check_status"] == "pass")
93
+ failed = sum(1 for r in check_results if r["check_status"] == "fail")
94
+ print(f"{check_name}: {passed} PASS / {failed} FAIL")
95
+ else:
96
+ print(f"{check_name}: (no results)")
97
+
teams/Mastodonte/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ifcopenshell
teams/Mastodonte/tools/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ IFCore Compliance Checker Tools
3
+
4
+ All check_* functions exported from this package follow the IFCore contract:
5
+ - Signature: check_*(model, **kwargs)
6
+ - Returns: list[dict] with IFCore schema (element_id, element_type, check_status, etc.)
7
+ - Platform auto-discovers check functions by name prefix "check_"
8
+ """
9
+
10
+ from .checker_dwelling import check_dwelling_area
11
+ from .checker_heights import check_living_area_height
12
+ from .checker_living_rooms import check_living_room_compliance
13
+ from .checker_service_spaces import check_service_spaces_min_height
14
+ from .checker_occupancy import check_bedroom_occupancy
15
+
16
+ __all__ = [
17
+ "check_dwelling_area",
18
+ "check_living_area_height",
19
+ "check_living_room_compliance",
20
+ "check_service_spaces_min_height",
21
+ "check_bedroom_occupancy",
22
+ ]
teams/Mastodonte/tools/checker_dwelling.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Requirements
2
+ import math
3
+ import json
4
+ import ifcopenshell
5
+ import ifcopenshell.geom
6
+
7
+ # --------------------------
8
+ # Geometry settings
9
+ # --------------------------
10
+ settings = ifcopenshell.geom.settings()
11
+ settings.set(settings.USE_WORLD_COORDS, True)
12
+
13
+ # --------------------------
14
+ # IFC helpers
15
+ # --------------------------
16
+ def calculate_space_area(space):
17
+ """Approximate area by summing triangle areas from the space mesh."""
18
+ try:
19
+ shape = ifcopenshell.geom.create_shape(settings, space)
20
+ verts = shape.geometry.verts
21
+ faces = shape.geometry.faces
22
+ area = 0.0
23
+
24
+ for i in range(0, len(faces), 3):
25
+ i0, i1, i2 = faces[i] * 3, faces[i + 1] * 3, faces[i + 2] * 3
26
+ v0 = verts[i0:i0 + 3]
27
+ v1 = verts[i1:i1 + 3]
28
+ v2 = verts[i2:i2 + 3]
29
+
30
+ a = math.sqrt((v1[0]-v0[0])**2 + (v1[1]-v0[1])**2 + (v1[2]-v0[2])**2)
31
+ b = math.sqrt((v2[0]-v1[0])**2 + (v2[1]-v1[1])**2 + (v2[2]-v1[2])**2)
32
+ c = math.sqrt((v0[0]-v2[0])**2 + (v0[1]-v2[1])**2 + (v0[2]-v2[2])**2)
33
+
34
+ s = (a + b + c) / 2.0
35
+ area += math.sqrt(max(s * (s - a) * (s - b) * (s - c), 0.0))
36
+
37
+ return float(area)
38
+ except Exception:
39
+ return 0.0
40
+
41
+ def check_dwelling_area(model, min_area=36.0):
42
+ """
43
+ Check if each space in the dwelling meets minimum area requirement.
44
+
45
+ Args:
46
+ model: ifcopenshell.file object (pre-loaded IFC model)
47
+ min_area: minimum total area in mΒ² (default 36.0)
48
+
49
+ Returns:
50
+ list[dict]: One dict per IfcSpace element, following IFCore schema
51
+ """
52
+ spaces = model.by_type("IfcSpace")
53
+ results = []
54
+ total_area = 0.0
55
+
56
+ for space in spaces:
57
+ area = calculate_space_area(space)
58
+ space_name = getattr(space, "LongName", None) or getattr(space, "Name", None) or f"Space #{space.id()}"
59
+ space_id = getattr(space, "GlobalId", str(space.id()))
60
+
61
+ total_area += float(area)
62
+
63
+ results.append({
64
+ "element_id": space_id,
65
+ "element_type": "IfcSpace",
66
+ "element_name": space_name,
67
+ "element_name_long": f"{space_name} (dwelling area check)",
68
+ "check_status": "pass", # Will be determined after loop for aggregate
69
+ "actual_value": f"{area:.2f} mΒ²",
70
+ "required_value": f"{min_area:.2f} mΒ²",
71
+ "comment": None,
72
+ "log": None,
73
+ })
74
+
75
+ # Update check_status based on total area
76
+ if len(results) == 0:
77
+ return results
78
+
79
+ status = "pass" if total_area >= float(min_area) else "fail"
80
+ for result in results:
81
+ result["check_status"] = status
82
+ if status == "fail":
83
+ result["comment"] = f"Total dwelling area {total_area:.2f} mΒ² is below required {min_area:.2f} mΒ²"
84
+
85
+ return results
86
+
87
+
88
+ # --------------------------
89
+ # Local testing (optional)
90
+ # --------------------------
91
+ if __name__ == "__main__":
92
+ import ifcopenshell
93
+ ifc_path = "C:/Users/OWNER/Desktop/IAAC/T05/AI_SpeedRun/AI-Speed-Run-Week/ifc_models/arc.ifc"
94
+ model = ifcopenshell.open(ifc_path)
95
+ results = check_dwelling_area(model, min_area=36.0)
96
+
97
+ print("Dwelling Area Check Results:")
98
+ for r in results:
99
+ print(f"[{r['check_status'].upper()}] {r['element_name']}: {r['actual_value']}")
100
+ if r['comment']:
101
+ print(f" β†’ {r['comment']}")
teams/Mastodonte/tools/checker_heights.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import ifcopenshell
3
+ import ifcopenshell.geom
4
+
5
+ # --------------------------
6
+ # Geometry settings
7
+ # --------------------------
8
+ settings = ifcopenshell.geom.settings()
9
+ settings.set(settings.USE_WORLD_COORDS, True)
10
+
11
+ # --------------------------
12
+ # Helper functions
13
+ # --------------------------
14
+ def load_model(ifc_model_path):
15
+ """Load IFC model"""
16
+ return ifcopenshell.open(ifc_model_path)
17
+
18
+ def get_main_living_areas(ifc_model):
19
+ """
20
+ Return spaces considered main living areas.
21
+ Filtering by common keywords in space name.
22
+ """
23
+ spaces = ifc_model.by_type("IfcSpace")
24
+ main_spaces = []
25
+ for s in spaces:
26
+ if s.Name and any(keyword in s.Name.lower() for keyword in ["living", "bedroom", "hall"]):
27
+ main_spaces.append(s)
28
+ return main_spaces
29
+
30
+ def get_space_height(space):
31
+ """
32
+ Approximate height of a space from geometry bounding box.
33
+ Uses Z coordinates of all vertices.
34
+ """
35
+ try:
36
+ shape = ifcopenshell.geom.create_shape(settings, space)
37
+ verts = shape.geometry.verts
38
+ if not verts:
39
+ return 0.0
40
+ zs = verts[2::3]
41
+ return float(max(zs) - min(zs))
42
+ except Exception:
43
+ return 0.0
44
+
45
+ # --------------------------
46
+ # Main check function
47
+ # --------------------------
48
+ def check_living_area_height(model, min_height=2.50):
49
+ """
50
+ Check if all main living areas meet the minimum height requirement.
51
+
52
+ Args:
53
+ model: ifcopenshell.file object (pre-loaded IFC model)
54
+ min_height: minimum height in meters (default 2.50)
55
+
56
+ Returns:
57
+ list[dict]: One dict per living/bedroom/hall space, following IFCore schema
58
+ """
59
+ spaces = get_main_living_areas(model)
60
+ results = []
61
+
62
+ for space in spaces:
63
+ height = get_space_height(space)
64
+ space_name = space.Name or f"Space #{space.id()}"
65
+ space_id = getattr(space, "GlobalId", str(space.id()))
66
+
67
+ status = "pass" if height >= min_height else "fail"
68
+ comment = None if height >= min_height else f"Height {height:.2f}m below required {min_height:.2f}m"
69
+
70
+ results.append({
71
+ "element_id": space_id,
72
+ "element_type": "IfcSpace",
73
+ "element_name": space_name,
74
+ "element_name_long": f"{space_name} (living area)",
75
+ "check_status": status,
76
+ "actual_value": f"{height:.2f} m",
77
+ "required_value": f"{min_height:.2f} m",
78
+ "comment": comment,
79
+ "log": None,
80
+ })
81
+
82
+ return results
83
+
84
+ # --------------------------
85
+ # Local testing (optional)
86
+ # --------------------------
87
+ if __name__ == "__main__":
88
+ import ifcopenshell
89
+ ifc_path = "C:/Users/OWNER/Desktop/IAAC/T05/AI_SpeedRun/AI-Speed-Run-Week/ifc_models/arc.ifc"
90
+ model = ifcopenshell.open(ifc_path)
91
+ results = check_living_area_height(model, min_height=2.50)
92
+
93
+ print("Living Area Height Check Results:")
94
+ for r in results:
95
+ print(f"[{r['check_status'].upper()}] {r['element_name']}: {r['actual_value']}")
96
+ if r['comment']:
97
+ print(f" β†’ {r['comment']}")
teams/Mastodonte/tools/checker_living_rooms.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import math
3
+ import ifcopenshell
4
+ import ifcopenshell.geom
5
+
6
+ # Optional: only needed if you truly have it available
7
+ # from ifcopenshell.util.element import get_psets as _get_psets
8
+
9
+ # --------------------------
10
+ # Geometry settings
11
+ # --------------------------
12
+ settings = ifcopenshell.geom.settings()
13
+ settings.set(settings.USE_WORLD_COORDS, True)
14
+
15
+ # --------------------------
16
+ # Helper functions
17
+ # --------------------------
18
+ def load_model(ifc_model_path):
19
+ return ifcopenshell.open(ifc_model_path)
20
+
21
+ def calculate_space_area(space):
22
+ """
23
+ Approximate area from mesh triangles.
24
+ Note: this is mesh surface area, not guaranteed to be floor area.
25
+ """
26
+ if hasattr(space, "NetFloorArea") and space.NetFloorArea:
27
+ return float(space.NetFloorArea)
28
+
29
+ try:
30
+ shape = ifcopenshell.geom.create_shape(settings, space)
31
+ verts = shape.geometry.verts
32
+ faces = shape.geometry.faces
33
+ area = 0.0
34
+
35
+ for i in range(0, len(faces), 3):
36
+ idx0, idx1, idx2 = faces[i] * 3, faces[i + 1] * 3, faces[i + 2] * 3
37
+ v0 = verts[idx0:idx0 + 3]
38
+ v1 = verts[idx1:idx1 + 3]
39
+ v2 = verts[idx2:idx2 + 3]
40
+
41
+ a = math.sqrt((v1[0]-v0[0])**2 + (v1[1]-v0[1])**2 + (v1[2]-v0[2])**2)
42
+ b = math.sqrt((v2[0]-v1[0])**2 + (v2[1]-v1[1])**2 + (v2[2]-v1[2])**2)
43
+ c = math.sqrt((v0[0]-v2[0])**2 + (v0[1]-v2[1])**2 + (v0[2]-v2[2])**2)
44
+
45
+ s = (a + b + c) / 2.0
46
+ area += math.sqrt(max(s * (s - a) * (s - b) * (s - c), 0.0))
47
+
48
+ return float(area)
49
+ except Exception:
50
+ return 0.0
51
+
52
+ def get_space_name(space):
53
+ """Get descriptive name if available, fallback to LongName/Name/GlobalId."""
54
+ if getattr(space, "LongName", None):
55
+ return str(space.LongName)
56
+ if getattr(space, "Name", None):
57
+ return str(space.Name)
58
+
59
+ # If you have ifcopenshell.util.element available, you can enable this block:
60
+ # try:
61
+ # psets = _get_psets(space)
62
+ # for props in (psets or {}).values():
63
+ # for k, v in (props or {}).items():
64
+ # if "name" in str(k).lower() and isinstance(v, str) and v.strip():
65
+ # return v.strip()
66
+ # except Exception:
67
+ # pass
68
+
69
+ return str(getattr(space, "GlobalId", "Unnamed"))
70
+
71
+ def can_fit_square(area_m2, width=2.4, depth=2.4):
72
+ """Approx check: area must allow a width x depth square."""
73
+ return float(area_m2) >= float(width) * float(depth)
74
+
75
+ # --------------------------
76
+ # Living room compliance
77
+ # --------------------------
78
+ def check_living_room_compliance(model):
79
+ """
80
+ Check living room spaces for minimum area and clearance rules.
81
+
82
+ Rule encoded:
83
+ - Living room min area 10 mΒ²
84
+ - If living+kitchen combined: min area 14 mΒ²
85
+ - Must allow 2.40 x 2.40 m clearance (approx via area check)
86
+
87
+ Args:
88
+ model: ifcopenshell.file object (pre-loaded IFC model)
89
+
90
+ Returns:
91
+ list[dict]: One dict per living room space, following IFCore schema
92
+ """
93
+ spaces = model.by_type("IfcSpace")
94
+ results = []
95
+
96
+ for space in spaces:
97
+ raw_name = get_space_name(space)
98
+ name = raw_name.lower()
99
+ area = calculate_space_area(space)
100
+ space_id = getattr(space, "GlobalId", str(space.id()))
101
+
102
+ if area <= 0:
103
+ continue
104
+
105
+ if "living" in name:
106
+ has_kitchen = "kitchen" in name
107
+ min_area = 14.0 if has_kitchen else 10.0
108
+
109
+ clearance_ok = can_fit_square(area, 2.4, 2.4)
110
+ passed = (area >= min_area) and clearance_ok
111
+
112
+ reasons = []
113
+ if area < min_area:
114
+ reasons.append(f"Area {area:.2f} mΒ² < required {min_area:.2f} mΒ²")
115
+ if not clearance_ok:
116
+ reasons.append("Does not allow 2.40 m x 2.40 m square (approx)")
117
+
118
+ results.append({
119
+ "element_id": space_id,
120
+ "element_type": "IfcSpace",
121
+ "element_name": raw_name,
122
+ "element_name_long": f"{raw_name} (living room zone)",
123
+ "check_status": "pass" if passed else "fail",
124
+ "actual_value": f"{area:.2f} mΒ²",
125
+ "required_value": f"{min_area:.2f} mΒ²",
126
+ "comment": "; ".join(reasons) if reasons else None,
127
+ "log": None,
128
+ })
129
+
130
+ return results
131
+
132
+ # --------------------------
133
+ # Local testing (optional)
134
+ # --------------------------
135
+ if __name__ == "__main__":
136
+ import ifcopenshell
137
+ ifc_path = "C:/Users/OWNER/Desktop/IAAC/T05/AI_SpeedRun/AI-Speed-Run-Week/ifc_models/arc.ifc"
138
+ model = ifcopenshell.open(ifc_path)
139
+ results = check_living_room_compliance(model)
140
+
141
+ print("Living Room Compliance Check Results:")
142
+ if not results:
143
+ print("No living room spaces matched (keyword 'living') or no computable areas.")
144
+ else:
145
+ for r in results:
146
+ print(f"[{r['check_status'].upper()}] {r['element_name']}: {r['actual_value']}")
147
+ if r['comment']:
148
+ print(f" -> {r['comment']}")
teams/Mastodonte/tools/checker_occupancy.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import json
3
+ import ifcopenshell
4
+ import ifcopenshell.geom
5
+
6
+ # --------------------------
7
+ # Geometry settings
8
+ # --------------------------
9
+ settings = ifcopenshell.geom.settings()
10
+ settings.set(settings.USE_WORLD_COORDS, True)
11
+
12
+ # --------------------------
13
+ # Helper functions
14
+ # --------------------------
15
+ def calculate_space_area(space):
16
+ """Approximate area by summing triangle areas from the space mesh."""
17
+ try:
18
+ shape = ifcopenshell.geom.create_shape(settings, space)
19
+ verts = shape.geometry.verts
20
+ faces = shape.geometry.faces
21
+ area = 0.0
22
+
23
+ for i in range(0, len(faces), 3):
24
+ i0, i1, i2 = faces[i] * 3, faces[i + 1] * 3, faces[i + 2] * 3
25
+ v0 = verts[i0:i0 + 3]
26
+ v1 = verts[i1:i1 + 3]
27
+ v2 = verts[i2:i2 + 3]
28
+
29
+ a = math.dist(v0, v1)
30
+ b = math.dist(v1, v2)
31
+ c = math.dist(v2, v0)
32
+
33
+ s = (a + b + c) / 2.0
34
+ area += math.sqrt(max(s * (s - a) * (s - b) * (s - c), 0.0))
35
+
36
+ return float(area)
37
+ except Exception:
38
+ return 0.0
39
+
40
+
41
+ def get_space_label(space):
42
+ """Get descriptive name if available, fallback to GlobalId."""
43
+ return str(
44
+ getattr(space, "LongName", None)
45
+ or getattr(space, "Name", None)
46
+ or f"Space #{space.id()}"
47
+ )
48
+
49
+
50
+ def area_to_occupancy(area_m2):
51
+ """
52
+ Regulation mapping:
53
+ <5 mΒ² -> invalid bedroom
54
+ β‰₯5 mΒ² -> 1 person
55
+ β‰₯8 mΒ² -> 2 people
56
+ β‰₯12 mΒ² -> 3 people
57
+ """
58
+ if area_m2 < 5.0:
59
+ return 0
60
+ elif area_m2 < 8.0:
61
+ return 1
62
+ elif area_m2 < 12.0:
63
+ return 2
64
+ else:
65
+ return 3
66
+
67
+
68
+ def get_spaces_by_keywords(model, keywords):
69
+ """Filter spaces matching any keyword in their name."""
70
+ spaces = model.by_type("IfcSpace")
71
+ matched = []
72
+ for s in spaces:
73
+ label_lower = get_space_label(s).lower()
74
+ if any(k in label_lower for k in keywords):
75
+ matched.append(s)
76
+ return matched
77
+
78
+
79
+ # --------------------------
80
+ # Main check function
81
+ # --------------------------
82
+ def check_bedroom_occupancy(model):
83
+ """
84
+ Check if all bedrooms meet minimum area requirement and calculate occupancy.
85
+
86
+ Regulation:
87
+ - Bedroom min area 5 mΒ² for 1 occupant
88
+ - 8 mΒ² for 2 occupants
89
+ - 12 mΒ² for 3 occupants
90
+ - Studio (no bedrooms): limited to max 2 occupants
91
+
92
+ Args:
93
+ model: ifcopenshell.file object (pre-loaded IFC model)
94
+
95
+ Returns:
96
+ list[dict]: One dict per bedroom/living space, following IFCore schema
97
+ """
98
+ results = []
99
+
100
+ bedroom_keywords = ["bedroom", "habitacion", "habitaciΓ³n", "dormitorio"]
101
+ living_keywords = ["living", "salon", "salΓ³n", "studio", "estudio", "common"]
102
+
103
+ bedrooms = get_spaces_by_keywords(model, bedroom_keywords)
104
+ living_spaces = get_spaces_by_keywords(model, living_keywords)
105
+
106
+ # --------------------------
107
+ # Studio case (no bedrooms)
108
+ # --------------------------
109
+ if len(bedrooms) == 0:
110
+ if living_spaces:
111
+ space = living_spaces[0]
112
+ label = get_space_label(space)
113
+ area = calculate_space_area(space)
114
+ space_id = getattr(space, "GlobalId", str(space.id()))
115
+ occupancy = min(area_to_occupancy(area), 2)
116
+ passed = occupancy > 0
117
+
118
+ results.append({
119
+ "element_id": space_id,
120
+ "element_type": "IfcSpace",
121
+ "element_name": label,
122
+ "element_name_long": f"{label} (studio dwelling)",
123
+ "check_status": "pass" if passed else "fail",
124
+ "actual_value": f"{area:.2f} mΒ² β†’ {occupancy} occupant(s) max",
125
+ "required_value": "Studio max 2 occupants",
126
+ "comment": "Studio dwelling limited to maximum 2 occupants" if passed else f"Area {area:.2f} mΒ² too small for studio",
127
+ "log": None,
128
+ })
129
+ return results
130
+
131
+ # --------------------------
132
+ # Bedroom-based case
133
+ # --------------------------
134
+ for space in bedrooms:
135
+ label = get_space_label(space)
136
+ area = calculate_space_area(space)
137
+ space_id = getattr(space, "GlobalId", str(space.id()))
138
+ occupancy = area_to_occupancy(area)
139
+
140
+ passed = occupancy > 0
141
+ comment = None
142
+ if not passed:
143
+ comment = f"Area {area:.2f} mΒ² below minimum 5 mΒ² for bedroom"
144
+
145
+ results.append({
146
+ "element_id": space_id,
147
+ "element_type": "IfcSpace",
148
+ "element_name": label,
149
+ "element_name_long": f"{label} (bedroom occupancy)",
150
+ "check_status": "pass" if passed else "fail",
151
+ "actual_value": f"{area:.2f} mΒ² β†’ {occupancy} occupant(s)",
152
+ "required_value": "β‰₯5 mΒ² (min 1 occupant)",
153
+ "comment": comment,
154
+ "log": None,
155
+ })
156
+
157
+ return results
158
+
159
+
160
+ # --------------------------
161
+ # Local testing (optional)
162
+ # --------------------------
163
+ if __name__ == "__main__":
164
+ import ifcopenshell
165
+ ifc_path = "C:/Users/OWNER/Desktop/IAAC/T05/AI_SpeedRun/AI-Speed-Run-Week/ifc_models/arc.ifc"
166
+ model = ifcopenshell.open(ifc_path)
167
+ results = check_bedroom_occupancy(model)
168
+
169
+ print("Bedroom Occupancy Check Results:")
170
+ for r in results:
171
+ print(f"[{r['check_status'].upper()}] {r['element_name']}: {r['actual_value']}")
172
+ if r['comment']:
173
+ print(f" β†’ {r['comment']}")
teams/Mastodonte/tools/checker_service_spaces.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import ifcopenshell
3
+ import ifcopenshell.geom
4
+
5
+ # --------------------------
6
+ # Geometry settings
7
+ # --------------------------
8
+ settings = ifcopenshell.geom.settings()
9
+ settings.set(settings.USE_WORLD_COORDS, True)
10
+
11
+ # --------------------------
12
+ # Helper functions
13
+ # --------------------------
14
+ def load_model(ifc_model_path):
15
+ return ifcopenshell.open(ifc_model_path)
16
+
17
+ def get_space_name(space):
18
+ """Get descriptive name if available, fallback to LongName/Name/GlobalId."""
19
+ if getattr(space, "LongName", None):
20
+ return str(space.LongName)
21
+ if getattr(space, "Name", None):
22
+ return str(space.Name)
23
+ return str(getattr(space, "GlobalId", "Unnamed"))
24
+
25
+ def get_space_height(space):
26
+ """
27
+ Approximate height of a space from geometry bounding box.
28
+ Uses Z coordinates of all vertices.
29
+ """
30
+ try:
31
+ shape = ifcopenshell.geom.create_shape(settings, space)
32
+ verts = shape.geometry.verts
33
+ if not verts:
34
+ return 0.0
35
+ zs = verts[2::3]
36
+ return float(max(zs) - min(zs))
37
+ except Exception:
38
+ return 0.0
39
+
40
+ def is_service_space(space_name_lower):
41
+ """Bathrooms, kitchens, hallways by keywords."""
42
+ service_keywords = [
43
+ "bath", "bathroom", "baΓ±o", "bano", "wc", "toilet",
44
+ "kitchen", "cocina",
45
+ "hall", "hallway", "corridor", "pasillo"
46
+ ]
47
+ return any(k in space_name_lower for k in service_keywords)
48
+
49
+ # --------------------------
50
+ # Compliance check
51
+ # --------------------------
52
+ def check_service_spaces_min_height(model, min_height=2.20):
53
+ """
54
+ Check if bathrooms, kitchens, and hallways meet minimum height requirement.
55
+
56
+ Regulation:
57
+ - Minimum height in bathrooms, kitchens, and hallways is 2.20 m
58
+
59
+ Args:
60
+ model: ifcopenshell.file object (pre-loaded IFC model)
61
+ min_height: minimum height in meters (default 2.20)
62
+
63
+ Returns:
64
+ list[dict]: One dict per service space, following IFCore schema
65
+ """
66
+ spaces = model.by_type("IfcSpace")
67
+ results = []
68
+
69
+ for space in spaces:
70
+ label = get_space_name(space)
71
+ label_l = label.lower()
72
+
73
+ if not is_service_space(label_l):
74
+ continue
75
+
76
+ height = get_space_height(space)
77
+ space_id = getattr(space, "GlobalId", str(space.id()))
78
+
79
+ status = "pass" if height >= min_height else "fail"
80
+ comment = None if height >= min_height else f"Height {height:.2f}m below required {min_height:.2f}m"
81
+
82
+ results.append({
83
+ "element_id": space_id,
84
+ "element_type": "IfcSpace",
85
+ "element_name": label,
86
+ "element_name_long": f"{label} (service space)",
87
+ "check_status": status,
88
+ "actual_value": f"{height:.2f} m",
89
+ "required_value": f"{min_height:.2f} m",
90
+ "comment": comment,
91
+ "log": None,
92
+ })
93
+
94
+ return results
95
+
96
+ # --------------------------
97
+ # Local testing (optional)
98
+ # --------------------------
99
+ if __name__ == "__main__":
100
+ import ifcopenshell
101
+ ifc_path = "C:/Users/OWNER/Desktop/IAAC/T05/AI_SpeedRun/AI-Speed-Run-Week/ifc_models/arc.ifc"
102
+ model = ifcopenshell.open(ifc_path)
103
+ results = check_service_spaces_min_height(model, min_height=2.20)
104
+
105
+ print("Service Spaces Min Height Check Results:")
106
+ for r in results:
107
+ print(f"[{r['check_status'].upper()}] {r['element_name']}: {r['actual_value']}")
108
+ if r['comment']:
109
+ print(f" β†’ {r['comment']}")
teams/demo/tools/checker_demo.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Demo check: counts doors and verifies minimum count."""
2
+
3
+
4
+ def check_door_count(model, min_doors=2):
5
+ doors = model.by_type("IfcDoor")
6
+ results = []
7
+ for door in doors:
8
+ name = door.Name or f"Door #{door.id()}"
9
+ results.append({
10
+ "element_id": door.GlobalId,
11
+ "element_type": "IfcDoor",
12
+ "element_name": name,
13
+ "element_name_long": None,
14
+ "check_status": "pass",
15
+ "actual_value": "Present",
16
+ "required_value": f">= {min_doors} doors total",
17
+ "comment": None,
18
+ "log": None,
19
+ })
20
+ if len(doors) < min_doors:
21
+ results.append({
22
+ "element_id": None,
23
+ "element_type": "Summary",
24
+ "element_name": "Door Count Check",
25
+ "element_name_long": None,
26
+ "check_status": "fail",
27
+ "actual_value": f"{len(doors)} doors",
28
+ "required_value": f">= {min_doors} doors",
29
+ "comment": f"Found {len(doors)} doors, need >= {min_doors}",
30
+ "log": None,
31
+ })
32
+ return results
teams/lux-ai/AGENT_HANDOFF_REPORT.md ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lux.Ai β€” IFC Metadata Extraction: Agent Handoff Report
2
+
3
+ **Date:** 2026-02-18
4
+ **Project:** Lux.Ai β€” AI for Architecture & Urbanism
5
+ **Repo root:** `c:\Users\LEGION\Documents\GitHub\Lux.Ai\`
6
+ **Python:** 3.12.10 via `.venv\` virtual environment
7
+
8
+ ---
9
+
10
+ ## What Was Built (Summary)
11
+
12
+ Three things were created in this session:
13
+
14
+ | # | What | File | Status |
15
+ |---|------|------|--------|
16
+ | 1 | IFC model scanner β€” extracts 4 building metrics from all IFC files | `scan_ifc_models.py` | Done, working |
17
+ | 2 | IFC metadata key discovery β€” inventories every property/quantity name across all models | `discover_ifc_keys.py` | Done, working |
18
+ | 3 | Canonical alias map β€” standardised lookup table for future extractors | `key_aliases.json` | Generated |
19
+
20
+ ---
21
+
22
+ ## Context & Problem Being Solved
23
+
24
+ The repo contains **36 IFC (Industry Foundation Classes) BIM model files** across 20 sample building projects in `Sample projects/projects/`. IFC is the open standard for Building Information Modeling used by Archicad, Revit, Vectorworks, and other tools.
25
+
26
+ **The challenge:** Different IFC exporters use different internal names for the same concept. For example, "floor area" might be stored as:
27
+ - `Qto_SpaceBaseQuantities / NetFloorArea` (IFC4 / buildingSMART standard)
28
+ - `BaseQuantities / NetFloorArea` (IFC2x3 legacy)
29
+ - `GSA Space Areas / GSA BIM Area` (Revit US government exports)
30
+ - `ArchiCADQuantities / Netto-GrundflΓ€che` (Archicad German)
31
+
32
+ The goal was to scan all files, discover every key name actually in use, and produce a normalised mapping so any future function can reliably extract data regardless of which software created the file.
33
+
34
+ ---
35
+
36
+ ## Repository Structure
37
+
38
+ ```
39
+ Lux.Ai/
40
+ β”œβ”€β”€ scan_ifc_models.py ← NEW: extracts 4 metrics from all IFC files
41
+ β”œβ”€β”€ discover_ifc_keys.py ← NEW: inventories all property/quantity keys
42
+ β”œβ”€β”€ key_aliases.json ← NEW: canonical key β†’ alias mapping (config)
43
+ β”œβ”€β”€ ifc_key_inventory.json ← NEW: raw inventory of all keys found
44
+ β”œβ”€β”€ ifc_scan_results.csv ← NEW: output data table (36 rows)
45
+ β”œβ”€β”€ ifc_scan.log ← NEW: per-file processing log
46
+ β”œβ”€β”€ .venv/ ← Python 3.12 virtual environment
47
+ β”‚ └── Scripts/python.exe
48
+ └── Sample projects/
49
+ └── projects/ ← 36 IFC files across 20 projects
50
+ β”œβ”€β”€ 4351/arc.ifc
51
+ β”œβ”€β”€ ac20/arc.ifc
52
+ β”œβ”€β”€ city_house_munich/arc.ifc
53
+ β”œβ”€β”€ dental_clinic/arc.ifc + mep.ifc + str.ifc
54
+ β”œβ”€β”€ digital_hub/arc.ifc + heating.ifc + plumbing.ifc + ventilation.ifc
55
+ β”œβ”€β”€ duplex/arc.ifc + mep.ifc
56
+ β”œβ”€β”€ ettenheim_gis/city.ifc
57
+ β”œβ”€β”€ fantasy_hotel_1/arc.ifc
58
+ β”œβ”€β”€ fantasy_hotel_2/arc.ifc
59
+ β”œβ”€β”€ fantasy_office_building_1/arc.ifc
60
+ β”œβ”€β”€ fantasy_office_building_2/arc.ifc
61
+ β”œβ”€β”€ fantasy_office_building_3/arc.ifc
62
+ β”œβ”€β”€ fantasy_residential_building_1/arc.ifc
63
+ β”œβ”€β”€ fzk_house/arc.ifc
64
+ β”œβ”€β”€ hitos/arc.ifc
65
+ β”œβ”€β”€ molio/arc.ifc
66
+ β”œβ”€β”€ samuel_macalister_sample_house/arc.ifc + mep.ifc
67
+ β”œβ”€β”€ schependomlaan/arc.ifc
68
+ β”œβ”€β”€ sixty5/arc.ifc + str.ifc + facade.ifc + electrical.ifc + kitchen.ifc + plumbing.ifc + ventilation.ifc
69
+ β”œβ”€β”€ smiley_west/arc.ifc
70
+ └── wbdg_office/arc.ifc + str.ifc + mep.ifc
71
+ ```
72
+
73
+ Each project also contains:
74
+ - `model_card.md` β€” YAML frontmatter with project metadata (source, license, discipline list)
75
+ - `snapshot.png` β€” thumbnail
76
+ - `license.txt`
77
+
78
+ ---
79
+
80
+ ## Dependencies Installed
81
+
82
+ ```bash
83
+ .venv/Scripts/python.exe -m pip install ifcopenshell tabulate
84
+ ```
85
+
86
+ Packages installed:
87
+ - `ifcopenshell==0.8.4.post1` β€” IFC file parser (binary wheel, no compiler needed)
88
+ - `tabulate==0.9.0` β€” console table formatting
89
+ - `numpy`, `shapely`, `lark`, `isodate` β€” pulled in as ifcopenshell dependencies
90
+
91
+ ---
92
+
93
+ ## File 1: `scan_ifc_models.py`
94
+
95
+ ### Purpose
96
+ Scans all IFC files and extracts 4 building metrics per file into a CSV.
97
+
98
+ ### Run
99
+ ```bash
100
+ .venv/Scripts/python.exe scan_ifc_models.py
101
+ # Optional args:
102
+ # --root "path/to/ifc/directory" (default: Sample projects/projects/)
103
+ # --output results.csv (default: ifc_scan_results.csv)
104
+ ```
105
+
106
+ ### What it extracts
107
+
108
+ | Canonical Key | IFC Source | Fallback Strategy |
109
+ |---|---|---|
110
+ | `window_area_m2` | `IfcWindow β†’ Qto_WindowBaseQuantities / Area` | `BaseQuantities/Area` β†’ `OverallHeight Γ— OverallWidth` |
111
+ | `floor_area_m2` | `IfcSpace β†’ Qto_SpaceBaseQuantities / NetFloorArea` | `GrossFloorArea` β†’ `BaseQuantities/NetFloorArea` β†’ `IfcSlab[FLOOR]/NetArea` |
112
+ | `roof_area_m2` | `IfcRoof β†’ Qto_RoofBaseQuantities / NetArea` | `GrossArea` β†’ `IfcSlab[ROOF]/NetArea` β†’ `GrossArea` |
113
+ | `true_north_angle_deg` | `IfcGeometricRepresentationContext.TrueNorth` | DirectionRatios (X,Y) β†’ `atan2(x,y)` β†’ compass bearing (clockwise from north) |
114
+ | `latitude` | `IfcSite.RefLatitude` | IfcCompoundPlaneAngleMeasure [deg, min, sec, microsec] β†’ decimal degrees |
115
+ | `longitude` | `IfcSite.RefLongitude` | Same decoding |
116
+
117
+ ### Key functions
118
+
119
+ ```python
120
+ find_ifc_files(root_dir: Path) -> list[Path]
121
+ # Recursively globs *.ifc under root_dir
122
+
123
+ process_ifc_file(ifc_path: Path) -> dict
124
+ # Opens file, calls 4 extractors, returns result dict with all 8 columns
125
+
126
+ extract_window_area(model) -> float | None
127
+ extract_floor_area(model) -> float | None
128
+ extract_roof_area(model) -> float | None
129
+ extract_orientation(model) -> dict # {true_north_angle_deg, latitude, longitude}
130
+
131
+ get_quantity(element, qset_name, qty_name) -> float | None
132
+ # Walks IsDefinedBy β†’ IfcRelDefinesByProperties β†’ IfcElementQuantity
133
+ # Handles both IfcQuantityArea (.AreaValue) and IfcQuantityLength (.LengthValue)
134
+
135
+ get_quantity_multi(element, qset_names: list[str], qty_name) -> float | None
136
+ # Tries each qset name in order β€” handles IFC2x3 vs IFC4 naming differences
137
+
138
+ get_area_scale(model) -> float
139
+ get_length_scale(model) -> float
140
+ # Uses ifcopenshell.util.unit.calculate_unit_scale to handle feet vs metres
141
+
142
+ decode_compound_angle(compound) -> float | None
143
+ # Converts IfcCompoundPlaneAngleMeasure [deg,min,sec,microsec] to decimal degrees
144
+ ```
145
+
146
+ ### Output β€” `ifc_scan_results.csv`
147
+
148
+ 36 rows, 9 columns:
149
+ ```
150
+ project_name, ifc_file, window_area_m2, floor_area_m2, roof_area_m2,
151
+ true_north_angle_deg, latitude, longitude, error
152
+ ```
153
+
154
+ ### Actual scan results (key data)
155
+
156
+ | Project | Window mΒ² | Floor mΒ² | Roof mΒ² | TrueNorthΒ° | Lat | Lon |
157
+ |---|---|---|---|---|---|---|
158
+ | 4351 | 10.87 | N/A | N/A | N/A | N/A | N/A |
159
+ | ac20 | 309.50 | 1939.38 | 688.77 | 0.00 | 49.15 | 8.72 |
160
+ | city_house_munich | 29.51 | N/A | N/A | 32.56 | 48.19 | 11.47 |
161
+ | dental_clinic | 100.63 | N/A | N/A | N/A | 42.36 | -71.06 |
162
+ | digital_hub | 375.00 | 2842.12 | N/A | 360.00 | 48.14 | 11.58 |
163
+ | duplex | 65.94 | N/A | N/A | N/A | 41.87 | -87.64 |
164
+ | ettenheim_gis | 725.97 | N/A | N/A | N/A | N/A | N/A |
165
+ | fantasy_hotel_1 | 58.78 | N/A | N/A | 360.00 | 42.36 | -71.06 |
166
+ | fantasy_hotel_2 | 400.31 | N/A | N/A | 360.00 | 42.36 | -71.06 |
167
+ | fantasy_office_building_1 | 156.82 | 1257.07 | 339.92 | 360.00 | 48.14 | 11.58 |
168
+ | fantasy_office_building_2 | 142.91 | 716.06 | 419.57 | 360.00 | 48.14 | 11.58 |
169
+ | fantasy_office_building_3 | 984.96 | N/A | N/A | 360.00 | 48.15 | 11.57 |
170
+ | fantasy_residential_building_1 | 19.56 | N/A | N/A | 360.00 | 48.14 | 11.58 |
171
+ | fzk_house | 23.17 | 173.34 | 165.12 | 310.00 | 49.10 | 8.44 |
172
+ | hitos | 1531.32 | N/A | N/A | 360.00 | 69.67 | 18.83 |
173
+ | molio | N/A | 140.78 | N/A | 0.00 | 55.67 | 12.62 |
174
+ | samuel_macalister_sample_house | 61.64 | N/A | N/A | 323.00 | 42.21 | -71.03 |
175
+ | schependomlaan | N/A | N/A | N/A | N/A | N/A | N/A |
176
+ | sixty5/arc | 4901.15 | 0.00 | N/A | 0.00 | 51.45 | 5.48 |
177
+ | smiley_west | 95.92 | 1821.17 | 614.41 | 147.50 | 49.03 | 8.39 |
178
+ | wbdg_office | 124.50 | N/A | N/A | N/A | 42.36 | -71.06 |
179
+
180
+ > **Note:** `360.00Β°` true north = same as `0.00Β°` (floating-point rounding artefact near exact north alignment β€” cosmetically different, data is correct). MEP/structural discipline files return N/A for most metrics β€” expected.
181
+
182
+ ### Summary statistics
183
+ - Files scanned: 36, Errors: 0
184
+ - Window data: 19/36 files
185
+ - Floor data: 9/36 files
186
+ - Roof data: 5/36 files
187
+ - Orientation: 25/36 files
188
+
189
+ ---
190
+
191
+ ## File 2: `discover_ifc_keys.py`
192
+
193
+ ### Purpose
194
+ Full metadata key discovery β€” enumerates every property set and quantity set used across all 36 IFC files, grouped by IFC element type. Produces the raw inventory and the canonical alias map.
195
+
196
+ ### Run
197
+ ```bash
198
+ .venv/Scripts/python.exe discover_ifc_keys.py
199
+ ```
200
+
201
+ ### Element types targeted
202
+ `IfcWindow`, `IfcDoor`, `IfcSpace`, `IfcSlab`, `IfcRoof`, `IfcWall`, `IfcWallStandardCase`, `IfcCovering`, `IfcSite`, `IfcBuilding`, `IfcBuildingStorey`
203
+
204
+ ### Key functions
205
+
206
+ ```python
207
+ collect_qsets(model, element_type: str) -> dict
208
+ # Returns {qset_name: {qty_name: count}} for all elements of that type in the model
209
+
210
+ collect_psets(model, element_type: str) -> dict
211
+ # Returns {pset_name: {prop_name: count}} for all elements of that type in the model
212
+
213
+ merge_into(global_inv, element_type, section, file_data, project_name)
214
+ # Accumulates per-file results: increments file_count, appends project_name
215
+
216
+ inventory_to_plain(global_inv) -> dict
217
+ # Converts defaultdict structure to plain dict for JSON serialisation
218
+
219
+ build_aliases(inventory) -> dict
220
+ # Creates key_aliases.json: pre-seeds known patterns + auto-discovers
221
+ # any area-related quantity keys not already covered
222
+ ```
223
+
224
+ ### Discovery results summary
225
+
226
+ | Element Type | Qty Sets | Qty Keys | Prop Sets | Prop Keys |
227
+ |---|---|---|---|---|
228
+ | IfcWindow | 59 | 8,834 | 102 | 20,413 |
229
+ | IfcDoor | 21 | 2,887 | 64 | 10,029 |
230
+ | IfcSpace | 8 | 201 | 38 | 1,315 |
231
+ | IfcSlab | 9 | 144 | 35 | 302 |
232
+ | IfcRoof | 1 | 3 | 25 | 117 |
233
+ | IfcWall | 22 | 234 | 41 | 392 |
234
+ | IfcWallStandardCase | 20 | 146 | 18 | 238 |
235
+ | IfcCovering | 3 | 114 | 33 | 325 |
236
+ | IfcSite | 1 | 2 | 10 | 58 |
237
+ | IfcBuilding | 1 | 1 | 13 | 77 |
238
+ | IfcBuildingStorey | 1 | 4 | 28 | 66 |
239
+
240
+ > **Why IfcWindow has 59 qsets and 8,834 keys:** Archicad creates one unique quantity set per window *type* family (named `AC_Equantity_IFC_Fenster_-_ein_Panel`, `AC_Equantity_R1_21`, etc.), each with its own set of dimension keys. The actual area extraction only needs the 2 standard sets below.
241
+
242
+ ---
243
+
244
+ ## File 3: `key_aliases.json`
245
+
246
+ ### Purpose
247
+ A **human-editable JSON config** that maps each canonical building metric to an ordered list of IFC lookup strategies. Any extractor function should iterate this list and return the first strategy that yields a value.
248
+
249
+ ### Structure
250
+ ```json
251
+ {
252
+ "canonical_key": [
253
+ {
254
+ "entity": "IfcWindow", // IFC element type to query
255
+ "source": "qset", // "qset" (quantity set) or "attr" (direct attribute)
256
+ "set_name": "Qto_WindowBaseQuantities", // quantity set name
257
+ "key": "Area" // quantity name within the set
258
+ },
259
+ ...
260
+ {
261
+ "entity": "IfcWindow",
262
+ "source": "attr", // direct IFC attribute (no set lookup)
263
+ "keys": ["OverallHeight", "OverallWidth"],
264
+ "op": "multiply" // operation to apply
265
+ }
266
+ ]
267
+ }
268
+ ```
269
+
270
+ ### Canonical keys defined
271
+
272
+ #### `window_area`
273
+ | Priority | Entity | Source | Set Name | Key | Notes |
274
+ |---|---|---|---|---|---|
275
+ | 1 | IfcWindow | qset | `Qto_WindowBaseQuantities` | `Area` | IFC4 standard |
276
+ | 2 | IfcWindow | qset | `BaseQuantities` | `Area` | IFC2x3 Archicad/Revit |
277
+ | 3 | IfcWindow | qset | `BaseQuantities` | `GrossArea` | IFC2x3 fallback |
278
+ | 4 | IfcWindow | attr | β€” | `OverallHeight Γ— OverallWidth` | Last resort, multiply length attrs |
279
+
280
+ #### `floor_area`
281
+ | Priority | Entity | Source | Set Name | Key | Filter |
282
+ |---|---|---|---|---|---|
283
+ | 1 | IfcSpace | qset | `Qto_SpaceBaseQuantities` | `NetFloorArea` | β€” |
284
+ | 2 | IfcSpace | qset | `Qto_SpaceBaseQuantities` | `GrossFloorArea` | β€” |
285
+ | 3 | IfcSpace | qset | `BaseQuantities` | `NetFloorArea` | IFC2x3 |
286
+ | 4 | IfcSpace | qset | `BaseQuantities` | `GrossFloorArea` | IFC2x3 |
287
+ | 5 | IfcSpace | qset | `GSA Space Areas` | `GSA BIM Area` | US Revit exports |
288
+ | 6 | IfcSlab | qset | `Qto_SlabBaseQuantities` | `NetArea` | PredefinedType=FLOOR |
289
+ | 7 | IfcSlab | qset | `BaseQuantities` | `NetArea` | PredefinedType=FLOOR |
290
+ | 8 | IfcSlab | qset | `BaseQuantities` | `GrossArea` | PredefinedType=FLOOR |
291
+
292
+ #### `roof_area`
293
+ | Priority | Entity | Source | Set Name | Key | Filter |
294
+ |---|---|---|---|---|---|
295
+ | 1 | IfcRoof | qset | `Qto_RoofBaseQuantities` | `NetArea` | β€” |
296
+ | 2 | IfcRoof | qset | `Qto_RoofBaseQuantities` | `GrossArea` | β€” |
297
+ | 3 | IfcRoof | qset | `BaseQuantities` | `NetArea` | IFC2x3 |
298
+ | 4 | IfcSlab | qset | `Qto_SlabBaseQuantities` | `NetArea` | PredefinedType=ROOF |
299
+ | 5 | IfcSlab | qset | `Qto_SlabBaseQuantities` | `GrossArea` | PredefinedType=ROOF |
300
+ | 6 | IfcSlab | qset | `BaseQuantities` | `NetArea` | PredefinedType=ROOF |
301
+ | 7 | IfcSlab | qset | `BaseQuantities` | `GrossArea` | PredefinedType=ROOF |
302
+
303
+ #### `true_north_angle`
304
+ - Entity: `IfcGeometricRepresentationContext`
305
+ - Source: `attr` β†’ `TrueNorth.DirectionRatios (X, Y)`
306
+ - Conversion: `compass_bearing = (-atan2(X, Y) in degrees) % 360`
307
+ - Meaning: degrees clockwise from north (0Β° = north aligned with +Y axis)
308
+
309
+ #### `latitude` / `longitude`
310
+ - Entity: `IfcSite`
311
+ - Source: `attr` β†’ `RefLatitude` / `RefLongitude`
312
+ - Format: `IfcCompoundPlaneAngleMeasure` = tuple of `[degrees, minutes, seconds, microseconds]`
313
+ - Conversion: `decimal = deg + min/60 + sec/3600 + microsec/3_600_000_000`
314
+
315
+ #### `_auto_discovered_area_keys`
316
+ Any quantity keys containing the word "area" found in the inventory that aren't already in the 6 canonical keys above are appended here automatically by `build_aliases()`. Review these manually to see if any should be promoted to a canonical key.
317
+
318
+ ---
319
+
320
+ ## File 4: `ifc_key_inventory.json`
321
+
322
+ Full raw inventory of every property/quantity key found across all 36 IFC files. Structure:
323
+
324
+ ```json
325
+ {
326
+ "IfcWindow": {
327
+ "quantity_sets": {
328
+ "Qto_WindowBaseQuantities": {
329
+ "Area": { "file_count": 12, "projects": ["ac20", "digital_hub", ...] },
330
+ "Height": { "file_count": 12, "projects": [...] },
331
+ "Width": { "file_count": 12, "projects": [...] }
332
+ },
333
+ "BaseQuantities": {
334
+ "Area": { "file_count": 7, "projects": [...] },
335
+ "GrossArea": { "file_count": 4, "projects": ["duplex", ...] }
336
+ }
337
+ },
338
+ "property_sets": {
339
+ "Pset_WindowCommon": {
340
+ "IsExternal": { "file_count": 18, "projects": [...] },
341
+ "ThermalTransmittance": { "file_count": 8, "projects": [...] }
342
+ }
343
+ }
344
+ },
345
+ "IfcSpace": { ... },
346
+ ...
347
+ }
348
+ ```
349
+
350
+ Use this file as a reference when adding new canonical keys to `key_aliases.json`.
351
+
352
+ ---
353
+
354
+ ## IFC Exporter Ecosystem Found in the Dataset
355
+
356
+ | Exporter | Quantity Set Style | Property Set Style | Files |
357
+ |---|---|---|---|
358
+ | **Archicad (German IFC4)** | `BaseQuantities` + per-type `AC_Equantity_*` | `AC_Pset_*`, `ArchiCADProperties` | ac20, fzk_house |
359
+ | **Archicad (Dutch IFC2x3)** | `ArchiCADQuantities`, `BaseQuantities` | Same | sixty5 |
360
+ | **Revit (IFC2x3)** | `BaseQuantities`, `GSA Space Areas` | `PSet_Revit_*` | duplex, dental_clinic, wbdg_office |
361
+ | **IFC4 modern tools** | `Qto_*BaseQuantities` | `Pset_*Common` | digital_hub, schependomlaan |
362
+ | **IFC4X3 (latest)** | `Qto_*BaseQuantities` | `Pset_*Common` | city_house_munich |
363
+ | **Norwegian tool (hitos)** | Material-named sets (`Bindingsverk`, `Isolasjon`) | β€” | hitos |
364
+ | **Synchro 4D** | β€” | `SynchroResourceProperty` | some |
365
+
366
+ ---
367
+
368
+ ## IFC Schema Versions in Dataset
369
+
370
+ | Schema | Files | Example Projects |
371
+ |---|---|---|
372
+ | IFC2X3 | ~20 | 4351, duplex, schependomlaan, sixty5, hitos, molio, wbdg_office |
373
+ | IFC4 | ~14 | ac20, digital_hub, fzk_house, smiley_west, fantasy_* |
374
+ | IFC4X3 | 1 | city_house_munich |
375
+
376
+ ---
377
+
378
+ ## Key IFC Concepts for the Next Agent
379
+
380
+ ### How properties are stored in IFC
381
+
382
+ ```
383
+ IfcWindow
384
+ └── IsDefinedBy (list of IfcRelDefinesByProperties)
385
+ └── RelatingPropertyDefinition
386
+ β”œβ”€β”€ IfcPropertySet (name="Pset_WindowCommon")
387
+ β”‚ └── HasProperties β†’ [IfcPropertySingleValue, ...]
388
+ β”‚ └── .Name = "IsExternal", .NominalValue = True
389
+ └── IfcElementQuantity (name="Qto_WindowBaseQuantities")
390
+ └── Quantities β†’ [IfcQuantityArea, IfcQuantityLength, ...]
391
+ └── .Name = "Area", .AreaValue = 2.5
392
+ ```
393
+
394
+ ### How orientation is stored
395
+
396
+ ```
397
+ IfcGeometricRepresentationContext
398
+ └── TrueNorth β†’ IfcDirection
399
+ └── DirectionRatios = (X, Y) # 2D vector pointing toward geographic north
400
+ # (0, 1) = north is +Y (no rotation)
401
+ # (1, 0) = north is +X (building rotated 90Β° CCW)
402
+ # compass_bearing = (-atan2(X,Y) in degrees) % 360
403
+ ```
404
+
405
+ ### How location is stored
406
+
407
+ ```
408
+ IfcSite
409
+ β”œβ”€β”€ RefLatitude β†’ IfcCompoundPlaneAngleMeasure = (51, 27, 0, 0) # 51Β°27'N
410
+ └── RefLongitude β†’ IfcCompoundPlaneAngleMeasure = (5, 29, 0, 0) # 5Β°29'E
411
+ ```
412
+
413
+ ### Unit handling
414
+
415
+ Always check the model's units before using extracted values:
416
+ ```python
417
+ import ifcopenshell.util.unit as ifc_unit_util
418
+ area_scale = ifc_unit_util.calculate_unit_scale(model, "AREAMEASURE")
419
+ length_scale = ifc_unit_util.calculate_unit_scale(model, "LENGTHUNIT")
420
+ # US Revit models may use square feet β†’ area_scale β‰ˆ 0.0929
421
+ ```
422
+
423
+ ---
424
+
425
+ ## How to Use `key_aliases.json` in a New Extractor
426
+
427
+ ```python
428
+ import json
429
+ import ifcopenshell
430
+ import ifcopenshell.util.unit as ifc_unit_util
431
+ import math
432
+
433
+ # Load the alias map once
434
+ with open("key_aliases.json") as f:
435
+ ALIASES = json.load(f)
436
+
437
+ def extract_metric(model, canonical_key: str) -> float | None:
438
+ """
439
+ Generic extractor that uses key_aliases.json to find a metric
440
+ regardless of which IFC exporter created the file.
441
+ """
442
+ aliases = ALIASES.get(canonical_key, [])
443
+ area_scale = ifc_unit_util.calculate_unit_scale(model, "AREAMEASURE")
444
+ length_scale = ifc_unit_util.calculate_unit_scale(model, "LENGTHUNIT")
445
+ total = 0.0
446
+ found = False
447
+
448
+ for alias in aliases:
449
+ entity = alias["entity"]
450
+ source = alias["source"]
451
+ pred_req = alias.get("predefined_type")
452
+
453
+ for elem in model.by_type(entity):
454
+ # Filter by PredefinedType if required
455
+ if pred_req and getattr(elem, "PredefinedType", None) != pred_req:
456
+ continue
457
+
458
+ if source == "qset":
459
+ val = get_quantity(elem, alias["set_name"], alias["key"])
460
+ if val is not None:
461
+ total += val * area_scale
462
+ found = True
463
+
464
+ elif source == "attr" and alias.get("op") == "multiply":
465
+ vals = [getattr(elem, k, None) for k in alias["keys"]]
466
+ if all(v is not None for v in vals):
467
+ product = 1.0
468
+ for v in vals:
469
+ product *= float(v)
470
+ total += product * (length_scale ** 2)
471
+ found = True
472
+
473
+ if found:
474
+ return round(total, 4) # Return on first successful strategy
475
+
476
+ return None
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Known Data Quality Notes
482
+
483
+ | Project | Issue | Explanation |
484
+ |---|---|---|
485
+ | `sixty5/arc.ifc` | `floor_area = 0.00` (not N/A) | Qto_SpaceBaseQuantities exists but values are 0 β€” model may lack space boundaries |
486
+ | `sixty5/str.ifc` | `floor_area = 34,199 mΒ²` | Structural slabs classified as FLOOR β€” over-counting, architectural file preferred |
487
+ | `schependomlaan/arc.ifc` | All N/A | IFC2x3 model has no quantity sets β€” geometry only |
488
+ | TrueNorth `360.00Β°` | Same as `0.00Β°` | Floating-point artefact when vector X is tiny negative; `(-Ξ΅) % 360 β‰ˆ 360` |
489
+ | `ettenheim_gis/city.ifc` | Windows = 725.97 mΒ² | GIS city model β€” likely summing openings across many buildings, not a single building |
490
+ | MEP/structural discipline files | All N/A | Correct β€” these files don't contain architectural elements |
491
+
492
+ ---
493
+
494
+ ## Running Everything
495
+
496
+ ```bash
497
+ # Activate venv (Windows)
498
+ .venv\Scripts\activate
499
+
500
+ # Run the 4-metric scanner
501
+ python scan_ifc_models.py
502
+ # β†’ prints table to console
503
+ # β†’ writes ifc_scan_results.csv
504
+ # β†’ writes ifc_scan.log
505
+
506
+ # Re-run the key discovery (if new IFC files are added)
507
+ python discover_ifc_keys.py
508
+ # β†’ overwrites ifc_key_inventory.json
509
+ # β†’ overwrites key_aliases.json
510
+ # β†’ prints full inventory to console
511
+ ```
512
+
513
+ ---
514
+
515
+ ## Suggested Next Steps
516
+
517
+ 1. **Extend `key_aliases.json`** β€” add more canonical keys (e.g., `wall_area`, `door_count`, `storey_height`, `building_height`) using the inventory as a reference
518
+ 2. **Handle `schependomlaan`** β€” this IFC2x3 model has no quantity sets; area must be computed from geometry using `ifcopenshell.geom`
519
+ 3. **Window-to-floor ratio** β€” divide `window_area` by `floor_area` per project for glazing ratio analysis
520
+ 4. **Solar orientation** β€” combine `true_north_angle` with `latitude` for solar exposure estimation
521
+ 5. **Energy analysis** β€” roof/floor ratio and glazing ratio are key inputs for energy simulation
522
+ 6. **Database ingestion** β€” load `ifc_scan_results.csv` into a dataframe or database for ML feature extraction
523
+
524
+ ---
525
+
526
+ *Report generated by Claude Sonnet 4.6 (claude-sonnet-4-6) on 2026-02-18*
teams/lux-ai/API function/SOLAR_PRODUCTION_PIPELINE.md ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # β˜€οΈ Solar Production Pipeline β€” Full Documentation
2
+
3
+ ## 1. Project Overview
4
+
5
+ This pipeline estimates the **annual solar energy production** of a building's roof by:
6
+
7
+ 1. **Parsing** an IFC (Industry Foundation Classes) file to extract roof geometry
8
+ 2. **Segmenting** the roof into logical surfaces based on orientation
9
+ 3. **Querying** the NREL PVWatts v8 API for each segment's solar yield
10
+ 4. **Aggregating** results for a total building-level production estimate (kWh/yr)
11
+
12
+ This per-segment approach is critical for **LEED certification** accuracy β€” a south-facing surface may produce 2.5Γ— more energy than a north-facing one on the same roof.
13
+
14
+ ---
15
+
16
+ ## 2. Architecture
17
+
18
+ ```
19
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
20
+ β”‚ IFC File (.ifc) β”‚
21
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
22
+ β”‚
23
+ β–Ό
24
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
25
+ β”‚ ifc_roof_parser.py β”‚
26
+ β”‚ β”‚
27
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
28
+ β”‚ β”‚ get_roof_elements(model) β”‚ β”‚
29
+ β”‚ β”‚ β†’ finds IfcRoof + decomposed slabs β”‚ β”‚
30
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
31
+ β”‚ β”‚ β”‚
32
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
33
+ β”‚ β”‚ extract_geometry(element, settings) β”‚ β”‚
34
+ β”‚ β”‚ β†’ triangulated mesh (verts/faces) β”‚ β”‚
35
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
36
+ β”‚ β”‚ β”‚
37
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
38
+ β”‚ β”‚ cluster_faces_by_normal(normals, β”‚ β”‚
39
+ β”‚ β”‚ areas, angle_tolerance=15Β°) β”‚ β”‚
40
+ β”‚ β”‚ β†’ groups triangles by orientation β”‚ β”‚
41
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
42
+ β”‚ β”‚ β”‚
43
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
44
+ β”‚ β”‚ compute_segment_properties(normals, β”‚ β”‚
45
+ β”‚ β”‚ areas, cluster_indices) β”‚ β”‚
46
+ β”‚ β”‚ β†’ tilt, azimuth, total area β”‚ β”‚
47
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
48
+ β”‚ β”‚ β”‚
49
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
50
+ β”‚ β”‚ parse_roof_segments(ifc_path) β”‚ β”‚
51
+ β”‚ β”‚ β†’ list[{id, area, tilt, azimuth}] β”‚ β”‚
52
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
53
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
54
+ β”‚
55
+ β–Ό list of segment dicts
56
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
57
+ β”‚ solar_production_engine.py β”‚
58
+ β”‚ β”‚
59
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
60
+ β”‚ β”‚ Location(lat, lon, name) β”‚ β”‚
61
+ β”‚ β”‚ β†’ dataclass for site coordinates β”‚ β”‚
62
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
63
+ β”‚ β”‚
64
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
65
+ β”‚ β”‚ calculate_segment_production( β”‚ β”‚
66
+ β”‚ β”‚ area, tilt, azimuth, location) β”‚ β”‚
67
+ β”‚ β”‚ β†’ annual kWh for one segment β”‚ β”‚
68
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
69
+ β”‚ β”‚ β”‚
70
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
71
+ β”‚ β”‚ run_production_analysis( β”‚ β”‚
72
+ β”‚ β”‚ segments, location) β”‚ β”‚
73
+ β”‚ β”‚ β†’ total kWh + per-segment results β”‚ β”‚
74
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
75
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
76
+ β”‚
77
+ β–Ό
78
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
79
+ β”‚ run_solar_analysis.py (orchestrator) β”‚
80
+ β”‚ β”‚
81
+ β”‚ 1. Reads IFC path (CLI arg or default) β”‚
82
+ β”‚ 2. Calls parse_roof_segments() β”‚
83
+ β”‚ 3. Calls run_production_analysis() β”‚
84
+ β”‚ 4. Prints full report + LEED estimate β”‚
85
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 3. Files Created
91
+
92
+ ### 3.1 `app/src/ifc_roof_parser.py` β€” IFC Geometry Parser
93
+
94
+ **Purpose:** Extract roof segments (area, tilt, azimuth) from any IFC file by analyzing 3D geometry.
95
+
96
+ | Function | Input | Output | Description |
97
+ |---|---|---|---|
98
+ | `get_roof_elements(model)` | `ifcopenshell.file` | `list[IfcElement]` | Finds all roof-related elements: `IfcRoof` entities, their decomposed `IfcSlab` children (via `IfcRelAggregates`), and standalone `IfcSlab` entities with `.ROOF.` predefined type |
99
+ | `extract_geometry(element, settings)` | IFC element + geom settings | `(vertices, faces)` as numpy arrays | Uses `ifcopenshell.geom.create_shape()` with `USE_WORLD_COORDS=True` to get triangulated mesh data |
100
+ | `compute_face_normals(vertices, faces)` | numpy arrays | `(normals, areas)` β€” Nx3 unit normals + N areas | Computes cross-product normals and triangle areas for every face in the mesh |
101
+ | `cluster_faces_by_normal(normals, areas, angle_tolerance)` | normals, areas, tolerance (default 15Β°) | `list[list[int]]` β€” cluster indices | Groups triangles whose normals are within `angle_tolerance` degrees of each other. Only considers **upward-facing** triangles (normal Z > 0). Uses greedy angular clustering. |
102
+ | `compute_segment_properties(normals, areas, cluster_indices)` | normals, areas, index groups | `dict{area, tilt, azimuth}` | Calculates **area-weighted average normal** per cluster, then derives: **tilt** = `arccos(nz)` in degrees, **azimuth** = `atan2(nx, ny) mod 360` in degrees |
103
+ | `parse_roof_segments(ifc_path)` | file path string | `list[{"id", "area", "tilt", "azimuth"}]` | **Main entry point.** Orchestrates all the above. Returns the standardized segment list that the production engine consumes. |
104
+
105
+ **Key Design Decisions:**
106
+ - **Geometry-based extraction** β€” does NOT rely on IFC property sets (`PitchAngle`, etc.) because these are unreliably populated across different BIM authoring tools
107
+ - **Clustering by face normals** β€” handles curved roofs (barrel vaults β†’ many normals) and planar roofs (hip/gable β†’ few distinct normals) with the same algorithm
108
+ - **Filters downward-facing triangles** β€” eliminates soffit/interior faces that aren't solar-relevant
109
+ - **Minimum area threshold** β€” ignores clusters smaller than 1 mΒ² to filter geometric noise
110
+
111
+ ---
112
+
113
+ ### 3.2 `app/src/solar_production_engine.py` β€” PVWatts API Client
114
+
115
+ **Purpose:** Calculate annual solar energy production (kWh) for roof segments using the NREL PVWatts v8 API.
116
+
117
+ | Component | Type | Description |
118
+ |---|---|---|
119
+ | `Location` | `@dataclass` | Stores `latitude: float`, `longitude: float`, `name: str` for a project site |
120
+ | `API_KEY` | `str` constant | NREL API key (`0zwEIS1a...` β€” production key) |
121
+ | `BASE_URL` | `str` constant | `https://developer.nrel.gov/api/pvwatts/v8.json` |
122
+ | `calculate_segment_production(area, tilt, azimuth, location)` | function | Computes `system_capacity = area Γ— 0.20` (20% panel efficiency = 1 kW per 5 mΒ²), calls PVWatts API with fixed roof mount settings, returns annual AC output in kWh |
123
+ | `run_production_analysis(segments, location)` | function | Iterates a segment list, calls `calculate_segment_production()` for each, returns `(total_kwh, per_segment_results)` |
124
+ | `main()` | function | Standalone mode with hardcoded dummy segments for testing |
125
+
126
+ **API Parameters Sent:**
127
+
128
+ | Parameter | Value | Meaning |
129
+ |---|---|---|
130
+ | `system_capacity` | `area Γ— 0.20` | kW capacity based on roof area |
131
+ | `azimuth` | from segment | Compass bearing (180Β° = south) |
132
+ | `tilt` | from segment | Angle from horizontal in degrees |
133
+ | `array_type` | `1` | Fixed β€” roof mount |
134
+ | `module_type` | `1` | Premium (monocrystalline) |
135
+ | `losses` | `14` | System losses (wiring, soiling, etc.) |
136
+
137
+ **Error Handling:**
138
+ - `requests.RequestException` β†’ prints error, returns 0 kWh for that segment
139
+ - `KeyError` on missing `outputs` β†’ prints API error response, returns 0
140
+ - `response.raise_for_status()` β†’ catches HTTP 4xx/5xx errors
141
+ - `time.sleep(1)` between API calls β†’ respects rate limits
142
+
143
+ ---
144
+
145
+ ### 3.3 `app/src/run_solar_analysis.py` β€” Orchestrator
146
+
147
+ **Purpose:** Glue script that connects the parser to the production engine.
148
+
149
+ | Component | Description |
150
+ |---|---|
151
+ | `SITE` | Hardcoded `Location(41.38, 2.17, "Barcelona_Project_Alpha")` β€” to be parameterized later |
152
+ | `DEFAULT_IFC` | Points to `00_data/ifc_models/Ifc4_SampleHouse_1_Roof.ifc` |
153
+ | CLI support | Accepts optional IFC file path as first argument: `python run_solar_analysis.py path/to/file.ifc` |
154
+ | Report output | Prints per-segment table + total production + LEED score example |
155
+
156
+ ---
157
+
158
+ ### 3.4 `requirements.txt` β€” Updated Dependencies
159
+
160
+ Added: `requests>=2.28.0` (required by the production engine for API calls)
161
+
162
+ Already present: `ifcopenshell`, `numpy`, `trimesh` (used by the parser)
163
+
164
+ ---
165
+
166
+ ## 4. Inputs & Outputs
167
+
168
+ ### Inputs
169
+
170
+ | Input | Format | Source | Required |
171
+ |---|---|---|---|
172
+ | **IFC file** | `.ifc` (IFC2x3 or IFC4) | BIM authoring tool (Revit, ArchiCAD, etc.) | βœ… |
173
+ | **Site location** | Latitude/Longitude | Hardcoded (future: extracted from `IfcSite`) | βœ… |
174
+ | **NREL API key** | String | `api.nrel.gov` registration | βœ… |
175
+
176
+ ### Outputs
177
+
178
+ | Output | Format | Description |
179
+ |---|---|---|
180
+ | **Per-segment data** | `list[dict]` | `{"id": str, "area": float, "tilt": float, "azimuth": float}` |
181
+ | **Per-segment yield** | `float` | Annual AC energy production in kWh/yr per roof surface |
182
+ | **Total building production** | `float` | Sum of all segment yields in kWh/yr |
183
+ | **LEED score estimate** | `float` | `(Total Production / Assumed Consumption) Γ— 100` |
184
+
185
+ ### Example Output (SampleHouse IFC)
186
+
187
+ ```
188
+ === SOLAR PRODUCTION ANALYSIS REPORT ===
189
+ Site: Barcelona_Project_Alpha (41.38Β°N, 2.17Β°E)
190
+ IFC: Ifc4_SampleHouse_1_Roof.ifc
191
+
192
+ --- Roof Segments Detected ---
193
+ Roof_Seg_01 | Area: 71.48 mΒ² | Tilt: 10.3Β° | Azimuth: 180Β° | Capacity: 14.3 kW | Yield: 19,063 kWh/yr
194
+ Roof_Seg_02 | Area: 45.44 mΒ² | Tilt: 27.9Β° | Azimuth: 0Β° | Capacity: 9.1 kW | Yield: 7,566 kWh/yr
195
+
196
+ --- Summary ---
197
+ Total Roof Area: 116.92 mΒ²
198
+ Total System Capacity: 23.4 kW
199
+ TOTAL ANNUAL PRODUCTION: 26,629 kWh/yr
200
+
201
+ --- LEED Estimate ---
202
+ Assumed consumption: 50,000 kWh/yr
203
+ Renewable coverage: 53.3%
204
+ Score = Total Production / Consumption Γ— 100
205
+ ```
206
+
207
+ ---
208
+
209
+ ## 5. IFC Compatibility
210
+
211
+ ### Does the parser work for different IFC files?
212
+
213
+ **Yes, with the following conditions:**
214
+
215
+ | IFC Feature | Supported | How |
216
+ |---|---|---|
217
+ | **IFC4** (`.ifc`) | βœ… | Tested with SampleHouse |
218
+ | **IFC2x3** (`.ifc`) | βœ… | Same `ifcopenshell.geom` API; element types are identical |
219
+ | **IfcRoof (monolithic)** | βœ… | Geometry is clustered by face normals into logical segments |
220
+ | **IfcRoof β†’ IfcSlab (decomposed)** | βœ… | Sub-slabs found via `IfcRelAggregates`; each processed individually |
221
+ | **IfcSlab with `.ROOF.` type** | βœ… | Standalone roof slabs detected by predefined type |
222
+ | **Barrel vault / curved roofs** | βœ… | Many triangle normals β†’ clustering groups them into ~2-4 orientations |
223
+ | **Hip / gable roofs** | βœ… | Few distinct planar groups β†’ clustering identifies each face |
224
+ | **Flat roofs** | βœ… | All normals β‰ˆ (0,0,1) β†’ single segment with tilt β‰ˆ 0Β° |
225
+ | **IFC-XML** (`.ifcXML`) | ⚠️ Partial | ifcopenshell can open it, but geometry extraction may differ |
226
+ | **IFC-ZIP** (`.ifcZIP`) | ❌ | Must be extracted first |
227
+ | **No roof elements** | βœ… Handled | Returns empty list with warning message |
228
+ | **Roof with skylights/openings** | ⚠️ | Openings not subtracted from area yet β€” future enhancement |
229
+
230
+ ### Potential Variations Across BIM Software
231
+
232
+ | BIM Tool | Typical IFC Export | Parser Behavior |
233
+ |---|---|---|
234
+ | **Autodesk Revit** | IfcRoof as single Brep; sub-slabs sometimes decomposed | βœ… Both paths handled |
235
+ | **ArchiCAD** | IfcRoof with clean decomposition into IfcSlab children | βœ… Ideal case |
236
+ | **Tekla / Trimble** | IfcSlab with `.ROOF.` type (no IfcRoof parent) | βœ… Caught by type scan |
237
+ | **Blender (BlenderBIM)** | Varies β€” may use IfcRoof or IfcSlab | βœ… Both paths handled |
238
+ | **Generic/Unknown** | May have non-standard geometry representations | ⚠️ Falls back to Brep triangulation |
239
+
240
+ ---
241
+
242
+ ## 6. Math Reference
243
+
244
+ ### System Capacity
245
+
246
+ $$P_{capacity} = A_{segment} \times 0.20 \text{ kW}$$
247
+
248
+ Where 0.20 = 20% module efficiency (1 kW per 5 mΒ²)
249
+
250
+ ### Tilt from Normal Vector
251
+
252
+ $$\theta_{tilt} = \arccos(n_z) \times \frac{180}{\pi}$$
253
+
254
+ ### Azimuth from Normal Vector
255
+
256
+ $$\phi_{azimuth} = \text{atan2}(n_x, n_y) \mod 360Β°$$
257
+
258
+ Where 0Β° = North, 90Β° = East, 180Β° = South, 270Β° = West
259
+
260
+ ### Area-Weighted Average Normal (per cluster)
261
+
262
+ $$\vec{n}_{avg} = \frac{\sum_{i \in cluster} A_i \cdot \vec{n}_i}{\left\| \sum_{i \in cluster} A_i \cdot \vec{n}_i \right\|}$$
263
+
264
+ ### LEED Renewable Energy Score
265
+
266
+ $$Score = \frac{\sum_{s=1}^{N} P_{s}}{C_{total}} \times 100$$
267
+
268
+ Where $P_s$ = annual production of segment $s$, $C_{total}$ = total building consumption
269
+
270
+ ---
271
+
272
+ ## 7. How to Run
273
+
274
+ ### Full Pipeline (IFC β†’ Solar Report)
275
+
276
+ ```bash
277
+ cd app/src
278
+ python run_solar_analysis.py
279
+ ```
280
+
281
+ With a custom IFC file:
282
+
283
+ ```bash
284
+ python run_solar_analysis.py "C:\path\to\your\model.ifc"
285
+ ```
286
+
287
+ ### Parser Only (IFC β†’ Segment List)
288
+
289
+ ```python
290
+ from ifc_roof_parser import parse_roof_segments
291
+
292
+ segments = parse_roof_segments("path/to/model.ifc")
293
+ for seg in segments:
294
+ print(f"{seg['id']}: {seg['area']:.1f} mΒ², tilt={seg['tilt']:.1f}Β°, azimuth={seg['azimuth']:.0f}Β°")
295
+ ```
296
+
297
+ ### Production Engine Only (Segments β†’ kWh)
298
+
299
+ ```python
300
+ from solar_production_engine import Location, run_production_analysis
301
+
302
+ segments = [
303
+ {"id": "South_Face", "area": 120, "tilt": 30, "azimuth": 180},
304
+ {"id": "East_Face", "area": 85, "tilt": 15, "azimuth": 90},
305
+ ]
306
+ site = Location(latitude=41.38, longitude=2.17, name="Barcelona")
307
+
308
+ total, results = run_production_analysis(segments, site)
309
+ print(f"Total: {total:,.0f} kWh/yr")
310
+ ```
311
+
312
+ ---
313
+
314
+ ## 8. Future Enhancements
315
+
316
+ | Enhancement | Impact | Difficulty |
317
+ |---|---|---|
318
+ | Extract location from `IfcSite` lat/lon automatically | Eliminates hardcoded coordinates | Low |
319
+ | Subtract skylight/opening areas from roof segments | More accurate area calculation | Medium |
320
+ | Add `pvlib` as offline fallback (no API dependency) | Works without internet | Medium |
321
+ | Export results to JSON/CSV for dashboard integration | Data pipeline ready | Low |
322
+ | Add panel layout optimizer (avoid low-yield segments) | Cost optimization | Medium |
323
+ | Support multiple buildings in one IFC file | Campus-scale analysis | Medium |
324
+ | Read building consumption from IFC `Pset_SpaceCommon` | Auto-calculate LEED score | High |
325
+
326
+ ---
327
+
328
+ ## 9. Dependencies
329
+
330
+ | Package | Version | Used By | Purpose |
331
+ |---|---|---|---|
332
+ | `ifcopenshell` | β‰₯ 0.7.0 | `ifc_roof_parser.py` | Open and query IFC files |
333
+ | `numpy` | β‰₯ 1.21.0 | `ifc_roof_parser.py` | Geometry math (normals, areas, clustering) |
334
+ | `requests` | β‰₯ 2.28.0 | `solar_production_engine.py` | HTTP calls to NREL PVWatts API |
335
+ | `trimesh` | β‰₯ 3.0.0 | `ifc_roof_parser.py` | (Optional) mesh utilities β€” currently using raw numpy |
336
+
337
+ ---
338
+
339
+ ## 10. File Tree
340
+
341
+ ```
342
+ iaac-bimwise-starter/
343
+ β”œβ”€β”€ 00_data/
344
+ β”‚ └── ifc_models/
345
+ β”‚ └── Ifc4_SampleHouse_1_Roof.ifc ← Test IFC file (barrel vault, London)
346
+ β”œβ”€β”€ app/
347
+ β”‚ └── src/
348
+ β”‚ β”œβ”€β”€ ifc_roof_parser.py ← NEW: IFC geometry β†’ roof segments
349
+ β”‚ β”œβ”€β”€ solar_production_engine.py ← NEW: Segments β†’ PVWatts API β†’ kWh
350
+ β”‚ β”œβ”€β”€ run_solar_analysis.py ← NEW: Orchestrator (parser + engine)
351
+ β”‚ β”œβ”€β”€ ifc_checker.py ← Existing: Basic IFC validation
352
+ β”‚ └── ifc_visualizer.py ← Existing: 3D IFC viewer
353
+ β”œβ”€β”€ docs/
354
+ β”‚ └── SOLAR_PRODUCTION_PIPELINE.md ← This file
355
+ └── requirements.txt ← Updated with requests>=2.28.0
356
+ ```
teams/lux-ai/API function/run_solar_analysis.py ADDED
File without changes
teams/lux-ai/API function/scan_ifc_models.py ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ scan_ifc_models.py
3
+ Scans all IFC files in the Sample projects directory and extracts:
4
+ - Window area (mΒ²)
5
+ - Floor area (mΒ²)
6
+ - Roof area (mΒ²)
7
+ - Orientation (true north angle, latitude, longitude)
8
+
9
+ Output: ifc_scan_results.csv + console table + ifc_scan.log
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import csv
16
+ import logging
17
+ import math
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ import ifcopenshell
22
+ import ifcopenshell.util.element as ifc_util
23
+ import ifcopenshell.util.unit as ifc_unit_util
24
+ from tabulate import tabulate
25
+
26
+
27
+ # ── Logging ───────────────────────────────────────────────────────────────────
28
+
29
+ logging.basicConfig(
30
+ level=logging.INFO,
31
+ format="%(levelname)-8s %(message)s",
32
+ handlers=[
33
+ logging.StreamHandler(),
34
+ logging.FileHandler(
35
+ Path(__file__).parent / "ifc_scan.log", mode="w", encoding="utf-8"
36
+ ),
37
+ ],
38
+ )
39
+ log = logging.getLogger(__name__)
40
+
41
+ # ── Constants ─────────────────────────────────────────────────────────────────
42
+
43
+ CSV_COLUMNS = [
44
+ "project_name",
45
+ "ifc_file",
46
+ "window_area_m2",
47
+ "floor_area_m2",
48
+ "roof_area_m2",
49
+ "true_north_angle_deg",
50
+ "latitude",
51
+ "longitude",
52
+ "error",
53
+ ]
54
+
55
+ WINDOW_QSETS = ["Qto_WindowBaseQuantities", "BaseQuantities"]
56
+ SPACE_QSETS = ["Qto_SpaceBaseQuantities", "BaseQuantities"]
57
+ SLAB_QSETS = ["Qto_SlabBaseQuantities", "BaseQuantities"]
58
+ ROOF_QSETS = ["Qto_RoofBaseQuantities", "BaseQuantities"]
59
+
60
+
61
+ # ── Discovery ─────────────────────────────────────────────────────────────────
62
+
63
+ def find_ifc_files(root_dir: Path) -> list[Path]:
64
+ """Recursively find all .ifc files under root_dir, sorted by path."""
65
+ files = sorted(root_dir.rglob("*.ifc"))
66
+ return files
67
+
68
+
69
+ # ── Unit helpers ──────────────────────────────────────────────────────────────
70
+
71
+ def get_length_scale(model: ifcopenshell.file) -> float:
72
+ """Return scale factor to convert model length units to metres."""
73
+ try:
74
+ return ifc_unit_util.calculate_unit_scale(model, "LENGTHUNIT")
75
+ except Exception:
76
+ return 1.0
77
+
78
+
79
+ def get_area_scale(model: ifcopenshell.file) -> float:
80
+ """Return scale factor to convert model area units to mΒ²."""
81
+ try:
82
+ return ifc_unit_util.calculate_unit_scale(model, "AREAMEASURE")
83
+ except Exception:
84
+ return 1.0
85
+
86
+
87
+ # ── Quantity extraction helpers ───────────────────────────────────────────────
88
+
89
+ def get_quantity(element, qset_name: str, qty_name: str) -> Optional[float]:
90
+ """
91
+ Walk IsDefinedBy relationships to find an IfcElementQuantity named
92
+ qset_name and return the numeric value of qty_name within it.
93
+ Returns None if not found.
94
+ """
95
+ for rel in getattr(element, "IsDefinedBy", []):
96
+ if not rel.is_a("IfcRelDefinesByProperties"):
97
+ continue
98
+ qset = rel.RelatingPropertyDefinition
99
+ if not qset.is_a("IfcElementQuantity"):
100
+ continue
101
+ if qset.Name != qset_name:
102
+ continue
103
+ for qty in qset.Quantities:
104
+ if qty.Name != qty_name:
105
+ continue
106
+ if hasattr(qty, "AreaValue") and qty.AreaValue is not None:
107
+ return float(qty.AreaValue)
108
+ if hasattr(qty, "LengthValue") and qty.LengthValue is not None:
109
+ return float(qty.LengthValue)
110
+ if hasattr(qty, "VolumeValue") and qty.VolumeValue is not None:
111
+ return float(qty.VolumeValue)
112
+ return None
113
+
114
+
115
+ def get_quantity_multi(
116
+ element, qset_names: list[str], qty_name: str
117
+ ) -> Optional[float]:
118
+ """Try multiple quantity set names in order, return first match."""
119
+ for qset_name in qset_names:
120
+ val = get_quantity(element, qset_name, qty_name)
121
+ if val is not None:
122
+ return val
123
+ return None
124
+
125
+
126
+ # ── Four metric extractors ────────────────────────────────────────────────────
127
+
128
+ def extract_window_area(model: ifcopenshell.file) -> Optional[float]:
129
+ """
130
+ Total window area in mΒ².
131
+ Strategy 1: Qto_WindowBaseQuantities / Area (IFC4) or BaseQuantities / Area (IFC2x3)
132
+ Strategy 2: OverallHeight Γ— OverallWidth (direct attributes)
133
+ """
134
+ windows = model.by_type("IfcWindow")
135
+ if not windows:
136
+ return None
137
+
138
+ area_scale = get_area_scale(model)
139
+ length_scale = get_length_scale(model)
140
+ total = 0.0
141
+ found_any = False
142
+
143
+ for win in windows:
144
+ # Strategy 1 β€” quantity set
145
+ area = get_quantity_multi(win, WINDOW_QSETS, "Area")
146
+ if area is not None:
147
+ total += area * area_scale
148
+ found_any = True
149
+ continue
150
+
151
+ # Strategy 2 β€” direct attributes
152
+ h = getattr(win, "OverallHeight", None)
153
+ w = getattr(win, "OverallWidth", None)
154
+ if h and w:
155
+ total += float(h) * float(w) * (length_scale ** 2)
156
+ found_any = True
157
+
158
+ return round(total, 4) if found_any else None
159
+
160
+
161
+ def extract_floor_area(model: ifcopenshell.file) -> Optional[float]:
162
+ """
163
+ Total floor area in mΒ².
164
+ Strategy 1: IfcSpace β†’ Qto_SpaceBaseQuantities / NetFloorArea
165
+ Strategy 2: IfcSpace β†’ Qto_SpaceBaseQuantities / GrossFloorArea
166
+ Strategy 3: IfcSlab[FLOOR/BASESLAB] β†’ Qto_SlabBaseQuantities / NetArea
167
+ """
168
+ area_scale = get_area_scale(model)
169
+ total = 0.0
170
+ found_any = False
171
+
172
+ # Strategies 1 & 2 β€” IfcSpace
173
+ spaces = model.by_type("IfcSpace")
174
+ for space in spaces:
175
+ area = get_quantity_multi(space, SPACE_QSETS, "NetFloorArea")
176
+ if area is None:
177
+ area = get_quantity_multi(space, SPACE_QSETS, "GrossFloorArea")
178
+ if area is not None:
179
+ total += area * area_scale
180
+ found_any = True
181
+
182
+ if found_any:
183
+ return round(total, 4)
184
+
185
+ # Strategy 3 β€” IfcSlab FLOOR type
186
+ for slab in model.by_type("IfcSlab"):
187
+ pred = getattr(slab, "PredefinedType", None)
188
+ if pred in ("FLOOR", "BASESLAB"):
189
+ area = get_quantity_multi(slab, SLAB_QSETS, "NetArea")
190
+ if area is None:
191
+ area = get_quantity_multi(slab, SLAB_QSETS, "GrossArea")
192
+ if area is not None:
193
+ total += area * area_scale
194
+ found_any = True
195
+
196
+ return round(total, 4) if found_any else None
197
+
198
+
199
+ def extract_roof_area(model: ifcopenshell.file) -> Optional[float]:
200
+ """
201
+ Total roof area in mΒ².
202
+ Strategy 1: IfcRoof β†’ Qto_RoofBaseQuantities / NetArea
203
+ Strategy 2: IfcSlab[ROOF] β†’ Qto_SlabBaseQuantities / NetArea
204
+ Strategy 3: IfcSlab[ROOF] β†’ Qto_SlabBaseQuantities / GrossArea
205
+ """
206
+ area_scale = get_area_scale(model)
207
+ total = 0.0
208
+ found_any = False
209
+
210
+ # Strategy 1 β€” IfcRoof entity
211
+ for roof in model.by_type("IfcRoof"):
212
+ area = get_quantity_multi(roof, ROOF_QSETS, "NetArea")
213
+ if area is None:
214
+ area = get_quantity_multi(roof, ROOF_QSETS, "GrossArea")
215
+ if area is not None:
216
+ total += area * area_scale
217
+ found_any = True
218
+
219
+ if found_any:
220
+ return round(total, 4)
221
+
222
+ # Strategies 2 & 3 β€” IfcSlab ROOF type
223
+ for slab in model.by_type("IfcSlab"):
224
+ pred = getattr(slab, "PredefinedType", None)
225
+ if pred == "ROOF":
226
+ area = get_quantity_multi(slab, SLAB_QSETS, "NetArea")
227
+ if area is None:
228
+ area = get_quantity_multi(slab, SLAB_QSETS, "GrossArea")
229
+ if area is not None:
230
+ total += area * area_scale
231
+ found_any = True
232
+
233
+ return round(total, 4) if found_any else None
234
+
235
+
236
+ def decode_compound_angle(compound) -> Optional[float]:
237
+ """Convert IfcCompoundPlaneAngleMeasure [deg, min, sec, microsec] to decimal degrees."""
238
+ if compound is None:
239
+ return None
240
+ parts = list(compound)
241
+ if not parts:
242
+ return None
243
+ deg = int(parts[0])
244
+ minutes = int(parts[1]) if len(parts) > 1 else 0
245
+ secs = int(parts[2]) if len(parts) > 2 else 0
246
+ microsecs = int(parts[3]) if len(parts) > 3 else 0
247
+ sign = -1 if deg < 0 else 1
248
+ return sign * (abs(deg) + abs(minutes) / 60 + abs(secs) / 3600 + abs(microsecs) / 3_600_000_000)
249
+
250
+
251
+ def extract_orientation(model: ifcopenshell.file) -> dict:
252
+ """
253
+ Extract true north angle (compass bearing, degrees CW from north)
254
+ and geographic coordinates from IfcSite.
255
+ """
256
+ result = {
257
+ "true_north_angle_deg": None,
258
+ "latitude": None,
259
+ "longitude": None,
260
+ }
261
+
262
+ # True North from IfcGeometricRepresentationContext
263
+ for ctx in model.by_type("IfcGeometricRepresentationContext"):
264
+ if ctx.is_a("IfcGeometricRepresentationSubContext"):
265
+ continue
266
+ true_north = getattr(ctx, "TrueNorth", None)
267
+ if true_north is not None:
268
+ ratios = true_north.DirectionRatios
269
+ x = float(ratios[0])
270
+ y = float(ratios[1])
271
+ # Angle from +Y axis, measured CCW, converted to clockwise compass bearing
272
+ angle_ccw = math.degrees(math.atan2(x, y))
273
+ result["true_north_angle_deg"] = round((-angle_ccw) % 360.0, 2)
274
+ break
275
+
276
+ # Lat/Lon from IfcSite
277
+ sites = model.by_type("IfcSite")
278
+ if sites:
279
+ site = sites[0]
280
+ result["latitude"] = decode_compound_angle(getattr(site, "RefLatitude", None))
281
+ result["longitude"] = decode_compound_angle(getattr(site, "RefLongitude", None))
282
+ if result["latitude"] is not None:
283
+ result["latitude"] = round(result["latitude"], 6)
284
+ if result["longitude"] is not None:
285
+ result["longitude"] = round(result["longitude"], 6)
286
+
287
+ return result
288
+
289
+
290
+ # ── Per-file orchestration ────────────────────────────────────────────────────
291
+
292
+ def process_ifc_file(ifc_path: Path) -> dict:
293
+ """Open one IFC file and extract all metrics."""
294
+ base = {
295
+ "project_name": ifc_path.parent.name,
296
+ "ifc_file": ifc_path.name,
297
+ "window_area_m2": None,
298
+ "floor_area_m2": None,
299
+ "roof_area_m2": None,
300
+ "true_north_angle_deg": None,
301
+ "latitude": None,
302
+ "longitude": None,
303
+ "error": None,
304
+ }
305
+
306
+ try:
307
+ model = ifcopenshell.open(str(ifc_path))
308
+ except Exception as exc:
309
+ log.error(f"Cannot open {ifc_path}: {exc}")
310
+ base["error"] = str(exc)
311
+ return base
312
+
313
+ schema = model.schema
314
+ log.info(f" Schema: {schema}")
315
+
316
+ extractors = {
317
+ "window_area_m2": extract_window_area,
318
+ "floor_area_m2": extract_floor_area,
319
+ "roof_area_m2": extract_roof_area,
320
+ }
321
+ for key, fn in extractors.items():
322
+ try:
323
+ base[key] = fn(model)
324
+ except Exception as exc:
325
+ log.warning(f" {key} extraction failed: {exc}")
326
+
327
+ try:
328
+ orientation = extract_orientation(model)
329
+ base.update(orientation)
330
+ except Exception as exc:
331
+ log.warning(f" orientation extraction failed: {exc}")
332
+
333
+ return base
334
+
335
+
336
+ # ── Output ────────────────────────────────────────────────────────────────────
337
+
338
+ def write_csv(results: list[dict], output_path: Path) -> None:
339
+ with open(output_path, "w", newline="", encoding="utf-8") as f:
340
+ writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS, extrasaction="ignore")
341
+ writer.writeheader()
342
+ writer.writerows(results)
343
+ log.info(f"CSV written to: {output_path}")
344
+
345
+
346
+ def print_summary_table(results: list[dict]) -> None:
347
+ display_cols = [
348
+ "project_name", "ifc_file",
349
+ "window_area_m2", "floor_area_m2", "roof_area_m2",
350
+ "true_north_angle_deg", "latitude", "longitude",
351
+ ]
352
+ headers = [
353
+ "Project", "File",
354
+ "Window mΒ²", "Floor mΒ²", "Roof mΒ²",
355
+ "TrueNorthΒ°", "Lat", "Lon",
356
+ ]
357
+
358
+ table_data = []
359
+ for r in results:
360
+ row = []
361
+ for col in display_cols:
362
+ val = r.get(col)
363
+ if val is None:
364
+ row.append("N/A")
365
+ elif isinstance(val, float):
366
+ row.append(f"{val:.2f}")
367
+ else:
368
+ row.append(str(val))
369
+ if r.get("error"):
370
+ row[-1] = "ERROR"
371
+ table_data.append(row)
372
+
373
+ print("\n" + tabulate(table_data, headers=headers, tablefmt="github"))
374
+
375
+ processed = [r for r in results if not r.get("error")]
376
+ errors = [r for r in results if r.get("error")]
377
+ print(f"\nFiles scanned : {len(results)}")
378
+ print(f"Errors : {len(errors)}")
379
+ print(f"Window data : {sum(1 for r in processed if r['window_area_m2'] is not None)}/{len(processed)} files")
380
+ print(f"Floor data : {sum(1 for r in processed if r['floor_area_m2'] is not None)}/{len(processed)} files")
381
+ print(f"Roof data : {sum(1 for r in processed if r['roof_area_m2'] is not None)}/{len(processed)} files")
382
+ print(f"Orientation : {sum(1 for r in processed if r['true_north_angle_deg'] is not None)}/{len(processed)} files")
383
+ if errors:
384
+ print("\nFiles with errors:")
385
+ for r in errors:
386
+ print(f" {r['project_name']}/{r['ifc_file']}: {r['error']}")
387
+
388
+
389
+ # ── Entry point ───────────────────────────────────────────────────────────────
390
+
391
+ def main() -> None:
392
+ parser = argparse.ArgumentParser(
393
+ description="Scan IFC files for window/floor/roof area and orientation."
394
+ )
395
+ parser.add_argument(
396
+ "--root",
397
+ type=Path,
398
+ default=Path(__file__).parent / "Sample projects" / "projects",
399
+ help="Root directory to search for .ifc files",
400
+ )
401
+ parser.add_argument(
402
+ "--output",
403
+ type=Path,
404
+ default=Path(__file__).parent / "ifc_scan_results.csv",
405
+ help="Output CSV file path",
406
+ )
407
+ args = parser.parse_args()
408
+
409
+ if not args.root.exists():
410
+ log.error(f"Root directory not found: {args.root}")
411
+ return
412
+
413
+ ifc_files = find_ifc_files(args.root)
414
+ log.info(f"Found {len(ifc_files)} IFC file(s) under: {args.root}")
415
+
416
+ results = []
417
+ for i, ifc_path in enumerate(ifc_files, 1):
418
+ log.info(f"[{i}/{len(ifc_files)}] {ifc_path.parent.name}/{ifc_path.name}")
419
+ result = process_ifc_file(ifc_path)
420
+ results.append(result)
421
+
422
+ write_csv(results, args.output)
423
+ print_summary_table(results)
424
+
425
+
426
+ if __name__ == "__main__":
427
+ main()
teams/lux-ai/API function/solar_production_engine.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Solar Production Engine [DEPRECATED β€” use solar_pipeline/ instead]
3
+
4
+ Superseded by: solar_pipeline/solar_production_engine.py
5
+
6
+ Original description:
7
+ Calculates annual solar energy production for a building by querying the
8
+ NREL PVWatts v8 API for each roof segment (slab) individually.
9
+
10
+ Each segment has its own orientation (tilt & azimuth), so the per-segment
11
+ approach yields a far more accurate LEED renewable-energy score than a
12
+ single whole-roof average.
13
+
14
+ LEED Score = (Ξ£ P_slabs / Consumption_total) Γ— 100
15
+
16
+ Future integration:
17
+ An IFC parser will populate `roof_segments` automatically from
18
+ IfcSlab / IfcRoof geometry. For now we use representative dummy data.
19
+ """
20
+
21
+ import requests
22
+ import time
23
+ from dataclasses import dataclass
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Constants
28
+ # ---------------------------------------------------------------------------
29
+ API_KEY = "0zwEIS1adJrx658O3gjQYfI7AprKLjQf4KP420o9"
30
+ BASE_URL = "https://developer.nrel.gov/api/pvwatts/v8.json"
31
+ PANEL_EFFICIENCY = 0.20 # 1 kW per 5 mΒ²
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Location dataclass
36
+ # ---------------------------------------------------------------------------
37
+ @dataclass
38
+ class Location:
39
+ """Stores site coordinates and a human-readable project name."""
40
+ latitude: float
41
+ longitude: float
42
+ name: str
43
+
44
+
45
+ # Default site – Barcelona
46
+ SITE_LOCATION = Location(
47
+ latitude=41.38,
48
+ longitude=2.17,
49
+ name="Barcelona_Project_Alpha",
50
+ )
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Dummy roof-segment data (will be populated by IFC parser in the future)
55
+ # ---------------------------------------------------------------------------
56
+ roof_segments: list[dict] = [
57
+ {"id": "Slab_01", "area": 120, "tilt": 30, "azimuth": 180}, # South face
58
+ {"id": "Slab_02", "area": 85, "tilt": 15, "azimuth": 90}, # East face
59
+ {"id": "Slab_03", "area": 45, "tilt": 45, "azimuth": 270}, # West face
60
+ ]
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Core calculation function
65
+ # ---------------------------------------------------------------------------
66
+ def calculate_segment_production(
67
+ area: float,
68
+ tilt: float,
69
+ azimuth: float,
70
+ location: Location,
71
+ ) -> float:
72
+ """
73
+ Query the NREL PVWatts v8 API for a single roof segment and return
74
+ the estimated annual AC energy production in kWh.
75
+
76
+ Parameters
77
+ ----------
78
+ area : float
79
+ Segment area in mΒ².
80
+ tilt : float
81
+ Panel tilt angle in degrees from horizontal.
82
+ azimuth : float
83
+ Panel azimuth in degrees (180 = due south).
84
+ location : Location
85
+ Site coordinates and name.
86
+
87
+ Returns
88
+ -------
89
+ float
90
+ Annual AC production in kWh, or 0 on error.
91
+ """
92
+ system_capacity = area * PANEL_EFFICIENCY # kW
93
+
94
+ params = {
95
+ "api_key": API_KEY,
96
+ "lat": location.latitude,
97
+ "lon": location.longitude,
98
+ "system_capacity": system_capacity,
99
+ "azimuth": azimuth,
100
+ "tilt": tilt,
101
+ "array_type": 1, # Fixed – roof mount
102
+ "module_type": 1, # Premium
103
+ "losses": 14,
104
+ }
105
+
106
+ try:
107
+ response = requests.get(BASE_URL, params=params, timeout=30)
108
+ response.raise_for_status()
109
+ data = response.json()
110
+
111
+ if "errors" in data and data["errors"]:
112
+ print(f" [API Error] {data['errors']}")
113
+ return 0.0
114
+
115
+ annual_kwh = float(data["outputs"]["ac_annual"])
116
+ return annual_kwh
117
+
118
+ except requests.RequestException as exc:
119
+ print(f" [Request Error] {exc}")
120
+ return 0.0
121
+ except (KeyError, TypeError, ValueError) as exc:
122
+ print(f" [Parse Error] Could not read API response: {exc}")
123
+ return 0.0
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Main execution
128
+ # ---------------------------------------------------------------------------
129
+ def run_production_analysis(
130
+ segments: list[dict] | None = None,
131
+ location: Location | None = None,
132
+ ) -> dict:
133
+ """Iterate over roof segments, query PVWatts, and return results.
134
+
135
+ Parameters
136
+ ----------
137
+ segments : list[dict] | None
138
+ Each dict must have keys: id, area, tilt, azimuth.
139
+ Falls back to the module-level ``roof_segments`` dummy data.
140
+ location : Location | None
141
+ Site coordinates. Falls back to ``SITE_LOCATION``.
142
+
143
+ Returns
144
+ -------
145
+ dict with keys:
146
+ "segments" – list of per-segment result dicts
147
+ "total_kwh" – total annual production (float)
148
+ "location" – the Location used
149
+ """
150
+ segments = segments or roof_segments
151
+ location = location or SITE_LOCATION
152
+ total_building_production = 0.0
153
+ results: list[dict] = []
154
+
155
+ print(f"--- Analyzing Roof Segments for {location.name} ---")
156
+ print(f" Site: ({location.latitude}, {location.longitude})")
157
+ print(f" Panel efficiency factor: {PANEL_EFFICIENCY}")
158
+ print()
159
+
160
+ for slab in segments:
161
+ capacity_kw = slab["area"] * PANEL_EFFICIENCY
162
+ annual_kwh = calculate_segment_production(
163
+ slab["area"],
164
+ slab["tilt"],
165
+ slab["azimuth"],
166
+ location,
167
+ )
168
+ total_building_production += annual_kwh
169
+
170
+ results.append({
171
+ "id": slab["id"],
172
+ "area": slab["area"],
173
+ "tilt": slab["tilt"],
174
+ "azimuth": slab["azimuth"],
175
+ "capacity_kw": capacity_kw,
176
+ "annual_kwh": annual_kwh,
177
+ })
178
+
179
+ print(
180
+ f" {slab['id']:>15s} | "
181
+ f"Area: {slab['area']:>7.1f} mΒ² | "
182
+ f"Tilt: {slab['tilt']:>5.1f}Β° | "
183
+ f"Azimuth: {slab['azimuth']:>5.1f}Β° | "
184
+ f"Capacity: {capacity_kw:>6.1f} kW | "
185
+ f"Yield: {annual_kwh:>10,.2f} kWh/yr"
186
+ )
187
+
188
+ # Respect rate limits
189
+ time.sleep(1)
190
+
191
+ print()
192
+ print("-" * 70)
193
+ print(f" TOTAL BUILDING PRODUCTION: {total_building_production:>12,.2f} kWh/yr")
194
+ print("-" * 70)
195
+ print()
196
+ print(" LEED hint: Score = Total Production / Total Consumption Γ— 100")
197
+
198
+ return {
199
+ "segments": results,
200
+ "total_kwh": total_building_production,
201
+ "location": location,
202
+ }
203
+
204
+
205
+ def main() -> None:
206
+ """Run with built-in dummy data (backward-compatible entry point)."""
207
+ run_production_analysis()
208
+
209
+
210
+ if __name__ == "__main__":
211
+ main()
teams/lux-ai/Final pipeline/README.md ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Solar Pipeline β€” README
2
+
3
+ ## What is this?
4
+
5
+ This tool answers **one question**:
6
+
7
+ > **"If I put solar panels on this building's roof, how much energy will they produce?"**
8
+
9
+ You give it a **building file** (`.ifc` format β€” the standard file that architects export from tools like Revit, ArchiCAD, or Vectorworks).
10
+
11
+ It gives you back a **solar score** β€” how much of the building's energy the roof solar panels could cover.
12
+
13
+ ```
14
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
15
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
16
+ β”‚ .ifc file β”‚ ──────▢ β”‚ analyze_ifc() β”‚ ──────▢ β”‚ Solar Score β”‚
17
+ β”‚ (building) β”‚ β”‚ (this tool) β”‚ β”‚ (% energy) β”‚
18
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
19
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
20
+ ```
21
+
22
+ **Score = 100%** means the roof produces as much energy as the building consumes.
23
+ That's **net-zero energy** β€” a key goal for green building certifications like **LEED**.
24
+
25
+ ---
26
+
27
+ ## Quick Start (for everyone)
28
+
29
+ ### Step 1 β€” Install
30
+
31
+ Open a terminal (Command Prompt, PowerShell, or Mac Terminal) and type:
32
+
33
+ ```bash
34
+ pip install ifcopenshell numpy requests tabulate
35
+ ```
36
+
37
+ ### Step 2 β€” Run
38
+
39
+ ```bash
40
+ python -m solar_pipeline.analyze "path/to/your/building.ifc"
41
+ ```
42
+
43
+ That's it. You'll see a report like this:
44
+
45
+ ```
46
+ ============================================================
47
+ SOLAR ANALYSIS β€” fzk_house
48
+ ============================================================
49
+
50
+ FILE arc.ifc
51
+ LOCATION 49.100435, 8.436539
52
+
53
+ ── Building Metadata ────────────────────────
54
+ Window area.................. 23.2 mΒ²
55
+ Floor area................... 173.3 mΒ²
56
+ Roof area (property-set)..... 165.1 mΒ²
57
+ True north................... 310.0Β°
58
+
59
+ ── Roof Segments (geometry) ─────────────────
60
+ Roof_Seg_01 Area: 82.6 mΒ² Tilt: 30.0Β° Azimuth: 310.0Β° β†’ 11,105.3 kWh/yr
61
+ Roof_Seg_02 Area: 82.6 mΒ² Tilt: 30.0Β° Azimuth: 130.0Β° β†’ 14,914.6 kWh/yr
62
+
63
+ ── Solar Production ────────────────────────
64
+ Total roof area ......... 165.1 mΒ²
65
+ System capacity ......... 33.0 kW
66
+ Annual production ....... 26,020.0 kWh/yr
67
+
68
+ ── LEED Score ──────────────────────────────
69
+ Consumption estimate .... 26,001 kWh/yr
70
+ Renewable production .... 26,020 kWh/yr
71
+ Score ................... 100.1 %
72
+
73
+ β˜… Net-zero energy achieved!
74
+ ```
75
+
76
+ ### If the building file has no location
77
+
78
+ Some IFC files don't include coordinates. Just add them manually:
79
+
80
+ ```bash
81
+ python -m solar_pipeline.analyze "building.ifc" --lat 48.14 --lon 11.58
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Quick Start (for developers / AI agents)
87
+
88
+ One import, one function, one result:
89
+
90
+ ```python
91
+ from solar_pipeline.analyze import analyze_ifc
92
+
93
+ result = analyze_ifc("building.ifc")
94
+
95
+ if result["ok"]:
96
+ print(result["leed_score"]) # 100.1
97
+ print(result["total_production"]) # 26019.96 (kWh/yr)
98
+ print(result["segments"]) # [{id, area, tilt, azimuth, annual_kwh}, ...]
99
+ else:
100
+ print(result["error"]) # what went wrong
101
+ ```
102
+
103
+ ### Function signature
104
+
105
+ ```python
106
+ analyze_ifc(
107
+ ifc_path, # path to .ifc file (required)
108
+ *,
109
+ lat=None, # override latitude
110
+ lon=None, # override longitude
111
+ name=None, # project name for the report
112
+ consumption_kwh_per_m2=None, # energy benchmark (default: 150)
113
+ call_api=True, # set False for offline mode
114
+ )
115
+ ```
116
+
117
+ ### What the result dict contains
118
+
119
+ | Key | Type | What it means |
120
+ |-----|------|---------------|
121
+ | `ok` | bool | `True` if everything worked |
122
+ | `error` | str or None | Error message if `ok` is False |
123
+ | `project_name` | str | Name of the project |
124
+ | `ifc_file` | str | File name |
125
+ | `window_area_m2` | float or None | Total window area in the building |
126
+ | `floor_area_m2` | float or None | Total floor area |
127
+ | `roof_area_m2` | float or None | Roof area from property sets (metadata) |
128
+ | `true_north_deg` | float or None | Building orientation (0Β° = facing north) |
129
+ | `latitude` | float | Site latitude |
130
+ | `longitude` | float | Site longitude |
131
+ | `segments` | list[dict] | Each roof face with its area, tilt, azimuth, and annual kWh |
132
+ | `total_roof_area_m2` | float | Total roof area from 3D geometry |
133
+ | `total_capacity_kw` | float | Solar panel capacity in kilowatts |
134
+ | `total_production` | float | Annual energy production in kWh/yr |
135
+ | `consumption` | float | Estimated annual consumption in kWh/yr |
136
+ | `leed_score` | float | Production Γ· Consumption Γ— 100 (%) |
137
+
138
+ ---
139
+
140
+ ## How It Works β€” The DNA of the Tool
141
+
142
+ The pipeline has **5 steps**. Each step is handled by a separate module, but `analyze_ifc()` runs them all for you automatically.
143
+
144
+ ```
145
+ STEP 1 STEP 2 STEP 3 STEP 4 STEP 5
146
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
147
+ β”‚ Open β”‚ β”‚ Read β”‚ β”‚ Analyse β”‚ β”‚ Query β”‚ β”‚ Calculateβ”‚
148
+ β”‚ IFC │───▢│ metadata │───▢│ roof 3D │───▢│ PVWatts │───▢│ LEED β”‚
149
+ β”‚ file β”‚ β”‚ from file β”‚ β”‚ geometry β”‚ β”‚ solar APIβ”‚ β”‚ score β”‚
150
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
151
+ β”‚ β”‚ β”‚ β”‚
152
+ window area roof segments kWh per score =
153
+ floor area (tilt, azimuth, segment production
154
+ roof area area each) Γ· consumption
155
+ lat, lon Γ— 100
156
+ true north
157
+ ```
158
+
159
+ ### Step 1 β€” Open the IFC file
160
+
161
+ **IFC** (Industry Foundation Classes) is the universal file format for buildings. It stores the entire building as a database of objects β€” walls, windows, roofs, floors, etc. β€” with their geometry and properties.
162
+
163
+ We use the open-source library `ifcopenshell` to read it.
164
+
165
+ ### Step 2 β€” Read building metadata
166
+
167
+ The tool reads **property sets** embedded in the IFC file:
168
+
169
+ | What | Where in the IFC file | Why we need it |
170
+ |------|----------------------|----------------|
171
+ | Window area | `IfcWindow` β†’ quantity sets | Glazing ratio analysis |
172
+ | Floor area | `IfcSpace` or `IfcSlab[FLOOR]` β†’ quantity sets | Estimate building energy consumption |
173
+ | Roof area | `IfcRoof` or `IfcSlab[ROOF]` β†’ quantity sets | Cross-validate with 3D geometry |
174
+ | Latitude & Longitude | `IfcSite.RefLatitude / RefLongitude` | Tell PVWatts where the building is |
175
+ | True North | `IfcGeometricRepresentationContext.TrueNorth` | Correct roof azimuths to real compass |
176
+
177
+ **The alias system**: Different software (Revit, ArchiCAD, Vectorworks) stores these values under different names. For example, floor area might be called `NetFloorArea`, `GrossFloorArea`, `GSA BIM Area`, or `Netto-GrundflΓ€che` (German). The file `key_aliases.json` contains a priority-ordered list of all known names for each property, so the tool tries each one until it finds a match.
178
+
179
+ ### Step 3 β€” Analyse roof 3D geometry
180
+
181
+ This is the key step that makes per-segment solar analysis possible:
182
+
183
+ 1. **Find roof elements** β€” `IfcRoof` objects and `IfcSlab` objects marked as roof type
184
+ 2. **Triangulate** β€” Convert each roof element into a mesh of triangles using world coordinates
185
+ 3. **Compute normals** β€” Each triangle has a direction it faces (its "normal vector")
186
+ 4. **Cluster** β€” Group triangles that face the same direction (within 15Β° tolerance)
187
+ 5. **Compute properties** β€” For each cluster, calculate:
188
+ - **Area** β€” total mΒ² of that roof face
189
+ - **Tilt** β€” angle from horizontal (0Β° = flat, 90Β° = vertical wall)
190
+ - **Azimuth** β€” compass direction the face points (0Β° = north, 180Β° = south)
191
+ 6. **True north correction** β€” Rotate all azimuths by the building's TrueNorth angle so they match real-world compass directions
192
+
193
+ *Why per-segment?* A south-facing roof face in Europe may produce **2.5Γ— more energy** than a north-facing one. A single whole-roof average would be misleading.
194
+
195
+ ### Step 4 β€” Query the PVWatts solar API
196
+
197
+ For each roof segment, we call the **NREL PVWatts v8** API (US National Renewable Energy Laboratory):
198
+
199
+ ```
200
+ Inputs: lat, lon, roof area, tilt, azimuth
201
+ Output: estimated annual solar energy in kWh
202
+ ```
203
+
204
+ The API uses real weather data, sun angles, and atmospheric conditions for the specific location. It accounts for panel losses (wiring, soiling, shading) at 14%.
205
+
206
+ ### Step 5 β€” Calculate LEED score
207
+
208
+ ```
209
+ Score = Total Production (kWh/yr) Γ· Total Consumption (kWh/yr) Γ— 100
210
+ ```
211
+
212
+ - **Production** = sum of all segment yields from PVWatts
213
+ - **Consumption** = floor area Γ— 150 kWh/mΒ²/yr (ASHRAE office benchmark)
214
+ - If the IFC file has no floor area data, defaults to 50,000 kWh/yr
215
+
216
+ | Score | Meaning |
217
+ |-------|---------|
218
+ | β‰₯ 100% | Net-zero energy β€” roof produces everything the building needs |
219
+ | 50–99% | Strong renewable contribution |
220
+ | 10–49% | Moderate contribution |
221
+ | < 10% | Low contribution β€” different design or additional renewables needed |
222
+
223
+ ---
224
+
225
+ ## Configuration β€” `config.py`
226
+
227
+ All defaults can be changed in [`solar_pipeline/config.py`](solar_pipeline/config.py):
228
+
229
+ ```python
230
+ # ── NREL PVWatts v8 API ──────────────────────────────────────────────
231
+ NREL_API_KEY = "0zwEIS1a..." # Free key from api.nrel.gov
232
+ PVWATTS_BASE_URL = "https://developer.nrel.gov/api/pvwatts/v8.json"
233
+
234
+ # ── Solar panel assumptions ──────────────────────────────────────────
235
+ PANEL_EFFICIENCY = 0.20 # 20% efficiency = 1 kW per 5 mΒ²
236
+ ARRAY_TYPE = 1 # 1 = fixed roof mount
237
+ MODULE_TYPE = 1 # 1 = premium (monocrystalline)
238
+ SYSTEM_LOSSES = 14 # 14% total system losses
239
+
240
+ # ── Roof geometry ────────────────────────────────────────────────────
241
+ DEFAULT_ANGLE_TOLERANCE_DEG = 15.0 # Clustering tolerance (degrees)
242
+ MIN_SEGMENT_AREA_M2 = 1.0 # Ignore tiny fragments
243
+
244
+ # ── Energy benchmarks ────────────────────────────────────────────────
245
+ DEFAULT_CONSUMPTION_KWH_PER_M2 = 150 # ASHRAE office (kWh/mΒ²/yr)
246
+ FALLBACK_CONSUMPTION_KWH = 50_000 # When floor area is unknown
247
+ ```
248
+
249
+ ### What each setting means
250
+
251
+ | Setting | Default | What it controls |
252
+ |---------|---------|-----------------|
253
+ | `PANEL_EFFICIENCY` | 0.20 | How much of the roof area converts to electricity. 0.20 means 1 kW of solar capacity per 5 mΒ² of roof. Premium panels are ~20%, budget panels ~15%. |
254
+ | `SYSTEM_LOSSES` | 14% | Energy lost in wiring, inverters, soiling, snow, etc. Industry standard is 14%. |
255
+ | `DEFAULT_ANGLE_TOLERANCE_DEG` | 15Β° | When grouping roof triangles by direction, faces within 15Β° of each other are merged into one segment. Lower = more segments, higher = fewer. |
256
+ | `MIN_SEGMENT_AREA_M2` | 1.0 | Roof faces smaller than 1 mΒ² are ignored (geometric noise). |
257
+ | `DEFAULT_CONSUMPTION_KWH_PER_M2` | 150 | ASHRAE benchmark for a typical office building. Residential is ~100, hospitals ~300. Changes the LEED score denominator. |
258
+ | `NREL_API_KEY` | provided | Free API key from [developer.nrel.gov](https://developer.nrel.gov/signup/). Rate limit: 1 request/second. |
259
+
260
+ ---
261
+
262
+ ## File Structure
263
+
264
+ ```
265
+ solar_pipeline/
266
+ β”‚
267
+ β”œβ”€β”€ analyze.py ← THE MAIN FILE β€” one function does everything
268
+ β”‚ analyze_ifc("building.ifc") β†’ score
269
+ β”‚
270
+ β”œβ”€β”€ ifc_metadata_extractor.py ← Step 2: reads building properties from IFC
271
+ β”‚ (areas, location, orientation)
272
+ β”‚
273
+ β”œβ”€β”€ ifc_roof_parser.py ← Step 3: 3D roof geometry β†’ segments
274
+ β”‚ (tilt, azimuth, area per face)
275
+ β”‚
276
+ β”œβ”€β”€ solar_production_engine.py ← Step 4: calls PVWatts API β†’ kWh/yr
277
+ β”‚
278
+ β”œβ”€β”€ run_solar_analysis.py ← Advanced CLI with batch modes
279
+ β”‚
280
+ β”œβ”€β”€ config.py ← All configurable settings
281
+ β”‚
282
+ β”œβ”€β”€ key_aliases.json ← Maps property names across BIM software
283
+ β”‚ (Revit, ArchiCAD, Vectorworks, etc.)
284
+ β”‚
285
+ β”œβ”€β”€ __init__.py ← Package marker
286
+ └── README.md ← This file
287
+ ```
288
+
289
+ ---
290
+
291
+ ## The Key Aliases System β€” Why It Exists
292
+
293
+ Different architecture software stores the same data under different names:
294
+
295
+ | Data | Revit calls it | ArchiCAD calls it | IFC4 standard |
296
+ |------|---------------|-------------------|---------------|
297
+ | Floor area | `GSA BIM Area` | `Netto-GrundflΓ€che` | `NetFloorArea` |
298
+ | Window area | `Area` in `BaseQuantities` | `FlΓ€che` in `ArchiCADQuantities` | `Area` in `Qto_WindowBaseQuantities` |
299
+ | Roof area | `GrossArea` in `BaseQuantities` | `OberflΓ€chenbereich` | `NetArea` in `Qto_RoofBaseQuantities` |
300
+
301
+ The file `key_aliases.json` contains a **priority-ordered list** for each property. The tool tries each name in order and uses the first match. This means it works with IFC files from **any** BIM software without manual configuration.
302
+
303
+ You can add new aliases by editing the JSON file β€” no code changes needed.
304
+
305
+ ---
306
+
307
+ ## Supported IFC Versions
308
+
309
+ | Version | Status | Example projects |
310
+ |---------|--------|-----------------|
311
+ | IFC2x3 | βœ… Fully supported | duplex, schependomlaan, hitos |
312
+ | IFC4 | βœ… Fully supported | fzk_house, digital_hub, fantasy_* |
313
+ | IFC4x3 | βœ… Supported | city_house_munich |
314
+
315
+ ---
316
+
317
+ ## Requirements
318
+
319
+ | Package | Version | Why |
320
+ |---------|---------|-----|
321
+ | Python | β‰₯ 3.10 | Type hints, match statements |
322
+ | ifcopenshell | β‰₯ 0.7.0 | Open and parse IFC files |
323
+ | numpy | β‰₯ 1.21 | 3D geometry math |
324
+ | requests | β‰₯ 2.28 | Call the PVWatts API |
325
+ | tabulate | β‰₯ 0.9 | Pretty-print tables (optional) |
326
+
327
+ Install all at once:
328
+ ```bash
329
+ pip install ifcopenshell numpy requests tabulate
330
+ ```
331
+
332
+ ---
333
+
334
+ ## FAQ
335
+
336
+ **Q: Do I need internet?**
337
+ A: Yes, for the solar production numbers (PVWatts API). You can run offline with `call_api=False` to get just the roof geometry without kWh values.
338
+
339
+ **Q: What if my IFC file has no roof?**
340
+ A: The tool returns `ok=False` with error `"No roof segments found"`.
341
+
342
+ **Q: What if there's no location in the file?**
343
+ A: Pass `--lat` and `--lon` on the command line, or `lat=` and `lon=` in Python.
344
+
345
+ **Q: Can I change the panel type?**
346
+ A: Yes β€” edit `PANEL_EFFICIENCY`, `MODULE_TYPE`, and `SYSTEM_LOSSES` in `config.py`.
347
+
348
+ **Q: Is the score accurate?**
349
+ A: It's an **estimate**, not a guarantee. PVWatts uses real weather data and is widely trusted in the industry, but the actual production depends on shading, panel brand, inverter quality, and maintenance. This tool gives you a solid ballpark for early-stage design decisions and LEED pre-assessment.
350
+
351
+ **Q: What's LEED?**
352
+ A: Leadership in Energy and Environmental Design β€” the world's most widely used green building rating system. One of its credits rewards buildings that generate renewable energy on-site. The score this tool gives approximates that credit.
teams/lux-ai/Final pipeline/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ solar_pipeline β€” Unified IFC metadata extraction + solar production analysis.
3
+
4
+ Merges:
5
+ - IFC property-set/quantity-set metadata scanner (window, floor, roof area,
6
+ orientation, lat/lon) with multi-exporter alias fallback
7
+ - Geometry-based roof segment parser (per-face tilt & azimuth via 3D mesh)
8
+ - NREL PVWatts v8 solar production engine
9
+
10
+ Supports single-file solar analysis and batch scanning of project directories.
11
+ """
12
+
13
+ from solar_pipeline.config import __version__
14
+
15
+ __all__ = ["__version__"]
teams/lux-ai/Final pipeline/analyze.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ analyze.py β€” One function. One IFC file in. Full solar score out.
3
+
4
+ from solar_pipeline.analyze import analyze_ifc
5
+ result = analyze_ifc("path/to/building.ifc")
6
+ print(result["leed_score"]) # e.g. 100.1
7
+ print(result["total_production"]) # e.g. 26019.96 (kWh/yr)
8
+
9
+ Also runnable from the command line:
10
+ python -m solar_pipeline.analyze "path/to/building.ifc"
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ def analyze_ifc(
23
+ ifc_path: str | Path,
24
+ *,
25
+ lat: float | None = None,
26
+ lon: float | None = None,
27
+ name: str | None = None,
28
+ consumption_kwh_per_m2: float | None = None,
29
+ call_api: bool = True,
30
+ ) -> dict:
31
+ """
32
+ Analyse one IFC building file and return a full solar production report.
33
+
34
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
35
+ β”‚ IFC file │────▢│ This function │────▢│ Result dict β”‚
36
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
37
+
38
+ What happens inside
39
+ -------------------
40
+ 1. Opens the IFC file
41
+ 2. Reads building metadata (areas, location, orientation)
42
+ 3. Extracts 3-D roof geometry and splits it into segments
43
+ 4. Queries the NREL PVWatts v8 API for each segment's solar yield
44
+ 5. Calculates a LEED-style renewable-energy score
45
+
46
+ Parameters
47
+ ----------
48
+ ifc_path : str or Path
49
+ Path to an .ifc file (IFC2x3, IFC4, or IFC4x3).
50
+
51
+ lat, lon : float, optional
52
+ Override latitude / longitude if the IFC file has no IfcSite
53
+ coordinates. When omitted the tool reads them automatically.
54
+
55
+ name : str, optional
56
+ Project name shown in the report. Defaults to the parent folder
57
+ name of the IFC file.
58
+
59
+ consumption_kwh_per_m2 : float, optional
60
+ Building energy consumption benchmark in kWh/mΒ²/yr.
61
+ Default = 150 (ASHRAE typical office).
62
+ The tool multiplies this by the floor area to estimate annual
63
+ consumption, which is the denominator in the LEED score.
64
+
65
+ call_api : bool, default True
66
+ Set to False to skip the PVWatts API call (useful for offline
67
+ testing). When False the per-segment kWh values are all 0.
68
+
69
+ Returns
70
+ -------
71
+ dict with these keys:
72
+
73
+ ok bool – True if the analysis succeeded
74
+ error str – error message (only if ok=False)
75
+
76
+ # ── Building metadata ──────────────────────────
77
+ project_name str – project / folder name
78
+ ifc_file str – file name
79
+ window_area_m2 float|None
80
+ floor_area_m2 float|None
81
+ roof_area_m2 float|None (from property sets)
82
+ true_north_deg float|None (compass bearing)
83
+ latitude float|None
84
+ longitude float|None
85
+
86
+ # ── Roof geometry ──────────────────────────────
87
+ segments list[dict] – per roof-face data
88
+ Each dict: id, area, tilt, azimuth, capacity_kw, annual_kwh
89
+
90
+ # ── Solar production ───────────────────────────
91
+ total_roof_area_m2 float – from geometry (sum of segments)
92
+ total_capacity_kw float
93
+ total_production float – kWh/yr
94
+ consumption float – estimated kWh/yr
95
+ leed_score float – production / consumption Γ— 100
96
+
97
+ Example
98
+ -------
99
+ >>> result = analyze_ifc("Sample projects/projects/fzk_house/arc.ifc")
100
+ >>> print(f"Score: {result['leed_score']:.1f}%")
101
+ Score: 100.1%
102
+ """
103
+
104
+ # ── Lazy imports (so the module is light to load) ─────────────────────
105
+ from solar_pipeline.config import (
106
+ DEFAULT_CONSUMPTION_KWH_PER_M2,
107
+ FALLBACK_CONSUMPTION_KWH,
108
+ PANEL_EFFICIENCY,
109
+ )
110
+ from solar_pipeline.ifc_metadata_extractor import (
111
+ Location,
112
+ extract_all,
113
+ extract_location,
114
+ )
115
+ from solar_pipeline.ifc_roof_parser import parse_roof_segments
116
+ from solar_pipeline.solar_production_engine import run_production_analysis
117
+
118
+ ifc_path = Path(ifc_path)
119
+ project = name or ifc_path.parent.name
120
+
121
+ # ── 1. Validate input ─────────────────────────────────────────────────
122
+ if not ifc_path.is_file():
123
+ return _error(f"File not found: {ifc_path}")
124
+
125
+ # ── 2. Metadata extraction ────────────────────────────────────────────
126
+ try:
127
+ metadata = extract_all(ifc_path)
128
+ except Exception as exc:
129
+ return _error(f"Metadata extraction failed: {exc}")
130
+
131
+ # ── 3. Resolve location ───────────────────────────────────────────────
132
+ if lat is not None and lon is not None:
133
+ location = Location(latitude=lat, longitude=lon, name=project)
134
+ elif metadata.get("latitude") is not None and metadata.get("longitude") is not None:
135
+ location = Location(
136
+ latitude=metadata["latitude"],
137
+ longitude=metadata["longitude"],
138
+ name=project,
139
+ )
140
+ else:
141
+ return _error(
142
+ "No location found. The IFC file has no IfcSite coordinates. "
143
+ "Pass lat= and lon= manually."
144
+ )
145
+
146
+ # ── 4. Roof geometry ─────────────────────────────────────────────────
147
+ try:
148
+ segments = parse_roof_segments(ifc_path)
149
+ except Exception as exc:
150
+ return _error(f"Roof geometry parsing failed: {exc}")
151
+
152
+ if not segments:
153
+ return _error("No roof segments found in this IFC file.")
154
+
155
+ # ── 5. Solar production (PVWatts API) ─────────────────────────────────
156
+ if call_api:
157
+ try:
158
+ prod = run_production_analysis(
159
+ segments, location, verbose=False,
160
+ )
161
+ except Exception as exc:
162
+ return _error(f"PVWatts API failed: {exc}")
163
+ segment_results = prod["segments"]
164
+ total_kwh = prod["total_kwh"]
165
+ else:
166
+ # Offline mode β€” geometry only, no kWh
167
+ segment_results = []
168
+ for seg in segments:
169
+ segment_results.append({
170
+ **seg,
171
+ "capacity_kw": round(seg["area"] * PANEL_EFFICIENCY, 2),
172
+ "annual_kwh": 0.0,
173
+ })
174
+ total_kwh = 0.0
175
+
176
+ # ── 6. LEED score ─────────────────────────────────────────────────────
177
+ bench = consumption_kwh_per_m2 or DEFAULT_CONSUMPTION_KWH_PER_M2
178
+ floor = metadata.get("floor_area_m2")
179
+ if floor and floor > 0:
180
+ consumption = floor * bench
181
+ else:
182
+ consumption = FALLBACK_CONSUMPTION_KWH
183
+
184
+ leed_score = (total_kwh / consumption * 100) if consumption > 0 else 0.0
185
+
186
+ # ── Build result ──────────────────────────────────────────────────────
187
+ total_roof = sum(s["area"] for s in segment_results)
188
+ total_cap = sum(s["capacity_kw"] for s in segment_results)
189
+
190
+ return {
191
+ "ok": True,
192
+ "error": None,
193
+
194
+ # Metadata
195
+ "project_name": project,
196
+ "ifc_file": ifc_path.name,
197
+ "window_area_m2": metadata.get("window_area_m2"),
198
+ "floor_area_m2": metadata.get("floor_area_m2"),
199
+ "roof_area_m2": metadata.get("roof_area_m2"),
200
+ "true_north_deg": metadata.get("true_north_angle_deg"),
201
+ "latitude": location.latitude,
202
+ "longitude": location.longitude,
203
+
204
+ # Segments
205
+ "segments": segment_results,
206
+
207
+ # Totals
208
+ "total_roof_area_m2": round(total_roof, 2),
209
+ "total_capacity_kw": round(total_cap, 2),
210
+ "total_production": round(total_kwh, 2),
211
+ "consumption": round(consumption, 2),
212
+ "leed_score": round(leed_score, 1),
213
+ }
214
+
215
+
216
+ # ── Helpers ───────────────────────────────────────────────────────────────────
217
+
218
+ def _error(msg: str) -> dict:
219
+ """Return a minimal error result."""
220
+ log.error(msg)
221
+ return {"ok": False, "error": msg}
222
+
223
+
224
+ def print_report(result: dict) -> None:
225
+ """Pretty-print an analysis result to the terminal."""
226
+ if not result.get("ok"):
227
+ print(f"\n ERROR: {result.get('error')}\n")
228
+ return
229
+
230
+ print()
231
+ print("=" * 60)
232
+ print(f" SOLAR ANALYSIS β€” {result['project_name']}")
233
+ print("=" * 60)
234
+
235
+ print(f"\n FILE {result['ifc_file']}")
236
+ print(f" LOCATION {result['latitude']}, {result['longitude']}")
237
+
238
+ print(f"\n ── Building Metadata ────────────────────────")
239
+ for key, label in [
240
+ ("window_area_m2", "Window area"),
241
+ ("floor_area_m2", "Floor area"),
242
+ ("roof_area_m2", "Roof area (property-set)"),
243
+ ("true_north_deg", "True north"),
244
+ ]:
245
+ val = result.get(key)
246
+ unit = "Β°" if "north" in key else " mΒ²"
247
+ print(f" {label:.<30s} {f'{val:,.1f}{unit}' if val is not None else 'N/A'}")
248
+
249
+ print(f"\n ── Roof Segments (geometry) ─────────────────")
250
+ for s in result["segments"]:
251
+ print(f" {s['id']:15s} "
252
+ f"Area: {s['area']:>7.1f} mΒ² "
253
+ f"Tilt: {s['tilt']:>5.1f}Β° "
254
+ f"Azimuth: {s['azimuth']:>5.1f}Β° "
255
+ f"β†’ {s['annual_kwh']:>10,.1f} kWh/yr")
256
+
257
+ print(f"\n ── Solar Production ────────────────────────")
258
+ print(f" Total roof area ......... {result['total_roof_area_m2']:>10,.1f} mΒ²")
259
+ print(f" System capacity ......... {result['total_capacity_kw']:>10,.1f} kW")
260
+ print(f" Annual production ....... {result['total_production']:>10,.1f} kWh/yr")
261
+
262
+ print(f"\n ── LEED Score ──────────────────────────────")
263
+ print(f" Consumption estimate .... {result['consumption']:>10,.0f} kWh/yr")
264
+ print(f" Renewable production .... {result['total_production']:>10,.0f} kWh/yr")
265
+ print(f" Score ................... {result['leed_score']:>10.1f} %")
266
+
267
+ if result["leed_score"] >= 100:
268
+ print(f"\n β˜… Net-zero energy achieved!")
269
+ elif result["leed_score"] >= 50:
270
+ print(f"\n ● Over 50% renewable coverage β€” strong LEED contribution")
271
+ elif result["leed_score"] >= 10:
272
+ print(f"\n β—‹ Moderate renewable contribution")
273
+ else:
274
+ print(f"\n Β· Low renewable contribution β€” consider design changes")
275
+ print()
276
+
277
+
278
+ # ── CLI entry point ───────────────────────────────────────────────────────────
279
+
280
+ def main() -> None:
281
+ """Minimal CLI: python -m solar_pipeline.analyze building.ifc"""
282
+ logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
283
+
284
+ if len(sys.argv) < 2:
285
+ print("Usage: python -m solar_pipeline.analyze <file.ifc> [--lat N] [--lon N]")
286
+ sys.exit(1)
287
+
288
+ # Simple arg parsing (no argparse needed for this one-shot entry)
289
+ ifc_path = sys.argv[1]
290
+ lat = lon = None
291
+ args = sys.argv[2:]
292
+ i = 0
293
+ while i < len(args):
294
+ if args[i] == "--lat" and i + 1 < len(args):
295
+ lat = float(args[i + 1]); i += 2
296
+ elif args[i] == "--lon" and i + 1 < len(args):
297
+ lon = float(args[i + 1]); i += 2
298
+ else:
299
+ i += 1
300
+
301
+ result = analyze_ifc(ifc_path, lat=lat, lon=lon)
302
+ print_report(result)
303
+
304
+
305
+ if __name__ == "__main__":
306
+ main()
teams/lux-ai/Final pipeline/config.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ config.py β€” Shared constants and defaults for the solar pipeline.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ # ── Paths ─────────────────────────────────────────────────────────────────────
10
+
11
+ PACKAGE_DIR = Path(__file__).resolve().parent
12
+ REPO_ROOT = PACKAGE_DIR.parent
13
+ KEY_ALIASES_PATH = PACKAGE_DIR / "key_aliases.json"
14
+ DEFAULT_SAMPLE_ROOT = REPO_ROOT / "Sample projects" / "projects"
15
+
16
+ # ── NREL PVWatts v8 API ──────────────────────────────────────────────────────
17
+
18
+ NREL_API_KEY = "0zwEIS1adJrx658O3gjQYfI7AprKLjQf4KP420o9"
19
+ PVWATTS_BASE_URL = "https://developer.nrel.gov/api/pvwatts/v8.json"
20
+
21
+ # ── Solar panel assumptions ───────────────────────────────────────────────────
22
+
23
+ PANEL_EFFICIENCY = 0.20 # 1 kW per 5 mΒ² of panel
24
+ ARRAY_TYPE = 1 # Fixed β€” roof mount
25
+ MODULE_TYPE = 1 # Premium (monocrystalline)
26
+ SYSTEM_LOSSES = 14 # % (wiring, soiling, mismatch, etc.)
27
+
28
+ # ── Roof geometry clustering ─────────────────────────────────────────────────
29
+
30
+ DEFAULT_ANGLE_TOLERANCE_DEG = 15.0 # max normal deviation within a cluster
31
+ MIN_SEGMENT_AREA_M2 = 1.0 # ignore clusters smaller than this
32
+
33
+ # ── LEED / energy benchmarks ─────────────────────────────────────────────────
34
+
35
+ DEFAULT_CONSUMPTION_KWH_PER_M2 = 150 # ASHRAE typical office (kWh/mΒ²/yr)
36
+ FALLBACK_CONSUMPTION_KWH = 50_000 # used when floor area is unknown
37
+
38
+ # ── CSV output columns ───────────────────────────────────────────────────────
39
+
40
+ CSV_COLUMNS = [
41
+ "project_name",
42
+ "ifc_file",
43
+ "window_area_m2",
44
+ "floor_area_m2",
45
+ "roof_area_m2",
46
+ "true_north_angle_deg",
47
+ "latitude",
48
+ "longitude",
49
+ "error",
50
+ ]
teams/lux-ai/Final pipeline/ifc_metadata_extractor.py ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ifc_metadata_extractor.py β€” Alias-driven IFC property-set / quantity-set
3
+ metadata extraction.
4
+
5
+ Reads key_aliases.json to resolve exporter-specific naming differences
6
+ (Archicad, Revit, IFC2x3, IFC4, GSA, BOMA, …) and extracts:
7
+ window_area, floor_area, roof_area, true_north_angle, latitude, longitude
8
+
9
+ Also exposes helpers consumed by the roof-geometry parser and the solar
10
+ production engine (extract_location, extract_true_north).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import csv
16
+ import json
17
+ import logging
18
+ import math
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+ import ifcopenshell
24
+ import ifcopenshell.util.unit as ifc_unit_util
25
+
26
+ from solar_pipeline.config import CSV_COLUMNS, KEY_ALIASES_PATH
27
+
28
+ # ── Logging ───────────────────────────────────────────────────────────────────
29
+
30
+ log = logging.getLogger(__name__)
31
+
32
+ # ── Load alias map once at module import ──────────────────────────────────────
33
+
34
+ def _load_aliases(path: Path | None = None) -> dict:
35
+ p = path or KEY_ALIASES_PATH
36
+ try:
37
+ with open(p, encoding="utf-8") as f:
38
+ return json.load(f)
39
+ except FileNotFoundError:
40
+ log.warning("key_aliases.json not found at %s β€” using empty alias map", p)
41
+ return {}
42
+
43
+ ALIASES: dict = _load_aliases()
44
+
45
+ # ── Location dataclass (re-used by solar engine) ─────────────────────────────
46
+
47
+ @dataclass
48
+ class Location:
49
+ """Site coordinates extracted from IfcSite."""
50
+ latitude: float
51
+ longitude: float
52
+ name: str = ""
53
+
54
+
55
+ # ── Unit helpers ──────────────────────────────────────────────────────────────
56
+
57
+ def get_length_scale(model: ifcopenshell.file) -> float:
58
+ """Scale factor: model length units β†’ metres."""
59
+ try:
60
+ return ifc_unit_util.calculate_unit_scale(model, "LENGTHUNIT")
61
+ except Exception:
62
+ return 1.0
63
+
64
+
65
+ def get_area_scale(model: ifcopenshell.file) -> float:
66
+ """Scale factor: model area units β†’ mΒ²."""
67
+ try:
68
+ return ifc_unit_util.calculate_unit_scale(model, "AREAMEASURE")
69
+ except Exception:
70
+ return 1.0
71
+
72
+
73
+ # ── Low-level quantity / property getters ─────────────────────────────────────
74
+
75
+ def get_quantity(element, qset_name: str, qty_name: str) -> Optional[float]:
76
+ """
77
+ Walk IsDefinedBy β†’ IfcRelDefinesByProperties β†’ IfcElementQuantity
78
+ and return the numeric value of *qty_name* inside *qset_name*.
79
+ """
80
+ for rel in getattr(element, "IsDefinedBy", []):
81
+ if not rel.is_a("IfcRelDefinesByProperties"):
82
+ continue
83
+ qset = rel.RelatingPropertyDefinition
84
+ if not qset.is_a("IfcElementQuantity"):
85
+ continue
86
+ if qset.Name != qset_name:
87
+ continue
88
+ for qty in qset.Quantities:
89
+ if qty.Name != qty_name:
90
+ continue
91
+ for attr in ("AreaValue", "LengthValue", "VolumeValue", "CountValue"):
92
+ v = getattr(qty, attr, None)
93
+ if v is not None:
94
+ return float(v)
95
+ return None
96
+
97
+
98
+ def get_property(element, pset_name: str, prop_name: str) -> Optional[float]:
99
+ """
100
+ Walk IsDefinedBy β†’ IfcRelDefinesByProperties β†’ IfcPropertySet
101
+ and return the nominal value of *prop_name* inside *pset_name*.
102
+ """
103
+ for rel in getattr(element, "IsDefinedBy", []):
104
+ if not rel.is_a("IfcRelDefinesByProperties"):
105
+ continue
106
+ pset = rel.RelatingPropertyDefinition
107
+ if not pset.is_a("IfcPropertySet"):
108
+ continue
109
+ if pset.Name != pset_name:
110
+ continue
111
+ for prop in pset.HasProperties:
112
+ if prop.Name != prop_name:
113
+ continue
114
+ nv = getattr(prop, "NominalValue", None)
115
+ if nv is not None:
116
+ try:
117
+ return float(nv.wrappedValue)
118
+ except (TypeError, ValueError, AttributeError):
119
+ return None
120
+ return None
121
+
122
+
123
+ def get_quantity_multi(
124
+ element, qset_names: list[str], qty_name: str
125
+ ) -> Optional[float]:
126
+ """Try multiple quantity-set names in priority order; return first match."""
127
+ for qset_name in qset_names:
128
+ val = get_quantity(element, qset_name, qty_name)
129
+ if val is not None:
130
+ return val
131
+ return None
132
+
133
+
134
+ # ── Alias-driven generic extractor ────────────────────────────��──────────────
135
+
136
+ def _extract_by_alias(
137
+ model: ifcopenshell.file,
138
+ canonical_key: str,
139
+ area_scale: float,
140
+ length_scale: float,
141
+ ) -> Optional[float]:
142
+ """
143
+ Use the alias chain in key_aliases.json to extract a value.
144
+ Iterates strategies in priority order; returns the first that yields data.
145
+ """
146
+ strategies = ALIASES.get(canonical_key, [])
147
+ if not strategies:
148
+ return None
149
+
150
+ for strat in strategies:
151
+ entity_type = strat.get("entity")
152
+ source = strat.get("source") # "qset", "pset", or "attr"
153
+
154
+ # ── Attribute-based strategies ────────────────────────────────────
155
+ if source == "attr":
156
+ if strat.get("op") == "multiply":
157
+ # e.g. OverallHeight Γ— OverallWidth for windows
158
+ elements = model.by_type(entity_type)
159
+ total = 0.0
160
+ found = False
161
+ for elem in elements:
162
+ vals = []
163
+ for k in strat["keys"]:
164
+ v = getattr(elem, k, None)
165
+ if v is not None:
166
+ vals.append(float(v))
167
+ if len(vals) == len(strat["keys"]):
168
+ product = 1.0
169
+ for v in vals:
170
+ product *= v
171
+ total += product * (length_scale ** len(strat["keys"]))
172
+ found = True
173
+ if found:
174
+ return round(total, 4)
175
+ # Other attr strategies (TrueNorth, RefLatitude) are handled
176
+ # by dedicated functions, not this generic extractor.
177
+ continue
178
+
179
+ # ── Quantity-set or property-set strategies ───────────────────────
180
+ predefined_type = strat.get("predefined_type")
181
+ set_name = strat["set_name"]
182
+ key = strat["key"]
183
+
184
+ elements = model.by_type(entity_type)
185
+ total = 0.0
186
+ found = False
187
+ for elem in elements:
188
+ # Filter by PredefinedType if required
189
+ if predefined_type:
190
+ pt = getattr(elem, "PredefinedType", None)
191
+ if pt != predefined_type:
192
+ continue
193
+
194
+ if source == "qset":
195
+ val = get_quantity(elem, set_name, key)
196
+ elif source == "pset":
197
+ val = get_property(elem, set_name, key)
198
+ else:
199
+ continue
200
+
201
+ if val is not None:
202
+ total += val * area_scale
203
+ found = True
204
+
205
+ if found:
206
+ return round(total, 4)
207
+
208
+ return None
209
+
210
+
211
+ # ── Specific extractors ──────────────────────────────────────────────────────
212
+
213
+ def extract_window_area(model: ifcopenshell.file) -> Optional[float]:
214
+ """Total window area in mΒ², using alias fallback chain."""
215
+ return _extract_by_alias(model, "window_area", get_area_scale(model), get_length_scale(model))
216
+
217
+
218
+ def extract_floor_area(model: ifcopenshell.file) -> Optional[float]:
219
+ """Total floor area in mΒ², using alias fallback chain."""
220
+ return _extract_by_alias(model, "floor_area", get_area_scale(model), get_length_scale(model))
221
+
222
+
223
+ def extract_roof_area(model: ifcopenshell.file) -> Optional[float]:
224
+ """Total roof area (metadata) in mΒ², using alias fallback chain."""
225
+ return _extract_by_alias(model, "roof_area", get_area_scale(model), get_length_scale(model))
226
+
227
+
228
+ def decode_compound_angle(compound) -> Optional[float]:
229
+ """Convert IfcCompoundPlaneAngleMeasure [deg, min, sec, Β΅sec] β†’ decimal degrees."""
230
+ if compound is None:
231
+ return None
232
+ parts = list(compound)
233
+ if not parts:
234
+ return None
235
+ deg = int(parts[0])
236
+ minutes = int(parts[1]) if len(parts) > 1 else 0
237
+ secs = int(parts[2]) if len(parts) > 2 else 0
238
+ microsecs = int(parts[3]) if len(parts) > 3 else 0
239
+ sign = -1 if deg < 0 else 1
240
+ return sign * (
241
+ abs(deg) + abs(minutes) / 60 + abs(secs) / 3600 + abs(microsecs) / 3_600_000_000
242
+ )
243
+
244
+
245
+ def extract_true_north(model: ifcopenshell.file) -> Optional[float]:
246
+ """
247
+ True north compass bearing (degrees clockwise from +Y axis).
248
+
249
+ Returns None if the model lacks TrueNorth data.
250
+ """
251
+ for ctx in model.by_type("IfcGeometricRepresentationContext"):
252
+ if ctx.is_a("IfcGeometricRepresentationSubContext"):
253
+ continue
254
+ true_north = getattr(ctx, "TrueNorth", None)
255
+ if true_north is not None:
256
+ ratios = true_north.DirectionRatios
257
+ x, y = float(ratios[0]), float(ratios[1])
258
+ angle_ccw = math.degrees(math.atan2(x, y))
259
+ return round((-angle_ccw) % 360.0, 2)
260
+ return None
261
+
262
+
263
+ def extract_location(model: ifcopenshell.file, project_name: str = "") -> Optional[Location]:
264
+ """
265
+ Extract lat/lon from IfcSite and return a Location dataclass.
266
+
267
+ Returns None if the model has no geographic coordinates.
268
+ """
269
+ sites = model.by_type("IfcSite")
270
+ if not sites:
271
+ return None
272
+ site = sites[0]
273
+ lat = decode_compound_angle(getattr(site, "RefLatitude", None))
274
+ lon = decode_compound_angle(getattr(site, "RefLongitude", None))
275
+ if lat is None or lon is None:
276
+ return None
277
+ return Location(
278
+ latitude=round(lat, 6),
279
+ longitude=round(lon, 6),
280
+ name=project_name or getattr(site, "Name", None) or "",
281
+ )
282
+
283
+
284
+ def extract_orientation(model: ifcopenshell.file) -> dict:
285
+ """
286
+ Extract true north angle + latitude + longitude.
287
+
288
+ Returns dict compatible with the original scan_ifc_models.py output.
289
+ """
290
+ result = {
291
+ "true_north_angle_deg": extract_true_north(model),
292
+ "latitude": None,
293
+ "longitude": None,
294
+ }
295
+ loc = extract_location(model)
296
+ if loc is not None:
297
+ result["latitude"] = loc.latitude
298
+ result["longitude"] = loc.longitude
299
+ return result
300
+
301
+
302
+ # ── Per-file orchestration ────────────────────────────────────────────────────
303
+
304
+ def extract_all(ifc_path: Path | str) -> dict:
305
+ """
306
+ Open one IFC file and extract all metadata metrics.
307
+
308
+ Returns a dict with keys matching CSV_COLUMNS.
309
+ """
310
+ ifc_path = Path(ifc_path)
311
+ base = {
312
+ "project_name": ifc_path.parent.name,
313
+ "ifc_file": ifc_path.name,
314
+ "window_area_m2": None,
315
+ "floor_area_m2": None,
316
+ "roof_area_m2": None,
317
+ "true_north_angle_deg": None,
318
+ "latitude": None,
319
+ "longitude": None,
320
+ "error": None,
321
+ }
322
+
323
+ try:
324
+ model = ifcopenshell.open(str(ifc_path))
325
+ except Exception as exc:
326
+ log.error("Cannot open %s: %s", ifc_path, exc)
327
+ base["error"] = str(exc)
328
+ return base
329
+
330
+ log.info(" Schema: %s", model.schema)
331
+
332
+ extractors = {
333
+ "window_area_m2": extract_window_area,
334
+ "floor_area_m2": extract_floor_area,
335
+ "roof_area_m2": extract_roof_area,
336
+ }
337
+ for key, fn in extractors.items():
338
+ try:
339
+ base[key] = fn(model)
340
+ except Exception as exc:
341
+ log.warning(" %s extraction failed: %s", key, exc)
342
+
343
+ try:
344
+ orientation = extract_orientation(model)
345
+ base.update(orientation)
346
+ except Exception as exc:
347
+ log.warning(" Orientation extraction failed: %s", exc)
348
+
349
+ return base
350
+
351
+
352
+ def open_model(ifc_path: Path | str) -> ifcopenshell.file:
353
+ """Open an IFC file and return the ifcopenshell model handle."""
354
+ return ifcopenshell.open(str(ifc_path))
355
+
356
+
357
+ # ── Batch scanning ───────────────────────────────────────────────────────────
358
+
359
+ def find_ifc_files(root_dir: Path) -> list[Path]:
360
+ """Recursively find all .ifc files under root_dir, sorted."""
361
+ return sorted(root_dir.rglob("*.ifc"))
362
+
363
+
364
+ def scan_all(
365
+ root_dir: Path,
366
+ output_csv: Path | None = None,
367
+ ) -> list[dict]:
368
+ """
369
+ Scan all IFC files under *root_dir* and return a list of result dicts.
370
+ Optionally writes results to a CSV file.
371
+ """
372
+ ifc_files = find_ifc_files(root_dir)
373
+ log.info("Found %d IFC file(s) under: %s", len(ifc_files), root_dir)
374
+
375
+ results: list[dict] = []
376
+ for i, ifc_path in enumerate(ifc_files, 1):
377
+ log.info("[%d/%d] %s/%s", i, len(ifc_files), ifc_path.parent.name, ifc_path.name)
378
+ result = extract_all(ifc_path)
379
+ results.append(result)
380
+
381
+ if output_csv:
382
+ _write_csv(results, output_csv)
383
+
384
+ return results
385
+
386
+
387
+ def _write_csv(results: list[dict], output_path: Path) -> None:
388
+ with open(output_path, "w", newline="", encoding="utf-8") as f:
389
+ writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS, extrasaction="ignore")
390
+ writer.writeheader()
391
+ writer.writerows(results)
392
+ log.info("CSV written to: %s", output_path)
393
+
394
+
395
+ def print_summary_table(results: list[dict]) -> None:
396
+ """Print a tabulate-formatted summary to stdout."""
397
+ try:
398
+ from tabulate import tabulate
399
+ except ImportError:
400
+ log.warning("tabulate not installed β€” skipping summary table")
401
+ return
402
+
403
+ display_cols = [
404
+ "project_name", "ifc_file",
405
+ "window_area_m2", "floor_area_m2", "roof_area_m2",
406
+ "true_north_angle_deg", "latitude", "longitude",
407
+ ]
408
+ headers = [
409
+ "Project", "File",
410
+ "Window mΒ²", "Floor mΒ²", "Roof mΒ²",
411
+ "TrueNorthΒ°", "Lat", "Lon",
412
+ ]
413
+
414
+ table_data = []
415
+ for r in results:
416
+ row = []
417
+ for col in display_cols:
418
+ val = r.get(col)
419
+ if val is None:
420
+ row.append("N/A")
421
+ elif isinstance(val, float):
422
+ row.append(f"{val:.2f}")
423
+ else:
424
+ row.append(str(val))
425
+ if r.get("error"):
426
+ row[-1] = "ERROR"
427
+ table_data.append(row)
428
+
429
+ print("\n" + tabulate(table_data, headers=headers, tablefmt="github"))
430
+
431
+ processed = [r for r in results if not r.get("error")]
432
+ errors = [r for r in results if r.get("error")]
433
+ print(f"\nFiles scanned : {len(results)}")
434
+ print(f"Errors : {len(errors)}")
435
+ print(f"Window data : {sum(1 for r in processed if r['window_area_m2'] is not None)}/{len(processed)} files")
436
+ print(f"Floor data : {sum(1 for r in processed if r['floor_area_m2'] is not None)}/{len(processed)} files")
437
+ print(f"Roof data : {sum(1 for r in processed if r['roof_area_m2'] is not None)}/{len(processed)} files")
438
+ print(f"Orientation : {sum(1 for r in processed if r['true_north_angle_deg'] is not None)}/{len(processed)} files")
439
+ if errors:
440
+ print("\nFiles with errors:")
441
+ for r in errors:
442
+ print(f" {r['project_name']}/{r['ifc_file']}: {r['error']}")
teams/lux-ai/Final pipeline/ifc_roof_parser.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ifc_roof_parser.py β€” Geometry-based roof segment extraction from IFC files.
3
+
4
+ Analyses the 3D triangulated mesh of roof elements to produce per-segment
5
+ tilt, azimuth, and area values that the solar production engine consumes.
6
+
7
+ Algorithm:
8
+ 1. Find all IfcRoof + decomposed IfcSlab children + standalone ROOF slabs
9
+ 2. Triangulate each element via ifcopenshell.geom (world coordinates)
10
+ 3. Compute face normals and triangle areas
11
+ 4. Cluster upward-facing triangles by normal direction (angular tolerance)
12
+ 5. Compute area-weighted average tilt & azimuth per cluster
13
+ 6. Apply true-north rotation so azimuths are real-world compass bearings
14
+
15
+ Cross-validates geometry-derived total area against property-set roof_area.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import math
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ import numpy as np
26
+
27
+ import ifcopenshell
28
+ import ifcopenshell.geom
29
+
30
+ from solar_pipeline.config import DEFAULT_ANGLE_TOLERANCE_DEG, MIN_SEGMENT_AREA_M2
31
+ from solar_pipeline.ifc_metadata_extractor import (
32
+ extract_roof_area,
33
+ extract_true_north,
34
+ get_area_scale,
35
+ )
36
+
37
+ log = logging.getLogger(__name__)
38
+
39
+
40
+ # ── Element discovery ─────────────────────────────────────────────────────────
41
+
42
+ def get_roof_elements(model: ifcopenshell.file) -> list:
43
+ """
44
+ Collect all roof-related IFC elements:
45
+ - IfcRoof entities (decomposed through IfcRelAggregates if present)
46
+ - Standalone IfcSlab with PredefinedType == ROOF
47
+ Returns leaf elements (slabs or monolithic roofs) ready for geometry extraction.
48
+ """
49
+ elements: list = []
50
+ seen_ids: set[int] = set()
51
+
52
+ # IfcRoof β†’ decomposed sub-elements
53
+ for roof in model.by_type("IfcRoof"):
54
+ children = _get_decomposed_children(roof)
55
+ if children:
56
+ for child in children:
57
+ if child.id() not in seen_ids:
58
+ elements.append(child)
59
+ seen_ids.add(child.id())
60
+ else:
61
+ # Monolithic roof β€” process the IfcRoof itself
62
+ if roof.id() not in seen_ids:
63
+ elements.append(roof)
64
+ seen_ids.add(roof.id())
65
+
66
+ # Standalone IfcSlab with ROOF type (not already captured)
67
+ for slab in model.by_type("IfcSlab"):
68
+ pt = getattr(slab, "PredefinedType", None)
69
+ if pt == "ROOF" and slab.id() not in seen_ids:
70
+ elements.append(slab)
71
+ seen_ids.add(slab.id())
72
+
73
+ log.info(" Found %d roof element(s) for geometry extraction", len(elements))
74
+ return elements
75
+
76
+
77
+ def _get_decomposed_children(element) -> list:
78
+ """Walk IfcRelAggregates to find child elements (e.g. IfcSlab under IfcRoof)."""
79
+ children: list = []
80
+ for rel in getattr(element, "IsDecomposedBy", []):
81
+ if rel.is_a("IfcRelAggregates"):
82
+ children.extend(rel.RelatedObjects)
83
+ return children
84
+
85
+
86
+ # ── Geometry extraction ───────────────────────────────────────────────────────
87
+
88
+ def _make_geom_settings() -> ifcopenshell.geom.settings:
89
+ """Create geometry settings for triangulated mesh in world coordinates."""
90
+ settings = ifcopenshell.geom.settings()
91
+ settings.set("use-world-coords", True)
92
+ return settings
93
+
94
+
95
+ def extract_geometry(
96
+ element,
97
+ settings: ifcopenshell.geom.settings,
98
+ ) -> tuple[np.ndarray, np.ndarray] | None:
99
+ """
100
+ Triangulate an IFC element and return (vertices, faces).
101
+
102
+ vertices: (N, 3) float64 β€” XYZ coordinates
103
+ faces: (M, 3) int β€” triangle vertex indices
104
+
105
+ Returns None if geometry extraction fails.
106
+ """
107
+ try:
108
+ shape = ifcopenshell.geom.create_shape(settings, element)
109
+ except Exception as exc:
110
+ log.warning(" Geometry extraction failed for #%d (%s): %s",
111
+ element.id(), element.is_a(), exc)
112
+ return None
113
+
114
+ # ifcopenshell returns flat arrays
115
+ verts_flat = shape.geometry.verts
116
+ faces_flat = shape.geometry.faces
117
+
118
+ if not verts_flat or not faces_flat:
119
+ return None
120
+
121
+ vertices = np.array(verts_flat, dtype=np.float64).reshape(-1, 3)
122
+ faces = np.array(faces_flat, dtype=np.int32).reshape(-1, 3)
123
+
124
+ return vertices, faces
125
+
126
+
127
+ # ── Normal & area computation ─────────────────────────────────────────────────
128
+
129
+ def compute_face_normals(
130
+ vertices: np.ndarray,
131
+ faces: np.ndarray,
132
+ ) -> tuple[np.ndarray, np.ndarray]:
133
+ """
134
+ Compute unit normals and areas for every triangle.
135
+
136
+ Returns:
137
+ normals: (M, 3) float64 β€” unit normal per face
138
+ areas: (M,) float64 β€” area per face in model unitsΒ²
139
+ """
140
+ v0 = vertices[faces[:, 0]]
141
+ v1 = vertices[faces[:, 1]]
142
+ v2 = vertices[faces[:, 2]]
143
+
144
+ edge1 = v1 - v0
145
+ edge2 = v2 - v0
146
+ cross = np.cross(edge1, edge2)
147
+
148
+ magnitudes = np.linalg.norm(cross, axis=1, keepdims=True)
149
+ # Avoid division by zero for degenerate triangles
150
+ safe_mag = np.where(magnitudes > 1e-12, magnitudes, 1e-12)
151
+ normals = cross / safe_mag
152
+ areas = (magnitudes.flatten() / 2.0)
153
+
154
+ return normals, areas
155
+
156
+
157
+ # ── Clustering ────────────────────────────────────────────────────────────────
158
+
159
+ def cluster_faces_by_normal(
160
+ normals: np.ndarray,
161
+ areas: np.ndarray,
162
+ angle_tolerance: float = DEFAULT_ANGLE_TOLERANCE_DEG,
163
+ ) -> list[list[int]]:
164
+ """
165
+ Greedy angular clustering of upward-facing triangles.
166
+
167
+ Only considers triangles where normal Z > 0 (upward-facing β€” solar relevant).
168
+ Groups triangles whose normals are within *angle_tolerance* degrees.
169
+
170
+ Returns list of clusters, each a list of face indices.
171
+ """
172
+ cos_tol = math.cos(math.radians(angle_tolerance))
173
+ clusters: list[list[int]] = []
174
+ cluster_normals: list[np.ndarray] = []
175
+
176
+ # Filter to upward-facing triangles
177
+ upward_mask = normals[:, 2] > 0
178
+ upward_indices = np.where(upward_mask)[0]
179
+
180
+ for idx in upward_indices:
181
+ n = normals[idx]
182
+ placed = False
183
+ for ci, cn in enumerate(cluster_normals):
184
+ dot = float(np.dot(n, cn))
185
+ if dot >= cos_tol:
186
+ clusters[ci].append(int(idx))
187
+ # Update cluster normal (area-weighted running average)
188
+ total_area = sum(areas[i] for i in clusters[ci])
189
+ if total_area > 0:
190
+ weighted = sum(areas[i] * normals[i] for i in clusters[ci])
191
+ mag = np.linalg.norm(weighted)
192
+ if mag > 1e-12:
193
+ cluster_normals[ci] = weighted / mag
194
+ placed = True
195
+ break
196
+
197
+ if not placed:
198
+ clusters.append([int(idx)])
199
+ cluster_normals.append(n.copy())
200
+
201
+ return clusters
202
+
203
+
204
+ # ── Segment properties ────────────────────────────────────────────────────────
205
+
206
+ def compute_segment_properties(
207
+ normals: np.ndarray,
208
+ areas: np.ndarray,
209
+ cluster_indices: list[int],
210
+ area_scale: float = 1.0,
211
+ ) -> dict:
212
+ """
213
+ Compute tilt, azimuth, and total area for one cluster of faces.
214
+
215
+ tilt: degrees from horizontal (0Β° = flat, 90Β° = vertical)
216
+ azimuth: degrees clockwise from north (0Β° N, 90Β° E, 180Β° S, 270Β° W)
217
+ β€” in MODEL coordinates (true-north correction applied later)
218
+ area: cluster area in mΒ²
219
+
220
+ Returns dict with keys: area, tilt, azimuth.
221
+ """
222
+ idx = np.array(cluster_indices)
223
+ cluster_areas = areas[idx]
224
+ cluster_normals = normals[idx]
225
+
226
+ total_area = float(cluster_areas.sum()) * area_scale
227
+
228
+ # Area-weighted average normal
229
+ weighted = (cluster_normals.T * cluster_areas).T.sum(axis=0)
230
+ mag = np.linalg.norm(weighted)
231
+ if mag < 1e-12:
232
+ return {"area": total_area, "tilt": 0.0, "azimuth": 0.0}
233
+ avg_n = weighted / mag
234
+
235
+ # Tilt = arccos(nz)
236
+ tilt = math.degrees(math.acos(min(max(avg_n[2], -1.0), 1.0)))
237
+
238
+ # Azimuth = atan2(nx, ny) mod 360
239
+ # Convention: 0Β° = North (+Y), 90Β° = East (+X), 180Β° = South (-Y)
240
+ azimuth = math.degrees(math.atan2(avg_n[0], avg_n[1])) % 360.0
241
+
242
+ return {
243
+ "area": round(total_area, 2),
244
+ "tilt": round(tilt, 1),
245
+ "azimuth": round(azimuth, 1),
246
+ }
247
+
248
+
249
+ # ── Main entry point ─────────────────────────────────────────────────────────
250
+
251
+ def parse_roof_segments(
252
+ ifc_path: str | Path,
253
+ angle_tolerance: float = DEFAULT_ANGLE_TOLERANCE_DEG,
254
+ min_area: float = MIN_SEGMENT_AREA_M2,
255
+ apply_true_north: bool = True,
256
+ ) -> list[dict]:
257
+ """
258
+ Parse an IFC file and return a list of roof segments.
259
+
260
+ Each segment dict has keys: id, area, tilt, azimuth.
261
+
262
+ Parameters
263
+ ----------
264
+ ifc_path : path to the .ifc file
265
+ angle_tolerance : degrees β€” max normal deviation within a cluster
266
+ min_area : mΒ² β€” ignore clusters smaller than this
267
+ apply_true_north : rotate azimuths by the model's TrueNorth angle
268
+
269
+ Returns
270
+ -------
271
+ list of dicts: [{"id": str, "area": float, "tilt": float, "azimuth": float}, ...]
272
+ """
273
+ ifc_path = Path(ifc_path)
274
+ log.info("Parsing roof geometry: %s", ifc_path.name)
275
+
276
+ try:
277
+ model = ifcopenshell.open(str(ifc_path))
278
+ except Exception as exc:
279
+ log.error("Cannot open %s: %s", ifc_path, exc)
280
+ return []
281
+
282
+ area_scale = get_area_scale(model)
283
+ # For geometry (vertex coordinates) we need the length scale squared
284
+ # because area = lengthΒ². However ifcopenshell.geom with world-coords
285
+ # already applies the unit conversion, so we assume metres here.
286
+ # If models are in feet, the geom API still returns metres when
287
+ # USE_WORLD_COORDS is True. We keep area_scale for the cross-validation
288
+ # with property-set values only.
289
+ geom_area_scale = 1.0 # world-coords are in metres
290
+
291
+ elements = get_roof_elements(model)
292
+ if not elements:
293
+ log.warning(" No roof elements found in %s", ifc_path.name)
294
+ return []
295
+
296
+ settings = _make_geom_settings()
297
+
298
+ # Collect all face normals + areas across all roof elements
299
+ all_normals: list[np.ndarray] = []
300
+ all_areas: list[np.ndarray] = []
301
+
302
+ for elem in elements:
303
+ geom_data = extract_geometry(elem, settings)
304
+ if geom_data is None:
305
+ continue
306
+ verts, faces = geom_data
307
+ normals, areas = compute_face_normals(verts, faces)
308
+ all_normals.append(normals)
309
+ all_areas.append(areas)
310
+
311
+ if not all_normals:
312
+ log.warning(" Could not extract geometry from any roof element in %s",
313
+ ifc_path.name)
314
+ return []
315
+
316
+ normals = np.vstack(all_normals)
317
+ areas = np.concatenate(all_areas)
318
+
319
+ # Cluster faces by orientation
320
+ clusters = cluster_faces_by_normal(normals, areas, angle_tolerance)
321
+
322
+ # Build segment list
323
+ segments: list[dict] = []
324
+ seg_idx = 1
325
+ for cluster in clusters:
326
+ props = compute_segment_properties(normals, areas, cluster, geom_area_scale)
327
+ if props["area"] < min_area:
328
+ continue
329
+ props["id"] = f"Roof_Seg_{seg_idx:02d}"
330
+ segments.append(props)
331
+ seg_idx += 1
332
+
333
+ # ── True-north azimuth correction ─────────────────────────────────────
334
+ if apply_true_north and segments:
335
+ tn = extract_true_north(model)
336
+ if tn is not None and abs(tn) > 0.01 and abs(tn - 360.0) > 0.01:
337
+ log.info(" Applying true-north correction: %.2fΒ°", tn)
338
+ for seg in segments:
339
+ seg["azimuth"] = round((seg["azimuth"] + tn) % 360.0, 1)
340
+
341
+ # ── Cross-validate against property-set roof area ─────────────────────
342
+ if segments:
343
+ geom_total = sum(s["area"] for s in segments)
344
+ pset_area = extract_roof_area(model)
345
+ if pset_area is not None and pset_area > 0:
346
+ diff_pct = abs(geom_total - pset_area) / pset_area * 100
347
+ if diff_pct > 20:
348
+ log.warning(
349
+ " Roof area mismatch: geometry=%.1f mΒ², property-set=%.1f mΒ² (%.0f%% diff)",
350
+ geom_total, pset_area, diff_pct,
351
+ )
352
+ else:
353
+ log.info(
354
+ " Roof area validated: geometry=%.1f mΒ², property-set=%.1f mΒ² (%.0f%% diff)",
355
+ geom_total, pset_area, diff_pct,
356
+ )
357
+
358
+ log.info(" Extracted %d roof segment(s) from %s", len(segments), ifc_path.name)
359
+ return segments
teams/lux-ai/Final pipeline/key_aliases.json ADDED
@@ -0,0 +1,3323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "window_area": [
3
+ {
4
+ "entity": "IfcWindow",
5
+ "source": "qset",
6
+ "set_name": "Qto_WindowBaseQuantities",
7
+ "key": "Area"
8
+ },
9
+ {
10
+ "entity": "IfcWindow",
11
+ "source": "qset",
12
+ "set_name": "BaseQuantities",
13
+ "key": "Area"
14
+ },
15
+ {
16
+ "entity": "IfcWindow",
17
+ "source": "qset",
18
+ "set_name": "BaseQuantities",
19
+ "key": "GrossArea"
20
+ },
21
+ {
22
+ "entity": "IfcWindow",
23
+ "source": "attr",
24
+ "keys": [
25
+ "OverallHeight",
26
+ "OverallWidth"
27
+ ],
28
+ "op": "multiply"
29
+ }
30
+ ],
31
+ "floor_area": [
32
+ {
33
+ "entity": "IfcSpace",
34
+ "source": "qset",
35
+ "set_name": "Qto_SpaceBaseQuantities",
36
+ "key": "NetFloorArea"
37
+ },
38
+ {
39
+ "entity": "IfcSpace",
40
+ "source": "qset",
41
+ "set_name": "Qto_SpaceBaseQuantities",
42
+ "key": "GrossFloorArea"
43
+ },
44
+ {
45
+ "entity": "IfcSpace",
46
+ "source": "qset",
47
+ "set_name": "BaseQuantities",
48
+ "key": "NetFloorArea"
49
+ },
50
+ {
51
+ "entity": "IfcSpace",
52
+ "source": "qset",
53
+ "set_name": "BaseQuantities",
54
+ "key": "GrossFloorArea"
55
+ },
56
+ {
57
+ "entity": "IfcSpace",
58
+ "source": "qset",
59
+ "set_name": "GSA Space Areas",
60
+ "key": "GSA BIM Area"
61
+ },
62
+ {
63
+ "entity": "IfcSlab",
64
+ "source": "qset",
65
+ "set_name": "Qto_SlabBaseQuantities",
66
+ "key": "NetArea",
67
+ "predefined_type": "FLOOR"
68
+ },
69
+ {
70
+ "entity": "IfcSlab",
71
+ "source": "qset",
72
+ "set_name": "BaseQuantities",
73
+ "key": "NetArea",
74
+ "predefined_type": "FLOOR"
75
+ },
76
+ {
77
+ "entity": "IfcSlab",
78
+ "source": "qset",
79
+ "set_name": "BaseQuantities",
80
+ "key": "GrossArea",
81
+ "predefined_type": "FLOOR"
82
+ }
83
+ ],
84
+ "roof_area": [
85
+ {
86
+ "entity": "IfcRoof",
87
+ "source": "qset",
88
+ "set_name": "Qto_RoofBaseQuantities",
89
+ "key": "NetArea"
90
+ },
91
+ {
92
+ "entity": "IfcRoof",
93
+ "source": "qset",
94
+ "set_name": "Qto_RoofBaseQuantities",
95
+ "key": "GrossArea"
96
+ },
97
+ {
98
+ "entity": "IfcRoof",
99
+ "source": "qset",
100
+ "set_name": "BaseQuantities",
101
+ "key": "NetArea"
102
+ },
103
+ {
104
+ "entity": "IfcSlab",
105
+ "source": "qset",
106
+ "set_name": "Qto_SlabBaseQuantities",
107
+ "key": "NetArea",
108
+ "predefined_type": "ROOF"
109
+ },
110
+ {
111
+ "entity": "IfcSlab",
112
+ "source": "qset",
113
+ "set_name": "Qto_SlabBaseQuantities",
114
+ "key": "GrossArea",
115
+ "predefined_type": "ROOF"
116
+ },
117
+ {
118
+ "entity": "IfcSlab",
119
+ "source": "qset",
120
+ "set_name": "BaseQuantities",
121
+ "key": "NetArea",
122
+ "predefined_type": "ROOF"
123
+ },
124
+ {
125
+ "entity": "IfcSlab",
126
+ "source": "qset",
127
+ "set_name": "BaseQuantities",
128
+ "key": "GrossArea",
129
+ "predefined_type": "ROOF"
130
+ }
131
+ ],
132
+ "true_north_angle": [
133
+ {
134
+ "entity": "IfcGeometricRepresentationContext",
135
+ "source": "attr",
136
+ "key": "TrueNorth",
137
+ "note": "DirectionRatios (X,Y) β†’ atan2(x,y) β†’ compass bearing"
138
+ }
139
+ ],
140
+ "latitude": [
141
+ {
142
+ "entity": "IfcSite",
143
+ "source": "attr",
144
+ "key": "RefLatitude",
145
+ "note": "IfcCompoundPlaneAngleMeasure [deg,min,sec,microsec]"
146
+ }
147
+ ],
148
+ "longitude": [
149
+ {
150
+ "entity": "IfcSite",
151
+ "source": "attr",
152
+ "key": "RefLongitude",
153
+ "note": "IfcCompoundPlaneAngleMeasure [deg,min,sec,microsec]"
154
+ }
155
+ ],
156
+ "roof_slope": [
157
+ {
158
+ "entity": "IfcRoof",
159
+ "source": "pset",
160
+ "set_name": "Dimensions",
161
+ "key": "Slope",
162
+ "note": "Revit-exported roof slope in degrees"
163
+ },
164
+ {
165
+ "entity": "IfcRoof",
166
+ "source": "pset",
167
+ "set_name": "Abmessungen",
168
+ "key": "Neigung",
169
+ "note": "ArchiCAD German β€” roof slope"
170
+ },
171
+ {
172
+ "entity": "IfcSlab",
173
+ "source": "pset",
174
+ "set_name": "Dimensions",
175
+ "key": "Slope",
176
+ "predefined_type": "ROOF",
177
+ "note": "Revit-exported roof slab slope"
178
+ }
179
+ ],
180
+ "thermal_transmittance": [
181
+ {
182
+ "entity": "IfcRoof",
183
+ "source": "pset",
184
+ "set_name": "Pset_RoofCommon",
185
+ "key": "ThermalTransmittance",
186
+ "note": "U-value in W/(mΒ²Β·K) β€” for energy modelling"
187
+ },
188
+ {
189
+ "entity": "IfcSlab",
190
+ "source": "pset",
191
+ "set_name": "Pset_SlabCommon",
192
+ "key": "ThermalTransmittance",
193
+ "predefined_type": "ROOF",
194
+ "note": "U-value for roof slabs"
195
+ },
196
+ {
197
+ "entity": "IfcWall",
198
+ "source": "pset",
199
+ "set_name": "Pset_WallCommon",
200
+ "key": "ThermalTransmittance",
201
+ "note": "U-value for walls β€” future energy modelling"
202
+ }
203
+ ],
204
+ "_auto_discovered_area_keys": [
205
+ {
206
+ "entity": "IfcWindow",
207
+ "source": "qset",
208
+ "set_name": "ArchiCADQuantities",
209
+ "key": "OberflΓ€chenbereich",
210
+ "file_count": 2,
211
+ "projects": [
212
+ "ac20",
213
+ "fzk_house"
214
+ ],
215
+ "auto_discovered": true
216
+ },
217
+ {
218
+ "entity": "IfcWindow",
219
+ "source": "qset",
220
+ "set_name": "ArchiCADQuantities",
221
+ "key": "F/T OberflΓ€che der Γ–ffnung auf der Anschlagseite",
222
+ "file_count": 2,
223
+ "projects": [
224
+ "ac20",
225
+ "fzk_house"
226
+ ],
227
+ "auto_discovered": true
228
+ },
229
+ {
230
+ "entity": "IfcWindow",
231
+ "source": "qset",
232
+ "set_name": "ArchiCADQuantities",
233
+ "key": "F/T OberflΓ€che der Γ–ffnung gegenΓΌber der Anschlagseite",
234
+ "file_count": 2,
235
+ "projects": [
236
+ "ac20",
237
+ "fzk_house"
238
+ ],
239
+ "auto_discovered": true
240
+ },
241
+ {
242
+ "entity": "IfcWindow",
243
+ "source": "qset",
244
+ "set_name": "ArchiCADQuantities",
245
+ "key": "Nominale T/F Γ–ffnungsoberflΓ€che auf der Anschlagsseite",
246
+ "file_count": 2,
247
+ "projects": [
248
+ "ac20",
249
+ "fzk_house"
250
+ ],
251
+ "auto_discovered": true
252
+ },
253
+ {
254
+ "entity": "IfcWindow",
255
+ "source": "qset",
256
+ "set_name": "ArchiCADQuantities",
257
+ "key": "Nominale T/F Γ–ffnungsoberflΓ€che auf der Seite gegenΓΌber der Anschlagsseite",
258
+ "file_count": 2,
259
+ "projects": [
260
+ "ac20",
261
+ "fzk_house"
262
+ ],
263
+ "auto_discovered": true
264
+ },
265
+ {
266
+ "entity": "IfcWindow",
267
+ "source": "qset",
268
+ "set_name": "ArchiCADQuantities",
269
+ "key": "T/F Γ–ffnung Nominaler OberflΓ€chenbereich",
270
+ "file_count": 2,
271
+ "projects": [
272
+ "ac20",
273
+ "fzk_house"
274
+ ],
275
+ "auto_discovered": true
276
+ },
277
+ {
278
+ "entity": "IfcWindow",
279
+ "source": "qset",
280
+ "set_name": "ArchiCADQuantities",
281
+ "key": "Surface Area",
282
+ "file_count": 1,
283
+ "projects": [
284
+ "sixty5"
285
+ ],
286
+ "auto_discovered": true
287
+ },
288
+ {
289
+ "entity": "IfcWindow",
290
+ "source": "qset",
291
+ "set_name": "ArchiCADQuantities",
292
+ "key": "W/D Opening Surface Area",
293
+ "file_count": 1,
294
+ "projects": [
295
+ "sixty5"
296
+ ],
297
+ "auto_discovered": true
298
+ },
299
+ {
300
+ "entity": "IfcWindow",
301
+ "source": "qset",
302
+ "set_name": "ArchiCADQuantities",
303
+ "key": "W/D Opening Nominal Surface Area",
304
+ "file_count": 1,
305
+ "projects": [
306
+ "sixty5"
307
+ ],
308
+ "auto_discovered": true
309
+ },
310
+ {
311
+ "entity": "IfcWindow",
312
+ "source": "qset",
313
+ "set_name": "ArchiCADQuantities",
314
+ "key": "Nominal W/D Opening Surface Area on the Reveal Side",
315
+ "file_count": 1,
316
+ "projects": [
317
+ "sixty5"
318
+ ],
319
+ "auto_discovered": true
320
+ },
321
+ {
322
+ "entity": "IfcWindow",
323
+ "source": "qset",
324
+ "set_name": "ArchiCADQuantities",
325
+ "key": "Nominal W/D Opening Surface Area on the Side Opposite to the Reveal Side",
326
+ "file_count": 1,
327
+ "projects": [
328
+ "sixty5"
329
+ ],
330
+ "auto_discovered": true
331
+ },
332
+ {
333
+ "entity": "IfcWindow",
334
+ "source": "qset",
335
+ "set_name": "ArchiCADQuantities",
336
+ "key": "Area",
337
+ "file_count": 1,
338
+ "projects": [
339
+ "sixty5"
340
+ ],
341
+ "auto_discovered": true
342
+ },
343
+ {
344
+ "entity": "IfcDoor",
345
+ "source": "qset",
346
+ "set_name": "BaseQuantities",
347
+ "key": "Area",
348
+ "file_count": 5,
349
+ "projects": [
350
+ "ac20",
351
+ "fzk_house",
352
+ "molio",
353
+ "sixty5",
354
+ "smiley_west"
355
+ ],
356
+ "auto_discovered": true
357
+ },
358
+ {
359
+ "entity": "IfcDoor",
360
+ "source": "qset",
361
+ "set_name": "ArchiCADQuantities",
362
+ "key": "OberflΓ€chenbereich",
363
+ "file_count": 2,
364
+ "projects": [
365
+ "ac20",
366
+ "fzk_house"
367
+ ],
368
+ "auto_discovered": true
369
+ },
370
+ {
371
+ "entity": "IfcDoor",
372
+ "source": "qset",
373
+ "set_name": "ArchiCADQuantities",
374
+ "key": "F/T OberflΓ€che der Γ–ffnung auf der Anschlagseite",
375
+ "file_count": 2,
376
+ "projects": [
377
+ "ac20",
378
+ "fzk_house"
379
+ ],
380
+ "auto_discovered": true
381
+ },
382
+ {
383
+ "entity": "IfcDoor",
384
+ "source": "qset",
385
+ "set_name": "ArchiCADQuantities",
386
+ "key": "F/T OberflΓ€che der Γ–ffnung gegenΓΌber der Anschlagseite",
387
+ "file_count": 2,
388
+ "projects": [
389
+ "ac20",
390
+ "fzk_house"
391
+ ],
392
+ "auto_discovered": true
393
+ },
394
+ {
395
+ "entity": "IfcDoor",
396
+ "source": "qset",
397
+ "set_name": "ArchiCADQuantities",
398
+ "key": "Nominale T/F Γ–ffnungsoberflΓ€che auf der Anschlagsseite",
399
+ "file_count": 2,
400
+ "projects": [
401
+ "ac20",
402
+ "fzk_house"
403
+ ],
404
+ "auto_discovered": true
405
+ },
406
+ {
407
+ "entity": "IfcDoor",
408
+ "source": "qset",
409
+ "set_name": "ArchiCADQuantities",
410
+ "key": "Nominale T/F Γ–ffnungsoberflΓ€che auf der Seite gegenΓΌber der Anschlagsseite",
411
+ "file_count": 2,
412
+ "projects": [
413
+ "ac20",
414
+ "fzk_house"
415
+ ],
416
+ "auto_discovered": true
417
+ },
418
+ {
419
+ "entity": "IfcDoor",
420
+ "source": "qset",
421
+ "set_name": "ArchiCADQuantities",
422
+ "key": "T/F Γ–ffnung Nominaler OberflΓ€chenbereich",
423
+ "file_count": 2,
424
+ "projects": [
425
+ "ac20",
426
+ "fzk_house"
427
+ ],
428
+ "auto_discovered": true
429
+ },
430
+ {
431
+ "entity": "IfcDoor",
432
+ "source": "qset",
433
+ "set_name": "ArchiCADQuantities",
434
+ "key": "Surface Area",
435
+ "file_count": 2,
436
+ "projects": [
437
+ "molio",
438
+ "sixty5"
439
+ ],
440
+ "auto_discovered": true
441
+ },
442
+ {
443
+ "entity": "IfcDoor",
444
+ "source": "qset",
445
+ "set_name": "ArchiCADQuantities",
446
+ "key": "Bottom Surface Area (Net)",
447
+ "file_count": 1,
448
+ "projects": [
449
+ "molio"
450
+ ],
451
+ "auto_discovered": true
452
+ },
453
+ {
454
+ "entity": "IfcDoor",
455
+ "source": "qset",
456
+ "set_name": "ArchiCADQuantities",
457
+ "key": "W/D Opening Surface Area",
458
+ "file_count": 2,
459
+ "projects": [
460
+ "molio",
461
+ "sixty5"
462
+ ],
463
+ "auto_discovered": true
464
+ },
465
+ {
466
+ "entity": "IfcDoor",
467
+ "source": "qset",
468
+ "set_name": "ArchiCADQuantities",
469
+ "key": "W/D Opening Nominal Surface Area",
470
+ "file_count": 2,
471
+ "projects": [
472
+ "molio",
473
+ "sixty5"
474
+ ],
475
+ "auto_discovered": true
476
+ },
477
+ {
478
+ "entity": "IfcDoor",
479
+ "source": "qset",
480
+ "set_name": "ArchiCADQuantities",
481
+ "key": "W/D Opening Surface Area on the Reveal Side",
482
+ "file_count": 1,
483
+ "projects": [
484
+ "molio"
485
+ ],
486
+ "auto_discovered": true
487
+ },
488
+ {
489
+ "entity": "IfcDoor",
490
+ "source": "qset",
491
+ "set_name": "ArchiCADQuantities",
492
+ "key": "W/D Opening Surface Area on the Side Opposite to the Reveal Side",
493
+ "file_count": 1,
494
+ "projects": [
495
+ "molio"
496
+ ],
497
+ "auto_discovered": true
498
+ },
499
+ {
500
+ "entity": "IfcDoor",
501
+ "source": "qset",
502
+ "set_name": "ArchiCADQuantities",
503
+ "key": "Nominal W/D Opening Surface Area on the Reveal Side",
504
+ "file_count": 2,
505
+ "projects": [
506
+ "molio",
507
+ "sixty5"
508
+ ],
509
+ "auto_discovered": true
510
+ },
511
+ {
512
+ "entity": "IfcDoor",
513
+ "source": "qset",
514
+ "set_name": "ArchiCADQuantities",
515
+ "key": "Nominal W/D Opening Surface Area on the Side Opposite to the Reveal Side",
516
+ "file_count": 2,
517
+ "projects": [
518
+ "molio",
519
+ "sixty5"
520
+ ],
521
+ "auto_discovered": true
522
+ },
523
+ {
524
+ "entity": "IfcDoor",
525
+ "source": "qset",
526
+ "set_name": "Qto_DoorBaseQuantities",
527
+ "key": "Area",
528
+ "file_count": 3,
529
+ "projects": [
530
+ "digital_hub",
531
+ "fantasy_office_building_1",
532
+ "fantasy_office_building_2"
533
+ ],
534
+ "auto_discovered": true
535
+ },
536
+ {
537
+ "entity": "IfcSpace",
538
+ "source": "qset",
539
+ "set_name": "GSA Space Areas",
540
+ "key": "GSA Space Areas",
541
+ "file_count": 1,
542
+ "projects": [
543
+ "samuel_macalister_sample_house"
544
+ ],
545
+ "auto_discovered": true
546
+ },
547
+ {
548
+ "entity": "IfcSpace",
549
+ "source": "qset",
550
+ "set_name": "BaseQuantities",
551
+ "key": "SpaceNetFloorAreaBOMA",
552
+ "file_count": 5,
553
+ "projects": [
554
+ "ac20",
555
+ "fzk_house",
556
+ "molio",
557
+ "sixty5",
558
+ "smiley_west"
559
+ ],
560
+ "auto_discovered": true
561
+ },
562
+ {
563
+ "entity": "IfcSpace",
564
+ "source": "qset",
565
+ "set_name": "BaseQuantities",
566
+ "key": "SpaceUsableFloorAreaBOMA",
567
+ "file_count": 5,
568
+ "projects": [
569
+ "ac20",
570
+ "fzk_house",
571
+ "molio",
572
+ "sixty5",
573
+ "smiley_west"
574
+ ],
575
+ "auto_discovered": true
576
+ },
577
+ {
578
+ "entity": "IfcSpace",
579
+ "source": "qset",
580
+ "set_name": "BaseQuantities",
581
+ "key": "GrossCeilingArea",
582
+ "file_count": 5,
583
+ "projects": [
584
+ "ac20",
585
+ "fzk_house",
586
+ "molio",
587
+ "sixty5",
588
+ "smiley_west"
589
+ ],
590
+ "auto_discovered": true
591
+ },
592
+ {
593
+ "entity": "IfcSpace",
594
+ "source": "qset",
595
+ "set_name": "BaseQuantities",
596
+ "key": "NetCeilingArea",
597
+ "file_count": 5,
598
+ "projects": [
599
+ "ac20",
600
+ "fzk_house",
601
+ "molio",
602
+ "sixty5",
603
+ "smiley_west"
604
+ ],
605
+ "auto_discovered": true
606
+ },
607
+ {
608
+ "entity": "IfcSpace",
609
+ "source": "qset",
610
+ "set_name": "BaseQuantities",
611
+ "key": "GrossWallArea",
612
+ "file_count": 5,
613
+ "projects": [
614
+ "ac20",
615
+ "fzk_house",
616
+ "molio",
617
+ "sixty5",
618
+ "smiley_west"
619
+ ],
620
+ "auto_discovered": true
621
+ },
622
+ {
623
+ "entity": "IfcSpace",
624
+ "source": "qset",
625
+ "set_name": "BaseQuantities",
626
+ "key": "NetWallArea",
627
+ "file_count": 5,
628
+ "projects": [
629
+ "ac20",
630
+ "fzk_house",
631
+ "molio",
632
+ "sixty5",
633
+ "smiley_west"
634
+ ],
635
+ "auto_discovered": true
636
+ },
637
+ {
638
+ "entity": "IfcSpace",
639
+ "source": "qset",
640
+ "set_name": "ArchiCADQuantities",
641
+ "key": "OberflΓ€chenbereich",
642
+ "file_count": 2,
643
+ "projects": [
644
+ "ac20",
645
+ "fzk_house"
646
+ ],
647
+ "auto_discovered": true
648
+ },
649
+ {
650
+ "entity": "IfcSpace",
651
+ "source": "qset",
652
+ "set_name": "ArchiCADQuantities",
653
+ "key": "FlΓ€che",
654
+ "file_count": 2,
655
+ "projects": [
656
+ "ac20",
657
+ "fzk_house"
658
+ ],
659
+ "auto_discovered": true
660
+ },
661
+ {
662
+ "entity": "IfcSpace",
663
+ "source": "qset",
664
+ "set_name": "ArchiCADQuantities",
665
+ "key": "Berechnete FlΓ€che (NRF)",
666
+ "file_count": 2,
667
+ "projects": [
668
+ "ac20",
669
+ "fzk_house"
670
+ ],
671
+ "auto_discovered": true
672
+ },
673
+ {
674
+ "entity": "IfcSpace",
675
+ "source": "qset",
676
+ "set_name": "ArchiCADQuantities",
677
+ "key": "TΓΌren-OberflΓ€chenbereich",
678
+ "file_count": 2,
679
+ "projects": [
680
+ "ac20",
681
+ "fzk_house"
682
+ ],
683
+ "auto_discovered": true
684
+ },
685
+ {
686
+ "entity": "IfcSpace",
687
+ "source": "qset",
688
+ "set_name": "ArchiCADQuantities",
689
+ "key": "StΓΌtzenabzugsflΓ€che",
690
+ "file_count": 2,
691
+ "projects": [
692
+ "ac20",
693
+ "fzk_house"
694
+ ],
695
+ "auto_discovered": true
696
+ },
697
+ {
698
+ "entity": "IfcSpace",
699
+ "source": "qset",
700
+ "set_name": "ArchiCADQuantities",
701
+ "key": "SchraffurabzugsflΓ€che",
702
+ "file_count": 2,
703
+ "projects": [
704
+ "ac20",
705
+ "fzk_house"
706
+ ],
707
+ "auto_discovered": true
708
+ },
709
+ {
710
+ "entity": "IfcSpace",
711
+ "source": "qset",
712
+ "set_name": "ArchiCADQuantities",
713
+ "key": "Niedrige AbzugsflΓ€che",
714
+ "file_count": 2,
715
+ "projects": [
716
+ "ac20",
717
+ "fzk_house"
718
+ ],
719
+ "auto_discovered": true
720
+ },
721
+ {
722
+ "entity": "IfcSpace",
723
+ "source": "qset",
724
+ "set_name": "ArchiCADQuantities",
725
+ "key": "WandabzugsflΓ€che",
726
+ "file_count": 2,
727
+ "projects": [
728
+ "ac20",
729
+ "fzk_house"
730
+ ],
731
+ "auto_discovered": true
732
+ },
733
+ {
734
+ "entity": "IfcSpace",
735
+ "source": "qset",
736
+ "set_name": "ArchiCADQuantities",
737
+ "key": "Gemessene FlΓ€che",
738
+ "file_count": 2,
739
+ "projects": [
740
+ "ac20",
741
+ "fzk_house"
742
+ ],
743
+ "auto_discovered": true
744
+ },
745
+ {
746
+ "entity": "IfcSpace",
747
+ "source": "qset",
748
+ "set_name": "ArchiCADQuantities",
749
+ "key": "NettoflΓ€che",
750
+ "file_count": 2,
751
+ "projects": [
752
+ "ac20",
753
+ "fzk_house"
754
+ ],
755
+ "auto_discovered": true
756
+ },
757
+ {
758
+ "entity": "IfcSpace",
759
+ "source": "qset",
760
+ "set_name": "ArchiCADQuantities",
761
+ "key": "Reduzierte FlΓ€che",
762
+ "file_count": 2,
763
+ "projects": [
764
+ "ac20",
765
+ "fzk_house"
766
+ ],
767
+ "auto_discovered": true
768
+ },
769
+ {
770
+ "entity": "IfcSpace",
771
+ "source": "qset",
772
+ "set_name": "ArchiCADQuantities",
773
+ "key": "RaumflΓ€chen Reduzierung",
774
+ "file_count": 2,
775
+ "projects": [
776
+ "ac20",
777
+ "fzk_house"
778
+ ],
779
+ "auto_discovered": true
780
+ },
781
+ {
782
+ "entity": "IfcSpace",
783
+ "source": "qset",
784
+ "set_name": "ArchiCADQuantities",
785
+ "key": "GesamtabzugsflΓ€che",
786
+ "file_count": 2,
787
+ "projects": [
788
+ "ac20",
789
+ "fzk_house"
790
+ ],
791
+ "auto_discovered": true
792
+ },
793
+ {
794
+ "entity": "IfcSpace",
795
+ "source": "qset",
796
+ "set_name": "ArchiCADQuantities",
797
+ "key": "HeizkΓΆrpernische OberflΓ€chenbereich hinten",
798
+ "file_count": 2,
799
+ "projects": [
800
+ "ac20",
801
+ "fzk_house"
802
+ ],
803
+ "auto_discovered": true
804
+ },
805
+ {
806
+ "entity": "IfcSpace",
807
+ "source": "qset",
808
+ "set_name": "ArchiCADQuantities",
809
+ "key": "HeizkΓΆrpernische OberflΓ€chenbereich seitlich",
810
+ "file_count": 2,
811
+ "projects": [
812
+ "ac20",
813
+ "fzk_house"
814
+ ],
815
+ "auto_discovered": true
816
+ },
817
+ {
818
+ "entity": "IfcSpace",
819
+ "source": "qset",
820
+ "set_name": "ArchiCADQuantities",
821
+ "key": "HeizkΓΆrpernische OberflΓ€chenbereich oben",
822
+ "file_count": 2,
823
+ "projects": [
824
+ "ac20",
825
+ "fzk_house"
826
+ ],
827
+ "auto_discovered": true
828
+ },
829
+ {
830
+ "entity": "IfcSpace",
831
+ "source": "qset",
832
+ "set_name": "ArchiCADQuantities",
833
+ "key": "Wand-OberflΓ€chenbereich",
834
+ "file_count": 2,
835
+ "projects": [
836
+ "ac20",
837
+ "fzk_house"
838
+ ],
839
+ "auto_discovered": true
840
+ },
841
+ {
842
+ "entity": "IfcSpace",
843
+ "source": "qset",
844
+ "set_name": "ArchiCADQuantities",
845
+ "key": "Fenster-OberflΓ€chenbereich",
846
+ "file_count": 2,
847
+ "projects": [
848
+ "ac20",
849
+ "fzk_house"
850
+ ],
851
+ "auto_discovered": true
852
+ },
853
+ {
854
+ "entity": "IfcSpace",
855
+ "source": "qset",
856
+ "set_name": "ArchiCADQuantities",
857
+ "key": "Area",
858
+ "file_count": 2,
859
+ "projects": [
860
+ "molio",
861
+ "sixty5"
862
+ ],
863
+ "auto_discovered": true
864
+ },
865
+ {
866
+ "entity": "IfcSpace",
867
+ "source": "qset",
868
+ "set_name": "ArchiCADQuantities",
869
+ "key": "Top Surface Area (Net)",
870
+ "file_count": 1,
871
+ "projects": [
872
+ "molio"
873
+ ],
874
+ "auto_discovered": true
875
+ },
876
+ {
877
+ "entity": "IfcSpace",
878
+ "source": "qset",
879
+ "set_name": "ArchiCADQuantities",
880
+ "key": "Edge Surface Area (Net)",
881
+ "file_count": 1,
882
+ "projects": [
883
+ "molio"
884
+ ],
885
+ "auto_discovered": true
886
+ },
887
+ {
888
+ "entity": "IfcSpace",
889
+ "source": "qset",
890
+ "set_name": "ArchiCADQuantities",
891
+ "key": "Bottom Surface Area (Net)",
892
+ "file_count": 1,
893
+ "projects": [
894
+ "molio"
895
+ ],
896
+ "auto_discovered": true
897
+ },
898
+ {
899
+ "entity": "IfcSpace",
900
+ "source": "qset",
901
+ "set_name": "ArchiCADQuantities",
902
+ "key": "Calculated Area",
903
+ "file_count": 2,
904
+ "projects": [
905
+ "molio",
906
+ "sixty5"
907
+ ],
908
+ "auto_discovered": true
909
+ },
910
+ {
911
+ "entity": "IfcSpace",
912
+ "source": "qset",
913
+ "set_name": "ArchiCADQuantities",
914
+ "key": "Measured Area",
915
+ "file_count": 2,
916
+ "projects": [
917
+ "molio",
918
+ "sixty5"
919
+ ],
920
+ "auto_discovered": true
921
+ },
922
+ {
923
+ "entity": "IfcSpace",
924
+ "source": "qset",
925
+ "set_name": "ArchiCADQuantities",
926
+ "key": "Windows Surface Area",
927
+ "file_count": 2,
928
+ "projects": [
929
+ "molio",
930
+ "sixty5"
931
+ ],
932
+ "auto_discovered": true
933
+ },
934
+ {
935
+ "entity": "IfcSpace",
936
+ "source": "qset",
937
+ "set_name": "ArchiCADQuantities",
938
+ "key": "Doors Surface Area",
939
+ "file_count": 2,
940
+ "projects": [
941
+ "molio",
942
+ "sixty5"
943
+ ],
944
+ "auto_discovered": true
945
+ },
946
+ {
947
+ "entity": "IfcSpace",
948
+ "source": "qset",
949
+ "set_name": "ArchiCADQuantities",
950
+ "key": "Walls Surface Area",
951
+ "file_count": 2,
952
+ "projects": [
953
+ "molio",
954
+ "sixty5"
955
+ ],
956
+ "auto_discovered": true
957
+ },
958
+ {
959
+ "entity": "IfcSpace",
960
+ "source": "qset",
961
+ "set_name": "ArchiCADQuantities",
962
+ "key": "Area Reducement",
963
+ "file_count": 2,
964
+ "projects": [
965
+ "molio",
966
+ "sixty5"
967
+ ],
968
+ "auto_discovered": true
969
+ },
970
+ {
971
+ "entity": "IfcSpace",
972
+ "source": "qset",
973
+ "set_name": "ArchiCADQuantities",
974
+ "key": "Extracted Column Area",
975
+ "file_count": 2,
976
+ "projects": [
977
+ "molio",
978
+ "sixty5"
979
+ ],
980
+ "auto_discovered": true
981
+ },
982
+ {
983
+ "entity": "IfcSpace",
984
+ "source": "qset",
985
+ "set_name": "ArchiCADQuantities",
986
+ "key": "Extracted Fill Area",
987
+ "file_count": 2,
988
+ "projects": [
989
+ "molio",
990
+ "sixty5"
991
+ ],
992
+ "auto_discovered": true
993
+ },
994
+ {
995
+ "entity": "IfcSpace",
996
+ "source": "qset",
997
+ "set_name": "ArchiCADQuantities",
998
+ "key": "Extracted Low Area",
999
+ "file_count": 2,
1000
+ "projects": [
1001
+ "molio",
1002
+ "sixty5"
1003
+ ],
1004
+ "auto_discovered": true
1005
+ },
1006
+ {
1007
+ "entity": "IfcSpace",
1008
+ "source": "qset",
1009
+ "set_name": "ArchiCADQuantities",
1010
+ "key": "Extracted Wall Area",
1011
+ "file_count": 2,
1012
+ "projects": [
1013
+ "molio",
1014
+ "sixty5"
1015
+ ],
1016
+ "auto_discovered": true
1017
+ },
1018
+ {
1019
+ "entity": "IfcSpace",
1020
+ "source": "qset",
1021
+ "set_name": "ArchiCADQuantities",
1022
+ "key": "Measured Net Area",
1023
+ "file_count": 1,
1024
+ "projects": [
1025
+ "molio"
1026
+ ],
1027
+ "auto_discovered": true
1028
+ },
1029
+ {
1030
+ "entity": "IfcSpace",
1031
+ "source": "qset",
1032
+ "set_name": "ArchiCADQuantities",
1033
+ "key": "Reduced Area",
1034
+ "file_count": 1,
1035
+ "projects": [
1036
+ "molio"
1037
+ ],
1038
+ "auto_discovered": true
1039
+ },
1040
+ {
1041
+ "entity": "IfcSpace",
1042
+ "source": "qset",
1043
+ "set_name": "ArchiCADQuantities",
1044
+ "key": "Total Extracted Area",
1045
+ "file_count": 2,
1046
+ "projects": [
1047
+ "molio",
1048
+ "sixty5"
1049
+ ],
1050
+ "auto_discovered": true
1051
+ },
1052
+ {
1053
+ "entity": "IfcSpace",
1054
+ "source": "qset",
1055
+ "set_name": "ArchiCADQuantities",
1056
+ "key": "Wall Inset Back Side Surface Area",
1057
+ "file_count": 2,
1058
+ "projects": [
1059
+ "molio",
1060
+ "sixty5"
1061
+ ],
1062
+ "auto_discovered": true
1063
+ },
1064
+ {
1065
+ "entity": "IfcSpace",
1066
+ "source": "qset",
1067
+ "set_name": "ArchiCADQuantities",
1068
+ "key": "Wall Inset Side Surface Area",
1069
+ "file_count": 2,
1070
+ "projects": [
1071
+ "molio",
1072
+ "sixty5"
1073
+ ],
1074
+ "auto_discovered": true
1075
+ },
1076
+ {
1077
+ "entity": "IfcSpace",
1078
+ "source": "qset",
1079
+ "set_name": "ArchiCADQuantities",
1080
+ "key": "Wall Inset Top Surface Area",
1081
+ "file_count": 2,
1082
+ "projects": [
1083
+ "molio",
1084
+ "sixty5"
1085
+ ],
1086
+ "auto_discovered": true
1087
+ },
1088
+ {
1089
+ "entity": "IfcSpace",
1090
+ "source": "qset",
1091
+ "set_name": "ArchiCADQuantities",
1092
+ "key": "Extracted Curtain Wall Area",
1093
+ "file_count": 1,
1094
+ "projects": [
1095
+ "molio"
1096
+ ],
1097
+ "auto_discovered": true
1098
+ },
1099
+ {
1100
+ "entity": "IfcSpace",
1101
+ "source": "qset",
1102
+ "set_name": "ArchiCADQuantities",
1103
+ "key": "Net Area",
1104
+ "file_count": 1,
1105
+ "projects": [
1106
+ "sixty5"
1107
+ ],
1108
+ "auto_discovered": true
1109
+ },
1110
+ {
1111
+ "entity": "IfcSpace",
1112
+ "source": "qset",
1113
+ "set_name": "Qto_SpaceBaseQuantities",
1114
+ "key": "GrossCeilingArea",
1115
+ "file_count": 2,
1116
+ "projects": [
1117
+ "fantasy_office_building_1",
1118
+ "fantasy_office_building_2"
1119
+ ],
1120
+ "auto_discovered": true
1121
+ },
1122
+ {
1123
+ "entity": "IfcSpace",
1124
+ "source": "qset",
1125
+ "set_name": "(unnamed)",
1126
+ "key": "NetFootprintArea",
1127
+ "file_count": 1,
1128
+ "projects": [
1129
+ "hitos"
1130
+ ],
1131
+ "auto_discovered": true
1132
+ },
1133
+ {
1134
+ "entity": "IfcSlab",
1135
+ "source": "qset",
1136
+ "set_name": "ArchiCADQuantities",
1137
+ "key": "OberflΓ€chenbereich",
1138
+ "file_count": 2,
1139
+ "projects": [
1140
+ "ac20",
1141
+ "fzk_house"
1142
+ ],
1143
+ "auto_discovered": true
1144
+ },
1145
+ {
1146
+ "entity": "IfcSlab",
1147
+ "source": "qset",
1148
+ "set_name": "ArchiCADQuantities",
1149
+ "key": "FlΓ€che",
1150
+ "file_count": 2,
1151
+ "projects": [
1152
+ "ac20",
1153
+ "fzk_house"
1154
+ ],
1155
+ "auto_discovered": true
1156
+ },
1157
+ {
1158
+ "entity": "IfcSlab",
1159
+ "source": "qset",
1160
+ "set_name": "ArchiCADQuantities",
1161
+ "key": "OberflΓ€chenbereich Unterseite",
1162
+ "file_count": 2,
1163
+ "projects": [
1164
+ "ac20",
1165
+ "fzk_house"
1166
+ ],
1167
+ "auto_discovered": true
1168
+ },
1169
+ {
1170
+ "entity": "IfcSlab",
1171
+ "source": "qset",
1172
+ "set_name": "ArchiCADQuantities",
1173
+ "key": "OberflΓ€chenbereich Oberseite",
1174
+ "file_count": 2,
1175
+ "projects": [
1176
+ "ac20",
1177
+ "fzk_house"
1178
+ ],
1179
+ "auto_discovered": true
1180
+ },
1181
+ {
1182
+ "entity": "IfcSlab",
1183
+ "source": "qset",
1184
+ "set_name": "ArchiCADQuantities",
1185
+ "key": "Kante OberflΓ€chenbereich",
1186
+ "file_count": 2,
1187
+ "projects": [
1188
+ "ac20",
1189
+ "fzk_house"
1190
+ ],
1191
+ "auto_discovered": true
1192
+ },
1193
+ {
1194
+ "entity": "IfcSlab",
1195
+ "source": "qset",
1196
+ "set_name": "ArchiCADQuantities",
1197
+ "key": "LΓΆcher OberflΓ€chenbereich",
1198
+ "file_count": 2,
1199
+ "projects": [
1200
+ "ac20",
1201
+ "fzk_house"
1202
+ ],
1203
+ "auto_discovered": true
1204
+ },
1205
+ {
1206
+ "entity": "IfcSlab",
1207
+ "source": "qset",
1208
+ "set_name": "ArchiCADQuantities",
1209
+ "key": "Brutto-OberflΓ€che der Deckenoberseite",
1210
+ "file_count": 2,
1211
+ "projects": [
1212
+ "ac20",
1213
+ "fzk_house"
1214
+ ],
1215
+ "auto_discovered": true
1216
+ },
1217
+ {
1218
+ "entity": "IfcSlab",
1219
+ "source": "qset",
1220
+ "set_name": "ArchiCADQuantities",
1221
+ "key": "Brutto-OberflΓ€chenbereich der Deckenunterseite",
1222
+ "file_count": 2,
1223
+ "projects": [
1224
+ "ac20",
1225
+ "fzk_house"
1226
+ ],
1227
+ "auto_discovered": true
1228
+ },
1229
+ {
1230
+ "entity": "IfcSlab",
1231
+ "source": "qset",
1232
+ "set_name": "ArchiCADQuantities",
1233
+ "key": "Brutto-OberflΓ€chenbereich der Deckenkanten",
1234
+ "file_count": 2,
1235
+ "projects": [
1236
+ "ac20",
1237
+ "fzk_house"
1238
+ ],
1239
+ "auto_discovered": true
1240
+ },
1241
+ {
1242
+ "entity": "IfcSlab",
1243
+ "source": "qset",
1244
+ "set_name": "ArchiCADQuantities",
1245
+ "key": "Konditionaler OberflΓ€chenbereich der Unterseite",
1246
+ "file_count": 2,
1247
+ "projects": [
1248
+ "ac20",
1249
+ "fzk_house"
1250
+ ],
1251
+ "auto_discovered": true
1252
+ },
1253
+ {
1254
+ "entity": "IfcSlab",
1255
+ "source": "qset",
1256
+ "set_name": "ArchiCADQuantities",
1257
+ "key": "Konditionaler OberflΓ€chenbereich der Oberseite",
1258
+ "file_count": 2,
1259
+ "projects": [
1260
+ "ac20",
1261
+ "fzk_house"
1262
+ ],
1263
+ "auto_discovered": true
1264
+ },
1265
+ {
1266
+ "entity": "IfcSlab",
1267
+ "source": "qset",
1268
+ "set_name": "ArchiCADQuantities",
1269
+ "key": "Brutto-OberflΓ€chenbereich der Deckenoberseite mit LΓΆchern",
1270
+ "file_count": 2,
1271
+ "projects": [
1272
+ "ac20",
1273
+ "fzk_house"
1274
+ ],
1275
+ "auto_discovered": true
1276
+ },
1277
+ {
1278
+ "entity": "IfcSlab",
1279
+ "source": "qset",
1280
+ "set_name": "ArchiCADQuantities",
1281
+ "key": "Brutto-OberflΓ€chenbereich der Deckenunterseite mit LΓΆchern",
1282
+ "file_count": 2,
1283
+ "projects": [
1284
+ "ac20",
1285
+ "fzk_house"
1286
+ ],
1287
+ "auto_discovered": true
1288
+ },
1289
+ {
1290
+ "entity": "IfcSlab",
1291
+ "source": "qset",
1292
+ "set_name": "ArchiCADQuantities",
1293
+ "key": "Brutto-OberflΓ€chenbereich der Deckenkanten mit LΓΆchern",
1294
+ "file_count": 2,
1295
+ "projects": [
1296
+ "ac20",
1297
+ "fzk_house"
1298
+ ],
1299
+ "auto_discovered": true
1300
+ },
1301
+ {
1302
+ "entity": "IfcSlab",
1303
+ "source": "qset",
1304
+ "set_name": "ArchiCADQuantities",
1305
+ "key": "Netto-OberflΓ€chenbereich Unterseite",
1306
+ "file_count": 2,
1307
+ "projects": [
1308
+ "ac20",
1309
+ "fzk_house"
1310
+ ],
1311
+ "auto_discovered": true
1312
+ },
1313
+ {
1314
+ "entity": "IfcSlab",
1315
+ "source": "qset",
1316
+ "set_name": "ArchiCADQuantities",
1317
+ "key": "Netto-OberflΓ€chenbereich Oberseite",
1318
+ "file_count": 2,
1319
+ "projects": [
1320
+ "ac20",
1321
+ "fzk_house"
1322
+ ],
1323
+ "auto_discovered": true
1324
+ },
1325
+ {
1326
+ "entity": "IfcSlab",
1327
+ "source": "qset",
1328
+ "set_name": "ArchiCADQuantities",
1329
+ "key": "Netto-OberflΓ€chenbereich der Kante",
1330
+ "file_count": 2,
1331
+ "projects": [
1332
+ "ac20",
1333
+ "fzk_house"
1334
+ ],
1335
+ "auto_discovered": true
1336
+ },
1337
+ {
1338
+ "entity": "IfcSlab",
1339
+ "source": "qset",
1340
+ "set_name": "ArchiCADQuantities",
1341
+ "key": "Brutto-OberflΓ€chenbereich an der Unterseite",
1342
+ "file_count": 2,
1343
+ "projects": [
1344
+ "ac20",
1345
+ "fzk_house"
1346
+ ],
1347
+ "auto_discovered": true
1348
+ },
1349
+ {
1350
+ "entity": "IfcSlab",
1351
+ "source": "qset",
1352
+ "set_name": "ArchiCADQuantities",
1353
+ "key": "Brutto-OberflΓ€chenbereich an der Oberseite",
1354
+ "file_count": 2,
1355
+ "projects": [
1356
+ "ac20",
1357
+ "fzk_house"
1358
+ ],
1359
+ "auto_discovered": true
1360
+ },
1361
+ {
1362
+ "entity": "IfcSlab",
1363
+ "source": "qset",
1364
+ "set_name": "ArchiCADQuantities",
1365
+ "key": "Brutto-OberflΓ€chenbereich der Dachkanten",
1366
+ "file_count": 2,
1367
+ "projects": [
1368
+ "ac20",
1369
+ "fzk_house"
1370
+ ],
1371
+ "auto_discovered": true
1372
+ },
1373
+ {
1374
+ "entity": "IfcSlab",
1375
+ "source": "qset",
1376
+ "set_name": "ArchiCADQuantities",
1377
+ "key": "Konditionaler OberflΓ€chenbereich unten",
1378
+ "file_count": 2,
1379
+ "projects": [
1380
+ "ac20",
1381
+ "fzk_house"
1382
+ ],
1383
+ "auto_discovered": true
1384
+ },
1385
+ {
1386
+ "entity": "IfcSlab",
1387
+ "source": "qset",
1388
+ "set_name": "ArchiCADQuantities",
1389
+ "key": "Konditionaler OberflΓ€chenbereich oben",
1390
+ "file_count": 2,
1391
+ "projects": [
1392
+ "ac20",
1393
+ "fzk_house"
1394
+ ],
1395
+ "auto_discovered": true
1396
+ },
1397
+ {
1398
+ "entity": "IfcSlab",
1399
+ "source": "qset",
1400
+ "set_name": "ArchiCADQuantities",
1401
+ "key": "OberflΓ€chenbereich der Γ–ffnungen",
1402
+ "file_count": 2,
1403
+ "projects": [
1404
+ "ac20",
1405
+ "fzk_house"
1406
+ ],
1407
+ "auto_discovered": true
1408
+ },
1409
+ {
1410
+ "entity": "IfcSlab",
1411
+ "source": "qset",
1412
+ "set_name": "ArchiCADQuantities",
1413
+ "key": "Area",
1414
+ "file_count": 2,
1415
+ "projects": [
1416
+ "molio",
1417
+ "sixty5"
1418
+ ],
1419
+ "auto_discovered": true
1420
+ },
1421
+ {
1422
+ "entity": "IfcSlab",
1423
+ "source": "qset",
1424
+ "set_name": "ArchiCADQuantities",
1425
+ "key": "Surface Area",
1426
+ "file_count": 2,
1427
+ "projects": [
1428
+ "molio",
1429
+ "sixty5"
1430
+ ],
1431
+ "auto_discovered": true
1432
+ },
1433
+ {
1434
+ "entity": "IfcSlab",
1435
+ "source": "qset",
1436
+ "set_name": "ArchiCADQuantities",
1437
+ "key": "Top Surface Area (Net)",
1438
+ "file_count": 1,
1439
+ "projects": [
1440
+ "molio"
1441
+ ],
1442
+ "auto_discovered": true
1443
+ },
1444
+ {
1445
+ "entity": "IfcSlab",
1446
+ "source": "qset",
1447
+ "set_name": "ArchiCADQuantities",
1448
+ "key": "Top Surface Area (Conditional)",
1449
+ "file_count": 1,
1450
+ "projects": [
1451
+ "molio"
1452
+ ],
1453
+ "auto_discovered": true
1454
+ },
1455
+ {
1456
+ "entity": "IfcSlab",
1457
+ "source": "qset",
1458
+ "set_name": "ArchiCADQuantities",
1459
+ "key": "Edge Surface Area (Net)",
1460
+ "file_count": 1,
1461
+ "projects": [
1462
+ "molio"
1463
+ ],
1464
+ "auto_discovered": true
1465
+ },
1466
+ {
1467
+ "entity": "IfcSlab",
1468
+ "source": "qset",
1469
+ "set_name": "ArchiCADQuantities",
1470
+ "key": "Bottom Surface Area (Net)",
1471
+ "file_count": 1,
1472
+ "projects": [
1473
+ "molio"
1474
+ ],
1475
+ "auto_discovered": true
1476
+ },
1477
+ {
1478
+ "entity": "IfcSlab",
1479
+ "source": "qset",
1480
+ "set_name": "ArchiCADQuantities",
1481
+ "key": "Bottom Surface Area (Conditional)",
1482
+ "file_count": 1,
1483
+ "projects": [
1484
+ "molio"
1485
+ ],
1486
+ "auto_discovered": true
1487
+ },
1488
+ {
1489
+ "entity": "IfcSlab",
1490
+ "source": "qset",
1491
+ "set_name": "ArchiCADQuantities",
1492
+ "key": "Bottom Surface Area (Gross)",
1493
+ "file_count": 1,
1494
+ "projects": [
1495
+ "molio"
1496
+ ],
1497
+ "auto_discovered": true
1498
+ },
1499
+ {
1500
+ "entity": "IfcSlab",
1501
+ "source": "qset",
1502
+ "set_name": "ArchiCADQuantities",
1503
+ "key": "Edge Surface Area (Gross)",
1504
+ "file_count": 1,
1505
+ "projects": [
1506
+ "molio"
1507
+ ],
1508
+ "auto_discovered": true
1509
+ },
1510
+ {
1511
+ "entity": "IfcSlab",
1512
+ "source": "qset",
1513
+ "set_name": "ArchiCADQuantities",
1514
+ "key": "Top Surface Area (Gross)",
1515
+ "file_count": 1,
1516
+ "projects": [
1517
+ "molio"
1518
+ ],
1519
+ "auto_discovered": true
1520
+ },
1521
+ {
1522
+ "entity": "IfcSlab",
1523
+ "source": "qset",
1524
+ "set_name": "ArchiCADQuantities",
1525
+ "key": "Surface Area of the Slab Top (Conditional)",
1526
+ "file_count": 1,
1527
+ "projects": [
1528
+ "molio"
1529
+ ],
1530
+ "auto_discovered": true
1531
+ },
1532
+ {
1533
+ "entity": "IfcSlab",
1534
+ "source": "qset",
1535
+ "set_name": "ArchiCADQuantities",
1536
+ "key": "Surface Area of the Slab Bottom (Gross)",
1537
+ "file_count": 1,
1538
+ "projects": [
1539
+ "molio"
1540
+ ],
1541
+ "auto_discovered": true
1542
+ },
1543
+ {
1544
+ "entity": "IfcSlab",
1545
+ "source": "qset",
1546
+ "set_name": "ArchiCADQuantities",
1547
+ "key": "Surface Area of the Slab Bottom (Gross, with holes)",
1548
+ "file_count": 1,
1549
+ "projects": [
1550
+ "molio"
1551
+ ],
1552
+ "auto_discovered": true
1553
+ },
1554
+ {
1555
+ "entity": "IfcSlab",
1556
+ "source": "qset",
1557
+ "set_name": "ArchiCADQuantities",
1558
+ "key": "Surface Area of the Slab Edges (Gross)",
1559
+ "file_count": 1,
1560
+ "projects": [
1561
+ "molio"
1562
+ ],
1563
+ "auto_discovered": true
1564
+ },
1565
+ {
1566
+ "entity": "IfcSlab",
1567
+ "source": "qset",
1568
+ "set_name": "ArchiCADQuantities",
1569
+ "key": "Surface Area of the Slab Edges (Gross, with holes)",
1570
+ "file_count": 1,
1571
+ "projects": [
1572
+ "molio"
1573
+ ],
1574
+ "auto_discovered": true
1575
+ },
1576
+ {
1577
+ "entity": "IfcSlab",
1578
+ "source": "qset",
1579
+ "set_name": "ArchiCADQuantities",
1580
+ "key": "Surface Area of the Slab Top (Gross)",
1581
+ "file_count": 1,
1582
+ "projects": [
1583
+ "molio"
1584
+ ],
1585
+ "auto_discovered": true
1586
+ },
1587
+ {
1588
+ "entity": "IfcSlab",
1589
+ "source": "qset",
1590
+ "set_name": "ArchiCADQuantities",
1591
+ "key": "Surface Area of the Slab Top (Gross, with holes)",
1592
+ "file_count": 1,
1593
+ "projects": [
1594
+ "molio"
1595
+ ],
1596
+ "auto_discovered": true
1597
+ },
1598
+ {
1599
+ "entity": "IfcSlab",
1600
+ "source": "qset",
1601
+ "set_name": "ArchiCADQuantities",
1602
+ "key": "Holes Surface Area",
1603
+ "file_count": 2,
1604
+ "projects": [
1605
+ "molio",
1606
+ "sixty5"
1607
+ ],
1608
+ "auto_discovered": true
1609
+ },
1610
+ {
1611
+ "entity": "IfcSlab",
1612
+ "source": "qset",
1613
+ "set_name": "ArchiCADQuantities",
1614
+ "key": "Bottom Surface Area",
1615
+ "file_count": 1,
1616
+ "projects": [
1617
+ "sixty5"
1618
+ ],
1619
+ "auto_discovered": true
1620
+ },
1621
+ {
1622
+ "entity": "IfcSlab",
1623
+ "source": "qset",
1624
+ "set_name": "ArchiCADQuantities",
1625
+ "key": "Conditional Surface Area of the Bottom",
1626
+ "file_count": 1,
1627
+ "projects": [
1628
+ "sixty5"
1629
+ ],
1630
+ "auto_discovered": true
1631
+ },
1632
+ {
1633
+ "entity": "IfcSlab",
1634
+ "source": "qset",
1635
+ "set_name": "ArchiCADQuantities",
1636
+ "key": "Conditional Surface Area of the Top",
1637
+ "file_count": 1,
1638
+ "projects": [
1639
+ "sixty5"
1640
+ ],
1641
+ "auto_discovered": true
1642
+ },
1643
+ {
1644
+ "entity": "IfcSlab",
1645
+ "source": "qset",
1646
+ "set_name": "ArchiCADQuantities",
1647
+ "key": "Edge Surface Area",
1648
+ "file_count": 1,
1649
+ "projects": [
1650
+ "sixty5"
1651
+ ],
1652
+ "auto_discovered": true
1653
+ },
1654
+ {
1655
+ "entity": "IfcSlab",
1656
+ "source": "qset",
1657
+ "set_name": "ArchiCADQuantities",
1658
+ "key": "Gross Surface Area of the Slab Bottom",
1659
+ "file_count": 1,
1660
+ "projects": [
1661
+ "sixty5"
1662
+ ],
1663
+ "auto_discovered": true
1664
+ },
1665
+ {
1666
+ "entity": "IfcSlab",
1667
+ "source": "qset",
1668
+ "set_name": "ArchiCADQuantities",
1669
+ "key": "Gross Surface Area of the Slab Bottom with Holes",
1670
+ "file_count": 1,
1671
+ "projects": [
1672
+ "sixty5"
1673
+ ],
1674
+ "auto_discovered": true
1675
+ },
1676
+ {
1677
+ "entity": "IfcSlab",
1678
+ "source": "qset",
1679
+ "set_name": "ArchiCADQuantities",
1680
+ "key": "Gross Surface Area of the Slab Edges",
1681
+ "file_count": 1,
1682
+ "projects": [
1683
+ "sixty5"
1684
+ ],
1685
+ "auto_discovered": true
1686
+ },
1687
+ {
1688
+ "entity": "IfcSlab",
1689
+ "source": "qset",
1690
+ "set_name": "ArchiCADQuantities",
1691
+ "key": "Gross Surface Area of the Slab Edges with Holes",
1692
+ "file_count": 1,
1693
+ "projects": [
1694
+ "sixty5"
1695
+ ],
1696
+ "auto_discovered": true
1697
+ },
1698
+ {
1699
+ "entity": "IfcSlab",
1700
+ "source": "qset",
1701
+ "set_name": "ArchiCADQuantities",
1702
+ "key": "Gross Surface Area of the Slab Top",
1703
+ "file_count": 1,
1704
+ "projects": [
1705
+ "sixty5"
1706
+ ],
1707
+ "auto_discovered": true
1708
+ },
1709
+ {
1710
+ "entity": "IfcSlab",
1711
+ "source": "qset",
1712
+ "set_name": "ArchiCADQuantities",
1713
+ "key": "Gross Surface Area of the Slab Top with Holes",
1714
+ "file_count": 1,
1715
+ "projects": [
1716
+ "sixty5"
1717
+ ],
1718
+ "auto_discovered": true
1719
+ },
1720
+ {
1721
+ "entity": "IfcSlab",
1722
+ "source": "qset",
1723
+ "set_name": "ArchiCADQuantities",
1724
+ "key": "Top Surface Area",
1725
+ "file_count": 1,
1726
+ "projects": [
1727
+ "sixty5"
1728
+ ],
1729
+ "auto_discovered": true
1730
+ },
1731
+ {
1732
+ "entity": "IfcSlab",
1733
+ "source": "qset",
1734
+ "set_name": "(unnamed)",
1735
+ "key": "NetFootprintArea",
1736
+ "file_count": 1,
1737
+ "projects": [
1738
+ "hitos"
1739
+ ],
1740
+ "auto_discovered": true
1741
+ },
1742
+ {
1743
+ "entity": "IfcRoof",
1744
+ "source": "qset",
1745
+ "set_name": "Qto_RoofBaseQuantities",
1746
+ "key": "ProjectedArea",
1747
+ "file_count": 2,
1748
+ "projects": [
1749
+ "fantasy_office_building_1",
1750
+ "fantasy_office_building_2"
1751
+ ],
1752
+ "auto_discovered": true
1753
+ },
1754
+ {
1755
+ "entity": "IfcWall",
1756
+ "source": "qset",
1757
+ "set_name": "BaseQuantities",
1758
+ "key": "GrossFootprintArea",
1759
+ "file_count": 7,
1760
+ "projects": [
1761
+ "ac20",
1762
+ "fzk_house",
1763
+ "molio",
1764
+ "sixty5",
1765
+ "smiley_west"
1766
+ ],
1767
+ "auto_discovered": true
1768
+ },
1769
+ {
1770
+ "entity": "IfcWall",
1771
+ "source": "qset",
1772
+ "set_name": "BaseQuantities",
1773
+ "key": "NetFootprintArea",
1774
+ "file_count": 6,
1775
+ "projects": [
1776
+ "ac20",
1777
+ "fzk_house",
1778
+ "molio",
1779
+ "sixty5",
1780
+ "smiley_west"
1781
+ ],
1782
+ "auto_discovered": true
1783
+ },
1784
+ {
1785
+ "entity": "IfcWall",
1786
+ "source": "qset",
1787
+ "set_name": "BaseQuantities",
1788
+ "key": "GrossSideArea",
1789
+ "file_count": 7,
1790
+ "projects": [
1791
+ "ac20",
1792
+ "fzk_house",
1793
+ "molio",
1794
+ "sixty5",
1795
+ "smiley_west"
1796
+ ],
1797
+ "auto_discovered": true
1798
+ },
1799
+ {
1800
+ "entity": "IfcWall",
1801
+ "source": "qset",
1802
+ "set_name": "BaseQuantities",
1803
+ "key": "NetSideArea",
1804
+ "file_count": 7,
1805
+ "projects": [
1806
+ "ac20",
1807
+ "fzk_house",
1808
+ "molio",
1809
+ "sixty5",
1810
+ "smiley_west"
1811
+ ],
1812
+ "auto_discovered": true
1813
+ },
1814
+ {
1815
+ "entity": "IfcWall",
1816
+ "source": "qset",
1817
+ "set_name": "ArchiCADQuantities",
1818
+ "key": "OberflΓ€chenbereich",
1819
+ "file_count": 2,
1820
+ "projects": [
1821
+ "ac20",
1822
+ "fzk_house"
1823
+ ],
1824
+ "auto_discovered": true
1825
+ },
1826
+ {
1827
+ "entity": "IfcWall",
1828
+ "source": "qset",
1829
+ "set_name": "ArchiCADQuantities",
1830
+ "key": "FlΓ€che",
1831
+ "file_count": 2,
1832
+ "projects": [
1833
+ "ac20",
1834
+ "fzk_house"
1835
+ ],
1836
+ "auto_discovered": true
1837
+ },
1838
+ {
1839
+ "entity": "IfcWall",
1840
+ "source": "qset",
1841
+ "set_name": "ArchiCADQuantities",
1842
+ "key": "Netto-OberflÀchenbereich an der Außenseite",
1843
+ "file_count": 2,
1844
+ "projects": [
1845
+ "ac20",
1846
+ "fzk_house"
1847
+ ],
1848
+ "auto_discovered": true
1849
+ },
1850
+ {
1851
+ "entity": "IfcWall",
1852
+ "source": "qset",
1853
+ "set_name": "ArchiCADQuantities",
1854
+ "key": "Netto-OberflΓ€chenbereich an der Innenseite",
1855
+ "file_count": 2,
1856
+ "projects": [
1857
+ "ac20",
1858
+ "fzk_house"
1859
+ ],
1860
+ "auto_discovered": true
1861
+ },
1862
+ {
1863
+ "entity": "IfcWall",
1864
+ "source": "qset",
1865
+ "set_name": "ArchiCADQuantities",
1866
+ "key": "Netto-OberflΓ€chenbereich an den Kanten",
1867
+ "file_count": 2,
1868
+ "projects": [
1869
+ "ac20",
1870
+ "fzk_house"
1871
+ ],
1872
+ "auto_discovered": true
1873
+ },
1874
+ {
1875
+ "entity": "IfcWall",
1876
+ "source": "qset",
1877
+ "set_name": "ArchiCADQuantities",
1878
+ "key": "FlΓ€che der Wand",
1879
+ "file_count": 2,
1880
+ "projects": [
1881
+ "ac20",
1882
+ "fzk_house"
1883
+ ],
1884
+ "auto_discovered": true
1885
+ },
1886
+ {
1887
+ "entity": "IfcWall",
1888
+ "source": "qset",
1889
+ "set_name": "ArchiCADQuantities",
1890
+ "key": "Brutto-WandoberflΓ€chenbereich an der Innenseite",
1891
+ "file_count": 2,
1892
+ "projects": [
1893
+ "ac20",
1894
+ "fzk_house"
1895
+ ],
1896
+ "auto_discovered": true
1897
+ },
1898
+ {
1899
+ "entity": "IfcWall",
1900
+ "source": "qset",
1901
+ "set_name": "ArchiCADQuantities",
1902
+ "key": "Brutto-WandoberflÀchenbereich an der Außenseite",
1903
+ "file_count": 2,
1904
+ "projects": [
1905
+ "ac20",
1906
+ "fzk_house"
1907
+ ],
1908
+ "auto_discovered": true
1909
+ },
1910
+ {
1911
+ "entity": "IfcWall",
1912
+ "source": "qset",
1913
+ "set_name": "ArchiCADQuantities",
1914
+ "key": "Analytische OberflΓ€che der Γ–ffnungen an der Innenseite",
1915
+ "file_count": 2,
1916
+ "projects": [
1917
+ "ac20",
1918
+ "fzk_house"
1919
+ ],
1920
+ "auto_discovered": true
1921
+ },
1922
+ {
1923
+ "entity": "IfcWall",
1924
+ "source": "qset",
1925
+ "set_name": "ArchiCADQuantities",
1926
+ "key": "Analytische OberflΓ€che der Γ–ffnungen an der Außenseite",
1927
+ "file_count": 2,
1928
+ "projects": [
1929
+ "ac20",
1930
+ "fzk_house"
1931
+ ],
1932
+ "auto_discovered": true
1933
+ },
1934
+ {
1935
+ "entity": "IfcWall",
1936
+ "source": "qset",
1937
+ "set_name": "ArchiCADQuantities",
1938
+ "key": "Konditionaler OberflÀchenbereich an der Außenseite",
1939
+ "file_count": 2,
1940
+ "projects": [
1941
+ "ac20",
1942
+ "fzk_house"
1943
+ ],
1944
+ "auto_discovered": true
1945
+ },
1946
+ {
1947
+ "entity": "IfcWall",
1948
+ "source": "qset",
1949
+ "set_name": "ArchiCADQuantities",
1950
+ "key": "Konditionaler OberflΓ€chenbereich an der Innenseite",
1951
+ "file_count": 2,
1952
+ "projects": [
1953
+ "ac20",
1954
+ "fzk_house"
1955
+ ],
1956
+ "auto_discovered": true
1957
+ },
1958
+ {
1959
+ "entity": "IfcWall",
1960
+ "source": "qset",
1961
+ "set_name": "ArchiCADQuantities",
1962
+ "key": "OberflΓ€chenbereich der Fenster in der Wand",
1963
+ "file_count": 2,
1964
+ "projects": [
1965
+ "ac20",
1966
+ "fzk_house"
1967
+ ],
1968
+ "auto_discovered": true
1969
+ },
1970
+ {
1971
+ "entity": "IfcWall",
1972
+ "source": "qset",
1973
+ "set_name": "ArchiCADQuantities",
1974
+ "key": "OberflΓ€chenbereich der TΓΌren in der Wand",
1975
+ "file_count": 2,
1976
+ "projects": [
1977
+ "ac20",
1978
+ "fzk_house"
1979
+ ],
1980
+ "auto_discovered": true
1981
+ },
1982
+ {
1983
+ "entity": "IfcWall",
1984
+ "source": "qset",
1985
+ "set_name": "ArchiCADQuantities",
1986
+ "key": "OberflΓ€chenbereich der leeren Γ–ffnungen in der Wand",
1987
+ "file_count": 2,
1988
+ "projects": [
1989
+ "ac20",
1990
+ "fzk_house"
1991
+ ],
1992
+ "auto_discovered": true
1993
+ },
1994
+ {
1995
+ "entity": "IfcWall",
1996
+ "source": "qset",
1997
+ "set_name": "ArchiCADQuantities",
1998
+ "key": "Area",
1999
+ "file_count": 3,
2000
+ "projects": [
2001
+ "molio",
2002
+ "sixty5"
2003
+ ],
2004
+ "auto_discovered": true
2005
+ },
2006
+ {
2007
+ "entity": "IfcWall",
2008
+ "source": "qset",
2009
+ "set_name": "ArchiCADQuantities",
2010
+ "key": "Surface Area",
2011
+ "file_count": 3,
2012
+ "projects": [
2013
+ "molio",
2014
+ "sixty5"
2015
+ ],
2016
+ "auto_discovered": true
2017
+ },
2018
+ {
2019
+ "entity": "IfcWall",
2020
+ "source": "qset",
2021
+ "set_name": "ArchiCADQuantities",
2022
+ "key": "Top Surface Area (Net)",
2023
+ "file_count": 1,
2024
+ "projects": [
2025
+ "molio"
2026
+ ],
2027
+ "auto_discovered": true
2028
+ },
2029
+ {
2030
+ "entity": "IfcWall",
2031
+ "source": "qset",
2032
+ "set_name": "ArchiCADQuantities",
2033
+ "key": "Edge Surface Area (Net)",
2034
+ "file_count": 1,
2035
+ "projects": [
2036
+ "molio"
2037
+ ],
2038
+ "auto_discovered": true
2039
+ },
2040
+ {
2041
+ "entity": "IfcWall",
2042
+ "source": "qset",
2043
+ "set_name": "ArchiCADQuantities",
2044
+ "key": "Bottom Surface Area (Net)",
2045
+ "file_count": 1,
2046
+ "projects": [
2047
+ "molio"
2048
+ ],
2049
+ "auto_discovered": true
2050
+ },
2051
+ {
2052
+ "entity": "IfcWall",
2053
+ "source": "qset",
2054
+ "set_name": "ArchiCADQuantities",
2055
+ "key": "Area of the Doors",
2056
+ "file_count": 3,
2057
+ "projects": [
2058
+ "molio",
2059
+ "sixty5"
2060
+ ],
2061
+ "auto_discovered": true
2062
+ },
2063
+ {
2064
+ "entity": "IfcWall",
2065
+ "source": "qset",
2066
+ "set_name": "ArchiCADQuantities",
2067
+ "key": "Area of the Windows",
2068
+ "file_count": 3,
2069
+ "projects": [
2070
+ "molio",
2071
+ "sixty5"
2072
+ ],
2073
+ "auto_discovered": true
2074
+ },
2075
+ {
2076
+ "entity": "IfcWall",
2077
+ "source": "qset",
2078
+ "set_name": "ArchiCADQuantities",
2079
+ "key": "Analytic Surface Area of Openings on the Inside Face",
2080
+ "file_count": 3,
2081
+ "projects": [
2082
+ "molio",
2083
+ "sixty5"
2084
+ ],
2085
+ "auto_discovered": true
2086
+ },
2087
+ {
2088
+ "entity": "IfcWall",
2089
+ "source": "qset",
2090
+ "set_name": "ArchiCADQuantities",
2091
+ "key": "Analytic Surface Area of Openings on the Outside Face",
2092
+ "file_count": 3,
2093
+ "projects": [
2094
+ "molio",
2095
+ "sixty5"
2096
+ ],
2097
+ "auto_discovered": true
2098
+ },
2099
+ {
2100
+ "entity": "IfcWall",
2101
+ "source": "qset",
2102
+ "set_name": "ArchiCADQuantities",
2103
+ "key": "Surface Area of the Wall Inside Face (Net)",
2104
+ "file_count": 1,
2105
+ "projects": [
2106
+ "molio"
2107
+ ],
2108
+ "auto_discovered": true
2109
+ },
2110
+ {
2111
+ "entity": "IfcWall",
2112
+ "source": "qset",
2113
+ "set_name": "ArchiCADQuantities",
2114
+ "key": "Surface Area of the Wall Outside Face (Net)",
2115
+ "file_count": 1,
2116
+ "projects": [
2117
+ "molio"
2118
+ ],
2119
+ "auto_discovered": true
2120
+ },
2121
+ {
2122
+ "entity": "IfcWall",
2123
+ "source": "qset",
2124
+ "set_name": "ArchiCADQuantities",
2125
+ "key": "Surface Area of the Wall Inside Face (Conditional)",
2126
+ "file_count": 1,
2127
+ "projects": [
2128
+ "molio"
2129
+ ],
2130
+ "auto_discovered": true
2131
+ },
2132
+ {
2133
+ "entity": "IfcWall",
2134
+ "source": "qset",
2135
+ "set_name": "ArchiCADQuantities",
2136
+ "key": "Surface Area of the Wall Outside Face (Conditional)",
2137
+ "file_count": 1,
2138
+ "projects": [
2139
+ "molio"
2140
+ ],
2141
+ "auto_discovered": true
2142
+ },
2143
+ {
2144
+ "entity": "IfcWall",
2145
+ "source": "qset",
2146
+ "set_name": "ArchiCADQuantities",
2147
+ "key": "Surface Area of the Wall Inside Face (Gross)",
2148
+ "file_count": 1,
2149
+ "projects": [
2150
+ "molio"
2151
+ ],
2152
+ "auto_discovered": true
2153
+ },
2154
+ {
2155
+ "entity": "IfcWall",
2156
+ "source": "qset",
2157
+ "set_name": "ArchiCADQuantities",
2158
+ "key": "Surface Area of the Wall Outside Face (Gross)",
2159
+ "file_count": 1,
2160
+ "projects": [
2161
+ "molio"
2162
+ ],
2163
+ "auto_discovered": true
2164
+ },
2165
+ {
2166
+ "entity": "IfcWall",
2167
+ "source": "qset",
2168
+ "set_name": "ArchiCADQuantities",
2169
+ "key": "Surface Area of Empty Openings in the Wall",
2170
+ "file_count": 3,
2171
+ "projects": [
2172
+ "molio",
2173
+ "sixty5"
2174
+ ],
2175
+ "auto_discovered": true
2176
+ },
2177
+ {
2178
+ "entity": "IfcWall",
2179
+ "source": "qset",
2180
+ "set_name": "ArchiCADQuantities",
2181
+ "key": "Net Surface Area of the Edges",
2182
+ "file_count": 2,
2183
+ "projects": [
2184
+ "sixty5"
2185
+ ],
2186
+ "auto_discovered": true
2187
+ },
2188
+ {
2189
+ "entity": "IfcWall",
2190
+ "source": "qset",
2191
+ "set_name": "ArchiCADQuantities",
2192
+ "key": "Net Surface Area on the Inside Face",
2193
+ "file_count": 2,
2194
+ "projects": [
2195
+ "sixty5"
2196
+ ],
2197
+ "auto_discovered": true
2198
+ },
2199
+ {
2200
+ "entity": "IfcWall",
2201
+ "source": "qset",
2202
+ "set_name": "ArchiCADQuantities",
2203
+ "key": "Net Surface Area on the Outside Face",
2204
+ "file_count": 2,
2205
+ "projects": [
2206
+ "sixty5"
2207
+ ],
2208
+ "auto_discovered": true
2209
+ },
2210
+ {
2211
+ "entity": "IfcWall",
2212
+ "source": "qset",
2213
+ "set_name": "ArchiCADQuantities",
2214
+ "key": "Conditional Surface Area on the Inside Face",
2215
+ "file_count": 2,
2216
+ "projects": [
2217
+ "sixty5"
2218
+ ],
2219
+ "auto_discovered": true
2220
+ },
2221
+ {
2222
+ "entity": "IfcWall",
2223
+ "source": "qset",
2224
+ "set_name": "ArchiCADQuantities",
2225
+ "key": "Conditional Surface Area on the Outside Face",
2226
+ "file_count": 2,
2227
+ "projects": [
2228
+ "sixty5"
2229
+ ],
2230
+ "auto_discovered": true
2231
+ },
2232
+ {
2233
+ "entity": "IfcWall",
2234
+ "source": "qset",
2235
+ "set_name": "ArchiCADQuantities",
2236
+ "key": "Gross Surface Area of the Wall on the Inside Face",
2237
+ "file_count": 2,
2238
+ "projects": [
2239
+ "sixty5"
2240
+ ],
2241
+ "auto_discovered": true
2242
+ },
2243
+ {
2244
+ "entity": "IfcWall",
2245
+ "source": "qset",
2246
+ "set_name": "ArchiCADQuantities",
2247
+ "key": "Gross Surface Area of the Wall on the Outside Face",
2248
+ "file_count": 2,
2249
+ "projects": [
2250
+ "sixty5"
2251
+ ],
2252
+ "auto_discovered": true
2253
+ },
2254
+ {
2255
+ "entity": "IfcWall",
2256
+ "source": "qset",
2257
+ "set_name": "ArchiCADQuantities",
2258
+ "key": "Area of the Wall",
2259
+ "file_count": 2,
2260
+ "projects": [
2261
+ "sixty5"
2262
+ ],
2263
+ "auto_discovered": true
2264
+ },
2265
+ {
2266
+ "entity": "IfcWall",
2267
+ "source": "qset",
2268
+ "set_name": "Qto_WallBaseQuantities",
2269
+ "key": "GrossFootprintArea",
2270
+ "file_count": 3,
2271
+ "projects": [
2272
+ "digital_hub",
2273
+ "fantasy_office_building_1",
2274
+ "fantasy_office_building_2"
2275
+ ],
2276
+ "auto_discovered": true
2277
+ },
2278
+ {
2279
+ "entity": "IfcWall",
2280
+ "source": "qset",
2281
+ "set_name": "Qto_WallBaseQuantities",
2282
+ "key": "GrossSideArea",
2283
+ "file_count": 3,
2284
+ "projects": [
2285
+ "digital_hub",
2286
+ "fantasy_office_building_1",
2287
+ "fantasy_office_building_2"
2288
+ ],
2289
+ "auto_discovered": true
2290
+ },
2291
+ {
2292
+ "entity": "IfcWall",
2293
+ "source": "qset",
2294
+ "set_name": "Qto_WallBaseQuantities",
2295
+ "key": "NetSideArea",
2296
+ "file_count": 3,
2297
+ "projects": [
2298
+ "digital_hub",
2299
+ "fantasy_office_building_1",
2300
+ "fantasy_office_building_2"
2301
+ ],
2302
+ "auto_discovered": true
2303
+ },
2304
+ {
2305
+ "entity": "IfcWall",
2306
+ "source": "qset",
2307
+ "set_name": "(unnamed)",
2308
+ "key": "NetFootprintArea",
2309
+ "file_count": 1,
2310
+ "projects": [
2311
+ "hitos"
2312
+ ],
2313
+ "auto_discovered": true
2314
+ },
2315
+ {
2316
+ "entity": "IfcWall",
2317
+ "source": "qset",
2318
+ "set_name": "(unnamed)",
2319
+ "key": "NetSideAreaLeft",
2320
+ "file_count": 1,
2321
+ "projects": [
2322
+ "hitos"
2323
+ ],
2324
+ "auto_discovered": true
2325
+ },
2326
+ {
2327
+ "entity": "IfcWall",
2328
+ "source": "qset",
2329
+ "set_name": "(unnamed)",
2330
+ "key": "NetSideAreaRight",
2331
+ "file_count": 1,
2332
+ "projects": [
2333
+ "hitos"
2334
+ ],
2335
+ "auto_discovered": true
2336
+ },
2337
+ {
2338
+ "entity": "IfcWallStandardCase",
2339
+ "source": "qset",
2340
+ "set_name": "BaseQuantities",
2341
+ "key": "GrossFootprintArea",
2342
+ "file_count": 5,
2343
+ "projects": [
2344
+ "ac20",
2345
+ "fzk_house",
2346
+ "sixty5",
2347
+ "smiley_west"
2348
+ ],
2349
+ "auto_discovered": true
2350
+ },
2351
+ {
2352
+ "entity": "IfcWallStandardCase",
2353
+ "source": "qset",
2354
+ "set_name": "BaseQuantities",
2355
+ "key": "NetFootprintArea",
2356
+ "file_count": 4,
2357
+ "projects": [
2358
+ "ac20",
2359
+ "fzk_house",
2360
+ "sixty5",
2361
+ "smiley_west"
2362
+ ],
2363
+ "auto_discovered": true
2364
+ },
2365
+ {
2366
+ "entity": "IfcWallStandardCase",
2367
+ "source": "qset",
2368
+ "set_name": "BaseQuantities",
2369
+ "key": "GrossSideArea",
2370
+ "file_count": 5,
2371
+ "projects": [
2372
+ "ac20",
2373
+ "fzk_house",
2374
+ "sixty5",
2375
+ "smiley_west"
2376
+ ],
2377
+ "auto_discovered": true
2378
+ },
2379
+ {
2380
+ "entity": "IfcWallStandardCase",
2381
+ "source": "qset",
2382
+ "set_name": "BaseQuantities",
2383
+ "key": "NetSideArea",
2384
+ "file_count": 5,
2385
+ "projects": [
2386
+ "ac20",
2387
+ "fzk_house",
2388
+ "sixty5",
2389
+ "smiley_west"
2390
+ ],
2391
+ "auto_discovered": true
2392
+ },
2393
+ {
2394
+ "entity": "IfcWallStandardCase",
2395
+ "source": "qset",
2396
+ "set_name": "ArchiCADQuantities",
2397
+ "key": "OberflΓ€chenbereich",
2398
+ "file_count": 2,
2399
+ "projects": [
2400
+ "ac20",
2401
+ "fzk_house"
2402
+ ],
2403
+ "auto_discovered": true
2404
+ },
2405
+ {
2406
+ "entity": "IfcWallStandardCase",
2407
+ "source": "qset",
2408
+ "set_name": "ArchiCADQuantities",
2409
+ "key": "FlΓ€che",
2410
+ "file_count": 2,
2411
+ "projects": [
2412
+ "ac20",
2413
+ "fzk_house"
2414
+ ],
2415
+ "auto_discovered": true
2416
+ },
2417
+ {
2418
+ "entity": "IfcWallStandardCase",
2419
+ "source": "qset",
2420
+ "set_name": "ArchiCADQuantities",
2421
+ "key": "Netto-OberflÀchenbereich an der Außenseite",
2422
+ "file_count": 2,
2423
+ "projects": [
2424
+ "ac20",
2425
+ "fzk_house"
2426
+ ],
2427
+ "auto_discovered": true
2428
+ },
2429
+ {
2430
+ "entity": "IfcWallStandardCase",
2431
+ "source": "qset",
2432
+ "set_name": "ArchiCADQuantities",
2433
+ "key": "Netto-OberflΓ€chenbereich an der Innenseite",
2434
+ "file_count": 2,
2435
+ "projects": [
2436
+ "ac20",
2437
+ "fzk_house"
2438
+ ],
2439
+ "auto_discovered": true
2440
+ },
2441
+ {
2442
+ "entity": "IfcWallStandardCase",
2443
+ "source": "qset",
2444
+ "set_name": "ArchiCADQuantities",
2445
+ "key": "Netto-OberflΓ€chenbereich an den Kanten",
2446
+ "file_count": 2,
2447
+ "projects": [
2448
+ "ac20",
2449
+ "fzk_house"
2450
+ ],
2451
+ "auto_discovered": true
2452
+ },
2453
+ {
2454
+ "entity": "IfcWallStandardCase",
2455
+ "source": "qset",
2456
+ "set_name": "ArchiCADQuantities",
2457
+ "key": "FlΓ€che der Wand",
2458
+ "file_count": 2,
2459
+ "projects": [
2460
+ "ac20",
2461
+ "fzk_house"
2462
+ ],
2463
+ "auto_discovered": true
2464
+ },
2465
+ {
2466
+ "entity": "IfcWallStandardCase",
2467
+ "source": "qset",
2468
+ "set_name": "ArchiCADQuantities",
2469
+ "key": "Brutto-WandoberflΓ€chenbereich an der Innenseite",
2470
+ "file_count": 2,
2471
+ "projects": [
2472
+ "ac20",
2473
+ "fzk_house"
2474
+ ],
2475
+ "auto_discovered": true
2476
+ },
2477
+ {
2478
+ "entity": "IfcWallStandardCase",
2479
+ "source": "qset",
2480
+ "set_name": "ArchiCADQuantities",
2481
+ "key": "Brutto-WandoberflÀchenbereich an der Außenseite",
2482
+ "file_count": 2,
2483
+ "projects": [
2484
+ "ac20",
2485
+ "fzk_house"
2486
+ ],
2487
+ "auto_discovered": true
2488
+ },
2489
+ {
2490
+ "entity": "IfcWallStandardCase",
2491
+ "source": "qset",
2492
+ "set_name": "ArchiCADQuantities",
2493
+ "key": "Analytische OberflΓ€che der Γ–ffnungen an der Innenseite",
2494
+ "file_count": 2,
2495
+ "projects": [
2496
+ "ac20",
2497
+ "fzk_house"
2498
+ ],
2499
+ "auto_discovered": true
2500
+ },
2501
+ {
2502
+ "entity": "IfcWallStandardCase",
2503
+ "source": "qset",
2504
+ "set_name": "ArchiCADQuantities",
2505
+ "key": "Analytische OberflΓ€che der Γ–ffnungen an der Außenseite",
2506
+ "file_count": 2,
2507
+ "projects": [
2508
+ "ac20",
2509
+ "fzk_house"
2510
+ ],
2511
+ "auto_discovered": true
2512
+ },
2513
+ {
2514
+ "entity": "IfcWallStandardCase",
2515
+ "source": "qset",
2516
+ "set_name": "ArchiCADQuantities",
2517
+ "key": "Konditionaler OberflÀchenbereich an der Außenseite",
2518
+ "file_count": 2,
2519
+ "projects": [
2520
+ "ac20",
2521
+ "fzk_house"
2522
+ ],
2523
+ "auto_discovered": true
2524
+ },
2525
+ {
2526
+ "entity": "IfcWallStandardCase",
2527
+ "source": "qset",
2528
+ "set_name": "ArchiCADQuantities",
2529
+ "key": "Konditionaler OberflΓ€chenbereich an der Innenseite",
2530
+ "file_count": 2,
2531
+ "projects": [
2532
+ "ac20",
2533
+ "fzk_house"
2534
+ ],
2535
+ "auto_discovered": true
2536
+ },
2537
+ {
2538
+ "entity": "IfcWallStandardCase",
2539
+ "source": "qset",
2540
+ "set_name": "ArchiCADQuantities",
2541
+ "key": "OberflΓ€chenbereich der Fenster in der Wand",
2542
+ "file_count": 2,
2543
+ "projects": [
2544
+ "ac20",
2545
+ "fzk_house"
2546
+ ],
2547
+ "auto_discovered": true
2548
+ },
2549
+ {
2550
+ "entity": "IfcWallStandardCase",
2551
+ "source": "qset",
2552
+ "set_name": "ArchiCADQuantities",
2553
+ "key": "OberflΓ€chenbereich der TΓΌren in der Wand",
2554
+ "file_count": 2,
2555
+ "projects": [
2556
+ "ac20",
2557
+ "fzk_house"
2558
+ ],
2559
+ "auto_discovered": true
2560
+ },
2561
+ {
2562
+ "entity": "IfcWallStandardCase",
2563
+ "source": "qset",
2564
+ "set_name": "ArchiCADQuantities",
2565
+ "key": "OberflΓ€chenbereich der leeren Γ–ffnungen in der Wand",
2566
+ "file_count": 2,
2567
+ "projects": [
2568
+ "ac20",
2569
+ "fzk_house"
2570
+ ],
2571
+ "auto_discovered": true
2572
+ },
2573
+ {
2574
+ "entity": "IfcWallStandardCase",
2575
+ "source": "qset",
2576
+ "set_name": "ArchiCADQuantities",
2577
+ "key": "Area",
2578
+ "file_count": 1,
2579
+ "projects": [
2580
+ "sixty5"
2581
+ ],
2582
+ "auto_discovered": true
2583
+ },
2584
+ {
2585
+ "entity": "IfcWallStandardCase",
2586
+ "source": "qset",
2587
+ "set_name": "ArchiCADQuantities",
2588
+ "key": "Surface Area",
2589
+ "file_count": 1,
2590
+ "projects": [
2591
+ "sixty5"
2592
+ ],
2593
+ "auto_discovered": true
2594
+ },
2595
+ {
2596
+ "entity": "IfcWallStandardCase",
2597
+ "source": "qset",
2598
+ "set_name": "ArchiCADQuantities",
2599
+ "key": "Area of the Doors",
2600
+ "file_count": 1,
2601
+ "projects": [
2602
+ "sixty5"
2603
+ ],
2604
+ "auto_discovered": true
2605
+ },
2606
+ {
2607
+ "entity": "IfcWallStandardCase",
2608
+ "source": "qset",
2609
+ "set_name": "ArchiCADQuantities",
2610
+ "key": "Area of the Windows",
2611
+ "file_count": 1,
2612
+ "projects": [
2613
+ "sixty5"
2614
+ ],
2615
+ "auto_discovered": true
2616
+ },
2617
+ {
2618
+ "entity": "IfcWallStandardCase",
2619
+ "source": "qset",
2620
+ "set_name": "ArchiCADQuantities",
2621
+ "key": "Analytic Surface Area of Openings on the Inside Face",
2622
+ "file_count": 1,
2623
+ "projects": [
2624
+ "sixty5"
2625
+ ],
2626
+ "auto_discovered": true
2627
+ },
2628
+ {
2629
+ "entity": "IfcWallStandardCase",
2630
+ "source": "qset",
2631
+ "set_name": "ArchiCADQuantities",
2632
+ "key": "Analytic Surface Area of Openings on the Outside Face",
2633
+ "file_count": 1,
2634
+ "projects": [
2635
+ "sixty5"
2636
+ ],
2637
+ "auto_discovered": true
2638
+ },
2639
+ {
2640
+ "entity": "IfcWallStandardCase",
2641
+ "source": "qset",
2642
+ "set_name": "ArchiCADQuantities",
2643
+ "key": "Net Surface Area of the Edges",
2644
+ "file_count": 1,
2645
+ "projects": [
2646
+ "sixty5"
2647
+ ],
2648
+ "auto_discovered": true
2649
+ },
2650
+ {
2651
+ "entity": "IfcWallStandardCase",
2652
+ "source": "qset",
2653
+ "set_name": "ArchiCADQuantities",
2654
+ "key": "Net Surface Area on the Inside Face",
2655
+ "file_count": 1,
2656
+ "projects": [
2657
+ "sixty5"
2658
+ ],
2659
+ "auto_discovered": true
2660
+ },
2661
+ {
2662
+ "entity": "IfcWallStandardCase",
2663
+ "source": "qset",
2664
+ "set_name": "ArchiCADQuantities",
2665
+ "key": "Net Surface Area on the Outside Face",
2666
+ "file_count": 1,
2667
+ "projects": [
2668
+ "sixty5"
2669
+ ],
2670
+ "auto_discovered": true
2671
+ },
2672
+ {
2673
+ "entity": "IfcWallStandardCase",
2674
+ "source": "qset",
2675
+ "set_name": "ArchiCADQuantities",
2676
+ "key": "Conditional Surface Area on the Inside Face",
2677
+ "file_count": 1,
2678
+ "projects": [
2679
+ "sixty5"
2680
+ ],
2681
+ "auto_discovered": true
2682
+ },
2683
+ {
2684
+ "entity": "IfcWallStandardCase",
2685
+ "source": "qset",
2686
+ "set_name": "ArchiCADQuantities",
2687
+ "key": "Conditional Surface Area on the Outside Face",
2688
+ "file_count": 1,
2689
+ "projects": [
2690
+ "sixty5"
2691
+ ],
2692
+ "auto_discovered": true
2693
+ },
2694
+ {
2695
+ "entity": "IfcWallStandardCase",
2696
+ "source": "qset",
2697
+ "set_name": "ArchiCADQuantities",
2698
+ "key": "Gross Surface Area of the Wall on the Inside Face",
2699
+ "file_count": 1,
2700
+ "projects": [
2701
+ "sixty5"
2702
+ ],
2703
+ "auto_discovered": true
2704
+ },
2705
+ {
2706
+ "entity": "IfcWallStandardCase",
2707
+ "source": "qset",
2708
+ "set_name": "ArchiCADQuantities",
2709
+ "key": "Gross Surface Area of the Wall on the Outside Face",
2710
+ "file_count": 1,
2711
+ "projects": [
2712
+ "sixty5"
2713
+ ],
2714
+ "auto_discovered": true
2715
+ },
2716
+ {
2717
+ "entity": "IfcWallStandardCase",
2718
+ "source": "qset",
2719
+ "set_name": "ArchiCADQuantities",
2720
+ "key": "Area of the Wall",
2721
+ "file_count": 1,
2722
+ "projects": [
2723
+ "sixty5"
2724
+ ],
2725
+ "auto_discovered": true
2726
+ },
2727
+ {
2728
+ "entity": "IfcWallStandardCase",
2729
+ "source": "qset",
2730
+ "set_name": "ArchiCADQuantities",
2731
+ "key": "Surface Area of Empty Openings in the Wall",
2732
+ "file_count": 1,
2733
+ "projects": [
2734
+ "sixty5"
2735
+ ],
2736
+ "auto_discovered": true
2737
+ },
2738
+ {
2739
+ "entity": "IfcWallStandardCase",
2740
+ "source": "qset",
2741
+ "set_name": "(unnamed)",
2742
+ "key": "NetFootprintArea",
2743
+ "file_count": 1,
2744
+ "projects": [
2745
+ "hitos"
2746
+ ],
2747
+ "auto_discovered": true
2748
+ },
2749
+ {
2750
+ "entity": "IfcWallStandardCase",
2751
+ "source": "qset",
2752
+ "set_name": "(unnamed)",
2753
+ "key": "NetSideAreaLeft",
2754
+ "file_count": 1,
2755
+ "projects": [
2756
+ "hitos"
2757
+ ],
2758
+ "auto_discovered": true
2759
+ },
2760
+ {
2761
+ "entity": "IfcWallStandardCase",
2762
+ "source": "qset",
2763
+ "set_name": "(unnamed)",
2764
+ "key": "NetSideAreaRight",
2765
+ "file_count": 1,
2766
+ "projects": [
2767
+ "hitos"
2768
+ ],
2769
+ "auto_discovered": true
2770
+ },
2771
+ {
2772
+ "entity": "IfcCovering",
2773
+ "source": "qset",
2774
+ "set_name": "Qto_CoveringBaseQuantities",
2775
+ "key": "GrossArea",
2776
+ "file_count": 1,
2777
+ "projects": [
2778
+ "digital_hub"
2779
+ ],
2780
+ "auto_discovered": true
2781
+ },
2782
+ {
2783
+ "entity": "IfcCovering",
2784
+ "source": "qset",
2785
+ "set_name": "Qto_CoveringBaseQuantities",
2786
+ "key": "NetArea",
2787
+ "file_count": 1,
2788
+ "projects": [
2789
+ "digital_hub"
2790
+ ],
2791
+ "auto_discovered": true
2792
+ },
2793
+ {
2794
+ "entity": "IfcCovering",
2795
+ "source": "qset",
2796
+ "set_name": "BaseQuantities",
2797
+ "key": "GrossArea",
2798
+ "file_count": 1,
2799
+ "projects": [
2800
+ "sixty5"
2801
+ ],
2802
+ "auto_discovered": true
2803
+ },
2804
+ {
2805
+ "entity": "IfcCovering",
2806
+ "source": "qset",
2807
+ "set_name": "BaseQuantities",
2808
+ "key": "NetArea",
2809
+ "file_count": 1,
2810
+ "projects": [
2811
+ "sixty5"
2812
+ ],
2813
+ "auto_discovered": true
2814
+ },
2815
+ {
2816
+ "entity": "IfcCovering",
2817
+ "source": "qset",
2818
+ "set_name": "ArchiCADQuantities",
2819
+ "key": "Area",
2820
+ "file_count": 1,
2821
+ "projects": [
2822
+ "sixty5"
2823
+ ],
2824
+ "auto_discovered": true
2825
+ },
2826
+ {
2827
+ "entity": "IfcCovering",
2828
+ "source": "qset",
2829
+ "set_name": "ArchiCADQuantities",
2830
+ "key": "Surface Area",
2831
+ "file_count": 1,
2832
+ "projects": [
2833
+ "sixty5"
2834
+ ],
2835
+ "auto_discovered": true
2836
+ },
2837
+ {
2838
+ "entity": "IfcCovering",
2839
+ "source": "qset",
2840
+ "set_name": "ArchiCADQuantities",
2841
+ "key": "Area of the Doors",
2842
+ "file_count": 1,
2843
+ "projects": [
2844
+ "sixty5"
2845
+ ],
2846
+ "auto_discovered": true
2847
+ },
2848
+ {
2849
+ "entity": "IfcCovering",
2850
+ "source": "qset",
2851
+ "set_name": "ArchiCADQuantities",
2852
+ "key": "Area of the Windows",
2853
+ "file_count": 1,
2854
+ "projects": [
2855
+ "sixty5"
2856
+ ],
2857
+ "auto_discovered": true
2858
+ },
2859
+ {
2860
+ "entity": "IfcCovering",
2861
+ "source": "qset",
2862
+ "set_name": "ArchiCADQuantities",
2863
+ "key": "Analytic Surface Area of Openings on the Inside Face",
2864
+ "file_count": 1,
2865
+ "projects": [
2866
+ "sixty5"
2867
+ ],
2868
+ "auto_discovered": true
2869
+ },
2870
+ {
2871
+ "entity": "IfcCovering",
2872
+ "source": "qset",
2873
+ "set_name": "ArchiCADQuantities",
2874
+ "key": "Analytic Surface Area of Openings on the Outside Face",
2875
+ "file_count": 1,
2876
+ "projects": [
2877
+ "sixty5"
2878
+ ],
2879
+ "auto_discovered": true
2880
+ },
2881
+ {
2882
+ "entity": "IfcCovering",
2883
+ "source": "qset",
2884
+ "set_name": "ArchiCADQuantities",
2885
+ "key": "Net Surface Area of the Edges",
2886
+ "file_count": 1,
2887
+ "projects": [
2888
+ "sixty5"
2889
+ ],
2890
+ "auto_discovered": true
2891
+ },
2892
+ {
2893
+ "entity": "IfcCovering",
2894
+ "source": "qset",
2895
+ "set_name": "ArchiCADQuantities",
2896
+ "key": "Net Surface Area on the Inside Face",
2897
+ "file_count": 1,
2898
+ "projects": [
2899
+ "sixty5"
2900
+ ],
2901
+ "auto_discovered": true
2902
+ },
2903
+ {
2904
+ "entity": "IfcCovering",
2905
+ "source": "qset",
2906
+ "set_name": "ArchiCADQuantities",
2907
+ "key": "Net Surface Area on the Outside Face",
2908
+ "file_count": 1,
2909
+ "projects": [
2910
+ "sixty5"
2911
+ ],
2912
+ "auto_discovered": true
2913
+ },
2914
+ {
2915
+ "entity": "IfcCovering",
2916
+ "source": "qset",
2917
+ "set_name": "ArchiCADQuantities",
2918
+ "key": "Conditional Surface Area on the Inside Face",
2919
+ "file_count": 1,
2920
+ "projects": [
2921
+ "sixty5"
2922
+ ],
2923
+ "auto_discovered": true
2924
+ },
2925
+ {
2926
+ "entity": "IfcCovering",
2927
+ "source": "qset",
2928
+ "set_name": "ArchiCADQuantities",
2929
+ "key": "Conditional Surface Area on the Outside Face",
2930
+ "file_count": 1,
2931
+ "projects": [
2932
+ "sixty5"
2933
+ ],
2934
+ "auto_discovered": true
2935
+ },
2936
+ {
2937
+ "entity": "IfcCovering",
2938
+ "source": "qset",
2939
+ "set_name": "ArchiCADQuantities",
2940
+ "key": "Gross Surface Area of the Wall on the Inside Face",
2941
+ "file_count": 1,
2942
+ "projects": [
2943
+ "sixty5"
2944
+ ],
2945
+ "auto_discovered": true
2946
+ },
2947
+ {
2948
+ "entity": "IfcCovering",
2949
+ "source": "qset",
2950
+ "set_name": "ArchiCADQuantities",
2951
+ "key": "Gross Surface Area of the Wall on the Outside Face",
2952
+ "file_count": 1,
2953
+ "projects": [
2954
+ "sixty5"
2955
+ ],
2956
+ "auto_discovered": true
2957
+ },
2958
+ {
2959
+ "entity": "IfcCovering",
2960
+ "source": "qset",
2961
+ "set_name": "ArchiCADQuantities",
2962
+ "key": "Area of the Wall",
2963
+ "file_count": 1,
2964
+ "projects": [
2965
+ "sixty5"
2966
+ ],
2967
+ "auto_discovered": true
2968
+ },
2969
+ {
2970
+ "entity": "IfcCovering",
2971
+ "source": "qset",
2972
+ "set_name": "ArchiCADQuantities",
2973
+ "key": "Surface Area of Empty Openings in the Wall",
2974
+ "file_count": 1,
2975
+ "projects": [
2976
+ "sixty5"
2977
+ ],
2978
+ "auto_discovered": true
2979
+ },
2980
+ {
2981
+ "entity": "IfcCovering",
2982
+ "source": "qset",
2983
+ "set_name": "ArchiCADQuantities",
2984
+ "key": "Bottom Surface Area",
2985
+ "file_count": 1,
2986
+ "projects": [
2987
+ "sixty5"
2988
+ ],
2989
+ "auto_discovered": true
2990
+ },
2991
+ {
2992
+ "entity": "IfcCovering",
2993
+ "source": "qset",
2994
+ "set_name": "ArchiCADQuantities",
2995
+ "key": "Conditional Surface Area of the Bottom",
2996
+ "file_count": 1,
2997
+ "projects": [
2998
+ "sixty5"
2999
+ ],
3000
+ "auto_discovered": true
3001
+ },
3002
+ {
3003
+ "entity": "IfcCovering",
3004
+ "source": "qset",
3005
+ "set_name": "ArchiCADQuantities",
3006
+ "key": "Conditional Surface Area of the Top",
3007
+ "file_count": 1,
3008
+ "projects": [
3009
+ "sixty5"
3010
+ ],
3011
+ "auto_discovered": true
3012
+ },
3013
+ {
3014
+ "entity": "IfcCovering",
3015
+ "source": "qset",
3016
+ "set_name": "ArchiCADQuantities",
3017
+ "key": "Edge Surface Area",
3018
+ "file_count": 1,
3019
+ "projects": [
3020
+ "sixty5"
3021
+ ],
3022
+ "auto_discovered": true
3023
+ },
3024
+ {
3025
+ "entity": "IfcCovering",
3026
+ "source": "qset",
3027
+ "set_name": "ArchiCADQuantities",
3028
+ "key": "Gross Surface Area of the Slab Bottom",
3029
+ "file_count": 1,
3030
+ "projects": [
3031
+ "sixty5"
3032
+ ],
3033
+ "auto_discovered": true
3034
+ },
3035
+ {
3036
+ "entity": "IfcCovering",
3037
+ "source": "qset",
3038
+ "set_name": "ArchiCADQuantities",
3039
+ "key": "Gross Surface Area of the Slab Bottom with Holes",
3040
+ "file_count": 1,
3041
+ "projects": [
3042
+ "sixty5"
3043
+ ],
3044
+ "auto_discovered": true
3045
+ },
3046
+ {
3047
+ "entity": "IfcCovering",
3048
+ "source": "qset",
3049
+ "set_name": "ArchiCADQuantities",
3050
+ "key": "Gross Surface Area of the Slab Edges",
3051
+ "file_count": 1,
3052
+ "projects": [
3053
+ "sixty5"
3054
+ ],
3055
+ "auto_discovered": true
3056
+ },
3057
+ {
3058
+ "entity": "IfcCovering",
3059
+ "source": "qset",
3060
+ "set_name": "ArchiCADQuantities",
3061
+ "key": "Gross Surface Area of the Slab Edges with Holes",
3062
+ "file_count": 1,
3063
+ "projects": [
3064
+ "sixty5"
3065
+ ],
3066
+ "auto_discovered": true
3067
+ },
3068
+ {
3069
+ "entity": "IfcCovering",
3070
+ "source": "qset",
3071
+ "set_name": "ArchiCADQuantities",
3072
+ "key": "Gross Surface Area of the Slab Top",
3073
+ "file_count": 1,
3074
+ "projects": [
3075
+ "sixty5"
3076
+ ],
3077
+ "auto_discovered": true
3078
+ },
3079
+ {
3080
+ "entity": "IfcCovering",
3081
+ "source": "qset",
3082
+ "set_name": "ArchiCADQuantities",
3083
+ "key": "Gross Surface Area of the Slab Top with Holes",
3084
+ "file_count": 1,
3085
+ "projects": [
3086
+ "sixty5"
3087
+ ],
3088
+ "auto_discovered": true
3089
+ },
3090
+ {
3091
+ "entity": "IfcCovering",
3092
+ "source": "qset",
3093
+ "set_name": "ArchiCADQuantities",
3094
+ "key": "Holes Surface Area",
3095
+ "file_count": 1,
3096
+ "projects": [
3097
+ "sixty5"
3098
+ ],
3099
+ "auto_discovered": true
3100
+ },
3101
+ {
3102
+ "entity": "IfcCovering",
3103
+ "source": "qset",
3104
+ "set_name": "ArchiCADQuantities",
3105
+ "key": "Top Surface Area",
3106
+ "file_count": 1,
3107
+ "projects": [
3108
+ "sixty5"
3109
+ ],
3110
+ "auto_discovered": true
3111
+ },
3112
+ {
3113
+ "entity": "IfcCovering",
3114
+ "source": "qset",
3115
+ "set_name": "ArchiCADQuantities",
3116
+ "key": "Area of the Column",
3117
+ "file_count": 1,
3118
+ "projects": [
3119
+ "sixty5"
3120
+ ],
3121
+ "auto_discovered": true
3122
+ },
3123
+ {
3124
+ "entity": "IfcCovering",
3125
+ "source": "qset",
3126
+ "set_name": "ArchiCADQuantities",
3127
+ "key": "Gross Surface Area of the Core (without Top/Bottom)",
3128
+ "file_count": 1,
3129
+ "projects": [
3130
+ "sixty5"
3131
+ ],
3132
+ "auto_discovered": true
3133
+ },
3134
+ {
3135
+ "entity": "IfcCovering",
3136
+ "source": "qset",
3137
+ "set_name": "ArchiCADQuantities",
3138
+ "key": "Gross Surface Area of the Core Top (or Bottom)",
3139
+ "file_count": 1,
3140
+ "projects": [
3141
+ "sixty5"
3142
+ ],
3143
+ "auto_discovered": true
3144
+ },
3145
+ {
3146
+ "entity": "IfcCovering",
3147
+ "source": "qset",
3148
+ "set_name": "ArchiCADQuantities",
3149
+ "key": "Gross Surface Area of the Veneer (without Top/Bottom)",
3150
+ "file_count": 1,
3151
+ "projects": [
3152
+ "sixty5"
3153
+ ],
3154
+ "auto_discovered": true
3155
+ },
3156
+ {
3157
+ "entity": "IfcCovering",
3158
+ "source": "qset",
3159
+ "set_name": "ArchiCADQuantities",
3160
+ "key": "Gross Surface Area of the Veneer Top (or Bottom)",
3161
+ "file_count": 1,
3162
+ "projects": [
3163
+ "sixty5"
3164
+ ],
3165
+ "auto_discovered": true
3166
+ },
3167
+ {
3168
+ "entity": "IfcCovering",
3169
+ "source": "qset",
3170
+ "set_name": "ArchiCADQuantities",
3171
+ "key": "Net Surface Area of the Core Bottom",
3172
+ "file_count": 1,
3173
+ "projects": [
3174
+ "sixty5"
3175
+ ],
3176
+ "auto_discovered": true
3177
+ },
3178
+ {
3179
+ "entity": "IfcCovering",
3180
+ "source": "qset",
3181
+ "set_name": "ArchiCADQuantities",
3182
+ "key": "Net Surface Area of the Core (without Top/Bottom)",
3183
+ "file_count": 1,
3184
+ "projects": [
3185
+ "sixty5"
3186
+ ],
3187
+ "auto_discovered": true
3188
+ },
3189
+ {
3190
+ "entity": "IfcCovering",
3191
+ "source": "qset",
3192
+ "set_name": "ArchiCADQuantities",
3193
+ "key": "Net Surface Area of the Core Top",
3194
+ "file_count": 1,
3195
+ "projects": [
3196
+ "sixty5"
3197
+ ],
3198
+ "auto_discovered": true
3199
+ },
3200
+ {
3201
+ "entity": "IfcCovering",
3202
+ "source": "qset",
3203
+ "set_name": "ArchiCADQuantities",
3204
+ "key": "Net Surface Area of the Veneer Bottom",
3205
+ "file_count": 1,
3206
+ "projects": [
3207
+ "sixty5"
3208
+ ],
3209
+ "auto_discovered": true
3210
+ },
3211
+ {
3212
+ "entity": "IfcCovering",
3213
+ "source": "qset",
3214
+ "set_name": "ArchiCADQuantities",
3215
+ "key": "Veneer Surface Area",
3216
+ "file_count": 1,
3217
+ "projects": [
3218
+ "sixty5"
3219
+ ],
3220
+ "auto_discovered": true
3221
+ },
3222
+ {
3223
+ "entity": "IfcCovering",
3224
+ "source": "qset",
3225
+ "set_name": "ArchiCADQuantities",
3226
+ "key": "Net Surface Area of the Veneer Top",
3227
+ "file_count": 1,
3228
+ "projects": [
3229
+ "sixty5"
3230
+ ],
3231
+ "auto_discovered": true
3232
+ },
3233
+ {
3234
+ "entity": "IfcCovering",
3235
+ "source": "qset",
3236
+ "set_name": "ArchiCADQuantities",
3237
+ "key": "End Surface Area",
3238
+ "file_count": 1,
3239
+ "projects": [
3240
+ "sixty5"
3241
+ ],
3242
+ "auto_discovered": true
3243
+ },
3244
+ {
3245
+ "entity": "IfcCovering",
3246
+ "source": "qset",
3247
+ "set_name": "ArchiCADQuantities",
3248
+ "key": "Holes Edge Surface Area",
3249
+ "file_count": 1,
3250
+ "projects": [
3251
+ "sixty5"
3252
+ ],
3253
+ "auto_discovered": true
3254
+ },
3255
+ {
3256
+ "entity": "IfcCovering",
3257
+ "source": "qset",
3258
+ "set_name": "ArchiCADQuantities",
3259
+ "key": "Left Side Surface Area",
3260
+ "file_count": 1,
3261
+ "projects": [
3262
+ "sixty5"
3263
+ ],
3264
+ "auto_discovered": true
3265
+ },
3266
+ {
3267
+ "entity": "IfcCovering",
3268
+ "source": "qset",
3269
+ "set_name": "ArchiCADQuantities",
3270
+ "key": "Right Side Surface Area",
3271
+ "file_count": 1,
3272
+ "projects": [
3273
+ "sixty5"
3274
+ ],
3275
+ "auto_discovered": true
3276
+ },
3277
+ {
3278
+ "entity": "IfcSite",
3279
+ "source": "qset",
3280
+ "set_name": "BaseQuantities",
3281
+ "key": "GrossArea",
3282
+ "file_count": 7,
3283
+ "projects": [
3284
+ "ac20",
3285
+ "fzk_house",
3286
+ "molio",
3287
+ "sixty5",
3288
+ "smiley_west"
3289
+ ],
3290
+ "auto_discovered": true
3291
+ },
3292
+ {
3293
+ "entity": "IfcBuilding",
3294
+ "source": "qset",
3295
+ "set_name": "BaseQuantities",
3296
+ "key": "GrossFloorArea",
3297
+ "file_count": 7,
3298
+ "projects": [
3299
+ "ac20",
3300
+ "fzk_house",
3301
+ "molio",
3302
+ "sixty5",
3303
+ "smiley_west"
3304
+ ],
3305
+ "auto_discovered": true
3306
+ },
3307
+ {
3308
+ "entity": "IfcBuildingStorey",
3309
+ "source": "qset",
3310
+ "set_name": "BaseQuantities",
3311
+ "key": "GrossFloorArea",
3312
+ "file_count": 7,
3313
+ "projects": [
3314
+ "ac20",
3315
+ "fzk_house",
3316
+ "molio",
3317
+ "sixty5",
3318
+ "smiley_west"
3319
+ ],
3320
+ "auto_discovered": true
3321
+ }
3322
+ ]
3323
+ }
teams/lux-ai/Final pipeline/run_solar_analysis.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ run_solar_analysis.py β€” Unified orchestrator.
3
+
4
+ Modes
5
+ -----
6
+ 1. Single-file solar analysis (default):
7
+ python -m solar_pipeline.run_solar_analysis path/to/model.ifc
8
+
9
+ 2. Batch metadata scan (no API calls):
10
+ python -m solar_pipeline.run_solar_analysis --scan-only --root "Sample projects/projects"
11
+
12
+ 3. Batch solar analysis:
13
+ python -m solar_pipeline.run_solar_analysis --batch --root "Sample projects/projects"
14
+
15
+ Location is auto-extracted from IfcSite. Override with --lat / --lon / --name
16
+ when the IFC file has no geographic coordinates.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import logging
24
+ import os
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ # ── Ensure package is importable when run as a script ─────────────────────────
29
+ _SCRIPT_DIR = Path(__file__).resolve().parent
30
+ _REPO_ROOT = _SCRIPT_DIR.parent
31
+ if str(_REPO_ROOT) not in sys.path:
32
+ sys.path.insert(0, str(_REPO_ROOT))
33
+
34
+ from solar_pipeline.config import ( # noqa: E402
35
+ DEFAULT_CONSUMPTION_KWH_PER_M2,
36
+ DEFAULT_SAMPLE_ROOT,
37
+ FALLBACK_CONSUMPTION_KWH,
38
+ )
39
+ from solar_pipeline.ifc_metadata_extractor import ( # noqa: E402
40
+ Location,
41
+ extract_all,
42
+ extract_floor_area,
43
+ extract_location,
44
+ extract_true_north,
45
+ find_ifc_files,
46
+ open_model,
47
+ print_summary_table,
48
+ scan_all,
49
+ )
50
+ from solar_pipeline.ifc_roof_parser import parse_roof_segments # noqa: E402
51
+ from solar_pipeline.solar_production_engine import run_production_analysis # noqa: E402
52
+
53
+ log = logging.getLogger(__name__)
54
+
55
+
56
+ # ── Single-file solar analysis ────────────────────────────────────────────────
57
+
58
+ def run_single(
59
+ ifc_path: Path,
60
+ lat: float | None = None,
61
+ lon: float | None = None,
62
+ name: str | None = None,
63
+ ) -> dict | None:
64
+ """
65
+ Full pipeline: IFC β†’ metadata + roof geometry β†’ PVWatts β†’ report.
66
+
67
+ Returns the production-analysis result dict, or None on failure.
68
+ """
69
+ if not ifc_path.is_file():
70
+ print(f"[Error] IFC file not found: {ifc_path}")
71
+ return None
72
+
73
+ project = ifc_path.parent.name
74
+
75
+ # ── Step 1: Metadata extraction ───────────────────────────────────────
76
+ print("=" * 70)
77
+ print(" STEP 1 β€” Extracting IFC metadata")
78
+ print("=" * 70)
79
+
80
+ metadata = extract_all(ifc_path)
81
+ for key in ("window_area_m2", "floor_area_m2", "roof_area_m2",
82
+ "true_north_angle_deg", "latitude", "longitude"):
83
+ val = metadata.get(key)
84
+ label = key.replace("_", " ").title()
85
+ print(f" {label:.<30s} {val if val is not None else 'N/A'}")
86
+
87
+ # ── Resolve location ──────────────────────────────────────────────────
88
+ if lat is not None and lon is not None:
89
+ location = Location(latitude=lat, longitude=lon, name=name or project)
90
+ print(f"\n Location (CLI override): {location.latitude}, {location.longitude}")
91
+ elif metadata.get("latitude") is not None and metadata.get("longitude") is not None:
92
+ location = Location(
93
+ latitude=metadata["latitude"],
94
+ longitude=metadata["longitude"],
95
+ name=name or project,
96
+ )
97
+ print(f"\n Location (auto from IfcSite): {location.latitude}, {location.longitude}")
98
+ else:
99
+ print("\n [Error] No location available. Use --lat / --lon or provide an IFC file with IfcSite coordinates.")
100
+ return None
101
+
102
+ # ── Step 2: Roof geometry parsing ─────────────────────────────────────
103
+ print()
104
+ print("=" * 70)
105
+ print(" STEP 2 β€” Parsing roof geometry")
106
+ print("=" * 70)
107
+
108
+ segments = parse_roof_segments(ifc_path)
109
+ if not segments:
110
+ print(" No roof segments found. Cannot run solar analysis.")
111
+ return None
112
+
113
+ for seg in segments:
114
+ print(f" {seg['id']} | Area: {seg['area']:>7.1f} mΒ² | "
115
+ f"Tilt: {seg['tilt']:>5.1f}Β° | Azimuth: {seg['azimuth']:>5.1f}Β°")
116
+
117
+ # ── Step 3: Solar production analysis ─────────────────────────────────
118
+ print()
119
+ print("=" * 70)
120
+ print(" STEP 3 β€” Querying NREL PVWatts v8")
121
+ print("=" * 70)
122
+
123
+ result = run_production_analysis(segments=segments, location=location)
124
+
125
+ # ── Step 4: Summary & LEED estimate ───────────────────────────────────
126
+ print()
127
+ print("=" * 70)
128
+ print(" STEP 4 β€” Summary")
129
+ print("=" * 70)
130
+
131
+ total_kwh = result["total_kwh"]
132
+ total_area = sum(s["area"] for s in result["segments"])
133
+ print(f" IFC file : {ifc_path.name}")
134
+ print(f" Project : {project}")
135
+ print(f" Location : {location.name} ({location.latitude}, {location.longitude})")
136
+ print(f" Segments : {len(result['segments'])}")
137
+ print(f" Total roof : {total_area:,.2f} mΒ²")
138
+ print(f" Total prod. : {total_kwh:,.2f} kWh/yr")
139
+
140
+ # LEED estimate β€” use floor area if available
141
+ floor_area = metadata.get("floor_area_m2")
142
+ if floor_area and floor_area > 0:
143
+ consumption = floor_area * DEFAULT_CONSUMPTION_KWH_PER_M2
144
+ print(f"\n LEED estimate (floor area = {floor_area:,.1f} mΒ² Γ— "
145
+ f"{DEFAULT_CONSUMPTION_KWH_PER_M2} kWh/mΒ²/yr):")
146
+ else:
147
+ consumption = FALLBACK_CONSUMPTION_KWH
148
+ print(f"\n LEED estimate (assumed consumption = {consumption:,} kWh/yr):")
149
+
150
+ leed_pct = (total_kwh / consumption) * 100 if consumption > 0 else 0
151
+ print(f" Consumption : {consumption:,.0f} kWh/yr")
152
+ print(f" Production : {total_kwh:,.0f} kWh/yr")
153
+ print(f" Score : {total_kwh:,.0f} / {consumption:,.0f} Γ— 100 = {leed_pct:.1f}%")
154
+ print()
155
+
156
+ return result
157
+
158
+
159
+ # ── Batch modes ───────────────────────────────────────────────────────────────
160
+
161
+ def run_batch_scan(root: Path, output_csv: Path) -> list[dict]:
162
+ """Scan all IFC files and write metadata CSV (no API calls)."""
163
+ results = scan_all(root, output_csv)
164
+ print_summary_table(results)
165
+ return results
166
+
167
+
168
+ def run_batch_solar(root: Path, output_csv: Path) -> None:
169
+ """
170
+ Scan all IFC files, then run solar analysis on each ``arc.ifc``
171
+ that has both roof elements and location data.
172
+ """
173
+ # First do the metadata scan
174
+ results = scan_all(root, output_csv)
175
+ print_summary_table(results)
176
+
177
+ # Then run solar on architectural files with location
178
+ print("\n" + "=" * 70)
179
+ print(" BATCH SOLAR ANALYSIS β€” processing arc.ifc files with location data")
180
+ print("=" * 70 + "\n")
181
+
182
+ solar_results: list[dict] = []
183
+ for r in results:
184
+ if r.get("error"):
185
+ continue
186
+ if r.get("latitude") is None or r.get("longitude") is None:
187
+ continue
188
+ # Only process architectural files
189
+ if r["ifc_file"] != "arc.ifc":
190
+ continue
191
+
192
+ ifc_path = root / r["project_name"] / r["ifc_file"]
193
+ if not ifc_path.is_file():
194
+ continue
195
+
196
+ print(f"\n--- {r['project_name']} ---")
197
+ try:
198
+ segments = parse_roof_segments(ifc_path)
199
+ if not segments:
200
+ print(f" No roof segments β€” skipped")
201
+ continue
202
+
203
+ location = Location(
204
+ latitude=r["latitude"],
205
+ longitude=r["longitude"],
206
+ name=r["project_name"],
207
+ )
208
+ result = run_production_analysis(segments, location, verbose=True)
209
+ solar_results.append({
210
+ "project": r["project_name"],
211
+ "total_kwh": result["total_kwh"],
212
+ "segments": len(result["segments"]),
213
+ "roof_area_m2": sum(s["area"] for s in result["segments"]),
214
+ })
215
+ except Exception as exc:
216
+ log.warning(" Solar analysis failed for %s: %s", r["project_name"], exc)
217
+
218
+ if solar_results:
219
+ print("\n\n" + "=" * 70)
220
+ print(" BATCH SOLAR SUMMARY")
221
+ print("=" * 70)
222
+ for sr in solar_results:
223
+ print(f" {sr['project']:.<30s} {sr['total_kwh']:>10,.0f} kWh/yr "
224
+ f"({sr['segments']} segs, {sr['roof_area_m2']:,.0f} mΒ²)")
225
+ total = sum(sr["total_kwh"] for sr in solar_results)
226
+ print(f"\n Grand total: {total:,.0f} kWh/yr across {len(solar_results)} buildings")
227
+
228
+
229
+ # ── CLI ───────────────────────────────────────────────────────────────────────
230
+
231
+ def build_parser() -> argparse.ArgumentParser:
232
+ parser = argparse.ArgumentParser(
233
+ prog="solar_pipeline",
234
+ description="Unified IFC metadata extraction + solar production analysis.",
235
+ )
236
+
237
+ # Modes (mutually exclusive)
238
+ mode = parser.add_mutually_exclusive_group()
239
+ mode.add_argument(
240
+ "--scan-only",
241
+ action="store_true",
242
+ help="Batch metadata scan only (no PVWatts API calls).",
243
+ )
244
+ mode.add_argument(
245
+ "--batch",
246
+ action="store_true",
247
+ help="Batch solar analysis of all arc.ifc files under --root.",
248
+ )
249
+
250
+ # Paths
251
+ parser.add_argument(
252
+ "ifc_file",
253
+ nargs="?",
254
+ type=Path,
255
+ default=None,
256
+ help="Path to a single IFC file (for single-file mode).",
257
+ )
258
+ parser.add_argument(
259
+ "--root",
260
+ type=Path,
261
+ default=DEFAULT_SAMPLE_ROOT,
262
+ help=f"Root directory for batch scanning (default: {DEFAULT_SAMPLE_ROOT}).",
263
+ )
264
+ parser.add_argument(
265
+ "--output",
266
+ type=Path,
267
+ default=_REPO_ROOT / "ifc_scan_results.csv",
268
+ help="Output CSV file for batch scan results.",
269
+ )
270
+
271
+ # Location override
272
+ parser.add_argument("--lat", type=float, default=None, help="Site latitude (override IfcSite).")
273
+ parser.add_argument("--lon", type=float, default=None, help="Site longitude (override IfcSite).")
274
+ parser.add_argument("--name", type=str, default=None, help="Project name for report.")
275
+
276
+ return parser
277
+
278
+
279
+ def main() -> None:
280
+ logging.basicConfig(
281
+ level=logging.INFO,
282
+ format="%(levelname)-8s %(message)s",
283
+ handlers=[
284
+ logging.StreamHandler(),
285
+ logging.FileHandler(
286
+ _REPO_ROOT / "solar_pipeline.log", mode="w", encoding="utf-8",
287
+ ),
288
+ ],
289
+ )
290
+
291
+ parser = build_parser()
292
+ args = parser.parse_args()
293
+
294
+ if args.scan_only:
295
+ # ── Batch metadata scan ───────────────────────────────────────────
296
+ if not args.root.exists():
297
+ print(f"[Error] Root directory not found: {args.root}")
298
+ sys.exit(1)
299
+ run_batch_scan(args.root, args.output)
300
+
301
+ elif args.batch:
302
+ # ── Batch solar analysis ──────────────────────────────────────────
303
+ if not args.root.exists():
304
+ print(f"[Error] Root directory not found: {args.root}")
305
+ sys.exit(1)
306
+ run_batch_solar(args.root, args.output)
307
+
308
+ else:
309
+ # ── Single-file solar analysis ────────────────────────────────────
310
+ if args.ifc_file is None:
311
+ parser.print_help()
312
+ print("\n[Error] Provide an IFC file path or use --scan-only / --batch.")
313
+ sys.exit(1)
314
+ result = run_single(
315
+ args.ifc_file,
316
+ lat=args.lat,
317
+ lon=args.lon,
318
+ name=args.name,
319
+ )
320
+ if result is None:
321
+ sys.exit(1)
322
+
323
+
324
+ if __name__ == "__main__":
325
+ main()
teams/lux-ai/Final pipeline/solar_production_engine.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ solar_production_engine.py β€” NREL PVWatts v8 API client.
3
+
4
+ Calculates annual solar energy production (kWh) for roof segments.
5
+ Each segment has its own tilt & azimuth, yielding per-orientation accuracy
6
+ for LEED renewable-energy scoring.
7
+
8
+ LEED Score = (Ξ£ Segment_kWh / Consumption_total) Γ— 100
9
+
10
+ Adapted from the original standalone engine to integrate with:
11
+ - ifc_metadata_extractor.py (auto location from IfcSite)
12
+ - ifc_roof_parser.py (geometry-based roof segments)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import time
19
+
20
+ import requests
21
+
22
+ from solar_pipeline.config import (
23
+ ARRAY_TYPE,
24
+ MODULE_TYPE,
25
+ NREL_API_KEY,
26
+ PANEL_EFFICIENCY,
27
+ PVWATTS_BASE_URL,
28
+ SYSTEM_LOSSES,
29
+ )
30
+ from solar_pipeline.ifc_metadata_extractor import Location
31
+
32
+ log = logging.getLogger(__name__)
33
+
34
+
35
+ # ── Core calculation ──────────────────────────────────────────────────────────
36
+
37
+ def calculate_segment_production(
38
+ area: float,
39
+ tilt: float,
40
+ azimuth: float,
41
+ location: Location,
42
+ ) -> float:
43
+ """
44
+ Query NREL PVWatts v8 for a single roof segment.
45
+
46
+ Parameters
47
+ ----------
48
+ area : segment area in mΒ²
49
+ tilt : panel tilt in degrees from horizontal
50
+ azimuth : compass bearing (180Β° = due south)
51
+ location : site coordinates
52
+
53
+ Returns
54
+ -------
55
+ float β€” annual AC production in kWh, or 0.0 on error.
56
+ """
57
+ system_capacity = area * PANEL_EFFICIENCY # kW
58
+
59
+ params = {
60
+ "api_key": NREL_API_KEY,
61
+ "lat": location.latitude,
62
+ "lon": location.longitude,
63
+ "system_capacity": round(system_capacity, 3),
64
+ "azimuth": azimuth,
65
+ "tilt": tilt,
66
+ "array_type": ARRAY_TYPE,
67
+ "module_type": MODULE_TYPE,
68
+ "losses": SYSTEM_LOSSES,
69
+ }
70
+
71
+ try:
72
+ response = requests.get(PVWATTS_BASE_URL, params=params, timeout=30)
73
+ response.raise_for_status()
74
+ data = response.json()
75
+
76
+ if "errors" in data and data["errors"]:
77
+ log.error(" PVWatts API error: %s", data["errors"])
78
+ return 0.0
79
+
80
+ annual_kwh = float(data["outputs"]["ac_annual"])
81
+ return annual_kwh
82
+
83
+ except requests.RequestException as exc:
84
+ log.error(" PVWatts request failed: %s", exc)
85
+ return 0.0
86
+ except (KeyError, TypeError, ValueError) as exc:
87
+ log.error(" Could not parse PVWatts response: %s", exc)
88
+ return 0.0
89
+
90
+
91
+ # ── Batch analysis ────────────────────────────────────────────────────────────
92
+
93
+ def run_production_analysis(
94
+ segments: list[dict],
95
+ location: Location,
96
+ *,
97
+ rate_limit_sec: float = 1.0,
98
+ verbose: bool = True,
99
+ ) -> dict:
100
+ """
101
+ Iterate over roof segments, query PVWatts for each, return results.
102
+
103
+ Parameters
104
+ ----------
105
+ segments : list of dicts, each with keys: id, area, tilt, azimuth
106
+ location : site coordinates
107
+ rate_limit_sec : pause between API calls (NREL rate-limit)
108
+ verbose : print per-segment progress to stdout
109
+
110
+ Returns
111
+ -------
112
+ dict with keys:
113
+ segments β€” list of per-segment result dicts (input + capacity_kw + annual_kwh)
114
+ total_kwh β€” total annual production (float)
115
+ location β€” the Location used
116
+ """
117
+ total_kwh = 0.0
118
+ results: list[dict] = []
119
+
120
+ if verbose:
121
+ print(f"\n--- Analysing Roof Segments for {location.name} ---")
122
+ print(f" Site: ({location.latitude}, {location.longitude})")
123
+ print(f" Panel efficiency: {PANEL_EFFICIENCY}")
124
+ print()
125
+
126
+ for i, seg in enumerate(segments):
127
+ capacity_kw = seg["area"] * PANEL_EFFICIENCY
128
+ annual = calculate_segment_production(
129
+ seg["area"], seg["tilt"], seg["azimuth"], location,
130
+ )
131
+ total_kwh += annual
132
+
133
+ result = {
134
+ "id": seg["id"],
135
+ "area": seg["area"],
136
+ "tilt": seg["tilt"],
137
+ "azimuth": seg["azimuth"],
138
+ "capacity_kw": round(capacity_kw, 2),
139
+ "annual_kwh": round(annual, 2),
140
+ }
141
+ results.append(result)
142
+
143
+ if verbose:
144
+ print(
145
+ f" {seg['id']:>15s} | "
146
+ f"Area: {seg['area']:>7.1f} mΒ² | "
147
+ f"Tilt: {seg['tilt']:>5.1f}Β° | "
148
+ f"Azimuth: {seg['azimuth']:>5.1f}Β° | "
149
+ f"Capacity: {capacity_kw:>6.1f} kW | "
150
+ f"Yield: {annual:>10,.2f} kWh/yr"
151
+ )
152
+
153
+ # Respect NREL rate limits
154
+ if i < len(segments) - 1:
155
+ time.sleep(rate_limit_sec)
156
+
157
+ if verbose:
158
+ print()
159
+ print("-" * 70)
160
+ print(f" TOTAL BUILDING PRODUCTION: {total_kwh:>12,.2f} kWh/yr")
161
+ print("-" * 70)
162
+
163
+ return {
164
+ "segments": results,
165
+ "total_kwh": round(total_kwh, 2),
166
+ "location": location,
167
+ }
teams/lux-ai/IFC key checker/discover_ifc_keys.py ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ discover_ifc_keys.py
3
+ Scans all IFC files and inventories every property set and quantity set
4
+ name used per element type, producing:
5
+ - ifc_key_inventory.json (raw: all keys found, with file counts)
6
+ - key_aliases.json (canonical: standard key β†’ all alias paths)
7
+
8
+ Run:
9
+ .venv/Scripts/python.exe discover_ifc_keys.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from collections import defaultdict
17
+ from pathlib import Path
18
+
19
+ import ifcopenshell
20
+ from tabulate import tabulate
21
+
22
+
23
+ # ── Logging ───────────────────────────────────────────────────────────────────
24
+
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format="%(levelname)-8s %(message)s",
28
+ handlers=[logging.StreamHandler()],
29
+ )
30
+ log = logging.getLogger(__name__)
31
+
32
+ # ── Element types to inventory ────────────────────────────────────────────────
33
+
34
+ TARGET_TYPES = [
35
+ "IfcWindow",
36
+ "IfcDoor",
37
+ "IfcSpace",
38
+ "IfcSlab",
39
+ "IfcRoof",
40
+ "IfcWall",
41
+ "IfcWallStandardCase",
42
+ "IfcCovering",
43
+ "IfcSite",
44
+ "IfcBuilding",
45
+ "IfcBuildingStorey",
46
+ ]
47
+
48
+ # Keywords that flag a property/quantity as area-related for highlighting
49
+ AREA_KEYWORDS = {"area", "flΓ€che", "superficie", "superficie"}
50
+ HEIGHT_KEYWORDS = {"height", "hΓΆhe", "width", "length", "depth", "perimeter"}
51
+ ORIENTATION_KEYWORDS = {"north", "latitude", "longitude", "orientation", "azimuth"}
52
+
53
+
54
+ # ── Inventory data structure ──────────────────────────────────────────────────
55
+ #
56
+ # global_inventory[element_type]["quantity_sets"][qset_name][qty_name]
57
+ # = {"file_count": int, "projects": [str, ...]}
58
+ # global_inventory[element_type]["property_sets"][pset_name][prop_name]
59
+ # = {"file_count": int, "projects": [str, ...]}
60
+
61
+ def _empty_entry() -> dict:
62
+ return {"file_count": 0, "projects": []}
63
+
64
+
65
+ def make_inventory() -> dict:
66
+ return {
67
+ t: {"quantity_sets": defaultdict(lambda: defaultdict(_empty_entry)),
68
+ "property_sets": defaultdict(lambda: defaultdict(_empty_entry))}
69
+ for t in TARGET_TYPES
70
+ }
71
+
72
+
73
+ # ── Per-element collection ────────────────────────────────────────────────────
74
+
75
+ def collect_qsets(model: ifcopenshell.file, element_type: str) -> dict:
76
+ """
77
+ Returns {qset_name: {qty_name: count_in_this_file}} for all
78
+ instances of element_type in this model.
79
+ """
80
+ result: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
81
+ for elem in model.by_type(element_type):
82
+ for rel in getattr(elem, "IsDefinedBy", []):
83
+ if not rel.is_a("IfcRelDefinesByProperties"):
84
+ continue
85
+ defn = rel.RelatingPropertyDefinition
86
+ if not defn.is_a("IfcElementQuantity"):
87
+ continue
88
+ qset_name = defn.Name or "(unnamed)"
89
+ for qty in defn.Quantities:
90
+ qty_name = qty.Name or "(unnamed)"
91
+ result[qset_name][qty_name] += 1
92
+ return result
93
+
94
+
95
+ def collect_psets(model: ifcopenshell.file, element_type: str) -> dict:
96
+ """
97
+ Returns {pset_name: {prop_name: count_in_this_file}} for all
98
+ instances of element_type in this model.
99
+ """
100
+ result: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
101
+ for elem in model.by_type(element_type):
102
+ for rel in getattr(elem, "IsDefinedBy", []):
103
+ if not rel.is_a("IfcRelDefinesByProperties"):
104
+ continue
105
+ defn = rel.RelatingPropertyDefinition
106
+ if not defn.is_a("IfcPropertySet"):
107
+ continue
108
+ pset_name = defn.Name or "(unnamed)"
109
+ for prop in getattr(defn, "HasProperties", []):
110
+ prop_name = getattr(prop, "Name", None) or "(unnamed)"
111
+ result[pset_name][prop_name] += 1
112
+ return result
113
+
114
+
115
+ # ── Merge into global inventory ───────────────────────────────────────────────
116
+
117
+ def merge_into(global_inv: dict, element_type: str, section: str,
118
+ file_data: dict, project_name: str) -> None:
119
+ """
120
+ Merge one file's {set_name: {key_name: count}} into the global inventory,
121
+ incrementing file_count and appending project_name.
122
+ """
123
+ target = global_inv[element_type][section]
124
+ for set_name, keys in file_data.items():
125
+ for key_name in keys:
126
+ entry = target[set_name][key_name]
127
+ entry["file_count"] += 1
128
+ if project_name not in entry["projects"]:
129
+ entry["projects"].append(project_name)
130
+
131
+
132
+ # ── Serialisation helpers ─────────────────────────────────────────────────────
133
+
134
+ def inventory_to_plain(global_inv: dict) -> dict:
135
+ """Convert defaultdict structure to plain dicts for JSON serialisation."""
136
+ out = {}
137
+ for etype, sections in global_inv.items():
138
+ out[etype] = {}
139
+ for section, sets in sections.items():
140
+ out[etype][section] = {}
141
+ for set_name, keys in sets.items():
142
+ out[etype][section][set_name] = {}
143
+ for key_name, entry in keys.items():
144
+ out[etype][section][set_name][key_name] = {
145
+ "file_count": entry["file_count"],
146
+ "projects": sorted(entry["projects"]),
147
+ }
148
+ return out
149
+
150
+
151
+ # ── Build key_aliases.json ────────────────────────────────────────────────────
152
+
153
+ def build_aliases(inventory: dict) -> dict:
154
+ """
155
+ Build the canonical key β†’ alias-list mapping.
156
+ Each alias is tried in order; the first one that yields data wins.
157
+ Pre-seeds with known patterns, then extends with anything discovered
158
+ in the inventory that looks area-related but isn't already covered.
159
+ """
160
+ aliases: dict[str, list[dict]] = {
161
+ "window_area": [
162
+ {"entity": "IfcWindow", "source": "qset",
163
+ "set_name": "Qto_WindowBaseQuantities", "key": "Area"},
164
+ {"entity": "IfcWindow", "source": "qset",
165
+ "set_name": "BaseQuantities", "key": "Area"},
166
+ {"entity": "IfcWindow", "source": "qset",
167
+ "set_name": "BaseQuantities", "key": "GrossArea"},
168
+ {"entity": "IfcWindow", "source": "attr",
169
+ "keys": ["OverallHeight", "OverallWidth"], "op": "multiply"},
170
+ ],
171
+ "floor_area": [
172
+ {"entity": "IfcSpace", "source": "qset",
173
+ "set_name": "Qto_SpaceBaseQuantities", "key": "NetFloorArea"},
174
+ {"entity": "IfcSpace", "source": "qset",
175
+ "set_name": "Qto_SpaceBaseQuantities", "key": "GrossFloorArea"},
176
+ {"entity": "IfcSpace", "source": "qset",
177
+ "set_name": "BaseQuantities", "key": "NetFloorArea"},
178
+ {"entity": "IfcSpace", "source": "qset",
179
+ "set_name": "BaseQuantities", "key": "GrossFloorArea"},
180
+ {"entity": "IfcSpace", "source": "qset",
181
+ "set_name": "GSA Space Areas", "key": "GSA BIM Area"},
182
+ {"entity": "IfcSlab", "source": "qset",
183
+ "set_name": "Qto_SlabBaseQuantities", "key": "NetArea",
184
+ "predefined_type": "FLOOR"},
185
+ {"entity": "IfcSlab", "source": "qset",
186
+ "set_name": "BaseQuantities", "key": "NetArea",
187
+ "predefined_type": "FLOOR"},
188
+ {"entity": "IfcSlab", "source": "qset",
189
+ "set_name": "BaseQuantities", "key": "GrossArea",
190
+ "predefined_type": "FLOOR"},
191
+ ],
192
+ "roof_area": [
193
+ {"entity": "IfcRoof", "source": "qset",
194
+ "set_name": "Qto_RoofBaseQuantities", "key": "NetArea"},
195
+ {"entity": "IfcRoof", "source": "qset",
196
+ "set_name": "Qto_RoofBaseQuantities", "key": "GrossArea"},
197
+ {"entity": "IfcRoof", "source": "qset",
198
+ "set_name": "BaseQuantities", "key": "NetArea"},
199
+ {"entity": "IfcSlab", "source": "qset",
200
+ "set_name": "Qto_SlabBaseQuantities", "key": "NetArea",
201
+ "predefined_type": "ROOF"},
202
+ {"entity": "IfcSlab", "source": "qset",
203
+ "set_name": "Qto_SlabBaseQuantities", "key": "GrossArea",
204
+ "predefined_type": "ROOF"},
205
+ {"entity": "IfcSlab", "source": "qset",
206
+ "set_name": "BaseQuantities", "key": "NetArea",
207
+ "predefined_type": "ROOF"},
208
+ {"entity": "IfcSlab", "source": "qset",
209
+ "set_name": "BaseQuantities", "key": "GrossArea",
210
+ "predefined_type": "ROOF"},
211
+ ],
212
+ "true_north_angle": [
213
+ {"entity": "IfcGeometricRepresentationContext",
214
+ "source": "attr", "key": "TrueNorth",
215
+ "note": "DirectionRatios (X,Y) β†’ atan2(x,y) β†’ compass bearing"},
216
+ ],
217
+ "latitude": [
218
+ {"entity": "IfcSite", "source": "attr", "key": "RefLatitude",
219
+ "note": "IfcCompoundPlaneAngleMeasure [deg,min,sec,microsec]"},
220
+ ],
221
+ "longitude": [
222
+ {"entity": "IfcSite", "source": "attr", "key": "RefLongitude",
223
+ "note": "IfcCompoundPlaneAngleMeasure [deg,min,sec,microsec]"},
224
+ ],
225
+ }
226
+
227
+ # Auto-extend: scan the inventory for any quantity set / property set
228
+ # entries that look area-related and are not already covered above.
229
+ already_covered: set[tuple] = set()
230
+ for canonical, alias_list in aliases.items():
231
+ for a in alias_list:
232
+ if a["source"] == "qset":
233
+ already_covered.add(
234
+ (a["entity"], "quantity_sets", a["set_name"], a["key"])
235
+ )
236
+
237
+ auto_discovered: list[dict] = []
238
+ for etype, sections in inventory.items():
239
+ for set_name, keys in sections.get("quantity_sets", {}).items():
240
+ for key_name, entry in keys.items():
241
+ if entry["file_count"] == 0:
242
+ continue
243
+ key_lower = key_name.lower()
244
+ is_area = any(kw in key_lower for kw in AREA_KEYWORDS)
245
+ if not is_area:
246
+ continue
247
+ tag = (etype, "quantity_sets", set_name, key_name)
248
+ if tag in already_covered:
249
+ continue
250
+ auto_discovered.append({
251
+ "entity": etype,
252
+ "source": "qset",
253
+ "set_name": set_name,
254
+ "key": key_name,
255
+ "file_count": entry["file_count"],
256
+ "projects": entry["projects"],
257
+ "auto_discovered": True,
258
+ })
259
+
260
+ if auto_discovered:
261
+ aliases["_auto_discovered_area_keys"] = auto_discovered
262
+
263
+ return aliases
264
+
265
+
266
+ # ── Console reporting ─────────────────────────────────────────────────────────
267
+
268
+ def print_report(inventory: dict) -> None:
269
+ """Print a per-element-type summary table to the console."""
270
+ HIGHLIGHT = {"area", "flΓ€che", "netfloorarea", "grossfloorarea",
271
+ "netarea", "grossarea", "grossfootprintarea"}
272
+
273
+ for etype in TARGET_TYPES:
274
+ data = inventory.get(etype, {})
275
+ qsets = data.get("quantity_sets", {})
276
+ psets = data.get("property_sets", {})
277
+
278
+ if not qsets and not psets:
279
+ continue
280
+
281
+ print(f"\n{'='*70}")
282
+ print(f" {etype}")
283
+ print(f"{'='*70}")
284
+
285
+ # Quantity sets
286
+ if qsets:
287
+ print("\n QUANTITY SETS:")
288
+ rows = []
289
+ for set_name, keys in sorted(qsets.items()):
290
+ for key_name, entry in sorted(
291
+ keys.items(),
292
+ key=lambda x: -x[1]["file_count"]
293
+ ):
294
+ flag = "*" if key_name.lower() in HIGHLIGHT else " "
295
+ rows.append([
296
+ flag,
297
+ set_name,
298
+ key_name,
299
+ entry["file_count"],
300
+ ", ".join(entry["projects"][:5]) +
301
+ ("..." if len(entry["projects"]) > 5 else ""),
302
+ ])
303
+ print(tabulate(
304
+ rows,
305
+ headers=["!", "QSet Name", "Quantity", "Files", "Projects"],
306
+ tablefmt="simple",
307
+ ))
308
+
309
+ # Property sets (condensed β€” just set name + count of unique props)
310
+ if psets:
311
+ print("\n PROPERTY SETS (summary):")
312
+ rows = []
313
+ for set_name, keys in sorted(psets.items()):
314
+ max_files = max(e["file_count"] for e in keys.values())
315
+ rows.append([set_name, len(keys), max_files])
316
+ print(tabulate(
317
+ rows,
318
+ headers=["PSet Name", "# Props", "Max Files"],
319
+ tablefmt="simple",
320
+ ))
321
+
322
+
323
+ # ── Main ──────────────────────────────────────────────────────────────────────
324
+
325
+ def main() -> None:
326
+ root = Path(__file__).parent / "Sample projects" / "projects"
327
+ ifc_files = sorted(root.rglob("*.ifc"))
328
+ log.info(f"Found {len(ifc_files)} IFC files")
329
+
330
+ global_inv = make_inventory()
331
+
332
+ for i, ifc_path in enumerate(ifc_files, 1):
333
+ project = ifc_path.parent.name
334
+ log.info(f"[{i}/{len(ifc_files)}] {project}/{ifc_path.name}")
335
+ try:
336
+ model = ifcopenshell.open(str(ifc_path))
337
+ except Exception as exc:
338
+ log.warning(f" Cannot open: {exc}")
339
+ continue
340
+
341
+ for etype in TARGET_TYPES:
342
+ try:
343
+ qdata = collect_qsets(model, etype)
344
+ merge_into(global_inv, etype, "quantity_sets", qdata, project)
345
+ except Exception as exc:
346
+ log.debug(f" qset {etype}: {exc}")
347
+
348
+ try:
349
+ pdata = collect_psets(model, etype)
350
+ merge_into(global_inv, etype, "property_sets", pdata, project)
351
+ except Exception as exc:
352
+ log.debug(f" pset {etype}: {exc}")
353
+
354
+ # Serialise inventory
355
+ plain_inv = inventory_to_plain(global_inv)
356
+ inv_path = Path(__file__).parent / "ifc_key_inventory.json"
357
+ with open(inv_path, "w", encoding="utf-8") as f:
358
+ json.dump(plain_inv, f, indent=2, ensure_ascii=False)
359
+ log.info(f"Inventory written to: {inv_path}")
360
+
361
+ # Build and write alias map
362
+ aliases = build_aliases(plain_inv)
363
+ alias_path = Path(__file__).parent / "key_aliases.json"
364
+ with open(alias_path, "w", encoding="utf-8") as f:
365
+ json.dump(aliases, f, indent=2, ensure_ascii=False)
366
+ log.info(f"Alias map written to: {alias_path}")
367
+
368
+ # Print console report
369
+ print_report(plain_inv)
370
+
371
+ # Final summary
372
+ print(f"\n{'='*70}")
373
+ print("SUMMARY")
374
+ print(f"{'='*70}")
375
+ for etype in TARGET_TYPES:
376
+ qsets = plain_inv.get(etype, {}).get("quantity_sets", {})
377
+ psets = plain_inv.get(etype, {}).get("property_sets", {})
378
+ if qsets or psets:
379
+ total_qkeys = sum(len(v) for v in qsets.values())
380
+ total_pkeys = sum(len(v) for v in psets.values())
381
+ print(f" {etype:<30} "
382
+ f"{len(qsets)} qsets / {total_qkeys} qty-keys | "
383
+ f"{len(psets)} psets / {total_pkeys} prop-keys")
384
+
385
+
386
+ if __name__ == "__main__":
387
+ main()
teams/lux-ai/IFC key checker/ifc_key_inventory.json ADDED
The diff for this file is too large to render. See raw diff
 
teams/lux-ai/IFC key checker/key_aliases.json ADDED
@@ -0,0 +1,3275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "window_area": [
3
+ {
4
+ "entity": "IfcWindow",
5
+ "source": "qset",
6
+ "set_name": "Qto_WindowBaseQuantities",
7
+ "key": "Area"
8
+ },
9
+ {
10
+ "entity": "IfcWindow",
11
+ "source": "qset",
12
+ "set_name": "BaseQuantities",
13
+ "key": "Area"
14
+ },
15
+ {
16
+ "entity": "IfcWindow",
17
+ "source": "qset",
18
+ "set_name": "BaseQuantities",
19
+ "key": "GrossArea"
20
+ },
21
+ {
22
+ "entity": "IfcWindow",
23
+ "source": "attr",
24
+ "keys": [
25
+ "OverallHeight",
26
+ "OverallWidth"
27
+ ],
28
+ "op": "multiply"
29
+ }
30
+ ],
31
+ "floor_area": [
32
+ {
33
+ "entity": "IfcSpace",
34
+ "source": "qset",
35
+ "set_name": "Qto_SpaceBaseQuantities",
36
+ "key": "NetFloorArea"
37
+ },
38
+ {
39
+ "entity": "IfcSpace",
40
+ "source": "qset",
41
+ "set_name": "Qto_SpaceBaseQuantities",
42
+ "key": "GrossFloorArea"
43
+ },
44
+ {
45
+ "entity": "IfcSpace",
46
+ "source": "qset",
47
+ "set_name": "BaseQuantities",
48
+ "key": "NetFloorArea"
49
+ },
50
+ {
51
+ "entity": "IfcSpace",
52
+ "source": "qset",
53
+ "set_name": "BaseQuantities",
54
+ "key": "GrossFloorArea"
55
+ },
56
+ {
57
+ "entity": "IfcSpace",
58
+ "source": "qset",
59
+ "set_name": "GSA Space Areas",
60
+ "key": "GSA BIM Area"
61
+ },
62
+ {
63
+ "entity": "IfcSlab",
64
+ "source": "qset",
65
+ "set_name": "Qto_SlabBaseQuantities",
66
+ "key": "NetArea",
67
+ "predefined_type": "FLOOR"
68
+ },
69
+ {
70
+ "entity": "IfcSlab",
71
+ "source": "qset",
72
+ "set_name": "BaseQuantities",
73
+ "key": "NetArea",
74
+ "predefined_type": "FLOOR"
75
+ },
76
+ {
77
+ "entity": "IfcSlab",
78
+ "source": "qset",
79
+ "set_name": "BaseQuantities",
80
+ "key": "GrossArea",
81
+ "predefined_type": "FLOOR"
82
+ }
83
+ ],
84
+ "roof_area": [
85
+ {
86
+ "entity": "IfcRoof",
87
+ "source": "qset",
88
+ "set_name": "Qto_RoofBaseQuantities",
89
+ "key": "NetArea"
90
+ },
91
+ {
92
+ "entity": "IfcRoof",
93
+ "source": "qset",
94
+ "set_name": "Qto_RoofBaseQuantities",
95
+ "key": "GrossArea"
96
+ },
97
+ {
98
+ "entity": "IfcRoof",
99
+ "source": "qset",
100
+ "set_name": "BaseQuantities",
101
+ "key": "NetArea"
102
+ },
103
+ {
104
+ "entity": "IfcSlab",
105
+ "source": "qset",
106
+ "set_name": "Qto_SlabBaseQuantities",
107
+ "key": "NetArea",
108
+ "predefined_type": "ROOF"
109
+ },
110
+ {
111
+ "entity": "IfcSlab",
112
+ "source": "qset",
113
+ "set_name": "Qto_SlabBaseQuantities",
114
+ "key": "GrossArea",
115
+ "predefined_type": "ROOF"
116
+ },
117
+ {
118
+ "entity": "IfcSlab",
119
+ "source": "qset",
120
+ "set_name": "BaseQuantities",
121
+ "key": "NetArea",
122
+ "predefined_type": "ROOF"
123
+ },
124
+ {
125
+ "entity": "IfcSlab",
126
+ "source": "qset",
127
+ "set_name": "BaseQuantities",
128
+ "key": "GrossArea",
129
+ "predefined_type": "ROOF"
130
+ }
131
+ ],
132
+ "true_north_angle": [
133
+ {
134
+ "entity": "IfcGeometricRepresentationContext",
135
+ "source": "attr",
136
+ "key": "TrueNorth",
137
+ "note": "DirectionRatios (X,Y) β†’ atan2(x,y) β†’ compass bearing"
138
+ }
139
+ ],
140
+ "latitude": [
141
+ {
142
+ "entity": "IfcSite",
143
+ "source": "attr",
144
+ "key": "RefLatitude",
145
+ "note": "IfcCompoundPlaneAngleMeasure [deg,min,sec,microsec]"
146
+ }
147
+ ],
148
+ "longitude": [
149
+ {
150
+ "entity": "IfcSite",
151
+ "source": "attr",
152
+ "key": "RefLongitude",
153
+ "note": "IfcCompoundPlaneAngleMeasure [deg,min,sec,microsec]"
154
+ }
155
+ ],
156
+ "_auto_discovered_area_keys": [
157
+ {
158
+ "entity": "IfcWindow",
159
+ "source": "qset",
160
+ "set_name": "ArchiCADQuantities",
161
+ "key": "OberflΓ€chenbereich",
162
+ "file_count": 2,
163
+ "projects": [
164
+ "ac20",
165
+ "fzk_house"
166
+ ],
167
+ "auto_discovered": true
168
+ },
169
+ {
170
+ "entity": "IfcWindow",
171
+ "source": "qset",
172
+ "set_name": "ArchiCADQuantities",
173
+ "key": "F/T OberflΓ€che der Γ–ffnung auf der Anschlagseite",
174
+ "file_count": 2,
175
+ "projects": [
176
+ "ac20",
177
+ "fzk_house"
178
+ ],
179
+ "auto_discovered": true
180
+ },
181
+ {
182
+ "entity": "IfcWindow",
183
+ "source": "qset",
184
+ "set_name": "ArchiCADQuantities",
185
+ "key": "F/T OberflΓ€che der Γ–ffnung gegenΓΌber der Anschlagseite",
186
+ "file_count": 2,
187
+ "projects": [
188
+ "ac20",
189
+ "fzk_house"
190
+ ],
191
+ "auto_discovered": true
192
+ },
193
+ {
194
+ "entity": "IfcWindow",
195
+ "source": "qset",
196
+ "set_name": "ArchiCADQuantities",
197
+ "key": "Nominale T/F Γ–ffnungsoberflΓ€che auf der Anschlagsseite",
198
+ "file_count": 2,
199
+ "projects": [
200
+ "ac20",
201
+ "fzk_house"
202
+ ],
203
+ "auto_discovered": true
204
+ },
205
+ {
206
+ "entity": "IfcWindow",
207
+ "source": "qset",
208
+ "set_name": "ArchiCADQuantities",
209
+ "key": "Nominale T/F Γ–ffnungsoberflΓ€che auf der Seite gegenΓΌber der Anschlagsseite",
210
+ "file_count": 2,
211
+ "projects": [
212
+ "ac20",
213
+ "fzk_house"
214
+ ],
215
+ "auto_discovered": true
216
+ },
217
+ {
218
+ "entity": "IfcWindow",
219
+ "source": "qset",
220
+ "set_name": "ArchiCADQuantities",
221
+ "key": "T/F Γ–ffnung Nominaler OberflΓ€chenbereich",
222
+ "file_count": 2,
223
+ "projects": [
224
+ "ac20",
225
+ "fzk_house"
226
+ ],
227
+ "auto_discovered": true
228
+ },
229
+ {
230
+ "entity": "IfcWindow",
231
+ "source": "qset",
232
+ "set_name": "ArchiCADQuantities",
233
+ "key": "Surface Area",
234
+ "file_count": 1,
235
+ "projects": [
236
+ "sixty5"
237
+ ],
238
+ "auto_discovered": true
239
+ },
240
+ {
241
+ "entity": "IfcWindow",
242
+ "source": "qset",
243
+ "set_name": "ArchiCADQuantities",
244
+ "key": "W/D Opening Surface Area",
245
+ "file_count": 1,
246
+ "projects": [
247
+ "sixty5"
248
+ ],
249
+ "auto_discovered": true
250
+ },
251
+ {
252
+ "entity": "IfcWindow",
253
+ "source": "qset",
254
+ "set_name": "ArchiCADQuantities",
255
+ "key": "W/D Opening Nominal Surface Area",
256
+ "file_count": 1,
257
+ "projects": [
258
+ "sixty5"
259
+ ],
260
+ "auto_discovered": true
261
+ },
262
+ {
263
+ "entity": "IfcWindow",
264
+ "source": "qset",
265
+ "set_name": "ArchiCADQuantities",
266
+ "key": "Nominal W/D Opening Surface Area on the Reveal Side",
267
+ "file_count": 1,
268
+ "projects": [
269
+ "sixty5"
270
+ ],
271
+ "auto_discovered": true
272
+ },
273
+ {
274
+ "entity": "IfcWindow",
275
+ "source": "qset",
276
+ "set_name": "ArchiCADQuantities",
277
+ "key": "Nominal W/D Opening Surface Area on the Side Opposite to the Reveal Side",
278
+ "file_count": 1,
279
+ "projects": [
280
+ "sixty5"
281
+ ],
282
+ "auto_discovered": true
283
+ },
284
+ {
285
+ "entity": "IfcWindow",
286
+ "source": "qset",
287
+ "set_name": "ArchiCADQuantities",
288
+ "key": "Area",
289
+ "file_count": 1,
290
+ "projects": [
291
+ "sixty5"
292
+ ],
293
+ "auto_discovered": true
294
+ },
295
+ {
296
+ "entity": "IfcDoor",
297
+ "source": "qset",
298
+ "set_name": "BaseQuantities",
299
+ "key": "Area",
300
+ "file_count": 5,
301
+ "projects": [
302
+ "ac20",
303
+ "fzk_house",
304
+ "molio",
305
+ "sixty5",
306
+ "smiley_west"
307
+ ],
308
+ "auto_discovered": true
309
+ },
310
+ {
311
+ "entity": "IfcDoor",
312
+ "source": "qset",
313
+ "set_name": "ArchiCADQuantities",
314
+ "key": "OberflΓ€chenbereich",
315
+ "file_count": 2,
316
+ "projects": [
317
+ "ac20",
318
+ "fzk_house"
319
+ ],
320
+ "auto_discovered": true
321
+ },
322
+ {
323
+ "entity": "IfcDoor",
324
+ "source": "qset",
325
+ "set_name": "ArchiCADQuantities",
326
+ "key": "F/T OberflΓ€che der Γ–ffnung auf der Anschlagseite",
327
+ "file_count": 2,
328
+ "projects": [
329
+ "ac20",
330
+ "fzk_house"
331
+ ],
332
+ "auto_discovered": true
333
+ },
334
+ {
335
+ "entity": "IfcDoor",
336
+ "source": "qset",
337
+ "set_name": "ArchiCADQuantities",
338
+ "key": "F/T OberflΓ€che der Γ–ffnung gegenΓΌber der Anschlagseite",
339
+ "file_count": 2,
340
+ "projects": [
341
+ "ac20",
342
+ "fzk_house"
343
+ ],
344
+ "auto_discovered": true
345
+ },
346
+ {
347
+ "entity": "IfcDoor",
348
+ "source": "qset",
349
+ "set_name": "ArchiCADQuantities",
350
+ "key": "Nominale T/F Γ–ffnungsoberflΓ€che auf der Anschlagsseite",
351
+ "file_count": 2,
352
+ "projects": [
353
+ "ac20",
354
+ "fzk_house"
355
+ ],
356
+ "auto_discovered": true
357
+ },
358
+ {
359
+ "entity": "IfcDoor",
360
+ "source": "qset",
361
+ "set_name": "ArchiCADQuantities",
362
+ "key": "Nominale T/F Γ–ffnungsoberflΓ€che auf der Seite gegenΓΌber der Anschlagsseite",
363
+ "file_count": 2,
364
+ "projects": [
365
+ "ac20",
366
+ "fzk_house"
367
+ ],
368
+ "auto_discovered": true
369
+ },
370
+ {
371
+ "entity": "IfcDoor",
372
+ "source": "qset",
373
+ "set_name": "ArchiCADQuantities",
374
+ "key": "T/F Γ–ffnung Nominaler OberflΓ€chenbereich",
375
+ "file_count": 2,
376
+ "projects": [
377
+ "ac20",
378
+ "fzk_house"
379
+ ],
380
+ "auto_discovered": true
381
+ },
382
+ {
383
+ "entity": "IfcDoor",
384
+ "source": "qset",
385
+ "set_name": "ArchiCADQuantities",
386
+ "key": "Surface Area",
387
+ "file_count": 2,
388
+ "projects": [
389
+ "molio",
390
+ "sixty5"
391
+ ],
392
+ "auto_discovered": true
393
+ },
394
+ {
395
+ "entity": "IfcDoor",
396
+ "source": "qset",
397
+ "set_name": "ArchiCADQuantities",
398
+ "key": "Bottom Surface Area (Net)",
399
+ "file_count": 1,
400
+ "projects": [
401
+ "molio"
402
+ ],
403
+ "auto_discovered": true
404
+ },
405
+ {
406
+ "entity": "IfcDoor",
407
+ "source": "qset",
408
+ "set_name": "ArchiCADQuantities",
409
+ "key": "W/D Opening Surface Area",
410
+ "file_count": 2,
411
+ "projects": [
412
+ "molio",
413
+ "sixty5"
414
+ ],
415
+ "auto_discovered": true
416
+ },
417
+ {
418
+ "entity": "IfcDoor",
419
+ "source": "qset",
420
+ "set_name": "ArchiCADQuantities",
421
+ "key": "W/D Opening Nominal Surface Area",
422
+ "file_count": 2,
423
+ "projects": [
424
+ "molio",
425
+ "sixty5"
426
+ ],
427
+ "auto_discovered": true
428
+ },
429
+ {
430
+ "entity": "IfcDoor",
431
+ "source": "qset",
432
+ "set_name": "ArchiCADQuantities",
433
+ "key": "W/D Opening Surface Area on the Reveal Side",
434
+ "file_count": 1,
435
+ "projects": [
436
+ "molio"
437
+ ],
438
+ "auto_discovered": true
439
+ },
440
+ {
441
+ "entity": "IfcDoor",
442
+ "source": "qset",
443
+ "set_name": "ArchiCADQuantities",
444
+ "key": "W/D Opening Surface Area on the Side Opposite to the Reveal Side",
445
+ "file_count": 1,
446
+ "projects": [
447
+ "molio"
448
+ ],
449
+ "auto_discovered": true
450
+ },
451
+ {
452
+ "entity": "IfcDoor",
453
+ "source": "qset",
454
+ "set_name": "ArchiCADQuantities",
455
+ "key": "Nominal W/D Opening Surface Area on the Reveal Side",
456
+ "file_count": 2,
457
+ "projects": [
458
+ "molio",
459
+ "sixty5"
460
+ ],
461
+ "auto_discovered": true
462
+ },
463
+ {
464
+ "entity": "IfcDoor",
465
+ "source": "qset",
466
+ "set_name": "ArchiCADQuantities",
467
+ "key": "Nominal W/D Opening Surface Area on the Side Opposite to the Reveal Side",
468
+ "file_count": 2,
469
+ "projects": [
470
+ "molio",
471
+ "sixty5"
472
+ ],
473
+ "auto_discovered": true
474
+ },
475
+ {
476
+ "entity": "IfcDoor",
477
+ "source": "qset",
478
+ "set_name": "Qto_DoorBaseQuantities",
479
+ "key": "Area",
480
+ "file_count": 3,
481
+ "projects": [
482
+ "digital_hub",
483
+ "fantasy_office_building_1",
484
+ "fantasy_office_building_2"
485
+ ],
486
+ "auto_discovered": true
487
+ },
488
+ {
489
+ "entity": "IfcSpace",
490
+ "source": "qset",
491
+ "set_name": "GSA Space Areas",
492
+ "key": "GSA Space Areas",
493
+ "file_count": 1,
494
+ "projects": [
495
+ "samuel_macalister_sample_house"
496
+ ],
497
+ "auto_discovered": true
498
+ },
499
+ {
500
+ "entity": "IfcSpace",
501
+ "source": "qset",
502
+ "set_name": "BaseQuantities",
503
+ "key": "SpaceNetFloorAreaBOMA",
504
+ "file_count": 5,
505
+ "projects": [
506
+ "ac20",
507
+ "fzk_house",
508
+ "molio",
509
+ "sixty5",
510
+ "smiley_west"
511
+ ],
512
+ "auto_discovered": true
513
+ },
514
+ {
515
+ "entity": "IfcSpace",
516
+ "source": "qset",
517
+ "set_name": "BaseQuantities",
518
+ "key": "SpaceUsableFloorAreaBOMA",
519
+ "file_count": 5,
520
+ "projects": [
521
+ "ac20",
522
+ "fzk_house",
523
+ "molio",
524
+ "sixty5",
525
+ "smiley_west"
526
+ ],
527
+ "auto_discovered": true
528
+ },
529
+ {
530
+ "entity": "IfcSpace",
531
+ "source": "qset",
532
+ "set_name": "BaseQuantities",
533
+ "key": "GrossCeilingArea",
534
+ "file_count": 5,
535
+ "projects": [
536
+ "ac20",
537
+ "fzk_house",
538
+ "molio",
539
+ "sixty5",
540
+ "smiley_west"
541
+ ],
542
+ "auto_discovered": true
543
+ },
544
+ {
545
+ "entity": "IfcSpace",
546
+ "source": "qset",
547
+ "set_name": "BaseQuantities",
548
+ "key": "NetCeilingArea",
549
+ "file_count": 5,
550
+ "projects": [
551
+ "ac20",
552
+ "fzk_house",
553
+ "molio",
554
+ "sixty5",
555
+ "smiley_west"
556
+ ],
557
+ "auto_discovered": true
558
+ },
559
+ {
560
+ "entity": "IfcSpace",
561
+ "source": "qset",
562
+ "set_name": "BaseQuantities",
563
+ "key": "GrossWallArea",
564
+ "file_count": 5,
565
+ "projects": [
566
+ "ac20",
567
+ "fzk_house",
568
+ "molio",
569
+ "sixty5",
570
+ "smiley_west"
571
+ ],
572
+ "auto_discovered": true
573
+ },
574
+ {
575
+ "entity": "IfcSpace",
576
+ "source": "qset",
577
+ "set_name": "BaseQuantities",
578
+ "key": "NetWallArea",
579
+ "file_count": 5,
580
+ "projects": [
581
+ "ac20",
582
+ "fzk_house",
583
+ "molio",
584
+ "sixty5",
585
+ "smiley_west"
586
+ ],
587
+ "auto_discovered": true
588
+ },
589
+ {
590
+ "entity": "IfcSpace",
591
+ "source": "qset",
592
+ "set_name": "ArchiCADQuantities",
593
+ "key": "OberflΓ€chenbereich",
594
+ "file_count": 2,
595
+ "projects": [
596
+ "ac20",
597
+ "fzk_house"
598
+ ],
599
+ "auto_discovered": true
600
+ },
601
+ {
602
+ "entity": "IfcSpace",
603
+ "source": "qset",
604
+ "set_name": "ArchiCADQuantities",
605
+ "key": "FlΓ€che",
606
+ "file_count": 2,
607
+ "projects": [
608
+ "ac20",
609
+ "fzk_house"
610
+ ],
611
+ "auto_discovered": true
612
+ },
613
+ {
614
+ "entity": "IfcSpace",
615
+ "source": "qset",
616
+ "set_name": "ArchiCADQuantities",
617
+ "key": "Berechnete FlΓ€che (NRF)",
618
+ "file_count": 2,
619
+ "projects": [
620
+ "ac20",
621
+ "fzk_house"
622
+ ],
623
+ "auto_discovered": true
624
+ },
625
+ {
626
+ "entity": "IfcSpace",
627
+ "source": "qset",
628
+ "set_name": "ArchiCADQuantities",
629
+ "key": "TΓΌren-OberflΓ€chenbereich",
630
+ "file_count": 2,
631
+ "projects": [
632
+ "ac20",
633
+ "fzk_house"
634
+ ],
635
+ "auto_discovered": true
636
+ },
637
+ {
638
+ "entity": "IfcSpace",
639
+ "source": "qset",
640
+ "set_name": "ArchiCADQuantities",
641
+ "key": "StΓΌtzenabzugsflΓ€che",
642
+ "file_count": 2,
643
+ "projects": [
644
+ "ac20",
645
+ "fzk_house"
646
+ ],
647
+ "auto_discovered": true
648
+ },
649
+ {
650
+ "entity": "IfcSpace",
651
+ "source": "qset",
652
+ "set_name": "ArchiCADQuantities",
653
+ "key": "SchraffurabzugsflΓ€che",
654
+ "file_count": 2,
655
+ "projects": [
656
+ "ac20",
657
+ "fzk_house"
658
+ ],
659
+ "auto_discovered": true
660
+ },
661
+ {
662
+ "entity": "IfcSpace",
663
+ "source": "qset",
664
+ "set_name": "ArchiCADQuantities",
665
+ "key": "Niedrige AbzugsflΓ€che",
666
+ "file_count": 2,
667
+ "projects": [
668
+ "ac20",
669
+ "fzk_house"
670
+ ],
671
+ "auto_discovered": true
672
+ },
673
+ {
674
+ "entity": "IfcSpace",
675
+ "source": "qset",
676
+ "set_name": "ArchiCADQuantities",
677
+ "key": "WandabzugsflΓ€che",
678
+ "file_count": 2,
679
+ "projects": [
680
+ "ac20",
681
+ "fzk_house"
682
+ ],
683
+ "auto_discovered": true
684
+ },
685
+ {
686
+ "entity": "IfcSpace",
687
+ "source": "qset",
688
+ "set_name": "ArchiCADQuantities",
689
+ "key": "Gemessene FlΓ€che",
690
+ "file_count": 2,
691
+ "projects": [
692
+ "ac20",
693
+ "fzk_house"
694
+ ],
695
+ "auto_discovered": true
696
+ },
697
+ {
698
+ "entity": "IfcSpace",
699
+ "source": "qset",
700
+ "set_name": "ArchiCADQuantities",
701
+ "key": "NettoflΓ€che",
702
+ "file_count": 2,
703
+ "projects": [
704
+ "ac20",
705
+ "fzk_house"
706
+ ],
707
+ "auto_discovered": true
708
+ },
709
+ {
710
+ "entity": "IfcSpace",
711
+ "source": "qset",
712
+ "set_name": "ArchiCADQuantities",
713
+ "key": "Reduzierte FlΓ€che",
714
+ "file_count": 2,
715
+ "projects": [
716
+ "ac20",
717
+ "fzk_house"
718
+ ],
719
+ "auto_discovered": true
720
+ },
721
+ {
722
+ "entity": "IfcSpace",
723
+ "source": "qset",
724
+ "set_name": "ArchiCADQuantities",
725
+ "key": "RaumflΓ€chen Reduzierung",
726
+ "file_count": 2,
727
+ "projects": [
728
+ "ac20",
729
+ "fzk_house"
730
+ ],
731
+ "auto_discovered": true
732
+ },
733
+ {
734
+ "entity": "IfcSpace",
735
+ "source": "qset",
736
+ "set_name": "ArchiCADQuantities",
737
+ "key": "GesamtabzugsflΓ€che",
738
+ "file_count": 2,
739
+ "projects": [
740
+ "ac20",
741
+ "fzk_house"
742
+ ],
743
+ "auto_discovered": true
744
+ },
745
+ {
746
+ "entity": "IfcSpace",
747
+ "source": "qset",
748
+ "set_name": "ArchiCADQuantities",
749
+ "key": "HeizkΓΆrpernische OberflΓ€chenbereich hinten",
750
+ "file_count": 2,
751
+ "projects": [
752
+ "ac20",
753
+ "fzk_house"
754
+ ],
755
+ "auto_discovered": true
756
+ },
757
+ {
758
+ "entity": "IfcSpace",
759
+ "source": "qset",
760
+ "set_name": "ArchiCADQuantities",
761
+ "key": "HeizkΓΆrpernische OberflΓ€chenbereich seitlich",
762
+ "file_count": 2,
763
+ "projects": [
764
+ "ac20",
765
+ "fzk_house"
766
+ ],
767
+ "auto_discovered": true
768
+ },
769
+ {
770
+ "entity": "IfcSpace",
771
+ "source": "qset",
772
+ "set_name": "ArchiCADQuantities",
773
+ "key": "HeizkΓΆrpernische OberflΓ€chenbereich oben",
774
+ "file_count": 2,
775
+ "projects": [
776
+ "ac20",
777
+ "fzk_house"
778
+ ],
779
+ "auto_discovered": true
780
+ },
781
+ {
782
+ "entity": "IfcSpace",
783
+ "source": "qset",
784
+ "set_name": "ArchiCADQuantities",
785
+ "key": "Wand-OberflΓ€chenbereich",
786
+ "file_count": 2,
787
+ "projects": [
788
+ "ac20",
789
+ "fzk_house"
790
+ ],
791
+ "auto_discovered": true
792
+ },
793
+ {
794
+ "entity": "IfcSpace",
795
+ "source": "qset",
796
+ "set_name": "ArchiCADQuantities",
797
+ "key": "Fenster-OberflΓ€chenbereich",
798
+ "file_count": 2,
799
+ "projects": [
800
+ "ac20",
801
+ "fzk_house"
802
+ ],
803
+ "auto_discovered": true
804
+ },
805
+ {
806
+ "entity": "IfcSpace",
807
+ "source": "qset",
808
+ "set_name": "ArchiCADQuantities",
809
+ "key": "Area",
810
+ "file_count": 2,
811
+ "projects": [
812
+ "molio",
813
+ "sixty5"
814
+ ],
815
+ "auto_discovered": true
816
+ },
817
+ {
818
+ "entity": "IfcSpace",
819
+ "source": "qset",
820
+ "set_name": "ArchiCADQuantities",
821
+ "key": "Top Surface Area (Net)",
822
+ "file_count": 1,
823
+ "projects": [
824
+ "molio"
825
+ ],
826
+ "auto_discovered": true
827
+ },
828
+ {
829
+ "entity": "IfcSpace",
830
+ "source": "qset",
831
+ "set_name": "ArchiCADQuantities",
832
+ "key": "Edge Surface Area (Net)",
833
+ "file_count": 1,
834
+ "projects": [
835
+ "molio"
836
+ ],
837
+ "auto_discovered": true
838
+ },
839
+ {
840
+ "entity": "IfcSpace",
841
+ "source": "qset",
842
+ "set_name": "ArchiCADQuantities",
843
+ "key": "Bottom Surface Area (Net)",
844
+ "file_count": 1,
845
+ "projects": [
846
+ "molio"
847
+ ],
848
+ "auto_discovered": true
849
+ },
850
+ {
851
+ "entity": "IfcSpace",
852
+ "source": "qset",
853
+ "set_name": "ArchiCADQuantities",
854
+ "key": "Calculated Area",
855
+ "file_count": 2,
856
+ "projects": [
857
+ "molio",
858
+ "sixty5"
859
+ ],
860
+ "auto_discovered": true
861
+ },
862
+ {
863
+ "entity": "IfcSpace",
864
+ "source": "qset",
865
+ "set_name": "ArchiCADQuantities",
866
+ "key": "Measured Area",
867
+ "file_count": 2,
868
+ "projects": [
869
+ "molio",
870
+ "sixty5"
871
+ ],
872
+ "auto_discovered": true
873
+ },
874
+ {
875
+ "entity": "IfcSpace",
876
+ "source": "qset",
877
+ "set_name": "ArchiCADQuantities",
878
+ "key": "Windows Surface Area",
879
+ "file_count": 2,
880
+ "projects": [
881
+ "molio",
882
+ "sixty5"
883
+ ],
884
+ "auto_discovered": true
885
+ },
886
+ {
887
+ "entity": "IfcSpace",
888
+ "source": "qset",
889
+ "set_name": "ArchiCADQuantities",
890
+ "key": "Doors Surface Area",
891
+ "file_count": 2,
892
+ "projects": [
893
+ "molio",
894
+ "sixty5"
895
+ ],
896
+ "auto_discovered": true
897
+ },
898
+ {
899
+ "entity": "IfcSpace",
900
+ "source": "qset",
901
+ "set_name": "ArchiCADQuantities",
902
+ "key": "Walls Surface Area",
903
+ "file_count": 2,
904
+ "projects": [
905
+ "molio",
906
+ "sixty5"
907
+ ],
908
+ "auto_discovered": true
909
+ },
910
+ {
911
+ "entity": "IfcSpace",
912
+ "source": "qset",
913
+ "set_name": "ArchiCADQuantities",
914
+ "key": "Area Reducement",
915
+ "file_count": 2,
916
+ "projects": [
917
+ "molio",
918
+ "sixty5"
919
+ ],
920
+ "auto_discovered": true
921
+ },
922
+ {
923
+ "entity": "IfcSpace",
924
+ "source": "qset",
925
+ "set_name": "ArchiCADQuantities",
926
+ "key": "Extracted Column Area",
927
+ "file_count": 2,
928
+ "projects": [
929
+ "molio",
930
+ "sixty5"
931
+ ],
932
+ "auto_discovered": true
933
+ },
934
+ {
935
+ "entity": "IfcSpace",
936
+ "source": "qset",
937
+ "set_name": "ArchiCADQuantities",
938
+ "key": "Extracted Fill Area",
939
+ "file_count": 2,
940
+ "projects": [
941
+ "molio",
942
+ "sixty5"
943
+ ],
944
+ "auto_discovered": true
945
+ },
946
+ {
947
+ "entity": "IfcSpace",
948
+ "source": "qset",
949
+ "set_name": "ArchiCADQuantities",
950
+ "key": "Extracted Low Area",
951
+ "file_count": 2,
952
+ "projects": [
953
+ "molio",
954
+ "sixty5"
955
+ ],
956
+ "auto_discovered": true
957
+ },
958
+ {
959
+ "entity": "IfcSpace",
960
+ "source": "qset",
961
+ "set_name": "ArchiCADQuantities",
962
+ "key": "Extracted Wall Area",
963
+ "file_count": 2,
964
+ "projects": [
965
+ "molio",
966
+ "sixty5"
967
+ ],
968
+ "auto_discovered": true
969
+ },
970
+ {
971
+ "entity": "IfcSpace",
972
+ "source": "qset",
973
+ "set_name": "ArchiCADQuantities",
974
+ "key": "Measured Net Area",
975
+ "file_count": 1,
976
+ "projects": [
977
+ "molio"
978
+ ],
979
+ "auto_discovered": true
980
+ },
981
+ {
982
+ "entity": "IfcSpace",
983
+ "source": "qset",
984
+ "set_name": "ArchiCADQuantities",
985
+ "key": "Reduced Area",
986
+ "file_count": 1,
987
+ "projects": [
988
+ "molio"
989
+ ],
990
+ "auto_discovered": true
991
+ },
992
+ {
993
+ "entity": "IfcSpace",
994
+ "source": "qset",
995
+ "set_name": "ArchiCADQuantities",
996
+ "key": "Total Extracted Area",
997
+ "file_count": 2,
998
+ "projects": [
999
+ "molio",
1000
+ "sixty5"
1001
+ ],
1002
+ "auto_discovered": true
1003
+ },
1004
+ {
1005
+ "entity": "IfcSpace",
1006
+ "source": "qset",
1007
+ "set_name": "ArchiCADQuantities",
1008
+ "key": "Wall Inset Back Side Surface Area",
1009
+ "file_count": 2,
1010
+ "projects": [
1011
+ "molio",
1012
+ "sixty5"
1013
+ ],
1014
+ "auto_discovered": true
1015
+ },
1016
+ {
1017
+ "entity": "IfcSpace",
1018
+ "source": "qset",
1019
+ "set_name": "ArchiCADQuantities",
1020
+ "key": "Wall Inset Side Surface Area",
1021
+ "file_count": 2,
1022
+ "projects": [
1023
+ "molio",
1024
+ "sixty5"
1025
+ ],
1026
+ "auto_discovered": true
1027
+ },
1028
+ {
1029
+ "entity": "IfcSpace",
1030
+ "source": "qset",
1031
+ "set_name": "ArchiCADQuantities",
1032
+ "key": "Wall Inset Top Surface Area",
1033
+ "file_count": 2,
1034
+ "projects": [
1035
+ "molio",
1036
+ "sixty5"
1037
+ ],
1038
+ "auto_discovered": true
1039
+ },
1040
+ {
1041
+ "entity": "IfcSpace",
1042
+ "source": "qset",
1043
+ "set_name": "ArchiCADQuantities",
1044
+ "key": "Extracted Curtain Wall Area",
1045
+ "file_count": 1,
1046
+ "projects": [
1047
+ "molio"
1048
+ ],
1049
+ "auto_discovered": true
1050
+ },
1051
+ {
1052
+ "entity": "IfcSpace",
1053
+ "source": "qset",
1054
+ "set_name": "ArchiCADQuantities",
1055
+ "key": "Net Area",
1056
+ "file_count": 1,
1057
+ "projects": [
1058
+ "sixty5"
1059
+ ],
1060
+ "auto_discovered": true
1061
+ },
1062
+ {
1063
+ "entity": "IfcSpace",
1064
+ "source": "qset",
1065
+ "set_name": "Qto_SpaceBaseQuantities",
1066
+ "key": "GrossCeilingArea",
1067
+ "file_count": 2,
1068
+ "projects": [
1069
+ "fantasy_office_building_1",
1070
+ "fantasy_office_building_2"
1071
+ ],
1072
+ "auto_discovered": true
1073
+ },
1074
+ {
1075
+ "entity": "IfcSpace",
1076
+ "source": "qset",
1077
+ "set_name": "(unnamed)",
1078
+ "key": "NetFootprintArea",
1079
+ "file_count": 1,
1080
+ "projects": [
1081
+ "hitos"
1082
+ ],
1083
+ "auto_discovered": true
1084
+ },
1085
+ {
1086
+ "entity": "IfcSlab",
1087
+ "source": "qset",
1088
+ "set_name": "ArchiCADQuantities",
1089
+ "key": "OberflΓ€chenbereich",
1090
+ "file_count": 2,
1091
+ "projects": [
1092
+ "ac20",
1093
+ "fzk_house"
1094
+ ],
1095
+ "auto_discovered": true
1096
+ },
1097
+ {
1098
+ "entity": "IfcSlab",
1099
+ "source": "qset",
1100
+ "set_name": "ArchiCADQuantities",
1101
+ "key": "FlΓ€che",
1102
+ "file_count": 2,
1103
+ "projects": [
1104
+ "ac20",
1105
+ "fzk_house"
1106
+ ],
1107
+ "auto_discovered": true
1108
+ },
1109
+ {
1110
+ "entity": "IfcSlab",
1111
+ "source": "qset",
1112
+ "set_name": "ArchiCADQuantities",
1113
+ "key": "OberflΓ€chenbereich Unterseite",
1114
+ "file_count": 2,
1115
+ "projects": [
1116
+ "ac20",
1117
+ "fzk_house"
1118
+ ],
1119
+ "auto_discovered": true
1120
+ },
1121
+ {
1122
+ "entity": "IfcSlab",
1123
+ "source": "qset",
1124
+ "set_name": "ArchiCADQuantities",
1125
+ "key": "OberflΓ€chenbereich Oberseite",
1126
+ "file_count": 2,
1127
+ "projects": [
1128
+ "ac20",
1129
+ "fzk_house"
1130
+ ],
1131
+ "auto_discovered": true
1132
+ },
1133
+ {
1134
+ "entity": "IfcSlab",
1135
+ "source": "qset",
1136
+ "set_name": "ArchiCADQuantities",
1137
+ "key": "Kante OberflΓ€chenbereich",
1138
+ "file_count": 2,
1139
+ "projects": [
1140
+ "ac20",
1141
+ "fzk_house"
1142
+ ],
1143
+ "auto_discovered": true
1144
+ },
1145
+ {
1146
+ "entity": "IfcSlab",
1147
+ "source": "qset",
1148
+ "set_name": "ArchiCADQuantities",
1149
+ "key": "LΓΆcher OberflΓ€chenbereich",
1150
+ "file_count": 2,
1151
+ "projects": [
1152
+ "ac20",
1153
+ "fzk_house"
1154
+ ],
1155
+ "auto_discovered": true
1156
+ },
1157
+ {
1158
+ "entity": "IfcSlab",
1159
+ "source": "qset",
1160
+ "set_name": "ArchiCADQuantities",
1161
+ "key": "Brutto-OberflΓ€che der Deckenoberseite",
1162
+ "file_count": 2,
1163
+ "projects": [
1164
+ "ac20",
1165
+ "fzk_house"
1166
+ ],
1167
+ "auto_discovered": true
1168
+ },
1169
+ {
1170
+ "entity": "IfcSlab",
1171
+ "source": "qset",
1172
+ "set_name": "ArchiCADQuantities",
1173
+ "key": "Brutto-OberflΓ€chenbereich der Deckenunterseite",
1174
+ "file_count": 2,
1175
+ "projects": [
1176
+ "ac20",
1177
+ "fzk_house"
1178
+ ],
1179
+ "auto_discovered": true
1180
+ },
1181
+ {
1182
+ "entity": "IfcSlab",
1183
+ "source": "qset",
1184
+ "set_name": "ArchiCADQuantities",
1185
+ "key": "Brutto-OberflΓ€chenbereich der Deckenkanten",
1186
+ "file_count": 2,
1187
+ "projects": [
1188
+ "ac20",
1189
+ "fzk_house"
1190
+ ],
1191
+ "auto_discovered": true
1192
+ },
1193
+ {
1194
+ "entity": "IfcSlab",
1195
+ "source": "qset",
1196
+ "set_name": "ArchiCADQuantities",
1197
+ "key": "Konditionaler OberflΓ€chenbereich der Unterseite",
1198
+ "file_count": 2,
1199
+ "projects": [
1200
+ "ac20",
1201
+ "fzk_house"
1202
+ ],
1203
+ "auto_discovered": true
1204
+ },
1205
+ {
1206
+ "entity": "IfcSlab",
1207
+ "source": "qset",
1208
+ "set_name": "ArchiCADQuantities",
1209
+ "key": "Konditionaler OberflΓ€chenbereich der Oberseite",
1210
+ "file_count": 2,
1211
+ "projects": [
1212
+ "ac20",
1213
+ "fzk_house"
1214
+ ],
1215
+ "auto_discovered": true
1216
+ },
1217
+ {
1218
+ "entity": "IfcSlab",
1219
+ "source": "qset",
1220
+ "set_name": "ArchiCADQuantities",
1221
+ "key": "Brutto-OberflΓ€chenbereich der Deckenoberseite mit LΓΆchern",
1222
+ "file_count": 2,
1223
+ "projects": [
1224
+ "ac20",
1225
+ "fzk_house"
1226
+ ],
1227
+ "auto_discovered": true
1228
+ },
1229
+ {
1230
+ "entity": "IfcSlab",
1231
+ "source": "qset",
1232
+ "set_name": "ArchiCADQuantities",
1233
+ "key": "Brutto-OberflΓ€chenbereich der Deckenunterseite mit LΓΆchern",
1234
+ "file_count": 2,
1235
+ "projects": [
1236
+ "ac20",
1237
+ "fzk_house"
1238
+ ],
1239
+ "auto_discovered": true
1240
+ },
1241
+ {
1242
+ "entity": "IfcSlab",
1243
+ "source": "qset",
1244
+ "set_name": "ArchiCADQuantities",
1245
+ "key": "Brutto-OberflΓ€chenbereich der Deckenkanten mit LΓΆchern",
1246
+ "file_count": 2,
1247
+ "projects": [
1248
+ "ac20",
1249
+ "fzk_house"
1250
+ ],
1251
+ "auto_discovered": true
1252
+ },
1253
+ {
1254
+ "entity": "IfcSlab",
1255
+ "source": "qset",
1256
+ "set_name": "ArchiCADQuantities",
1257
+ "key": "Netto-OberflΓ€chenbereich Unterseite",
1258
+ "file_count": 2,
1259
+ "projects": [
1260
+ "ac20",
1261
+ "fzk_house"
1262
+ ],
1263
+ "auto_discovered": true
1264
+ },
1265
+ {
1266
+ "entity": "IfcSlab",
1267
+ "source": "qset",
1268
+ "set_name": "ArchiCADQuantities",
1269
+ "key": "Netto-OberflΓ€chenbereich Oberseite",
1270
+ "file_count": 2,
1271
+ "projects": [
1272
+ "ac20",
1273
+ "fzk_house"
1274
+ ],
1275
+ "auto_discovered": true
1276
+ },
1277
+ {
1278
+ "entity": "IfcSlab",
1279
+ "source": "qset",
1280
+ "set_name": "ArchiCADQuantities",
1281
+ "key": "Netto-OberflΓ€chenbereich der Kante",
1282
+ "file_count": 2,
1283
+ "projects": [
1284
+ "ac20",
1285
+ "fzk_house"
1286
+ ],
1287
+ "auto_discovered": true
1288
+ },
1289
+ {
1290
+ "entity": "IfcSlab",
1291
+ "source": "qset",
1292
+ "set_name": "ArchiCADQuantities",
1293
+ "key": "Brutto-OberflΓ€chenbereich an der Unterseite",
1294
+ "file_count": 2,
1295
+ "projects": [
1296
+ "ac20",
1297
+ "fzk_house"
1298
+ ],
1299
+ "auto_discovered": true
1300
+ },
1301
+ {
1302
+ "entity": "IfcSlab",
1303
+ "source": "qset",
1304
+ "set_name": "ArchiCADQuantities",
1305
+ "key": "Brutto-OberflΓ€chenbereich an der Oberseite",
1306
+ "file_count": 2,
1307
+ "projects": [
1308
+ "ac20",
1309
+ "fzk_house"
1310
+ ],
1311
+ "auto_discovered": true
1312
+ },
1313
+ {
1314
+ "entity": "IfcSlab",
1315
+ "source": "qset",
1316
+ "set_name": "ArchiCADQuantities",
1317
+ "key": "Brutto-OberflΓ€chenbereich der Dachkanten",
1318
+ "file_count": 2,
1319
+ "projects": [
1320
+ "ac20",
1321
+ "fzk_house"
1322
+ ],
1323
+ "auto_discovered": true
1324
+ },
1325
+ {
1326
+ "entity": "IfcSlab",
1327
+ "source": "qset",
1328
+ "set_name": "ArchiCADQuantities",
1329
+ "key": "Konditionaler OberflΓ€chenbereich unten",
1330
+ "file_count": 2,
1331
+ "projects": [
1332
+ "ac20",
1333
+ "fzk_house"
1334
+ ],
1335
+ "auto_discovered": true
1336
+ },
1337
+ {
1338
+ "entity": "IfcSlab",
1339
+ "source": "qset",
1340
+ "set_name": "ArchiCADQuantities",
1341
+ "key": "Konditionaler OberflΓ€chenbereich oben",
1342
+ "file_count": 2,
1343
+ "projects": [
1344
+ "ac20",
1345
+ "fzk_house"
1346
+ ],
1347
+ "auto_discovered": true
1348
+ },
1349
+ {
1350
+ "entity": "IfcSlab",
1351
+ "source": "qset",
1352
+ "set_name": "ArchiCADQuantities",
1353
+ "key": "OberflΓ€chenbereich der Γ–ffnungen",
1354
+ "file_count": 2,
1355
+ "projects": [
1356
+ "ac20",
1357
+ "fzk_house"
1358
+ ],
1359
+ "auto_discovered": true
1360
+ },
1361
+ {
1362
+ "entity": "IfcSlab",
1363
+ "source": "qset",
1364
+ "set_name": "ArchiCADQuantities",
1365
+ "key": "Area",
1366
+ "file_count": 2,
1367
+ "projects": [
1368
+ "molio",
1369
+ "sixty5"
1370
+ ],
1371
+ "auto_discovered": true
1372
+ },
1373
+ {
1374
+ "entity": "IfcSlab",
1375
+ "source": "qset",
1376
+ "set_name": "ArchiCADQuantities",
1377
+ "key": "Surface Area",
1378
+ "file_count": 2,
1379
+ "projects": [
1380
+ "molio",
1381
+ "sixty5"
1382
+ ],
1383
+ "auto_discovered": true
1384
+ },
1385
+ {
1386
+ "entity": "IfcSlab",
1387
+ "source": "qset",
1388
+ "set_name": "ArchiCADQuantities",
1389
+ "key": "Top Surface Area (Net)",
1390
+ "file_count": 1,
1391
+ "projects": [
1392
+ "molio"
1393
+ ],
1394
+ "auto_discovered": true
1395
+ },
1396
+ {
1397
+ "entity": "IfcSlab",
1398
+ "source": "qset",
1399
+ "set_name": "ArchiCADQuantities",
1400
+ "key": "Top Surface Area (Conditional)",
1401
+ "file_count": 1,
1402
+ "projects": [
1403
+ "molio"
1404
+ ],
1405
+ "auto_discovered": true
1406
+ },
1407
+ {
1408
+ "entity": "IfcSlab",
1409
+ "source": "qset",
1410
+ "set_name": "ArchiCADQuantities",
1411
+ "key": "Edge Surface Area (Net)",
1412
+ "file_count": 1,
1413
+ "projects": [
1414
+ "molio"
1415
+ ],
1416
+ "auto_discovered": true
1417
+ },
1418
+ {
1419
+ "entity": "IfcSlab",
1420
+ "source": "qset",
1421
+ "set_name": "ArchiCADQuantities",
1422
+ "key": "Bottom Surface Area (Net)",
1423
+ "file_count": 1,
1424
+ "projects": [
1425
+ "molio"
1426
+ ],
1427
+ "auto_discovered": true
1428
+ },
1429
+ {
1430
+ "entity": "IfcSlab",
1431
+ "source": "qset",
1432
+ "set_name": "ArchiCADQuantities",
1433
+ "key": "Bottom Surface Area (Conditional)",
1434
+ "file_count": 1,
1435
+ "projects": [
1436
+ "molio"
1437
+ ],
1438
+ "auto_discovered": true
1439
+ },
1440
+ {
1441
+ "entity": "IfcSlab",
1442
+ "source": "qset",
1443
+ "set_name": "ArchiCADQuantities",
1444
+ "key": "Bottom Surface Area (Gross)",
1445
+ "file_count": 1,
1446
+ "projects": [
1447
+ "molio"
1448
+ ],
1449
+ "auto_discovered": true
1450
+ },
1451
+ {
1452
+ "entity": "IfcSlab",
1453
+ "source": "qset",
1454
+ "set_name": "ArchiCADQuantities",
1455
+ "key": "Edge Surface Area (Gross)",
1456
+ "file_count": 1,
1457
+ "projects": [
1458
+ "molio"
1459
+ ],
1460
+ "auto_discovered": true
1461
+ },
1462
+ {
1463
+ "entity": "IfcSlab",
1464
+ "source": "qset",
1465
+ "set_name": "ArchiCADQuantities",
1466
+ "key": "Top Surface Area (Gross)",
1467
+ "file_count": 1,
1468
+ "projects": [
1469
+ "molio"
1470
+ ],
1471
+ "auto_discovered": true
1472
+ },
1473
+ {
1474
+ "entity": "IfcSlab",
1475
+ "source": "qset",
1476
+ "set_name": "ArchiCADQuantities",
1477
+ "key": "Surface Area of the Slab Top (Conditional)",
1478
+ "file_count": 1,
1479
+ "projects": [
1480
+ "molio"
1481
+ ],
1482
+ "auto_discovered": true
1483
+ },
1484
+ {
1485
+ "entity": "IfcSlab",
1486
+ "source": "qset",
1487
+ "set_name": "ArchiCADQuantities",
1488
+ "key": "Surface Area of the Slab Bottom (Gross)",
1489
+ "file_count": 1,
1490
+ "projects": [
1491
+ "molio"
1492
+ ],
1493
+ "auto_discovered": true
1494
+ },
1495
+ {
1496
+ "entity": "IfcSlab",
1497
+ "source": "qset",
1498
+ "set_name": "ArchiCADQuantities",
1499
+ "key": "Surface Area of the Slab Bottom (Gross, with holes)",
1500
+ "file_count": 1,
1501
+ "projects": [
1502
+ "molio"
1503
+ ],
1504
+ "auto_discovered": true
1505
+ },
1506
+ {
1507
+ "entity": "IfcSlab",
1508
+ "source": "qset",
1509
+ "set_name": "ArchiCADQuantities",
1510
+ "key": "Surface Area of the Slab Edges (Gross)",
1511
+ "file_count": 1,
1512
+ "projects": [
1513
+ "molio"
1514
+ ],
1515
+ "auto_discovered": true
1516
+ },
1517
+ {
1518
+ "entity": "IfcSlab",
1519
+ "source": "qset",
1520
+ "set_name": "ArchiCADQuantities",
1521
+ "key": "Surface Area of the Slab Edges (Gross, with holes)",
1522
+ "file_count": 1,
1523
+ "projects": [
1524
+ "molio"
1525
+ ],
1526
+ "auto_discovered": true
1527
+ },
1528
+ {
1529
+ "entity": "IfcSlab",
1530
+ "source": "qset",
1531
+ "set_name": "ArchiCADQuantities",
1532
+ "key": "Surface Area of the Slab Top (Gross)",
1533
+ "file_count": 1,
1534
+ "projects": [
1535
+ "molio"
1536
+ ],
1537
+ "auto_discovered": true
1538
+ },
1539
+ {
1540
+ "entity": "IfcSlab",
1541
+ "source": "qset",
1542
+ "set_name": "ArchiCADQuantities",
1543
+ "key": "Surface Area of the Slab Top (Gross, with holes)",
1544
+ "file_count": 1,
1545
+ "projects": [
1546
+ "molio"
1547
+ ],
1548
+ "auto_discovered": true
1549
+ },
1550
+ {
1551
+ "entity": "IfcSlab",
1552
+ "source": "qset",
1553
+ "set_name": "ArchiCADQuantities",
1554
+ "key": "Holes Surface Area",
1555
+ "file_count": 2,
1556
+ "projects": [
1557
+ "molio",
1558
+ "sixty5"
1559
+ ],
1560
+ "auto_discovered": true
1561
+ },
1562
+ {
1563
+ "entity": "IfcSlab",
1564
+ "source": "qset",
1565
+ "set_name": "ArchiCADQuantities",
1566
+ "key": "Bottom Surface Area",
1567
+ "file_count": 1,
1568
+ "projects": [
1569
+ "sixty5"
1570
+ ],
1571
+ "auto_discovered": true
1572
+ },
1573
+ {
1574
+ "entity": "IfcSlab",
1575
+ "source": "qset",
1576
+ "set_name": "ArchiCADQuantities",
1577
+ "key": "Conditional Surface Area of the Bottom",
1578
+ "file_count": 1,
1579
+ "projects": [
1580
+ "sixty5"
1581
+ ],
1582
+ "auto_discovered": true
1583
+ },
1584
+ {
1585
+ "entity": "IfcSlab",
1586
+ "source": "qset",
1587
+ "set_name": "ArchiCADQuantities",
1588
+ "key": "Conditional Surface Area of the Top",
1589
+ "file_count": 1,
1590
+ "projects": [
1591
+ "sixty5"
1592
+ ],
1593
+ "auto_discovered": true
1594
+ },
1595
+ {
1596
+ "entity": "IfcSlab",
1597
+ "source": "qset",
1598
+ "set_name": "ArchiCADQuantities",
1599
+ "key": "Edge Surface Area",
1600
+ "file_count": 1,
1601
+ "projects": [
1602
+ "sixty5"
1603
+ ],
1604
+ "auto_discovered": true
1605
+ },
1606
+ {
1607
+ "entity": "IfcSlab",
1608
+ "source": "qset",
1609
+ "set_name": "ArchiCADQuantities",
1610
+ "key": "Gross Surface Area of the Slab Bottom",
1611
+ "file_count": 1,
1612
+ "projects": [
1613
+ "sixty5"
1614
+ ],
1615
+ "auto_discovered": true
1616
+ },
1617
+ {
1618
+ "entity": "IfcSlab",
1619
+ "source": "qset",
1620
+ "set_name": "ArchiCADQuantities",
1621
+ "key": "Gross Surface Area of the Slab Bottom with Holes",
1622
+ "file_count": 1,
1623
+ "projects": [
1624
+ "sixty5"
1625
+ ],
1626
+ "auto_discovered": true
1627
+ },
1628
+ {
1629
+ "entity": "IfcSlab",
1630
+ "source": "qset",
1631
+ "set_name": "ArchiCADQuantities",
1632
+ "key": "Gross Surface Area of the Slab Edges",
1633
+ "file_count": 1,
1634
+ "projects": [
1635
+ "sixty5"
1636
+ ],
1637
+ "auto_discovered": true
1638
+ },
1639
+ {
1640
+ "entity": "IfcSlab",
1641
+ "source": "qset",
1642
+ "set_name": "ArchiCADQuantities",
1643
+ "key": "Gross Surface Area of the Slab Edges with Holes",
1644
+ "file_count": 1,
1645
+ "projects": [
1646
+ "sixty5"
1647
+ ],
1648
+ "auto_discovered": true
1649
+ },
1650
+ {
1651
+ "entity": "IfcSlab",
1652
+ "source": "qset",
1653
+ "set_name": "ArchiCADQuantities",
1654
+ "key": "Gross Surface Area of the Slab Top",
1655
+ "file_count": 1,
1656
+ "projects": [
1657
+ "sixty5"
1658
+ ],
1659
+ "auto_discovered": true
1660
+ },
1661
+ {
1662
+ "entity": "IfcSlab",
1663
+ "source": "qset",
1664
+ "set_name": "ArchiCADQuantities",
1665
+ "key": "Gross Surface Area of the Slab Top with Holes",
1666
+ "file_count": 1,
1667
+ "projects": [
1668
+ "sixty5"
1669
+ ],
1670
+ "auto_discovered": true
1671
+ },
1672
+ {
1673
+ "entity": "IfcSlab",
1674
+ "source": "qset",
1675
+ "set_name": "ArchiCADQuantities",
1676
+ "key": "Top Surface Area",
1677
+ "file_count": 1,
1678
+ "projects": [
1679
+ "sixty5"
1680
+ ],
1681
+ "auto_discovered": true
1682
+ },
1683
+ {
1684
+ "entity": "IfcSlab",
1685
+ "source": "qset",
1686
+ "set_name": "(unnamed)",
1687
+ "key": "NetFootprintArea",
1688
+ "file_count": 1,
1689
+ "projects": [
1690
+ "hitos"
1691
+ ],
1692
+ "auto_discovered": true
1693
+ },
1694
+ {
1695
+ "entity": "IfcRoof",
1696
+ "source": "qset",
1697
+ "set_name": "Qto_RoofBaseQuantities",
1698
+ "key": "ProjectedArea",
1699
+ "file_count": 2,
1700
+ "projects": [
1701
+ "fantasy_office_building_1",
1702
+ "fantasy_office_building_2"
1703
+ ],
1704
+ "auto_discovered": true
1705
+ },
1706
+ {
1707
+ "entity": "IfcWall",
1708
+ "source": "qset",
1709
+ "set_name": "BaseQuantities",
1710
+ "key": "GrossFootprintArea",
1711
+ "file_count": 7,
1712
+ "projects": [
1713
+ "ac20",
1714
+ "fzk_house",
1715
+ "molio",
1716
+ "sixty5",
1717
+ "smiley_west"
1718
+ ],
1719
+ "auto_discovered": true
1720
+ },
1721
+ {
1722
+ "entity": "IfcWall",
1723
+ "source": "qset",
1724
+ "set_name": "BaseQuantities",
1725
+ "key": "NetFootprintArea",
1726
+ "file_count": 6,
1727
+ "projects": [
1728
+ "ac20",
1729
+ "fzk_house",
1730
+ "molio",
1731
+ "sixty5",
1732
+ "smiley_west"
1733
+ ],
1734
+ "auto_discovered": true
1735
+ },
1736
+ {
1737
+ "entity": "IfcWall",
1738
+ "source": "qset",
1739
+ "set_name": "BaseQuantities",
1740
+ "key": "GrossSideArea",
1741
+ "file_count": 7,
1742
+ "projects": [
1743
+ "ac20",
1744
+ "fzk_house",
1745
+ "molio",
1746
+ "sixty5",
1747
+ "smiley_west"
1748
+ ],
1749
+ "auto_discovered": true
1750
+ },
1751
+ {
1752
+ "entity": "IfcWall",
1753
+ "source": "qset",
1754
+ "set_name": "BaseQuantities",
1755
+ "key": "NetSideArea",
1756
+ "file_count": 7,
1757
+ "projects": [
1758
+ "ac20",
1759
+ "fzk_house",
1760
+ "molio",
1761
+ "sixty5",
1762
+ "smiley_west"
1763
+ ],
1764
+ "auto_discovered": true
1765
+ },
1766
+ {
1767
+ "entity": "IfcWall",
1768
+ "source": "qset",
1769
+ "set_name": "ArchiCADQuantities",
1770
+ "key": "OberflΓ€chenbereich",
1771
+ "file_count": 2,
1772
+ "projects": [
1773
+ "ac20",
1774
+ "fzk_house"
1775
+ ],
1776
+ "auto_discovered": true
1777
+ },
1778
+ {
1779
+ "entity": "IfcWall",
1780
+ "source": "qset",
1781
+ "set_name": "ArchiCADQuantities",
1782
+ "key": "FlΓ€che",
1783
+ "file_count": 2,
1784
+ "projects": [
1785
+ "ac20",
1786
+ "fzk_house"
1787
+ ],
1788
+ "auto_discovered": true
1789
+ },
1790
+ {
1791
+ "entity": "IfcWall",
1792
+ "source": "qset",
1793
+ "set_name": "ArchiCADQuantities",
1794
+ "key": "Netto-OberflÀchenbereich an der Außenseite",
1795
+ "file_count": 2,
1796
+ "projects": [
1797
+ "ac20",
1798
+ "fzk_house"
1799
+ ],
1800
+ "auto_discovered": true
1801
+ },
1802
+ {
1803
+ "entity": "IfcWall",
1804
+ "source": "qset",
1805
+ "set_name": "ArchiCADQuantities",
1806
+ "key": "Netto-OberflΓ€chenbereich an der Innenseite",
1807
+ "file_count": 2,
1808
+ "projects": [
1809
+ "ac20",
1810
+ "fzk_house"
1811
+ ],
1812
+ "auto_discovered": true
1813
+ },
1814
+ {
1815
+ "entity": "IfcWall",
1816
+ "source": "qset",
1817
+ "set_name": "ArchiCADQuantities",
1818
+ "key": "Netto-OberflΓ€chenbereich an den Kanten",
1819
+ "file_count": 2,
1820
+ "projects": [
1821
+ "ac20",
1822
+ "fzk_house"
1823
+ ],
1824
+ "auto_discovered": true
1825
+ },
1826
+ {
1827
+ "entity": "IfcWall",
1828
+ "source": "qset",
1829
+ "set_name": "ArchiCADQuantities",
1830
+ "key": "FlΓ€che der Wand",
1831
+ "file_count": 2,
1832
+ "projects": [
1833
+ "ac20",
1834
+ "fzk_house"
1835
+ ],
1836
+ "auto_discovered": true
1837
+ },
1838
+ {
1839
+ "entity": "IfcWall",
1840
+ "source": "qset",
1841
+ "set_name": "ArchiCADQuantities",
1842
+ "key": "Brutto-WandoberflΓ€chenbereich an der Innenseite",
1843
+ "file_count": 2,
1844
+ "projects": [
1845
+ "ac20",
1846
+ "fzk_house"
1847
+ ],
1848
+ "auto_discovered": true
1849
+ },
1850
+ {
1851
+ "entity": "IfcWall",
1852
+ "source": "qset",
1853
+ "set_name": "ArchiCADQuantities",
1854
+ "key": "Brutto-WandoberflÀchenbereich an der Außenseite",
1855
+ "file_count": 2,
1856
+ "projects": [
1857
+ "ac20",
1858
+ "fzk_house"
1859
+ ],
1860
+ "auto_discovered": true
1861
+ },
1862
+ {
1863
+ "entity": "IfcWall",
1864
+ "source": "qset",
1865
+ "set_name": "ArchiCADQuantities",
1866
+ "key": "Analytische OberflΓ€che der Γ–ffnungen an der Innenseite",
1867
+ "file_count": 2,
1868
+ "projects": [
1869
+ "ac20",
1870
+ "fzk_house"
1871
+ ],
1872
+ "auto_discovered": true
1873
+ },
1874
+ {
1875
+ "entity": "IfcWall",
1876
+ "source": "qset",
1877
+ "set_name": "ArchiCADQuantities",
1878
+ "key": "Analytische OberflΓ€che der Γ–ffnungen an der Außenseite",
1879
+ "file_count": 2,
1880
+ "projects": [
1881
+ "ac20",
1882
+ "fzk_house"
1883
+ ],
1884
+ "auto_discovered": true
1885
+ },
1886
+ {
1887
+ "entity": "IfcWall",
1888
+ "source": "qset",
1889
+ "set_name": "ArchiCADQuantities",
1890
+ "key": "Konditionaler OberflÀchenbereich an der Außenseite",
1891
+ "file_count": 2,
1892
+ "projects": [
1893
+ "ac20",
1894
+ "fzk_house"
1895
+ ],
1896
+ "auto_discovered": true
1897
+ },
1898
+ {
1899
+ "entity": "IfcWall",
1900
+ "source": "qset",
1901
+ "set_name": "ArchiCADQuantities",
1902
+ "key": "Konditionaler OberflΓ€chenbereich an der Innenseite",
1903
+ "file_count": 2,
1904
+ "projects": [
1905
+ "ac20",
1906
+ "fzk_house"
1907
+ ],
1908
+ "auto_discovered": true
1909
+ },
1910
+ {
1911
+ "entity": "IfcWall",
1912
+ "source": "qset",
1913
+ "set_name": "ArchiCADQuantities",
1914
+ "key": "OberflΓ€chenbereich der Fenster in der Wand",
1915
+ "file_count": 2,
1916
+ "projects": [
1917
+ "ac20",
1918
+ "fzk_house"
1919
+ ],
1920
+ "auto_discovered": true
1921
+ },
1922
+ {
1923
+ "entity": "IfcWall",
1924
+ "source": "qset",
1925
+ "set_name": "ArchiCADQuantities",
1926
+ "key": "OberflΓ€chenbereich der TΓΌren in der Wand",
1927
+ "file_count": 2,
1928
+ "projects": [
1929
+ "ac20",
1930
+ "fzk_house"
1931
+ ],
1932
+ "auto_discovered": true
1933
+ },
1934
+ {
1935
+ "entity": "IfcWall",
1936
+ "source": "qset",
1937
+ "set_name": "ArchiCADQuantities",
1938
+ "key": "OberflΓ€chenbereich der leeren Γ–ffnungen in der Wand",
1939
+ "file_count": 2,
1940
+ "projects": [
1941
+ "ac20",
1942
+ "fzk_house"
1943
+ ],
1944
+ "auto_discovered": true
1945
+ },
1946
+ {
1947
+ "entity": "IfcWall",
1948
+ "source": "qset",
1949
+ "set_name": "ArchiCADQuantities",
1950
+ "key": "Area",
1951
+ "file_count": 3,
1952
+ "projects": [
1953
+ "molio",
1954
+ "sixty5"
1955
+ ],
1956
+ "auto_discovered": true
1957
+ },
1958
+ {
1959
+ "entity": "IfcWall",
1960
+ "source": "qset",
1961
+ "set_name": "ArchiCADQuantities",
1962
+ "key": "Surface Area",
1963
+ "file_count": 3,
1964
+ "projects": [
1965
+ "molio",
1966
+ "sixty5"
1967
+ ],
1968
+ "auto_discovered": true
1969
+ },
1970
+ {
1971
+ "entity": "IfcWall",
1972
+ "source": "qset",
1973
+ "set_name": "ArchiCADQuantities",
1974
+ "key": "Top Surface Area (Net)",
1975
+ "file_count": 1,
1976
+ "projects": [
1977
+ "molio"
1978
+ ],
1979
+ "auto_discovered": true
1980
+ },
1981
+ {
1982
+ "entity": "IfcWall",
1983
+ "source": "qset",
1984
+ "set_name": "ArchiCADQuantities",
1985
+ "key": "Edge Surface Area (Net)",
1986
+ "file_count": 1,
1987
+ "projects": [
1988
+ "molio"
1989
+ ],
1990
+ "auto_discovered": true
1991
+ },
1992
+ {
1993
+ "entity": "IfcWall",
1994
+ "source": "qset",
1995
+ "set_name": "ArchiCADQuantities",
1996
+ "key": "Bottom Surface Area (Net)",
1997
+ "file_count": 1,
1998
+ "projects": [
1999
+ "molio"
2000
+ ],
2001
+ "auto_discovered": true
2002
+ },
2003
+ {
2004
+ "entity": "IfcWall",
2005
+ "source": "qset",
2006
+ "set_name": "ArchiCADQuantities",
2007
+ "key": "Area of the Doors",
2008
+ "file_count": 3,
2009
+ "projects": [
2010
+ "molio",
2011
+ "sixty5"
2012
+ ],
2013
+ "auto_discovered": true
2014
+ },
2015
+ {
2016
+ "entity": "IfcWall",
2017
+ "source": "qset",
2018
+ "set_name": "ArchiCADQuantities",
2019
+ "key": "Area of the Windows",
2020
+ "file_count": 3,
2021
+ "projects": [
2022
+ "molio",
2023
+ "sixty5"
2024
+ ],
2025
+ "auto_discovered": true
2026
+ },
2027
+ {
2028
+ "entity": "IfcWall",
2029
+ "source": "qset",
2030
+ "set_name": "ArchiCADQuantities",
2031
+ "key": "Analytic Surface Area of Openings on the Inside Face",
2032
+ "file_count": 3,
2033
+ "projects": [
2034
+ "molio",
2035
+ "sixty5"
2036
+ ],
2037
+ "auto_discovered": true
2038
+ },
2039
+ {
2040
+ "entity": "IfcWall",
2041
+ "source": "qset",
2042
+ "set_name": "ArchiCADQuantities",
2043
+ "key": "Analytic Surface Area of Openings on the Outside Face",
2044
+ "file_count": 3,
2045
+ "projects": [
2046
+ "molio",
2047
+ "sixty5"
2048
+ ],
2049
+ "auto_discovered": true
2050
+ },
2051
+ {
2052
+ "entity": "IfcWall",
2053
+ "source": "qset",
2054
+ "set_name": "ArchiCADQuantities",
2055
+ "key": "Surface Area of the Wall Inside Face (Net)",
2056
+ "file_count": 1,
2057
+ "projects": [
2058
+ "molio"
2059
+ ],
2060
+ "auto_discovered": true
2061
+ },
2062
+ {
2063
+ "entity": "IfcWall",
2064
+ "source": "qset",
2065
+ "set_name": "ArchiCADQuantities",
2066
+ "key": "Surface Area of the Wall Outside Face (Net)",
2067
+ "file_count": 1,
2068
+ "projects": [
2069
+ "molio"
2070
+ ],
2071
+ "auto_discovered": true
2072
+ },
2073
+ {
2074
+ "entity": "IfcWall",
2075
+ "source": "qset",
2076
+ "set_name": "ArchiCADQuantities",
2077
+ "key": "Surface Area of the Wall Inside Face (Conditional)",
2078
+ "file_count": 1,
2079
+ "projects": [
2080
+ "molio"
2081
+ ],
2082
+ "auto_discovered": true
2083
+ },
2084
+ {
2085
+ "entity": "IfcWall",
2086
+ "source": "qset",
2087
+ "set_name": "ArchiCADQuantities",
2088
+ "key": "Surface Area of the Wall Outside Face (Conditional)",
2089
+ "file_count": 1,
2090
+ "projects": [
2091
+ "molio"
2092
+ ],
2093
+ "auto_discovered": true
2094
+ },
2095
+ {
2096
+ "entity": "IfcWall",
2097
+ "source": "qset",
2098
+ "set_name": "ArchiCADQuantities",
2099
+ "key": "Surface Area of the Wall Inside Face (Gross)",
2100
+ "file_count": 1,
2101
+ "projects": [
2102
+ "molio"
2103
+ ],
2104
+ "auto_discovered": true
2105
+ },
2106
+ {
2107
+ "entity": "IfcWall",
2108
+ "source": "qset",
2109
+ "set_name": "ArchiCADQuantities",
2110
+ "key": "Surface Area of the Wall Outside Face (Gross)",
2111
+ "file_count": 1,
2112
+ "projects": [
2113
+ "molio"
2114
+ ],
2115
+ "auto_discovered": true
2116
+ },
2117
+ {
2118
+ "entity": "IfcWall",
2119
+ "source": "qset",
2120
+ "set_name": "ArchiCADQuantities",
2121
+ "key": "Surface Area of Empty Openings in the Wall",
2122
+ "file_count": 3,
2123
+ "projects": [
2124
+ "molio",
2125
+ "sixty5"
2126
+ ],
2127
+ "auto_discovered": true
2128
+ },
2129
+ {
2130
+ "entity": "IfcWall",
2131
+ "source": "qset",
2132
+ "set_name": "ArchiCADQuantities",
2133
+ "key": "Net Surface Area of the Edges",
2134
+ "file_count": 2,
2135
+ "projects": [
2136
+ "sixty5"
2137
+ ],
2138
+ "auto_discovered": true
2139
+ },
2140
+ {
2141
+ "entity": "IfcWall",
2142
+ "source": "qset",
2143
+ "set_name": "ArchiCADQuantities",
2144
+ "key": "Net Surface Area on the Inside Face",
2145
+ "file_count": 2,
2146
+ "projects": [
2147
+ "sixty5"
2148
+ ],
2149
+ "auto_discovered": true
2150
+ },
2151
+ {
2152
+ "entity": "IfcWall",
2153
+ "source": "qset",
2154
+ "set_name": "ArchiCADQuantities",
2155
+ "key": "Net Surface Area on the Outside Face",
2156
+ "file_count": 2,
2157
+ "projects": [
2158
+ "sixty5"
2159
+ ],
2160
+ "auto_discovered": true
2161
+ },
2162
+ {
2163
+ "entity": "IfcWall",
2164
+ "source": "qset",
2165
+ "set_name": "ArchiCADQuantities",
2166
+ "key": "Conditional Surface Area on the Inside Face",
2167
+ "file_count": 2,
2168
+ "projects": [
2169
+ "sixty5"
2170
+ ],
2171
+ "auto_discovered": true
2172
+ },
2173
+ {
2174
+ "entity": "IfcWall",
2175
+ "source": "qset",
2176
+ "set_name": "ArchiCADQuantities",
2177
+ "key": "Conditional Surface Area on the Outside Face",
2178
+ "file_count": 2,
2179
+ "projects": [
2180
+ "sixty5"
2181
+ ],
2182
+ "auto_discovered": true
2183
+ },
2184
+ {
2185
+ "entity": "IfcWall",
2186
+ "source": "qset",
2187
+ "set_name": "ArchiCADQuantities",
2188
+ "key": "Gross Surface Area of the Wall on the Inside Face",
2189
+ "file_count": 2,
2190
+ "projects": [
2191
+ "sixty5"
2192
+ ],
2193
+ "auto_discovered": true
2194
+ },
2195
+ {
2196
+ "entity": "IfcWall",
2197
+ "source": "qset",
2198
+ "set_name": "ArchiCADQuantities",
2199
+ "key": "Gross Surface Area of the Wall on the Outside Face",
2200
+ "file_count": 2,
2201
+ "projects": [
2202
+ "sixty5"
2203
+ ],
2204
+ "auto_discovered": true
2205
+ },
2206
+ {
2207
+ "entity": "IfcWall",
2208
+ "source": "qset",
2209
+ "set_name": "ArchiCADQuantities",
2210
+ "key": "Area of the Wall",
2211
+ "file_count": 2,
2212
+ "projects": [
2213
+ "sixty5"
2214
+ ],
2215
+ "auto_discovered": true
2216
+ },
2217
+ {
2218
+ "entity": "IfcWall",
2219
+ "source": "qset",
2220
+ "set_name": "Qto_WallBaseQuantities",
2221
+ "key": "GrossFootprintArea",
2222
+ "file_count": 3,
2223
+ "projects": [
2224
+ "digital_hub",
2225
+ "fantasy_office_building_1",
2226
+ "fantasy_office_building_2"
2227
+ ],
2228
+ "auto_discovered": true
2229
+ },
2230
+ {
2231
+ "entity": "IfcWall",
2232
+ "source": "qset",
2233
+ "set_name": "Qto_WallBaseQuantities",
2234
+ "key": "GrossSideArea",
2235
+ "file_count": 3,
2236
+ "projects": [
2237
+ "digital_hub",
2238
+ "fantasy_office_building_1",
2239
+ "fantasy_office_building_2"
2240
+ ],
2241
+ "auto_discovered": true
2242
+ },
2243
+ {
2244
+ "entity": "IfcWall",
2245
+ "source": "qset",
2246
+ "set_name": "Qto_WallBaseQuantities",
2247
+ "key": "NetSideArea",
2248
+ "file_count": 3,
2249
+ "projects": [
2250
+ "digital_hub",
2251
+ "fantasy_office_building_1",
2252
+ "fantasy_office_building_2"
2253
+ ],
2254
+ "auto_discovered": true
2255
+ },
2256
+ {
2257
+ "entity": "IfcWall",
2258
+ "source": "qset",
2259
+ "set_name": "(unnamed)",
2260
+ "key": "NetFootprintArea",
2261
+ "file_count": 1,
2262
+ "projects": [
2263
+ "hitos"
2264
+ ],
2265
+ "auto_discovered": true
2266
+ },
2267
+ {
2268
+ "entity": "IfcWall",
2269
+ "source": "qset",
2270
+ "set_name": "(unnamed)",
2271
+ "key": "NetSideAreaLeft",
2272
+ "file_count": 1,
2273
+ "projects": [
2274
+ "hitos"
2275
+ ],
2276
+ "auto_discovered": true
2277
+ },
2278
+ {
2279
+ "entity": "IfcWall",
2280
+ "source": "qset",
2281
+ "set_name": "(unnamed)",
2282
+ "key": "NetSideAreaRight",
2283
+ "file_count": 1,
2284
+ "projects": [
2285
+ "hitos"
2286
+ ],
2287
+ "auto_discovered": true
2288
+ },
2289
+ {
2290
+ "entity": "IfcWallStandardCase",
2291
+ "source": "qset",
2292
+ "set_name": "BaseQuantities",
2293
+ "key": "GrossFootprintArea",
2294
+ "file_count": 5,
2295
+ "projects": [
2296
+ "ac20",
2297
+ "fzk_house",
2298
+ "sixty5",
2299
+ "smiley_west"
2300
+ ],
2301
+ "auto_discovered": true
2302
+ },
2303
+ {
2304
+ "entity": "IfcWallStandardCase",
2305
+ "source": "qset",
2306
+ "set_name": "BaseQuantities",
2307
+ "key": "NetFootprintArea",
2308
+ "file_count": 4,
2309
+ "projects": [
2310
+ "ac20",
2311
+ "fzk_house",
2312
+ "sixty5",
2313
+ "smiley_west"
2314
+ ],
2315
+ "auto_discovered": true
2316
+ },
2317
+ {
2318
+ "entity": "IfcWallStandardCase",
2319
+ "source": "qset",
2320
+ "set_name": "BaseQuantities",
2321
+ "key": "GrossSideArea",
2322
+ "file_count": 5,
2323
+ "projects": [
2324
+ "ac20",
2325
+ "fzk_house",
2326
+ "sixty5",
2327
+ "smiley_west"
2328
+ ],
2329
+ "auto_discovered": true
2330
+ },
2331
+ {
2332
+ "entity": "IfcWallStandardCase",
2333
+ "source": "qset",
2334
+ "set_name": "BaseQuantities",
2335
+ "key": "NetSideArea",
2336
+ "file_count": 5,
2337
+ "projects": [
2338
+ "ac20",
2339
+ "fzk_house",
2340
+ "sixty5",
2341
+ "smiley_west"
2342
+ ],
2343
+ "auto_discovered": true
2344
+ },
2345
+ {
2346
+ "entity": "IfcWallStandardCase",
2347
+ "source": "qset",
2348
+ "set_name": "ArchiCADQuantities",
2349
+ "key": "OberflΓ€chenbereich",
2350
+ "file_count": 2,
2351
+ "projects": [
2352
+ "ac20",
2353
+ "fzk_house"
2354
+ ],
2355
+ "auto_discovered": true
2356
+ },
2357
+ {
2358
+ "entity": "IfcWallStandardCase",
2359
+ "source": "qset",
2360
+ "set_name": "ArchiCADQuantities",
2361
+ "key": "FlΓ€che",
2362
+ "file_count": 2,
2363
+ "projects": [
2364
+ "ac20",
2365
+ "fzk_house"
2366
+ ],
2367
+ "auto_discovered": true
2368
+ },
2369
+ {
2370
+ "entity": "IfcWallStandardCase",
2371
+ "source": "qset",
2372
+ "set_name": "ArchiCADQuantities",
2373
+ "key": "Netto-OberflÀchenbereich an der Außenseite",
2374
+ "file_count": 2,
2375
+ "projects": [
2376
+ "ac20",
2377
+ "fzk_house"
2378
+ ],
2379
+ "auto_discovered": true
2380
+ },
2381
+ {
2382
+ "entity": "IfcWallStandardCase",
2383
+ "source": "qset",
2384
+ "set_name": "ArchiCADQuantities",
2385
+ "key": "Netto-OberflΓ€chenbereich an der Innenseite",
2386
+ "file_count": 2,
2387
+ "projects": [
2388
+ "ac20",
2389
+ "fzk_house"
2390
+ ],
2391
+ "auto_discovered": true
2392
+ },
2393
+ {
2394
+ "entity": "IfcWallStandardCase",
2395
+ "source": "qset",
2396
+ "set_name": "ArchiCADQuantities",
2397
+ "key": "Netto-OberflΓ€chenbereich an den Kanten",
2398
+ "file_count": 2,
2399
+ "projects": [
2400
+ "ac20",
2401
+ "fzk_house"
2402
+ ],
2403
+ "auto_discovered": true
2404
+ },
2405
+ {
2406
+ "entity": "IfcWallStandardCase",
2407
+ "source": "qset",
2408
+ "set_name": "ArchiCADQuantities",
2409
+ "key": "FlΓ€che der Wand",
2410
+ "file_count": 2,
2411
+ "projects": [
2412
+ "ac20",
2413
+ "fzk_house"
2414
+ ],
2415
+ "auto_discovered": true
2416
+ },
2417
+ {
2418
+ "entity": "IfcWallStandardCase",
2419
+ "source": "qset",
2420
+ "set_name": "ArchiCADQuantities",
2421
+ "key": "Brutto-WandoberflΓ€chenbereich an der Innenseite",
2422
+ "file_count": 2,
2423
+ "projects": [
2424
+ "ac20",
2425
+ "fzk_house"
2426
+ ],
2427
+ "auto_discovered": true
2428
+ },
2429
+ {
2430
+ "entity": "IfcWallStandardCase",
2431
+ "source": "qset",
2432
+ "set_name": "ArchiCADQuantities",
2433
+ "key": "Brutto-WandoberflÀchenbereich an der Außenseite",
2434
+ "file_count": 2,
2435
+ "projects": [
2436
+ "ac20",
2437
+ "fzk_house"
2438
+ ],
2439
+ "auto_discovered": true
2440
+ },
2441
+ {
2442
+ "entity": "IfcWallStandardCase",
2443
+ "source": "qset",
2444
+ "set_name": "ArchiCADQuantities",
2445
+ "key": "Analytische OberflΓ€che der Γ–ffnungen an der Innenseite",
2446
+ "file_count": 2,
2447
+ "projects": [
2448
+ "ac20",
2449
+ "fzk_house"
2450
+ ],
2451
+ "auto_discovered": true
2452
+ },
2453
+ {
2454
+ "entity": "IfcWallStandardCase",
2455
+ "source": "qset",
2456
+ "set_name": "ArchiCADQuantities",
2457
+ "key": "Analytische OberflΓ€che der Γ–ffnungen an der Außenseite",
2458
+ "file_count": 2,
2459
+ "projects": [
2460
+ "ac20",
2461
+ "fzk_house"
2462
+ ],
2463
+ "auto_discovered": true
2464
+ },
2465
+ {
2466
+ "entity": "IfcWallStandardCase",
2467
+ "source": "qset",
2468
+ "set_name": "ArchiCADQuantities",
2469
+ "key": "Konditionaler OberflÀchenbereich an der Außenseite",
2470
+ "file_count": 2,
2471
+ "projects": [
2472
+ "ac20",
2473
+ "fzk_house"
2474
+ ],
2475
+ "auto_discovered": true
2476
+ },
2477
+ {
2478
+ "entity": "IfcWallStandardCase",
2479
+ "source": "qset",
2480
+ "set_name": "ArchiCADQuantities",
2481
+ "key": "Konditionaler OberflΓ€chenbereich an der Innenseite",
2482
+ "file_count": 2,
2483
+ "projects": [
2484
+ "ac20",
2485
+ "fzk_house"
2486
+ ],
2487
+ "auto_discovered": true
2488
+ },
2489
+ {
2490
+ "entity": "IfcWallStandardCase",
2491
+ "source": "qset",
2492
+ "set_name": "ArchiCADQuantities",
2493
+ "key": "OberflΓ€chenbereich der Fenster in der Wand",
2494
+ "file_count": 2,
2495
+ "projects": [
2496
+ "ac20",
2497
+ "fzk_house"
2498
+ ],
2499
+ "auto_discovered": true
2500
+ },
2501
+ {
2502
+ "entity": "IfcWallStandardCase",
2503
+ "source": "qset",
2504
+ "set_name": "ArchiCADQuantities",
2505
+ "key": "OberflΓ€chenbereich der TΓΌren in der Wand",
2506
+ "file_count": 2,
2507
+ "projects": [
2508
+ "ac20",
2509
+ "fzk_house"
2510
+ ],
2511
+ "auto_discovered": true
2512
+ },
2513
+ {
2514
+ "entity": "IfcWallStandardCase",
2515
+ "source": "qset",
2516
+ "set_name": "ArchiCADQuantities",
2517
+ "key": "OberflΓ€chenbereich der leeren Γ–ffnungen in der Wand",
2518
+ "file_count": 2,
2519
+ "projects": [
2520
+ "ac20",
2521
+ "fzk_house"
2522
+ ],
2523
+ "auto_discovered": true
2524
+ },
2525
+ {
2526
+ "entity": "IfcWallStandardCase",
2527
+ "source": "qset",
2528
+ "set_name": "ArchiCADQuantities",
2529
+ "key": "Area",
2530
+ "file_count": 1,
2531
+ "projects": [
2532
+ "sixty5"
2533
+ ],
2534
+ "auto_discovered": true
2535
+ },
2536
+ {
2537
+ "entity": "IfcWallStandardCase",
2538
+ "source": "qset",
2539
+ "set_name": "ArchiCADQuantities",
2540
+ "key": "Surface Area",
2541
+ "file_count": 1,
2542
+ "projects": [
2543
+ "sixty5"
2544
+ ],
2545
+ "auto_discovered": true
2546
+ },
2547
+ {
2548
+ "entity": "IfcWallStandardCase",
2549
+ "source": "qset",
2550
+ "set_name": "ArchiCADQuantities",
2551
+ "key": "Area of the Doors",
2552
+ "file_count": 1,
2553
+ "projects": [
2554
+ "sixty5"
2555
+ ],
2556
+ "auto_discovered": true
2557
+ },
2558
+ {
2559
+ "entity": "IfcWallStandardCase",
2560
+ "source": "qset",
2561
+ "set_name": "ArchiCADQuantities",
2562
+ "key": "Area of the Windows",
2563
+ "file_count": 1,
2564
+ "projects": [
2565
+ "sixty5"
2566
+ ],
2567
+ "auto_discovered": true
2568
+ },
2569
+ {
2570
+ "entity": "IfcWallStandardCase",
2571
+ "source": "qset",
2572
+ "set_name": "ArchiCADQuantities",
2573
+ "key": "Analytic Surface Area of Openings on the Inside Face",
2574
+ "file_count": 1,
2575
+ "projects": [
2576
+ "sixty5"
2577
+ ],
2578
+ "auto_discovered": true
2579
+ },
2580
+ {
2581
+ "entity": "IfcWallStandardCase",
2582
+ "source": "qset",
2583
+ "set_name": "ArchiCADQuantities",
2584
+ "key": "Analytic Surface Area of Openings on the Outside Face",
2585
+ "file_count": 1,
2586
+ "projects": [
2587
+ "sixty5"
2588
+ ],
2589
+ "auto_discovered": true
2590
+ },
2591
+ {
2592
+ "entity": "IfcWallStandardCase",
2593
+ "source": "qset",
2594
+ "set_name": "ArchiCADQuantities",
2595
+ "key": "Net Surface Area of the Edges",
2596
+ "file_count": 1,
2597
+ "projects": [
2598
+ "sixty5"
2599
+ ],
2600
+ "auto_discovered": true
2601
+ },
2602
+ {
2603
+ "entity": "IfcWallStandardCase",
2604
+ "source": "qset",
2605
+ "set_name": "ArchiCADQuantities",
2606
+ "key": "Net Surface Area on the Inside Face",
2607
+ "file_count": 1,
2608
+ "projects": [
2609
+ "sixty5"
2610
+ ],
2611
+ "auto_discovered": true
2612
+ },
2613
+ {
2614
+ "entity": "IfcWallStandardCase",
2615
+ "source": "qset",
2616
+ "set_name": "ArchiCADQuantities",
2617
+ "key": "Net Surface Area on the Outside Face",
2618
+ "file_count": 1,
2619
+ "projects": [
2620
+ "sixty5"
2621
+ ],
2622
+ "auto_discovered": true
2623
+ },
2624
+ {
2625
+ "entity": "IfcWallStandardCase",
2626
+ "source": "qset",
2627
+ "set_name": "ArchiCADQuantities",
2628
+ "key": "Conditional Surface Area on the Inside Face",
2629
+ "file_count": 1,
2630
+ "projects": [
2631
+ "sixty5"
2632
+ ],
2633
+ "auto_discovered": true
2634
+ },
2635
+ {
2636
+ "entity": "IfcWallStandardCase",
2637
+ "source": "qset",
2638
+ "set_name": "ArchiCADQuantities",
2639
+ "key": "Conditional Surface Area on the Outside Face",
2640
+ "file_count": 1,
2641
+ "projects": [
2642
+ "sixty5"
2643
+ ],
2644
+ "auto_discovered": true
2645
+ },
2646
+ {
2647
+ "entity": "IfcWallStandardCase",
2648
+ "source": "qset",
2649
+ "set_name": "ArchiCADQuantities",
2650
+ "key": "Gross Surface Area of the Wall on the Inside Face",
2651
+ "file_count": 1,
2652
+ "projects": [
2653
+ "sixty5"
2654
+ ],
2655
+ "auto_discovered": true
2656
+ },
2657
+ {
2658
+ "entity": "IfcWallStandardCase",
2659
+ "source": "qset",
2660
+ "set_name": "ArchiCADQuantities",
2661
+ "key": "Gross Surface Area of the Wall on the Outside Face",
2662
+ "file_count": 1,
2663
+ "projects": [
2664
+ "sixty5"
2665
+ ],
2666
+ "auto_discovered": true
2667
+ },
2668
+ {
2669
+ "entity": "IfcWallStandardCase",
2670
+ "source": "qset",
2671
+ "set_name": "ArchiCADQuantities",
2672
+ "key": "Area of the Wall",
2673
+ "file_count": 1,
2674
+ "projects": [
2675
+ "sixty5"
2676
+ ],
2677
+ "auto_discovered": true
2678
+ },
2679
+ {
2680
+ "entity": "IfcWallStandardCase",
2681
+ "source": "qset",
2682
+ "set_name": "ArchiCADQuantities",
2683
+ "key": "Surface Area of Empty Openings in the Wall",
2684
+ "file_count": 1,
2685
+ "projects": [
2686
+ "sixty5"
2687
+ ],
2688
+ "auto_discovered": true
2689
+ },
2690
+ {
2691
+ "entity": "IfcWallStandardCase",
2692
+ "source": "qset",
2693
+ "set_name": "(unnamed)",
2694
+ "key": "NetFootprintArea",
2695
+ "file_count": 1,
2696
+ "projects": [
2697
+ "hitos"
2698
+ ],
2699
+ "auto_discovered": true
2700
+ },
2701
+ {
2702
+ "entity": "IfcWallStandardCase",
2703
+ "source": "qset",
2704
+ "set_name": "(unnamed)",
2705
+ "key": "NetSideAreaLeft",
2706
+ "file_count": 1,
2707
+ "projects": [
2708
+ "hitos"
2709
+ ],
2710
+ "auto_discovered": true
2711
+ },
2712
+ {
2713
+ "entity": "IfcWallStandardCase",
2714
+ "source": "qset",
2715
+ "set_name": "(unnamed)",
2716
+ "key": "NetSideAreaRight",
2717
+ "file_count": 1,
2718
+ "projects": [
2719
+ "hitos"
2720
+ ],
2721
+ "auto_discovered": true
2722
+ },
2723
+ {
2724
+ "entity": "IfcCovering",
2725
+ "source": "qset",
2726
+ "set_name": "Qto_CoveringBaseQuantities",
2727
+ "key": "GrossArea",
2728
+ "file_count": 1,
2729
+ "projects": [
2730
+ "digital_hub"
2731
+ ],
2732
+ "auto_discovered": true
2733
+ },
2734
+ {
2735
+ "entity": "IfcCovering",
2736
+ "source": "qset",
2737
+ "set_name": "Qto_CoveringBaseQuantities",
2738
+ "key": "NetArea",
2739
+ "file_count": 1,
2740
+ "projects": [
2741
+ "digital_hub"
2742
+ ],
2743
+ "auto_discovered": true
2744
+ },
2745
+ {
2746
+ "entity": "IfcCovering",
2747
+ "source": "qset",
2748
+ "set_name": "BaseQuantities",
2749
+ "key": "GrossArea",
2750
+ "file_count": 1,
2751
+ "projects": [
2752
+ "sixty5"
2753
+ ],
2754
+ "auto_discovered": true
2755
+ },
2756
+ {
2757
+ "entity": "IfcCovering",
2758
+ "source": "qset",
2759
+ "set_name": "BaseQuantities",
2760
+ "key": "NetArea",
2761
+ "file_count": 1,
2762
+ "projects": [
2763
+ "sixty5"
2764
+ ],
2765
+ "auto_discovered": true
2766
+ },
2767
+ {
2768
+ "entity": "IfcCovering",
2769
+ "source": "qset",
2770
+ "set_name": "ArchiCADQuantities",
2771
+ "key": "Area",
2772
+ "file_count": 1,
2773
+ "projects": [
2774
+ "sixty5"
2775
+ ],
2776
+ "auto_discovered": true
2777
+ },
2778
+ {
2779
+ "entity": "IfcCovering",
2780
+ "source": "qset",
2781
+ "set_name": "ArchiCADQuantities",
2782
+ "key": "Surface Area",
2783
+ "file_count": 1,
2784
+ "projects": [
2785
+ "sixty5"
2786
+ ],
2787
+ "auto_discovered": true
2788
+ },
2789
+ {
2790
+ "entity": "IfcCovering",
2791
+ "source": "qset",
2792
+ "set_name": "ArchiCADQuantities",
2793
+ "key": "Area of the Doors",
2794
+ "file_count": 1,
2795
+ "projects": [
2796
+ "sixty5"
2797
+ ],
2798
+ "auto_discovered": true
2799
+ },
2800
+ {
2801
+ "entity": "IfcCovering",
2802
+ "source": "qset",
2803
+ "set_name": "ArchiCADQuantities",
2804
+ "key": "Area of the Windows",
2805
+ "file_count": 1,
2806
+ "projects": [
2807
+ "sixty5"
2808
+ ],
2809
+ "auto_discovered": true
2810
+ },
2811
+ {
2812
+ "entity": "IfcCovering",
2813
+ "source": "qset",
2814
+ "set_name": "ArchiCADQuantities",
2815
+ "key": "Analytic Surface Area of Openings on the Inside Face",
2816
+ "file_count": 1,
2817
+ "projects": [
2818
+ "sixty5"
2819
+ ],
2820
+ "auto_discovered": true
2821
+ },
2822
+ {
2823
+ "entity": "IfcCovering",
2824
+ "source": "qset",
2825
+ "set_name": "ArchiCADQuantities",
2826
+ "key": "Analytic Surface Area of Openings on the Outside Face",
2827
+ "file_count": 1,
2828
+ "projects": [
2829
+ "sixty5"
2830
+ ],
2831
+ "auto_discovered": true
2832
+ },
2833
+ {
2834
+ "entity": "IfcCovering",
2835
+ "source": "qset",
2836
+ "set_name": "ArchiCADQuantities",
2837
+ "key": "Net Surface Area of the Edges",
2838
+ "file_count": 1,
2839
+ "projects": [
2840
+ "sixty5"
2841
+ ],
2842
+ "auto_discovered": true
2843
+ },
2844
+ {
2845
+ "entity": "IfcCovering",
2846
+ "source": "qset",
2847
+ "set_name": "ArchiCADQuantities",
2848
+ "key": "Net Surface Area on the Inside Face",
2849
+ "file_count": 1,
2850
+ "projects": [
2851
+ "sixty5"
2852
+ ],
2853
+ "auto_discovered": true
2854
+ },
2855
+ {
2856
+ "entity": "IfcCovering",
2857
+ "source": "qset",
2858
+ "set_name": "ArchiCADQuantities",
2859
+ "key": "Net Surface Area on the Outside Face",
2860
+ "file_count": 1,
2861
+ "projects": [
2862
+ "sixty5"
2863
+ ],
2864
+ "auto_discovered": true
2865
+ },
2866
+ {
2867
+ "entity": "IfcCovering",
2868
+ "source": "qset",
2869
+ "set_name": "ArchiCADQuantities",
2870
+ "key": "Conditional Surface Area on the Inside Face",
2871
+ "file_count": 1,
2872
+ "projects": [
2873
+ "sixty5"
2874
+ ],
2875
+ "auto_discovered": true
2876
+ },
2877
+ {
2878
+ "entity": "IfcCovering",
2879
+ "source": "qset",
2880
+ "set_name": "ArchiCADQuantities",
2881
+ "key": "Conditional Surface Area on the Outside Face",
2882
+ "file_count": 1,
2883
+ "projects": [
2884
+ "sixty5"
2885
+ ],
2886
+ "auto_discovered": true
2887
+ },
2888
+ {
2889
+ "entity": "IfcCovering",
2890
+ "source": "qset",
2891
+ "set_name": "ArchiCADQuantities",
2892
+ "key": "Gross Surface Area of the Wall on the Inside Face",
2893
+ "file_count": 1,
2894
+ "projects": [
2895
+ "sixty5"
2896
+ ],
2897
+ "auto_discovered": true
2898
+ },
2899
+ {
2900
+ "entity": "IfcCovering",
2901
+ "source": "qset",
2902
+ "set_name": "ArchiCADQuantities",
2903
+ "key": "Gross Surface Area of the Wall on the Outside Face",
2904
+ "file_count": 1,
2905
+ "projects": [
2906
+ "sixty5"
2907
+ ],
2908
+ "auto_discovered": true
2909
+ },
2910
+ {
2911
+ "entity": "IfcCovering",
2912
+ "source": "qset",
2913
+ "set_name": "ArchiCADQuantities",
2914
+ "key": "Area of the Wall",
2915
+ "file_count": 1,
2916
+ "projects": [
2917
+ "sixty5"
2918
+ ],
2919
+ "auto_discovered": true
2920
+ },
2921
+ {
2922
+ "entity": "IfcCovering",
2923
+ "source": "qset",
2924
+ "set_name": "ArchiCADQuantities",
2925
+ "key": "Surface Area of Empty Openings in the Wall",
2926
+ "file_count": 1,
2927
+ "projects": [
2928
+ "sixty5"
2929
+ ],
2930
+ "auto_discovered": true
2931
+ },
2932
+ {
2933
+ "entity": "IfcCovering",
2934
+ "source": "qset",
2935
+ "set_name": "ArchiCADQuantities",
2936
+ "key": "Bottom Surface Area",
2937
+ "file_count": 1,
2938
+ "projects": [
2939
+ "sixty5"
2940
+ ],
2941
+ "auto_discovered": true
2942
+ },
2943
+ {
2944
+ "entity": "IfcCovering",
2945
+ "source": "qset",
2946
+ "set_name": "ArchiCADQuantities",
2947
+ "key": "Conditional Surface Area of the Bottom",
2948
+ "file_count": 1,
2949
+ "projects": [
2950
+ "sixty5"
2951
+ ],
2952
+ "auto_discovered": true
2953
+ },
2954
+ {
2955
+ "entity": "IfcCovering",
2956
+ "source": "qset",
2957
+ "set_name": "ArchiCADQuantities",
2958
+ "key": "Conditional Surface Area of the Top",
2959
+ "file_count": 1,
2960
+ "projects": [
2961
+ "sixty5"
2962
+ ],
2963
+ "auto_discovered": true
2964
+ },
2965
+ {
2966
+ "entity": "IfcCovering",
2967
+ "source": "qset",
2968
+ "set_name": "ArchiCADQuantities",
2969
+ "key": "Edge Surface Area",
2970
+ "file_count": 1,
2971
+ "projects": [
2972
+ "sixty5"
2973
+ ],
2974
+ "auto_discovered": true
2975
+ },
2976
+ {
2977
+ "entity": "IfcCovering",
2978
+ "source": "qset",
2979
+ "set_name": "ArchiCADQuantities",
2980
+ "key": "Gross Surface Area of the Slab Bottom",
2981
+ "file_count": 1,
2982
+ "projects": [
2983
+ "sixty5"
2984
+ ],
2985
+ "auto_discovered": true
2986
+ },
2987
+ {
2988
+ "entity": "IfcCovering",
2989
+ "source": "qset",
2990
+ "set_name": "ArchiCADQuantities",
2991
+ "key": "Gross Surface Area of the Slab Bottom with Holes",
2992
+ "file_count": 1,
2993
+ "projects": [
2994
+ "sixty5"
2995
+ ],
2996
+ "auto_discovered": true
2997
+ },
2998
+ {
2999
+ "entity": "IfcCovering",
3000
+ "source": "qset",
3001
+ "set_name": "ArchiCADQuantities",
3002
+ "key": "Gross Surface Area of the Slab Edges",
3003
+ "file_count": 1,
3004
+ "projects": [
3005
+ "sixty5"
3006
+ ],
3007
+ "auto_discovered": true
3008
+ },
3009
+ {
3010
+ "entity": "IfcCovering",
3011
+ "source": "qset",
3012
+ "set_name": "ArchiCADQuantities",
3013
+ "key": "Gross Surface Area of the Slab Edges with Holes",
3014
+ "file_count": 1,
3015
+ "projects": [
3016
+ "sixty5"
3017
+ ],
3018
+ "auto_discovered": true
3019
+ },
3020
+ {
3021
+ "entity": "IfcCovering",
3022
+ "source": "qset",
3023
+ "set_name": "ArchiCADQuantities",
3024
+ "key": "Gross Surface Area of the Slab Top",
3025
+ "file_count": 1,
3026
+ "projects": [
3027
+ "sixty5"
3028
+ ],
3029
+ "auto_discovered": true
3030
+ },
3031
+ {
3032
+ "entity": "IfcCovering",
3033
+ "source": "qset",
3034
+ "set_name": "ArchiCADQuantities",
3035
+ "key": "Gross Surface Area of the Slab Top with Holes",
3036
+ "file_count": 1,
3037
+ "projects": [
3038
+ "sixty5"
3039
+ ],
3040
+ "auto_discovered": true
3041
+ },
3042
+ {
3043
+ "entity": "IfcCovering",
3044
+ "source": "qset",
3045
+ "set_name": "ArchiCADQuantities",
3046
+ "key": "Holes Surface Area",
3047
+ "file_count": 1,
3048
+ "projects": [
3049
+ "sixty5"
3050
+ ],
3051
+ "auto_discovered": true
3052
+ },
3053
+ {
3054
+ "entity": "IfcCovering",
3055
+ "source": "qset",
3056
+ "set_name": "ArchiCADQuantities",
3057
+ "key": "Top Surface Area",
3058
+ "file_count": 1,
3059
+ "projects": [
3060
+ "sixty5"
3061
+ ],
3062
+ "auto_discovered": true
3063
+ },
3064
+ {
3065
+ "entity": "IfcCovering",
3066
+ "source": "qset",
3067
+ "set_name": "ArchiCADQuantities",
3068
+ "key": "Area of the Column",
3069
+ "file_count": 1,
3070
+ "projects": [
3071
+ "sixty5"
3072
+ ],
3073
+ "auto_discovered": true
3074
+ },
3075
+ {
3076
+ "entity": "IfcCovering",
3077
+ "source": "qset",
3078
+ "set_name": "ArchiCADQuantities",
3079
+ "key": "Gross Surface Area of the Core (without Top/Bottom)",
3080
+ "file_count": 1,
3081
+ "projects": [
3082
+ "sixty5"
3083
+ ],
3084
+ "auto_discovered": true
3085
+ },
3086
+ {
3087
+ "entity": "IfcCovering",
3088
+ "source": "qset",
3089
+ "set_name": "ArchiCADQuantities",
3090
+ "key": "Gross Surface Area of the Core Top (or Bottom)",
3091
+ "file_count": 1,
3092
+ "projects": [
3093
+ "sixty5"
3094
+ ],
3095
+ "auto_discovered": true
3096
+ },
3097
+ {
3098
+ "entity": "IfcCovering",
3099
+ "source": "qset",
3100
+ "set_name": "ArchiCADQuantities",
3101
+ "key": "Gross Surface Area of the Veneer (without Top/Bottom)",
3102
+ "file_count": 1,
3103
+ "projects": [
3104
+ "sixty5"
3105
+ ],
3106
+ "auto_discovered": true
3107
+ },
3108
+ {
3109
+ "entity": "IfcCovering",
3110
+ "source": "qset",
3111
+ "set_name": "ArchiCADQuantities",
3112
+ "key": "Gross Surface Area of the Veneer Top (or Bottom)",
3113
+ "file_count": 1,
3114
+ "projects": [
3115
+ "sixty5"
3116
+ ],
3117
+ "auto_discovered": true
3118
+ },
3119
+ {
3120
+ "entity": "IfcCovering",
3121
+ "source": "qset",
3122
+ "set_name": "ArchiCADQuantities",
3123
+ "key": "Net Surface Area of the Core Bottom",
3124
+ "file_count": 1,
3125
+ "projects": [
3126
+ "sixty5"
3127
+ ],
3128
+ "auto_discovered": true
3129
+ },
3130
+ {
3131
+ "entity": "IfcCovering",
3132
+ "source": "qset",
3133
+ "set_name": "ArchiCADQuantities",
3134
+ "key": "Net Surface Area of the Core (without Top/Bottom)",
3135
+ "file_count": 1,
3136
+ "projects": [
3137
+ "sixty5"
3138
+ ],
3139
+ "auto_discovered": true
3140
+ },
3141
+ {
3142
+ "entity": "IfcCovering",
3143
+ "source": "qset",
3144
+ "set_name": "ArchiCADQuantities",
3145
+ "key": "Net Surface Area of the Core Top",
3146
+ "file_count": 1,
3147
+ "projects": [
3148
+ "sixty5"
3149
+ ],
3150
+ "auto_discovered": true
3151
+ },
3152
+ {
3153
+ "entity": "IfcCovering",
3154
+ "source": "qset",
3155
+ "set_name": "ArchiCADQuantities",
3156
+ "key": "Net Surface Area of the Veneer Bottom",
3157
+ "file_count": 1,
3158
+ "projects": [
3159
+ "sixty5"
3160
+ ],
3161
+ "auto_discovered": true
3162
+ },
3163
+ {
3164
+ "entity": "IfcCovering",
3165
+ "source": "qset",
3166
+ "set_name": "ArchiCADQuantities",
3167
+ "key": "Veneer Surface Area",
3168
+ "file_count": 1,
3169
+ "projects": [
3170
+ "sixty5"
3171
+ ],
3172
+ "auto_discovered": true
3173
+ },
3174
+ {
3175
+ "entity": "IfcCovering",
3176
+ "source": "qset",
3177
+ "set_name": "ArchiCADQuantities",
3178
+ "key": "Net Surface Area of the Veneer Top",
3179
+ "file_count": 1,
3180
+ "projects": [
3181
+ "sixty5"
3182
+ ],
3183
+ "auto_discovered": true
3184
+ },
3185
+ {
3186
+ "entity": "IfcCovering",
3187
+ "source": "qset",
3188
+ "set_name": "ArchiCADQuantities",
3189
+ "key": "End Surface Area",
3190
+ "file_count": 1,
3191
+ "projects": [
3192
+ "sixty5"
3193
+ ],
3194
+ "auto_discovered": true
3195
+ },
3196
+ {
3197
+ "entity": "IfcCovering",
3198
+ "source": "qset",
3199
+ "set_name": "ArchiCADQuantities",
3200
+ "key": "Holes Edge Surface Area",
3201
+ "file_count": 1,
3202
+ "projects": [
3203
+ "sixty5"
3204
+ ],
3205
+ "auto_discovered": true
3206
+ },
3207
+ {
3208
+ "entity": "IfcCovering",
3209
+ "source": "qset",
3210
+ "set_name": "ArchiCADQuantities",
3211
+ "key": "Left Side Surface Area",
3212
+ "file_count": 1,
3213
+ "projects": [
3214
+ "sixty5"
3215
+ ],
3216
+ "auto_discovered": true
3217
+ },
3218
+ {
3219
+ "entity": "IfcCovering",
3220
+ "source": "qset",
3221
+ "set_name": "ArchiCADQuantities",
3222
+ "key": "Right Side Surface Area",
3223
+ "file_count": 1,
3224
+ "projects": [
3225
+ "sixty5"
3226
+ ],
3227
+ "auto_discovered": true
3228
+ },
3229
+ {
3230
+ "entity": "IfcSite",
3231
+ "source": "qset",
3232
+ "set_name": "BaseQuantities",
3233
+ "key": "GrossArea",
3234
+ "file_count": 7,
3235
+ "projects": [
3236
+ "ac20",
3237
+ "fzk_house",
3238
+ "molio",
3239
+ "sixty5",
3240
+ "smiley_west"
3241
+ ],
3242
+ "auto_discovered": true
3243
+ },
3244
+ {
3245
+ "entity": "IfcBuilding",
3246
+ "source": "qset",
3247
+ "set_name": "BaseQuantities",
3248
+ "key": "GrossFloorArea",
3249
+ "file_count": 7,
3250
+ "projects": [
3251
+ "ac20",
3252
+ "fzk_house",
3253
+ "molio",
3254
+ "sixty5",
3255
+ "smiley_west"
3256
+ ],
3257
+ "auto_discovered": true
3258
+ },
3259
+ {
3260
+ "entity": "IfcBuildingStorey",
3261
+ "source": "qset",
3262
+ "set_name": "BaseQuantities",
3263
+ "key": "GrossFloorArea",
3264
+ "file_count": 7,
3265
+ "projects": [
3266
+ "ac20",
3267
+ "fzk_house",
3268
+ "molio",
3269
+ "sixty5",
3270
+ "smiley_west"
3271
+ ],
3272
+ "auto_discovered": true
3273
+ }
3274
+ ]
3275
+ }
teams/lux-ai/Logs/ifc_scan.log ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ INFO Found 36 IFC file(s) under: C:\Users\LEGION\Documents\GitHub\Lux.Ai\Sample projects\projects
2
+ INFO [1/36] 4351/arc.ifc
3
+ INFO Schema: IFC2X3
4
+ INFO [2/36] ac20/arc.ifc
5
+ INFO Schema: IFC4
6
+ INFO [3/36] city_house_munich/arc.ifc
7
+ INFO Schema: IFC4X3
8
+ INFO [4/36] dental_clinic/arc.ifc
9
+ INFO Schema: IFC2X3
10
+ INFO [5/36] dental_clinic/mep.ifc
11
+ INFO Schema: IFC2X3
12
+ INFO [6/36] dental_clinic/str.ifc
13
+ INFO Schema: IFC2X3
14
+ INFO [7/36] digital_hub/arc.ifc
15
+ INFO Schema: IFC4
16
+ INFO [8/36] digital_hub/heating.ifc
17
+ INFO Schema: IFC4
18
+ INFO [9/36] digital_hub/plumbing.ifc
19
+ INFO Schema: IFC4
20
+ INFO [10/36] digital_hub/ventilation.ifc
21
+ INFO Schema: IFC4
22
+ INFO [11/36] duplex/arc.ifc
23
+ INFO Schema: IFC2X3
24
+ INFO [12/36] duplex/mep.ifc
25
+ INFO Schema: IFC2X3
26
+ INFO [13/36] ettenheim_gis/city.ifc
27
+ INFO Schema: IFC2X3
28
+ INFO [14/36] fantasy_hotel_1/arc.ifc
29
+ INFO Schema: IFC4
30
+ INFO [15/36] fantasy_hotel_2/arc.ifc
31
+ INFO Schema: IFC4
32
+ INFO [16/36] fantasy_office_building_1/arc.ifc
33
+ INFO Schema: IFC4
34
+ INFO [17/36] fantasy_office_building_2/arc.ifc
35
+ INFO Schema: IFC4
36
+ INFO [18/36] fantasy_office_building_3/arc.ifc
37
+ INFO Schema: IFC4
38
+ INFO [19/36] fantasy_residential_building_1/arc.ifc
39
+ INFO Schema: IFC4
40
+ INFO [20/36] fzk_house/arc.ifc
41
+ INFO Schema: IFC4
42
+ INFO [21/36] hitos/arc.ifc
43
+ INFO Schema: IFC2X3
44
+ INFO [22/36] molio/arc.ifc
45
+ INFO Schema: IFC2X3
46
+ INFO [23/36] samuel_macalister_sample_house/arc.ifc
47
+ INFO Schema: IFC4
48
+ INFO [24/36] samuel_macalister_sample_house/mep.ifc
49
+ INFO Schema: IFC4
50
+ INFO [25/36] schependomlaan/arc.ifc
51
+ INFO Schema: IFC2X3
52
+ INFO [26/36] sixty5/arc.ifc
53
+ INFO Schema: IFC2X3
54
+ INFO [27/36] sixty5/electrical.ifc
55
+ INFO Schema: IFC2X3
56
+ INFO [28/36] sixty5/facade.ifc
57
+ INFO Schema: IFC2X3
58
+ INFO [29/36] sixty5/kitchen.ifc
59
+ INFO Schema: IFC2X3
60
+ INFO [30/36] sixty5/plumbing.ifc
61
+ INFO Schema: IFC2X3
62
+ INFO [31/36] sixty5/str.ifc
63
+ INFO Schema: IFC2X3
64
+ INFO [32/36] sixty5/ventilation.ifc
65
+ INFO Schema: IFC2X3
66
+ INFO [33/36] smiley_west/arc.ifc
67
+ INFO Schema: IFC4
68
+ INFO [34/36] wbdg_office/arc.ifc
69
+ INFO Schema: IFC2X3
70
+ INFO [35/36] wbdg_office/mep.ifc
71
+ INFO Schema: IFC2X3
72
+ INFO [36/36] wbdg_office/str.ifc
73
+ INFO Schema: IFC2X3
74
+ INFO CSV written to: C:\Users\LEGION\Documents\GitHub\Lux.Ai\ifc_scan_results.csv
teams/lux-ai/Logs/ifc_scan_results.csv ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ project_name,ifc_file,window_area_m2,floor_area_m2,roof_area_m2,true_north_angle_deg,latitude,longitude,error
2
+ 4351,arc.ifc,10.8697,,,,,,
3
+ ac20,arc.ifc,309.5,1939.3839,688.7667,0.0,49.15,8.716667,
4
+ city_house_munich,arc.ifc,29.5085,,,32.56,48.187378,11.47271,
5
+ dental_clinic,arc.ifc,100.63,,,,42.358429,-71.059776,
6
+ dental_clinic,mep.ifc,,,,,,,
7
+ dental_clinic,str.ifc,,,,,42.358429,-71.059776,
8
+ digital_hub,arc.ifc,375.0,2842.1215,,360.0,48.1376,11.57994,
9
+ digital_hub,heating.ifc,,,,360.0,48.1376,11.57994,
10
+ digital_hub,plumbing.ifc,,,,360.0,48.1376,11.57994,
11
+ digital_hub,ventilation.ifc,,,,360.0,48.1376,11.57994,
12
+ duplex,arc.ifc,65.9415,,,,41.8744,-87.6394,
13
+ duplex,mep.ifc,,,,,41.8744,-87.6394,
14
+ ettenheim_gis,city.ifc,725.9732,,,,,,
15
+ fantasy_hotel_1,arc.ifc,58.7815,,,360.0,42.358723,-71.056763,
16
+ fantasy_hotel_2,arc.ifc,400.305,,,360.0,42.358723,-71.056763,
17
+ fantasy_office_building_1,arc.ifc,156.816,1257.0688,339.9228,360.0,48.139126,11.580186,
18
+ fantasy_office_building_2,arc.ifc,142.91,716.0568,419.5723,360.0,48.1376,11.57994,
19
+ fantasy_office_building_3,arc.ifc,984.96,,,360.0,48.148853,11.567998,
20
+ fantasy_residential_building_1,arc.ifc,19.563,,,360.0,48.139126,11.580186,
21
+ fzk_house,arc.ifc,23.1702,173.3424,165.1222,310.0,49.100435,8.436539,
22
+ hitos,arc.ifc,1531.3183,,,360.0,69.666667,18.833333,
23
+ molio,arc.ifc,,140.7827,,0.0,55.667574,12.619383,
24
+ samuel_macalister_sample_house,arc.ifc,61.6407,,,323.0,42.213,-71.0335,
25
+ samuel_macalister_sample_house,mep.ifc,,,,26.62,51.45,5.483333,
26
+ schependomlaan,arc.ifc,,,,,,,
27
+ sixty5,arc.ifc,4901.1499,0.0,,0.0,51.45,5.483333,
28
+ sixty5,electrical.ifc,,,,360.0,42.387,-71.242,
29
+ sixty5,facade.ifc,,,,0.0,51.45,5.483333,
30
+ sixty5,kitchen.ifc,,,,270.0,0.0,0.0,
31
+ sixty5,plumbing.ifc,,,,360.0,42.387,-71.242,
32
+ sixty5,str.ifc,,34199.084,,360.0,42.387,-71.242,
33
+ sixty5,ventilation.ifc,,,,360.0,42.387,-71.242,
34
+ smiley_west,arc.ifc,95.9195,1821.1689,614.4088,147.5,49.033245,8.39098,
35
+ wbdg_office,arc.ifc,124.5,,,,42.358429,-71.059776,
36
+ wbdg_office,mep.ifc,,,,,42.358429,-71.059776,
37
+ wbdg_office,str.ifc,,,,,42.358429,-71.059776,
teams/lux-ai/Logs/solar_pipeline.log ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ INFO Schema: IFC4
2
+ INFO Parsing roof geometry: arc.ifc
3
+ INFO Found 2 roof element(s) for geometry extraction
4
+ INFO Applying true-north correction: 310.00Β°
5
+ INFO Roof area validated: geometry=165.1 mΒ², property-set=165.1 mΒ² (0% diff)
6
+ INFO Extracted 2 roof segment(s) from arc.ifc
teams/lux-ai/Logs/solar_pipeline_scan.csv ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ project_name,ifc_file,window_area_m2,floor_area_m2,roof_area_m2,true_north_angle_deg,latitude,longitude,error
2
+ 4351,arc.ifc,10.8697,7517.2622,,,,,
3
+ ac20,arc.ifc,309.5,1939.3839,688.7667,0.0,49.15,8.716667,
4
+ city_house_munich,arc.ifc,29.5085,,,32.56,48.187378,11.47271,
5
+ dental_clinic,arc.ifc,100.63,6935.7986,,,42.358429,-71.059776,
6
+ dental_clinic,mep.ifc,,,,,,,
7
+ dental_clinic,str.ifc,,,,,42.358429,-71.059776,
8
+ digital_hub,arc.ifc,375.0,2842.1215,,360.0,48.1376,11.57994,
9
+ digital_hub,heating.ifc,,,,360.0,48.1376,11.57994,
10
+ digital_hub,plumbing.ifc,,,,360.0,48.1376,11.57994,
11
+ digital_hub,ventilation.ifc,,,,360.0,48.1376,11.57994,
12
+ duplex,arc.ifc,65.9415,422.0466,,,41.8744,-87.6394,
13
+ duplex,mep.ifc,,797.7923,,,41.8744,-87.6394,
14
+ ettenheim_gis,city.ifc,725.9732,,,,,,
15
+ fantasy_hotel_1,arc.ifc,58.7815,,,360.0,42.358723,-71.056763,
16
+ fantasy_hotel_2,arc.ifc,400.305,,,360.0,42.358723,-71.056763,
17
+ fantasy_office_building_1,arc.ifc,156.816,1257.0688,339.9228,360.0,48.139126,11.580186,
18
+ fantasy_office_building_2,arc.ifc,142.91,716.0568,419.5723,360.0,48.1376,11.57994,
19
+ fantasy_office_building_3,arc.ifc,984.96,,,360.0,48.148853,11.567998,
20
+ fantasy_residential_building_1,arc.ifc,19.563,,,360.0,48.139126,11.580186,
21
+ fzk_house,arc.ifc,23.1702,173.3424,165.1222,310.0,49.100435,8.436539,
22
+ hitos,arc.ifc,1531.3183,,,360.0,69.666667,18.833333,
23
+ molio,arc.ifc,,140.7827,,0.0,55.667574,12.619383,
24
+ samuel_macalister_sample_house,arc.ifc,61.6407,,,323.0,42.213,-71.0335,
25
+ samuel_macalister_sample_house,mep.ifc,,,,26.62,51.45,5.483333,
26
+ schependomlaan,arc.ifc,,,,,,,
27
+ sixty5,arc.ifc,,,,,,,<built-in function open> returned a result with an exception set
28
+ sixty5,electrical.ifc,,,,,,,<built-in function open> returned a result with an exception set
29
+ sixty5,facade.ifc,,,,0.0,51.45,5.483333,
30
+ sixty5,kitchen.ifc,,,,270.0,0.0,0.0,
31
+ sixty5,plumbing.ifc,,,,360.0,42.387,-71.242,
32
+ sixty5,str.ifc,,33915.088,,360.0,42.387,-71.242,
33
+ sixty5,ventilation.ifc,,,,,,,<built-in function open> returned a result with an exception set
34
+ smiley_west,arc.ifc,95.9195,1821.1689,614.4088,147.5,49.033245,8.39098,
35
+ wbdg_office,arc.ifc,124.5,3582.0077,,,42.358429,-71.059776,
36
+ wbdg_office,mep.ifc,,7009.6301,,,42.358429,-71.059776,
37
+ wbdg_office,str.ifc,,,,,42.358429,-71.059776,
teams/lux-ai/Lux ai tool/README.md ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lux.Ai β€” IFCore Platform Tool
2
+
3
+ > Solar energy compliance checks for the IFCore platform.
4
+
5
+ ---
6
+
7
+ ## Repo Structure (IFCore Contract)
8
+
9
+ ```
10
+ Lux ai tool/
11
+ β”œβ”€β”€ tools/
12
+ β”‚ └── checker_solar.py ← 5 check_* functions (platform-scanned)
13
+ β”œβ”€β”€ final_pipeline/ ← bundled IFC parsing + solar engine
14
+ β”‚ β”œβ”€β”€ config.py
15
+ β”‚ β”œβ”€β”€ ifc_metadata_extractor.py
16
+ β”‚ β”œβ”€β”€ ifc_roof_parser.py
17
+ β”‚ β”œβ”€β”€ solar_production_engine.py
18
+ β”‚ └── key_aliases.json
19
+ β”œβ”€β”€ run.py ← CLI entry-point
20
+ β”œβ”€β”€ requirements.txt ← team dependencies
21
+ β”œβ”€β”€ test_contract.py ← contract validation tests
22
+ └── README.md ← this file
23
+ ```
24
+
25
+ The platform auto-discovers all `check_*` functions inside `tools/checker_*.py`.
26
+
27
+ ---
28
+
29
+ ## Quick Start
30
+
31
+ ```bash
32
+ cd "Lux ai tool"
33
+ pip install -r requirements.txt
34
+
35
+ # Run all 5 checks on an IFC file
36
+ python run.py path/to/model.ifc
37
+
38
+ # Run specific checks only
39
+ python run.py path/to/model.ifc --checks location building_areas roof_geometry
40
+
41
+ # Override coordinates for solar / LEED checks
42
+ python run.py path/to/model.ifc --lat 48.13 --lon 11.58
43
+
44
+ # Export results to JSON
45
+ python run.py path/to/model.ifc --output results.json
46
+
47
+ # List available checks
48
+ python run.py --list-checks
49
+ ```
50
+
51
+ | Function | What it checks | Needs internet? |
52
+ |----------|---------------|-----------------|
53
+ | `check_location` | IfcSite has latitude + longitude | No |
54
+ | `check_building_areas` | Window, floor, and roof area present | No |
55
+ | `check_roof_geometry` | 3D roof segments extractable (tilt, azimuth, area) | No |
56
+ | `check_solar_production` | PVWatts returns kWh/yr > 0 for each segment | **Yes** |
57
+ | `check_leed_score` | LEED renewable-energy score β‰₯ 50% | **Yes** |
58
+
59
+ ---
60
+
61
+ ## Usage
62
+
63
+ ### Local testing
64
+
65
+ ```python
66
+ import ifcopenshell
67
+ from tools.checker_solar import check_location
68
+
69
+ model = ifcopenshell.open("path/to/model.ifc")
70
+ results = check_location(model)
71
+ for r in results:
72
+ print(f"[{r['check_status'].upper()}] {r['element_name']}: {r['actual_value']} (req: {r['required_value']})")
73
+ ```
74
+
75
+ ### Contract validation
76
+
77
+ ```bash
78
+ cd "Lux ai tool"
79
+ python test_contract.py
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Contract Compliance
85
+
86
+ Every `check_*` function:
87
+
88
+ - **First arg:** `model` (`ifcopenshell.file` object)
89
+ - **Returns:** `list[dict]` β€” one dict per element
90
+ - **Dict keys:** `element_id`, `element_type`, `element_name`, `element_name_long`, `check_status`, `actual_value`, `required_value`, `comment`, `log`
91
+ - **Status values:** `pass`, `fail`, `warning`, `blocked`, `log`
92
+
93
+ ---
94
+
95
+ ## Dependencies
96
+
97
+ This tool is fully self-contained. All `final_pipeline/` modules are bundled. Install with:
98
+
99
+ ```bash
100
+ pip install ifcopenshell numpy requests
101
+ ```
teams/lux-ai/Lux ai tool/final_pipeline/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ final_pipeline β€” Unified IFC metadata extraction + solar production analysis.
3
+
4
+ Merges:
5
+ - IFC property-set/quantity-set metadata scanner (window, floor, roof area,
6
+ orientation, lat/lon) with multi-exporter alias fallback
7
+ - Geometry-based roof segment parser (per-face tilt & azimuth via 3D mesh)
8
+ - NREL PVWatts v8 solar production engine
9
+
10
+ Supports single-file solar analysis and batch scanning of project directories.
11
+ """
12
+
13
+ from final_pipeline.config import __version__
14
+
15
+ __all__ = ["__version__"]