Hent mye mer ut av IFC-modellen
Energirapporter / rapporter (release) Successful in 57s
Release enok / bygg (release) Successful in 1m8s
test / test (push) Successful in 1m16s

Modellen for Nordhusstykket 5 viste seg å inneholde langt mer enn importen brukte.

Etasjer
- IfcBuildingStorey med høyde og navn, og tilknytningen av rom og bygningsdeler, sier
  hva som er hva. Etasjen «Kjeller» ligger 2,35 m under referansenivået.
- En etasje under terreng settes nå opp som en egen uoppvarmet sone med sine egne
  vegger og gulv, og dekket over blir grensen mot den oppvarmede delen.
- Oppvarmet BRA er rommene i etasjene over terreng. Rom som heter kott, bod, loft
  eller garasje blir flagget, siden de sjelden er oppvarmet.
- Et dekke kan bare ligge mot grunnen i nederste etasje. Før ble etasjeskillere høyere
  opp regnet som gulv mot grunn.

Egenskaper
- Byggeår, bygningskategori, kommune, lekkasjetall, ventilasjonstype, varmegjenvinning,
  varmekilde og oppvarmet BRA leses fra et hvilket som helst egenskapssett på bygningen,
  tomta eller prosjektet, på norsk eller engelsk. Da kan et postformat i BIM-verktøyet
  bære de opplysningene IFC ikke har noen fast plass til.
- Modellen viste seg allerede å ha YearOfConstruction 1950. Bare den ene verdien flytter
  den automatiske beregningen fra karakter E til G, siden lekkasjetallet følger byggeåret.
