Geometri fra IFC, grunnmodell etter ISO 13370 og uoppvarmede soner
test / test (push) Failing after 3s
test / test (push) Failing after 3s
Importen er prøvd mot en virkelig IFC-fil fra Vectorworks Architect 2026 og avdekket tre feil og ett hull. IFC-import - Lengdeenheten hentes nå gjennom prosjektets enhetsoppsett. Filen inneholder to lengdeenheter, og oppslaget traff tilfeldig den ene. - Arealer regnes av geometrien, siden modellen ikke har mengder for vegger og dekker. Trekantene grupperes etter retning, og den største gruppen er delens hovedflate. Metoden gir riktig areal også når vinduer er skåret ut av veggen. - Materialsjikt eksporteres som hver sin geometridel. Deler som ligger i samme plan og overlapper hverandre telles én gang, ellers ble en vegg talt sju ganger. - Himmelretningen følger flatenormalen i stedet for et anslag ut fra tyngdepunktet. - Flater med samme konstruksjon og himmelretning slås sammen, og rommene listes med areal, siden IFC ikke sier hvilke som er oppvarmet. Grunnen og kjelleren - Flater mot grunnen regnes etter NS-EN ISO 13370. En kjellervegg deles i en del over terreng og en nedgravd del, etter høydene `over_terreng` og `dybde`. - Uoppvarmede soner får egen temperatur time for time etter NS-EN ISO 13789, med varmetilskudd fra distribusjonstap og beredere som står der. Uisolerte varmerør i en kjeller teller dermed både som tap og som varme som holder etasjeskilleren varm. - Varmetapsbudsjettet oppgir konstruksjonens egen U-verdi, slik NS 3031 Tabell 3 og energikravene i TEK er formulert, mens timesberegningen bruker grunnens demping. Nordhusstykket 5 er modellert av IFC-geometrien med kjelleren som egen sone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
@@ -55,6 +55,14 @@ klimaavhengige energipostene til referanseklima Oslo med klimakorrigeringsfaktor
|
||||
standardens tillegg, slik energimerkeforskriften krever. Arkivet lastes ned én gang og
|
||||
hurtiglagres.
|
||||
|
||||
**Grunnen og kjelleren.** Flater mot grunnen regnes etter NS-EN ISO 13370: jordas varmemotstand
|
||||
gjør at en uisolert grunnmur taper langt mindre enn U-verdien alene tilsier, og en kjellervegg
|
||||
deles i en del over terreng, som er en vanlig yttervegg, og en nedgravd del. En uoppvarmet
|
||||
kjeller eller et kaldt loft kan modelleres som en egen sone. Temperaturen der regnes ut time
|
||||
for time av sonens varmebalanse etter NS-EN ISO 13789, med varme fra rommene over og fra
|
||||
varmerør og beredere som står der. Uisolerte rør i kjelleren teller dermed både som tap fra
|
||||
varmeanlegget og som varme som holder etasjeskilleren varm.
|
||||
|
||||
**Overoppheting.** Bygninger uten kjøleanlegg luftes gjennom vinduer. Uten det ville en tett
|
||||
bygning med store vindusflater fått urealistisk høye innetemperaturer om våren og høsten, og
|
||||
lagret solvarme ville kunstig redusert oppvarmingsbehovet. Grensen settes til den normerte
|
||||
@@ -130,6 +138,20 @@ g_verdi = 0.76
|
||||
[infiltrasjon]
|
||||
n50 = 8.0
|
||||
|
||||
[[sone]] # uoppvarmet kjeller, med egen temperaturberegning
|
||||
navn = "Kjeller"
|
||||
volum = 165.0
|
||||
andel_distribusjonstap = 1.0 # uisolerte varmerør avgir varmen sin her
|
||||
|
||||
[[sone.flate]]
|
||||
navn = "Kjellermur"
|
||||
type = "vegg"
|
||||
areal = 108.0
|
||||
u_verdi = 3.13
|
||||
mot = "grunn"
|
||||
dybde = 1.6 # nedgravd høyde
|
||||
over_terreng = 0.6 # høyden som står over terreng
|
||||
|
||||
[ventilasjon]
|
||||
type = "naturlig" # naturlig | avtrekk | balansert
|
||||
|
||||
@@ -188,6 +210,8 @@ Motoren er kontrollert mot uavhengige referanser:
|
||||
| --- | --- |
|
||||
| Solinnstråling på skrå og orienterte flater, mot pvlib (Hay–Davies) | avvik under 1 % for alle himmelretninger i Oslo-klima |
|
||||
| Stasjonært varmetap mot U·A·ΔT | innenfor 2 % |
|
||||
| Grunnmodellen mot NS-EN ISO 13370 | kompakthet, dybde og isolasjon virker som standarden beskriver |
|
||||
| Kjellermodellen mot NS-EN ISO 13789 | varme rør hever kjellertemperaturen og reduserer romoppvarmingen |
|
||||
| Årlig energibalanse | går opp innenfor 2 % |
|
||||
| Varmebehov som funksjon av temperaturdifferanse | eksakt lineært |
|
||||
| Varmetapsbudsjett for TEK17-referansebygget | identisk med publisert tabell, 0,69 W/(m²K) |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,392 @@
|
||||
# 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.
|
||||
|
||||
[bygning]
|
||||
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"
|
||||
|
||||
[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"
|
||||
type = "tak"
|
||||
areal = 130.1
|
||||
helning = 40
|
||||
u_verdi = 0.55
|
||||
masseklasse = "lett"
|
||||
kommentar = "Areal fra IFC; U-verdi anslått etter termografirapporten"
|
||||
|
||||
[[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"
|
||||
type = "gulv"
|
||||
areal = 75.0
|
||||
u_verdi = 0.31
|
||||
mot = "uoppvarmet"
|
||||
sone = "Kjeller"
|
||||
masseklasse = "middels"
|
||||
|
||||
# ── 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.
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer nord"
|
||||
areal = 5.58
|
||||
orientering = 0
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer øst"
|
||||
areal = 2.86
|
||||
orientering = 90
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer sør"
|
||||
areal = 3.96
|
||||
orientering = 180
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer vest"
|
||||
areal = 1.05
|
||||
orientering = 270
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[[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.
|
||||
|
||||
[[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
|
||||
|
||||
[[sone.flate]]
|
||||
navn = "Kjellermur 30 cm uisolert, øst"
|
||||
type = "vegg"
|
||||
areal = 9.7
|
||||
orientering = 90
|
||||
u_verdi = 3.13
|
||||
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
|
||||
|
||||
[[sone.flate]]
|
||||
navn = "Kjellermur 25 cm uisolert"
|
||||
type = "vegg"
|
||||
areal = 40.7
|
||||
orientering = 90
|
||||
u_verdi = 3.39
|
||||
mot = "grunn"
|
||||
dybde = 1.6
|
||||
over_terreng = 0.6
|
||||
|
||||
[[sone.flate]]
|
||||
navn = "Kjellermur med treullsement"
|
||||
type = "vegg"
|
||||
areal = 2.2
|
||||
orientering = 90
|
||||
u_verdi = 0.46
|
||||
mot = "grunn"
|
||||
dybde = 1.6
|
||||
over_terreng = 0.6
|
||||
|
||||
[[sone.flate]]
|
||||
navn = "Kjellergulv"
|
||||
type = "gulv"
|
||||
areal = 78.9
|
||||
u_verdi = 3.85
|
||||
mot = "grunn"
|
||||
omkrets = 40.0
|
||||
dybde = 2.2
|
||||
|
||||
# Kjellervinduene. Termografien viser stort varmetap gjennom kjellervinduet.
|
||||
[[sone.flate]]
|
||||
navn = "Kjellervinduer"
|
||||
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"
|
||||
|
||||
# ── varmeforsyning ─────────────────────────────────────────────────────
|
||||
# Vannbårent radiatoranlegg med elektrokjel i kjelleren, supplert med vedovn.
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "vedovn"
|
||||
andel_rom = 0.25
|
||||
lukket = false
|
||||
kommentar = "Åpent ildsted; energimerkeforskriften regner inntil 10 % av romoppvarmingen"
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "el_kjel"
|
||||
andel_rom = 0.75
|
||||
andel_tappevann = 1.0
|
||||
|
||||
# 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
|
||||
|
||||
# ── tiltak ─────────────────────────────────────────────────────────────
|
||||
|
||||
[[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 }
|
||||
|
||||
[[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 },
|
||||
]
|
||||
@@ -538,6 +538,8 @@ fn bygg(
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
over_terreng: None,
|
||||
sone: None,
|
||||
kommentar: konstruksjoner
|
||||
.get(&o.konstruksjon)
|
||||
.map(|k| k.navn.clone())
|
||||
@@ -580,6 +582,8 @@ fn bygg(
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
sone: Vec::new(),
|
||||
grunn: Grunn::default(),
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
|
||||
+414
-240
@@ -9,6 +9,7 @@
|
||||
//! tyngdepunkt. Prosjektets sanne nord (`IfcGeometricRepresentationContext.TrueNorth`)
|
||||
//! legges til.
|
||||
|
||||
use crate::inndata::ifcgeom;
|
||||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
@@ -19,6 +20,8 @@ use std::path::Path;
|
||||
pub struct Importrapport {
|
||||
pub bygning: Bygning,
|
||||
pub merknader: Vec<String>,
|
||||
/// Bygningsdeler som ikke er tatt med i klimaskjermen: navn, areal og U-verdi.
|
||||
pub utelatt: Vec<(String, f64, Option<f64>)>,
|
||||
}
|
||||
|
||||
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
||||
@@ -39,48 +42,77 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let fil = step::les(tekst);
|
||||
let mut merknader = Vec::new();
|
||||
|
||||
let lengdeskala = lengdeenhet(&fil);
|
||||
let skala = ifcgeom::lengdeenhet(&fil);
|
||||
let sant_nord = sant_nord(&fil);
|
||||
let egenskaper = samle_egenskaper(&fil);
|
||||
let mengder = samle_mengder(&fil);
|
||||
let (vindu_vert, dor_vert) = samle_apninger(&fil);
|
||||
|
||||
// Areal og volum fra rom.
|
||||
// ── oppvarmet areal fra rommene ─────────────────────────────────
|
||||
let mut bra = 0.0;
|
||||
let mut volum = 0.0;
|
||||
for (r, _) in fil.av_type("IFCSPACE") {
|
||||
if let Some(m) = mengder.get(&r) {
|
||||
bra += m
|
||||
.get("NetFloorArea")
|
||||
.or_else(|| m.get("GrossFloorArea"))
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
volum += m
|
||||
.get("NetVolume")
|
||||
.or_else(|| m.get("GrossVolume"))
|
||||
.copied()
|
||||
let mut rom: Vec<(String, f64)> = 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
rom.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
if !rom.is_empty() {
|
||||
let liste: Vec<String> = 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²]: {}.",
|
||||
liste.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
// Plasseringer for alle bygningsdeler.
|
||||
let mut plassering: HashMap<u64, (f64, f64, f64, f64, f64)> = HashMap::new();
|
||||
for (r, e) in fil.av_typer(&VEGGTYPER).chain(fil.av_typer(&PLATETYPER)) {
|
||||
if let Some(p) = e.arg(5).and_then(|a| a.som_ref()) {
|
||||
if let Some(v) = global_plassering(&fil, p, lengdeskala) {
|
||||
plassering.insert(r, v);
|
||||
// ── plassering og geometri for alle bygningsdeler ───────────────
|
||||
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
|
||||
let mut deler: Vec<Del> = Vec::new();
|
||||
for (r, e) in fil.av_typer(&typer) {
|
||||
let plass = e
|
||||
.arg(5)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|p| global_plassering(&fil, p, skala));
|
||||
let netter = ifcgeom::netter_for_produkt(&fil, e.arg(6).and_then(|a| a.som_ref()));
|
||||
let flate = ifcgeom::samlet_hovedflate(&netter, skala);
|
||||
deler.push(Del {
|
||||
nr: r,
|
||||
typ: e.typ.clone(),
|
||||
navn: navn_av(e, r, standardnavn_for(&e.typ)),
|
||||
ytre: er_ytre(&egenskaper, r),
|
||||
u_verdi: u_verdi(&egenskaper, r),
|
||||
plass,
|
||||
areal: flate.map(|f| f.areal).unwrap_or(0.0),
|
||||
normal: flate.map(|f| f.normal),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tyngdepunkt av ytterveggene, for å bestemme hvilken vei normalen peker.
|
||||
let ytre: Vec<u64> = fil
|
||||
.av_typer(&VEGGTYPER)
|
||||
.filter(|(r, _)| er_ytre(&egenskaper, *r).unwrap_or(true))
|
||||
.map(|(r, _)| r)
|
||||
|
||||
// Tyngdepunkt av de ytre veggene bestemmer hvilken vei normalen peker.
|
||||
let ytre_deler: Vec<&Del> = deler
|
||||
.iter()
|
||||
.filter(|d| VEGGTYPER.contains(&d.typ.as_str()) && d.ytre.unwrap_or(false))
|
||||
.collect();
|
||||
let referanse: Vec<&Del> = if ytre_deler.is_empty() {
|
||||
deler
|
||||
.iter()
|
||||
.filter(|d| VEGGTYPER.contains(&d.typ.as_str()))
|
||||
.collect()
|
||||
} else {
|
||||
ytre_deler
|
||||
};
|
||||
let (mut cx, mut cy, mut n) = (0.0, 0.0, 0.0);
|
||||
for r in &ytre {
|
||||
if let Some((x, y, _, _, _)) = plassering.get(r) {
|
||||
for d in &referanse {
|
||||
if let Some((x, y, _, _, _)) = d.plass {
|
||||
cx += x;
|
||||
cy += y;
|
||||
n += 1.0;
|
||||
@@ -91,93 +123,44 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
cy /= n;
|
||||
}
|
||||
|
||||
// ── klimaskjerm ─────────────────────────────────────────────────
|
||||
let mut flater: Vec<Flate> = Vec::new();
|
||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
|
||||
let mut uten_uverdi = 0usize;
|
||||
let mut uten_geometri = 0usize;
|
||||
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
||||
|
||||
// ── vegger ──────────────────────────────────────────────────────
|
||||
for (r, e) in fil.av_typer(&VEGGTYPER) {
|
||||
let ytre_vegg = er_ytre(&egenskaper, r).unwrap_or(true);
|
||||
let asimut = plassering
|
||||
.get(&r)
|
||||
.map(|(x, y, _, dx, dy)| asimut_av(*x, *y, *dx, *dy, cx, cy, sant_nord))
|
||||
.unwrap_or(0.0);
|
||||
asimut_for_vegg.insert(r, asimut);
|
||||
if !ytre_vegg {
|
||||
continue;
|
||||
}
|
||||
let m = mengder.get(&r);
|
||||
let brutto = m
|
||||
.and_then(|m| {
|
||||
m.get("NetSideArea")
|
||||
.or_else(|| m.get("GrossSideArea"))
|
||||
.copied()
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
if brutto <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let u = u_verdi(&egenskaper, r);
|
||||
if u.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
navn: navn_av(e, r, "Vegg"),
|
||||
typ: FlateType::Vegg,
|
||||
areal: brutto,
|
||||
orientering: asimut,
|
||||
helning: Some(90.0),
|
||||
u_verdi: u,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot: Mot::Ute,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// ── plater: tak, gulv, dekke mot grunn ──────────────────────────
|
||||
for (r, e) in fil.av_typer(&PLATETYPER) {
|
||||
if !er_ytre(&egenskaper, r).unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let m = mengder.get(&r);
|
||||
let a = m
|
||||
.and_then(|m| m.get("NetArea").or_else(|| m.get("GrossArea")).copied())
|
||||
.unwrap_or(0.0);
|
||||
if a <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let type_tekst = e
|
||||
.args
|
||||
// Laveste gulvnivå brukes til å skille dekke mot grunn fra etasjeskillere.
|
||||
let laveste = deler
|
||||
.iter()
|
||||
.filter_map(|x| x.som_tekst())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.to_uppercase();
|
||||
// Plater uten ROOF-merking regnes som dekke mot grunn; kontroller `mot` etter importen
|
||||
// dersom modellen har dekker mot det fri eller mot uoppvarmet kjeller.
|
||||
let (typ, mot, helning) = if e.typ == "IFCROOF" || type_tekst.contains("ROOF") {
|
||||
(FlateType::Tak, Mot::Ute, 0.0)
|
||||
} else {
|
||||
(FlateType::Gulv, Mot::Grunn, 180.0)
|
||||
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
||||
.filter_map(|d| d.plass.map(|p| p.2))
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
|
||||
for d in &deler {
|
||||
let er_vegg = VEGGTYPER.contains(&d.typ.as_str());
|
||||
let (asimut, helning) = retning_av(d, cx, cy, sant_nord);
|
||||
if er_vegg {
|
||||
asimut_for_vegg.insert(d.nr, asimut);
|
||||
}
|
||||
if d.areal <= 0.05 {
|
||||
uten_geometri += 1;
|
||||
continue;
|
||||
}
|
||||
let Some((typ, mot)) = klassifiser(d, laveste, helning) else {
|
||||
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
|
||||
continue;
|
||||
};
|
||||
let u = u_verdi(&egenskaper, r);
|
||||
if u.is_none() {
|
||||
if d.u_verdi.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
navn: navn_av(e, r, if typ == FlateType::Tak { "Tak" } else { "Gulv" }),
|
||||
navn: d.navn.clone(),
|
||||
typ,
|
||||
areal: a,
|
||||
orientering: 0.0,
|
||||
areal: d.areal,
|
||||
orientering: if typ == FlateType::Vegg { asimut } else { 0.0 },
|
||||
helning: Some(helning),
|
||||
u_verdi: u,
|
||||
u_verdi: d.u_verdi,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
@@ -186,21 +169,26 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
over_terreng: None,
|
||||
sone: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
slaa_sammen(&mut flater);
|
||||
|
||||
// ── vinduer og dører ────────────────────────────────────────────
|
||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
||||
let dor = e.typ == "IFCDOOR";
|
||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
||||
let bredde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||
let bredde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||
let areal = mengder
|
||||
.get(&r)
|
||||
.and_then(|m| m.get("Area").or_else(|| m.get("GrossArea")).copied())
|
||||
.filter(|a| *a > 0.01)
|
||||
.map(arealmengde)
|
||||
.filter(|a| *a > 0.05)
|
||||
.unwrap_or(hoyde * bredde);
|
||||
if areal <= 0.01 {
|
||||
if areal <= 0.05 {
|
||||
continue;
|
||||
}
|
||||
let vert = if dor {
|
||||
@@ -208,6 +196,19 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
} else {
|
||||
vindu_vert.get(&r)
|
||||
};
|
||||
// Vinduer i innervegger hører ikke til klimaskjermen.
|
||||
let i_ytre_vegg = vert
|
||||
.and_then(|v| deler.iter().find(|d| d.nr == *v))
|
||||
.map(|d| d.ytre.unwrap_or(true))
|
||||
.unwrap_or(true);
|
||||
if !i_ytre_vegg {
|
||||
utelatt.push((
|
||||
format!("{} (i innervegg)", navn_av(e, r, "Vindu")),
|
||||
areal,
|
||||
u_verdi(&egenskaper, r),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let asimut = vert
|
||||
.and_then(|v| asimut_for_vegg.get(v))
|
||||
.copied()
|
||||
@@ -237,84 +238,75 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
});
|
||||
}
|
||||
|
||||
// Trekk vindus- og dørareal fra veggene de sitter i.
|
||||
for (v, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let areal = vinduer
|
||||
.iter()
|
||||
.find(|x| x.kommentar.is_some() && x.navn.ends_with(&format!("#{v}")))
|
||||
.map(|x| x.areal);
|
||||
let _ = (areal, vegg);
|
||||
}
|
||||
let apningsareal_per_vegg = {
|
||||
let mut m: HashMap<u64, f64> = HashMap::new();
|
||||
// Vindus- og dørarealet trekkes fra veggene de sitter i, dersom geometrien ikke
|
||||
// allerede har hull der åpningene er.
|
||||
let mut apning_per_vegg: HashMap<u64, f64> = HashMap::new();
|
||||
for (a, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let e = fil.hent(*a);
|
||||
let hoyde = e
|
||||
let h = e
|
||||
.and_then(|e| e.arg(8))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* lengdeskala;
|
||||
let bredde = e
|
||||
* skala;
|
||||
let b = e
|
||||
.and_then(|e| e.arg(9))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* lengdeskala;
|
||||
let areal = mengder
|
||||
.get(a)
|
||||
.and_then(|q| q.get("Area").or_else(|| q.get("GrossArea")).copied())
|
||||
.filter(|v| *v > 0.01)
|
||||
.unwrap_or(hoyde * bredde);
|
||||
*m.entry(*vegg).or_default() += areal;
|
||||
* skala;
|
||||
*apning_per_vegg.entry(*vegg).or_default() += h * b;
|
||||
}
|
||||
m
|
||||
let mut trukket_fra = 0usize;
|
||||
for d in &deler {
|
||||
let Some(ap) = apning_per_vegg.get(&d.nr) else {
|
||||
continue;
|
||||
};
|
||||
for (r, _) in fil.av_typer(&VEGGTYPER) {
|
||||
if let Some(ap) = apningsareal_per_vegg.get(&r) {
|
||||
let navn = fil.hent(r).map(|e| navn_av(e, r, "Vegg"));
|
||||
if let Some(f) = navn.and_then(|n| flater.iter_mut().find(|f| f.navn == n)) {
|
||||
f.areal = (f.areal - ap).max(0.0);
|
||||
let Some(f) = flater.iter_mut().find(|f| f.navn == d.navn) else {
|
||||
continue;
|
||||
};
|
||||
// Ligger veggarealet nær bruttoarealet, har geometrien ikke hull, og åpningene trekkes fra.
|
||||
if f.areal > *ap * 1.5 && d.normal.is_some() {
|
||||
let netto = (f.areal - ap).max(0.0);
|
||||
if netto > 0.05 {
|
||||
f.areal = netto;
|
||||
trukket_fra += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
flater.retain(|f| f.areal > 0.01);
|
||||
if trukket_fra > 0 {
|
||||
merknader.push(format!(
|
||||
"Vindus- og dørareal er trukket fra {trukket_fra} vegger. Kontroller veggarealene mot tegningen."
|
||||
));
|
||||
}
|
||||
|
||||
// ── merknader ───────────────────────────────────────────────────
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
.iter()
|
||||
.filter(|f| f.typ == FlateType::Gulv)
|
||||
.map(|f| f.areal)
|
||||
.sum::<f64>();
|
||||
if bra > 0.0 {
|
||||
merknader.push(format!(
|
||||
"IFC-modellen har ingen romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvarealet. Kontroller verdien."
|
||||
));
|
||||
merknader.push(if bra > 0.0 {
|
||||
format!("IFC-modellen mangler romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvflatene. Kontroller verdien.")
|
||||
} else {
|
||||
merknader.push(
|
||||
"IFC-modellen har verken romarealer eller gulvflater; fyll inn oppvarmet_bra."
|
||||
.into(),
|
||||
);
|
||||
"IFC-modellen har verken romarealer eller gulvflater; fyll inn oppvarmet_bra.".into()
|
||||
});
|
||||
}
|
||||
if uten_geometri > 0 {
|
||||
merknader.push(format!(
|
||||
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
|
||||
));
|
||||
}
|
||||
if uten_uverdi > 0 {
|
||||
merknader.push(format!(
|
||||
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok \
|
||||
bruke typiske verdier for byggeåret."
|
||||
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok bruke typiske verdier for byggeåret."
|
||||
));
|
||||
}
|
||||
if n == 0.0 {
|
||||
merknader.push(
|
||||
"Fant ingen plasseringer for ytterveggene; alle himmelretninger er satt til nord."
|
||||
"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."
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
merknader.push(
|
||||
"Himmelretningene er utledet av elementenes plassering og bygningens tyngdepunkt. Kontroller dem mot tegningen."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
merknader.push(
|
||||
"IFC beskriver ikke drift, lekkasjetall eller energiforsyning. Fyll inn [ventilasjon], \
|
||||
[infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
"IFC beskriver ikke drift, lekkasjetall eller energiforsyning. Fyll inn [ventilasjon], [infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
.into(),
|
||||
);
|
||||
|
||||
@@ -322,7 +314,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
.av_type("IFCBUILDING")
|
||||
.next()
|
||||
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
|
||||
.filter(|s| !s.is_empty())
|
||||
.filter(|s| !s.is_empty() && s != "Default Building")
|
||||
.unwrap_or_else(|| standardnavn.to_string());
|
||||
|
||||
let bygning = Bygning {
|
||||
@@ -342,6 +334,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
sone: Vec::new(),
|
||||
grunn: Grunn::default(),
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
@@ -355,7 +349,185 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
};
|
||||
Ok(Importrapport { bygning, merknader })
|
||||
Ok(Importrapport {
|
||||
bygning,
|
||||
merknader,
|
||||
utelatt,
|
||||
})
|
||||
}
|
||||
|
||||
/// Slår sammen flater med samme konstruksjon, type og himmelretning til én oppføring.
|
||||
///
|
||||
/// En BIM-modell deler gjerne den samme veggen i mange elementer, ett per rom eller etasje.
|
||||
/// For en energiberegning er det konstruksjonen og himmelretningen som teller, så oppføringene
|
||||
/// summeres. Himmelretningen rundes til nærmeste 45 grader før sammenligningen.
|
||||
fn slaa_sammen(flater: &mut Vec<Flate>) {
|
||||
let nokkel = |f: &Flate| {
|
||||
let navn = f.navn.split(" #").next().unwrap_or(&f.navn).to_string();
|
||||
let sektor = ((f.orientering / 45.0).round() as i64).rem_euclid(8);
|
||||
let u = (f.u_verdi.unwrap_or(-1.0) * 100.0).round() as i64;
|
||||
(navn, f.typ, f.mot, sektor, u)
|
||||
};
|
||||
let mut samlet: Vec<Flate> = Vec::new();
|
||||
for f in flater.drain(..) {
|
||||
match samlet.iter_mut().find(|g| nokkel(g) == nokkel(&f)) {
|
||||
Some(g) => {
|
||||
// Arealvektet himmelretning, slik at summen peker riktig vei.
|
||||
let sum = g.areal + f.areal;
|
||||
g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum;
|
||||
g.areal = sum;
|
||||
}
|
||||
None => samlet.push(f),
|
||||
}
|
||||
}
|
||||
for f in &mut samlet {
|
||||
f.navn = f
|
||||
.navn
|
||||
.split(" #")
|
||||
.next()
|
||||
.unwrap_or(&f.navn)
|
||||
.trim()
|
||||
.to_string();
|
||||
f.orientering = (f.orientering / 45.0).round() * 45.0;
|
||||
}
|
||||
*flater = samlet;
|
||||
}
|
||||
|
||||
/// En bygningsdel slik den er lest av IFC-filen.
|
||||
struct Del {
|
||||
nr: u64,
|
||||
typ: String,
|
||||
navn: String,
|
||||
ytre: Option<bool>,
|
||||
u_verdi: Option<f64>,
|
||||
plass: Option<(f64, f64, f64, f64, f64)>,
|
||||
areal: f64,
|
||||
normal: Option<[f64; 3]>,
|
||||
}
|
||||
|
||||
fn standardnavn_for(typ: &str) -> &'static str {
|
||||
if typ == "IFCROOF" {
|
||||
"Tak"
|
||||
} else if PLATETYPER.contains(&typ) {
|
||||
"Dekke"
|
||||
} else {
|
||||
"Vegg"
|
||||
}
|
||||
}
|
||||
|
||||
fn romnavn(e: &Entitet) -> String {
|
||||
e.arg(7)
|
||||
.and_then(|a| a.som_tekst())
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| e.arg(2).and_then(|a| a.som_tekst()))
|
||||
.unwrap_or("rom")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Arealet fra en mengdesamling. Godtar både standardnavnene og verktøyspesifikke navn.
|
||||
fn arealmengde(m: &HashMap<String, f64>) -> f64 {
|
||||
for navn in [
|
||||
"NetFloorArea",
|
||||
"GrossFloorArea",
|
||||
"NetArea",
|
||||
"GrossArea",
|
||||
"Area",
|
||||
] {
|
||||
if let Some(v) = m.get(navn) {
|
||||
return *v;
|
||||
}
|
||||
}
|
||||
// Ellers: et hvilket som helst areal, slik at verktøyspesifikke navn som
|
||||
// «GSA BIM Area» også virker.
|
||||
m.iter()
|
||||
.filter(|(k, _)| k.to_lowercase().contains("area"))
|
||||
.map(|(_, v)| *v)
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
/// Himmelretning og helning ut fra hovedflatens normal.
|
||||
fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
||||
let Some(n) = d.normal else {
|
||||
return (
|
||||
0.0,
|
||||
if PLATETYPER.contains(&d.typ.as_str()) {
|
||||
0.0
|
||||
} else {
|
||||
90.0
|
||||
},
|
||||
);
|
||||
};
|
||||
// Normalen er gitt i lokale koordinater; roter den med delens plassering.
|
||||
let (dx, dy) = d.plass.map(|p| (p.3, p.4)).unwrap_or((1.0, 0.0));
|
||||
let mut g = [n[0] * dx - n[1] * dy, n[0] * dy + n[1] * dx, n[2]];
|
||||
|
||||
let vannrett = (g[0] * g[0] + g[1] * g[1]).sqrt();
|
||||
if vannrett > 0.2 {
|
||||
// Veggflate: velg den normalen som peker bort fra bygningens tyngdepunkt.
|
||||
if let Some((x, y, _, _, _)) = d.plass {
|
||||
if g[0] * (x - cx) + g[1] * (y - cy) < 0.0 {
|
||||
g = [-g[0], -g[1], -g[2]];
|
||||
}
|
||||
}
|
||||
} else if g[2] < 0.0 {
|
||||
// Vannrett flate: normalen oppover gir helning 0, nedover 180.
|
||||
g = [-g[0], -g[1], -g[2]];
|
||||
}
|
||||
let helning = g[2].clamp(-1.0, 1.0).acos().to_degrees();
|
||||
let asimut = (g[0].atan2(g[1]).to_degrees() + sant_nord).rem_euclid(360.0);
|
||||
(asimut, helning)
|
||||
}
|
||||
|
||||
/// 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)> {
|
||||
let navn = d.navn.to_lowercase();
|
||||
let grunnord = [
|
||||
"kjellermur",
|
||||
"grunnmur",
|
||||
"ringmur",
|
||||
"såle",
|
||||
"sale",
|
||||
"fundament",
|
||||
];
|
||||
let takord = ["tak", "roof", "loft"];
|
||||
|
||||
if PLATETYPER.contains(&d.typ.as_str()) {
|
||||
if d.typ == "IFCROOF" || takord.iter().any(|o| navn.contains(o)) {
|
||||
return Some((
|
||||
FlateType::Tak,
|
||||
if navn.contains("loft") {
|
||||
Mot::Uoppvarmet
|
||||
} else {
|
||||
Mot::Ute
|
||||
},
|
||||
));
|
||||
}
|
||||
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.
|
||||
if d.ytre == Some(true) {
|
||||
return Some((FlateType::Gulv, Mot::Ute));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Vegger.
|
||||
if d.ytre == Some(true) {
|
||||
return Some((FlateType::Vegg, Mot::Ute));
|
||||
}
|
||||
if grunnord.iter().any(|o| navn.contains(o)) {
|
||||
return Some((FlateType::Vegg, Mot::Grunn));
|
||||
}
|
||||
if d.ytre.is_none() && helning > 60.0 {
|
||||
// Uten `IsExternal` må vi anta at veggen er ytre.
|
||||
return Some((FlateType::Vegg, Mot::Ute));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn navn_av(e: &Entitet, r: u64, standard: &str) -> String {
|
||||
@@ -384,25 +556,6 @@ fn adresse(fil: &Fil) -> Option<String> {
|
||||
(!ut.is_empty()).then_some(ut)
|
||||
}
|
||||
|
||||
/// Faktor som gjør lengder om til meter.
|
||||
fn lengdeenhet(fil: &Fil) -> f64 {
|
||||
for (_, e) in fil.av_type("IFCSIUNIT") {
|
||||
let er_lengde = e.arg(1).and_then(|a| a.som_tekst()) == Some("LENGTHUNIT");
|
||||
if !er_lengde {
|
||||
continue;
|
||||
}
|
||||
let prefiks = e.arg(2).and_then(|a| a.som_tekst()).unwrap_or("");
|
||||
return match prefiks {
|
||||
"MILLI" => 0.001,
|
||||
"CENTI" => 0.01,
|
||||
"DECI" => 0.1,
|
||||
"KILO" => 1000.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
}
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Prosjektets dreining mot sant nord, i grader med klokka.
|
||||
fn sant_nord(fil: &Fil) -> f64 {
|
||||
for (_, e) in fil.av_type("IFCGEOMETRICREPRESENTATIONCONTEXT") {
|
||||
@@ -489,22 +642,6 @@ fn retning(fil: &Fil, r: Option<u64>) -> Option<(f64, f64)> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Himmelretning for en vegg: normalen står vinkelrett på veggens akse, og velges slik at
|
||||
/// den peker bort fra bygningens tyngdepunkt.
|
||||
fn asimut_av(x: f64, y: f64, dx: f64, dy: f64, cx: f64, cy: f64, sant_nord: f64) -> f64 {
|
||||
let kandidater = [(dy, -dx), (-dy, dx)];
|
||||
let ut = (x - cx, y - cy);
|
||||
let valgt = if kandidater[0].0 * ut.0 + kandidater[0].1 * ut.1
|
||||
>= kandidater[1].0 * ut.0 + kandidater[1].1 * ut.1
|
||||
{
|
||||
kandidater[0]
|
||||
} else {
|
||||
kandidater[1]
|
||||
};
|
||||
// Asimut fra nord med klokka; prosjektets y-akse peker mot nord etter TrueNorth-dreining.
|
||||
(valgt.0.atan2(valgt.1).to_degrees() + sant_nord).rem_euclid(360.0)
|
||||
}
|
||||
|
||||
type Egenskaper = HashMap<u64, HashMap<String, Arg>>;
|
||||
|
||||
/// Samler alle egenskaper fra `IfcRelDefinesByProperties` per objekt.
|
||||
@@ -668,54 +805,83 @@ fn er_ytre(e: &Egenskaper, r: u64) -> Option<bool> {
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
const PROVE: &str = r#"ISO-10303-21;
|
||||
HEADER;ENDSEC;
|
||||
DATA;
|
||||
#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#2= IFCDIRECTION((0.,1.));
|
||||
#3= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#2);
|
||||
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);
|
||||
#20= IFCLOCALPLACEMENT($,#21);
|
||||
#21= IFCAXIS2PLACEMENT3D(#22,$,$);
|
||||
#22= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#100= IFCWALLSTANDARDCASE('w1',$,'Sorvegg',$,$,#110,$,$);
|
||||
#110= IFCLOCALPLACEMENT(#20,#111);
|
||||
#111= IFCAXIS2PLACEMENT3D(#112,$,#113);
|
||||
#112= IFCCARTESIANPOINT((0.,-5.,0.));
|
||||
#113= IFCDIRECTION((1.,0.,0.));
|
||||
#120= IFCWALLSTANDARDCASE('w2',$,'Nordvegg',$,$,#130,$,$);
|
||||
#130= IFCLOCALPLACEMENT(#20,#131);
|
||||
#131= IFCAXIS2PLACEMENT3D(#132,$,#133);
|
||||
#132= IFCCARTESIANPOINT((0.,5.,0.));
|
||||
#133= IFCDIRECTION((1.,0.,0.));
|
||||
#200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);
|
||||
#201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
|
||||
#202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));
|
||||
#203= IFCRELDEFINESBYPROPERTIES('r',$,$,$,(#100,#120),#202);
|
||||
#210= IFCQUANTITYAREA('NetSideArea',$,$,25.0,$);
|
||||
#211= IFCELEMENTQUANTITY('q',$,'BaseQuantities',$,$,(#210));
|
||||
#212= IFCRELDEFINESBYPROPERTIES('r2',$,$,$,(#100,#120),#211);
|
||||
#300= IFCSPACE('s',$,'Stue',$,$,#20,$,$,.ELEMENT.,.INTERNAL.,$);
|
||||
#310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);
|
||||
#311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);
|
||||
#312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));
|
||||
#313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;"#;
|
||||
/// To yttervegger på 5 × 2,5 m med hver sin trekantgeometri, i millimeter.
|
||||
/// Sørveggen ligger ved y = −5 m, nordveggen ved y = +5 m.
|
||||
fn prove() -> String {
|
||||
let mut s = String::from(
|
||||
"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n #1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n #2= IFCUNITASSIGNMENT((#1));\n #3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);\n #4= IFCDIRECTION((0.,1.));\n #40= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#4);\n #5= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n #10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);\n #20= IFCLOCALPLACEMENT($,#21);\n #21= IFCAXIS2PLACEMENT3D(#22,$,$);\n #22= IFCCARTESIANPOINT((0.,0.,0.));\n #200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);\n #201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);\n #202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));\n #300= IFCSPACE('s',$,'Stue',$,$,#20,$,'Stue',.ELEMENT.,.INTERNAL.,$);\n #310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);\n #311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);\n #312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));\n #313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);\n",
|
||||
);
|
||||
// En vegg som en tynn kasse: 5 m lang, 2,5 m høy, 0,2 m tykk, i millimeter.
|
||||
let mut vegg = |nr: u64, navn: &str, y: f64| {
|
||||
let (p, l, h, tj) = (nr, 5000.0, 2500.0, 200.0);
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCWALL('w{p}',$,'{navn}',$,$,#{},#{},$,$);\n",
|
||||
p,
|
||||
p + 1,
|
||||
p + 4
|
||||
));
|
||||
s.push_str(&format!("#{}= IFCLOCALPLACEMENT(#20,#{});\n", p + 1, p + 2));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCAXIS2PLACEMENT3D(#{},$,#{});\n",
|
||||
p + 2,
|
||||
p + 3,
|
||||
p + 9
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCCARTESIANPOINT((0.,{:.1},0.));\n",
|
||||
p + 3,
|
||||
y * 1000.0
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCPRODUCTDEFINITIONSHAPE($,$,(#{}));\n",
|
||||
p + 4,
|
||||
p + 5
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCSHAPEREPRESENTATION(#40,'Body','Tessellation',(#{}));\n",
|
||||
p + 5,
|
||||
p + 6
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCTRIANGULATEDFACESET(#{},$,.T.,((1,2,3),(1,3,4),(5,7,6),(5,8,7),(1,5,6),(1,6,2),(2,6,7),(2,7,3),(3,7,8),(3,8,4),(4,8,5),(4,5,1)),$);\n",
|
||||
p + 6,
|
||||
p + 7
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCCARTESIANPOINTLIST3D(((0.,0.,0.),({l:.1},0.,0.),({l:.1},{tj:.1},0.),(0.,{tj:.1},0.),(0.,0.,{h:.1}),({l:.1},0.,{h:.1}),({l:.1},{tj:.1},{h:.1}),(0.,{tj:.1},{h:.1})));\n",
|
||||
p + 7
|
||||
));
|
||||
s.push_str(&format!("#{}= IFCDIRECTION((1.,0.,0.));\n", p + 9));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCRELDEFINESBYPROPERTIES('rel{p}',$,$,$,(#{p}),#202);\n",
|
||||
p + 8
|
||||
));
|
||||
};
|
||||
vegg(100, "Sorvegg", -5.0);
|
||||
vegg(120, "Nordvegg", 5.0);
|
||||
s.push_str("ENDSEC;\nEND-ISO-10303-21;");
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leser_vegger_areal_uverdi_og_himmelretning() {
|
||||
let r = fra_tekst(PROVE, "prove").unwrap();
|
||||
fn leser_areal_uverdi_og_himmelretning_av_geometrien() {
|
||||
let r = fra_tekst(&prove(), "prove").unwrap();
|
||||
let b = &r.bygning;
|
||||
assert_eq!(b.bygning.navn, "Testhus");
|
||||
assert_eq!(b.bygning.oppvarmet_bra, 100.0);
|
||||
assert_eq!(b.bygning.oppvarmet_volum, Some(250.0));
|
||||
assert_eq!(b.flate.len(), 2);
|
||||
assert_eq!(b.flate.len(), 2, "flater: {:?}", b.flate);
|
||||
for f in &b.flate {
|
||||
assert_eq!(f.areal, 25.0);
|
||||
// Hovedflaten er 5 × 2,5 m, ikke summen av alle sider.
|
||||
assert!(
|
||||
(f.areal - 12.5).abs() < 0.01,
|
||||
"{} fikk areal {}",
|
||||
f.navn,
|
||||
f.areal
|
||||
);
|
||||
assert_eq!(f.u_verdi, Some(0.18));
|
||||
assert_eq!(f.mot, Mot::Ute);
|
||||
}
|
||||
// Veggen ved y = −5 vender sør (180°), den ved y = +5 vender nord (0°).
|
||||
let sor = b
|
||||
.flate
|
||||
.iter()
|
||||
@@ -738,14 +904,22 @@ END-ISO-10303-21;"#;
|
||||
);
|
||||
}
|
||||
|
||||
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
|
||||
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
|
||||
#[test]
|
||||
fn millimeterenhet_skaleres() {
|
||||
let t = PROVE.replace(
|
||||
"#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);",
|
||||
"#1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);",
|
||||
);
|
||||
let r = fra_tekst(&t, "prove").unwrap();
|
||||
// Arealene kommer fra mengder og er alltid i m², så de endres ikke.
|
||||
assert_eq!(r.bygning.flate[0].areal, 25.0);
|
||||
fn lengdeenheten_hentes_fra_prosjektet() {
|
||||
let fil = step::les(&prove());
|
||||
assert_eq!(ifcgeom::lengdeenhet(&fil), 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vegg_uten_geometri_utelates() {
|
||||
let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'");
|
||||
let r = fra_tekst(&uten, "prove").unwrap();
|
||||
assert!(r.bygning.flate.is_empty());
|
||||
assert!(r
|
||||
.merknader
|
||||
.iter()
|
||||
.any(|m| m.contains("geometrien ikke ga noe areal")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,586 @@
|
||||
//! Geometri fra IFC: arealer og flatenormaler fra trekantnett, brep og ekstruderte profiler.
|
||||
//!
|
||||
//! BIM-verktøy eksporterer sjelden mengder for alle bygningsdeler, og IFC-visningen
|
||||
//! ReferenceView bruker tessellert geometri. Denne modulen regner derfor arealet av en
|
||||
//! bygningsdel direkte av geometrien: trekantene grupperes etter retning, og den største
|
||||
//! gruppen av parallelle flater er delens hovedflate. Summen deles på to fordi et lukket
|
||||
//! volum har en forside og en bakside.
|
||||
//!
|
||||
//! Metoden gir riktig areal også når vinduer og dører er skåret ut av veggen, siden hullene
|
||||
//! rett og slett mangler trekanter.
|
||||
|
||||
use crate::inndata::step::{Arg, Fil};
|
||||
|
||||
/// Et trekantnett i bygningsdelens lokale koordinater.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Nett {
|
||||
pub punkter: Vec<[f64; 3]>,
|
||||
pub trekanter: Vec<[usize; 3]>,
|
||||
}
|
||||
|
||||
/// Hovedflaten til en bygningsdel.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Hovedflate {
|
||||
/// Areal av én side, m².
|
||||
pub areal: f64,
|
||||
/// Enhetsnormal i lokale koordinater.
|
||||
pub normal: [f64; 3],
|
||||
/// Utstrekning i flatens plan: (u_min, u_maks, v_min, v_maks), m.
|
||||
plan: [f64; 4],
|
||||
}
|
||||
|
||||
impl Nett {
|
||||
pub fn tom(&self) -> bool {
|
||||
self.trekanter.is_empty()
|
||||
}
|
||||
|
||||
fn trekant(&self, t: [usize; 3]) -> Option<([f64; 3], f64)> {
|
||||
let (a, b, c) = (
|
||||
*self.punkter.get(t[0])?,
|
||||
*self.punkter.get(t[1])?,
|
||||
*self.punkter.get(t[2])?,
|
||||
);
|
||||
let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
let n = [
|
||||
u[1] * v[2] - u[2] * v[1],
|
||||
u[2] * v[0] - u[0] * v[2],
|
||||
u[0] * v[1] - u[1] * v[0],
|
||||
];
|
||||
let lengde = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
|
||||
if lengde < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
Some(([n[0] / lengde, n[1] / lengde, n[2] / lengde], 0.5 * lengde))
|
||||
}
|
||||
|
||||
/// Samlet overflate, m². Tar med begge sider av et lukket volum.
|
||||
pub fn overflate(&self, skala: f64) -> f64 {
|
||||
let s2 = skala * skala;
|
||||
self.trekanter
|
||||
.iter()
|
||||
.filter_map(|t| self.trekant(*t))
|
||||
.map(|(_, a)| a * s2)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Finner den største gruppen av parallelle trekanter, som er bygningsdelens hovedflate.
|
||||
///
|
||||
/// `skala` gjør lengdene om til meter. Trekanter regnes som parallelle når normalene
|
||||
/// er innenfor omtrent 18 grader, målt uten hensyn til retning, slik at for- og bakside
|
||||
/// havner i samme gruppe.
|
||||
pub fn hovedflate(&self, skala: f64) -> Option<Hovedflate> {
|
||||
let s2 = skala * skala;
|
||||
let mut flater: Vec<([f64; 3], f64)> = self
|
||||
.trekanter
|
||||
.iter()
|
||||
.filter_map(|t| self.trekant(*t))
|
||||
.map(|(n, a)| (n, a * s2))
|
||||
.collect();
|
||||
if flater.is_empty() {
|
||||
return None;
|
||||
}
|
||||
flater.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let mut grupper: Vec<([f64; 3], f64)> = Vec::new();
|
||||
for (n, a) in &flater {
|
||||
let treff = grupper
|
||||
.iter_mut()
|
||||
.find(|(g, _)| (g[0] * n[0] + g[1] * n[1] + g[2] * n[2]).abs() > 0.95);
|
||||
match treff {
|
||||
Some((_, sum)) => *sum += a,
|
||||
None => grupper.push((*n, *a)),
|
||||
}
|
||||
}
|
||||
let (normal, sum) = grupper
|
||||
.into_iter()
|
||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))?;
|
||||
Some(Hovedflate {
|
||||
areal: sum / 2.0,
|
||||
normal,
|
||||
plan: self.utstrekning(normal, skala),
|
||||
})
|
||||
}
|
||||
|
||||
/// Punktenes utstrekning i planet vinkelrett på `normal`, m.
|
||||
fn utstrekning(&self, normal: [f64; 3], skala: f64) -> [f64; 4] {
|
||||
let (u, v) = planakser(normal);
|
||||
let mut ut = [
|
||||
f64::INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
f64::INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
];
|
||||
for p in &self.punkter {
|
||||
let a = (p[0] * u[0] + p[1] * u[1] + p[2] * u[2]) * skala;
|
||||
let b = (p[0] * v[0] + p[1] * v[1] + p[2] * v[2]) * skala;
|
||||
ut[0] = ut[0].min(a);
|
||||
ut[1] = ut[1].max(a);
|
||||
ut[2] = ut[2].min(b);
|
||||
ut[3] = ut[3].max(b);
|
||||
}
|
||||
ut
|
||||
}
|
||||
}
|
||||
|
||||
/// To akser som spenner planet vinkelrett på `n`.
|
||||
fn planakser(n: [f64; 3]) -> ([f64; 3], [f64; 3]) {
|
||||
let hjelp = if n[2].abs() < 0.9 {
|
||||
[0.0, 0.0, 1.0]
|
||||
} else {
|
||||
[1.0, 0.0, 0.0]
|
||||
};
|
||||
let u = enhet([
|
||||
n[1] * hjelp[2] - n[2] * hjelp[1],
|
||||
n[2] * hjelp[0] - n[0] * hjelp[2],
|
||||
n[0] * hjelp[1] - n[1] * hjelp[0],
|
||||
])
|
||||
.unwrap_or([1.0, 0.0, 0.0]);
|
||||
let v = [
|
||||
n[1] * u[2] - n[2] * u[1],
|
||||
n[2] * u[0] - n[0] * u[2],
|
||||
n[0] * u[1] - n[1] * u[0],
|
||||
];
|
||||
(u, v)
|
||||
}
|
||||
|
||||
/// Slår sammen hovedflatene til en bygningsdels geometrideler til ett areal.
|
||||
///
|
||||
/// BIM-verktøy eksporterer ofte hvert materialsjikt i en vegg eller et dekke som sin egen
|
||||
/// geometridel. Sjiktene ligger parallelt og dekker den samme flaten, så de skal telles én
|
||||
/// gang. Deler som ligger i samme plan og overlapper hverandre regnes derfor som én flate,
|
||||
/// der den største av dem gjelder, mens deler som dekker forskjellige områder legges sammen.
|
||||
pub fn samlet_hovedflate(deler: &[Nett], skala: f64) -> Option<Hovedflate> {
|
||||
let mut flater: Vec<Hovedflate> = deler.iter().filter_map(|n| n.hovedflate(skala)).collect();
|
||||
if flater.is_empty() {
|
||||
return None;
|
||||
}
|
||||
flater.sort_by(|a, b| {
|
||||
b.areal
|
||||
.partial_cmp(&a.areal)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let mut grupper: Vec<Hovedflate> = Vec::new();
|
||||
for f in flater {
|
||||
let samme = grupper.iter().any(|g| {
|
||||
let parallell =
|
||||
(g.normal[0] * f.normal[0] + g.normal[1] * f.normal[1] + g.normal[2] * f.normal[2])
|
||||
.abs()
|
||||
> 0.95;
|
||||
parallell && overlapper(&g.plan, &f.plan)
|
||||
});
|
||||
if !samme {
|
||||
grupper.push(f);
|
||||
}
|
||||
}
|
||||
let areal = grupper.iter().map(|g| g.areal).sum();
|
||||
Some(Hovedflate {
|
||||
areal,
|
||||
..grupper[0]
|
||||
})
|
||||
}
|
||||
|
||||
/// Sant når to utstrekninger dekker mer enn halvparten av den minste i begge retninger.
|
||||
fn overlapper(a: &[f64; 4], b: &[f64; 4]) -> bool {
|
||||
let dekning = |a0: f64, a1: f64, b0: f64, b1: f64| {
|
||||
let felles = (a1.min(b1) - a0.max(b0)).max(0.0);
|
||||
let minste = (a1 - a0).min(b1 - b0);
|
||||
if minste <= 1e-9 {
|
||||
felles > 0.0
|
||||
} else {
|
||||
felles / minste > 0.5
|
||||
}
|
||||
};
|
||||
dekning(a[0], a[1], b[0], b[1]) && dekning(a[2], a[3], b[2], b[3])
|
||||
}
|
||||
|
||||
/// Henter geometrien til et produkt via `Representation`, med én oppføring per geometridel.
|
||||
pub fn netter_for_produkt(fil: &Fil, representasjon: Option<u64>) -> Vec<Nett> {
|
||||
let mut deler = Vec::new();
|
||||
let Some(r) = representasjon else {
|
||||
return deler;
|
||||
};
|
||||
let Some(form) = fil.hent(r) else {
|
||||
return deler;
|
||||
};
|
||||
// IfcProductDefinitionShape.Representations
|
||||
for rep in form.arg(2).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(sr) = rep.som_ref().and_then(|x| fil.hent(x)) else {
|
||||
continue;
|
||||
};
|
||||
if sr.typ != "IFCSHAPEREPRESENTATION" {
|
||||
continue;
|
||||
}
|
||||
// Bare kroppen beskriver volumet; akse- og fotavtrykksrepresentasjoner hoppes over.
|
||||
if sr.arg(1).and_then(|a| a.som_tekst()) != Some("Body") {
|
||||
continue;
|
||||
}
|
||||
for post in sr.arg(3).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
if let Some(n) = post.som_ref() {
|
||||
let mut nett = Nett::default();
|
||||
les_element(fil, n, &mut nett, 0);
|
||||
if !nett.tom() {
|
||||
deler.push(nett);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
deler
|
||||
}
|
||||
|
||||
fn les_element(fil: &Fil, r: u64, nett: &mut Nett, dybde: u32) {
|
||||
if dybde > 8 {
|
||||
return;
|
||||
}
|
||||
let Some(e) = fil.hent(r) else {
|
||||
return;
|
||||
};
|
||||
match e.typ.as_str() {
|
||||
"IFCTRIANGULATEDFACESET" => les_trekantnett(fil, r, nett),
|
||||
"IFCPOLYGONALFACESET" => les_polygonnett(fil, r, nett),
|
||||
"IFCFACETEDBREP" | "IFCADVANCEDBREP" => {
|
||||
if let Some(skall) = e.arg(0).and_then(|a| a.som_ref()) {
|
||||
les_skall(fil, skall, nett);
|
||||
}
|
||||
}
|
||||
"IFCEXTRUDEDAREASOLID" => les_ekstrudering(fil, r, nett),
|
||||
"IFCMAPPEDITEM" => {
|
||||
// Kartlagt geometri: følg kilden. Transformasjonen ses bort fra, siden
|
||||
// arealet er uavhengig av flytting og rotasjon.
|
||||
if let Some(kilde) = e.arg(0).and_then(|a| a.som_ref()).and_then(|k| fil.hent(k)) {
|
||||
if let Some(rep) = kilde
|
||||
.arg(1)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|x| fil.hent(x))
|
||||
{
|
||||
for post in rep.arg(3).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
if let Some(n) = post.som_ref() {
|
||||
les_element(fil, n, nett, dybde + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => {
|
||||
// Bruk førsteoperanden; utsparinger gir uansett litt for stort areal.
|
||||
if let Some(f) = e.arg(1).and_then(|a| a.som_ref()) {
|
||||
les_element(fil, f, nett, dybde + 1);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn punktliste(fil: &Fil, r: u64) -> Vec<[f64; 3]> {
|
||||
let Some(e) = fil.hent(r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
e.arg(0)
|
||||
.map(|a| a.som_liste())
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let k = p.som_liste();
|
||||
let g = |i: usize| k.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||
[g(0), g(1), g(2)]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn les_trekantnett(fil: &Fil, r: u64, nett: &mut Nett) {
|
||||
let Some(e) = fil.hent(r) else { return };
|
||||
let Some(liste) = e.arg(0).and_then(|a| a.som_ref()) else {
|
||||
return;
|
||||
};
|
||||
let start = nett.punkter.len();
|
||||
nett.punkter.extend(punktliste(fil, liste));
|
||||
// CoordIndex er 1-basert.
|
||||
for t in e.arg(3).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let k = t.som_liste();
|
||||
if k.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let g = |i: usize| k[i].som_tall().unwrap_or(1.0) as usize;
|
||||
nett.trekanter.push([
|
||||
start + g(0).saturating_sub(1),
|
||||
start + g(1).saturating_sub(1),
|
||||
start + g(2).saturating_sub(1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
fn les_polygonnett(fil: &Fil, r: u64, nett: &mut Nett) {
|
||||
let Some(e) = fil.hent(r) else { return };
|
||||
let Some(liste) = e.arg(0).and_then(|a| a.som_ref()) else {
|
||||
return;
|
||||
};
|
||||
let start = nett.punkter.len();
|
||||
nett.punkter.extend(punktliste(fil, liste));
|
||||
for f in e.arg(2).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(fe) = f.som_ref().and_then(|x| fil.hent(x)) else {
|
||||
continue;
|
||||
};
|
||||
let indekser: Vec<usize> = fe
|
||||
.arg(0)
|
||||
.map(|a| a.som_liste())
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|x| x.som_tall())
|
||||
.map(|v| start + (v as usize).saturating_sub(1))
|
||||
.collect();
|
||||
vifte(&indekser, nett);
|
||||
}
|
||||
}
|
||||
|
||||
fn les_skall(fil: &Fil, r: u64, nett: &mut Nett) {
|
||||
let Some(skall) = fil.hent(r) else { return };
|
||||
for f in skall.arg(0).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(flate) = f.som_ref().and_then(|x| fil.hent(x)) else {
|
||||
continue;
|
||||
};
|
||||
for b in flate.arg(0).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(grense) = b.som_ref().and_then(|x| fil.hent(x)) else {
|
||||
continue;
|
||||
};
|
||||
// Bare ytre grense; hull i flaten gir litt for stort areal.
|
||||
if grense.typ != "IFCFACEOUTERBOUND" {
|
||||
continue;
|
||||
}
|
||||
let Some(lokke) = grense
|
||||
.arg(0)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|x| fil.hent(x))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut indekser = Vec::new();
|
||||
for p in lokke.arg(0).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
if let Some(pr) = p.som_ref() {
|
||||
if let Some(pe) = fil.hent(pr) {
|
||||
let k = pe.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let g = |i: usize| k.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||
indekser.push(nett.punkter.len());
|
||||
nett.punkter.push([g(0), g(1), g(2)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
vifte(&indekser, nett);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deler en polygonløkke i trekanter fra første hjørne.
|
||||
fn vifte(indekser: &[usize], nett: &mut Nett) {
|
||||
for i in 1..indekser.len().saturating_sub(1) {
|
||||
nett.trekanter
|
||||
.push([indekser[0], indekser[i], indekser[i + 1]]);
|
||||
}
|
||||
}
|
||||
|
||||
fn les_ekstrudering(fil: &Fil, r: u64, nett: &mut Nett) {
|
||||
let Some(e) = fil.hent(r) else { return };
|
||||
let Some(profil) = e.arg(0).and_then(|a| a.som_ref()).and_then(|x| fil.hent(x)) else {
|
||||
return;
|
||||
};
|
||||
let dybde = e.arg(3).and_then(|a| a.som_tall()).unwrap_or(0.0);
|
||||
let retning = e
|
||||
.arg(2)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|x| fil.hent(x))
|
||||
.map(|d| {
|
||||
let k = d.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let g = |i: usize| k.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||
[g(0), g(1), g(2)]
|
||||
})
|
||||
.unwrap_or([0.0, 0.0, 1.0]);
|
||||
|
||||
// Profilet ligger i xy-planet. Hent ytre kurve som punkter.
|
||||
let punkter = profilpunkter(fil, profil);
|
||||
if punkter.len() < 3 {
|
||||
return;
|
||||
}
|
||||
let start = nett.punkter.len();
|
||||
for p in &punkter {
|
||||
nett.punkter.push([p[0], p[1], 0.0]);
|
||||
}
|
||||
let topp = nett.punkter.len();
|
||||
for p in &punkter {
|
||||
nett.punkter.push([
|
||||
p[0] + retning[0] * dybde,
|
||||
p[1] + retning[1] * dybde,
|
||||
retning[2] * dybde,
|
||||
]);
|
||||
}
|
||||
// Bunn og topp.
|
||||
let bunn: Vec<usize> = (start..topp).collect();
|
||||
let topp_i: Vec<usize> = (topp..nett.punkter.len()).collect();
|
||||
vifte(&bunn, nett);
|
||||
vifte(&topp_i, nett);
|
||||
// Sideflater.
|
||||
let n = punkter.len();
|
||||
for i in 0..n {
|
||||
let j = (i + 1) % n;
|
||||
nett.trekanter.push([start + i, start + j, topp + j]);
|
||||
nett.trekanter.push([start + i, topp + j, topp + i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn profilpunkter(fil: &Fil, profil: &crate::inndata::step::Entitet) -> Vec<[f64; 2]> {
|
||||
match profil.typ.as_str() {
|
||||
"IFCARBITRARYCLOSEDPROFILEDEF" | "IFCARBITRARYPROFILEDEFWITHVOIDS" => {
|
||||
let Some(kurve) = profil
|
||||
.arg(2)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|x| fil.hent(x))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if kurve.typ != "IFCPOLYLINE" {
|
||||
return Vec::new();
|
||||
}
|
||||
kurve
|
||||
.arg(0)
|
||||
.map(|a| a.som_liste())
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|p| p.som_ref().and_then(|x| fil.hent(x)))
|
||||
.map(|pe| {
|
||||
let k = pe.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let g = |i: usize| k.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||
[g(0), g(1)]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
"IFCRECTANGLEPROFILEDEF" => {
|
||||
let x = profil.arg(3).and_then(|a| a.som_tall()).unwrap_or(0.0) / 2.0;
|
||||
let y = profil.arg(4).and_then(|a| a.som_tall()).unwrap_or(0.0) / 2.0;
|
||||
vec![[-x, -y], [x, -y], [x, y], [-x, y]]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lengdeenheten i meter, hentet gjennom prosjektets enhetsoppsett.
|
||||
///
|
||||
/// En IFC-fil kan inneholde flere `IfcSIUnit` med lengdeenhet, for eksempel når et
|
||||
/// underoppsett bruker meter mens modellen er i millimeter. Derfor må enheten slås opp
|
||||
/// gjennom `IfcProject.UnitsInContext` og ikke ved å lete gjennom alle enheter.
|
||||
pub fn lengdeenhet(fil: &Fil) -> f64 {
|
||||
let fra_oppsett = fil
|
||||
.av_type("IFCPROJECT")
|
||||
.next()
|
||||
.and_then(|(_, p)| p.arg(8).and_then(|a| a.som_ref()))
|
||||
.and_then(|r| fil.hent(r))
|
||||
.and_then(|oppsett| {
|
||||
oppsett
|
||||
.arg(0)
|
||||
.map(|a| a.som_liste())
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|u| u.som_ref().and_then(|r| fil.hent(r)))
|
||||
.find(|e| {
|
||||
e.typ == "IFCSIUNIT"
|
||||
&& e.arg(1).and_then(|a| a.som_tekst()) == Some("LENGTHUNIT")
|
||||
})
|
||||
.map(prefiksfaktor)
|
||||
});
|
||||
fra_oppsett.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
fn prefiksfaktor(e: &crate::inndata::step::Entitet) -> f64 {
|
||||
match e.arg(2).and_then(|a| a.som_tekst()).unwrap_or("") {
|
||||
"MILLI" => 0.001,
|
||||
"CENTI" => 0.01,
|
||||
"DECI" => 0.1,
|
||||
"KILO" => 1000.0,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhetsvektor, eller `None` for en nullvektor.
|
||||
pub fn enhet(v: [f64; 3]) -> Option<[f64; 3]> {
|
||||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
(l > 1e-12).then(|| [v[0] / l, v[1] / l, v[2] / l])
|
||||
}
|
||||
|
||||
/// Sant når et argument er en liste med minst ett element.
|
||||
pub fn har_innhold(a: Option<&Arg>) -> bool {
|
||||
a.map(|x| !x.som_liste().is_empty()).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
/// En kasse på 4 × 3 × 0,2 m skal gi hovedflate 12 m² med normal langs z.
|
||||
#[test]
|
||||
fn hovedflate_av_kasse() {
|
||||
let mut n = Nett::default();
|
||||
let (x, y, z) = (4.0, 3.0, 0.2);
|
||||
let p = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[x, 0.0, 0.0],
|
||||
[x, y, 0.0],
|
||||
[0.0, y, 0.0],
|
||||
[0.0, 0.0, z],
|
||||
[x, 0.0, z],
|
||||
[x, y, z],
|
||||
[0.0, y, z],
|
||||
];
|
||||
n.punkter.extend_from_slice(&p);
|
||||
let flater = [
|
||||
[0, 1, 2],
|
||||
[0, 2, 3], // bunn
|
||||
[4, 6, 5],
|
||||
[4, 7, 6], // topp
|
||||
[0, 4, 5],
|
||||
[0, 5, 1],
|
||||
[1, 5, 6],
|
||||
[1, 6, 2],
|
||||
[2, 6, 7],
|
||||
[2, 7, 3],
|
||||
[3, 7, 4],
|
||||
[3, 4, 0],
|
||||
];
|
||||
n.trekanter.extend_from_slice(&flater);
|
||||
let h = n.hovedflate(1.0).unwrap();
|
||||
assert!((h.areal - 12.0).abs() < 1e-9, "areal ble {}", h.areal);
|
||||
assert!(h.normal[2].abs() > 0.99, "normal ble {:?}", h.normal);
|
||||
}
|
||||
|
||||
/// Et hull i flaten skal trekkes fra, siden hullet mangler trekanter.
|
||||
#[test]
|
||||
fn hull_reduserer_hovedflaten() {
|
||||
let mut n = Nett::default();
|
||||
// To flater på 4 × 3 med et hull på 1 × 1 er modellert som fire striper.
|
||||
for z in [0.0, 0.2] {
|
||||
let base = n.punkter.len();
|
||||
n.punkter.extend_from_slice(&[
|
||||
[0.0, 0.0, z],
|
||||
[4.0, 0.0, z],
|
||||
[4.0, 1.0, z],
|
||||
[0.0, 1.0, z],
|
||||
]);
|
||||
n.trekanter.push([base, base + 1, base + 2]);
|
||||
n.trekanter.push([base, base + 2, base + 3]);
|
||||
}
|
||||
let h = n.hovedflate(1.0).unwrap();
|
||||
assert!((h.areal - 4.0).abs() < 1e-9, "areal ble {}", h.areal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skala_gjor_om_millimeter_til_kvadratmeter() {
|
||||
let mut n = Nett::default();
|
||||
n.punkter.extend_from_slice(&[
|
||||
[0.0, 0.0, 0.0],
|
||||
[2000.0, 0.0, 0.0],
|
||||
[2000.0, 1000.0, 0.0],
|
||||
[0.0, 1000.0, 0.0],
|
||||
]);
|
||||
n.trekanter.push([0, 1, 2]);
|
||||
n.trekanter.push([0, 2, 3]);
|
||||
// Én enkelt flate: hovedflaten deler på to, så 2 m² blir 1 m².
|
||||
let h = n.hovedflate(0.001).unwrap();
|
||||
assert!((h.areal - 1.0).abs() < 1e-9, "areal ble {}", h.areal);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod gbxml;
|
||||
pub mod ifc;
|
||||
pub mod ifcgeom;
|
||||
pub mod regneark;
|
||||
pub mod step;
|
||||
pub mod toml_inn;
|
||||
|
||||
@@ -127,6 +127,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
over_terreng: None,
|
||||
sone: None,
|
||||
kommentar: None,
|
||||
});
|
||||
}
|
||||
@@ -197,6 +199,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
sone: Vec::new(),
|
||||
grunn: Grunn::default(),
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
|
||||
+24
-1
@@ -139,8 +139,31 @@ pub fn kjor_med_klima(
|
||||
let systemer_oppsett = systemer::sett_opp_systemer(b, &lost, kilde)?;
|
||||
let mut merknader = systemer_oppsett.merknader.clone();
|
||||
|
||||
// Tap fra varmerør og bereder som står i en uoppvarmet sone varmer den sonen, og
|
||||
// reduserer dermed varmetapet gjennom etasjeskilleren. Andelen som avgis i den
|
||||
// oppvarmede delen teller som uregulerbart varmetilskudd der.
|
||||
let tappevannstap = systemer::tappevannstilskudd_totalt(&lost, &systemer_oppsett);
|
||||
let sone_tappevann: f64 = lost
|
||||
.soner
|
||||
.iter()
|
||||
.map(|s| s.andel_tappevannstap)
|
||||
.sum::<f64>()
|
||||
.min(1.0);
|
||||
let tilskudd = Systemtilskudd {
|
||||
tappevann: systemer::tappevannstilskudd(&lost, &systemer_oppsett),
|
||||
tappevann: tappevannstap * systemer_oppsett.tappevann_tap_i_sone * (1.0 - sone_tappevann),
|
||||
sone_fast: lost
|
||||
.soner
|
||||
.iter()
|
||||
.map(|s| s.fast_effekt + tappevannstap * s.andel_tappevannstap)
|
||||
.collect(),
|
||||
sone_andel_romvarme: lost
|
||||
.soner
|
||||
.iter()
|
||||
.map(|s| {
|
||||
s.andel_distribusjonstap * systemer_oppsett.distribusjon_tap_ut
|
||||
/ (1.0 - systemer_oppsett.distribusjon_tap_ut).max(1e-6)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let timeserie = simuler(&lost, &vaerdata, tilskudd)?;
|
||||
let budsjett = systemer::energibudsjett(×erie, &systemer_oppsett);
|
||||
|
||||
+263
-1
@@ -4,6 +4,7 @@
|
||||
|
||||
use crate::data::{Datakilde, Materialer, Normerte};
|
||||
use crate::modell::*;
|
||||
use crate::motor::grunn;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use serde::Serialize;
|
||||
@@ -31,7 +32,11 @@ pub struct LostFlate {
|
||||
pub navn: String,
|
||||
pub typ: FlateType,
|
||||
pub areal: f64,
|
||||
/// U-verdien som brukes i timesberegningen. Mot grunnen er den korrigert etter
|
||||
/// NS-EN ISO 13370 og dermed lavere enn konstruksjonens egen.
|
||||
pub u_verdi: f64,
|
||||
/// Konstruksjonens egen U-verdi, som varmetapsbudsjettet og TEK-kravene gjelder.
|
||||
pub u_konstruksjon: f64,
|
||||
pub helning: f64,
|
||||
pub orientering: f64,
|
||||
pub kappa: f64,
|
||||
@@ -41,6 +46,8 @@ pub struct LostFlate {
|
||||
pub absorptans: f64,
|
||||
/// Sant for vinduer og dører (to noder, ingen masse, solinnstråling gjennom glasset).
|
||||
pub glass: bool,
|
||||
/// Indeks til den uoppvarmede sonen flaten vender mot, når temperaturen beregnes.
|
||||
pub sone: Option<usize>,
|
||||
/// Effektiv solfaktor inkl. karmandel og skjerming (bare vinduer).
|
||||
pub g_effektiv: f64,
|
||||
/// Skjermingsfaktor når solskjerming er i bruk (1,0 = ingen skjerming).
|
||||
@@ -49,6 +56,39 @@ pub struct LostFlate {
|
||||
pub horisont: f64,
|
||||
}
|
||||
|
||||
/// En uoppvarmet sone med varmebalanse etter NS-EN ISO 13789.
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct LostSone {
|
||||
pub navn: String,
|
||||
/// Varmetransport fra den oppvarmede delen inn i sonen, W/K.
|
||||
pub h_mot_oppvarmet: f64,
|
||||
/// Varmetransport fra sonen til uteluft, inkludert ventilasjon, W/K.
|
||||
pub h_mot_ute: f64,
|
||||
/// Varmetransport fra sonen til grunnen, W/K.
|
||||
pub h_mot_grunn: f64,
|
||||
/// Andel av varmeanleggets distribusjonstap som avgis i sonen.
|
||||
pub andel_distribusjonstap: f64,
|
||||
/// Andel av tappevannsanleggets tap som avgis i sonen.
|
||||
pub andel_tappevannstap: f64,
|
||||
/// Fast varmetilskudd, W.
|
||||
pub fast_effekt: f64,
|
||||
}
|
||||
|
||||
impl LostSone {
|
||||
/// Sonens temperatur, °C, av den stasjonære varmebalansen.
|
||||
pub fn temperatur(&self, t_inne: f64, t_ute: f64, t_grunn: f64, tilskudd: f64) -> f64 {
|
||||
let h = self.h_mot_oppvarmet + self.h_mot_ute + self.h_mot_grunn;
|
||||
if h <= 1e-9 {
|
||||
return t_ute;
|
||||
}
|
||||
(self.h_mot_oppvarmet * t_inne
|
||||
+ self.h_mot_ute * t_ute
|
||||
+ self.h_mot_grunn * t_grunn
|
||||
+ tilskudd)
|
||||
/ h
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Lost {
|
||||
pub navn: String,
|
||||
@@ -60,6 +100,7 @@ pub struct Lost {
|
||||
/// Areal som brukes i arealkorreksjonen på karakterskalaen.
|
||||
pub skala_areal: f64,
|
||||
pub flater: Vec<LostFlate>,
|
||||
pub soner: Vec<LostSone>,
|
||||
/// W/K, summert transmisjonstap uten kuldebroer.
|
||||
pub kuldebro_w_per_k: f64,
|
||||
pub n50: f64,
|
||||
@@ -128,9 +169,43 @@ impl Lost {
|
||||
|
||||
let periode = materialer.periode_for(info.byggeaar);
|
||||
|
||||
let ledningsevne = b.grunn.ledningsevne.unwrap_or_else(|| {
|
||||
if b.flate
|
||||
.iter()
|
||||
.chain(b.sone.iter().flat_map(|s| s.flater.iter()))
|
||||
.any(|x| x.mot == Mot::Grunn)
|
||||
{
|
||||
f.push(Forutsetning {
|
||||
felt: "grunn.ledningsevne".into(),
|
||||
verdi: format!("{:.1} W/(m·K)", grunn::LEDNINGSEVNE_JORD),
|
||||
kilde: "NS-EN ISO 13370 tabell 1, leire eller silt".into(),
|
||||
});
|
||||
}
|
||||
grunn::LEDNINGSEVNE_JORD
|
||||
});
|
||||
|
||||
// Sonenavn må være kjent før flatene løses, slik at flatene kan peke på dem.
|
||||
let sonenavn: Vec<String> = b.sone.iter().map(|s| s.navn.clone()).collect();
|
||||
for fl in &b.flate {
|
||||
if let Some(n) = &fl.sone {
|
||||
if !sonenavn.contains(n) {
|
||||
bail!("flaten «{}» viser til den ukjente sonen «{}»", fl.navn, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut flater = Vec::new();
|
||||
for fl in &b.flate {
|
||||
flater.push(los_flate(fl, &materialer, periode, &mut f)?);
|
||||
los_flate_med_grunn(
|
||||
fl,
|
||||
&b.flate,
|
||||
&materialer,
|
||||
periode,
|
||||
ledningsevne,
|
||||
&sonenavn,
|
||||
&mut flater,
|
||||
&mut f,
|
||||
)?;
|
||||
}
|
||||
for v in &b.vindu {
|
||||
flater.push(los_vindu(v, &mut f));
|
||||
@@ -139,6 +214,65 @@ impl Lost {
|
||||
bail!("bygningen har ingen flater; legg inn minst én [[flate]]");
|
||||
}
|
||||
|
||||
// Uoppvarmede soner.
|
||||
let mut soner = Vec::new();
|
||||
for (i, sone) in b.sone.iter().enumerate() {
|
||||
let mut egne = Vec::new();
|
||||
for fl in &sone.flater {
|
||||
los_flate_med_grunn(
|
||||
fl,
|
||||
&sone.flater,
|
||||
&materialer,
|
||||
periode,
|
||||
ledningsevne,
|
||||
&[],
|
||||
&mut egne,
|
||||
&mut f,
|
||||
)?;
|
||||
}
|
||||
let h_mot_oppvarmet: f64 = flater
|
||||
.iter()
|
||||
.filter(|x| x.sone == Some(i))
|
||||
.map(|x| x.areal * x.u_verdi)
|
||||
.sum();
|
||||
if h_mot_oppvarmet <= 1e-9 {
|
||||
bail!(
|
||||
"sonen «{}» har ingen flater mot den oppvarmede delen; sett `sone = \"{}\"` på etasjeskilleren",
|
||||
sone.navn,
|
||||
sone.navn
|
||||
);
|
||||
}
|
||||
let volum = sone.volum.unwrap_or(0.0);
|
||||
let luftveksling = sone.luftveksling.unwrap_or_else(|| {
|
||||
f.push(Forutsetning {
|
||||
felt: format!("sone.{}.luftveksling", sone.navn),
|
||||
verdi: "0,3 h⁻¹".into(),
|
||||
kilde: "antatt luftveksling i uoppvarmet sone".into(),
|
||||
});
|
||||
0.3
|
||||
});
|
||||
let h_mot_ute: f64 = egne
|
||||
.iter()
|
||||
.filter(|x| x.mot == Mot::Ute)
|
||||
.map(|x| x.areal * x.u_verdi)
|
||||
.sum::<f64>()
|
||||
+ volum * luftveksling * crate::motor::RHO_C_LUFT;
|
||||
let h_mot_grunn: f64 = egne
|
||||
.iter()
|
||||
.filter(|x| x.mot == Mot::Grunn)
|
||||
.map(|x| x.areal * x.u_verdi)
|
||||
.sum();
|
||||
soner.push(LostSone {
|
||||
navn: sone.navn.clone(),
|
||||
h_mot_oppvarmet,
|
||||
h_mot_ute,
|
||||
h_mot_grunn,
|
||||
andel_distribusjonstap: sone.andel_distribusjonstap.unwrap_or(1.0),
|
||||
andel_tappevannstap: sone.andel_tappevannstap.unwrap_or(0.0),
|
||||
fast_effekt: sone.fast_effekt,
|
||||
});
|
||||
}
|
||||
|
||||
// Kuldebroer.
|
||||
let kuldebro_w_per_k = if !b.kuldebroer.liste.is_empty() {
|
||||
b.kuldebroer.liste.iter().map(|k| k.psi * k.lengde).sum()
|
||||
@@ -243,6 +377,7 @@ impl Lost {
|
||||
antall_boenheter: info.antall_boenheter.max(1),
|
||||
skala_areal,
|
||||
flater,
|
||||
soner,
|
||||
kuldebro_w_per_k,
|
||||
n50,
|
||||
skjerming: b.infiltrasjon.skjerming,
|
||||
@@ -279,6 +414,129 @@ impl Lost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Løser en flate, og deler kjellervegger i en del over og en del under terreng.
|
||||
///
|
||||
/// Mot grunnen brukes NS-EN ISO 13370: jordas varmemotstand gjør at en uisolert kjellervegg
|
||||
/// eller et uisolert kjellergulv taper langt mindre varme enn U-verdien alene skulle tilsi.
|
||||
/// Den delen av kjellerveggen som står over terreng er derimot en vanlig yttervegg, og
|
||||
/// arealet deles mellom de to i forhold til høydene.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn los_flate_med_grunn(
|
||||
fl: &Flate,
|
||||
naboflater: &[Flate],
|
||||
materialer: &Materialer,
|
||||
periode: Option<&crate::data::Periode>,
|
||||
ledningsevne: f64,
|
||||
sonenavn: &[String],
|
||||
ut: &mut Vec<LostFlate>,
|
||||
logg: &mut Vec<Forutsetning>,
|
||||
) -> Result<()> {
|
||||
let mut lost = los_flate(fl, materialer, periode, logg)?;
|
||||
lost.sone = fl
|
||||
.sone
|
||||
.as_ref()
|
||||
.and_then(|n| sonenavn.iter().position(|x| x == n));
|
||||
|
||||
if fl.mot != Mot::Grunn {
|
||||
ut.push(lost);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Gulvet mot grunnen gir den ekvivalente tykkelsen som kjellerveggene regnes mot.
|
||||
let gulv = naboflater
|
||||
.iter()
|
||||
.filter(|x| x.mot == Mot::Grunn && x.typ == FlateType::Gulv)
|
||||
.max_by(|a, b| {
|
||||
a.areal
|
||||
.partial_cmp(&b.areal)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let (u_gulv_k, areal_gulv, omkrets) = match gulv {
|
||||
Some(g) => (
|
||||
los_flate(g, materialer, periode, &mut Vec::new())?.u_verdi,
|
||||
g.areal,
|
||||
g.omkrets.unwrap_or(0.0),
|
||||
),
|
||||
None => (lost.u_verdi, fl.areal, fl.omkrets.unwrap_or(0.0)),
|
||||
};
|
||||
|
||||
match fl.typ {
|
||||
FlateType::Gulv => {
|
||||
let dybde = fl.dybde.unwrap_or(0.0);
|
||||
if fl.omkrets.is_none() {
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("flate.{}.omkrets", fl.navn),
|
||||
verdi: format!("{:.1} m", 4.0 * fl.areal.sqrt()),
|
||||
kilde: "antatt kvadratisk gulv; oppgi omkrets for riktig B′".into(),
|
||||
});
|
||||
}
|
||||
let u = grunn::u_gulv(
|
||||
lost.u_verdi,
|
||||
fl.areal,
|
||||
fl.omkrets.unwrap_or(0.0),
|
||||
dybde,
|
||||
ledningsevne,
|
||||
);
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("flate.{}.u_mot_grunn", fl.navn),
|
||||
verdi: format!("{u:.3} W/(m²K)"),
|
||||
kilde: format!(
|
||||
"NS-EN ISO 13370 av konstruksjonens {:.2} W/(m²K) og {dybde:.1} m dybde",
|
||||
lost.u_verdi
|
||||
),
|
||||
});
|
||||
lost.u_verdi = u;
|
||||
ut.push(lost);
|
||||
}
|
||||
FlateType::Vegg | FlateType::Tak => {
|
||||
let dybde = fl.dybde.unwrap_or(0.0);
|
||||
let over = fl.over_terreng.unwrap_or(0.0);
|
||||
let hoyde = dybde + over;
|
||||
let andel_over = if hoyde > 1e-6 { over / hoyde } else { 0.0 };
|
||||
if fl.dybde.is_none() {
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("flate.{}.dybde", fl.navn),
|
||||
verdi: "0 m".into(),
|
||||
kilde: "uten oppgitt dybde regnes veggen som å stå i terrengnivå".into(),
|
||||
});
|
||||
}
|
||||
if andel_over > 1e-6 {
|
||||
let mut over_flate = lost.clone();
|
||||
over_flate.navn = format!("{} (over terreng)", lost.navn);
|
||||
over_flate.areal = fl.areal * andel_over;
|
||||
over_flate.mot = Mot::Ute;
|
||||
ut.push(over_flate);
|
||||
}
|
||||
if andel_over < 1.0 - 1e-6 {
|
||||
let u = grunn::u_kjellervegg(
|
||||
lost.u_verdi,
|
||||
u_gulv_k,
|
||||
areal_gulv,
|
||||
omkrets,
|
||||
dybde,
|
||||
ledningsevne,
|
||||
);
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("flate.{}.u_mot_grunn", fl.navn),
|
||||
verdi: format!("{u:.3} W/(m²K)"),
|
||||
kilde: format!(
|
||||
"NS-EN ISO 13370 av konstruksjonens {:.2} W/(m²K) og {dybde:.1} m under terreng",
|
||||
lost.u_verdi
|
||||
),
|
||||
});
|
||||
let mut under = lost;
|
||||
if andel_over > 1e-6 {
|
||||
under.navn = format!("{} (under terreng)", under.navn);
|
||||
}
|
||||
under.areal = fl.areal * (1.0 - andel_over);
|
||||
under.u_verdi = u;
|
||||
ut.push(under);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn los_flate(
|
||||
fl: &Flate,
|
||||
materialer: &Materialer,
|
||||
@@ -358,6 +616,7 @@ fn los_flate(
|
||||
typ: fl.typ,
|
||||
areal: fl.areal,
|
||||
u_verdi: u,
|
||||
u_konstruksjon: u,
|
||||
helning,
|
||||
orientering: fl.orientering,
|
||||
kappa,
|
||||
@@ -366,6 +625,7 @@ fn los_flate(
|
||||
b_faktor,
|
||||
absorptans: fl.absorptans.unwrap_or(0.6),
|
||||
glass: false,
|
||||
sone: None,
|
||||
g_effektiv: 0.0,
|
||||
skjermingsfaktor: 1.0,
|
||||
automatisk_skjerming: false,
|
||||
@@ -416,6 +676,7 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
|
||||
typ: FlateType::Vegg,
|
||||
areal,
|
||||
u_verdi: v.u_verdi,
|
||||
u_konstruksjon: v.u_verdi,
|
||||
helning: v.helning,
|
||||
orientering: v.orientering,
|
||||
kappa: 0.0,
|
||||
@@ -424,6 +685,7 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
|
||||
b_faktor: 1.0,
|
||||
absorptans: 0.0,
|
||||
glass: true,
|
||||
sone: None,
|
||||
g_effektiv,
|
||||
skjermingsfaktor,
|
||||
automatisk_skjerming: automatisk,
|
||||
|
||||
+55
-1
@@ -53,6 +53,11 @@ pub struct Bygning {
|
||||
pub klima: Klima,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub flate: Vec<Flate>,
|
||||
/// Uoppvarmede soner, typisk kjeller eller loft, med egen temperaturberegning.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub sone: Vec<Sone>,
|
||||
#[serde(default)]
|
||||
pub grunn: Grunn,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub vindu: Vec<Vindu>,
|
||||
#[serde(default)]
|
||||
@@ -212,11 +217,20 @@ pub struct Flate {
|
||||
/// Solabsorptans for utvendig overflate. Utelatt: 0,6.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub absorptans: Option<f64>,
|
||||
/// For gulv mot grunn / kjellervegg mot grunn: eksponert omkrets (m) og dybde under terreng (m).
|
||||
/// Gulv mot grunn: eksponert omkrets, m. Brukes i den karakteristiske dimensjonen B′.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub omkrets: Option<f64>,
|
||||
/// Dybde under terreng, m. For gulv: gulvets dybde. For kjellervegg: veggens nedgravde høyde.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dybde: Option<f64>,
|
||||
/// Kjellervegg: høyden som står over terreng, m. Den delen regnes som vanlig yttervegg,
|
||||
/// og arealet deles mellom over og under terreng i forhold til høydene.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub over_terreng: Option<f64>,
|
||||
/// Navnet på den uoppvarmede sonen flaten vender mot, når `mot = "uoppvarmet"`.
|
||||
/// Uten dette brukes temperaturfaktoren `b_faktor` i stedet.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sone: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kommentar: Option<String>,
|
||||
}
|
||||
@@ -276,6 +290,46 @@ fn nitti() -> f64 {
|
||||
90.0
|
||||
}
|
||||
|
||||
/// Grunnforhold for beregning etter NS-EN ISO 13370.
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Grunn {
|
||||
/// Varmeledningsevne for jorda, W/(m·K). Utelatt: 2,0 (leire eller silt).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ledningsevne: Option<f64>,
|
||||
}
|
||||
|
||||
/// En uoppvarmet sone, typisk kjeller, loft eller garasje.
|
||||
///
|
||||
/// Sonen har ingen egen oppvarming, men får varme fra den oppvarmede delen gjennom
|
||||
/// etasjeskilleren og fra varmerør og bereder som står der. Temperaturen regnes ut time for
|
||||
/// time av varmebalansen etter NS-EN ISO 13789, slik at et varmt rørnett i kjelleren både
|
||||
/// teller som tap fra varmeanlegget og som varme som holder etasjeskilleren varm.
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Sone {
|
||||
pub navn: String,
|
||||
/// Luftvolum, m³. Brukes til ventilasjonstapet fra sonen.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub volum: Option<f64>,
|
||||
/// Luftvekslinger per time mot det fri. Utelatt: 0,3 for en kjeller.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub luftveksling: Option<f64>,
|
||||
/// Sonens egne flater mot det fri og mot grunnen.
|
||||
#[serde(default, rename = "flate", skip_serializing_if = "Vec::is_empty")]
|
||||
pub flater: Vec<Flate>,
|
||||
/// Andel av varmeanleggets distribusjonstap som avgis i denne sonen. Utelatt: 1,0 når
|
||||
/// bygningen har vannbåren varme og sonen er den eneste uoppvarmede.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub andel_distribusjonstap: Option<f64>,
|
||||
/// Andel av tappevannsanleggets tap som avgis her.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub andel_tappevannstap: Option<f64>,
|
||||
/// Fast varmetilskudd i sonen, W. For eksempel kappetap fra en kjel.
|
||||
#[serde(default)]
|
||||
pub fast_effekt: f64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kommentar: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Kuldebroer {
|
||||
/// Normalisert kuldebroverdi, W/(m²K) av oppvarmet BRA. Utelatt: verdi etter byggeår.
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Varmetap mot grunnen etter NS-EN ISO 13370.
|
||||
//!
|
||||
//! Grunnen er ikke en flate mot uteluft: varmen går en lang vei gjennom jorda før den når
|
||||
//! overflaten, og motstanden vokser med dybden. Standarden uttrykker dette med en ekvivalent
|
||||
//! tykkelse, som er den jordtykkelsen som gir samme varmemotstand som konstruksjonen, og med
|
||||
//! en karakteristisk dimensjon som beskriver hvor kompakt gulvet er.
|
||||
//!
|
||||
//! Dybden `z` er avstanden fra terrenget ned til gulvet. For en kjellervegg gjelder
|
||||
//! ligningene bare den delen som står under terreng; delen over terreng er en helt vanlig
|
||||
//! yttervegg og regnes for seg.
|
||||
|
||||
/// Varmeledningsevne for jord, W/(m·K). NS-EN ISO 13370 tabell 1, «leire eller silt».
|
||||
pub const LEDNINGSEVNE_JORD: f64 = 2.0;
|
||||
/// Antatt tykkelse på ytterveggen over gulvet, m.
|
||||
const VEGGTYKKELSE: f64 = 0.3;
|
||||
|
||||
/// Karakteristisk dimensjon B′ = A / (0,5 · P).
|
||||
///
|
||||
/// Et stort, kompakt gulv har høy B′ og taper forholdsvis lite varme, fordi mesteparten av
|
||||
/// arealet ligger langt fra terrengoverflaten.
|
||||
pub fn karakteristisk_dimensjon(areal: f64, omkrets: f64) -> f64 {
|
||||
if omkrets <= 1e-6 {
|
||||
// Uten oppgitt omkrets antas et kvadratisk gulv.
|
||||
return areal.sqrt() / 2.0;
|
||||
}
|
||||
areal / (0.5 * omkrets)
|
||||
}
|
||||
|
||||
/// Ekvivalent tykkelse for gulvet, m: `d_t = w + λ / U`.
|
||||
fn ekvivalent_tykkelse_gulv(u_gulv: f64, ledningsevne: f64) -> f64 {
|
||||
VEGGTYKKELSE + ledningsevne / u_gulv.max(1e-6)
|
||||
}
|
||||
|
||||
/// Varmegjennomgangskoeffisient for gulv mot grunn, W/(m²K).
|
||||
///
|
||||
/// `u_gulv` er gulvkonstruksjonens egen U-verdi med overgangsmotstander, `dybde` er gulvets
|
||||
/// dybde under terreng (0 for plate på mark).
|
||||
pub fn u_gulv(
|
||||
u_gulv_konstruksjon: f64,
|
||||
areal: f64,
|
||||
omkrets: f64,
|
||||
dybde: f64,
|
||||
ledningsevne: f64,
|
||||
) -> f64 {
|
||||
let b = karakteristisk_dimensjon(areal, omkrets);
|
||||
if b <= 1e-6 {
|
||||
return u_gulv_konstruksjon;
|
||||
}
|
||||
let d_t = ekvivalent_tykkelse_gulv(u_gulv_konstruksjon, ledningsevne);
|
||||
let nevner = d_t + 0.5 * dybde.max(0.0);
|
||||
let u = if nevner < b {
|
||||
// Godt isolert eller lite gulv: logaritmisk uttrykk.
|
||||
2.0 * ledningsevne / (std::f64::consts::PI * b + nevner)
|
||||
* (std::f64::consts::PI * b / nevner + 1.0).ln()
|
||||
} else {
|
||||
// Tykk ekvivalent jordlag: varmestrømmen er nær endimensjonal.
|
||||
ledningsevne / (0.457 * b + nevner)
|
||||
};
|
||||
// Grunnen kan aldri gi lavere motstand enn konstruksjonen alene.
|
||||
u.min(u_gulv_konstruksjon)
|
||||
}
|
||||
|
||||
/// Varmegjennomgangskoeffisient for kjellervegg under terreng, W/(m²K).
|
||||
///
|
||||
/// `dybde` er veggens dybde under terreng. Ligningen gjelder hele den nedgravde delen;
|
||||
/// arealet over terreng regnes som vanlig yttervegg.
|
||||
pub fn u_kjellervegg(
|
||||
u_vegg_konstruksjon: f64,
|
||||
u_gulv_konstruksjon: f64,
|
||||
areal_gulv: f64,
|
||||
omkrets: f64,
|
||||
dybde: f64,
|
||||
ledningsevne: f64,
|
||||
) -> f64 {
|
||||
if dybde <= 0.05 {
|
||||
return u_vegg_konstruksjon;
|
||||
}
|
||||
let d_t = ekvivalent_tykkelse_gulv(u_gulv_konstruksjon, ledningsevne);
|
||||
let d_w = ledningsevne / u_vegg_konstruksjon.max(1e-6);
|
||||
let _ = (areal_gulv, omkrets);
|
||||
let u = 2.0 * ledningsevne / (std::f64::consts::PI * dybde)
|
||||
* (1.0 + 0.5 * d_t / (d_t + dybde))
|
||||
* (dybde / d_w + 1.0).ln();
|
||||
u.min(u_vegg_konstruksjon)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn kompakt_gulv_taper_mindre_enn_avlangt() {
|
||||
// Samme areal, men ulik omkrets: det kompakte gulvet har høyere B′.
|
||||
let kompakt = u_gulv(0.30, 100.0, 40.0, 0.0, LEDNINGSEVNE_JORD);
|
||||
let avlangt = u_gulv(0.30, 100.0, 70.0, 0.0, LEDNINGSEVNE_JORD);
|
||||
assert!(kompakt < avlangt, "kompakt {kompakt}, avlangt {avlangt}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grunnen_demper_et_daarlig_isolert_gulv_kraftig() {
|
||||
// Et uisolert kjellergulv med U = 3,85 får langt lavere effektiv U-verdi mot grunnen.
|
||||
let u = u_gulv(3.85, 79.0, 40.0, 1.8, LEDNINGSEVNE_JORD);
|
||||
assert!(u < 1.0, "effektiv U-verdi ble {u}");
|
||||
assert!(u > 0.1, "effektiv U-verdi ble urimelig lav: {u}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effektiv_u_verdi_overstiger_aldri_konstruksjonens() {
|
||||
for u_k in [0.1, 0.3, 1.0, 3.9] {
|
||||
let u = u_gulv(u_k, 80.0, 40.0, 1.5, LEDNINGSEVNE_JORD);
|
||||
assert!(u <= u_k + 1e-12, "U {u} > konstruksjon {u_k}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dypere_kjellervegg_taper_mindre_per_kvadratmeter() {
|
||||
let grunn = u_kjellervegg(3.13, 3.85, 79.0, 40.0, 0.8, LEDNINGSEVNE_JORD);
|
||||
let dyp = u_kjellervegg(3.13, 3.85, 79.0, 40.0, 2.4, LEDNINGSEVNE_JORD);
|
||||
assert!(dyp < grunn, "dyp {dyp}, grunn {grunn}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vegg_over_terreng_beholder_sin_egen_u_verdi() {
|
||||
assert_eq!(
|
||||
u_kjellervegg(3.13, 3.85, 79.0, 40.0, 0.0, LEDNINGSEVNE_JORD),
|
||||
3.13
|
||||
);
|
||||
}
|
||||
}
|
||||
+42
-4
@@ -51,6 +51,7 @@ struct Del {
|
||||
kapasitet: Vec<f64>,
|
||||
glass: bool,
|
||||
mot: Mot,
|
||||
sone: Option<usize>,
|
||||
b_faktor: f64,
|
||||
absorptans: f64,
|
||||
helning: f64,
|
||||
@@ -94,6 +95,8 @@ pub struct Timeserie {
|
||||
pub sol_horisontal: Vec<f64>,
|
||||
/// Luftmengde ved vinduslufting, m³/h.
|
||||
pub vinduslufting: Vec<f64>,
|
||||
/// Temperatur i hver uoppvarmede sone, °C.
|
||||
pub sonetemperatur: Vec<Vec<f64>>,
|
||||
}
|
||||
|
||||
impl Timeserie {
|
||||
@@ -116,6 +119,7 @@ impl Timeserie {
|
||||
internlast: n(),
|
||||
sol_horisontal: n(),
|
||||
vinduslufting: n(),
|
||||
sonetemperatur: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +144,15 @@ impl Timeserie {
|
||||
/// romoppvarmingsbehovet, samtidig som energien telles under tappevann. Distribusjonstap for
|
||||
/// romvarme i oppvarmet sone regnes ikke med her: den varmen ville uansett blitt avgitt i sonen,
|
||||
/// og påvirker derfor bare fordelingen mellom rør og varmegiver, ikke energibehovet.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Systemtilskudd {
|
||||
/// Konstant tilskudd fra tappevannsanlegget, W.
|
||||
/// Konstant tilskudd fra tappevannsanlegget i den oppvarmede sonen, W.
|
||||
pub tappevann: f64,
|
||||
/// Fast varmetilskudd i hver uoppvarmede sone, W.
|
||||
pub sone_fast: Vec<f64>,
|
||||
/// Varmetilskudd i hver uoppvarmede sone som andel av romoppvarmingseffekten.
|
||||
/// Dette er distribusjonstapet fra varmerør som går gjennom sonen.
|
||||
pub sone_andel_romvarme: Vec<f64>,
|
||||
}
|
||||
|
||||
pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Timeserie> {
|
||||
@@ -155,6 +164,13 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
|
||||
let bakketemp = bakketemperatur(v);
|
||||
|
||||
let mut ts = Timeserie::ny();
|
||||
ts.sonetemperatur = vec![vec![0.0; TIMER]; l.soner.len()];
|
||||
let mut t_soner: Vec<f64> = l
|
||||
.soner
|
||||
.iter()
|
||||
.map(|_| l.settpunkt_oppvarming - 5.0)
|
||||
.collect();
|
||||
let mut romvarme_forrige = 0.0;
|
||||
let mut tilstand = vec![l.settpunkt_oppvarming; n_noder];
|
||||
let mut t_a_forrige = l.settpunkt_oppvarming;
|
||||
|
||||
@@ -215,6 +231,18 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
|
||||
l.settpunkt_natt
|
||||
};
|
||||
|
||||
// ── uoppvarmede soner ───────────────────────────────────
|
||||
// Temperaturen følger av den stasjonære varmebalansen for sonen, med
|
||||
// varmetilskudd fra rør og bereder som står der. Romoppvarmingen fra forrige
|
||||
// time brukes, siden rørtapet følger varmeanleggets drift.
|
||||
let t_bakke_naa = bakketemp[crate::maaned_for_time(t)];
|
||||
for (i, sone) in l.soner.iter().enumerate() {
|
||||
let tilskudd = tilskudd.sone_fast.get(i).copied().unwrap_or(0.0)
|
||||
+ tilskudd.sone_andel_romvarme.get(i).copied().unwrap_or(0.0)
|
||||
* romvarme_forrige;
|
||||
t_soner[i] = sone.temperatur(t_a_forrige, t_ute, t_bakke_naa, tilskudd);
|
||||
}
|
||||
|
||||
// ── løsning ─────────────────────────────────────────────
|
||||
let prove = PROVEEFFEKT * l.bra;
|
||||
let ctrl = |s: &[f64]| -> f64 {
|
||||
@@ -238,8 +266,9 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
|
||||
},
|
||||
t_ute,
|
||||
t_himmel,
|
||||
t_bakke: bakketemp[crate::maaned_for_time(t)],
|
||||
t_bakke: t_bakke_naa,
|
||||
t_a_forrige,
|
||||
t_soner: &t_soner,
|
||||
internlast,
|
||||
sol_gjennom,
|
||||
time: t,
|
||||
@@ -301,6 +330,7 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
|
||||
tilstand[i] = los0[i] + andel * (los1[i] - los0[i]);
|
||||
}
|
||||
t_a_forrige = tilstand[0];
|
||||
romvarme_forrige = fi.max(0.0);
|
||||
|
||||
if skriv {
|
||||
ts.t_ute[t] = t_ute;
|
||||
@@ -318,6 +348,9 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
|
||||
ts.internlast[t] = internlast;
|
||||
ts.sol_horisontal[t] = v.ghi[t];
|
||||
ts.vinduslufting[t] = h_luft / RHO_C_LUFT;
|
||||
for (i, v) in t_soner.iter().enumerate() {
|
||||
ts.sonetemperatur[i][t] = *v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,6 +369,7 @@ struct Kontekst<'a> {
|
||||
t_himmel: f64,
|
||||
t_bakke: f64,
|
||||
t_a_forrige: f64,
|
||||
t_soner: &'a [f64],
|
||||
internlast: f64,
|
||||
sol_gjennom: f64,
|
||||
time: usize,
|
||||
@@ -414,7 +448,10 @@ impl Kontekst<'_> {
|
||||
fn ytre_temperatur(&self, d: &Del) -> f64 {
|
||||
match d.mot {
|
||||
Mot::Grunn => self.t_bakke,
|
||||
Mot::Uoppvarmet => self.t_a_forrige - d.b_faktor * (self.t_a_forrige - self.t_ute),
|
||||
Mot::Uoppvarmet => match d.sone.and_then(|i| self.t_soner.get(i)) {
|
||||
Some(t) => *t,
|
||||
None => self.t_a_forrige - d.b_faktor * (self.t_a_forrige - self.t_ute),
|
||||
},
|
||||
Mot::Ute => {
|
||||
// Himmelandel etter flatens helning; resten ser bakken med utelufttemperatur.
|
||||
let f_himmel = (1.0 + d.helning.to_radians().cos()) / 2.0;
|
||||
@@ -463,6 +500,7 @@ fn bygg_deler(l: &Lost, v: &Vaerdata) -> Vec<Del> {
|
||||
kapasitet,
|
||||
glass: f.glass,
|
||||
mot: f.mot,
|
||||
sone: f.sone,
|
||||
b_faktor: f.b_faktor,
|
||||
absorptans: f.absorptans,
|
||||
helning: f.helning,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Beregningsmotoren: varmetapsbudsjett, laster, ventilasjon og den dynamiske timesberegningen.
|
||||
|
||||
pub mod grunn;
|
||||
pub mod iso52016;
|
||||
pub mod lin;
|
||||
pub mod varmetap;
|
||||
|
||||
@@ -28,10 +28,12 @@ impl Varmetapsbudsjett {
|
||||
let mut b = Varmetapsbudsjett::default();
|
||||
let a = l.bra;
|
||||
for f in &l.flater {
|
||||
// Gulv mot grunn tas med sin U-verdi; den dempede bakketemperaturen håndteres
|
||||
// i timesberegningen, mens varmetapstallet oppgis som for flate mot det fri.
|
||||
// Varmetapstallet oppgis med konstruksjonens egen U-verdi, slik NS 3031 Tabell 3
|
||||
// og energikravene i TEK er formulert. Timesberegningen bruker i tillegg grunnens
|
||||
// dempende virkning etter NS-EN ISO 13370; de korrigerte verdiene står i
|
||||
// forutsetningene i rapporten.
|
||||
let bidrag = f.areal
|
||||
* f.u_verdi
|
||||
* f.u_konstruksjon
|
||||
* if f.mot == Mot::Uoppvarmet {
|
||||
f.b_faktor
|
||||
} else {
|
||||
|
||||
+37
-4
@@ -4,9 +4,23 @@ use crate::kjor::Resultat;
|
||||
use crate::Result;
|
||||
use std::path::Path;
|
||||
|
||||
/// Gjør et sonenavn om til en kolonneoverskrift uten mellomrom og særnorske tegn.
|
||||
fn filnavnvennlig(s: &str) -> String {
|
||||
s.chars()
|
||||
.map(|c| match c {
|
||||
'æ' | 'Æ' => 'a',
|
||||
'ø' | 'Ø' => 'o',
|
||||
'å' | 'Å' => 'a',
|
||||
c if c.is_alphanumeric() => c.to_ascii_lowercase(),
|
||||
_ => '_',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn timeserie(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
let mut w = csv::Writer::from_path(sti)?;
|
||||
w.write_record([
|
||||
w.write_record(
|
||||
[
|
||||
"time",
|
||||
"maaned",
|
||||
"t_ute_C",
|
||||
@@ -25,12 +39,23 @@ pub fn timeserie(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
"belysning_W",
|
||||
"utstyr_W",
|
||||
"solceller_W",
|
||||
])?;
|
||||
]
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.chain(
|
||||
r.lost
|
||||
.soner
|
||||
.iter()
|
||||
.map(|s| format!("t_{}_C", filnavnvennlig(&s.navn))),
|
||||
)
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
let sol = r.solceller.as_ref().map(|p| p.serie.as_slice());
|
||||
let ts = &r.timeserie;
|
||||
for t in 0..crate::TIMER {
|
||||
let f = |v: f64| format!("{v:.1}");
|
||||
w.write_record([
|
||||
w.write_record(
|
||||
[
|
||||
t.to_string(),
|
||||
crate::MAANEDER[crate::maaned_for_time(t)].to_string(),
|
||||
format!("{:.2}", ts.t_ute[t]),
|
||||
@@ -49,7 +74,15 @@ pub fn timeserie(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
f(ts.belysning[t]),
|
||||
f(ts.utstyr[t]),
|
||||
f(sol.map(|s| s[t]).unwrap_or(0.0)),
|
||||
])?;
|
||||
]
|
||||
.into_iter()
|
||||
.chain(
|
||||
ts.sonetemperatur
|
||||
.iter()
|
||||
.map(|serie| format!("{:.2}", serie[t])),
|
||||
)
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
}
|
||||
w.flush()?;
|
||||
Ok(())
|
||||
|
||||
@@ -387,6 +387,31 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
||||
])
|
||||
));
|
||||
|
||||
// ── uoppvarmede soner ───────────────────────────────────────────
|
||||
if !l.soner.is_empty() {
|
||||
s.push_str("== Uoppvarmede soner\n\n");
|
||||
s.push_str(
|
||||
"En uoppvarmet kjeller eller et kaldt loft har ingen egen oppvarming, men får varme fra rommene over gjennom etasjeskilleren og fra varmerør og beredere som står der. Temperaturen er regnet ut time for time av sonens varmebalanse etter NS-EN ISO 13789, slik at varme rør både teller som tap fra varmeanlegget og som varme som holder etasjeskilleren varm.\n\n",
|
||||
);
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto),\n justering: (left, right, right, right, right),\n overskrift: (\"Sone\", \"Middeltemperatur\", \"Laveste\", \"Mot oppvarmet\", \"Mot ute og grunn\"),\n rader: (\n");
|
||||
for (i, sone) in l.soner.iter().enumerate() {
|
||||
let serie = &r.timeserie.sonetemperatur[i];
|
||||
let middel = serie.iter().sum::<f64>() / serie.len() as f64;
|
||||
let laveste = serie.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(&sone.navn),
|
||||
format!("{} °C", tall(middel, 1)),
|
||||
format!("{} °C", tall(laveste, 1)),
|
||||
format!("{} W/K", tall(sone.h_mot_oppvarmet, 1)),
|
||||
format!("{} W/K", tall(sone.h_mot_ute + sone.h_mot_grunn, 1)),
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
}
|
||||
|
||||
// ── inndata ─────────────────────────────────────────────────────
|
||||
s.push_str("#pagebreak()\n\n= Inndata\n\n== Bygningskropp\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto, auto),\n justering: (left, left, right, right, right, left),\n overskrift: (\"Bygningsdel\", \"Type\", \"Areal, m²\", \"U-verdi\", \"Orientering\", \"Mot\"),\n rader: (\n");
|
||||
|
||||
+3
-3
@@ -308,10 +308,10 @@ fn baerer_for(t: VarmekildeType, lukket: bool) -> Energibaerer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Uregulerbart varmetilskudd fra tappevannsanlegget, W.
|
||||
pub fn tappevannstilskudd(l: &Lost, s: &Systemoppsett) -> f64 {
|
||||
/// Samlet tap fra tappevannsanlegget, W. Tapet varmer opp der anlegget står.
|
||||
pub fn tappevannstilskudd_totalt(l: &Lost, s: &Systemoppsett) -> f64 {
|
||||
let netto_w: f64 = l.laster.varmtvann.iter().sum::<f64>() / 24.0 * l.bra;
|
||||
netto_w * s.tappevann_tap_andel * s.tappevann_tap_i_sone
|
||||
netto_w * s.tappevann_tap_andel
|
||||
}
|
||||
|
||||
/// Bygger energibudsjettet (punkt A–C) av timeseriene.
|
||||
|
||||
+149
@@ -172,3 +172,152 @@ fn varmebehovet_er_lineaert_i_temperaturdifferansen() {
|
||||
"forholdet skulle vært 2,0, men ble {forhold:.3}"
|
||||
);
|
||||
}
|
||||
|
||||
const MED_KJELLER: &str = r#"
|
||||
[bygning]
|
||||
navn = "Hus med kjeller"
|
||||
kategori = "smaahus"
|
||||
byggeaar = 1950
|
||||
oppvarmet_bra = 100.0
|
||||
oppvarmet_volum = 240.0
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg"
|
||||
type = "vegg"
|
||||
areal = 120.0
|
||||
u_verdi = 1.0
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Tak"
|
||||
type = "tak"
|
||||
areal = 100.0
|
||||
u_verdi = 0.5
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Etasjeskiller mot kjeller"
|
||||
type = "gulv"
|
||||
areal = 100.0
|
||||
u_verdi = 0.8
|
||||
mot = "uoppvarmet"
|
||||
sone = "Kjeller"
|
||||
masseklasse = "middels"
|
||||
|
||||
[[sone]]
|
||||
navn = "Kjeller"
|
||||
volum = 220.0
|
||||
luftveksling = 0.4
|
||||
andel_distribusjonstap = 1.0
|
||||
fast_effekt = 0.0
|
||||
|
||||
[[sone.flate]]
|
||||
navn = "Kjellermur"
|
||||
type = "vegg"
|
||||
areal = 90.0
|
||||
u_verdi = 3.0
|
||||
mot = "grunn"
|
||||
dybde = 1.6
|
||||
over_terreng = 0.6
|
||||
|
||||
[[sone.flate]]
|
||||
navn = "Kjellergulv"
|
||||
type = "gulv"
|
||||
areal = 100.0
|
||||
u_verdi = 3.5
|
||||
mot = "grunn"
|
||||
omkrets = 40.0
|
||||
dybde = 2.2
|
||||
|
||||
[kuldebroer]
|
||||
normalisert = 0.0
|
||||
|
||||
[infiltrasjon]
|
||||
n50 = 4.0
|
||||
|
||||
[ventilasjon]
|
||||
type = "naturlig"
|
||||
luftmengde = 1.2
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "el_kjel"
|
||||
andel_rom = 1.0
|
||||
andel_tappevann = 1.0
|
||||
|
||||
[distribusjon]
|
||||
type = "radiator"
|
||||
roer_uoppvarmet = 1.0
|
||||
"#;
|
||||
|
||||
fn med_kjeller() -> (Lost, Vaerdata) {
|
||||
let b: Bygning = toml::from_str(MED_KJELLER).unwrap();
|
||||
let l = Lost::fra(&b, &Datakilde::default()).unwrap();
|
||||
(l, stille_vaer(-5.0))
|
||||
}
|
||||
|
||||
/// Kjellertemperaturen skal ligge mellom ute og inne, og beregnes for hver time.
|
||||
#[test]
|
||||
fn kjelleren_ligger_mellom_ute_og_inne() {
|
||||
let (l, v) = med_kjeller();
|
||||
assert_eq!(l.soner.len(), 1);
|
||||
let ts = simuler(&l, &v, Systemtilskudd::default()).unwrap();
|
||||
let t_kjeller = &ts.sonetemperatur[0];
|
||||
let snitt = t_kjeller.iter().sum::<f64>() / t_kjeller.len() as f64;
|
||||
assert!(
|
||||
snitt > -5.0 && snitt < 21.0,
|
||||
"kjellertemperaturen ble {snitt:.1} °C"
|
||||
);
|
||||
// Uten varmetilskudd holder etasjeskilleren kjelleren godt over utetemperaturen.
|
||||
assert!(snitt > 0.0, "kjellertemperaturen ble {snitt:.1} °C");
|
||||
}
|
||||
|
||||
/// Varme fra uisolerte rør i kjelleren skal heve kjellertemperaturen og redusere
|
||||
/// varmetapet gjennom etasjeskilleren, slik termografirapporter på slike hus beskriver.
|
||||
#[test]
|
||||
fn varme_ror_i_kjelleren_reduserer_romoppvarmingen() {
|
||||
let (l, v) = med_kjeller();
|
||||
let uten = simuler(&l, &v, Systemtilskudd::default()).unwrap();
|
||||
let med = simuler(
|
||||
&l,
|
||||
&v,
|
||||
Systemtilskudd {
|
||||
tappevann: 0.0,
|
||||
sone_fast: vec![1500.0],
|
||||
sone_andel_romvarme: vec![0.0],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let t_uten = uten.sonetemperatur[0].iter().sum::<f64>() / TIMER as f64;
|
||||
let t_med = med.sonetemperatur[0].iter().sum::<f64>() / TIMER as f64;
|
||||
assert!(
|
||||
t_med > t_uten + 2.0,
|
||||
"kjelleren ble bare {t_med:.1} mot {t_uten:.1} °C"
|
||||
);
|
||||
|
||||
let varme_uten = Timeserie::sum_kwh(&uten.romoppvarming);
|
||||
let varme_med = Timeserie::sum_kwh(&med.romoppvarming);
|
||||
assert!(
|
||||
varme_med < varme_uten,
|
||||
"romoppvarmingen økte: {varme_med:.0} mot {varme_uten:.0} kWh"
|
||||
);
|
||||
}
|
||||
|
||||
/// Grunnen demper varmetapet fra kjelleren: en uisolert kjellermur under terreng skal
|
||||
/// gi lavere varmetransport enn den samme muren over terreng.
|
||||
#[test]
|
||||
fn nedgravd_mur_taper_mindre_enn_mur_over_terreng() {
|
||||
let (l, _) = med_kjeller();
|
||||
let sone = &l.soner[0];
|
||||
// Muren er 90 m² med U = 3,0. Over terreng ville hele gitt 270 W/K.
|
||||
assert!(
|
||||
sone.h_mot_grunn < 150.0,
|
||||
"grunnen dempet ikke nok: {} W/K",
|
||||
sone.h_mot_grunn
|
||||
);
|
||||
// Den delen som står over terreng skal fortsatt telle fullt.
|
||||
assert!(
|
||||
sone.h_mot_ute > 40.0,
|
||||
"delen over terreng mangler: {} W/K",
|
||||
sone.h_mot_ute
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user