From 81544d73e6a33a0e99a9ef9af56ccc82790d35fc Mon Sep 17 00:00:00 2001 From: Bendik Lynghaug Date: Mon, 14 Sep 2026 22:36:54 +0200 Subject: [PATCH] Hent mye mer ut av IFC-modellen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 56 ++- bygninger/nordhusstykket-5.toml | 741 +++++++++++++++++--------------- src/inndata/ifc.rs | 650 ++++++++++++++++++++++++++-- src/main.rs | 7 +- 6 files changed, 1081 insertions(+), 377 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48d9ae2..ab2e799 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,7 +222,7 @@ dependencies = [ [[package]] name = "enok" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", "approx", diff --git a/Cargo.toml b/Cargo.toml index 7750d2f..947e414 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "enok" -version = "0.1.1" +version = "0.2.0" edition = "2021" description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)" license = "MIT" diff --git a/README.md b/README.md index 68c889f..ee26b05 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,58 @@ Ferdige eksempler ligger i `eksempler/`. | gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag | | 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 -ytre normal velges som den som peker bort fra bygningens tyngdepunkt, korrigert for prosjektets -sanne nord. Kontroller dem mot tegningen etter import. +### Hva enok henter ut av en IFC-modell -Ingen av formatene beskriver ventilasjon, lekkasjetall eller varmekilder. Importen skriver -merknader øverst i TOML-filen om hva som må fylles ut. +| Opplysning | Hvor den kommer fra | +| --- | --- | +| 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: diff --git a/bygninger/nordhusstykket-5.toml b/bygninger/nordhusstykket-5.toml index 7c0c40b..57d4a67 100644 --- a/bygninger/nordhusstykket-5.toml +++ b/bygninger/nordhusstykket-5.toml @@ -1,392 +1,457 @@ -# Nordhusstykket 5, 1825 Tomter — enebolig i tre fra 1948, 1,5 etasje på betong grunnmur. -# -# Geometri, U-verdier og himmelretninger er importert fra byggets IFC-modell -# (Vectorworks Architect 2026, «Nordhusstykket 5.ifc»). Arealene er regnet av geometrien, -# og oppføringene er slått sammen per konstruksjon og himmelretning. -# Vurderingene av isolasjon og lekkasjer er hentet fra termografirapporten av 10.3.2016. -# -# Verdier som ikke står i modellen og må kontrolleres på stedet er merket KONTROLLER. +# 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. +# 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. +# 30 bygningsdeler er utelatt fordi geometrien ikke ga noe areal. +# 17 bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok bruke typiske verdier for byggeåret. +# 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. +# 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] -navn = "Nordhusstykket 5" +navn = "nordhusstykket-5" kategori = "smaahus" -byggeaar = 1948 -# Oppvarmet BRA er summen av rommene i 1. og 2. etasje i IFC-modellen: -# stue 39,6 + kjøkken 22,6 + entré 10,6 + vindfang 1,2 = 74,0 m² i 1. etasje, -# tre soverom 33,9 + gang/trapperom 11,2 + toalett 0,9 = 46,0 m² i 2. etasje. -# 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" +byggeaar = 1950 +oppvarmet_bra = 139.533 +antall_boenheter = 1 +beskrivelse = "Importert fra IFC" [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]] -navn = "Yttervegg nord" -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" +navn = "Roof Slab" type = "tak" -areal = 130.1 -helning = 40 -u_verdi = 0.55 -masseklasse = "lett" -kommentar = "Areal fra IFC; U-verdi anslått etter termografirapporten" +areal = 130.11978717725776 +orientering = 0.0 +helning = 14.999984009080658 +masseklasse = "middels" +masseplassering = "fordelt" +mot = "ute" +kommentar = "importert fra IFC" [[flate]] -navn = "Himling mot kaldt loft" -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" +navn = "Etasjeskille" type = "gulv" -areal = 75.0 +areal = 77.04260758445601 +orientering = 0.0 +helning = 0.0 u_verdi = 0.31 +masseklasse = "lett" +masseplassering = "fordelt" mot = "uoppvarmet" sone = "Kjeller" -masseklasse = "middels" +kommentar = "1. etg., importert fra IFC" -# ── vinduer og dører ─────────────────────────────────────────────────── -# Areal og himmelretning fra IFC. KONTROLLER: modellen oppgir ingen U-verdi for vinduene. -# Termografirapporten beskriver dem som dårlig isolerte, og 2,8 W/(m²K) svarer til -# eldre tolagsglass. Med originale enkle glass ville verdien vært nærmere 5. +[[flate]] +navn = "Yttervegg panel ute og porøs huntonitt inne" +type = "vegg" +areal = 55.719888250487756 +orientering = 180.0 +helning = 90.0 +u_verdi = 0.93 +masseplassering = "fordelt" +mot = "ute" +kommentar = "2. etg., importert fra IFC" -[[vindu]] -navn = "Vinduer nord" -areal = 5.58 -orientering = 0 -u_verdi = 2.80 -g_verdi = 0.76 +[[flate]] +navn = "Yttervegg panel ute og porøs huntonitt inne" +type = "vegg" +areal = 59.50176490568849 +orientering = 270.0 +helning = 90.0 +u_verdi = 0.93 +masseklasse = "lett" +masseplassering = "fordelt" +mot = "ute" +kommentar = "1. etg., importert fra IFC" -[[vindu]] -navn = "Vinduer øst" -areal = 2.86 -orientering = 90 -u_verdi = 2.80 -g_verdi = 0.76 +[[flate]] +navn = "Yttervegg panel ute og porøs huntonitt inne" +type = "vegg" +areal = 41.833542336937 +orientering = 0.0 +helning = 90.0 +u_verdi = 0.93 +masseklasse = "lett" +masseplassering = "fordelt" +mot = "ute" +kommentar = "1. etg., importert fra IFC" -[[vindu]] -navn = "Vinduer sør" -areal = 3.96 -orientering = 180 -u_verdi = 2.80 -g_verdi = 0.76 +[[flate]] +navn = "Slab on Grade-100mm" +type = "gulv" +areal = 16.3663225 +orientering = 0.0 +helning = 0.0 +u_verdi = 3.51 +masseklasse = "tung" +masseplassering = "fordelt" +mot = "uoppvarmet" +sone = "Kjeller" +kommentar = "1. etg., importert fra IFC" -[[vindu]] -navn = "Vinduer vest" -areal = 1.05 -orientering = 270 -u_verdi = 2.80 -g_verdi = 0.76 +[[flate]] +navn = "Yttervegg panel ute og porøs huntonitt inne" +type = "vegg" +areal = 54.21918162153324 +orientering = 90.0 +helning = 90.0 +u_verdi = 0.93 +masseklasse = "lett" +masseplassering = "fordelt" +mot = "ute" +kommentar = "2. etg., importert fra IFC" -[[vindu]] -navn = "Inngangsdør med vindfang" -areal = 1.46 -orientering = 270 -u_verdi = 2.00 -dor = true - -[[vindu]] -navn = "Verandadør øst" -areal = 2.94 -orientering = 90 -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. +[[flate]] +navn = "Etasjeskille-loft" +type = "tak" +areal = 32.594422635 +orientering = 0.0 +helning = 0.0 +u_verdi = 0.45 +masseklasse = "lett" +masseplassering = "fordelt" +mot = "uoppvarmet" +kommentar = "2. etg., importert fra IFC" [[sone]] navn = "Kjeller" -volum = 165.0 -luftveksling = 0.4 -# Alt distribusjonstapet fra det vannbårne anlegget avgis her, og berederen står her. -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 +volum = 176.1231 +fast_effekt = 0.0 +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" [[sone.flate]] -navn = "Kjellermur 30 cm uisolert, øst" +navn = "Kjellermur med treullsement-2" type = "vegg" -areal = 9.7 -orientering = 90 -u_verdi = 3.13 +areal = 1.4834999999999998 +orientering = 90.0 +helning = 90.0 +u_verdi = 0.46 +masseklasse = "lett" +masseplassering = "fordelt" mot = "grunn" -dybde = 1.6 -over_terreng = 0.6 - -[[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 +dybde = 2.35 +over_terreng = 0.0 +kommentar = "Kjeller, importert fra IFC" [[sone.flate]] navn = "Kjellermur 25 cm uisolert" type = "vegg" -areal = 40.7 -orientering = 90 +areal = 16.33814305 +orientering = 90.0 +helning = 90.0 u_verdi = 3.39 +masseklasse = "tung" +masseplassering = "fordelt" mot = "grunn" -dybde = 1.6 -over_terreng = 0.6 +dybde = 2.35 +over_terreng = 0.0 +kommentar = "Kjeller, importert fra IFC" [[sone.flate]] -navn = "Kjellermur med treullsement" +navn = "Kjellermur 30 cm uisolert" type = "vegg" -areal = 2.2 -orientering = 90 -u_verdi = 0.46 +areal = 22.69398933457598 +orientering = 180.0 +helning = 90.0 +u_verdi = 3.13 +masseklasse = "tung" +masseplassering = "fordelt" mot = "grunn" -dybde = 1.6 -over_terreng = 0.6 +dybde = 2.35 +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]] navn = "Kjellergulv" type = "gulv" -areal = 78.9 +areal = 78.88690000000003 +orientering = 0.0 +helning = 0.0 u_verdi = 3.85 +masseklasse = "tung" +masseplassering = "fordelt" mot = "grunn" -omkrets = 40.0 -dybde = 2.2 +dybde = 2.35 +over_terreng = 0.0 +kommentar = "Kjeller, importert fra IFC" -# Kjellervinduene. Termografien viser stort varmetap gjennom kjellervinduet. [[sone.flate]] -navn = "Kjellervinduer" +navn = "Kjellermur 30 cm uisolert" type = "vegg" -areal = 1.8 -orientering = 180 -u_verdi = 4.50 -mot = "ute" -kommentar = "KONTROLLER: antall og størrelse; enkle glass i støpt karm" +areal = 21.215968003727976 +orientering = 0.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" -# ── varmeforsyning ───────────────────────────────────────────────────── -# Vannbårent radiatoranlegg med elektrokjel i kjelleren, supplert med vedovn. +[[sone.flate]] +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]] -type = "vedovn" -andel_rom = 0.25 -lukket = false -kommentar = "Åpent ildsted; energimerkeforskriften regner inntil 10 % av romoppvarmingen" +[grunn] -[[oppvarming.kilde]] -type = "el_kjel" -andel_rom = 0.75 -andel_tappevann = 1.0 +[[vindu]] +navn = "V-5 #3833" +areal = 0.8400000000000001 +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] -type = "radiator" -turtemperatur = 60 -tap_andel = 0.25 -roer_uoppvarmet = 1.0 +type = "punkt" -# ── tiltak ───────────────────────────────────────────────────────────── +[kjoling] +ingen = true -[[scenario]] -navn = "Isolerte varmerør" -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 } +[lufting] +ingen = false -[[scenario]] -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 }, -] +[drift] diff --git a/src/inndata/ifc.rs b/src/inndata/ifc.rs index ac79a62..0873d8b 100644 --- a/src/inndata/ifc.rs +++ b/src/inndata/ifc.rs @@ -47,33 +47,65 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { let egenskaper = samle_egenskaper(&fil); let mengder = samle_mengder(&fil); let (vindu_vert, dor_vert) = samle_apninger(&fil); + let (etasjer, etasje_for) = les_etasjer(&fil, skala); // ── 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 volum = 0.0; + let mut kjellerareal = 0.0; + let mut kjellervolum = 0.0; let mut rom: Vec<(String, f64)> = Vec::new(); + let mut kjellerrom: Vec<(String, f64)> = Vec::new(); + let mut mistenkelige: Vec = Vec::new(); for (r, e) in fil.av_type("IFCSPACE") { let navn = romnavn(e); let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0); // 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")); - if areal > 0.0 && !maalerom { + if areal <= 0.0 || maalerom { + continue; + } + let romvolum = mengder + .get(&r) + .and_then(|m| m.get("NetVolume").or_else(|| m.get("GrossVolume")).copied()) + .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} m²")); + } rom.push((navn, areal)); bra += areal; - volum += mengder - .get(&r) - .and_then(|m| m.get("NetVolume").or_else(|| m.get("GrossVolume")).copied()) - .unwrap_or(0.0); + volum += romvolum; } } rom.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); if !rom.is_empty() { let liste: Vec = rom.iter().map(|(n, a)| format!("{n} {a:.1}")).collect(); 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(", ") )); } + 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 ─────────────── let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect(); @@ -89,6 +121,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { nr: r, typ: e.typ.clone(), navn: navn_av(e, r, standardnavn_for(&e.typ)), + etasje: etasje_for.get(&r).copied(), ytre: er_ytre(&egenskaper, r), u_verdi: u_verdi(&egenskaper, r), plass, @@ -123,14 +156,42 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { 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 ───────────────────────────────────────────────── + let mut kjellerflater: Vec = Vec::new(); let mut flater: Vec = Vec::new(); let mut utelatt: Vec<(String, f64, Option)> = Vec::new(); let mut uten_uverdi = 0usize; let mut uten_geometri = 0usize; let mut asimut_for_vegg: HashMap = 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 .iter() .filter(|d| PLATETYPER.contains(&d.typ.as_str())) @@ -147,14 +208,30 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { uten_geometri += 1; 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)); 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() { uten_uverdi += 1; } - flater.push(Flate { + let mal = if i_kjeller { + &mut kjellerflater + } else { + &mut flater + }; + mal.push(Flate { navn: d.navn.clone(), typ, areal: d.areal, @@ -162,20 +239,25 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { helning: Some(helning), u_verdi: d.u_verdi, lag: None, - masseklasse: None, + masseklasse: masseklasse_av_materialer(&fil, d.nr), masseplassering: Masseplassering::Fordelt, mot, b_faktor: None, absorptans: None, omkrets: None, - dybde: None, - over_terreng: None, - sone: None, - kommentar: Some("importert fra IFC".into()), + dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde), + over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(0.0), + sone, + 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 kjellerflater); // ── vinduer og dører ──────────────────────────────────────────── let mut vinduer: Vec = Vec::new(); @@ -310,6 +392,21 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { .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 .av_type("IFCBUILDING") .next() @@ -317,12 +414,71 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { .filter(|s| !s.is_empty() && s != "Default Building") .unwrap_or_else(|| standardnavn.to_string()); + // Kjellersonen settes sammen av flatene i etasjen under terreng. + let mut soner: Vec = 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 = 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 { bygning: BygningInfo { navn, - kategori: Kategori::Smaahus, - byggeaar: 2020, - oppvarmet_bra: bra, + kategori: data + .kategori + .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), antall_boenheter: 1, bygningens_oppvarmede_bra: None, @@ -332,16 +488,51 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { energiraadgiver: None, oppdragsgiver: None, }, - klima: Klima::default(), + klima: Klima { + kommune: data.kommune.clone(), + ..Klima::default() + }, flate: flater, - sone: Vec::new(), + sone: soner, grunn: Grunn::default(), vindu: vinduer, kuldebroer: Kuldebroer::default(), - infiltrasjon: Infiltrasjon::default(), - ventilasjon: Ventilasjon::default(), + infiltrasjon: Infiltrasjon { + 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(), - 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(), solcelle: None, kjoling: Kjoling::default(), @@ -393,11 +584,290 @@ fn slaa_sammen(flater: &mut Vec) { *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, HashMap) { + 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 = etasjer + .iter() + .enumerate() + .map(|(i, (r, _))| (*r, i)) + .collect(); + let mut for_element: HashMap = 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 { + let mut navn: Vec = 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, 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, + pub kategori: Option, + pub kommune: Option, + pub n50: Option, + pub ventilasjon: Option, + pub gjenvinning: Option, + pub varmekilde: Option, + pub oppvarmet_bra: Option, +} + +/// Leser `Byggdata` fra egenskapssettene på bygningen, tomta eller prosjektet. +fn les_byggdata(fil: &Fil, egenskaper: &Egenskaper) -> (Byggdata, Vec) { + let mut d = Byggdata::default(); + let mut funnet = Vec::new(); + let baerere: Vec = ["IFCBUILDING", "IFCSITE", "IFCPROJECT"] + .iter() + .flat_map(|t| fil.av_type(t).map(|(r, _)| r).collect::>()) + .collect(); + + let mut hent = |navn: &[&str]| -> Option { + 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| -> Option { + let a = a?; + a.som_tall().or_else(|| { + a.som_tekst() + .and_then(|s| s.replace(',', ".").trim().parse().ok()) + }) + }; + let tekst = |a: Option| -> Option { + 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 { + 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. struct Del { nr: u64, typ: String, navn: String, + etasje: Option, ytre: Option, u_verdi: Option, plass: Option<(f64, f64, f64, f64, f64)>, @@ -405,6 +875,72 @@ struct Del { normal: Option<[f64; 3]>, } +/// Oversetter en fritekstverdi fra modellen til en bygningskategori. +fn kategori_av_tekst(s: &str) -> Option { + 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 { + 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 { + 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 { if typ == "IFCROOF" { "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. -fn klassifiser(d: &Del, laveste: f64, helning: f64) -> Option<(FlateType, Mot)> { +/// Etasjeinndelingen som klassifiseringen trenger. +struct Etasjekontekst { + /// Nederste etasje i modellen. + nederste: Option, + /// Etasjen under terreng, dersom modellen har en. + kjeller: Option, + /// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger. + forste_over: Option, +} + +fn klassifiser( + d: &Del, + laveste: f64, + helning: f64, + e: &Etasjekontekst, +) -> Option<(FlateType, Mot)> { let navn = d.navn.to_lowercase(); let grunnord = [ "kjellermur", @@ -502,17 +1053,26 @@ fn klassifiser(d: &Del, laveste: f64, helning: f64) -> Option<(FlateType, Mot)> }, )); } - let nederst = d - .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. + // Et utkraget dekke eller balkonggulv er merket som ytre og vender mot det fri. if d.ytre == Some(true) { 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; } @@ -912,6 +1472,34 @@ mod tester { 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] fn vegg_uten_geometri_utelates() { let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'"); diff --git a/src/main.rs b/src/main.rs index aab1006..ecb9712 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,7 +152,12 @@ fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> { 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 = 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}"); } skriv_sammendrag(&resultat, &scenarier);