- Masseklasse utledes av materialnavnene, og posisjonen leses av IfcSite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
bl
2026-09-14 22:36:54 +02:00
co-authored by Claude Opus 5
parent afb8ea3fc4
commit 81544d73e6
6 changed files with 1081 additions and 377 deletions
Generated
+1 -1
View File
@@ -222,7 +222,7 @@ dependencies = [
[[package]] [[package]]
name = "enok" name = "enok"
version = "0.1.1" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "enok" name = "enok"
version = "0.1.1" version = "0.2.0"
edition = "2021" edition = "2021"
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)" description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
license = "MIT" license = "MIT"
+51 -5
View File
@@ -178,12 +178,58 @@ Ferdige eksempler ligger i `eksempler/`.
| gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag | | gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag |
| Regneark | *Fil → Eksporter → Export Worksheet* | Enkel CSV med én rad per bygningsdel | | Regneark | *Fil → Eksporter → Export Worksheet* | Enkel CSV med én rad per bygningsdel |
Himmelretningene i IFC utledes av elementenes plassering: veggens lokale akse gir planet, og ### Hva enok henter ut av en IFC-modell
ytre normal velges som den som peker bort fra bygningens tyngdepunkt, korrigert for prosjektets
sanne nord. Kontroller dem mot tegningen etter import.
Ingen av formatene beskriver ventilasjon, lekkasjetall eller varmekilder. Importen skriver | Opplysning | Hvor den kommer fra |
merknader øverst i TOML-filen om hva som må fylles ut. | --- | --- |
| Areal for vegger, tak og dekker | Geometrien. Trekantene grupperes etter retning, og den største gruppen er delens hovedflate. Vinduer som er skåret ut av veggen kommer riktig fram, siden hullene mangler trekanter. Materialsjikt som ligger oppå hverandre telles én gang |
| Himmelretning og helning | Flatenormalen, dreid med elementets plassering og prosjektets sanne nord |
| U-verdi | `ThermalTransmittance` i egenskapssettene, for både konstruksjoner og vinduer |
| Ytre eller innvendig | `IsExternal` |
| Masseklasse | Materialnavnene. Betong, tegl og mur gir tung konstruksjon, bindingsverk og isolasjon lett |
| Etasjer og hva som ligger i dem | `IfcBuildingStorey` med høyde, og tilknytningen av rom og bygningsdeler |
| Uoppvarmet kjeller | En etasje under referansenivået blir satt opp som egen sone, med sine egne vegger og gulv, og dekket over som grense mot den oppvarmede delen |
| Oppvarmet BRA | Rommene i etasjene over terreng. Rom som heter kott, bod, loft eller garasje blir flagget |
| Vindus- og dørareal | `OverallHeight` og `OverallWidth`, og hvilken vegg de sitter i |
| Posisjon | `IfcSite` med breddegrad og lengdegrad |
Himmelretningene og arealene bør likevel kontrolleres mot tegningen. Importen skriver merknader
øverst i TOML-filen om hva den måtte anta.
### Egenskaper enok leser
IFC beskriver geometri og konstruksjon, men har ingen fast plass til byggeår, tetthet,
ventilasjon eller hvordan huset varmes opp. Det er nettopp de opplysningene som skiller en god
energiberegning fra en middelmådig: for et hus fra 1948 er forskjellen mellom lekkasjetall 0,6
og 8 langt større enn alt annet i modellen.
De kan likevel følge med. I Vectorworks lager du et postformat, fester det på bygningen og
knytter feltene til et IFC-egenskapssett. enok leser da disse navnene fra et hvilket som helst
egenskapssett på `IfcBuilding`, `IfcSite` eller `IfcProject`:
| Felt | Godtar også | Eksempel |
| --- | --- | --- |
| `Byggeaar` | `Byggeår`, `YearOfConstruction` | `1948` |
| `Bygningskategori` | `Kategori`, `BuildingCategory` | `Småhus` |
| `Kommune` | `Municipality`, `Klimasted` | `Indre Østfold` |
| `Lekkasjetall` | `n50`, `AirPermeability` | `8.0` |
| `Ventilasjonstype` | `Ventilasjon`, `VentilationType` | `Balansert` |
| `Varmegjenvinning` | `Gjenvinning`, `HeatRecoveryEfficiency` | `80` |
| `Varmekilde` | `Oppvarming`, `HeatingSource` | `Bergvarmepumpe` |
| `OppvarmetBRA` | `HeatedFloorArea` | `120` |
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
modellen alene.
Fire ting i selve modellen er verdt å gjøre riktig:
- **Sett prosjektstedet.** Da får `IfcSite` en posisjon, og klimadataene kan velges etter den.
Uten det står Vectorworks' standardverdi, som ligger i Washington.
- **Merk yttervegger med `IsExternal`.** I modellen for Nordhusstykket 5 er bare tolv av
hundre og fem vegger merket, og kjellermurene er ikke blant dem.
- **Fyll inn U-verdi for tak og vinduer.** Veggene har den, takflatene og vinduene ikke.
- **Gi etasjene navn.** «Kjeller», «1. etg.» og «2. etg.» er nok til at enok finner den
uoppvarmede kjelleren og regner oppvarmet areal riktig.
Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i BIM-verktøyet: Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i BIM-verktøyet:
+403 -338
View File
@@ -1,392 +1,457 @@
# Nordhusstykket 5, 1825 Tomter — enebolig i tre fra 1948, 1,5 etasje på betong grunnmur. # Importert med enok. Fyll ut feltene som er merket nedenfor.
# # Oppvarmet BRA er satt til 139.5 m², summen av rommene i etasjene over terreng. Rom [m²]: Stue 39.5, Kjøkken 22.6, Soverom 15.4, Gang/trapperom 11.2, Entré 10.6, Soverom 9.3, Soverom 9.2, Kott 7.3, Kott 4.6, Kott 4.1, Kott 3.6, Vindfang 1.2, Toalett 0.9.
# Geometri, U-verdier og himmelretninger er importert fra byggets IFC-modell # Disse rommene er regnet som oppvarmet, men navnet tyder på at de ikke er det: Kott 3.6 m², Kott 4.1 m², Kott 7.3 m², Kott 4.6 m². Trekk dem fra oppvarmet_bra om de står uoppvarmet.
# (Vectorworks Architect 2026, «Nordhusstykket 5.ifc»). Arealene er regnet av geometrien, # 30 bygningsdeler er utelatt fordi geometrien ikke ga noe areal.
# og oppføringene er slått sammen per konstruksjon og himmelretning. # 17 bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok bruke typiske verdier for byggeåret.
# Vurderingene av isolasjon og lekkasjer er hentet fra termografirapporten av 10.3.2016. # Arealene er regnet av geometrien: trekantene er gruppert etter retning, og den største gruppen er delens hovedflate. Himmelretningene følger flatenormalen. Kontroller begge mot tegningen.
# # IFC beskriver ikke drift, lekkasjetall eller energiforsyning. Fyll inn [ventilasjon], [infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning.
# Verdier som ikke står i modellen og må kontrolleres på stedet er merket KONTROLLER. # Modellen har egenskaper for drift og forsyning, og disse er tatt i bruk: YearOfConstruction.
# Etasjen «Kjeller» ligger under terreng og er satt opp som en uoppvarmet sone med 75 m² gulv. Kjellerveggene er regnet 2.4 m under terreng uten noe over. Står en del av grunnmuren over bakken, sett `over_terreng` i meter på veggene; utslaget er lite, om lag én prosent per meter.
# Er kjelleren oppvarmet likevel, slett [[sone]] og legg flatene inn i klimaskjermen. Går det uisolerte varmerør der, sett `andel_distribusjonstap = 1.0` på sonen.
# Modellen oppgir posisjon 38.8894° nord, -77.0353° øst. Kontroller at den stemmer, og sett `kommune` under [klima] deretter.
[bygning] [bygning]
navn = "Nordhusstykket 5" navn = "nordhusstykket-5"
kategori = "smaahus" kategori = "smaahus"
byggeaar = 1948 byggeaar = 1950
# Oppvarmet BRA er summen av rommene i 1. og 2. etasje i IFC-modellen: oppvarmet_bra = 139.533
# stue 39,6 + kjøkken 22,6 + entré 10,6 + vindfang 1,2 = 74,0 m² i 1. etasje, antall_boenheter = 1
# tre soverom 33,9 + gang/trapperom 11,2 + toalett 0,9 = 46,0 m² i 2. etasje. beskrivelse = "Importert fra IFC"
# Kjeller (74,9 m²) og kott (19,6 m²) er uoppvarmede og regnes som egne soner.
oppvarmet_bra = 120.0
oppvarmet_volum = 288.0
adresse = "Nordhusstykket 5, 1825 Tomter"
oppdragsgiver = "Ellen Brødholt"
beskrivelse = "Enebolig i tre, 1,5 etasje, bebygget areal 99,6 m², uoppvarmet kjeller på betong grunnmur"
[klima] [klima]
kommune = "Indre Østfold"
[grunn]
ledningsevne = 2.0
# ── klimaskjerm for den oppvarmede delen ───────────────────────────────
# Ytterveggene er stenderverk med panel ute og porøs huntonitt inne. IFC-modellen oppgir
# U-verdi 0,93 W/(m²K), som stemmer med termografirapportens vurdering av at veggen har
# noe, men lite isolasjon.
[[flate]] [[flate]]
navn = "Yttervegg nord" navn = "Roof Slab"
type = "vegg"
areal = 41.8
orientering = 0
u_verdi = 0.93
masseklasse = "lett"
[[flate]]
navn = "Yttervegg øst"
type = "vegg"
areal = 54.2
orientering = 90
u_verdi = 0.93
masseklasse = "lett"
[[flate]]
navn = "Yttervegg sør"
type = "vegg"
areal = 55.7
orientering = 180
u_verdi = 0.93
masseklasse = "lett"
[[flate]]
navn = "Yttervegg vest"
type = "vegg"
areal = 59.5
orientering = 270
u_verdi = 0.93
masseklasse = "lett"
# KONTROLLER: IFC-modellen oppgir ingen U-verdi for takflatene. Termografien viser
# luftlekkasje til loftet og ut gjennom pipen, og lite isolasjon i skråtaket.
[[flate]]
navn = "Skråtak"
type = "tak" type = "tak"
areal = 130.1 areal = 130.11978717725776
helning = 40 orientering = 0.0
u_verdi = 0.55 helning = 14.999984009080658
masseklasse = "lett" masseklasse = "middels"
kommentar = "Areal fra IFC; U-verdi anslått etter termografirapporten" masseplassering = "fordelt"
mot = "ute"
kommentar = "importert fra IFC"
[[flate]] [[flate]]
navn = "Himling mot kaldt loft" navn = "Etasjeskille"
type = "tak"
areal = 32.6
helning = 0
u_verdi = 0.45
mot = "uoppvarmet"
b_faktor = 0.9
masseklasse = "lett"
# Etasjeskilleren mot kjelleren. IFC-modellen har 150,4 m² «Etasjeskille», som dekker både
# gulvet over kjelleren og skillet mellom 1. og 2. etasje. Halvparten ligger over kjelleren,
# og stemmer med kjellergulvets 78,9 m².
[[flate]]
navn = "Etasjeskiller mot kjeller"
type = "gulv" type = "gulv"
areal = 75.0 areal = 77.04260758445601
orientering = 0.0
helning = 0.0
u_verdi = 0.31 u_verdi = 0.31
masseklasse = "lett"
masseplassering = "fordelt"
mot = "uoppvarmet" mot = "uoppvarmet"
sone = "Kjeller" sone = "Kjeller"
masseklasse = "middels" kommentar = "1. etg., importert fra IFC"
# ── vinduer og dører ─────────────────────────────────────────────────── [[flate]]
# Areal og himmelretning fra IFC. KONTROLLER: modellen oppgir ingen U-verdi for vinduene. navn = "Yttervegg panel ute og porøs huntonitt inne"
# Termografirapporten beskriver dem som dårlig isolerte, og 2,8 W/(m²K) svarer til type = "vegg"
# eldre tolagsglass. Med originale enkle glass ville verdien vært nærmere 5. areal = 55.719888250487756
orientering = 180.0
helning = 90.0
u_verdi = 0.93
masseplassering = "fordelt"
mot = "ute"
kommentar = "2. etg., importert fra IFC"
[[vindu]] [[flate]]
navn = "Vinduer nord" navn = "Yttervegg panel ute og porøs huntonitt inne"
areal = 5.58 type = "vegg"
orientering = 0 areal = 59.50176490568849
u_verdi = 2.80 orientering = 270.0
g_verdi = 0.76 helning = 90.0
u_verdi = 0.93
masseklasse = "lett"
masseplassering = "fordelt"
mot = "ute"
kommentar = "1. etg., importert fra IFC"
[[vindu]] [[flate]]
navn = "Vinduer øst" navn = "Yttervegg panel ute og porøs huntonitt inne"
areal = 2.86 type = "vegg"
orientering = 90 areal = 41.833542336937
u_verdi = 2.80 orientering = 0.0
g_verdi = 0.76 helning = 90.0
u_verdi = 0.93
masseklasse = "lett"
masseplassering = "fordelt"
mot = "ute"
kommentar = "1. etg., importert fra IFC"
[[vindu]] [[flate]]
navn = "Vinduer sør" navn = "Slab on Grade-100mm"
areal = 3.96 type = "gulv"
orientering = 180 areal = 16.3663225
u_verdi = 2.80 orientering = 0.0
g_verdi = 0.76 helning = 0.0
u_verdi = 3.51
masseklasse = "tung"
masseplassering = "fordelt"
mot = "uoppvarmet"
sone = "Kjeller"
kommentar = "1. etg., importert fra IFC"
[[vindu]] [[flate]]
navn = "Vinduer vest" navn = "Yttervegg panel ute og porøs huntonitt inne"
areal = 1.05 type = "vegg"
orientering = 270 areal = 54.21918162153324
u_verdi = 2.80 orientering = 90.0
g_verdi = 0.76 helning = 90.0
u_verdi = 0.93
masseklasse = "lett"
masseplassering = "fordelt"
mot = "ute"
kommentar = "2. etg., importert fra IFC"
[[vindu]] [[flate]]
navn = "Inngangsdør med vindfang" navn = "Etasjeskille-loft"
areal = 1.46 type = "tak"
orientering = 270 areal = 32.594422635
u_verdi = 2.00 orientering = 0.0
dor = true helning = 0.0
u_verdi = 0.45
[[vindu]] masseklasse = "lett"
navn = "Verandadør øst" masseplassering = "fordelt"
areal = 2.94 mot = "uoppvarmet"
orientering = 90 kommentar = "2. etg., importert fra IFC"
u_verdi = 2.00
dor = true
[[vindu]]
navn = "Balkongdør sør"
areal = 1.91
orientering = 180
u_verdi = 2.00
dor = true
# Utkragede bjelker som bærer balkongen er en tydelig kuldebro i termografien.
[kuldebroer]
normalisert = 0.15
# KONTROLLER: lekkasjetallet er ikke målt. Termografien viser luftlekkasje til loft og
# gjennom pipen, som er typisk for et utett hus fra 1948.
[infiltrasjon]
n50 = 8.0
skjerming = "moderat"
[ventilasjon]
type = "naturlig"
luftmengde = 1.2
[tappevann]
system = "aapent"
# ── uoppvarmet kjeller ─────────────────────────────────────────────────
# Kjelleren brukes ikke til opphold, men varmes av uisolerte varmerør og av kjelen som står
# i det tekniske rommet. Temperaturen regnes ut time for time av kjellerens varmebalanse,
# slik at rørtapet både teller som tap fra varmeanlegget og som varme som holder
# etasjeskilleren varm. Det var dette termografirapporten pekte på som det største funnet.
[[sone]] [[sone]]
navn = "Kjeller" navn = "Kjeller"
volum = 165.0 volum = 176.1231
luftveksling = 0.4 fast_effekt = 0.0
# Alt distribusjonstapet fra det vannbårne anlegget avgis her, og berederen står her. kommentar = "Etasje «Kjeller» ligger 2.35 m under referansenivået. Rom [m²]: Bod 10.4, Gang 11.1, Rom 21.1, Teknisk rom 2.2, Bod 9.3, Vaskrom 9.8, Vaskrom 11.1"
andel_distribusjonstap = 1.0
andel_tappevannstap = 1.0
# KONTROLLER: anslått kappetap fra kjelen.
fast_effekt = 100.0
kommentar = "Rom 21,1 + gang 11,1 + vaskerom 9,9 + 11,1 + boder 10,4 + 9,3 + teknisk rom 2,2 = 74,9 m²"
# Grunnmuren. KONTROLLER: høyden over terreng er anslått til 0,6 m av fasadebildene i
# termografirapporten, og kjellerhøyden til 2,2 m. Disse to tallene avgjør hvor mye av
# muren som taper varme rett til uteluft, og er derfor verdt å måle på stedet.
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert, nord"
type = "vegg"
areal = 21.2
orientering = 0
u_verdi = 3.13
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]] [[sone.flate]]
navn = "Kjellermur 30 cm uisolert, øst" navn = "Kjellermur med treullsement-2"
type = "vegg" type = "vegg"
areal = 9.7 areal = 1.4834999999999998
orientering = 90 orientering = 90.0
u_verdi = 3.13 helning = 90.0
u_verdi = 0.46
masseklasse = "lett"
masseplassering = "fordelt"
mot = "grunn" mot = "grunn"
dybde = 1.6 dybde = 2.35
over_terreng = 0.6 over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert, sør"
type = "vegg"
areal = 22.7
orientering = 180
u_verdi = 3.13
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert, vest"
type = "vegg"
areal = 15.0
orientering = 270
u_verdi = 3.13
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]] [[sone.flate]]
navn = "Kjellermur 25 cm uisolert" navn = "Kjellermur 25 cm uisolert"
type = "vegg" type = "vegg"
areal = 40.7 areal = 16.33814305
orientering = 90 orientering = 90.0
helning = 90.0
u_verdi = 3.39 u_verdi = 3.39
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn" mot = "grunn"
dybde = 1.6 dybde = 2.35
over_terreng = 0.6 over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]] [[sone.flate]]
navn = "Kjellermur med treullsement" navn = "Kjellermur 30 cm uisolert"
type = "vegg" type = "vegg"
areal = 2.2 areal = 22.69398933457598
orientering = 90 orientering = 180.0
u_verdi = 0.46 helning = 90.0
u_verdi = 3.13
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn" mot = "grunn"
dybde = 1.6 dybde = 2.35
over_terreng = 0.6 over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert"
type = "vegg"
areal = 15.028989334576002
orientering = 270.0
helning = 90.0
u_verdi = 3.13
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn"
dybde = 2.35
over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]]
navn = "Kjellermur 25 cm uisolert"
type = "vegg"
areal = 0.33587572106361
orientering = 135.0
helning = 90.0
u_verdi = 3.39
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn"
dybde = 2.35
over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]]
navn = "Kjellermur 25 cm uisolert"
type = "vegg"
areal = 14.155249999999999
orientering = 0.0
helning = 90.0
u_verdi = 3.39
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn"
dybde = 2.35
over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]]
navn = "Kjellermur 25 cm uisolert"
type = "vegg"
areal = 7.481249999999999
orientering = 180.0
helning = 90.0
u_verdi = 3.39
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn"
dybde = 2.35
over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]]
navn = "Kjellermur 25 cm uisolert"
type = "vegg"
areal = 2.35212255
orientering = 270.0
helning = 90.0
u_verdi = 3.39
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn"
dybde = 2.35
over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert"
type = "vegg"
areal = 9.688000000000002
orientering = 90.0
helning = 90.0
u_verdi = 3.13
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn"
dybde = 2.35
over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[sone.flate]] [[sone.flate]]
navn = "Kjellergulv" navn = "Kjellergulv"
type = "gulv" type = "gulv"
areal = 78.9 areal = 78.88690000000003
orientering = 0.0
helning = 0.0
u_verdi = 3.85 u_verdi = 3.85
masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn" mot = "grunn"
omkrets = 40.0 dybde = 2.35
dybde = 2.2 over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
# Kjellervinduene. Termografien viser stort varmetap gjennom kjellervinduet.
[[sone.flate]] [[sone.flate]]
navn = "Kjellervinduer" navn = "Kjellermur 30 cm uisolert"
type = "vegg" type = "vegg"
areal = 1.8 areal = 21.215968003727976
orientering = 180 orientering = 0.0
u_verdi = 4.50 helning = 90.0
mot = "ute" u_verdi = 3.13
kommentar = "KONTROLLER: antall og størrelse; enkle glass i støpt karm" masseklasse = "tung"
masseplassering = "fordelt"
mot = "grunn"
dybde = 2.35
over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
# ── varmeforsyning ───────────────────────────────────────────────────── [[sone.flate]]
# Vannbårent radiatoranlegg med elektrokjel i kjelleren, supplert med vedovn. navn = "Kjellermur med treullsement-2"
type = "vegg"
areal = 0.731
orientering = 0.0
helning = 90.0
u_verdi = 0.46
masseklasse = "lett"
masseplassering = "fordelt"
mot = "grunn"
dybde = 2.35
over_terreng = 0.0
kommentar = "Kjeller, importert fra IFC"
[[oppvarming.kilde]] [grunn]
type = "vedovn"
andel_rom = 0.25
lukket = false
kommentar = "Åpent ildsted; energimerkeforskriften regner inntil 10 % av romoppvarmingen"
[[oppvarming.kilde]] [[vindu]]
type = "el_kjel" navn = "V-5 #3833"
andel_rom = 0.75 areal = 0.8400000000000001
andel_tappevann = 1.0 antall = 1.0
orientering = 180.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "W-01 #16334"
areal = 0.45
antall = 1.0
orientering = 0.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "W-01 #16303"
areal = 0.45
antall = 1.0
orientering = 0.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "V-3 #11303"
areal = 1.4300000000000002
antall = 1.0
orientering = 89.92370720902136
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "V-3 #11350"
areal = 1.4300000000000002
antall = 1.0
orientering = 89.92370720902136
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "V-2 #3207"
areal = 1.56
antall = 1.0
orientering = 0.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "V-2 #3882"
areal = 1.56
antall = 1.0
orientering = 180.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "V-4 #10945"
areal = 1.0450000000000002
antall = 1.0
orientering = 270.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "V-2 #3110"
areal = 1.56
antall = 1.0
orientering = 0.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "V-2 #3160"
areal = 1.56
antall = 1.0
orientering = 0.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "V-2 #3730"
areal = 1.56
antall = 1.0
orientering = 180.0
helning = 90.0
u_verdi = 2.4
dor = false
kommentar = "importert fra IFC"
[[vindu]]
navn = "VD-1 #3549"
areal = 2.9400000000000004
antall = 1.0
orientering = 89.92370720902136
helning = 90.0
u_verdi = 2.0
dor = true
kommentar = "importert fra IFC"
[[vindu]]
navn = "D-01 #3801"
areal = 1.9110000000000003
antall = 1.0
orientering = 180.0
helning = 90.0
u_verdi = 2.0
dor = true
kommentar = "importert fra IFC"
[[vindu]]
navn = "VD-2 #10977"
areal = 1.4625
antall = 1.0
orientering = 270.0
helning = 90.0
u_verdi = 2.0
dor = true
kommentar = "importert fra IFC"
[kuldebroer]
[infiltrasjon]
skjerming = "moderat"
[ventilasjon]
type = "naturlig"
[tappevann]
system = "aapent"
[oppvarming]
# Uisolerte varmerør gjennom den uoppvarmede kjelleren er hovedgrunnen til at kjelleren
# er varm. Termografirapporten anbefaler å isolere dem.
[distribusjon] [distribusjon]
type = "radiator" type = "punkt"
turtemperatur = 60
tap_andel = 0.25
roer_uoppvarmet = 1.0
# ── tiltak ───────────────────────────────────────────────────────────── [kjoling]
ingen = true
[[scenario]] [lufting]
navn = "Isolerte varmerør" ingen = false
beskrivelse = "Isolering av varmerørene gjennom kjelleren, som termografirapporten anbefaler. Rørtapet faller, men kjelleren blir kaldere, så noe av gevinsten spises opp av økt varmetap gjennom etasjeskilleren."
endringer.distribusjon = { tap_andel = 0.06 }
[[scenario]] [drift]
navn = "Isolerte kjellervegger"
beskrivelse = "Innvendig isolering av grunnmuren til U-verdi 0,35 W/(m²K)."
endringer.sone = [{ navn = "Kjeller", flate = [
{ navn = "Kjellermur 30 cm uisolert, nord", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, øst", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, sør", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, vest", u_verdi = 0.35 },
{ navn = "Kjellermur 25 cm uisolert", u_verdi = 0.35 },
] }]
[[scenario]]
navn = "Isolerte rør og kjellervegger"
beskrivelse = "Begge kjellertiltakene samlet, slik termografirapporten anbefaler dem."
endringer.distribusjon = { tap_andel = 0.06 }
endringer.sone = [{ navn = "Kjeller", flate = [
{ navn = "Kjellermur 30 cm uisolert, nord", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, øst", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, sør", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, vest", u_verdi = 0.35 },
{ navn = "Kjellermur 25 cm uisolert", u_verdi = 0.35 },
] }]
[[scenario]]
navn = "Nye vinduer"
beskrivelse = "Utskifting til trelags energiglass med U-verdi 0,8 W/(m²K)."
endringer.vindu = [
{ navn = "Vinduer nord", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer øst", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer vest", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Balkongdør sør", u_verdi = 1.20 },
{ navn = "Verandadør øst", u_verdi = 1.20 },
]
[[scenario]]
navn = "Etterisolert klimaskjerm"
beskrivelse = "Etterisolering av yttervegger til 20 cm og loft til 30 cm, med tetting av luftlekkasjer."
endringer.flate = [
{ navn = "Yttervegg nord", u_verdi = 0.22 },
{ navn = "Yttervegg øst", u_verdi = 0.22 },
{ navn = "Yttervegg sør", u_verdi = 0.22 },
{ navn = "Yttervegg vest", u_verdi = 0.22 },
{ navn = "Skråtak", u_verdi = 0.18 },
{ navn = "Himling mot kaldt loft", u_verdi = 0.15 },
]
endringer.infiltrasjon = { n50 = 4.0 }
endringer.kuldebroer = { normalisert = 0.10 }
[[scenario]]
navn = "Væske-vann-varmepumpe"
beskrivelse = "Bergvarmepumpe på det vannbårne anlegget, med elektrokjel som spisslast."
endringer.oppvarming.kilde = [
{ type = "vedovn", slett = true },
{ type = "vaeske_vann_vp", andel_rom = 0.9, andel_tappevann = 0.8, turtemperatur = 55 },
{ type = "el_kjel", andel_rom = 0.1, andel_tappevann = 0.2 },
]
[[scenario]]
navn = "Full pakke"
beskrivelse = "Alle tiltakene samlet: isolerte rør og kjellervegger, nye vinduer, etterisolert klimaskjerm, balansert ventilasjon med varmegjenvinning og bergvarmepumpe."
endringer.distribusjon = { tap_andel = 0.06, turtemperatur = 45 }
endringer.infiltrasjon = { n50 = 2.5 }
endringer.kuldebroer = { normalisert = 0.09 }
endringer.ventilasjon = { type = "balansert", gjenvinning = 0.80, sfp = 1.5 }
endringer.flate = [
{ navn = "Yttervegg nord", u_verdi = 0.22 },
{ navn = "Yttervegg øst", u_verdi = 0.22 },
{ navn = "Yttervegg sør", u_verdi = 0.22 },
{ navn = "Yttervegg vest", u_verdi = 0.22 },
{ navn = "Skråtak", u_verdi = 0.18 },
{ navn = "Himling mot kaldt loft", u_verdi = 0.15 },
{ navn = "Etasjeskiller mot kjeller", u_verdi = 0.18 },
]
endringer.vindu = [
{ navn = "Vinduer nord", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer øst", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer vest", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Balkongdør sør", u_verdi = 1.20 },
{ navn = "Verandadør øst", u_verdi = 1.20 },
]
endringer.sone = [{ navn = "Kjeller", flate = [
{ navn = "Kjellermur 30 cm uisolert, nord", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, øst", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, sør", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, vest", u_verdi = 0.35 },
{ navn = "Kjellermur 25 cm uisolert", u_verdi = 0.35 },
] }]
endringer.oppvarming.kilde = [
{ type = "vedovn", slett = true },
{ type = "vaeske_vann_vp", andel_rom = 0.9, andel_tappevann = 0.8, turtemperatur = 45 },
{ type = "el_kjel", andel_rom = 0.1, andel_tappevann = 0.2 },
]
+618 -30
View File
@@ -47,33 +47,65 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
let egenskaper = samle_egenskaper(&fil); let egenskaper = samle_egenskaper(&fil);
let mengder = samle_mengder(&fil); let mengder = samle_mengder(&fil);
let (vindu_vert, dor_vert) = samle_apninger(&fil); let (vindu_vert, dor_vert) = samle_apninger(&fil);
let (etasjer, etasje_for) = les_etasjer(&fil, skala);
// ── oppvarmet areal fra rommene ───────────────────────────────── // ── oppvarmet areal fra rommene ─────────────────────────────────
// Rom i en etasje under terreng hører til kjelleren og ikke til oppvarmet BRA.
let mut bra = 0.0; let mut bra = 0.0;
let mut volum = 0.0; let mut volum = 0.0;
let mut kjellerareal = 0.0;
let mut kjellervolum = 0.0;
let mut rom: Vec<(String, f64)> = Vec::new(); let mut rom: Vec<(String, f64)> = Vec::new();
let mut kjellerrom: Vec<(String, f64)> = Vec::new();
let mut mistenkelige: Vec<String> = Vec::new();
for (r, e) in fil.av_type("IFCSPACE") { for (r, e) in fil.av_type("IFCSPACE") {
let navn = romnavn(e); let navn = romnavn(e);
let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0); let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0);
// Rom som beskriver bebygget areal eller bruttoareal er ikke gulvareal. // Rom som beskriver bebygget areal eller bruttoareal er ikke gulvareal.
let maalerom = matches!(navn.to_uppercase().as_str(), n if n.starts_with("BYA") || n.starts_with("BTA")); let maalerom = matches!(navn.to_uppercase().as_str(), n if n.starts_with("BYA") || n.starts_with("BTA"));
if areal > 0.0 && !maalerom { if areal <= 0.0 || maalerom {
rom.push((navn, areal)); continue;
bra += areal; }
volum += mengder let romvolum = mengder
.get(&r) .get(&r)
.and_then(|m| m.get("NetVolume").or_else(|| m.get("GrossVolume")).copied()) .and_then(|m| m.get("NetVolume").or_else(|| m.get("GrossVolume")).copied())
.unwrap_or(0.0); .unwrap_or(0.0);
let under = etasje_for
.get(&r)
.and_then(|i| etasjer.get(*i))
.map(|e| e.under_terreng)
.unwrap_or(false);
if under {
kjellerrom.push((navn, areal));
kjellerareal += areal;
kjellervolum += romvolum;
} else {
let lavt = navn.to_lowercase();
if ["kott", "bod", "loft", "garasje", "carport", "uoppvarmet"]
.iter()
.any(|o| lavt.contains(o))
{
mistenkelige.push(format!("{navn} {areal:.1}"));
}
rom.push((navn, areal));
bra += areal;
volum += romvolum;
} }
} }
rom.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); rom.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
if !rom.is_empty() { if !rom.is_empty() {
let liste: Vec<String> = rom.iter().map(|(n, a)| format!("{n} {a:.1}")).collect(); let liste: Vec<String> = rom.iter().map(|(n, a)| format!("{n} {a:.1}")).collect();
merknader.push(format!( merknader.push(format!(
"Oppvarmet BRA er satt til summen av alle rom, {bra:.1} m². IFC sier ikke hvilke rom som er oppvarmet, så trekk fra kjeller, kott, loft og garasje. Rom [m²]: {}.", "Oppvarmet BRA er satt til {bra:.1} m², summen av rommene i etasjene over terreng. Rom [m²]: {}.",
liste.join(", ") liste.join(", ")
)); ));
} }
if !mistenkelige.is_empty() {
merknader.push(format!(
"Disse rommene er regnet som oppvarmet, men navnet tyder på at de ikke er det: {}. Trekk dem fra oppvarmet_bra om de står uoppvarmet.",
mistenkelige.join(", ")
));
}
// ── plassering og geometri for alle bygningsdeler ─────────────── // ── plassering og geometri for alle bygningsdeler ───────────────
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect(); let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
@@ -89,6 +121,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
nr: r, nr: r,
typ: e.typ.clone(), typ: e.typ.clone(),
navn: navn_av(e, r, standardnavn_for(&e.typ)), navn: navn_av(e, r, standardnavn_for(&e.typ)),
etasje: etasje_for.get(&r).copied(),
ytre: er_ytre(&egenskaper, r), ytre: er_ytre(&egenskaper, r),
u_verdi: u_verdi(&egenskaper, r), u_verdi: u_verdi(&egenskaper, r),
plass, plass,
@@ -123,14 +156,42 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
cy /= n; cy /= n;
} }
// ── kjeller som egen sone ───────────────────────────────────────
// Finnes det en etasje under terreng med rom, er den nesten alltid uoppvarmet.
// Bygningsdelene der hører til kjellerens klimaskjerm, og etasjeskilleren over blir
// grensen mellom den oppvarmede delen og sonen.
let kjelleretasje = (!kjellerrom.is_empty())
.then(|| etasjer.iter().position(|e| e.under_terreng))
.flatten();
let kjellernavn = kjelleretasje
.and_then(|i| etasjer.get(i))
.map(|e| e.navn.clone())
.unwrap_or_default();
// Etasjehøyden gir hvor dypt kjelleren ligger.
let kjellerdybde = kjelleretasje
.and_then(|i| {
let denne = etasjer.get(i)?.hoyde;
let over = etasjer.get(i + 1).map(|e| e.hoyde).unwrap_or(denne + 2.4);
Some(over - denne)
})
.unwrap_or(0.0);
// Den laveste etasjen over terreng, der etasjeskilleren mot kjelleren ligger.
let forste_over = etasjer.iter().position(|e| !e.under_terreng);
// ── klimaskjerm ───────────────────────────────────────────────── // ── klimaskjerm ─────────────────────────────────────────────────
let mut kjellerflater: Vec<Flate> = Vec::new();
let mut flater: Vec<Flate> = Vec::new(); let mut flater: Vec<Flate> = Vec::new();
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new(); let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
let mut uten_uverdi = 0usize; let mut uten_uverdi = 0usize;
let mut uten_geometri = 0usize; let mut uten_geometri = 0usize;
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new(); let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
// Laveste gulvnivå brukes til å skille dekke mot grunn fra etasjeskillere. // Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
let etasjekontekst = Etasjekontekst {
nederste: (!etasjer.is_empty()).then_some(0),
kjeller: kjelleretasje,
forste_over,
};
let laveste = deler let laveste = deler
.iter() .iter()
.filter(|d| PLATETYPER.contains(&d.typ.as_str())) .filter(|d| PLATETYPER.contains(&d.typ.as_str()))
@@ -147,14 +208,30 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
uten_geometri += 1; uten_geometri += 1;
continue; continue;
} }
let Some((typ, mot)) = klassifiser(d, laveste, helning) else { let i_kjeller = kjelleretasje.is_some() && d.etasje == kjelleretasje;
let Some((typ, mot)) = klassifiser(d, laveste, helning, &etasjekontekst) else {
utelatt.push((d.navn.clone(), d.areal, d.u_verdi)); utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
continue; continue;
}; };
// En etasjeskiller i den laveste etasjen over terreng er grensen mot kjelleren.
let (mot, sone) = if kjelleretasje.is_some()
&& !i_kjeller
&& d.etasje == forste_over
&& typ == FlateType::Gulv
{
(Mot::Uoppvarmet, Some(kjellernavn.clone()))
} else {
(mot, None)
};
if d.u_verdi.is_none() { if d.u_verdi.is_none() {
uten_uverdi += 1; uten_uverdi += 1;
} }
flater.push(Flate { let mal = if i_kjeller {
&mut kjellerflater
} else {
&mut flater
};
mal.push(Flate {
navn: d.navn.clone(), navn: d.navn.clone(),
typ, typ,
areal: d.areal, areal: d.areal,
@@ -162,20 +239,25 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
helning: Some(helning), helning: Some(helning),
u_verdi: d.u_verdi, u_verdi: d.u_verdi,
lag: None, lag: None,
masseklasse: None, masseklasse: masseklasse_av_materialer(&fil, d.nr),
masseplassering: Masseplassering::Fordelt, masseplassering: Masseplassering::Fordelt,
mot, mot,
b_faktor: None, b_faktor: None,
absorptans: None, absorptans: None,
omkrets: None, omkrets: None,
dybde: None, dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde),
over_terreng: None, over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(0.0),
sone: None, sone,
kommentar: Some("importert fra IFC".into()), kommentar: d
.etasje
.and_then(|i| etasjer.get(i))
.map(|e| format!("{}, importert fra IFC", e.navn))
.or_else(|| Some("importert fra IFC".into())),
}); });
} }
slaa_sammen(&mut flater); slaa_sammen(&mut flater);
slaa_sammen(&mut kjellerflater);
// ── vinduer og dører ──────────────────────────────────────────── // ── vinduer og dører ────────────────────────────────────────────
let mut vinduer: Vec<Vindu> = Vec::new(); let mut vinduer: Vec<Vindu> = Vec::new();
@@ -310,6 +392,21 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
.into(), .into(),
); );
let (data, funnet) = les_byggdata(&fil, &egenskaper);
if !funnet.is_empty() {
merknader.push(format!(
"Modellen har egenskaper for drift og forsyning, og disse er tatt i bruk: {}.",
funnet.join(", ")
));
} else {
merknader.push(
"Modellen har ingen egenskaper for byggeår, tetthet, ventilasjon eller varmekilde. \
Festes et postformat med slike felter på bygningen i BIM-verktøyet og knyttes til et \
IFC-egenskapssett, kommer de med hit. Se «Egenskaper enok leser» i enoks dokumentasjon."
.into(),
);
}
let navn = fil let navn = fil
.av_type("IFCBUILDING") .av_type("IFCBUILDING")
.next() .next()
@@ -317,12 +414,71 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
.filter(|s| !s.is_empty() && s != "Default Building") .filter(|s| !s.is_empty() && s != "Default Building")
.unwrap_or_else(|| standardnavn.to_string()); .unwrap_or_else(|| standardnavn.to_string());
// Kjellersonen settes sammen av flatene i etasjen under terreng.
let mut soner: Vec<Sone> = Vec::new();
if let Some(i) = kjelleretasje {
if !kjellerflater.is_empty() {
let volum = if kjellervolum > 0.0 {
kjellervolum
} else {
kjellerareal * kjellerdybde.max(2.0)
};
let rom: Vec<String> = kjellerrom
.iter()
.map(|(n, a)| format!("{n} {a:.1}"))
.collect();
soner.push(Sone {
navn: kjellernavn.clone(),
volum: (volum > 0.0).then_some(volum),
luftveksling: None,
flater: kjellerflater,
andel_distribusjonstap: None,
andel_tappevannstap: None,
fast_effekt: 0.0,
kommentar: Some(format!(
"Etasje «{}» ligger {:.2} m under referansenivået. Rom [m²]: {}",
etasjer[i].navn,
-etasjer[i].hoyde,
rom.join(", ")
)),
});
merknader.push(format!(
"Etasjen «{}» ligger under terreng og er satt opp som en uoppvarmet sone med {:.0} m² gulv. \
Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Står en del av \
grunnmuren over bakken, sett `over_terreng` i meter på veggene; utslaget er lite, om lag \
én prosent per meter.",
etasjer[i].navn, kjellerareal
));
merknader.push("Er kjelleren oppvarmet likevel, slett [[sone]] og legg flatene inn i klimaskjermen. \
Går det uisolerte varmerør der, sett `andel_distribusjonstap = 1.0` på sonen.".to_string());
}
}
if let Some((lat, lon)) = posisjon(&fil) {
merknader.push(format!(
"Modellen oppgir posisjon {lat:.4}° nord, {lon:.4}° øst. Kontroller at den stemmer, og sett \
`kommune` under [klima] deretter."
));
} else {
merknader.push(
"Modellen oppgir ingen posisjon. Settes prosjektstedet i Vectorworks, kommer det med som \
IfcSite og kan brukes til å velge klimadata."
.into(),
);
}
let bygning = Bygning { let bygning = Bygning {
bygning: BygningInfo { bygning: BygningInfo {
navn, navn,
kategori: Kategori::Smaahus, kategori: data
byggeaar: 2020, .kategori
oppvarmet_bra: bra, .as_deref()
.and_then(kategori_av_tekst)
.unwrap_or(Kategori::Smaahus),
byggeaar: data
.byggeaar
.unwrap_or(crate::modell::kontroll::IMPORTERT_BYGGEAAR),
oppvarmet_bra: data.oppvarmet_bra.unwrap_or(bra),
oppvarmet_volum: (volum > 0.0).then_some(volum), oppvarmet_volum: (volum > 0.0).then_some(volum),
antall_boenheter: 1, antall_boenheter: 1,
bygningens_oppvarmede_bra: None, bygningens_oppvarmede_bra: None,
@@ -332,16 +488,51 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
energiraadgiver: None, energiraadgiver: None,
oppdragsgiver: None, oppdragsgiver: None,
}, },
klima: Klima::default(), klima: Klima {
kommune: data.kommune.clone(),
..Klima::default()
},
flate: flater, flate: flater,
sone: Vec::new(), sone: soner,
grunn: Grunn::default(), grunn: Grunn::default(),
vindu: vinduer, vindu: vinduer,
kuldebroer: Kuldebroer::default(), kuldebroer: Kuldebroer::default(),
infiltrasjon: Infiltrasjon::default(), infiltrasjon: Infiltrasjon {
ventilasjon: Ventilasjon::default(), n50: data.n50,
..Infiltrasjon::default()
},
ventilasjon: Ventilasjon {
typ: data
.ventilasjon
.as_deref()
.and_then(ventilasjon_av_tekst)
.unwrap_or_default(),
gjenvinning: data
.gjenvinning
.map(|v| if v > 1.0 { v / 100.0 } else { v }),
..Ventilasjon::default()
},
tappevann: Tappevann::default(), tappevann: Tappevann::default(),
oppvarming: Oppvarming::default(), oppvarming: Oppvarming {
kilde: data
.varmekilde
.as_deref()
.and_then(varmekilde_av_tekst)
.map(|t| {
vec![Varmekilde {
typ: t,
andel_rom: 1.0,
andel_ventilasjon: None,
andel_tappevann: 1.0,
spf: None,
turtemperatur: None,
lukket: true,
kommentar: Some("fra egenskap i IFC-modellen".into()),
}]
})
.unwrap_or_default(),
..Oppvarming::default()
},
distribusjon: Distribusjon::default(), distribusjon: Distribusjon::default(),
solcelle: None, solcelle: None,
kjoling: Kjoling::default(), kjoling: Kjoling::default(),
@@ -393,11 +584,290 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
*flater = samlet; *flater = samlet;
} }
/// En etasje i modellen, med navn og høyde over byggets referansenivå.
#[derive(Clone, Debug)]
pub struct Etasje {
pub navn: String,
/// Høyde over byggets referansenivå, m. Negativ for kjeller.
pub hoyde: f64,
/// Ligger etasjen under terreng, slik navn eller høyde tyder på?
pub under_terreng: bool,
}
/// Leser etasjene og hvilken etasje hver bygningsdel og hvert rom hører til.
///
/// Etasjeinndelingen er den sikreste opplysningen en BIM-modell har om hva som er hva.
/// En etasje som heter «Kjeller» og ligger under referansenivået er nesten alltid
/// uoppvarmet, og bygningsdelene der hører til kjellerens klimaskjerm og ikke husets.
fn les_etasjer(fil: &Fil, skala: f64) -> (Vec<Etasje>, HashMap<u64, usize>) {
let mut etasjer: Vec<(u64, Etasje)> = fil
.av_type("IFCBUILDINGSTOREY")
.map(|(r, e)| {
let navn = e
.navn()
.filter(|s| !s.is_empty())
.unwrap_or("etasje")
.to_string();
let hoyde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
let lavt = navn.to_lowercase();
let under_terreng = hoyde < -0.5
|| ["kjeller", "basement", "souterreng", "underetasje"]
.iter()
.any(|o| lavt.contains(o));
(
r,
Etasje {
navn,
hoyde,
under_terreng,
},
)
})
.collect();
etasjer.sort_by(|a, b| {
a.1.hoyde
.partial_cmp(&b.1.hoyde)
.unwrap_or(std::cmp::Ordering::Equal)
});
let indeks: HashMap<u64, usize> = etasjer
.iter()
.enumerate()
.map(|(i, (r, _))| (*r, i))
.collect();
let mut for_element: HashMap<u64, usize> = HashMap::new();
// Bygningsdeler knyttes til etasjen med IfcRelContainedInSpatialStructure.
for (_, rel) in fil.av_type("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
let Some(etasje) = rel
.arg(5)
.and_then(|a| a.som_ref())
.and_then(|r| indeks.get(&r))
else {
continue;
};
for e in rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]) {
if let Some(r) = e.som_ref() {
for_element.insert(r, *etasje);
}
}
}
// Rom knyttes til etasjen med IfcRelAggregates.
for (_, rel) in fil.av_type("IFCRELAGGREGATES") {
let Some(etasje) = rel
.arg(4)
.and_then(|a| a.som_ref())
.and_then(|r| indeks.get(&r))
else {
continue;
};
for e in rel.arg(5).map(|a| a.som_liste()).unwrap_or(&[]) {
if let Some(r) = e.som_ref() {
for_element.insert(r, *etasje);
}
}
}
(etasjer.into_iter().map(|(_, e)| e).collect(), for_element)
}
/// Masseklasse ut fra materialnavnene i konstruksjonen.
///
/// IFC-filer fra Vectorworks oppgir materialene som andeler uten tykkelse, så U-verdien
/// kan ikke regnes av dem. Navnene sier likevel nok om massen: betong og tegl er tungt,
/// bindingsverk og isolasjon er lett.
fn masseklasse_av_materialer(fil: &Fil, element: u64) -> Option<Masseklasse> {
let mut navn: Vec<String> = Vec::new();
for (_, rel) in fil.av_type("IFCRELASSOCIATESMATERIAL") {
let treff = rel
.arg(4)
.map(|a| a.som_liste())
.unwrap_or(&[])
.iter()
.any(|o| o.som_ref() == Some(element));
if !treff {
continue;
}
let Some(m) = rel
.arg(5)
.and_then(|a| a.som_ref())
.and_then(|r| fil.hent(r))
else {
continue;
};
samle_materialnavn(fil, m, &mut navn, 0);
}
if navn.is_empty() {
return None;
}
let tekst = navn.join(" ").to_lowercase();
let tungt = [
"concrete", "betong", "brick", "tegl", "masonry", "mur", "stone", "stein", "screed",
];
let lett = [
"insulat",
"isolasjon",
"timber",
"wood",
"tre",
"panel",
"gypsum",
"gips",
"air",
];
let t = tungt.iter().filter(|o| tekst.contains(**o)).count();
let l = lett.iter().filter(|o| tekst.contains(**o)).count();
Some(match (t, l) {
(0, 0) => return None,
(t, l) if t > l => Masseklasse::Tung,
(t, l) if t == l && t > 0 => Masseklasse::Middels,
_ => Masseklasse::Lett,
})
}
fn samle_materialnavn(fil: &Fil, e: &Entitet, ut: &mut Vec<String>, dybde: u32) {
if dybde > 4 {
return;
}
if e.typ == "IFCMATERIAL" {
if let Some(n) = e.arg(0).and_then(|a| a.som_tekst()) {
ut.push(n.to_string());
}
return;
}
for a in &e.args {
match a {
Arg::Ref(r) => {
if let Some(n) = fil.hent(*r) {
samle_materialnavn(fil, n, ut, dybde + 1);
}
}
Arg::Liste(l) => {
for x in l {
if let Some(n) = x.som_ref().and_then(|r| fil.hent(r)) {
samle_materialnavn(fil, n, ut, dybde + 1);
}
}
}
_ => {}
}
}
}
/// Opplysninger om drift og forsyning som IFC ikke har noen fast plass til.
///
/// IFC beskriver geometri og konstruksjon, men ikke byggeår, tetthet, ventilasjon eller
/// hvordan huset varmes opp. Det er nettopp de opplysningene som skiller en god
/// energiberegning fra en middelmådig. De kan likevel følge med modellen: i Vectorworks
/// lager man et postformat, fester det på bygningen og knytter det til et IFC-egenskapssett.
/// Feltnavnene under leses fra et hvilket som helst egenskapssett på `IfcBuilding`,
/// `IfcSite` eller `IfcProject`, på norsk eller engelsk.
#[derive(Default, Debug)]
pub struct Byggdata {
pub byggeaar: Option<i32>,
pub kategori: Option<String>,
pub kommune: Option<String>,
pub n50: Option<f64>,
pub ventilasjon: Option<String>,
pub gjenvinning: Option<f64>,
pub varmekilde: Option<String>,
pub oppvarmet_bra: Option<f64>,
}
/// Leser `Byggdata` fra egenskapssettene på bygningen, tomta eller prosjektet.
fn les_byggdata(fil: &Fil, egenskaper: &Egenskaper) -> (Byggdata, Vec<String>) {
let mut d = Byggdata::default();
let mut funnet = Vec::new();
let baerere: Vec<u64> = ["IFCBUILDING", "IFCSITE", "IFCPROJECT"]
.iter()
.flat_map(|t| fil.av_type(t).map(|(r, _)| r).collect::<Vec<_>>())
.collect();
let mut hent = |navn: &[&str]| -> Option<Arg> {
for b in &baerere {
let Some(sett) = egenskaper.get(b) else {
continue;
};
for n in navn {
if let Some((nokkel, v)) = sett.iter().find(|(k, _)| {
k.eq_ignore_ascii_case(n) || k.replace(' ', "").eq_ignore_ascii_case(n)
}) {
funnet.push(nokkel.clone());
return Some(v.clone());
}
}
}
None
};
let tall = |a: Option<Arg>| -> Option<f64> {
let a = a?;
a.som_tall().or_else(|| {
a.som_tekst()
.and_then(|s| s.replace(',', ".").trim().parse().ok())
})
};
let tekst = |a: Option<Arg>| -> Option<String> {
a.and_then(|x| x.som_tekst().map(|s| s.trim().to_string()))
.filter(|s| !s.is_empty())
};
d.byggeaar = tall(hent(&[
"Byggeaar",
"Byggeår",
"YearOfConstruction",
"ConstructionYear",
]))
.map(|v| v as i32);
d.kategori = tekst(hent(&["Bygningskategori", "Kategori", "BuildingCategory"]));
d.kommune = tekst(hent(&["Kommune", "Municipality", "Klimasted"]));
d.n50 = tall(hent(&[
"Lekkasjetall",
"n50",
"AirPermeability",
"AirTightness",
]));
d.ventilasjon = tekst(hent(&[
"Ventilasjonstype",
"Ventilasjon",
"VentilationType",
]));
d.gjenvinning = tall(hent(&[
"Varmegjenvinning",
"Gjenvinning",
"HeatRecoveryEfficiency",
"TemperaturVirkningsgrad",
]));
d.varmekilde = tekst(hent(&[
"Varmekilde",
"Oppvarming",
"HeatingSource",
"HeatSource",
]));
d.oppvarmet_bra = tall(hent(&["OppvarmetBRA", "Oppvarmet BRA", "HeatedFloorArea"]));
(d, funnet)
}
/// Byggets posisjon fra `IfcSite`, som grader nord og øst.
fn posisjon(fil: &Fil) -> Option<(f64, f64)> {
let (_, e) = fil.av_type("IFCSITE").next()?;
let grad = |a: Option<&Arg>| -> Option<f64> {
let l = a?.som_liste();
if l.is_empty() {
return None;
}
let g = |i: usize| l.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
let verdi = g(0).abs() + g(1).abs() / 60.0 + g(2).abs() / 3600.0 + g(3).abs() / 3.6e9;
Some(if g(0) < 0.0 { -verdi } else { verdi })
};
Some((grad(e.arg(9))?, grad(e.arg(10))?))
}
/// En bygningsdel slik den er lest av IFC-filen. /// En bygningsdel slik den er lest av IFC-filen.
struct Del { struct Del {
nr: u64, nr: u64,
typ: String, typ: String,
navn: String, navn: String,
etasje: Option<usize>,
ytre: Option<bool>, ytre: Option<bool>,
u_verdi: Option<f64>, u_verdi: Option<f64>,
plass: Option<(f64, f64, f64, f64, f64)>, plass: Option<(f64, f64, f64, f64, f64)>,
@@ -405,6 +875,72 @@ struct Del {
normal: Option<[f64; 3]>, normal: Option<[f64; 3]>,
} }
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
fn kategori_av_tekst(s: &str) -> Option<Kategori> {
let t = s.to_lowercase();
Some(match t.as_str() {
x if x.contains("småhus") || x.contains("smaahus") || x.contains("enebolig") => {
Kategori::Smaahus
}
x if x.contains("boligblokk") || x.contains("leilighet") => Kategori::Boligblokk,
x if x.contains("kontor") => Kategori::Kontorbygg,
x if x.contains("skole") => Kategori::Skolebygg,
x if x.contains("barnehage") => Kategori::Barnehage,
x if x.contains("hotell") => Kategori::Hotell,
x if x.contains("sykehus") => Kategori::Sykehus,
x if x.contains("sykehjem") => Kategori::Sykehjem,
x if x.contains("idrett") => Kategori::Idrettsbygg,
x if x.contains("forretning") || x.contains("butikk") => Kategori::Forretningsbygg,
x if x.contains("kultur") => Kategori::Kulturbygg,
x if x.contains("industri") || x.contains("verksted") => Kategori::Industribygg,
_ => return None,
})
}
fn ventilasjon_av_tekst(s: &str) -> Option<VentilasjonType> {
let t = s.to_lowercase();
Some(match t.as_str() {
x if x.contains("balansert") || x.contains("balanced") || x.contains("mekanisk til") => {
VentilasjonType::Balansert
}
x if x.contains("avtrekk") || x.contains("exhaust") => VentilasjonType::Avtrekk,
x if x.contains("naturlig") || x.contains("natural") => VentilasjonType::Naturlig,
_ => return None,
})
}
fn varmekilde_av_tekst(s: &str) -> Option<VarmekildeType> {
let t = s.to_lowercase();
Some(match t.as_str() {
x if x.contains("luft-luft") || x.contains("luft/luft") || x.contains("air-air") => {
VarmekildeType::LuftLuftVp
}
x if x.contains("luft-vann") || x.contains("luft/vann") => VarmekildeType::LuftVannVp,
x if x.contains("berg")
|| x.contains("væske")
|| x.contains("vaeske")
|| x.contains("ground") =>
{
VarmekildeType::VaeskeVannVp
}
x if x.contains("avtrekksvarmepumpe") => VarmekildeType::AvtrekkVp,
x if x.contains("vedovn") || x.contains("ved") || x.contains("wood") => {
VarmekildeType::Vedovn
}
x if x.contains("pellet") => VarmekildeType::Pellets,
x if x.contains("fjernvarme") || x.contains("district") => VarmekildeType::Fjernvarme,
x if x.contains("olje") => VarmekildeType::Olje,
x if x.contains("gass") || x.contains("gas") => VarmekildeType::Gass,
x if x.contains("elkjel") || x.contains("elektrokjel") || x.contains("kjel") => {
VarmekildeType::ElKjel
}
x if x.contains("elektr") || x.contains("panelovn") || x.contains("electric") => {
VarmekildeType::DirekteEl
}
_ => return None,
})
}
fn standardnavn_for(typ: &str) -> &'static str { fn standardnavn_for(typ: &str) -> &'static str {
if typ == "IFCROOF" { if typ == "IFCROOF" {
"Tak" "Tak"
@@ -479,7 +1015,22 @@ fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
} }
/// Avgjør om en bygningsdel hører til klimaskjermen, og hva den vender mot. /// Avgjør om en bygningsdel hører til klimaskjermen, og hva den vender mot.
fn klassifiser(d: &Del, laveste: f64, helning: f64) -> Option<(FlateType, Mot)> { /// Etasjeinndelingen som klassifiseringen trenger.
struct Etasjekontekst {
/// Nederste etasje i modellen.
nederste: Option<usize>,
/// Etasjen under terreng, dersom modellen har en.
kjeller: Option<usize>,
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
forste_over: Option<usize>,
}
fn klassifiser(
d: &Del,
laveste: f64,
helning: f64,
e: &Etasjekontekst,
) -> Option<(FlateType, Mot)> {
let navn = d.navn.to_lowercase(); let navn = d.navn.to_lowercase();
let grunnord = [ let grunnord = [
"kjellermur", "kjellermur",
@@ -502,17 +1053,26 @@ fn klassifiser(d: &Del, laveste: f64, helning: f64) -> Option<(FlateType, Mot)>
}, },
)); ));
} }
let nederst = d // Et utkraget dekke eller balkonggulv er merket som ytre og vender mot det fri.
.plass
.map(|p| (p.2 - laveste).abs() < 0.5)
.unwrap_or(false);
if nederst || navn.contains("grunn") || navn.contains("kjellergulv") {
return Some((FlateType::Gulv, Mot::Grunn));
}
// Etasjeskillere og balkongdekker er ikke klimaskjerm med mindre de er merket ytre.
if d.ytre == Some(true) { if d.ytre == Some(true) {
return Some((FlateType::Gulv, Mot::Ute)); return Some((FlateType::Gulv, Mot::Ute));
} }
// Mot grunnen bare i nederste etasje. Er etasjen ukjent, brukes høyden.
let nederst = match (d.etasje, e.nederste) {
(Some(a), Some(b)) => a == b,
_ => d
.plass
.map(|p| (p.2 - laveste).abs() < 0.5)
.unwrap_or(false),
};
if nederst || navn.contains("kjellergulv") {
return Some((FlateType::Gulv, Mot::Grunn));
}
// Dekket i laveste etasje over en kjeller er grensen mot den uoppvarmede sonen.
if e.kjeller.is_some() && d.etasje.is_some() && d.etasje == e.forste_over {
return Some((FlateType::Gulv, Mot::Uoppvarmet));
}
// Dekker lenger opp er etasjeskillere inne i bygningen.
return None; return None;
} }
@@ -912,6 +1472,34 @@ mod tester {
assert_eq!(ifcgeom::lengdeenhet(&fil), 0.001); assert_eq!(ifcgeom::lengdeenhet(&fil), 0.001);
} }
/// Et postformat festet på bygningen og eksportert som IFC-egenskapssett skal leses,
/// slik at byggeår, tetthet, ventilasjon og varmekilde kommer med fra modellen.
#[test]
fn egenskapssett_paa_bygningen_leses() {
let t = prove().replace(
"#300= IFCSPACE(",
"#400= IFCPROPERTYSINGLEVALUE('Byggeaar',$,IFCINTEGER(1948),$);\n\
#401= IFCPROPERTYSINGLEVALUE('Lekkasjetall',$,IFCREAL(8.0),$);\n\
#402= IFCPROPERTYSINGLEVALUE('Ventilasjonstype',$,IFCLABEL('Balansert'),$);\n\
#403= IFCPROPERTYSINGLEVALUE('Varmegjenvinning',$,IFCREAL(80.0),$);\n\
#404= IFCPROPERTYSINGLEVALUE('Varmekilde',$,IFCLABEL('Bergvarmepumpe'),$);\n\
#405= IFCPROPERTYSINGLEVALUE('Kommune',$,IFCLABEL('Indre \u{d8}stfold'),$);\n\
#406= IFCPROPERTYSET('ps2',$,'Enok',$,(#400,#401,#402,#403,#404,#405));\n\
#407= IFCRELDEFINESBYPROPERTIES('r4',$,$,$,(#10),#406);\n\
#300= IFCSPACE(",
);
let r = fra_tekst(&t, "prove").unwrap();
let b = &r.bygning;
assert_eq!(b.bygning.byggeaar, 1948);
assert_eq!(b.infiltrasjon.n50, Some(8.0));
assert_eq!(b.ventilasjon.typ, VentilasjonType::Balansert);
assert_eq!(b.ventilasjon.gjenvinning, Some(0.8));
assert_eq!(b.klima.kommune.as_deref(), Some("Indre Østfold"));
assert_eq!(b.oppvarming.kilde.len(), 1);
assert_eq!(b.oppvarming.kilde[0].typ, VarmekildeType::VaeskeVannVp);
assert!(r.merknader.iter().any(|m| m.contains("tatt i bruk")));
}
#[test] #[test]
fn vegg_uten_geometri_utelates() { fn vegg_uten_geometri_utelates() {
let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'"); let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'");
+6 -1
View File
@@ -152,7 +152,12 @@ fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> {
eprintln!("Rapport: {}", pdf.display()); eprintln!("Rapport: {}", pdf.display());
} }
for m in &resultat.merknader { // Manglene er allerede skrevet ut før beregningen; her kommer bare det som kom i tillegg.
let alt_sagt: Vec<String> = enok::modell::kontroll::kontroller(&bygning)
.into_iter()
.map(|m| m.melding)
.collect();
for m in resultat.merknader.iter().filter(|m| !alt_sagt.contains(m)) {
eprintln!(" ! {m}"); eprintln!(" ! {m}");
} }
skriv_sammendrag(&resultat, &scenarier); skriv_sammendrag(&resultat, &scenarier);